diff --git a/.github/workflows/python-release.yml b/.github/workflows/python-release.yml new file mode 100644 index 0000000..d7a2803 --- /dev/null +++ b/.github/workflows/python-release.yml @@ -0,0 +1,169 @@ +name: Python release + +# Publishes the Python bindings to PyPI: a source distribution, and a +# wheel for Linux, macOS and Windows. +# +# This is a file of its own rather than jobs in python.yml, and that is +# the point. python.yml runs on every push and every pull request; this +# one has a single trigger, `push: tags`, with no `branches` key and no +# `pull_request` key at all. A branch push cannot reach a macOS runner +# from here -- not because a condition forbids it and could be edited +# away, but because no event is wired to it. macOS bills at ten times the +# ubuntu rate and Windows at twice, so once per release is a fair price +# and once per pull request is not. +# +# The `if:` on each job is redundant with that trigger, deliberately: it +# is there so that anyone who later adds a second event to this file has +# to walk past it before these runners start answering to it. +# +# ── How the upload is authorised ───────────────────────────────────── +# +# PyPI Trusted Publishing, so there is no API token in this repository's +# secrets and nothing to leak or rotate. The publisher registered on PyPI +# for `numeria` names three things, and all three have to keep matching or +# the upload is refused: +# +# repository Magic-Man-us/RustPhysicsEngine +# workflow python-release.yml <- this file's name +# environment pypi <- the publish job's environment +# +# So renaming this file, moving the publish step into another one, or +# changing that environment name breaks the release until PyPI is told. +# +# The environment is worth having for its own sake as well: a required +# reviewer on it, set in the repository's settings, makes every publish +# wait for a human. A version can never be re-uploaded to PyPI, even after +# deleting it, so the one irreversible step in this file is the one worth +# putting a person in front of. +# +# Every `run:` is quoted, for the reason given in verify.yml: an unquoted +# value containing a colon followed by a space parses as a nested mapping +# and invalidates the file. + +on: + push: + tags: ["v*"] + +# Nothing here needs write access to the repository. The publish job adds +# the one permission it does need, and nothing else inherits it. +permissions: {} + +env: + CARGO_TERM_COLOR: always + +jobs: + # Cheap, and first: if the tag and Cargo.toml disagree, say so before + # spending three platforms' worth of runner minutes finding out. + version: + name: "Version matches the tag" + if: "startsWith(github.ref, 'refs/tags/v')" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: The tag, the crate and the bindings agree + run: "python3 bindings/python/check_version.py '${{ github.ref }}'" + + sdist: + name: "Source distribution" + if: "startsWith(github.ref, 'refs/tags/v')" + needs: version + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + # The bindings depend on the library by path, so the archive has to + # carry the library too. maturin vendors it: the sdist contains the + # whole crate, and `pip install` of it builds against that copy. + # This is what makes the package installable on a platform none of + # the wheels below covers. + - uses: PyO3/maturin-action@v1 + with: + command: sdist + args: "--out dist --manifest-path bindings/python/Cargo.toml" + - uses: actions/upload-artifact@v4 + with: + name: dist-sdist + path: dist + + linux: + name: "Wheel (Linux x86_64)" + if: "startsWith(github.ref, 'refs/tags/v')" + needs: version + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + # `manylinux: auto` builds inside a manylinux container, so the + # wheel is not pinned to whatever glibc this runner happens to + # carry. Plain `maturin build` here produces a manylinux_2_35 tag + # that will not install on an older distribution. + - uses: PyO3/maturin-action@v1 + with: + target: x86_64 + manylinux: auto + args: "--release --out dist --manifest-path bindings/python/Cargo.toml" + - uses: actions/upload-artifact@v4 + with: + name: dist-linux-x86_64 + path: dist + + macos: + name: "Wheel (macOS universal2)" + if: "startsWith(github.ref, 'refs/tags/v')" + needs: version + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + # One universal2 wheel covers both Apple Silicon and Intel, so this + # is a single mac runner rather than two. At ten times the ubuntu + # rate that halving is the whole reason for the target choice. + - uses: PyO3/maturin-action@v1 + with: + target: universal2-apple-darwin + args: "--release --out dist --manifest-path bindings/python/Cargo.toml" + - uses: actions/upload-artifact@v4 + with: + name: dist-macos-universal2 + path: dist + + windows: + name: "Wheel (Windows x64)" + if: "startsWith(github.ref, 'refs/tags/v')" + needs: version + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - uses: PyO3/maturin-action@v1 + with: + target: x64 + args: "--release --out dist --manifest-path bindings/python/Cargo.toml" + - uses: actions/upload-artifact@v4 + with: + name: dist-windows-x64 + path: dist + + publish: + name: "Publish to PyPI" + if: "startsWith(github.ref, 'refs/tags/v')" + needs: [sdist, linux, macos, windows] + runs-on: ubuntu-latest + # Named so PyPI can be told to trust exactly this job, and so that a + # required reviewer on the environment gates the one step here that + # cannot be undone. + environment: + name: pypi + url: "https://pypi.org/p/numeria" + permissions: + # The OIDC token Trusted Publishing exchanges for an upload. This is + # the only elevated permission in the file, and it is scoped to this + # job. + id-token: write + steps: + - uses: actions/download-artifact@v4 + with: + pattern: dist-* + path: dist + merge-multiple: true + - name: What is about to be published + run: "ls -l dist" + - uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: dist diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml new file mode 100644 index 0000000..dc2410b --- /dev/null +++ b/.github/workflows/python.yml @@ -0,0 +1,77 @@ +name: Python + +# The Python bindings under bindings/python. They are a separate Cargo +# workspace -- the library's own Cargo.lock stays a single package -- so +# they need a job of their own; ci.yml never builds them. +# +# One job, on ubuntu-latest. There is deliberately no wheel-building +# matrix: macOS runners bill at ten times the ubuntu rate and Windows at +# twice, and building the same crate on three platforms establishes +# nothing this job has not. When there is a release to cut, build the +# wheels then -- locally, or in a workflow added for that purpose. +# +# Every `run:` is quoted, for the reason given in verify.yml: an unquoted +# value containing a colon followed by a space parses as a nested mapping +# and invalidates the file. + +on: + push: + branches: [main] + pull_request: + branches: [main] + +env: + CARGO_TERM_COLOR: always + +jobs: + bindings: + name: "Bindings" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + + - uses: Swatinem/rust-cache@v2 + with: + workspaces: "bindings/python" + + # 3.9 is the floor the abi3 wheel targets, and abi3 means one binary + # serves every version above it -- so a matrix here would compile + # the same crate twice to learn nothing. + - uses: actions/setup-python@v5 + with: + python-version: "3.9" + + - name: Install the build and test tools + run: "python -m pip install --upgrade pip maturin pytest" + + # The bindings are generated from the library's source. If a commit + # changes the library and does not re-run the generator, what is + # committed here describes a library that no longer exists -- and + # nothing else would notice, because stale bindings still compile. + # This is the check that notices. + - name: The committed bindings match the source + run: "python3 bindings/python/generate.py --check" + + # Checked here as well as at release time, because a mismatch + # introduced now is cheapest to fix now. At release time the same + # check is the last thing standing between a wrong version number + # and a PyPI upload that cannot be taken back. + - name: The crate and the bindings claim one version + run: "python3 bindings/python/check_version.py" + + # `pip install` rather than `maturin develop`: develop wants a + # virtualenv to install into, and setup-python does not make one. + - name: Build and install + run: "python -m pip install --no-build-isolation ./bindings/python" + + - name: Test + run: "python -m pytest bindings/python/tests -q" + + # The stubs are generated too, and a stub that disagrees with the + # module it describes is worse than no stub: it type-checks code + # that will fail at run time. + - name: Stubs describe the module that was built + run: "python3 bindings/python/check_stubs.py" + diff --git a/.gitignore b/.gitignore index 6b3e392..dd35642 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ /target *.profraw +/bindings/python/target +/bindings/python/**/__pycache__ +# Where `maturin develop` drops the built extension. +/bindings/python/python/rust_physics_engine/*.so diff --git a/README.md b/README.md index ef7d1b4..de5f6ab 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,39 @@ assert_eq!(one, Rational::one()); // not 0.9999999999999999 --- +## From Python + +Published to PyPI as **numeria**: 4,086 of this crate's 4,149 free +functions, 2,254 of its 2,277 methods, 416 of its 426 types and every +constant. The bindings live in [`bindings/python`](bindings/python) and +are generated from this crate's source, so they cannot fall behind it. + +```console +$ pip install numeria +``` + +```python +import math +import numeria as nm + +nm.classical.projectile_range(50.0, math.pi / 4, 9.81) # 254.841997961... +nm.linalg.lu.solve([[2, 1], [1, 3]], [5, 10]) # [1.0, 3.0] +nm.numerical.integrate.simpson(math.sin, 0.0, math.pi, 1000) +nm.exact.bigint.factorial(100) # a Python int +``` + +Every Python module mirrors a Rust module of the same name beneath +`numeria`. `Result` errors +become exceptions under one `PhysicsError` root; a `Vec3` argument accepts +`(x, y, z)`; `Complex`, `BigInt` and `Rational` cross over as Python's own +`complex`, `int` and `fractions.Fraction`; and anywhere the library takes a +`&dyn Fn`, a Python callable will do. See +[`bindings/python/README.md`](bindings/python/README.md), and +[`bindings/python/COVERAGE.md`](bindings/python/COVERAGE.md) for the list of +what is not bound and why. + +--- + # What's in it Equations below are the ones the code actually implements, not a diff --git a/bindings/python/COVERAGE.md b/bindings/python/COVERAGE.md new file mode 100644 index 0000000..e3fbea4 --- /dev/null +++ b/bindings/python/COVERAGE.md @@ -0,0 +1,419 @@ + + +# What is bound, and what is not + +Generated alongside the bindings themselves, so it cannot drift +from them. Every item the generator could not bind is listed at +the bottom with the reason. + +## Totals + +| | in Rust | bound | +|---|---:|---:| +| Free functions | 4149 | 4086 | +| Methods | 2277 | 2254 | +| Classes | 426 | 416 | +| Constants | 106 | 106 | + +Of those methods, 70 belong to `Complex`, `BigInt` and +`Rational`. Those three have no wrapper class -- they cross over as +Python's own `complex`, `int` and `Fraction` -- so their methods appear +as functions in the module that defines the type: +`exact.bigint.mod_pow(base, exponent, modulus)` rather than +`BigInt.mod_pow`. + +The tree comes to 296 Python modules. + +## Why the rest is not bound + +| reason | count | +|---|---:| +| argument `...` | 66 | +| return type `...` | 11 | +| re-export shadowed by the submodule `...`; reach it at `...` | 9 | +| generic () | 3 | +| generic type | 2 | +| generic () | 1 | +| generic () | 1 | +| generic () | 1 | +| generic () | 1 | +| mutates an immutable Python type | 1 | +| generic ( f64>) | 1 | + +## Per module + +| module | functions bound | classes | +|---|---:|---:| +| `acoustics` | 31 | 0 | +| `atmosphere` | 17 | 0 | +| `biophysics` | 19 | 0 | +| `chemistry` | 18 | 0 | +| `classical` | 54 | 0 | +| `color_science` | 14 | 0 | +| `continuum_mechanics` | 19 | 0 | +| `control_systems` | 18 | 1 | +| `curves` | 25 | 0 | +| `electromagnetism` | 60 | 0 | +| `electronics` | 17 | 0 | +| `fields` | 0 | 2 | +| `fluid_instabilities` | 19 | 0 | +| `fluids` | 45 | 0 | +| `fractals` | 15 | 0 | +| `general_relativity` | 23 | 0 | +| `geometry` | 30 | 0 | +| `geophysics` | 22 | 0 | +| `gravitation` | 14 | 0 | +| `information_theory` | 16 | 0 | +| `linalg` | 10 | 2 | +| `magnetohydrodynamics` | 19 | 0 | +| `math` | 0 | 2 | +| `mesh` | 0 | 1 | +| `monte_carlo` | 13 | 1 | +| `neutronics` | 22 | 0 | +| `nonlinear` | 13 | 0 | +| `nuclear` | 20 | 0 | +| `optics` | 20 | 0 | +| `optimization` | 11 | 0 | +| `particle_physics` | 20 | 0 | +| `photonics` | 24 | 0 | +| `plasma` | 16 | 0 | +| `propulsion` | 15 | 0 | +| `quantum` | 27 | 0 | +| `quaternion` | 2 | 1 | +| `radiation` | 14 | 0 | +| `relativity` | 18 | 0 | +| `rf` | 28 | 0 | +| `signal_processing` | 16 | 0 | +| `solid_mechanics` | 23 | 0 | +| `statistical_mechanics` | 21 | 0 | +| `statistics` | 2 | 0 | +| `thermodynamics` | 52 | 0 | +| `trigonometry` | 33 | 0 | +| `units` | 80 | 0 | +| `vector_calculus` | 14 | 0 | +| `waves` | 48 | 0 | +| `astrophysics::collisions` | 8 | 3 | +| `astrophysics::coords` | 11 | 2 | +| `astrophysics::gravitational_waves` | 8 | 0 | +| `astrophysics::habitable_zone` | 6 | 0 | +| `astrophysics::kepler` | 9 | 0 | +| `astrophysics::lagrange` | 4 | 0 | +| `astrophysics::lambert` | 4 | 0 | +| `astrophysics::magnetosphere` | 6 | 1 | +| `astrophysics::maneuvers` | 8 | 0 | +| `astrophysics::nbody` | 8 | 2 | +| `astrophysics::orbital_elements` | 15 | 1 | +| `astrophysics::tidal` | 7 | 0 | +| `astrophysics::time_systems` | 5 | 0 | +| `audio::analysis` | 59 | 2 | +| `audio::effects` | 20 | 20 | +| `audio::envelope` | 9 | 5 | +| `audio::oscillators` | 16 | 5 | +| `audio::physical` | 11 | 8 | +| `audio::spatial` | 28 | 0 | +| `audio::synthesis` | 26 | 3 | +| `audio::tuning` | 25 | 2 | +| `audio::vocoder` | 8 | 2 | +| `audio::wav` | 8 | 1 | +| `biophysics::epidemiology` | 22 | 1 | +| `biophysics::neuro` | 35 | 1 | +| `biophysics::phylo` | 9 | 2 | +| `biophysics::population` | 36 | 1 | +| `biophysics::seq_align` | 30 | 2 | +| `cfd::advection` | 17 | 2 | +| `cfd::boundary_layer` | 31 | 0 | +| `cfd::grid` | 0 | 4 | +| `cfd::lbm` | 8 | 4 | +| `cfd::level_set` | 12 | 6 | +| `cfd::multiphase` | 34 | 2 | +| `cfd::porous` | 24 | 1 | +| `cfd::potential_flow` | 22 | 5 | +| `cfd::riemann` | 26 | 6 | +| `cfd::shallow_water` | 14 | 1 | +| `cfd::sph` | 9 | 7 | +| `cfd::stable_fluids` | 8 | 3 | +| `cfd::turbulence` | 33 | 4 | +| `cfd::vortex` | 17 | 4 | +| `codes::block` | 11 | 2 | +| `codes::checksum` | 21 | 0 | +| `codes::compression` | 28 | 2 | +| `codes::convolutional` | 15 | 3 | +| `codes::crypto_math` | 24 | 2 | +| `codes::reed_solomon` | 4 | 5 | +| `control_systems::kalman` | 0 | 2 | +| `core::compensated` | 3 | 0 | +| `core::dual` | 1 | 1 | +| `core::interval` | 1 | 1 | +| `discrete::combinatorics` | 58 | 0 | +| `discrete::disjoint_set` | 0 | 1 | +| `discrete::number_theory` | 49 | 0 | +| `discrete::partitions` | 15 | 0 | +| `discrete::primes` | 26 | 0 | +| `discrete::sequences` | 22 | 0 | +| `dsp::fir` | 18 | 1 | +| `dsp::iir` | 22 | 4 | +| `dsp::phase` | 8 | 0 | +| `dsp::resample` | 10 | 0 | +| `dsp::windows` | 7 | 2 | +| `exact::bigfloat` | 5 | 1 | +| `exact::bigint` | 39 | 0 | +| `exact::contfrac` | 10 | 0 | +| `exact::polynomial` | 5 | 2 | +| `exact::rational` | 34 | 0 | +| `exact::symbolic` | 3 | 3 | +| `fem::fdtd` | 7 | 3 | +| `fem::fem1d` | 7 | 2 | +| `fem::fem2d` | 16 | 1 | +| `fem::spectral_pde` | 7 | 0 | +| `finance::options` | 17 | 3 | +| `finance::portfolio` | 16 | 0 | +| `finance::rates` | 19 | 2 | +| `finance::risk` | 7 | 1 | +| `fractals::attractors` | 7 | 3 | +| `fractals::automata` | 17 | 16 | +| `fractals::escape_time` | 21 | 3 | +| `fractals::ifs` | 2 | 3 | +| `fractals::lsystem` | 2 | 4 | +| `fractals::noise` | 19 | 8 | +| `geometry::delaunay` | 3 | 0 | +| `geometry::geodesy` | 5 | 1 | +| `geometry::hull` | 4 | 0 | +| `geometry::mesh` | 0 | 2 | +| `graph::coloring` | 19 | 1 | +| `graph::core` | 24 | 1 | +| `graph::flow` | 14 | 0 | +| `graph::layout` | 15 | 0 | +| `graph::matching` | 10 | 0 | +| `graph::paths` | 26 | 0 | +| `graph::spectral` | 32 | 0 | +| `learn::cluster` | 13 | 3 | +| `learn::gp` | 1 | 2 | +| `learn::nn` | 2 | 4 | +| `learn::tree` | 11 | 4 | +| `linalg::cholesky` | 2 | 0 | +| `linalg::eigen` | 2 | 1 | +| `linalg::lu` | 2 | 1 | +| `linalg::matrix` | 0 | 1 | +| `linalg::qr` | 2 | 1 | +| `linalg::sparse` | 2 | 1 | +| `linalg::svd` | 4 | 1 | +| `linalg::tridiagonal` | 2 | 0 | +| `manifold::clifford` | 4 | 1 | +| `manifold::dec` | 3 | 1 | +| `manifold::embedding` | 39 | 0 | +| `manifold::geodesic` | 8 | 2 | +| `manifold::hyperbolic` | 43 | 2 | +| `manifold::lie` | 16 | 14 | +| `manifold::metric` | 1 | 2 | +| `manifold::polytope4` | 27 | 2 | +| `manifold::spacetime` | 28 | 5 | +| `manifold::spherical` | 67 | 0 | +| `manifold::vecn` | 2 | 2 | +| `materials::common` | 2 | 1 | +| `materials::elements` | 5 | 3 | +| `materials::fluids` | 2 | 1 | +| `materials::gases` | 2 | 1 | +| `mesh::analyze` | 20 | 1 | +| `mesh::generate` | 14 | 0 | +| `mesh::isosurface` | 8 | 2 | +| `mesh::parameterize` | 7 | 1 | +| `mesh::subdivide` | 9 | 1 | +| `mesh::surfaces` | 19 | 4 | +| `monte_carlo::quasi` | 1 | 2 | +| `numerical::bvp` | 2 | 0 | +| `numerical::integrate` | 8 | 1 | +| `numerical::interpolate` | 6 | 2 | +| `numerical::roots` | 7 | 0 | +| `optimization::convex` | 31 | 0 | +| `optimization::game_theory` | 36 | 9 | +| `optimization::integer` | 32 | 1 | +| `optimization::least_squares` | 2 | 1 | +| `optimization::lp` | 13 | 3 | +| `optimization::metaheuristics` | 12 | 2 | +| `optimization::network` | 17 | 1 | +| `patterns::aperiodic` | 13 | 2 | +| `patterns::knots` | 20 | 0 | +| `patterns::packing` | 17 | 0 | +| `patterns::phyllotaxis` | 18 | 0 | +| `patterns::polygon_ops` | 29 | 1 | +| `patterns::polyhedra` | 32 | 2 | +| `patterns::sampling` | 27 | 0 | +| `patterns::space_filling` | 20 | 0 | +| `patterns::symmetry` | 14 | 4 | +| `patterns::tilings` | 10 | 3 | +| `quantum::algorithms` | 22 | 0 | +| `quantum::circuit` | 15 | 5 | +| `quantum::schrodinger` | 25 | 1 | +| `quantum::solid_state` | 38 | 0 | +| `quantum::spin` | 18 | 1 | +| `quantum::wavefunction` | 13 | 1 | +| `resonance::cavity` | 32 | 3 | +| `resonance::coupled` | 6 | 1 | +| `resonance::nonlinear` | 22 | 0 | +| `resonance::oscillator` | 9 | 2 | +| `resonance::structural` | 6 | 1 | +| `sim::cloth_sim` | 2 | 3 | +| `sim::em_sim` | 0 | 2 | +| `sim::fluid_sim` | 0 | 3 | +| `sim::heat_sim` | 0 | 3 | +| `sim::rigid_body` | 2 | 2 | +| `sim::wave_sim` | 0 | 2 | +| `spatial::bvh` | 0 | 1 | +| `spatial::contain` | 20 | 0 | +| `spatial::distance` | 18 | 0 | +| `spatial::frame` | 0 | 1 | +| `spatial::intersect` | 23 | 1 | +| `spatial::kdtree` | 0 | 3 | +| `spatial::mat4` | 0 | 1 | +| `spatial::octree` | 1 | 1 | +| `spatial::primitives` | 0 | 15 | +| `spatial::projective` | 8 | 1 | +| `spatial::sdf` | 37 | 0 | +| `spatial::transform2d` | 0 | 1 | +| `special::bessel` | 12 | 0 | +| `special::beta` | 2 | 0 | +| `special::elliptic` | 7 | 0 | +| `special::erf` | 3 | 0 | +| `special::expint` | 2 | 0 | +| `special::gamma` | 4 | 0 | +| `special::legendre` | 4 | 0 | +| `statistical_mechanics::ising` | 13 | 4 | +| `statistical_mechanics::kinetics` | 36 | 2 | +| `statistical_mechanics::lattice_models` | 17 | 0 | +| `statistical_mechanics::md` | 12 | 3 | +| `statistics::descriptive` | 12 | 0 | +| `statistics::distributions` | 7 | 11 | +| `statistics::fourier` | 4 | 0 | +| `statistics::inference` | 10 | 1 | +| `statistics::resampling` | 4 | 1 | +| `stochastic::extreme` | 27 | 1 | +| `stochastic::hmm` | 4 | 4 | +| `stochastic::markov` | 0 | 3 | +| `stochastic::point_process` | 21 | 0 | +| `stochastic::queueing` | 15 | 4 | +| `stochastic::rmt` | 18 | 0 | +| `stochastic::sde` | 31 | 1 | +| `stochastic::timeseries` | 28 | 6 | +| `transforms::dct` | 12 | 1 | +| `transforms::fft` | 21 | 1 | +| `transforms::hilbert` | 13 | 0 | +| `transforms::laplace` | 8 | 0 | +| `transforms::radon` | 9 | 1 | +| `transforms::spectral` | 18 | 0 | +| `transforms::stft` | 10 | 1 | +| `transforms::wavelet` | 16 | 4 | +| `units::dimensional` | 7 | 0 | +| `units::quantity` | 6 | 2 | +| `fractals::attractors::presets` | 35 | 0 | +| `fractals::automata::patterns` | 11 | 0 | +| `fractals::ifs::presets` | 16 | 0 | +| `fractals::lsystem::presets` | 23 | 0 | +| `manifold::clifford::cga3` | 37 | 1 | +| `manifold::clifford::cl3` | 11 | 0 | +| `manifold::clifford::pga3` | 28 | 0 | +| `manifold::clifford::sta` | 12 | 0 | +| `numerical::ode::adaptive` | 2 | 1 | +| `numerical::ode::explicit` | 4 | 0 | +| `numerical::ode::symplectic` | 3 | 0 | + +## Every unbound item + +| module | item | reason | +|---|---|---| +| `audio::analysis` | `key_estimate()` | argument `chroma_track: &[[f64; 12]]` | +| `audio::effects` | `bitcrush()` | argument `counter: &mut usize` | +| `audio::physical` | `sympathetic_resonance()` | argument `strings: &mut [WaveguideString]` | +| `audio::synthesis` | `FmOperator.new()` | argument `env: Adsr` | +| `audio::synthesis` | `FmSynth.new()` | argument `ops: Vec` | +| `audio::synthesis` | `render_note()` | argument `synth: &mut dyn Synth` | +| `audio::synthesis` | `render_sequence()` | argument `synth: &mut dyn Synth` | +| `audio::synthesis` | `vector_synth()` | argument `sources: [&[f64]; 4]` | +| `cfd` | `vortex` | re-export shadowed by the submodule `cfd::vortex`; reach it at `cfd.potential_flow.vortex` | +| `cfd::grid` | `CellField2.at_mut()` | return type `&mut f64` | +| `cfd::multiphase` | `particle_tracking_step()` | argument `p: &mut (Vec3, Vec3)` | +| `cfd::potential_flow` | `doublet()` | return type `impl Fn(Vec2) -> (Vec2, f64, f64)` | +| `cfd::potential_flow` | `sink()` | return type `impl Fn(Vec2) -> (Vec2, f64, f64)` | +| `cfd::potential_flow` | `source()` | return type `impl Fn(Vec2) -> (Vec2, f64, f64)` | +| `cfd::potential_flow` | `uniform_flow()` | return type `impl Fn(Vec2) -> (Vec2, f64, f64)` | +| `cfd::potential_flow` | `vortex()` | return type `impl Fn(Vec2) -> (Vec2, f64, f64)` | +| `cfd::turbulence` | `rans_step()` | argument `model: &mut dyn RansModel` | +| `codes::block` | `LinearCode.syndrome_table_small()` | return type `BTreeMap, Vec>` | +| `codes::compression` | `BitReader` | generic type | +| `codes::convolutional` | `apply_permutation()` | generic () | +| `codes::convolutional` | `invert_permutation()` | generic () | +| `core::dual` | `gradient()` | argument `f: impl Fn(&[Dual]) -> Dual` | +| `core::dual` | `jacobian()` | argument `f: impl Fn(&[Dual]) -> Vec` | +| `discrete::combinatorics` | `inclusion_exclusion()` | argument `sizes: &dyn Fn(&[usize]) -> BigInt` | +| `exact::bigint` | `BigInt.cmp_abs()` | return type `Ordering` | +| `exact::bigint` | `BigInt.set_bit()` | mutates an immutable Python type | +| `exact::polynomial` | `Poly.chebyshev_fit()` | generic ( f64>) | +| `fractals::attractors` | `Attractor2Map.bifurcation_diagram()` | argument `f: &dyn Fn(f64) -> Attractor2Map` | +| `fractals::automata` | `totalistic_rule()` | return type `impl Fn(&[u8]) -> u8` | +| `fractals::escape_time` | `color_smooth_iter()` | argument `palette: &dyn Fn(f64) -> [f64; 3]` | +| `fractals::escape_time` | `render_grid()` | argument `f: &dyn Fn(Complex) -> EscapeResult` | +| `fractals::escape_time` | `render_grid_supersampled()` | argument `f: &dyn Fn(Complex) -> EscapeResult` | +| `fractals::lsystem` | `LSystem.generate()` | argument `rng: Option<&mut Rng>` | +| `graph::coloring` | `is_k_colorable_sat_style()` | argument `time_limit: Duration` | +| `linalg` | `cholesky` | re-export shadowed by the submodule `linalg::cholesky`; reach it at `linalg.cholesky.cholesky` | +| `linalg` | `svd` | re-export shadowed by the submodule `linalg::svd`; reach it at `linalg.svd.svd` | +| `manifold::clifford::sta` | `maxwell_residual()` | argument `f: &dyn Fn(&[f64; 4]) -> Multivector` | +| `manifold::embedding` | `riemannian_gradient_descent_stiefel()` | argument `grad: &dyn Fn(&Matrix) -> Matrix` | +| `manifold::lie` | `Sl2R.exp()` | argument `a: [[f64; 2]; 2]` | +| `manifold::lie` | `Su2.from_matrix_2x2()` | argument `m: [[Complex; 2]; 2]` | +| `manifold::lie` | `pose_graph_optimize()` | argument `edges: &[(usize, usize, Se3, [[f64; 6]; 6])]` | +| `manifold::metric` | `Metric.covariant_derivative_tensor()` | argument `t: &dyn Fn(&VecN) -> TensorN` | +| `manifold::metric` | `Metric.frw()` | argument `a: fn(f64) -> f64` | +| `manifold::metric` | `Metric.gaussian_curvature_surface()` | argument `f: fn(f64, f64) -> Vec3` | +| `manifold::metric` | `Metric.kaluza_klein_5d()` | argument `g4: Metric` | +| `manifold::metric` | `Metric.new()` | argument `g: impl Fn(&VecN) -> Matrix + 'static` | +| `manifold::metric` | `frw_metric()` | argument `a: fn(f64) -> f64` | +| `manifold::metric` | `schwarzschild_metric_fn()` | return type `impl Fn(&VecN) -> Matrix` | +| `manifold::metric` | `surface_metric_from_parametrization()` | argument `f: fn(f64, f64) -> Vec3` | +| `manifold::metric` | `warped_product()` | argument `base: Metric` | +| `manifold::spacetime` | `frw_geodesic()` | argument `a_fn: fn(f64) -> f64` | +| `manifold::spacetime` | `kaluza_klein_metric()` | argument `g4: Metric` | +| `manifold::vecn` | `exterior_derivative_numeric()` | argument `omega: &dyn Fn(&VecN) -> TensorN` | +| `mesh` | `Mesh.transform()` | argument `m: &Mat4` | +| `monte_carlo` | `mc_integrate_importance()` | argument `sampler: &dyn Fn(&mut Rng) -> f64` | +| `monte_carlo` | `metropolis_sample()` | argument `proposal: &dyn Fn(f64, &mut Rng) -> f64` | +| `monte_carlo::quasi` | `scrambled()` | argument `seq: Sobol` | +| `numerical::ode::implicit` | `backward_euler()` | argument `jac: Option<&dyn Fn(f64, &[f64]) -> Matrix>` | +| `numerical::ode::implicit` | `bdf2()` | argument `jac: Option<&dyn Fn(f64, &[f64]) -> Matrix>` | +| `optimization::convex` | `augmented_lagrangian()` | argument `constraint_gradients: &dyn Fn(&[f64]) -> Vec>` | +| `optimization::convex` | `newton_method_nd()` | argument `hess: &dyn Fn(&[f64]) -> Matrix` | +| `optimization::convex` | `penalty_method()` | argument `constraint_gradients: &dyn Fn(&[f64]) -> Vec>` | +| `optimization::convex` | `trust_region_dogleg()` | argument `hess: &dyn Fn(&[f64]) -> Matrix` | +| `optimization::game_theory` | `alpha_beta_search()` | generic () | +| `optimization::game_theory` | `iterated_pd_tournament()` | argument `strategies: &[Box]` | +| `optimization::game_theory` | `mcts_lite()` | generic () | +| `optimization::game_theory` | `minimax_search()` | generic () | +| `optimization::game_theory` | `standard_ipd_strategies()` | return type `Vec>` | +| `optimization::integer` | `sudoku_solve()` | argument `grid: &[[u8; 9]; 9]` | +| `optimization::least_squares` | `levenberg_marquardt()` | argument `jacobian: Option<&dyn Fn(&[f64]) -> Matrix>` | +| `optimization::metaheuristics` | `nsga2()` | argument `objectives: &[&dyn Fn(&[f64]) -> f64]` | +| `optimization::metaheuristics` | `simulated_annealing_generic()` | generic () | +| `optimization::metaheuristics` | `tabu_search()` | generic () | +| `photonics` | `apply_ray_matrix()` | argument `matrix: &RayMatrix` | +| `photonics` | `multiply_ray_matrices()` | argument `m1: &RayMatrix` | +| `quantum::algorithms` | `hhl_lite_2x2()` | argument `a: &[[f64; 2]; 2]` | +| `quantum::algorithms` | `vqe_lite()` | argument `ansatz: &dyn Fn(&[f64]) -> Result` | +| `quantum::circuit` | `Gate.from_matrix()` | argument `matrix: [[Complex; 2]; 2]` | +| `quantum::spin` | `lanczos()` | argument `matvec: &dyn Fn(&[Complex]) -> Vec` | +| `quaternion` | `Quaternion.from_rotation_matrix()` | argument `m: &[[f64; 3]; 3]` | +| `sim::rigid_body` | `RigidBodySystem.add_body()` | argument `body: RigidBody` | +| `spatial::quadtree` | `Quadtree` | generic type | +| `special` | `beta` | re-export shadowed by the submodule `special::beta`; reach it at `special.beta.beta` | +| `special` | `erf` | re-export shadowed by the submodule `special::erf`; reach it at `special.erf.erf` | +| `special` | `gamma` | re-export shadowed by the submodule `special::gamma`; reach it at `special.gamma.gamma` | +| `stochastic::hmm` | `ParticleFilter.new()` | argument `init: &dyn Fn(&mut Rng) -> Vec` | +| `stochastic::hmm` | `ParticleFilter.predict()` | argument `dynamics: &dyn Fn(&[f64], &mut Rng) -> Vec` | +| `stochastic::markov` | `Mcmc.gibbs()` | argument `conditionals: &[&dyn Fn(&[f64], &mut Rng) -> f64]` | +| `stochastic::point_process` | `compound_poisson()` | argument `jump_dist: &dyn Fn(&mut Rng) -> f64` | +| `stochastic::point_process` | `cox_process()` | argument `rate_dist: &dyn Fn(&mut Rng) -> f64` | +| `stochastic::point_process` | `renewal_function_estimate()` | argument `interarrival: &dyn Fn(&mut Rng) -> f64` | +| `stochastic::point_process` | `renewal_process()` | argument `interarrival: &dyn Fn(&mut Rng) -> f64` | +| `stochastic::queueing` | `queue_simulate()` | argument `arrival: &dyn Fn(&mut Rng) -> f64` | +| `stochastic::sde` | `euler_maruyama_nd()` | argument `sigma: &dyn Fn(f64, &[f64]) -> Matrix` | +| `transforms` | `fft` | re-export shadowed by the submodule `transforms::fft`; reach it at `transforms.fft.fft` | +| `transforms` | `hilbert` | re-export shadowed by the submodule `transforms::hilbert`; reach it at `transforms.hilbert.hilbert` | +| `transforms` | `radon` | re-export shadowed by the submodule `transforms::radon`; reach it at `transforms.radon.radon` | diff --git a/bindings/python/Cargo.lock b/bindings/python/Cargo.lock new file mode 100644 index 0000000..b54ebe4 --- /dev/null +++ b/bindings/python/Cargo.lock @@ -0,0 +1,137 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pyo3" +version = "0.29.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4688ddedf473e32662b9b067670129a8afb8c18e351482c70d62ba4a88171e8b" +dependencies = [ + "libc", + "once_cell", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", +] + +[[package]] +name = "pyo3-build-config" +version = "0.29.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f41027e41b4bd03f6e60f9f417fe24a6341a6bb744edd62b6f709f2a52ea30e9" +dependencies = [ + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.29.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e591a95526fead067432c3b3a33fc74770b87b1e04e73671090d9c2055a2b327" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.29.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73225868fc1cd84eef2c3c230ddb91273bf1de46aeb8a4248da76d32a0924a1c" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.29.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "571575aa3749fa6216757dd47d2a3e7ef360f329a40f0666a9fbd14889024952" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rust_physics_engine" +version = "0.1.0" + +[[package]] +name = "rust_physics_engine_py" +version = "0.1.0" +dependencies = [ + "pyo3", + "rust_physics_engine", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" diff --git a/bindings/python/Cargo.toml b/bindings/python/Cargo.toml new file mode 100644 index 0000000..de07bfb --- /dev/null +++ b/bindings/python/Cargo.toml @@ -0,0 +1,31 @@ +# A workspace of its own. The parent crate is deliberately dependency-free +# -- its Cargo.lock holds exactly one package, itself -- and making these +# bindings a member of it would put PyO3 and its tree into that lock file +# and into every `cargo build` at the repository root. Detaching keeps the +# library's build exactly as it was; this crate is built by maturin. +[workspace] + +[package] +name = "rust_physics_engine_py" +version = "0.1.0" +edition = "2021" +description = "Python bindings for rust_physics_engine" +license = "MIT" +repository = "https://github.com/Magic-Man-us/RustPhysicsEngine" +publish = false + +[lib] +name = "_core" +crate-type = ["cdylib"] + +[dependencies] +rust_physics_engine = { path = "../.." } +pyo3 = { version = "0.29", features = ["extension-module", "abi3-py39"] } + +[profile.release] +# The generated code is ~6,000 small `#[pyfunction]` wrappers. Sixteen +# codegen units keeps the compile parallel; `opt-level = 2` is as fast as +# 3 here because the real work happens in the library, which is compiled +# separately. +opt-level = 2 +codegen-units = 16 diff --git a/bindings/python/README.md b/bindings/python/README.md new file mode 100644 index 0000000..318dd32 --- /dev/null +++ b/bindings/python/README.md @@ -0,0 +1,222 @@ +# Numeria + +The [rust_physics_engine][crate] library, from Python. + +4,086 functions, 2,254 methods, 416 classes and 106 constants across 71 +domains, from Newtonian mechanics to Reed–Solomon codes, with no runtime +dependencies on either side. + +```console +$ pip install numeria +``` + +Wheels are published for Linux, macOS (universal2, so both Apple Silicon +and Intel) and Windows, and need no Rust toolchain. Anywhere else, pip +falls back to the source distribution, which carries the library with it +and builds against that copy — a Rust toolchain is the only requirement. + +To build from a checkout instead: + +```console +$ pip install ./bindings/python +``` + +To work on the bindings themselves, from an activated virtualenv: + +```console +$ pip install maturin +$ maturin develop --release -m bindings/python/Cargo.toml +``` + +```python +>>> import math +>>> import numeria as nm +>>> nm.classical.projectile_range(speed=20.0, angle_rad=math.pi / 4, g=9.80665) +40.78864851911713 +``` + +Every Python module mirrors a Rust module of the same name, and every +function keeps the name, the argument order and the units it has in Rust, +so `rust_physics_engine::linalg::lu::solve` is `numeria.linalg.lu.solve`. +If you can read `docs/MODULE_MAP.md`, you can find your way around here. + +## What the bindings add + +The Rust API is not changed, but five things are translated so that it +reads as Python rather than as Rust seen through glass. + +**Errors are exceptions.** `Result` becomes a return value +and a raise; the variants that carry data carry it onto the exception. + +```python +>>> try: +... nm.linalg.lu.solve([[1.0, 2.0], [2.0, 4.0]], [1.0, 2.0]) +... except nm.SingularMatrixError as e: +... print(e) +matrix is singular or pivot below threshold +``` + +Everything raised derives from `PhysicsError`, so `except PhysicsError` +catches all of it and nothing else: + +``` +PhysicsError +├── InvalidArgumentError a documented precondition was violated +├── SolverError +│ ├── SingularMatrixError +│ ├── NotPositiveDefiniteError +│ ├── ConvergenceError .iterations, .residual +│ └── DimensionMismatchError .expected, .got +├── GeometryError +│ ├── DegenerateGeometryError +│ ├── NotManifoldError +│ └── EmptyInputError +└── UnitsError +``` + +The library also validates arguments with `assert!`, which in Rust is the +right call — a negative mass is a programming error, not a runtime +condition. Those become `InvalidArgumentError` carrying the assertion's +own message, rather than aborting the interpreter: + +```python +>>> nm.classical.acceleration(force=10.0, mass=-1.0) +Traceback (most recent call last): +numeria.InvalidArgumentError: mass must be positive +``` + +**Small value types accept literals.** Anywhere a `Vec2`, `Vec3`, `Vec4`, +`Mat3` or `Quaternion` is expected, a sequence of the right length will +do; anywhere a `Matrix` is expected, a list of rows will do. The wrapper +classes still exist, with their methods, their operators and their +`tolist()`. + +```python +>>> nm.classical.position_3d((0, 0, 100), (5, 0, 0), (0, 0, -9.81), 2.0).tolist() +[10.0, 0.0, 80.38] +>>> v = nm.math.Vec3(1, 2, 2) +>>> v.magnitude(), (v + (1, 0, 0)).tolist(), v[0], list(v) +(3.0, [2.0, 2.0, 2.0], 1.0, [1.0, 2.0, 2.0]) +``` + +**Three Rust types are Python types.** They have exact counterparts, so +they are translated rather than wrapped, and the round trip loses nothing: + +| Rust | Python | +|---|---| +| `fractals::Complex` | `complex` | +| `exact::bigint::BigInt` | `int`, of any size | +| `exact::rational::Rational` | `fractions.Fraction` | + +```python +>>> nm.exact.bigint.factorial(30) +265252859812191058636308480000000 +>>> nm.transforms.fft.fft([1, 0, 0, 0]) +[(1+0j), (1+0j), (1+0j), (1+0j)] +``` + +**Builders chain.** A Rust method that takes `&mut self` and returns +`&mut Self` hands the same Python object back, so a circuit reads the way +it does in Rust: + +```python +>>> c = nm.quantum.circuit.Circuit(2) +>>> c.h(0).cx(0, 1) # a Bell pair +>>> state = c.run(nm.quantum.circuit.QState.zero(2)) +>>> [round(abs(z) ** 2, 3) for z in state.amps] +[0.5, 0.0, 0.0, 0.5] +``` + +**Functions can be Python functions.** Anywhere the library takes a +`&dyn Fn`, pass a callable. An exception raised inside it comes back out +of the call with its own traceback, rather than turning into a NaN: + +```python +>>> import math +>>> nm.numerical.integrate.simpson(math.sin, 0.0, math.pi, 1000) +2.0000000000010805 +>>> nm.numerical.roots.newton_raphson(lambda x: x*x - 2, lambda x: 2*x, 1.0, 1e-12, 50) +1.414213562373095 +``` + +## Types, and your editor + +The package ships `py.typed` and a `.pyi` stub for every module, so +`mypy`, `pyright` and editor completion all work without importing the +extension. + +## What is not bound + +4,086 of the library's 4,149 free functions, 2,254 of its 2,277 methods, +416 of its 426 types and all 106 of its constants, across 296 modules. + +The rest is mostly three things: functions generic over a type parameter, +which cannot be monomorphised without knowing what to monomorphise to; +`&dyn Trait` arguments for traits with no Python equivalent; and routines +returning a closure. [COVERAGE.md](COVERAGE.md) lists every unbound item +by name with its reason, and is regenerated with the bindings, so it +cannot drift from them. + +## How this is built + +`generate.py` reads the library's source with `rustscan.py` and writes the +wrapper for every item it can bind. The alternative — writing 6,000 +wrappers by hand — fails quietly: the first commit that adds a function to +the library leaves the binding stale, and nothing breaks to tell you. +Here, regenerating is one command, and CI runs + +```console +$ python3 bindings/python/generate.py --check +``` + +which fails if what is committed differs from what the current source +produces. + +To work on the bindings: + +```console +$ python3 bindings/python/generate.py # after changing the library +$ maturin develop --release -m bindings/python/Cargo.toml +$ python -m pytest bindings/python/tests +``` + +The hand-written half is small and lives in `src/runtime/`: the exception +hierarchy and the panic guard (`errors.rs`), the literal coercions and the +three type identifications (`coerce.rs`), and the callable adapter +(`callback.rs`). Anything a generator cannot reach — a Python protocol +like `__getitem__`, a method defined by a `macro_rules!` the scanner +cannot see — is declared in a table at the top of `generate.py` and +spliced in, so there is one place to look. + +## Releasing + +`.github/workflows/python-release.yml` runs on a `v*` tag and nothing +else, and publishes to PyPI by Trusted Publishing — there is no API token +in the repository to leak or rotate. + +```console +$ # bump the version in Cargo.toml and bindings/python/Cargo.toml together +$ python3 bindings/python/check_version.py v0.2.0 +$ git tag v0.2.0 && git push origin v0.2.0 +``` + +The tag check runs first, before any runner minutes are spent, because +PyPI will not let a version be re-uploaded even after it is deleted — so +tagging `v0.2.0` against a `Cargo.toml` that still says `0.1.0` is a +mistake with no undo. + +PyPI's trusted publisher for `numeria` is pinned to this repository, to +the file name `python-release.yml`, and to the `pypi` environment. Renaming +any of those three breaks the release until PyPI is told about it — the +header of that workflow file spells them out. Putting a required reviewer +on the `pypi` environment makes every publish wait for a human, which is +worth doing for the same reason the version check exists. + +## Performance notes + +The GIL is released around calls that do real work — those taking or +returning arrays — so several threads can compute at once. It is held for +scalar calls, where releasing it would cost more than the call, and for +anything involving a Python callable, which needs it. + +[crate]: https://github.com/Magic-Man-us/RustPhysicsEngine diff --git a/bindings/python/check_stubs.py b/bindings/python/check_stubs.py new file mode 100644 index 0000000..144b438 --- /dev/null +++ b/bindings/python/check_stubs.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +"""Check that the type stubs describe the extension that was actually built. + +A stub that disagrees with its module is worse than no stub at all: it +type-checks code that will fail at run time, and it hides code that would +have worked. Both halves come out of the same generator, so they agree +when the generator is right -- and this is what establishes that, by +importing the built extension and comparing what it exposes against what +the `.pyi` files declare, name by name. + +Run it after `maturin develop`: + + python3 bindings/python/check_stubs.py +""" + +from __future__ import annotations + +import ast +import os +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +STUBS = os.path.join(HERE, "python", "numeria") + + +def stub_names(path: str) -> set[str]: + """The top-level names a stub file declares.""" + with open(path, encoding="utf-8") as fh: + tree = ast.parse(fh.read(), filename=path) + names: set[str] = set() + for node in tree.body: + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): + names.add(node.name) + elif isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name): + names.add(node.target.id) + elif isinstance(node, ast.ImportFrom): + for alias in node.names: + names.add(alias.asname or alias.name) + elif isinstance(node, ast.Import): + for alias in node.names: + names.add((alias.asname or alias.name).split(".")[0]) + return names + + +def stub_path(dotted: str) -> str | None: + rel = dotted.replace(".", os.sep) + for candidate in ( + os.path.join(STUBS, rel + ".pyi"), + os.path.join(STUBS, rel, "__init__.pyi"), + ): + if os.path.exists(candidate): + return candidate + return None + + +def module_names(mod) -> set[str]: + out = set() + for name in dir(mod): + if name.startswith("_"): + continue + value = getattr(mod, name) + if type(value).__name__ == "module": + continue + out.add(name) + return out + + +def main() -> int: + try: + import numeria as nm + except ImportError as exc: # pragma: no cover - the message is the point + print(f"cannot import the extension: {exc}") + print("build it first: maturin develop --release -m bindings/python/Cargo.toml") + return 2 + + problems: list[str] = [] + checked = 0 + for dotted in nm._core.__submodules__: + mod = sys.modules.get(f"numeria.{dotted}") + if mod is None: + problems.append(f"{dotted}: not installed in sys.modules") + continue + path = stub_path(dotted) + if path is None: + problems.append(f"{dotted}: no stub file") + continue + checked += 1 + declared = stub_names(path) + actual = module_names(mod) + for missing in sorted(actual - declared): + problems.append(f"{dotted}.{missing}: in the module, not in the stub") + # A stub may legitimately name imported classes it only refers to, + # so only flag a declared name the module does not have if the + # stub defines it rather than importing it. + with open(path, encoding="utf-8") as fh: + tree = ast.parse(fh.read()) + defined = { + n.name + for n in tree.body + if isinstance(n, (ast.FunctionDef, ast.ClassDef)) + } | { + n.target.id + for n in tree.body + if isinstance(n, ast.AnnAssign) and isinstance(n.target, ast.Name) + } + for extra in sorted(defined - actual): + problems.append(f"{dotted}.{extra}: in the stub, not in the module") + + if problems: + print(f"{len(problems)} disagreement(s) between the stubs and the module:") + for p in problems[:60]: + print(" ", p) + if len(problems) > 60: + print(f" ... and {len(problems) - 60} more") + return 1 + print(f"stubs agree with the module across {checked} modules") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/bindings/python/check_version.py b/bindings/python/check_version.py new file mode 100644 index 0000000..96ef149 --- /dev/null +++ b/bindings/python/check_version.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""Check that the library, the bindings and the tag all claim one version. + +A publish is the one operation here that cannot be taken back: PyPI does +not allow a version to be re-uploaded, even after a delete. So the thing +worth checking before it happens is the thing that is easy to get wrong -- +tagging `v0.2.0` while `Cargo.toml` still says `0.1.0`, and publishing +0.1.0 under a name that can never be used again. + +The Python package is the same library seen from Python, so its version +tracks the crate's rather than moving on its own. + + python3 bindings/python/check_version.py # the two agree + python3 bindings/python/check_version.py v0.2.0 # ...and match the tag +""" + +from __future__ import annotations + +import os +import re +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.abspath(os.path.join(HERE, "..", "..")) + + +def package_version(cargo_toml: str) -> str | None: + """The `version` of the `[package]` table, ignoring every other table.""" + with open(cargo_toml, encoding="utf-8") as fh: + text = fh.read() + in_package = False + for line in text.splitlines(): + stripped = line.strip() + if stripped.startswith("["): + in_package = stripped == "[package]" + continue + if not in_package: + continue + m = re.match(r'version\s*=\s*"([^"]+)"', stripped) + if m: + return m.group(1) + return None + + +def main(argv: list[str]) -> int: + crate = package_version(os.path.join(ROOT, "Cargo.toml")) + bindings = package_version(os.path.join(HERE, "Cargo.toml")) + + problems = [] + if crate is None: + problems.append("the crate's Cargo.toml has no [package] version") + if bindings is None: + problems.append("the bindings' Cargo.toml has no [package] version") + if crate and bindings and crate != bindings: + problems.append( + f"the crate is {crate} but the bindings are {bindings}; " + "the Python package is the same library, so the two move together" + ) + + if len(argv) > 1: + tag = argv[1].removeprefix("refs/tags/") + wanted = tag.removeprefix("v") + if not re.fullmatch(r"\d+\.\d+\.\d+([-.+][0-9A-Za-z.-]+)?", wanted): + problems.append(f"the tag {tag!r} is not a version tag of the form vX.Y.Z") + elif bindings and wanted != bindings: + problems.append( + f"the tag says {wanted} but Cargo.toml says {bindings}. " + "PyPI will not let a version be re-uploaded, so publishing the " + "wrong one is not recoverable -- bump Cargo.toml, or retag" + ) + + if problems: + print("version check failed:") + for p in problems: + print(f" {p}") + return 1 + + described = f"{crate}" + (f", matching tag {argv[1]}" if len(argv) > 1 else "") + print(f"crate and bindings agree on {described}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv)) diff --git a/bindings/python/generate.py b/bindings/python/generate.py new file mode 100644 index 0000000..c9898f1 --- /dev/null +++ b/bindings/python/generate.py @@ -0,0 +1,2737 @@ +#!/usr/bin/env python3 +"""Generate the PyO3 bindings for `rust_physics_engine`. + +The library is about 266,000 lines across 71 top-level modules, and its +public surface is roughly 4,100 free functions, 2,200 inherent methods, +336 structs, 88 enums and 117 constants. Writing that by hand is not the +hard part -- keeping it in step with the library afterwards is. A binding +written once goes stale on the first commit that adds a function, and the +staleness is invisible: nothing fails, the function simply is not there. + +So the bindings are derived from the source instead. `rustscan` reads the +crate's public API; this file decides how each item crosses into Python +and writes the wrapper. Re-running it after a change to the library +produces the binding for the changed library, and CI re-runs it and fails +if the committed output differs, which is the only thing that keeps +generated code honest. + +What it cannot bind it says so about, in COVERAGE.md, with the reason -- +a generic parameter it cannot monomorphise, a `&dyn Trait` argument with +no Python equivalent. Silence about a gap is worse than the gap. + +Usage: + python3 generate.py # write the bindings + python3 generate.py --check # fail if what is committed is stale +""" + +from __future__ import annotations + +import argparse +import dataclasses +import os +import re +import shutil +import sys +from dataclasses import dataclass, field + +HERE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, HERE) + +import rustscan # noqa: E402 + +CRATE_ROOT = os.path.abspath(os.path.join(HERE, "..", "..")) +SRC = os.path.join(CRATE_ROOT, "src") +OUT_RS = os.path.join(HERE, "src", "generated") +OUT_PY = os.path.join(HERE, "python", "numeria") +PKG = "numeria" + +# Error types are exceptions, not classes. +ERROR_TYPES = { + "error::SolveError", + "error::GeomError", + "units::quantity::DimError", + "codes::reed_solomon::TooManyErrors", + "graph::paths::NegativeCycle", +} + +# Types with an exact Python counterpart, translated rather than wrapped. +IDENTIFIED = { + "fractals::Complex": "complex", + "exact::bigint::BigInt": "bigint", + "exact::rational::Rational": "rational", +} + +PRIMS = { + "f64": "float", + "f32": "float", + "usize": "int", + "isize": "int", + "u8": "int", + "u16": "int", + "u32": "int", + "u64": "int", + "i8": "int", + "i16": "int", + "i32": "int", + "i64": "int", + "bool": "bool", + "char": "str", +} + +PY_KEYWORDS = { + "False", "None", "True", "and", "as", "assert", "async", "await", "break", + "class", "continue", "def", "del", "elif", "else", "except", "finally", + "for", "from", "global", "if", "import", "in", "is", "lambda", "nonlocal", + "not", "or", "pass", "raise", "return", "try", "while", "with", "yield", + "match", "case", "type", +} + +RUST_KEYWORDS = { + "as", "break", "const", "continue", "crate", "dyn", "else", "enum", + "extern", "false", "fn", "for", "if", "impl", "in", "let", "loop", + "match", "mod", "move", "mut", "pub", "ref", "return", "self", "static", + "struct", "super", "trait", "true", "type", "unsafe", "use", "where", + "while", "async", "await", "box", "final", "macro", "override", "priv", + "try", "typeof", "unsized", "virtual", "yield", "abstract", "become", + "do", +} + + +# ── The type language ─────────────────────────────────────────────────── + + +@dataclass(frozen=True) +class Ty: + """A Rust type, reduced to the shapes that matter for binding. + + `by_ref` and `mutable` are kept because they change what the wrapper + has to pass. `Vec3` and `&Vec3` need different call sites, and + `&mut [f64]` is not an input at all -- it is an output written through + an argument, and a binding that quietly dropped the writes would be + worse than one that refused to bind it. + """ + + kind: str + inner: tuple = () + name: str = "" + n: int = 0 + by_ref: bool = False + mutable: bool = False + + def amp(self, expr: str) -> str: + """`expr`, borrowed if this type is a reference.""" + return f"&{expr}" if self.by_ref else expr + + def __repr__(self) -> str: # pragma: no cover - debugging aid + pre = ("&mut " if self.mutable else "&") if self.by_ref else "" + if self.kind in ("prim", "user"): + return f"{pre}{self.kind}:{self.name}" + return f"{pre}{self.kind}({', '.join(map(repr, self.inner))})" + + +BAD = Ty("bad") + + +class Resolver: + """Turns a type as written into a fully-qualified crate path.""" + + def __init__(self, crate: rustscan.Crate): + self.crate = crate + self.by_path: dict[str, object] = {} + self.by_name: dict[str, list[str]] = {} + for item in list(crate.structs) + list(crate.enums): + self.by_path[item.path] = item + self.by_name.setdefault(item.name, []).append(item.path) + self.aliases = {} + for a in crate.aliases: + self.aliases[f"{a.module}::{a.name}" if a.module else a.name] = a.target + # Every module that exists, so `use` targets can be checked. + self.modules = set() + for item in list(crate.structs) + list(crate.enums) + list(crate.funcs): + parts = item.module.split("::") + for i in range(len(parts)): + self.modules.add("::".join(parts[: i + 1])) + + def resolve(self, name: str, file: str, module: str) -> str | None: + """Full crate path for the type `name` as written inside `file`.""" + name = name.strip() + if name.startswith("crate::"): + name = name[len("crate::") :] + if name.startswith("self::"): + name = f"{module}::{name[6:]}" + head = name.split("::")[0] + tail = name.split("::")[1:] + + candidates: list[str] = [] + # Defined in the same module. + candidates.append(f"{module}::{name}" if module else name) + # Imported by name. + mapped = self.crate.uses.get(file, {}).get(head) + if mapped: + candidates.append("::".join([mapped] + tail)) + # Brought in by a glob. + for g in self.crate.glob_uses.get(file, []): + candidates.append(f"{g}::{name}") + # An ancestor module. + parts = module.split("::") if module else [] + for i in range(len(parts) - 1, -1, -1): + candidates.append("::".join(parts[:i] + [name])) + # Written out in full already. + candidates.append(name) + # A unique match anywhere in the crate. + if len(self.by_name.get(name, [])) == 1: + candidates.append(self.by_name[name][0]) + + for c in candidates: + if c in self.by_path: + return c + if c in self.aliases: + return None # an alias; the caller re-parses the target + return None + + def alias_target(self, name: str, file: str, module: str) -> str | None: + name = name.strip().removeprefix("crate::") + head = name.split("::")[0] + for cand in ( + f"{module}::{name}" if module else name, + self.crate.uses.get(file, {}).get(head, ""), + name, + ): + if cand and cand in self.aliases: + return self.aliases[cand] + return None + + +def parse_type(text: str, res: Resolver, file: str, module: str, depth: int = 0) -> Ty: + """Parse a Rust type into the reduced language above.""" + if depth > 8: + return BAD + s = " ".join(text.split()).strip() + if not s: + return Ty("unit") + if s in ("()", "!"): + return Ty("unit") + + # A leading reference belongs to the type it points at, not to a + # wrapper around it: `&[f64]` is the slice type, borrowed. + if s.startswith("&"): + m = re.match(r"^&\s*(?:'[A-Za-z_][A-Za-z0-9_]*\s+)?(mut\s+)?", s) + inner = parse_type(s[m.end() :], res, file, module, depth + 1) + if inner.kind == "bad": + return BAD + return dataclasses.replace(inner, by_ref=True, mutable=bool(m.group(1))) + if s.startswith("*"): + return BAD + + # Slices and arrays. + if s.startswith("[") and s.endswith("]"): + body = s[1:-1] + parts = rustscan._split_top(body) + if len(parts) == 1 and ";" in body: + base, count = body.rsplit(";", 1) + count = count.strip() + elem = parse_type(base, res, file, module, depth + 1) + if elem.kind == "bad" or not count.isdigit(): + return BAD + return Ty("array", (elem,), n=int(count)) + elem = parse_type(body, res, file, module, depth + 1) + return BAD if elem.kind == "bad" else Ty("vec", (elem,)) + + # Tuples. + if s.startswith("(") and s.endswith(")") and _balanced(s): + parts = rustscan._split_top(s[1:-1]) + if len(parts) == 1: + return parse_type(parts[0], res, file, module, depth + 1) + elems = tuple(parse_type(p, res, file, module, depth + 1) for p in parts) + if any(e.kind == "bad" for e in elems): + return BAD + return Ty("tuple", elems) + + # Callables. A bare `fn(..)` pointer is deliberately not one: a Python + # callable has no address to hand over. + m = re.match(r"^(?:dyn\s+|impl\s+)(?:Fn|FnMut|FnOnce)\s*\((.*?)\)\s*(?:->\s*(.+?))?(?:\s*\+\s*.*)?$", s) + if m: + argtypes = tuple( + parse_type(p, res, file, module, depth + 1) for p in rustscan._split_top(m.group(1)) + ) + ret = parse_type(m.group(2) or "()", res, file, module, depth + 1) + if any(a.kind == "bad" for a in argtypes) or ret.kind == "bad": + return BAD + return Ty("callable", argtypes + (ret,)) + + # `impl Iterator`: a Python caller gets the list. The + # iterators here are all finite by construction -- subsets, dyck + # paths, partitions -- so collecting is a change of laziness, not of + # termination. + m = re.match(r"^impl\s+Iterator\s*<\s*Item\s*=\s*(.+?)\s*>(?:\s*\+.*)?$", s) + if m: + elem = parse_type(m.group(1), res, file, module, depth + 1) + return BAD if elem.kind == "bad" else Ty("iter", (elem,)) + if s.startswith("dyn ") or s.startswith("impl "): + return BAD + + if s in ("String", "str", "std::string::String"): + return Ty("str") + if s in PRIMS: + return Ty("prim", name=s) + + # Generic containers. + m = re.match(r"^([A-Za-z_][A-Za-z0-9_:]*)\s*<(.+)>$", s) + if m: + head = m.group(1).split("::")[-1] + args = [parse_type(p, res, file, module, depth + 1) for p in rustscan._split_top(m.group(2))] + if head in ("Vec", "VecDeque"): + return BAD if args[0].kind == "bad" else Ty("vec", (args[0],)) + if head == "Box": + return args[0] if args else BAD + if head == "Option": + return BAD if args[0].kind == "bad" else Ty("opt", (args[0],)) + if head == "Result": + ok = args[0] + parts = rustscan._split_top(m.group(2)) + errname = parts[1].strip() if len(parts) > 1 else "" + if ok.kind == "bad": + return BAD + return Ty("result", (ok,), name=errname.split("::")[-1]) + return BAD + + # A plain path: a struct, an enum, or an alias for one. + if re.match(r"^[A-Za-z_][A-Za-z0-9_:]*$", s): + if s.split("::")[-1] == "Self": + return Ty("selfty") + target = res.alias_target(s, file, module) + if target is not None: + return parse_type(target, res, file, module, depth + 1) + full = res.resolve(s, file, module) + if full: + return Ty("user", name=full) + return BAD + + +def _balanced(s: str) -> bool: + depth = 0 + for i, c in enumerate(s): + if c == "(": + depth += 1 + elif c == ")": + depth -= 1 + if depth == 0 and i != len(s) - 1: + return False + return depth == 0 + + +# ── Wrappers ──────────────────────────────────────────────────────────── + + +@dataclass +class Wrapper: + """A Python class standing in for one Rust struct or enum.""" + + item: object + ident: str # the Rust identifier of the wrapper struct + py_name: str + py_module: str + rust_path: str + is_enum: bool + simple_enum: bool + clone: bool + debug: bool + partial_eq: bool + copy: bool + coerce_n: int = 0 # >0: accepts a sequence of this many floats + coerce_rows: bool = False # accepts a sequence of rows + coerce_seq: bool = False # accepts a sequence of floats of any length + unsendable: bool = False + fields: list = field(default_factory=list) # bindable (name, Ty, rust_name) + consts: list = field(default_factory=list) + + @property + def arg_ident(self) -> str: + return f"{self.ident}Arg" + + +def camel(s: str) -> str: + return "".join(p[:1].upper() + p[1:] for p in re.split(r"[_:]+", s) if p) + + +class Generator: + def __init__(self) -> None: + self.crate = rustscan.scan_crate(SRC) + _add_macro_items(self.crate) + self.res = Resolver(self.crate) + self.wrappers: dict[str, Wrapper] = {} + self.skipped: list[tuple[str, str, str]] = [] # (module, item, reason) + self.counts: dict[str, dict[str, int]] = {} + self.modules: list[str] = [] + self._plan_wrappers() + + # ── planning ──────────────────────────────────────────────────── + + def _plan_wrappers(self) -> None: + items = [(s, False) for s in self.crate.structs] + [(e, True) for e in self.crate.enums] + by_name: dict[str, list] = {} + keep = [] + for item, is_enum in items: + if item.path in ERROR_TYPES or item.path in IDENTIFIED: + continue + if item.generics.strip(): + self.skipped.append((item.module, item.name, "generic type")) + continue + if item.name.startswith("_"): + continue + keep.append((item, is_enum)) + by_name.setdefault(item.name, []).append(item) + + for item, is_enum in keep: + derives = item.derives() + if len(by_name[item.name]) == 1: + ident = "Py" + item.name + else: + ident = "Py" + camel(item.module.split("::")[-1]) + item.name + if sum(1 for o in by_name[item.name] if o.module.split("::")[-1] == item.module.split("::")[-1]) > 1: + ident = "Py" + camel(item.module) + item.name + simple_enum = is_enum and all(not v.payload for v in item.variants) + unsendable = any( + re.search(r"\bdyn\b|\bRc<|\bCell<|\bRefCell<", f.ty) for f in getattr(item, "fields", []) + ) + self.wrappers[item.path] = Wrapper( + item=item, + ident=ident, + py_name=item.name, + py_module=f"{PKG}.{item.module.replace('::', '.')}", + rust_path=f"rust_physics_engine::{item.path}", + is_enum=is_enum, + simple_enum=simple_enum, + clone="Clone" in derives, + debug="Debug" in derives, + partial_eq="PartialEq" in derives, + copy="Copy" in derives, + unsendable=unsendable, + ) + + # Which structs can be built from a bare sequence? + for path, w in self.wrappers.items(): + if w.is_enum or not w.clone: + continue + fields = w.item.fields + if w.item.kind == "named" and fields and all(f.public and f.ty == "f64" for f in fields): + if len(fields) <= 6: + w.coerce_n = len(fields) + if w.item.kind == "named" and len(fields) == 1 and fields[0].ty == "Vec" and fields[0].public: + w.coerce_seq = True + matrix = self.wrappers.get("linalg::matrix::Matrix") + if matrix: + matrix.coerce_rows = True + + # A type is unsendable if it holds something unsendable, however + # deep. PyO3 needs that to be right: a `#[pyclass]` that is not + # `Send` and not declared `unsendable` will not compile, and one + # that is declared `unsendable` must not have the GIL released + # around it. Iterate to a fixed point rather than looking only one + # field deep. + changed = True + while changed: + changed = False + for w in self.wrappers.values(): + if w.unsendable or w.is_enum: + continue + for f in w.item.fields: + ty = parse_type(f.ty, self.res, w.item.file, w.item.module) + if self._holds_unsendable(ty): + w.unsendable = True + changed = True + break + + # Bindable fields, for getters. + for path, w in self.wrappers.items(): + if w.is_enum or w.item.kind != "named": + continue + for f in w.item.fields: + if not f.public: + continue + ty = parse_type(f.ty, self.res, w.item.file, w.item.module) + if ty.kind != "bad" and self.ret_plan(ty, "x") is not None: + w.fields.append((f.name, ty)) + + # Associated constants. + for c in self.crate.consts: + if not c.owner: + continue + for path, w in self.wrappers.items(): + if w.item.name == c.owner and w.item.module == c.module: + ty = parse_type(c.ty, self.res, c.file, c.module) + if self.ret_plan(ty, "x") is not None: + w.consts.append((c.name, ty, c.doc)) + break + + def _holds_unsendable(self, ty: Ty, depth: int = 0) -> bool: + if depth > 6: + return False + if ty.kind == "user": + w = self.wrappers.get(ty.name) + return bool(w and w.unsendable) + return any(self._holds_unsendable(t, depth + 1) for t in ty.inner) + + def is_clone(self, ty: Ty, depth: int = 0) -> bool: + """Whether an owned value of `ty` can be cloned out of a field.""" + if depth > 6: + return False + k = ty.kind + if k in ("prim", "str", "unit"): + return True + if k in ("vec", "array", "opt"): + return self.is_clone(ty.inner[0], depth + 1) + if k == "tuple": + return all(self.is_clone(t, depth + 1) for t in ty.inner) + if k == "user": + if ty.name in IDENTIFIED: + return True + w = self.wrappers.get(ty.name) + return bool(w and (w.clone or w.simple_enum)) + return False + + # ── argument plans ────────────────────────────────────────────── + # + # Every argument is described by two things: the type the wrapper + # declares, and a Rust expression turning a value of that type into + # what the library wants. Keeping the conversion an *expression* + # rather than a statement is what lets it nest -- the conversion for + # `&[(Vec2, f64)]` is the conversion for `(Vec2, f64)` inside a + # `.map()`, and that in turn is the conversion for `Vec2` and for + # `f64`. Two shapes cannot be expressions and get their own paths: + # callables, which need an object that outlives the call, and `&mut` + # slices, which are outputs and have to be written back afterwards. + + def param_type(self, ty: Ty) -> str | None: + """The type the generated wrapper declares for `ty`.""" + k = ty.kind + if k == "prim": + return ty.name + if k == "str": + return "String" + if k == "user": + return self._user_param(ty.name) + if k == "vec": + inner = self.param_type(ty.inner[0]) + return None if inner is None else f"Vec<{inner}>" + if k == "array": + inner = self.param_type(ty.inner[0]) + return None if inner is None else f"Vec<{inner}>" + if k == "tuple": + parts = [self.param_type(t) for t in ty.inner] + if any(p is None for p in parts): + return None + return "(" + ", ".join(parts) + ")" + if k == "opt": + inner = self.param_type(ty.inner[0]) + return None if inner is None else f"Option<{inner}>" + return None + + def _user_param(self, path: str) -> str | None: + if path in IDENTIFIED: + return { + "complex": "crate::runtime::coerce::ComplexArg", + "bigint": "crate::runtime::coerce::BigIntArg", + "rational": "crate::runtime::coerce::RationalArg", + }[IDENTIFIED[path]] + w = self.wrappers.get(path) + if w is None or not (w.clone or w.simple_enum): + return None + if w.coerce_n or w.coerce_rows or w.coerce_seq: + return f"crate::generated::types::{w.arg_ident}" + return f"crate::generated::types::{w.ident}" + + def conv_expr(self, ty: Ty, var: str) -> tuple[str, bool] | None: + """Rust expression turning `var` into an owned value of `ty`. + + The flag says whether the expression uses `?`, which decides + whether a surrounding `.map()` has to collect into a `PyResult`. + """ + k = ty.kind + if k in ("prim", "str"): + return var, False + if k == "user": + path = ty.name + if path in IDENTIFIED: + return f"{var}.0", False + w = self.wrappers.get(path) + if w is None: + return None + if w.simple_enum: + return f"{var}.to_rust()", False + if w.coerce_n or w.coerce_rows or w.coerce_seq: + return f"{var}.0", False + if not w.clone: + return None + return f"{var}.inner", False + if k == "vec": + inner = self.conv_expr(ty.inner[0], "__e") + if inner is None: + return None + expr, fallible = inner + if expr == "__e": + return var, False + if fallible: + elem = self.rust_type(ty.inner[0]) + return ( + f"{var}.into_iter().map(|__e| -> PyResult<{elem}> {{ Ok({expr}) }})" + f".collect::>>()?", + True, + ) + return f"{var}.into_iter().map(|__e| {expr}).collect::>()", False + if k == "array": + elem = self.rust_type(ty.inner[0]) + if elem is None: + return None + inner = self.conv_expr(ty.inner[0], "__e") + if inner is None: + return None + expr, fallible = inner + body = var if expr == "__e" else ( + f"{var}.into_iter().map(|__e| {expr}).collect::>()" + ) + if fallible: + return None + return ( + f"<[{elem}; {ty.n}]>::try_from({body}).map_err(|__v: Vec<{elem}>| " + f'pyo3::exceptions::PyValueError::new_err(format!("expected {ty.n} values, got {{}}", __v.len())))?', + True, + ) + if k == "tuple": + parts = [self.conv_expr(t, f"{var}.{i}") for i, t in enumerate(ty.inner)] + if any(p is None for p in parts): + return None + fallible = any(f for _e, f in parts) + return "(" + ", ".join(e for e, _f in parts) + ")", fallible + if k == "opt": + inner = self.conv_expr(ty.inner[0], "__o") + if inner is None: + return None + expr, fallible = inner + if expr == "__o": + return var, False + if fallible: + elem = self.rust_type(ty.inner[0]) + return ( + f"match {var} {{ Some(__o) => Some({expr}), None => None }}", + True, + ) + return f"{var}.map(|__o| {expr})", False + return None + + def rust_type(self, ty: Ty) -> str | None: + """The library-side Rust type an owned conversion produces.""" + k = ty.kind + if k == "prim": + return ty.name + if k == "str": + return "String" + if k == "user": + if ty.name in IDENTIFIED: + return { + "complex": "rust_physics_engine::fractals::Complex", + "bigint": "rust_physics_engine::exact::bigint::BigInt", + "rational": "rust_physics_engine::exact::rational::Rational", + }[IDENTIFIED[ty.name]] + w = self.wrappers.get(ty.name) + return w.rust_path if w else None + if k == "vec": + inner = self.rust_type(ty.inner[0]) + return None if inner is None else f"Vec<{inner}>" + if k == "array": + inner = self.rust_type(ty.inner[0]) + return None if inner is None else f"[{inner}; {ty.n}]" + if k == "tuple": + parts = [self.rust_type(t) for t in ty.inner] + if any(p is None for p in parts): + return None + return "(" + ", ".join(parts) + ")" + if k == "opt": + inner = self.rust_type(ty.inner[0]) + return None if inner is None else f"Option<{inner}>" + return None + + def arg_plan(self, ty: Ty, name: str) -> dict | None: + """How to accept `ty` from Python. + + Returns `param` (the wrapper's parameter type), `pre` (statements + run before the call), `expr` (what is passed to the Rust routine), + `post` (statements run after it), `py` (the stub annotation) and + `owns` (whether the value is free of Python state, and so safe to + hold with the GIL released). + """ + if ty.kind == "callable": + return self._callable_arg(ty, name) + if ty.mutable: + return self._mut_arg(ty, name) + if ty.kind in ("unit", "bad", "selfty", "result", "iter"): + return None + if ty.kind == "user" and ty.by_ref and ty.name in self.wrappers: + w = self.wrappers[ty.name] + if not (w.clone or w.simple_enum): + # No `Clone`, so there is no owned value to make. The + # wrapper is borrowed for the duration of the call + # instead, which is what `&T` means anyway. + return dict( + param=f"pyo3::PyRef<'_, crate::generated::types::{w.ident}>", + pre=[], + expr=f"&{name}.inner", + post=[], + py=w.py_name, + owns=False, + ) + param = self.param_type(ty) + conv = self.conv_expr(ty, name) + if param is None or conv is None: + return None + expr, _fallible = conv + pre = [] if expr == name else [f"let {name} = {expr};"] + # An `&[T]` parameter takes a borrow of the owned vector; a `Vec` + # parameter takes it whole. Slices of references need one more + # step, because `&Vec>` is not `&[&[f64]]`. + call = self._borrow(ty, name, pre) + if call is None: + return None + return dict( + param=param, + pre=pre, + expr=call, + post=[], + py=self._py_of(ty), + owns=True, + ) + + def borrow_rust_type(self, ty: Ty) -> str | None: + """The Rust type as the callee writes it, references and all.""" + base = self.rust_type(dataclasses.replace(ty, by_ref=False, mutable=False)) + if ty.kind == "vec": + elem = self.borrow_rust_type(ty.inner[0]) + if elem is None: + return None + base = f"[{elem}]" if ty.by_ref else f"Vec<{elem}>" + elif ty.kind == "tuple": + parts = [self.borrow_rust_type(t) for t in ty.inner] + if any(p is None for p in parts): + return None + base = "(" + ", ".join(parts) + ")" + elif ty.kind == "opt": + inner = self.borrow_rust_type(ty.inner[0]) + if inner is None: + return None + base = f"Option<{inner}>" + elif ty.kind == "str" and ty.by_ref: + return "&str" + if base is None: + return None + return f"&{base}" if ty.by_ref else base + + def _needs_deep_borrow(self, ty: Ty, top: bool = True) -> bool: + """Whether anything below the outermost level is a reference. + + `&[f64]` does not need one: a `&Vec` coerces. `&[&str]` and + `&[(&str, Dim)]` do, because no coercion turns an owned collection + into a collection of borrows. + """ + if not top and ty.by_ref: + return True + return any(self._needs_deep_borrow(t, False) for t in ty.inner) + + def _place_expr(self, ty: Ty, place: str) -> str | None: + """The borrowed form of `ty`, given `place` names the owned value.""" + if ty.by_ref: + if ty.kind == "str": + return f"{place}.as_str()" + if ty.kind == "vec": + return f"{place}.as_slice()" + return f"&{place}" + if ty.kind == "prim": + return place + if ty.kind == "tuple": + parts = [self._place_expr(t, f"{place}.{i}") for i, t in enumerate(ty.inner)] + return None if any(p is None for p in parts) else "(" + ", ".join(parts) + ")" + if self.is_clone(ty): + return f"{place}.clone()" + return None + + def _borrow(self, ty: Ty, name: str, pre: list[str]) -> str | None: + """What to write at the call site, given `name` holds the owned value.""" + if ty.kind == "vec" and self._needs_deep_borrow(ty.inner[0], top=False): + elem_ty = self.borrow_rust_type(ty.inner[0]) + elem_expr = self._place_expr(ty.inner[0], "(*__b)") + if elem_ty is None or elem_expr is None: + return None + pre.append( + f"let {name}__b: Vec<{elem_ty}> = {name}.iter().map(|__b| {elem_expr}).collect();" + ) + return f"&{name}__b" if ty.by_ref else f"{name}__b" + if ty.kind == "tuple" and any( + self._needs_deep_borrow(t, top=False) or t.by_ref for t in ty.inner + ): + parts = [self._place_expr(t, f"{name}.{i}") for i, t in enumerate(ty.inner)] + if any(p is None for p in parts): + return None + inner = "(" + ", ".join(parts) + ")" + return f"&{inner}" if ty.by_ref else inner + if ty.kind == "opt" and ty.inner[0].by_ref: + inner = ty.inner[0] + if inner.kind == "vec": + take = "__o.as_slice()" + elif inner.kind == "str": + take = "__o.as_str()" + else: + take = "__o" + return f"{name}.as_ref().map(|__o| {take})" + if ty.kind == "array" and ( + ty.inner[0].by_ref or self._needs_deep_borrow(ty.inner[0], top=False) + ): + return None + return ty.amp(name) + + MUT_WRITEBACK = {"f64", "f32", "usize", "u8", "u16", "u32", "u64", "i8", "i16", "i32", "i64", "bool"} + + def _mut_arg(self, ty: Ty, name: str) -> dict | None: + """`&mut` arguments: an output written through the argument.""" + if ty.kind == "user": + w = self.wrappers.get(ty.name) + if w is None or w.simple_enum: + return None + return dict( + param=f"pyo3::PyRefMut<'_, crate::generated::types::{w.ident}>", + pre=[f"let mut {name} = {name};"], + expr=f"&mut {name}.inner", + post=[], + py=w.py_name, + owns=False, + ) + if ty.kind == "vec" and ty.inner[0].kind == "user": + elem = ty.inner[0] + param = self.param_type(elem) + conv = self.conv_expr(elem, "__e") + back = self._writeback_object(elem, "__e") + if param is None or conv is None or back is None or conv[1]: + return None + rust_elem = self.rust_type(elem) + return dict( + param="pyo3::Bound<'py, pyo3::PyAny>", + pre=[ + f"let mut {name}__v: Vec<{rust_elem}> = {name}.extract::>()?" + f".into_iter().map(|__e| {conv[0]}).collect();", + ], + expr=f"&mut {name}__v", + post=[ + f"crate::runtime::coerce::write_back_objects(&{name}, " + f"{name}__v.into_iter().map(|__e| {back}).collect::>())?;" + ], + py=f"MutableSequence[{self._py_of(elem)}]", + owns=False, + lifetime=True, + ) + if ty.kind == "vec" and ty.inner[0].kind == "prim" and ty.inner[0].name in self.MUT_WRITEBACK: + p = ty.inner[0].name + return dict( + param="pyo3::Bound<'py, pyo3::PyAny>", + pre=[ + f"let mut {name}__v: Vec<{p}> = {name}.extract()?;", + ], + expr=f"&mut {name}__v", + post=[f"crate::runtime::coerce::write_back(&{name}, &{name}__v)?;"], + py="MutableSequence[" + PRIMS[p] + "]", + owns=False, + lifetime=True, + ) + return None + + def _writeback_object(self, ty: Ty, var: str) -> str | None: + """An expression turning an owned Rust value back into a Python one.""" + if ty.kind != "user": + return None + if ty.name in IDENTIFIED: + return f"crate::runtime::coerce::Cx({var})" if IDENTIFIED[ty.name] == "complex" else None + w = self.wrappers.get(ty.name) + if w is None or w.simple_enum: + return None + return f"crate::generated::types::{w.ident} {{ inner: {var} }}" + + CALLABLE_FALLBACK = { + "f64": "f64::NAN", + "f32": "f32::NAN", + "bool": "false", + "usize": "0", + "u8": "0", + "u16": "0", + "u32": "0", + "u64": "0", + "i8": "0", + "i16": "0", + "i32": "0", + "i64": "0", + } + + def _callable_ret(self, ty: Ty) -> tuple[str, str, str] | None: + """(extracted type, fallback expression, expression turning it into the Rust type).""" + if ty.kind == "prim": + fb = self.CALLABLE_FALLBACK.get(ty.name) + return (ty.name, fb, "__r") if fb else None + if ty.kind == "unit": + return "()", "()", "__r" + if ty.kind == "str": + return "String", "String::new()", "__r" + if ty.kind == "vec" and ty.inner[0].kind == "prim": + return f"Vec<{ty.inner[0].name}>", "Vec::new()", "__r" + if ty.kind == "tuple" and all(t.kind == "prim" for t in ty.inner): + rust = "(" + ", ".join(t.name for t in ty.inner) + ")" + fb = "(" + ", ".join(self.CALLABLE_FALLBACK.get(t.name, "0") for t in ty.inner) + ")" + return rust, fb, "__r" + if ty.kind == "opt": + inner = self._callable_ret(ty.inner[0]) + if inner is None or inner[2] != "__r": + return None + return f"Option<{inner[0]}>", "None", "__r" + if ty.kind == "user": + if ty.name in IDENTIFIED and IDENTIFIED[ty.name] == "complex": + return ( + "crate::runtime::coerce::ComplexArg", + "crate::runtime::coerce::ComplexArg(rust_physics_engine::fractals::Complex::new(f64::NAN, f64::NAN))", + "__r.0", + ) + w = self.wrappers.get(ty.name) + if w is None or not w.clone: + return None + if w.coerce_n: + nan = ", ".join(f"{f.name}: f64::NAN" for f in w.item.fields) + return ( + f"crate::generated::types::{w.arg_ident}", + f"crate::generated::types::{w.arg_ident}({w.rust_path} {{ {nan} }})", + "__r.0", + ) + if w.coerce_seq: + fname = w.item.fields[0].name + return ( + f"crate::generated::types::{w.arg_ident}", + f"crate::generated::types::{w.arg_ident}({w.rust_path} {{ {fname}: Vec::new() }})", + "__r.0", + ) + return None + + def _callable_arg(self, ty: Ty, name: str) -> dict | None: + *argtys, retty = ty.inner + params, passed = [], [] + for i, a in enumerate(argtys): + v = f"__a{i}" + if a.kind == "prim": + params.append(f"{v}: {a.name}") + passed.append(v) + elif a.kind == "str": + params.append(f"{v}: &str") + passed.append(f"{v}.to_string()") + elif a.kind == "vec" and a.inner[0].kind == "prim": + params.append(f"{v}: &[{a.inner[0].name}]") + passed.append(f"{v}.to_vec()") + elif a.kind == "user" and a.name in IDENTIFIED and IDENTIFIED[a.name] == "complex": + params.append(f"{v}: rust_physics_engine::fractals::Complex") + passed.append(f"crate::runtime::coerce::Cx({v})") + elif a.kind == "user" and a.name in self.wrappers and self.wrappers[a.name].clone: + w = self.wrappers[a.name] + ref = "&" if a.by_ref else "" + params.append(f"{v}: {ref}{w.rust_path}") + passed.append( + f"crate::generated::types::{w.ident} {{ inner: {v}{'.clone()' if a.by_ref else ''} }}" + ) + else: + return None + ret = self._callable_ret(retty) + if ret is None: + return None + extracted, fallback, unwrap = ret + rust_ret = self.rust_type(retty) if retty.kind != "unit" else "()" + if rust_ret is None: + return None + args_tuple = f"({', '.join(passed)},)" if len(passed) == 1 else f"({', '.join(passed)})" + body = f"__cb_{name}.call::<_, {extracted}>({args_tuple}, {fallback})" + if unwrap != "__r": + body = f"{{ let __r = {body}; {unwrap} }}" + # The closure owns a handle rather than borrowing one, because + # some of these arguments carry a `+ 'static` bound and a borrow + # of a local cannot satisfy it. The wrapper keeps its own handle + # so that it can still ask, after the call, whether the callable + # raised. + body = body.replace(f"__cb_{name}", "__cb") + pre = [ + f"let __cb_{name} = std::rc::Rc::new(crate::runtime::Callback::new({name}));", + f"let {name} = {{ let __cb = __cb_{name}.clone(); " + f"move |{', '.join(params)}| -> {rust_ret} {{ {body} }} }};", + ] + py_sig = ( + "Callable[[" + ", ".join(self._py_of(a) for a in argtys) + "], " + self._py_of(retty) + "]" + ) + return dict( + param="pyo3::Py", + pre=pre, + expr=ty.amp(name), + post=[], + py=py_sig, + owns=False, + callback=f"__cb_{name}", + ) + + def _coerce_py(self, w: Wrapper) -> str: + if w.coerce_rows: + return f"{w.py_name} | Sequence[Sequence[float]]" + return f"{w.py_name} | Sequence[float]" + + def _py_of(self, ty: Ty) -> str: + if ty.kind == "prim": + return PRIMS[ty.name] + if ty.kind == "str": + return "str" + if ty.kind == "unit": + return "None" + if ty.kind in ("vec", "array", "iter"): + return f"list[{self._py_of(ty.inner[0])}]" + if ty.kind == "tuple": + return "tuple[" + ", ".join(self._py_of(t) for t in ty.inner) + "]" + if ty.kind == "opt": + return f"Optional[{self._py_of(ty.inner[0])}]" + if ty.kind == "result": + return self._py_of(ty.inner[0]) + if ty.kind == "callable": + *a, r = ty.inner + return "Callable[[" + ", ".join(self._py_of(x) for x in a) + "], " + self._py_of(r) + "]" + if ty.kind == "user": + if ty.name in IDENTIFIED: + return {"complex": "complex", "bigint": "int", "rational": "Fraction"}[ + IDENTIFIED[ty.name] + ] + w = self.wrappers.get(ty.name) + if w is None: + return "Any" + if w.coerce_n or w.coerce_rows or w.coerce_seq: + return self._coerce_py(w) + return w.py_name + return "Any" + + # ── return plans ──────────────────────────────────────────────── + + def ret_plan(self, ty: Ty, expr: str) -> dict | None: + """How to hand `ty` back to Python. `expr` names the Rust value.""" + k = ty.kind + if ty.by_ref and k not in ("str", "unit"): + # A returned reference cannot outlive the call on the Python + # side, so it is copied out. `&mut` is different: it exists to + # be written through, and a copy would look like it worked. + if ty.mutable: + return None + base = dataclasses.replace(ty, by_ref=False, mutable=False) + if k == "prim": + return self.ret_plan(base, f"(*{expr})") + if not self.is_clone(base): + return None + if k == "vec": + return self.ret_plan(base, f"{expr}.to_vec()") + return self.ret_plan(base, f"{expr}.clone()") + if k == "iter": + return self.ret_plan(Ty("vec", ty.inner), f"{expr}.collect::>()") + if k == "unit": + return dict(rust="()", conv="()", py="None", fallible=False) + if k == "prim": + return dict(rust=ty.name, conv=expr, py=PRIMS[ty.name], fallible=False) + if k == "str": + return dict(rust="String", conv=f"{expr}.to_string()", py="str", fallible=False) + if k == "user": + return self._user_ret(ty.name, expr) + if k == "vec": + inner = self.ret_plan(ty.inner[0], "__x") + if inner is None: + return None + if inner["conv"] == "__x": + return dict( + rust=f"Vec<{inner['rust']}>", + conv=expr, + py=f"list[{inner['py']}]", + fallible=False, + ) + if inner["fallible"]: + return dict( + rust=f"Vec<{inner['rust']}>", + conv=( + f"{expr}.into_iter()" + f".map(|__x| -> PyResult<{inner['rust']}> {{ Ok({inner['conv']}) }})" + ".collect::>>()?" + ), + py=f"list[{inner['py']}]", + fallible=True, + ) + return dict( + rust=f"Vec<{inner['rust']}>", + conv=f"{expr}.into_iter().map(|__x| {inner['conv']}).collect::>()", + py=f"list[{inner['py']}]", + fallible=False, + ) + if k == "array": + inner = self.ret_plan(ty.inner[0], "__x") + if inner is None: + return None + if inner["conv"] == "__x": + return dict( + rust=f"Vec<{inner['rust']}>", + conv=f"{expr}.to_vec()", + py=f"list[{inner['py']}]", + fallible=False, + ) + if inner["fallible"]: + return None + return dict( + rust=f"Vec<{inner['rust']}>", + conv=f"{expr}.into_iter().map(|__x| {inner['conv']}).collect::>()", + py=f"list[{inner['py']}]", + fallible=False, + ) + if k == "tuple": + plans = [self.ret_plan(t, f"{expr}.{i}") for i, t in enumerate(ty.inner)] + if any(p is None for p in plans): + return None + return dict( + rust="(" + ", ".join(p["rust"] for p in plans) + ")", + conv="(" + ", ".join(p["conv"] for p in plans) + ")", + py="tuple[" + ", ".join(p["py"] for p in plans) + "]", + fallible=any(p["fallible"] for p in plans), + ) + if k == "opt": + elem = ty.inner[0] + if elem.by_ref and not elem.mutable and elem.kind == "vec": + base = dataclasses.replace(elem, by_ref=False, mutable=False) + inner = self.ret_plan(base, "__x.to_vec()") if self.is_clone(base) else None + else: + inner = self.ret_plan(elem, "__x") + if inner is None: + return None + if inner["fallible"]: + return dict( + rust=f"Option<{inner['rust']}>", + conv=f"match {expr} {{ Some(__x) => Some({inner['conv']}), None => None }}", + py=f"Optional[{inner['py']}]", + fallible=True, + ) + return dict( + rust=f"Option<{inner['rust']}>", + conv=f"{expr}.map(|__x| {inner['conv']})", + py=f"Optional[{inner['py']}]", + fallible=False, + ) + return None + + ERR_MAP = { + "GeomError": "crate::runtime::map_geom", + "SolveError": "crate::runtime::map_solve", + "DimError": "crate::runtime::errors::map_dim", + # These carry a message, so `Display` is the right rendering. + "TooManyErrors": "crate::runtime::errors::map_display", + "NegativeCycle": "crate::runtime::errors::map_display", + "Error": "crate::runtime::errors::map_display", + "String": "crate::runtime::errors::map_display", + # An unnamed error type: `io::Result` and friends. + "": "crate::runtime::errors::map_display", + } + + def _user_ret(self, path: str, expr: str) -> dict | None: + if path in IDENTIFIED: + kind = IDENTIFIED[path] + if kind == "complex": + return dict( + rust="pyo3::Bound<'py, pyo3::types::PyComplex>", + conv=f"crate::runtime::coerce::complex_out(py, {expr})", + py="complex", + fallible=False, + ) + if kind == "bigint": + return dict( + rust="pyo3::Bound<'py, pyo3::PyAny>", + conv=f"crate::runtime::coerce::bigint_out(py, &{expr})?", + py="int", + fallible=True, + ) + if kind == "rational": + return dict( + rust="pyo3::Bound<'py, pyo3::PyAny>", + conv=f"crate::runtime::coerce::rational_out(py, &{expr})?", + py="Fraction", + fallible=True, + ) + w = self.wrappers.get(path) + if w is None: + return None + if w.simple_enum: + return dict( + rust=f"crate::generated::types::{w.ident}", + conv=f"crate::generated::types::{w.ident}::from_rust(&{expr})", + py=w.py_name, + fallible=False, + ) + return dict( + rust=f"crate::generated::types::{w.ident}", + conv=f"crate::generated::types::{w.ident} {{ inner: {expr} }}", + py=w.py_name, + fallible=False, + ) + + + +# ── Hand-written supplements ──────────────────────────────────────────── + +# Methods that no generator would produce, injected verbatim into a +# class's `#[pymethods]` block. PyO3 allows one such block per class, so +# extras cannot live in a file of their own; they are spliced in here. +# Everything below is either a Python protocol the Rust type has no +# equivalent for (`__len__`, `__getitem__`, `__iter__`) or a conversion +# out of the wrapper and into a plain Python value. +EXTRAS: dict[str, list[str]] = { + "math::Vec2": [ + 'fn __len__(&self) -> usize { 2 }', + 'fn __iter__(slf: pyo3::PyRef<\'_, Self>) -> PyResult> {' + ' let v = vec![slf.inner.x, slf.inner.y];' + ' Ok(v.into_pyobject(slf.py())?.try_iter()?.unbind().into_any()) }', + 'fn __getitem__(&self, i: isize) -> PyResult {' + ' match i { 0 | -2 => Ok(self.inner.x), 1 | -1 => Ok(self.inner.y),' + ' _ => Err(pyo3::exceptions::PyIndexError::new_err("Vec2 index out of range")) } }', + '/// The components as a plain list.\n fn tolist(&self) -> Vec { vec![self.inner.x, self.inner.y] }', + ], + "math::Vec3": [ + 'fn __len__(&self) -> usize { 3 }', + 'fn __iter__(slf: pyo3::PyRef<\'_, Self>) -> PyResult> {' + ' let v = vec![slf.inner.x, slf.inner.y, slf.inner.z];' + ' Ok(v.into_pyobject(slf.py())?.try_iter()?.unbind().into_any()) }', + 'fn __getitem__(&self, i: isize) -> PyResult {' + ' match i { 0 | -3 => Ok(self.inner.x), 1 | -2 => Ok(self.inner.y), 2 | -1 => Ok(self.inner.z),' + ' _ => Err(pyo3::exceptions::PyIndexError::new_err("Vec3 index out of range")) } }', + '/// The components as a plain list.\n fn tolist(&self) -> Vec {' + ' vec![self.inner.x, self.inner.y, self.inner.z] }', + ], + "manifold::vecn::VecN": [ + 'fn __len__(&self) -> usize { self.inner.data.len() }', + 'fn __getitem__(&self, i: isize) -> PyResult {' + ' let n = self.inner.data.len() as isize; let j = if i < 0 { i + n } else { i };' + ' if j < 0 || j >= n { return Err(pyo3::exceptions::PyIndexError::new_err("VecN index out of range")); }' + ' Ok(self.inner.data[j as usize]) }', + 'fn __iter__(slf: pyo3::PyRef<\'_, Self>) -> PyResult> {' + ' let v = slf.inner.data.clone();' + ' Ok(v.into_pyobject(slf.py())?.try_iter()?.unbind().into_any()) }', + '/// The components as a plain list.\n fn tolist(&self) -> Vec { self.inner.data.clone() }', + ], + "linalg::matrix::Matrix": [ + 'fn __len__(&self) -> usize { self.inner.rows }', + '/// `m[i, j]`, or `m[i]` for a whole row.\n ' + 'fn __getitem__(&self, py: Python<\'_>, key: pyo3::Py) -> PyResult> {' + ' let k = key.bind(py);' + ' if let Ok((i, j)) = k.extract::<(isize, isize)>() {' + ' let (r, c) = (self.wrap_row(i)?, self.wrap_col(j)?);' + ' return Ok(self.inner.data[r * self.inner.cols + c].into_pyobject(py)?.unbind().into_any()); }' + ' let i = k.extract::().map_err(|_| pyo3::exceptions::PyTypeError::new_err(' + ' "index a Matrix with m[i, j] or m[i]"))?;' + ' let r = self.wrap_row(i)?;' + ' let row: Vec = self.inner.data[r * self.inner.cols..(r + 1) * self.inner.cols].to_vec();' + ' Ok(row.into_pyobject(py)?.unbind().into_any()) }', + '/// `m[i, j] = v`.\n ' + 'fn __setitem__(&mut self, key: (isize, isize), v: f64) -> PyResult<()> {' + ' let (r, c) = (self.wrap_row(key.0)?, self.wrap_col(key.1)?);' + ' let cols = self.inner.cols; self.inner.data[r * cols + c] = v; Ok(()) }', + '/// The rows as a list of lists.\n fn tolist(&self) -> Vec> {' + ' self.inner.data.chunks(self.inner.cols).map(<[f64]>::to_vec).collect() }', + '/// `(rows, cols)`.\n #[getter]\n fn shape(&self) -> (usize, usize) {' + ' (self.inner.rows, self.inner.cols) }', + ], + "quaternion::Quaternion": [ + 'fn __len__(&self) -> usize { 4 }', + '/// `(w, x, y, z)` as a plain list.\n fn tolist(&self) -> Vec {' + ' vec![self.inner.w, self.inner.x, self.inner.y, self.inner.z] }', + ], +} + +# Private helpers on the wrapper struct (outside `#[pymethods]`). +PRIVATE_IMPLS: dict[str, str] = { + "linalg::matrix::Matrix": """ +impl PyMatrix { + fn wrap_row(&self, i: isize) -> PyResult { + let n = self.inner.rows as isize; + let j = if i < 0 { i + n } else { i }; + if j < 0 || j >= n { + return Err(pyo3::exceptions::PyIndexError::new_err("row index out of range")); + } + Ok(j as usize) + } + fn wrap_col(&self, i: isize) -> PyResult { + let n = self.inner.cols as isize; + let j = if i < 0 { i + n } else { i }; + if j < 0 || j >= n { + return Err(pyo3::exceptions::PyIndexError::new_err("column index out of range")); + } + Ok(j as usize) + } +} +""", +} + +# Operator traits worth exposing as Python dunders. +DUNDERS = { + ("Add", "add"): "__add__", + ("Sub", "sub"): "__sub__", + ("Mul", "mul"): "__mul__", + ("Div", "div"): "__truediv__", + ("Neg", "neg"): "__neg__", +} + + +def _without_detach(plan: dict) -> dict: + """Turn off GIL release, and stop asking for a `py` nobody now uses.""" + needs_py = bool( + re.search(r"\bpy\b", plan["ret"]["conv"]) + or re.search(r"\bpy\b", " ".join(plan["pre"])) + ) + return dict(plan, detach=False, needs_py=needs_py) + + +def sanitize_py(name: str) -> str: + name = name.lstrip("_") or "arg" + if name in PY_KEYWORDS: + return name + "_" + return name + + +def sanitize_rust(name: str) -> str: + if name in RUST_KEYWORDS: + return "r#" + name if name not in ("self", "super", "crate", "Self") else name + "_" + return name + + +def clean_markup(text: str) -> str: + """Strip the Rust-specific markup from a doc comment.""" + # rustdoc disambiguators: `[`mod@cholesky`]` names a module, not a + # function; the prefix means nothing outside rustdoc. + text = re.sub( + r"\b(?:mod|fn|struct|enum|trait|type|macro|value|derive|prim|const|static|union)@", + "", + text, + ) + text = re.sub(r"\[`([^`]+)`\]\([^)]*\)", r"`\1`", text) + text = re.sub(r"\[`([^`]+)`\]", r"`\1`", text) + text = re.sub(r"\[([^\]\[]+)\]\([^)]*\)", r"\1", text) + return text.replace("crate::", "") + + +def pydoc(doc: str, rust_path: str) -> list[str]: + """Rust doc comment -> Python docstring lines.""" + out: list[str] = [] + fenced = False + for line in doc.splitlines(): + stripped = line.strip() + if stripped.startswith("```"): + fenced = not fenced + continue + if fenced: + continue + if stripped.startswith("# "): + out.append(stripped[2:].rstrip(".") + ":") + continue + out.append(line) + text = clean_markup("\n".join(out)) + lines = [ln.rstrip() for ln in text.splitlines()] + while lines and not lines[0].strip(): + lines.pop(0) + while lines and not lines[-1].strip(): + lines.pop() + lines.append("") + lines.append(f"Rust: `{rust_path}`") + return lines + + +def rust_doc_lines(lines: list[str]) -> str: + out = [] + for ln in lines: + ln = ln.replace("\r", "") + out.append("/// " + ln if ln else "///") + return "\n".join(out) + + +def py_doc_literal(lines: list[str], indent: str) -> str: + body = "\n".join(lines).replace("\\", "\\\\").replace('"""', '\\"\\"\\"') + if body.endswith('"'): + body += " " + return f'{indent}"""' + ("\n" + body + "\n" + indent if body else "") + '"""' + + +# ── Emission ──────────────────────────────────────────────────────────── + + +@dataclass +class Emitted: + py_name: str + code: str + stub: str + kind: str = "function" + + +LIFETIME_ONLY = re.compile(r"^<\s*(?:'[A-Za-z_][A-Za-z0-9_]*\s*,?\s*)*>$") + + +class Emitter(Generator): + """Turns the plans in `Generator` into Rust and `.pyi` text.""" + + def __init__(self) -> None: + super().__init__() + self.by_module: dict[str, list[Emitted]] = {} + self.class_of_module: dict[str, list[Wrapper]] = {} + self.consts_of_module: dict[str, list] = {} + self.class_by_name: dict[str, list[Wrapper]] = {} + for w in self.wrappers.values(): + self.class_by_name.setdefault(w.py_name, []).append(w) + self.class_of_module.setdefault(w.item.module, []).append(w) + + # ── shared call planning ──────────────────────────────────────── + + def _self_subst(self, text: str, owner: object | None) -> str: + if owner is None: + return text + return re.sub(r"\bSelf\b", f"crate::{owner.path}", text) + + def _plan(self, fn, owner=None): + """Plan one call. Returns a dict, or a string naming why it cannot be bound.""" + if fn.is_unsafe: + return "unsafe fn" + if fn.generics.strip() and not LIFETIME_ONLY.match(fn.generics.strip()): + return f"generic ({fn.generics.strip()})" + if fn.where_clause.strip() and not re.match( + r"^(?:'[A-Za-z_][A-Za-z0-9_]*\s*:[^,]*,?\s*)*$", fn.where_clause.strip() + ): + return "where clause" + + params, pre, post, callargs, annots, callbacks = [], [], [], [], [], [] + owns = True + seen: set[str] = set() + heavy = False + for pat, ty_text in fn.args: + name = pat.replace("mut ", "").strip() + if not re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", name): + return f"argument pattern `{pat}`" + py_name = sanitize_py(name) + if py_name in seen: + return f"duplicate argument `{py_name}`" + seen.add(py_name) + ty = parse_type(self._self_subst(ty_text, owner), self.res, fn.file, fn.module) + if ty.kind == "bad": + return f"argument `{name}: {ty_text}`" + plan = self.arg_plan(ty, py_name) + if plan is None: + return f"argument `{name}: {ty_text}`" + params.append(f"{py_name}: {plan['param']}") + pre.extend(plan["pre"]) + post.extend(plan.get("post", [])) + callargs.append(plan["expr"]) + annots.append((py_name, plan["py"], ty)) + owns = owns and plan["owns"] + if "callback" in plan: + callbacks.append(plan["callback"]) + if ty.kind in ("vec", "array") or (ty.kind == "user" and ty.name in self.wrappers + and self.wrappers[ty.name].coerce_rows): + heavy = True + + ret_text = self._self_subst(fn.ret, owner) + ret_ty = parse_type(ret_text, self.res, fn.file, fn.module) if ret_text else Ty("unit") + if ret_ty.kind == "bad": + return f"return type `{fn.ret}`" + err_map = None + if ret_ty.kind == "result": + err_map = self.ERR_MAP.get(ret_ty.name, "crate::runtime::errors::map_debug") + ret_ty = ret_ty.inner[0] + rp = self.ret_plan(ret_ty, "__v") + if rp is None: + return f"return type `{fn.ret}`" + if ret_ty.kind in ("vec", "array"): + heavy = True + + needs_py = bool(re.search(r"\bpy\b", rp["conv"]) or re.search(r"\bpy\b", " ".join(pre))) + plain_ret = self._is_plain(ret_ty) + detach = owns and not callbacks and plain_ret and heavy + return dict( + params=params, + pre=pre, + post=post, + callargs=callargs, + annots=annots, + callbacks=callbacks, + ret=rp, + err_map=err_map, + detach=detach, + needs_py=needs_py or detach, + lifetime="'py" in rp["rust"] or any("'py" in p for p in params), + ) + + def _is_plain(self, ty: Ty) -> bool: + if ty.kind in ("prim", "str", "unit"): + return True + if ty.kind in ("vec", "array", "opt"): + return self._is_plain(ty.inner[0]) + if ty.kind == "tuple": + return all(self._is_plain(t) for t in ty.inner) + return False + + def _body(self, plan, callee: str) -> str: + lines: list[str] = [] + lines.extend(plan["pre"]) + call = f"{callee}({', '.join(plan['callargs'])})" + if plan.get("discard") == "unit": + # A builder returns `&mut Self` for chaining. Keeping that + # borrow alive past the call would stop the wrapper being + # handed back, and the mutation is the whole point anyway. + call = f"{{ {call}; }}" + elif plan.get("discard") == "result": + # The fallible form: the error still matters, the borrow does + # not, and dropping it inside the closure ends it there. + call = f"{call}.map(|_| ())" + mv = "move " if plan["detach"] else "" + guarded = f"crate::runtime::guard({mv}|| {call})" + if plan["detach"]: + guarded = f"py.detach(move || {guarded})" + lines.append(f"let __r = {guarded};") + if plan["callbacks"]: + refs = ", ".join(f"&{c}" for c in plan["callbacks"]) + lines.append(f"crate::runtime::callback::check(&[{refs}], ())?;") + lines.append( + "let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?;" + ) + if plan["err_map"]: + lines.append(f"let __v = __v.map_err({plan['err_map']})?;") + lines.extend(plan.get("post", [])) + lines.append(f"Ok({plan['ret']['conv']})") + return "\n".join(" " + ln for ln in lines) + + def _signature_attr(self, annots) -> str: + parts = [] + for i, (name, _py, ty) in enumerate(annots): + trailing = all(a[2].kind == "opt" for a in annots[i:]) + if ty.kind == "opt" and trailing: + parts.append(f"{name}=None") + else: + parts.append(name) + return "(" + ", ".join(parts) + ")" + + def _stub_sig(self, annots, ret_py: str) -> str: + parts = [] + for i, (name, py, ty) in enumerate(annots): + trailing = all(a[2].kind == "opt" for a in annots[i:]) + parts.append(f"{name}: {py} = None" if (ty.kind == "opt" and trailing) else f"{name}: {py}") + return "(" + ", ".join(parts) + ") -> " + ret_py + + # ── free functions ────────────────────────────────────────────── + + def emit_free(self, fn) -> Emitted | None: + plan = self._plan(fn) + if isinstance(plan, str): + self.skipped.append((fn.module, fn.name + "()", plan)) + return None + py_name = sanitize_py(fn.name) + ident = "pyfn_" + fn.name + rust_path = f"rust_physics_engine::{fn.module}::{fn.name}" if fn.module else f"rust_physics_engine::{fn.name}" + doc = pydoc(fn.doc, f"{fn.module}::{fn.name}") + params = plan["params"] + head_params = [] + if plan["needs_py"]: + head_params.append("py: Python<'py>") + head_params.extend(params) + lt = "<'py>" if (plan["needs_py"] or plan["lifetime"]) else "" + code = "\n".join( + [ + rust_doc_lines(doc), + "#[pyfunction]", + f'#[pyo3(name = "{py_name}", signature = {self._signature_attr(plan["annots"])})]', + f"pub fn {ident}{lt}({', '.join(head_params)}) -> PyResult<{plan['ret']['rust']}> {{", + self._body(plan, rust_path), + "}", + ] + ) + stub = "\n".join( + [ + f"def {py_name}{self._stub_sig(plan['annots'], plan['ret']['py'])}:", + py_doc_literal(doc, " "), + " ...", + ] + ) + return Emitted(py_name=py_name, code=code, stub=stub) + + def emit_identified_method(self, fn, path: str) -> Emitted | None: + """A method of `BigInt`, `Rational` or `Complex`, as a free function. + + These three types cross over as `int`, `Fraction` and `complex`, + so there is no class to hang their methods on -- but the methods + are not all redundant. Python has `math.factorial` and three- + argument `pow`; it has no integer `nth_root`, no + `is_perfect_square`, and no `to_continued_fraction`. Each becomes + a function in the module the type is defined in, with the receiver + as its first argument. + """ + owner = self.res.by_path.get(path) + if owner is None: + return None + if fn.self_kind == "&mut self": + # The receiver is a Python `int`, `Fraction` or `complex`, all + # immutable. A method that works by mutating the value in place + # has nowhere to put the result. + self.skipped.append( + (fn.module, f"{fn.impl_type}.{fn.name}()", "mutates an immutable Python type") + ) + return None + plan = self._plan(fn, owner=owner) + if isinstance(plan, str): + self.skipped.append((fn.module, f"{fn.impl_type}.{fn.name}()", plan)) + return None + recv_ty = Ty("user", name=path, by_ref=fn.self_kind != "self") + recv_name = {"complex": "z", "bigint": "n", "rational": "q"}[IDENTIFIED[path]] + # `BigInt::nth_root(&self, n: u32)` already uses the short name, so + # the receiver falls back to the type's own. + if any(p.split(":")[0].strip() == recv_name for p in plan["params"]): + recv_name = {"complex": "z_value", "bigint": "bigint", "rational": "rational"}[ + IDENTIFIED[path] + ] + if fn.self_kind: + recv = self.arg_plan(recv_ty, recv_name) + if recv is None: + return None + plan = dict( + plan, + params=[f"{recv_name}: {recv['param']}"] + plan["params"], + pre=recv["pre"] + plan["pre"], + callargs=plan["callargs"], + annots=[(recv_name, recv["py"], recv_ty)] + plan["annots"], + ) + callee = f"{recv['expr'].lstrip('&')}.{fn.name}" + else: + callee = f"{self.rust_type(Ty('user', name=path))}::{fn.name}" + py_name = sanitize_py(fn.name) + ident = f"pyfn_{IDENTIFIED[path]}_{fn.name}" + doc = pydoc(fn.doc, f"{path}::{fn.name}") + head = [] + if plan["needs_py"]: + head.append("py: Python<'py>") + head.extend(plan["params"]) + lt = "<'py>" if (plan["needs_py"] or plan["lifetime"]) else "" + code = "\n".join( + [ + rust_doc_lines(doc), + "#[pyfunction]", + f'#[pyo3(name = "{py_name}", signature = {self._signature_attr(plan["annots"])})]', + f"pub fn {ident}{lt}({', '.join(head)}) -> PyResult<{plan['ret']['rust']}> {{", + self._body(plan, callee), + "}", + ] + ) + stub = "\n".join( + [ + f"def {py_name}{self._stub_sig(plan['annots'], plan['ret']['py'])}:", + py_doc_literal(doc, " "), + " ...", + ] + ) + return Emitted(py_name=py_name, code=code, stub=stub) + + # ── classes ───────────────────────────────────────────────────── + + def emit_class(self, w: Wrapper) -> tuple[str, str]: + """Returns (rust code, stub text) for one wrapper class.""" + item = w.item + doc = pydoc(item.doc, item.path) + flags = [f'name = "{w.py_name}"', f'module = "{w.py_module}"'] + if w.clone: + flags.append("from_py_object") + if w.partial_eq or w.simple_enum: + flags.append("eq") + if w.simple_enum: + flags.append("eq_int") + if w.unsendable: + flags.append("unsendable") + derives = ["Clone"] if (w.clone or w.simple_enum) else [] + if w.simple_enum: + derives += ["Copy", "PartialEq"] + elif w.partial_eq: + derives.append("PartialEq") + + rust: list[str] = [rust_doc_lines(doc)] + rust.append(f"#[pyclass({', '.join(flags)})]") + if derives: + rust.append(f"#[derive({', '.join(derives)})]") + if w.simple_enum: + rust.append(f"pub enum {w.ident} {{") + for v in item.variants: + rust.append(f" {v.name},") + rust.append("}") + rust.append(f"impl {w.ident} {{") + rust.append(f" pub fn to_rust(&self) -> {w.rust_path} {{ match self {{") + for v in item.variants: + rust.append(f" Self::{v.name} => {w.rust_path}::{v.name},") + rust.append(" } }") + rust.append(f" pub fn from_rust(v: &{w.rust_path}) -> Self {{ match v {{") + for v in item.variants: + rust.append(f" {w.rust_path}::{v.name} => Self::{v.name},") + rust.append(" } }") + rust.append("}") + else: + rust.append(f"pub struct {w.ident} {{ pub inner: {w.rust_path} }}") + + rust.append(PRIVATE_IMPLS.get(item.path, "").strip()) + methods, stub_methods = self.emit_methods(w) + rust.append(f"#[pymethods]\nimpl {w.ident} {{\n" + "\n\n".join(methods) + "\n}") + + if w.coerce_n or w.coerce_rows or w.coerce_seq: + rust.append(self._arg_adapter(w)) + + stub = [f"class {w.py_name}:", py_doc_literal(doc, " ")] + stub.extend(stub_methods or [" ..."]) + return "\n".join(x for x in rust if x), "\n".join(stub) + + def _arg_adapter(self, w: Wrapper) -> str: + item = w.item + if w.coerce_n: + names = [f.name for f in item.fields] + build = ", ".join(f"{n}: __v[{i}]" for i, n in enumerate(names)) + body = ( + f" let __v = crate::runtime::coerce::floats_exact(obj, {w.coerce_n}, " + f'"{w.py_name}")?;\n' + f" Ok({w.arg_ident}({w.rust_path} {{ {build} }}))" + ) + elif w.coerce_rows: + body = ( + f' let __rows = crate::runtime::coerce::rows(obj, "{w.py_name}")?;\n' + " let __refs: Vec<&[f64]> = __rows.iter().map(Vec::as_slice).collect();\n" + f" {w.rust_path}::from_rows(&__refs)\n" + f" .map({w.arg_ident})\n" + " .map_err(crate::runtime::map_solve)" + ) + else: + field_name = item.fields[0].name + body = ( + " let __v: Vec = obj.extract().map_err(|_| " + f'pyo3::exceptions::PyTypeError::new_err("{w.py_name} expects a sequence of floats"))?;\n' + f" Ok({w.arg_ident}({w.rust_path} {{ {field_name}: __v }}))" + ) + return f""" +/// A `{w.py_name}` argument, or anything that can stand in for one. +pub struct {w.arg_ident}(pub {w.rust_path}); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for {w.arg_ident} {{ + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result {{ + if let Ok(__w) = obj.extract::<{w.ident}>() {{ + return Ok({w.arg_ident}(__w.inner)); + }} +{body} + }} +}} +""" + + def emit_methods(self, w: Wrapper) -> tuple[list[str], list[str]]: + item = w.item + out: list[str] = [] + stubs: list[str] = [] + taken: set[str] = set() + + methods = [ + f + for f in self.crate.funcs + if f.impl_type == item.name and f.module == item.module and not f.impl_trait + ] + # `new` is the constructor whether it returns `Self` or + # `Result`; the fallible form is common enough here that + # missing it would leave a third of the classes unconstructible. + ctor_ret = re.compile( + rf"^(?:Self|{re.escape(item.name)}" + rf"|Result\s*<\s*(?:Self|{re.escape(item.name)})\s*,.*>)$" + ) + ctor = next( + ( + f + for f in methods + if f.name == "new" and not f.self_kind and ctor_ret.match(f.ret.strip()) + ), + None, + ) + + # A constructor from the fields, when the type has no `new` and its + # fields are all public and all bindable. + if ctor is None and not w.is_enum and item.kind == "named" and item.fields: + plans = [] + ok = all(f.public for f in item.fields) + for f in item.fields: + ty = parse_type(f.ty, self.res, item.file, item.module) + if ty.kind in ("callable", "bad") or ty.mutable or ty.by_ref: + ok = False + break + p = self.arg_plan(ty, sanitize_py(f.name)) + if p is None: + ok = False + break + plans.append((sanitize_py(f.name), f.name, p, ty)) + if ok and plans: + params = ", ".join(f"{n}: {p['param']}" for n, _r, p, _t in plans) + pre = "\n".join(" " + s for _n, _r, p, _t in plans for s in p["pre"]) + build = ", ".join( + f"{r}: {p['expr'].lstrip('&')}" if not t.by_ref else f"{r}: {p['expr']}" + for _n, r, p, t in plans + ) + sig = ", ".join(n for n, _r, _p, _t in plans) + fallible = any("?" in st for _n, _r, p, _t in plans for st in p["pre"]) + ret = "PyResult" if fallible else "Self" + value = f"Self {{ inner: {w.rust_path} {{ {build} }} }}" + value = f"Ok({value})" if fallible else value + out.append( + f" /// Builds a `{w.py_name}` from its fields.\n" + f" #[new]\n #[pyo3(signature = ({sig}))]\n" + f" fn __new__({params}) -> {ret} {{\n{pre}\n" + f" {value}\n }}" + ) + stubs.append( + " def __init__(self, " + + ", ".join(f"{n}: {p['py']}" for n, _r, p, _t in plans) + + ") -> None: ..." + ) + taken.add("__new__") + + for fn in methods: + name = sanitize_py(fn.name) + if name in taken: + continue + is_ctor = fn is ctor + # A builder -- `&mut self` in, `&mut Self` out, for chaining. + # Python gets the same object back, so `c.h(0).cx(0, 1)` reads + # as it does in Rust. + builder = False + if fn.self_kind == "&mut self": + own = re.escape(item.name) + plain = re.match(rf"^&\s*mut\s+(?:Self|{own})$", fn.ret.strip()) + wrapped = re.match( + rf"^Result\s*<\s*&\s*mut\s+(?:Self|{own})\s*,\s*(.+)>$", fn.ret.strip() + ) + if plain: + fn = dataclasses.replace(fn, ret="") + builder = True + elif wrapped: + fn = dataclasses.replace(fn, ret=f"Result<(), {wrapped.group(1)}>") + builder = True + plan = self._plan(fn, owner=item) + if isinstance(plan, str): + self.skipped.append((item.module, f"{item.name}.{fn.name}()", plan)) + continue + if w.unsendable and fn.self_kind: + plan = _without_detach(plan) + taken.add(name) + doc = pydoc(fn.doc, f"{item.path}::{fn.name}") + head: list[str] = [] + recv = "" + if builder: + plan = dict( + _without_detach(plan), + discard="result" if plan["err_map"] else "unit", + ) + head.append("mut slf: pyo3::PyRefMut<'py, Self>") + recv = "slf.inner." + elif fn.self_kind == "&self": + head.append("&self") + recv = "self.to_rust()." if w.simple_enum else "self.inner." + elif fn.self_kind == "&mut self": + if w.simple_enum: + self.skipped.append( + (item.module, f"{item.name}.{fn.name}()", "mutates a unit-variant enum") + ) + continue + head.append("&mut self") + recv = "self.inner." + elif fn.self_kind == "self": + if w.simple_enum: + head.append("&self") + recv = "self.to_rust()." + elif not w.clone: + self.skipped.append( + (item.module, f"{item.name}.{fn.name}()", "takes self by value, not Clone") + ) + continue + else: + head.append("&self") + recv = "self.inner.clone()." + if plan["needs_py"] and not builder: + head.append("py: Python<'py>") + head.extend(plan["params"]) + lt = "<'py>" if (plan["needs_py"] or plan["lifetime"] or builder) else "" + attrs = [] + if is_ctor: + attrs.append(" #[new]") + elif not fn.self_kind: + attrs.append(" #[staticmethod]") + attrs.append(f' #[pyo3(signature = {self._signature_attr(plan["annots"])})]') + if is_ctor: + callee = f"{w.rust_path}::{fn.name}" + body = self._body(plan, callee) + ident = "__new__" + else: + callee = f"{recv}{fn.name}" if recv else f"{w.rust_path}::{fn.name}" + body = self._body(plan, callee) + ident = sanitize_rust(fn.name) + ret_rust = plan["ret"]["rust"] + ret_py = plan["ret"]["py"] + if builder: + ret_rust = "pyo3::PyRefMut<'py, Self>" + ret_py = w.py_name + body = body[: body.rindex("Ok(())")] + "Ok(slf)" + rust_name_attr = f' #[pyo3(name = "{name}")]' if not is_ctor else "" + block = [rust_doc_lines(doc).replace("///", " ///")] + if rust_name_attr: + block.append(rust_name_attr) + block.extend(attrs) + block.append(f" fn {ident}{lt}({', '.join(head)}) -> PyResult<{ret_rust}> {{") + block.append(" " + body.replace("\n", "\n ")) + block.append(" }") + out.append("\n".join(block)) + if is_ctor: + stubs.append( + " def __init__(self, " + + ", ".join(f"{n}: {p}" for n, p, _t in plan["annots"]) + + ") -> None: ..." + ) + elif fn.self_kind: + stubs.append( + f" def {name}(self" + + ("".join(f", {n}: {p}" for n, p, _t in plan["annots"])) + + f") -> {ret_py}: ..." + ) + else: + stubs.append(" @staticmethod") + stubs.append( + f" def {name}" + self._stub_sig(plan["annots"], ret_py) + ": ..." + ) + + out.extend(self._operator_methods(w, taken, stubs)) + out.extend(self._field_accessors(w, taken, stubs)) + out.extend(self._const_attrs(w)) + out.extend(f" {e}" for e in EXTRAS.get(item.path, [])) + out.append(self._repr(w)) + if w.clone: + out.append( + " fn __copy__(&self) -> Self { self.clone() }\n\n" + " #[pyo3(signature = (_memo=None))]\n" + " fn __deepcopy__(&self, _memo: Option>) -> Self " + "{ self.clone() }" + ) + return out, stubs + + def _repr(self, w: Wrapper) -> str: + item = w.item + if w.simple_enum: + arms = "\n".join( + f' Self::{v.name} => "{w.py_name}.{v.name}",' for v in item.variants + ) + return ( + " fn __repr__(&self) -> &'static str {\n" + f" match self {{\n{arms}\n }}\n }}" + ) + simple = [ + (f.name, ty) + for f in getattr(item, "fields", []) + if f.public + for ty in [parse_type(f.ty, self.res, item.file, item.module)] + if ty.kind == "prim" + ] + if getattr(item, "kind", "") == "named" and 0 < len(simple) <= 6 and len(simple) == len( + [f for f in item.fields if f.public] + ): + fmt = ", ".join(f"{n}={{:?}}" for n, _ in simple) + args = ", ".join(f"self.inner.{n}" for n, _ in simple) + return ( + f' fn __repr__(&self) -> String {{ format!("{w.py_name}({fmt})", {args}) }}' + ) + if w.debug: + return ( + ' fn __repr__(&self) -> String { format!("{:?}", self.inner)' + f'.replacen("{item.name}", "{w.py_name}", 1) }}' + ) + return f' fn __repr__(&self) -> String {{ "<{w.py_name}>".to_string() }}' + + def _operator_methods(self, w: Wrapper, taken: set[str], stubs: list[str]) -> list[str]: + """`__add__` and friends, from the crate's `std::ops` impls. + + The call is written out in full -- `::add(a, + b)` -- rather than as `a.add(b)`. Method syntax needs the trait in + scope, and bringing all of `std::ops` into scope would let an + inherent `add` on some unrelated type silently win the lookup. The + qualified form names exactly the impl the crate wrote. + """ + item = w.item + out = [] + for fn in self.crate.funcs: + if fn.impl_type != item.name or fn.module != item.module or not fn.impl_trait: + continue + trait = fn.impl_trait.split("::")[-1].strip() + base = trait.split("<")[0].strip() + dunder = DUNDERS.get((base, fn.name)) + if not dunder or dunder in taken or not w.clone: + continue + if fn.self_kind != "self" or len(fn.args) > 1: + continue + # `impl Mul for Vec3` is worth naming; `impl Mul for + # f64` is the same operator from the other side and Python + # reaches it through `__rmul__`, which is not generated. + generic = trait[len(base) :].strip() + if generic and not re.fullmatch(r"<\s*(f64|f32|i64|i32|u64|u32|usize|isize)\s*>", generic): + continue + plan = self._plan(fn, owner=item) + if isinstance(plan, str) or plan["needs_py"] or plan["lifetime"]: + continue + taken.add(dunder) + plan = dict(plan, detach=False, callargs=["self.inner.clone()"] + plan["callargs"]) + callee = f"<{w.rust_path} as std::ops::{base}{generic}>::{fn.name}" + body = self._body(plan, callee) + head = ["&self"] + plan["params"] + out.append( + f" fn {dunder}({', '.join(head)}) -> PyResult<{plan['ret']['rust']}> {{\n" + + " " + + body.replace("\n", "\n ") + + "\n }" + ) + args = "".join(f", {n}: {p}" for n, p, _t in plan["annots"]) + stubs.append(f" def {dunder}(self{args}) -> {plan['ret']['py']}: ...") + return out + + def _field_accessors(self, w: Wrapper, taken: set[str], stubs: list[str]) -> list[str]: + """Read (and, for numbers, write) access to a struct's public fields. + + A getter hands out a copy, so a field whose type cannot be cloned + has no getter -- returning a borrow of the wrapper's interior is + not something the Python object model can express. The Rust + identifiers are prefixed because a struct is free to have both a + field `phase` and a method `set_phase`, and the two would collide. + """ + out = [] + for name, ty in w.fields: + py_name = sanitize_py(name) + if py_name in taken: + continue + needs_clone = ty.kind not in ("prim", "str") + if needs_clone and not self.is_clone(ty): + continue + access = f"self.inner.{name}.clone()" if needs_clone else f"self.inner.{name}" + rp = self.ret_plan(ty, access) + if rp is None: + continue + taken.add(py_name) + needs_py = bool(re.search(r"\bpy\b", rp["conv"])) + lt = "<'py>" if needs_py else "" + args = "&self, py: Python<'py>" if needs_py else "&self" + out.append( + f" #[getter]\n" + f' #[pyo3(name = "{py_name}")]\n' + f" fn py_get_{sanitize_rust(name)}{lt}({args}) -> PyResult<{rp['rust']}> " + f"{{ Ok({rp['conv']}) }}" + ) + stubs.append(f" @property\n def {py_name}(self) -> {rp['py']}: ...") + if ty.kind == "prim": + out.append( + f" #[setter]\n" + f' #[pyo3(name = "{py_name}")]\n' + f" fn py_set_{sanitize_rust(name)}(&mut self, v: {ty.name}) " + f"{{ self.inner.{name} = v; }}" + ) + return out + + def _const_attrs(self, w: Wrapper) -> list[str]: + out = [] + for name, ty, doc in w.consts: + rp = self.ret_plan(ty, f"{w.rust_path}::{name}") + if rp is None or re.search(r"\bpy\b", rp["conv"]): + continue + out.append( + f" #[classattr]\n" + f' #[pyo3(name = "{name}")]\n' + f" fn const_{name.lower()}() -> {rp['rust']} {{ {rp['conv']} }}" + ) + return out + + +# ── Items the scanner cannot see: macro expansions ────────────────────── + + +def _add_macro_items(crate: rustscan.Crate) -> None: + """Add the items that `macro_rules!` produces. + + `rustscan` reads source, not expanded source, so anything a macro + defines is invisible to it. Two macros in this crate define public + API: `unit_ctor!` in `units::quantity`, which defines 31 constructors + on `Quantity`, and `kd_impl!` in `spatial::kdtree`, which defines the + two k-d tree types outright. Both are regular enough to reconstruct + from their invocations, which is better than leaving 40-odd public + items unbound and better than pretending they do not exist. + """ + qfile = os.path.join(SRC, "units", "quantity.rs") + if os.path.exists(qfile): + text = open(qfile, encoding="utf-8").read() + for m in re.finditer(r'unit_ctor!\(\s*(\w+)\s*,[^,]+,\s*(.+?)\s*,\s*"(.*?)"\s*\)', text): + crate.funcs.append( + rustscan.Func( + name=m.group(1), + module="units::quantity", + file=qfile, + doc=m.group(3), + attrs=[], + args=[("v", "f64")], + ret="Quantity", + generics="", + where_clause="", + self_kind="", + impl_type="Quantity", + impl_trait="", + is_const=False, + is_unsafe=False, + ) + ) + + kfile = os.path.join(SRC, "spatial", "kdtree.rs") + if os.path.exists(kfile): + text = open(kfile, encoding="utf-8").read() + methods = [ + ("build", [("points", "&[{vec}]")], "Self", "", "Builds by recursive median split."), + ("nearest", [("p", "{vec}")], "Option<(usize, f64)>", "&self", + "Index of and distance to the nearest stored point."), + ("k_nearest", [("p", "{vec}"), ("k", "usize")], "Vec<(usize, f64)>", "&self", + "The `k` nearest stored points, nearest first."), + ("within_radius", [("p", "{vec}"), ("r", "f64")], "Vec<(usize, f64)>", "&self", + "Every stored point within `r` of `p`."), + ("all_pairs_within", [("r", "f64")], "Vec<(usize, usize)>", "&self", + "Every pair of stored points closer than `r`."), + ] + for m in re.finditer(r"kd_impl!\(\s*(\w+)\s*,\s*(\w+)\s*,", text): + name, vec = m.group(1), m.group(2) + crate.structs.append( + rustscan.Struct( + name=name, + module="spatial::kdtree", + file=kfile, + doc=f"Median-split k-d tree over `{vec}` points.", + attrs=["derive(Debug, Clone)"], + fields=[], + kind="named", + generics="", + ) + ) + for mname, margs, mret, mself, mdoc in methods: + crate.funcs.append( + rustscan.Func( + name=mname, + module="spatial::kdtree", + file=kfile, + doc=mdoc, + attrs=[], + args=[(a, t.format(vec=vec)) for a, t in margs], + ret=mret, + generics="", + where_clause="", + self_kind=mself, + impl_type=name, + impl_trait="", + is_const=False, + is_unsafe=False, + ) + ) + + +# ── Writing it all out ────────────────────────────────────────────────── + +HEADER = """// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py +""" + +ALLOWS = """#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] +""" + + +class Build(Emitter): + def run(self, out_rs: str, out_py: str) -> None: + self.identified_fns = 0 + modules = self._module_list() + free_by_module: dict[str, list[Emitted]] = {m: [] for m in modules} + for fn in self.crate.funcs: + if fn.impl_type or fn.impl_trait: + continue + if fn.module not in free_by_module: + continue + e = self.emit_free(fn) + if e is not None: + free_by_module[fn.module].append(e) + + # The three identified types have no class, so their methods are + # emitted as functions in the module that defines them. + for path in IDENTIFIED: + owner = self.res.by_path.get(path) + if owner is None or owner.module not in free_by_module: + continue + taken = {e.py_name for e in free_by_module[owner.module]} + for fn in self.crate.funcs: + if fn.impl_type != owner.name or fn.module != owner.module or fn.impl_trait: + continue + e = self.emit_identified_method(fn, path) + if e is None: + continue + self.identified_fns += 1 + if e.py_name in taken: + # A name the module already uses: qualify rather than + # shadow. `fractals.norm` stays the free function; + # the method becomes `fractals.complex_norm`. + prefixed = f"{IDENTIFIED[path]}_{e.py_name}" + if prefixed in taken: + continue + e = Emitted( + py_name=prefixed, + code=e.code.replace(f'name = "{e.py_name}"', f'name = "{prefixed}"'), + stub=e.stub.replace(f"def {e.py_name}(", f"def {prefixed}(", 1), + ) + taken.add(e.py_name) + free_by_module[owner.module].append(e) + + consts_by_module: dict[str, list] = {m: [] for m in modules} + for c in self.crate.consts: + if c.owner or c.module not in consts_by_module: + continue + ty = parse_type(c.ty, self.res, c.file, c.module) + rp = self.ret_plan(ty, f"rust_physics_engine::{c.module}::{c.name}") + if rp is None or rp["fallible"] or re.search(r"\bpy\b", rp["conv"]): + self.skipped.append((c.module, c.name, f"constant of type `{c.ty}`")) + continue + consts_by_module[c.module].append((c, rp)) + + # Rust keeps types, values and modules in separate namespaces; + # Python does not. `special::gamma` is both a module and, through a + # re-export, a function, and only one of them can be + # `special.gamma`. The module wins, because dropping it would take + # `special.gamma.gamma_p` and everything beside it with it. + self._resolve_shadowing(modules, free_by_module, consts_by_module) + self.aliases = self._compute_aliases(modules, free_by_module) + + os.makedirs(out_rs, exist_ok=True) + self._write_types(out_rs) + for mod in modules: + self._write_module(out_rs, mod, free_by_module[mod], consts_by_module[mod]) + self._write_mod_rs(out_rs, modules, consts_by_module) + self._write_python(out_py, modules, free_by_module, consts_by_module) + self._write_coverage(modules, free_by_module, consts_by_module) + + def _child_modules(self, modules) -> dict[str, set[str]]: + out: dict[str, set[str]] = {} + for m in modules: + parent, _, leaf = m.rpartition("::") + if parent: + out.setdefault(parent, set()).add(leaf) + return out + + def _resolve_shadowing(self, modules, free_by_module, consts_by_module) -> None: + """Drop anything a submodule of the same name would shadow.""" + children = self._child_modules(modules) + for mod, names in children.items(): + kept = [e for e in free_by_module.get(mod, []) if e.py_name not in names] + for e in free_by_module.get(mod, []): + if e.py_name in names: + self.skipped.append( + (mod, f"{e.py_name}()", f"shadowed by the submodule `{mod}::{e.py_name}`") + ) + free_by_module[mod] = kept + for w in list(self.class_of_module.get(mod, [])): + if w.py_name in names: + self.class_of_module[mod].remove(w) + self.skipped.append( + (mod, w.py_name, f"shadowed by the submodule `{mod}::{w.py_name}`") + ) + consts_by_module[mod] = [ + (c, rp) for c, rp in consts_by_module.get(mod, []) if c.name not in names + ] + + # A class and a function of the same name in one module would also + # collide. Rust's naming conventions make it unlikely rather than + # impossible, and "unlikely" is not a thing to leave unchecked. + for mod in modules: + class_names = {w.py_name for w in self.class_of_module.get(mod, [])} + clash = [e for e in free_by_module.get(mod, []) if e.py_name in class_names] + for e in clash: + free_by_module[mod].remove(e) + self.skipped.append( + (mod, f"{e.py_name}()", f"a class in `{mod}` already has that name") + ) + + def _compute_aliases(self, modules, free_by_module) -> dict[str, list[tuple[str, str]]]: + """Where a `pub use` re-export puts a name, put the Python name too. + + `linalg` re-exports `Matrix` from `linalg::matrix` and `solve` from + `linalg::lu`, and the crate's own documentation refers to them by + the short path. A binding that only offered the long one would send + readers of those docs to an `AttributeError`. + """ + modset = set(modules) + children = self._child_modules(modules) + fn_names = {m: {e.py_name for e in v} for m, v in free_by_module.items()} + class_names = { + m: {w.py_name for w in ws} for m, ws in self.class_of_module.items() + } + out: dict[str, list[tuple[str, str]]] = {} + for mod, entries in sorted(self.crate.pub_uses.items()): + if mod not in modset: + continue + # A submodule of the same name is already there and must stay. + here = fn_names.get(mod, set()) | class_names.get(mod, set()) | children.get( + mod, set() + ) + for name, target in sorted(entries.items()): + # `use matrix::Matrix` inside `linalg` is a relative path. + for full in (target, f"{mod}::{target}"): + src_mod = "::".join(full.split("::")[:-1]) + tail = full.split("::")[-1] + if src_mod == mod or src_mod not in modset or tail != name: + continue + if name in here: + if name in children.get(mod, set()): + self.skipped.append( + ( + mod, + name, + f"re-export shadowed by the submodule `{mod}::{name}`;" + f" reach it at `{src_mod.replace('::', '.')}.{name}`", + ) + ) + break + if name in fn_names.get(src_mod, set()) or name in class_names.get( + src_mod, set() + ): + out.setdefault(mod, []).append((name, src_mod)) + here.add(name) + break + return out + + def _module_list(self) -> list[str]: + mods: set[str] = set() + for fn in self.crate.funcs: + if not fn.impl_type: + mods.add(fn.module) + for w in self.wrappers.values(): + mods.add(w.item.module) + for c in self.crate.consts: + if not c.owner: + mods.add(c.module) + # The three identified types have no class, and their module may + # contain nothing else -- `exact::bigint` is only `BigInt`. Their + # methods still become functions there, so the module has to exist. + for path in IDENTIFIED: + item = self.res.by_path.get(path) + if item is not None: + mods.add(item.module) + # Every ancestor has to exist so the tree can be attached. + full = set() + for m in mods: + parts = m.split("::") + for i in range(len(parts)): + full.add("::".join(parts[: i + 1])) + full.discard("") + return sorted(full, key=lambda m: (m.count("::"), m)) + + @staticmethod + def _rs_name(mod: str) -> str: + return "m_" + mod.replace("::", "__") + + # ── Rust ──────────────────────────────────────────────────────── + + def _write_types(self, out_rs: str) -> None: + tdir = os.path.join(out_rs, "types") + os.makedirs(tdir, exist_ok=True) + groups: dict[str, list[Wrapper]] = {} + for w in sorted(self.wrappers.values(), key=lambda w: w.item.path): + groups.setdefault(w.item.module.split("::")[0], []).append(w) + self._class_stubs: dict[str, list[str]] = {} + names = [] + for top, ws in sorted(groups.items()): + body = [HEADER, ALLOWS, "use pyo3::prelude::*;\n"] + for w in ws: + code, stub = self.emit_class(w) + body.append(code) + self._class_stubs[w.item.path] = stub + with open(os.path.join(tdir, f"{top}.rs"), "w") as fh: + fh.write("\n\n".join(body) + "\n") + names.append(top) + mod = [HEADER, ALLOWS] + for n in names: + mod.append(f"mod {n};\npub use {n}::*;") + with open(os.path.join(tdir, "mod.rs"), "w") as fh: + fh.write("\n".join(mod) + "\n") + + def _write_module(self, out_rs: str, mod: str, fns: list[Emitted], consts: list) -> None: + path = os.path.join(out_rs, self._rs_name(mod) + ".rs") + body = [HEADER, ALLOWS, "use pyo3::prelude::*;\nuse pyo3::types::PyModule;\n"] + body.extend(e.code for e in fns) + reg = [ + "/// Registers this module's contents.", + "pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {", + " let _ = (py, m);", + ] + for e in fns: + reg.append(f" m.add_function(wrap_pyfunction!({self._ident_of(e)}, m)?)?;") + for c, rp in consts: + reg.append(f' m.add("{c.name}", {rp["conv"]})?;') + for w in sorted(self.class_of_module.get(mod, []), key=lambda w: w.py_name): + reg.append(f" m.add_class::()?;") + reg.append(" Ok(())") + reg.append("}") + body.append("\n".join(reg)) + with open(path, "w") as fh: + fh.write("\n\n".join(body) + "\n") + + @staticmethod + def _ident_of(e: Emitted) -> str: + return e.code.split("pub fn ", 1)[1].split("<", 1)[0].split("(", 1)[0] + + def _write_mod_rs(self, out_rs: str, modules: list[str], consts_by_module) -> None: + lines = [HEADER, ALLOWS, "use pyo3::prelude::*;", "use pyo3::types::PyModule;", + "use std::collections::HashMap;", "", "pub mod types;"] + for m in modules: + lines.append(f"pub mod {self._rs_name(m)};") + lines.append("") + lines.append("/// Builds the module tree under the extension module.") + lines.append( + "pub fn register<'py>(py: Python<'py>, root: &Bound<'py, PyModule>) -> PyResult<()> {" + ) + lines.append(" let mut mods: HashMap<&'static str, Bound<'py, PyModule>> = HashMap::new();") + for m in modules: + short = m.split("::")[-1] + parent = "::".join(m.split("::")[:-1]) + doc = clean_markup(self.crate.module_docs.get(m, "")).replace( + "\\", "\\\\" + ).replace('"', '\\"') + lines.append(" {") + lines.append(f' let sub = PyModule::new(py, "{PKG}.{m.replace("::", ".")}")?;') + if doc: + lines.append(f' sub.setattr("__doc__", "{doc}")?;') + lines.append(f" {self._rs_name(m)}::register(py, &sub)?;") + if parent: + lines.append(f' mods["{parent}"].add("{short}", &sub)?;') + else: + lines.append(f' root.add("{short}", &sub)?;') + lines.append(f' mods.insert("{m}", sub);') + lines.append(" }") + for mod, entries in self.aliases.items(): + for name, src_mod in entries: + lines.append( + f' {{ let v = mods["{src_mod}"].getattr("{name}")?; ' + f'mods["{mod}"].add("{name}", v)?; }}' + ) + lines.append(" let names: Vec<&str> = vec![") + lines.append( + " " + ", ".join(f'"{m.replace("::", ".")}"' for m in modules) + ) + lines.append(" ];") + lines.append(' root.add("__submodules__", names)?;') + lines.append(" Ok(())") + lines.append("}") + with open(os.path.join(out_rs, "mod.rs"), "w") as fh: + fh.write("\n".join(lines) + "\n") + + # ── Python ────────────────────────────────────────────────────── + + STUB_PRELUDE = ( + "# @generated by bindings/python/generate.py -- do not edit.\n" + "from __future__ import annotations\n" + "from collections.abc import Callable, Sequence\n" + "from fractions import Fraction\n" + "from typing import Any, Optional\n" + ) + + def _write_python(self, out_py: str, modules, free_by_module, consts_by_module) -> None: + # Wipe the generated subtree, keeping the hand-written files. + for entry in sorted(os.listdir(out_py)) if os.path.isdir(out_py) else []: + full = os.path.join(out_py, entry) + if os.path.isdir(full): + shutil.rmtree(full) + elif entry.endswith(".pyi") and entry != "py.typed": + os.remove(full) + os.makedirs(out_py, exist_ok=True) + + unique = {n: ws[0] for n, ws in self.class_by_name.items() if len(ws) == 1} + for mod in modules: + parts = mod.split(".") if "." in mod else mod.split("::") + rel = os.path.join(*parts) + children = [m for m in modules if m.startswith(mod + "::") and m.count("::") == mod.count("::") + 1] + if children: + target = os.path.join(out_py, rel, "__init__.pyi") + os.makedirs(os.path.join(out_py, rel), exist_ok=True) + else: + target = os.path.join(out_py, rel + ".pyi") + os.makedirs(os.path.dirname(target), exist_ok=True) + + classes = sorted(self.class_of_module.get(mod, []), key=lambda w: w.py_name) + fns = free_by_module.get(mod, []) + chunks = [self._class_stubs[w.item.path] for w in classes] + chunks += [e.stub for e in fns] + chunks += [f"{c.name}: {rp['py']}" for c, rp in consts_by_module.get(mod, [])] + aliased = self.aliases.get(mod, []) + local = {w.py_name for w in classes} | {n for n, _ in aliased} + imports = self._stub_imports("\n".join(chunks), mod, local, unique) + imports += [ + f"from {PKG}.{src.replace('::', '.')} import {name} as {name}" + for name, src in aliased + ] + # The docstring has to be the first statement in the file, or + # it is not the module's docstring at all. + doc = clean_markup(self.crate.module_docs.get(mod, "")) + head = [py_doc_literal([doc], "")] if doc else [] + head.append(self.STUB_PRELUDE) + if children: + head.append("from . import " + ", ".join(sorted(c.split("::")[-1] for c in children))) + head.extend(imports) + with open(target, "w") as fh: + fh.write("\n".join(head) + "\n\n" + "\n\n".join(chunks) + "\n") + + with open(os.path.join(out_py, "py.typed"), "w") as fh: + fh.write("") + self._write_init(out_py, modules) + + def _stub_imports(self, text: str, mod: str, local: set[str], unique) -> list[str]: + used = set(re.findall(r"\b[A-Z][A-Za-z0-9_]*\b", text)) + out = [] + for name in sorted(used): + if name in local or name in ("Any", "Optional", "Sequence", "Callable", "Fraction", "None"): + continue + w = unique.get(name) + if w is None: + continue + out.append(f"from {PKG}.{w.item.module.replace('::', '.')} import {name}") + return out + + def _write_init(self, out_py: str, modules) -> None: + top = [m for m in modules if "::" not in m] + text = f'''"""{PKG} -- the rust_physics_engine library, from Python. + +Generated from the Rust source by ``bindings/python/generate.py``. Every +module here mirrors a Rust module of the same name, and every function +keeps the name, the argument order and the units it has in Rust. + + >>> import {PKG} as nm + >>> import math + >>> round(nm.classical.projectile_range(20.0, math.pi / 4, 9.80665), 4) + 40.7886 + +Units are SI and angles are radians unless a docstring says otherwise. +Everything this package raises derives from :class:`PhysicsError`. +""" + +from __future__ import annotations + +import sys as _sys + +from . import _core +from ._core import ( + ConvergenceError, + DegenerateGeometryError, + DimensionMismatchError, + EmptyInputError, + GeometryError, + InvalidArgumentError, + NotManifoldError, + NotPositiveDefiniteError, + PhysicsError, + SingularMatrixError, + SolverError, + UnitsError, +) + +__version__ = _core.__version__ + + +def _install() -> list[str]: + """Make every submodule importable by name. + + PyO3 builds the module tree as attributes of the extension module, + which is enough for ``numeria.linalg.lu``. It is not enough for ``import + {PKG}.linalg.lu``, or for ``from {PKG}.linalg import lu``: + both go through ``sys.modules``, and nothing has put the submodules + there. This does, once, at import. + """ + installed = [] + for dotted in _core.__submodules__: + obj = _core + for part in dotted.split("."): + obj = getattr(obj, part) + _sys.modules[f"{{__name__}}.{{dotted}}"] = obj + if "." not in dotted: + globals()[dotted] = obj + installed.append(dotted) + return installed + + +_MODULES = _install() + +#: The physical and mathematical constants, as a module of their own. +constants = _sys.modules[f"{{__name__}}.math.constants"] + +__all__ = [ + "PhysicsError", + "InvalidArgumentError", + "SolverError", + "SingularMatrixError", + "NotPositiveDefiniteError", + "ConvergenceError", + "DimensionMismatchError", + "GeometryError", + "DegenerateGeometryError", + "NotManifoldError", + "EmptyInputError", + "UnitsError", + "constants", + *_MODULES, +] +''' + with open(os.path.join(out_py, "__init__.py"), "w") as fh: + fh.write(text) + + stub = [ + self.STUB_PRELUDE, + "from . import " + ", ".join(sorted(top)), + "", + "__version__: str", + "constants: Any", + "", + "class PhysicsError(Exception): ...", + "class InvalidArgumentError(PhysicsError): ...", + "class SolverError(PhysicsError): ...", + "class SingularMatrixError(SolverError): ...", + "class NotPositiveDefiniteError(SolverError): ...", + "class ConvergenceError(SolverError):", + " iterations: int", + " residual: float", + "class DimensionMismatchError(SolverError):", + " expected: int", + " got: int", + "class GeometryError(PhysicsError): ...", + "class DegenerateGeometryError(GeometryError): ...", + "class NotManifoldError(GeometryError): ...", + "class EmptyInputError(GeometryError): ...", + "class UnitsError(PhysicsError): ...", + ] + with open(os.path.join(out_py, "__init__.pyi"), "w") as fh: + fh.write("\n".join(stub) + "\n") + + # ── the coverage report ───────────────────────────────────────── + + def _write_coverage(self, modules, free_by_module, consts_by_module) -> None: + total_fns = sum(1 for f in self.crate.funcs if not f.impl_type and not f.impl_trait) + bound_fns = sum(len(v) for v in free_by_module.values()) - self.identified_fns + total_methods = sum(1 for f in self.crate.funcs if f.impl_type and not f.impl_trait) + bound_classes = len(self.wrappers) + skipped_methods = sum(1 for _m, item, _r in self.skipped if "." in item) + by_reason: dict[str, int] = {} + for _m, _i, reason in self.skipped: + key = re.sub(r"`[^`]*`", "`...`", reason) + by_reason[key] = by_reason.get(key, 0) + 1 + + lines = [ + "", + "", + "# What is bound, and what is not", + "", + "Generated alongside the bindings themselves, so it cannot drift", + "from them. Every item the generator could not bind is listed at", + "the bottom with the reason.", + "", + "## Totals", + "", + "| | in Rust | bound |", + "|---|---:|---:|", + f"| Free functions | {total_fns} | {bound_fns} |", + f"| Methods | {total_methods} | {total_methods - skipped_methods} |", + f"| Classes | {len(self.crate.structs) + len(self.crate.enums)} | {bound_classes} |", + f"| Constants | {sum(1 for c in self.crate.consts if not c.owner)} | " + f"{sum(len(v) for v in consts_by_module.values())} |", + "", + f"Of those methods, {self.identified_fns} belong to `Complex`, `BigInt` and", + "`Rational`. Those three have no wrapper class -- they cross over as", + "Python's own `complex`, `int` and `Fraction` -- so their methods appear", + "as functions in the module that defines the type:", + "`exact.bigint.mod_pow(base, exponent, modulus)` rather than", + "`BigInt.mod_pow`.", + "", + f"The tree comes to {len(modules)} Python modules.", + "", + "## Why the rest is not bound", + "", + "| reason | count |", + "|---|---:|", + ] + for reason, n in sorted(by_reason.items(), key=lambda kv: -kv[1]): + lines.append(f"| {reason} | {n} |") + lines += ["", "## Per module", "", "| module | functions bound | classes |", "|---|---:|---:|"] + for m in modules: + nf = len(free_by_module.get(m, [])) + nc = len(self.class_of_module.get(m, [])) + if nf or nc: + lines.append(f"| `{m}` | {nf} | {nc} |") + lines += ["", "## Every unbound item", "", "| module | item | reason |", "|---|---|---|"] + for m, item, reason in sorted(self.skipped): + lines.append(f"| `{m}` | `{item}` | {reason} |") + with open(os.path.join(HERE, "COVERAGE.md"), "w") as fh: + fh.write("\n".join(lines) + "\n") + + +def main() -> int: + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--check", action="store_true", help="fail if the committed output is stale") + args = ap.parse_args() + + if args.check: + import tempfile + + with tempfile.TemporaryDirectory() as tmp: + rs = os.path.join(tmp, "generated") + py = os.path.join(tmp, PKG) + os.makedirs(py) + Build().run(rs, py) + stale = [] + for old, new in ((OUT_RS, rs), (OUT_PY, py)): + stale += _diff_trees(old, new) + if stale: + print("The committed bindings are stale. Re-run:") + print(" python3 bindings/python/generate.py") + for s in stale[:40]: + print(" ", s) + return 1 + print("bindings are up to date") + return 0 + + Build().run(OUT_RS, OUT_PY) + print(f"wrote {OUT_RS} and {OUT_PY}") + return 0 + + +def _diff_trees(a: str, b: str) -> list[str]: + def listing(root: str) -> dict[str, str]: + out = {} + for dirpath, _dirs, files in os.walk(root): + for f in files: + full = os.path.join(dirpath, f) + out[os.path.relpath(full, root)] = open(full, encoding="utf-8").read() + return out + + old, new = listing(a), listing(b) + diffs = [] + for k in sorted(set(old) | set(new)): + if k not in old: + diffs.append(f"missing: {k}") + elif k not in new: + diffs.append(f"stale: {k}") + elif old[k] != new[k]: + diffs.append(f"differs: {k}") + return diffs + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/bindings/python/pyproject.toml b/bindings/python/pyproject.toml new file mode 100644 index 0000000..5db9f9b --- /dev/null +++ b/bindings/python/pyproject.toml @@ -0,0 +1,46 @@ +[build-system] +requires = ["maturin>=1.5,<2.0"] +build-backend = "maturin" + +[project] +name = "numeria" +description = "Physics, mathematics and engineering computation: 4,000+ validated functions across 71 domains, in Rust, callable from Python" +readme = "README.md" +license = { text = "MIT" } +requires-python = ">=3.9" +authors = [{ name = "Magic-Man", email = "mimsec-contact@mimsec.com" }] +keywords = ["physics", "mathematics", "simulation", "science", "engineering"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Rust", + "Topic :: Scientific/Engineering :: Physics", + "Topic :: Scientific/Engineering :: Mathematics", + "Typing :: Typed", +] +dynamic = ["version"] + +[project.urls] +Homepage = "https://github.com/Magic-Man-us/RustPhysicsEngine" +Source = "https://github.com/Magic-Man-us/RustPhysicsEngine" +Documentation = "https://github.com/Magic-Man-us/RustPhysicsEngine/blob/main/bindings/python/README.md" +Issues = "https://github.com/Magic-Man-us/RustPhysicsEngine/issues" +Changelog = "https://github.com/Magic-Man-us/RustPhysicsEngine/releases" + +[tool.maturin] +# A mixed layout: the compiled `_core` extension lands inside the pure +# Python package, which owns `__init__.py` and the `.pyi` stubs. +python-source = "python" +module-name = "numeria._core" +features = ["pyo3/extension-module"] +include = ["python/numeria/py.typed"] + +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/bindings/python/python/numeria/__init__.py b/bindings/python/python/numeria/__init__.py new file mode 100644 index 0000000..b08b477 --- /dev/null +++ b/bindings/python/python/numeria/__init__.py @@ -0,0 +1,80 @@ +"""numeria -- the rust_physics_engine library, from Python. + +Generated from the Rust source by ``bindings/python/generate.py``. Every +module here mirrors a Rust module of the same name, and every function +keeps the name, the argument order and the units it has in Rust. + + >>> import numeria as nm + >>> import math + >>> round(nm.classical.projectile_range(20.0, math.pi / 4, 9.80665), 4) + 40.7886 + +Units are SI and angles are radians unless a docstring says otherwise. +Everything this package raises derives from :class:`PhysicsError`. +""" + +from __future__ import annotations + +import sys as _sys + +from . import _core +from ._core import ( + ConvergenceError, + DegenerateGeometryError, + DimensionMismatchError, + EmptyInputError, + GeometryError, + InvalidArgumentError, + NotManifoldError, + NotPositiveDefiniteError, + PhysicsError, + SingularMatrixError, + SolverError, + UnitsError, +) + +__version__ = _core.__version__ + + +def _install() -> list[str]: + """Make every submodule importable by name. + + PyO3 builds the module tree as attributes of the extension module, + which is enough for ``numeria.linalg.lu``. It is not enough for ``import + numeria.linalg.lu``, or for ``from numeria.linalg import lu``: + both go through ``sys.modules``, and nothing has put the submodules + there. This does, once, at import. + """ + installed = [] + for dotted in _core.__submodules__: + obj = _core + for part in dotted.split("."): + obj = getattr(obj, part) + _sys.modules[f"{__name__}.{dotted}"] = obj + if "." not in dotted: + globals()[dotted] = obj + installed.append(dotted) + return installed + + +_MODULES = _install() + +#: The physical and mathematical constants, as a module of their own. +constants = _sys.modules[f"{__name__}.math.constants"] + +__all__ = [ + "PhysicsError", + "InvalidArgumentError", + "SolverError", + "SingularMatrixError", + "NotPositiveDefiniteError", + "ConvergenceError", + "DimensionMismatchError", + "GeometryError", + "DegenerateGeometryError", + "NotManifoldError", + "EmptyInputError", + "UnitsError", + "constants", + *_MODULES, +] diff --git a/bindings/python/python/numeria/__init__.pyi b/bindings/python/python/numeria/__init__.pyi new file mode 100644 index 0000000..ff8c1c4 --- /dev/null +++ b/bindings/python/python/numeria/__init__.pyi @@ -0,0 +1,27 @@ +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from . import acoustics, astrophysics, atmosphere, audio, biophysics, cfd, chemistry, classical, codes, color_science, continuum_mechanics, control_systems, core, curves, discrete, dsp, electromagnetism, electronics, exact, fem, fields, finance, fluid_instabilities, fluids, fractals, general_relativity, geometry, geophysics, graph, gravitation, information_theory, learn, linalg, magnetohydrodynamics, manifold, materials, math, mesh, monte_carlo, neutronics, nonlinear, nuclear, numerical, optics, optimization, particle_physics, patterns, photonics, plasma, propulsion, quantum, quaternion, radiation, relativity, resonance, rf, signal_processing, sim, solid_mechanics, spatial, special, statistical_mechanics, statistics, stochastic, thermodynamics, transforms, trigonometry, units, vector_calculus, waves + +__version__: str +constants: Any + +class PhysicsError(Exception): ... +class InvalidArgumentError(PhysicsError): ... +class SolverError(PhysicsError): ... +class SingularMatrixError(SolverError): ... +class NotPositiveDefiniteError(SolverError): ... +class ConvergenceError(SolverError): + iterations: int + residual: float +class DimensionMismatchError(SolverError): + expected: int + got: int +class GeometryError(PhysicsError): ... +class DegenerateGeometryError(GeometryError): ... +class NotManifoldError(GeometryError): ... +class EmptyInputError(GeometryError): ... +class UnitsError(PhysicsError): ... diff --git a/bindings/python/python/numeria/acoustics.pyi b/bindings/python/python/numeria/acoustics.pyi new file mode 100644 index 0000000..064ddd6 --- /dev/null +++ b/bindings/python/python/numeria/acoustics.pyi @@ -0,0 +1,276 @@ +""" +Room acoustics, psychoacoustic scales, and musical pitch. Reverberation by Sabine (`RT60 = 0.161 V / A`) and by Eyring, which differ in how they treat a very absorptive room: Sabine's formula is a diffuse-field approximation that never reaches zero however absorptive the surfaces, while Eyring's does. Room modes, critical distance and the mass-law transmission loss follow. The perceptual scales -- mel, bark, ERB, A-weighting, equal-loudness phon -- map physical frequency and level onto what a listener reports, and are fits to listening data rather than derivations. Musical pitch is here too: equal temperament, cents, and MIDI note conversion. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def sabine_reverberation(volume: float, total_absorption: float) -> float: + """ +Sabine reverberation time: T60 = 0.161 V / A (seconds). + +Rust: `acoustics::sabine_reverberation` + """ + ... + +def eyring_reverberation(volume: float, surface_area: float, avg_absorption_coeff: float) -> float: + """ +Eyring reverberation time: T60 = 0.161 V / (-S × ln(1 - ā)) (seconds). + +Rust: `acoustics::eyring_reverberation` + """ + ... + +def total_absorption(surfaces: list[tuple[float, float]]) -> float: + """ +Total absorption area: A = Σ(Si × αi). +Each tuple is (area_m2, absorption_coefficient). + +Rust: `acoustics::total_absorption` + """ + ... + +def room_constant(surface_area: float, avg_absorption: float) -> float: + """ +Room constant: R = S × ā / (1 - ā). + +Rust: `acoustics::room_constant` + """ + ... + +def critical_distance(room_constant: float, directivity: float) -> float: + """ +Critical distance: dc = √(Q × R / (16π)). + +Rust: `acoustics::critical_distance` + """ + ... + +def room_mode_frequency(length: float, width: float, height: float, nx: int, ny: int, nz: int, speed: float) -> float: + """ +Axial/tangential/oblique room mode frequency: +f = (c/2) × √((nx/L)² + (ny/W)² + (nz/H)²). + +Rust: `acoustics::room_mode_frequency` + """ + ... + +def add_db(db1: float, db2: float) -> float: + """ +Energetic sum of two decibel levels: 10 × log₁₀(10^(dB1/10) + 10^(dB2/10)). + +Rust: `acoustics::add_db` + """ + ... + +def add_db_multiple(levels: list[float]) -> float: + """ +Energetic sum of multiple decibel levels. + +Rust: `acoustics::add_db_multiple` + """ + ... + +def subtract_db(total_db: float, background_db: float) -> float: + """ +Subtract background noise: 10 × log₁₀(10^(total/10) - 10^(bg/10)). + +Rust: `acoustics::subtract_db` + """ + ... + +def distance_attenuation(db_at_ref: float, ref_distance: float, distance: float) -> float: + """ +Inverse-square distance attenuation: L2 = L1 - 20 × log₁₀(d2 / d1). + +Rust: `acoustics::distance_attenuation` + """ + ... + +def a_weighting(frequency: float) -> float: + """ +A-weighting filter approximation (dBA relative weighting at a given frequency). + +Rust: `acoustics::a_weighting` + """ + ... + +def equal_loudness_phon(spl: float, frequency: float) -> float: + """ +Rough equal-loudness approximation in phon. +At 1 kHz the phon value equals the SPL. At other frequencies a simple +A-weighting-derived correction is applied. This is NOT a full ISO 226 +implementation. + +Rust: `acoustics::equal_loudness_phon` + """ + ... + +def bark_scale(frequency: float) -> float: + """ +Bark critical-band rate: z = 13 × atan(0.76 f/1000) + 3.5 × atan((f/7500)²). + +Rust: `acoustics::bark_scale` + """ + ... + +def mel_scale(frequency: float) -> float: + """ +Mel scale: m = 2595 × log₁₀(1 + f/700). + +Rust: `acoustics::mel_scale` + """ + ... + +def frequency_from_mel(mel: float) -> float: + """ +Inverse mel scale: f = 700 × (10^(m/2595) - 1). + +Rust: `acoustics::frequency_from_mel` + """ + ... + +def noise_reduction(tl: float, receiving_absorption: float, common_area: float) -> float: + """ +Noise reduction through a partition: NR = TL + 10 × log₁₀(A / S). + +Rust: `acoustics::noise_reduction` + """ + ... + +def transmission_loss_mass_law(surface_density: float, frequency: float) -> float: + """ +Single-panel mass law transmission loss: TL = 20 × log₁₀(m × f) - 47. + +Rust: `acoustics::transmission_loss_mass_law` + """ + ... + +def sound_transmission_class_estimate(tl_500: float) -> float: + """ +Rough STC estimate (≈ TL at 500 Hz). + +Rust: `acoustics::sound_transmission_class_estimate` + """ + ... + +def atmospheric_absorption_coeff(frequency: float, temperature: float, humidity: float) -> float: + """ +Simplified atmospheric absorption coefficient in dB/km. +α ≈ 0.01 × (f/1000)^1.7 × (1 + 0.01×(T-20)) × (1 - 0.005×RH). + +Rust: `acoustics::atmospheric_absorption_coeff` + """ + ... + +def ground_effect_excess(distance: float, source_height: float, receiver_height: float) -> float: + """ +Simplified excess ground attenuation (dB) for propagation over soft ground. +Uses a basic geometric model based on path-length difference. + +Rust: `acoustics::ground_effect_excess` + """ + ... + +def harmonic_frequency(fundamental: float, n: int) -> float: + """ +Nth harmonic frequency: f_n = n × f_fundamental + +Rust: `acoustics::harmonic_frequency` + """ + ... + +def harmonic_series(fundamental: float, max_harmonic: int) -> list[float]: + """ +Generate harmonic series up to max_harmonic: [f, 2f, 3f, ..., nf] + +Rust: `acoustics::harmonic_series` + """ + ... + +def equal_temperament_ratio(semitones: float) -> float: + """ +Frequency ratio between two musical interval semitones (equal temperament): +ratio = 2^(semitones/12) + +Rust: `acoustics::equal_temperament_ratio` + """ + ... + +def midi_to_frequency(midi_note: float) -> float: + """ +Frequency of a note in equal temperament given A4=440Hz reference: +f = 440 × 2^((midi_note - 69)/12) where midi_note 69 = A4 + +Rust: `acoustics::midi_to_frequency` + """ + ... + +def frequency_to_midi(frequency: float) -> float: + """ +MIDI note number from frequency: n = 69 + 12×log₂(f/440) + +Rust: `acoustics::frequency_to_midi` + """ + ... + +def cents(f1: float, f2: float) -> float: + """ +Cents difference between two frequencies: c = 1200 × log₂(f2/f1) + +Rust: `acoustics::cents` + """ + ... + +def circular_membrane_frequency(bessel_zero: float, wave_speed: float, radius: float) -> float: + """ +Frequency of a circular membrane mode (drum): +f_mn = (α_mn × v) / (2π × r) +where α_mn are zeros of Bessel functions. Common modes: +(0,1)=2.405, (1,1)=3.832, (2,1)=5.136, (0,2)=5.520 + +Rust: `acoustics::circular_membrane_frequency` + """ + ... + +def rectangular_plate_fundamental(length: float, flexural_rigidity: float, mass_per_area: float) -> float: + """ +Rectangular plate fundamental frequency: +f = (π/(2L²)) × √(D/(ρh)) where D = Eh³/(12(1-ν²)) +Simplified: takes flexural rigidity D, density×thickness (ρh), and length + +Rust: `acoustics::rectangular_plate_fundamental` + """ + ... + +def total_harmonic_distortion(fundamental_amplitude: float, harmonic_amplitudes: list[float]) -> float: + """ +Harmonic distortion: THD = √(Σ V_n²) / V_1 for n=2..N +Takes fundamental amplitude and harmonic amplitudes [2nd, 3rd, ...] + +Rust: `acoustics::total_harmonic_distortion` + """ + ... + +def harmonic_synthesis(fundamental: float, harmonics: list[tuple[int, float, float]], t: float) -> float: + """ +Synthesize a waveform from harmonics at a given time: +y(t) = Σ a_n × sin(2π × n × f × t + φ_n) +Each tuple is (harmonic_number, amplitude, phase) + +Rust: `acoustics::harmonic_synthesis` + """ + ... + +def inharmonic_frequency(fundamental: float, n: int, b_coeff: float) -> float: + """ +Inharmonicity coefficient for a stiff string (piano): +f_n = n × f₁ × √(1 + B × n²) where B = π³Ed⁴/(64TL²) +Takes the precomputed inharmonicity coefficient B + +Rust: `acoustics::inharmonic_frequency` + """ + ... diff --git a/bindings/python/python/numeria/astrophysics/__init__.pyi b/bindings/python/python/numeria/astrophysics/__init__.pyi new file mode 100644 index 0000000..309e8ea --- /dev/null +++ b/bindings/python/python/numeria/astrophysics/__init__.pyi @@ -0,0 +1,12 @@ +""" +Astrodynamics and astrophysics. Orbits are the core. `kepler` solves Kepler's equation for elliptic, parabolic and hyperbolic orbits; `orbital_elements` converts between state vectors and Keplerian elements; `maneuvers` covers Hohmann and bi-elliptic transfers, plane changes, phasing and J2 secular rates; and `lambert` solves for the transfer orbit connecting two positions in a given time. `time_systems` and `coords` are the bookkeeping that makes those answers refer to anything real -- Julian dates, UT1/TAI/TT/TDB, sidereal time, and the equatorial, ecliptic, galactic, horizontal and ITRF frames with precession and nutation. Many-body gravity is handled by `nbody` with a leapfrog integrator and `octree` for Barnes-Hut O(N log N) forces. The remaining modules cover `tidal` forces and Roche limits, `lagrange` points, `gravitational_waves`, `magnetosphere` field-line tracing, `habitable_zone` boundaries, and `collisions` and impact cratering. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from . import collisions, coords, gravitational_waves, habitable_zone, kepler, lagrange, lambert, magnetosphere, maneuvers, nbody, orbital_elements, tidal, time_systems + + diff --git a/bindings/python/python/numeria/astrophysics/collisions.pyi b/bindings/python/python/numeria/astrophysics/collisions.pyi new file mode 100644 index 0000000..d5a3b0d --- /dev/null +++ b/bindings/python/python/numeria/astrophysics/collisions.pyi @@ -0,0 +1,115 @@ +""" +Impacts, mergers, and collision probability. Impact geometry and speed (including the gravitational focusing that makes the impact speed at least the escape velocity, however slowly the bodies approach), perfectly inelastic merger of mass and momentum, and the energy released. Crater scaling and the collision probability for objects sharing a volume of space follow, along with the debris-flux relations used for orbital collision risk. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.math import Vec3 + +class CollisionKind: + """ + +Rust: `astrophysics::collisions::CollisionKind` + """ + ... + +class CollisionResult: + """ + +Rust: `astrophysics::collisions::CollisionResult` + """ + def __init__(self, kind: CollisionKind, merged_mass: float, merged_velocity: Vec3 | Sequence[float], merged_radius: float, temperature_increase: float, debris: DebrisParams) -> None: ... + @property + def kind(self) -> CollisionKind: ... + @property + def merged_mass(self) -> float: ... + @property + def merged_velocity(self) -> Vec3: ... + @property + def merged_radius(self) -> float: ... + @property + def temperature_increase(self) -> float: ... + @property + def debris(self) -> DebrisParams: ... + +class DebrisParams: + """ + +Rust: `astrophysics::collisions::DebrisParams` + """ + def __init__(self, count: int, speed_factor: float, mass_fraction: float, base_temperature: float) -> None: ... + @property + def count(self) -> int: ... + @property + def speed_factor(self) -> float: ... + @property + def mass_fraction(self) -> float: ... + @property + def base_temperature(self) -> float: ... + +def impact_angle(pos1: Vec3 | Sequence[float], vel1: Vec3 | Sequence[float], pos2: Vec3 | Sequence[float], vel2: Vec3 | Sequence[float]) -> float: + """ +Computes the impact angle between two colliding bodies: θ = acos(v_radial / |v_rel|). + +Rust: `astrophysics::collisions::impact_angle` + """ + ... + +def impact_speed(vel1: Vec3 | Sequence[float], vel2: Vec3 | Sequence[float]) -> float: + """ +Computes the relative impact speed between two bodies: |v1 - v2|. + +Rust: `astrophysics::collisions::impact_speed` + """ + ... + +def merge_velocity(m1: float, v1: Vec3 | Sequence[float], m2: float, v2: Vec3 | Sequence[float]) -> Vec3: + """ +Computes the post-merger velocity via conservation of momentum: v_cm = (m1 v1 + m2 v2) / (m1 + m2). + +Rust: `astrophysics::collisions::merge_velocity` + """ + ... + +def merge_radius(r1: float, r2: float) -> float: + """ +Computes the merged body radius assuming volume conservation: r = (r1³ + r2³)^(1/3). + +Rust: `astrophysics::collisions::merge_radius` + """ + ... + +def collision_energy(m1: float, v1: Vec3 | Sequence[float], m2: float, v2: Vec3 | Sequence[float]) -> float: + """ +Computes the kinetic energy available in the center-of-mass frame: KE_cm = Σ ½m_i |v_i - v_cm|². + +Rust: `astrophysics::collisions::collision_energy` + """ + ... + +def escape_speed(mass: float, radius: float) -> float: + """ +Computes the surface escape speed: v_esc = √(2GM/r). + +Rust: `astrophysics::collisions::escape_speed` + """ + ... + +def debris_params(kind: CollisionKind) -> DebrisParams: + """ +Returns debris generation parameters (count, speed, mass fraction, temperature) for a given collision type. + +Rust: `astrophysics::collisions::debris_params` + """ + ... + +def resolve_collision(m1: float, r1: float, v1: Vec3 | Sequence[float], m2: float, r2: float, v2: Vec3 | Sequence[float], kind: CollisionKind) -> CollisionResult: + """ +Resolves a collision between two bodies, computing the merged properties and debris parameters. + +Rust: `astrophysics::collisions::resolve_collision` + """ + ... diff --git a/bindings/python/python/numeria/astrophysics/coords.pyi b/bindings/python/python/numeria/astrophysics/coords.pyi new file mode 100644 index 0000000..8a31849 --- /dev/null +++ b/bindings/python/python/numeria/astrophysics/coords.pyi @@ -0,0 +1,286 @@ +""" +Astronomical coordinates, low-precision ephemerides and TLE parsing. # Four frames and what each is for *Equatorial* coordinates -- right ascension and declination -- are fixed to the stars, or nearly so, and are what a catalogue lists. *Horizontal* coordinates -- azimuth and altitude -- are what an observer sees, and depend on where and when they are looking. *Ecliptic* coordinates are referred to the Earth's orbital plane, which is the natural frame for anything in the solar system. And the *perifocal* and inertial frames of `astrophysics::kepler` are where orbits live. Converting between the first three is pure spherical trigonometry, and all of it is exactly invertible. Which is worth saying because the *ephemerides* here are not: they are truncated series good to a fraction of a degree, and their inverses do not exist in any useful sense. # What "low precision" means `sun_position_approx` is good to about a hundredth of a degree over a couple of centuries around J2000. `moon_position_approx` is good to a few tenths of a degree, because the Moon's motion has hundreds of terms of comparable size and this keeps a handful. `planet_position_low_precision` uses mean elements with linear rates and no perturbations at all, which is good to a fraction of a degree for the inner planets over a few centuries and steadily worse outward, where Jupiter and Saturn pull each other around by degrees. None of these is suitable for an occultation, a transit timing, or anything where arcseconds matter. They are for pointing a small telescope, checking whether a planet is up, and drawing a sky map. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +class Planet: + """ +The planets this module's low-precision ephemeris covers. + +Rust: `astrophysics::coords::Planet` + """ + ... + +class TleElements: + """ +The fields a two-line element set carries. + +Rust: `astrophysics::coords::TleElements` + """ + def __init__(self, catalog_number: int, designator: str, epoch_jd: float, mean_motion_dot: float, bstar: float, inclination: float, raan: float, eccentricity: float, arg_perigee: float, mean_anomaly: float, mean_motion: float, revolution: int) -> None: ... + @property + def catalog_number(self) -> int: ... + @property + def designator(self) -> str: ... + @property + def epoch_jd(self) -> float: ... + @property + def mean_motion_dot(self) -> float: ... + @property + def bstar(self) -> float: ... + @property + def inclination(self) -> float: ... + @property + def raan(self) -> float: ... + @property + def eccentricity(self) -> float: ... + @property + def arg_perigee(self) -> float: ... + @property + def mean_anomaly(self) -> float: ... + @property + def mean_motion(self) -> float: ... + @property + def revolution(self) -> int: ... + +def equatorial_to_horizontal(right_ascension: float, declination: float, latitude: float, local_sidereal_time: float) -> tuple[float, float]: + """ +Converts equatorial coordinates to horizontal, returning +`(azimuth, altitude)` in radians. + +Azimuth is measured from north through east, which is the navigator's +convention; astronomers sometimes measure from south, and the two +differ by half a turn. Altitude is positive above the horizon. + +The local hour angle `lst - ra` is what carries the time dependence: +it is zero when the object is due south, so an object is highest +exactly then. Everything else is one spherical triangle. + +No refraction. Near the horizon the atmosphere lifts an object by +about half a degree -- more than the Sun's own diameter -- so a +computed altitude of zero is a body that has already visibly set. + +Errors: +Returns an error for a non-finite input or a latitude outside +`[-pi/2, pi/2]`. + +Rust: `astrophysics::coords::equatorial_to_horizontal` + """ + ... + +def horizontal_to_equatorial(azimuth: float, altitude: float, latitude: float, local_sidereal_time: float) -> tuple[float, float]: + """ +Converts horizontal coordinates back to equatorial, returning +`(right ascension, declination)`. + +The exact inverse of `equatorial_to_horizontal`, which is worth +having as a separate function precisely so the pair can be checked +against each other. + +Errors: +As `equatorial_to_horizontal`, with the altitude taking the place of +the declination. + +Rust: `astrophysics::coords::horizontal_to_equatorial` + """ + ... + +def ecliptic_to_equatorial(ecliptic_longitude: float, ecliptic_latitude: float, obliquity: float) -> tuple[float, float]: + """ +Converts ecliptic coordinates to equatorial, returning +`(right ascension, declination)`. + +A rotation by the obliquity about the vernal equinox, and nothing +more. The ecliptic frame is where the planets nearly lie -- their +latitudes are a few degrees at most -- which is why an ephemeris +computes there and converts at the end. + +Errors: +Returns an error for a non-finite angle or a latitude outside +`[-pi/2, pi/2]`. + +Rust: `astrophysics::coords::ecliptic_to_equatorial` + """ + ... + +def equatorial_to_ecliptic(right_ascension: float, declination: float, obliquity: float) -> tuple[float, float]: + """ +Converts equatorial coordinates to ecliptic, returning +`(longitude, latitude)`. + +Errors: +As `ecliptic_to_equatorial`. + +Rust: `astrophysics::coords::equatorial_to_ecliptic` + """ + ... + +def mean_obliquity(jd: float) -> float: + """ +The mean obliquity of the ecliptic at a Julian date, by the IAU 1980 +polynomial. + +It decreases by about 47 arcseconds a century, which over the span of +recorded astronomy is enough to matter: the tropics have moved +measurably since the term was coined. + +Errors: +Returns an error for a non-finite or out-of-range Julian date. + +Rust: `astrophysics::coords::mean_obliquity` + """ + ... + +def precession_approx(right_ascension: float, declination: float, jd: float) -> tuple[float, float]: + """ +Precesses equatorial coordinates from J2000 to another epoch, to first +order in the precession angles. + +The equinox itself moves, at about 50 arcseconds a year, so a +catalogue position is meaningless without the epoch it belongs to. +This is the rigorous rotation truncated to its linear terms, which is +good to an arcsecond over a century and degrades quadratically beyond. + +It is a coordinate change, not a motion: the star has not moved, the +grid has. + +Errors: +Returns an error for a non-finite coordinate, a declination outside +`[-pi/2, pi/2]`, or an out-of-range date. + +Rust: `astrophysics::coords::precession_approx` + """ + ... + +def sun_position_approx(jd: float) -> tuple[float, float, float]: + """ +The Sun's apparent geocentric position, returning +`(right ascension, declination, distance in astronomical units)`. + +The low-precision series from the Astronomical Almanac: a mean +longitude, a mean anomaly, and two terms of the equation of centre. +Good to about a hundredth of a degree for a couple of centuries either +side of J2000, which is a hundredth of the Sun's own diameter. + +The declination is what drives the seasons, and it reaches the +obliquity at the solstices and zero at the equinoxes -- which is what +makes those the definitions of the days rather than consequences of +them. + +Errors: +Returns an error for a non-finite or out-of-range Julian date. + +Rust: `astrophysics::coords::sun_position_approx` + """ + ... + +def moon_position_approx(jd: float) -> tuple[float, float, float]: + """ +The Moon's apparent geocentric position, returning +`(right ascension, declination, distance in kilometres)`. + +A handful of the largest terms in longitude, latitude and distance: +the evection, the variation, the annual equation and the principal +latitude term. Good to a few tenths of a degree, which is about the +Moon's own diameter -- enough to say where it is in the sky and not +enough to predict an occultation. + +The Moon is the hardest classical ephemeris there is. Its orbit is +perturbed by the Sun at the percent level, and the full theory runs to +thousands of terms; what is kept here is the first page of a long +book. + +Errors: +Returns an error for a non-finite or out-of-range Julian date. + +Rust: `astrophysics::coords::moon_position_approx` + """ + ... + +def planet_position_low_precision(planet: Planet, jd: float) -> tuple[float, float, float]: + """ +A planet's heliocentric position, returning +`(ecliptic longitude, ecliptic latitude, distance in AU)`. + +Mean elements advanced linearly in time, Kepler's equation solved, and +the result rotated into the ecliptic. There are no mutual +perturbations at all, which is what makes this "low precision": the +inner planets come out within a fraction of a degree over a few +centuries around J2000, and Jupiter and Saturn drift by degrees over +the same span because they pull on each other and this does not know +it. + +The elements are the Standish set, whose stated validity is 1800 to +2050. Outside that window the answer degrades quickly and silently, +which is a property of the data rather than of the arithmetic. + +Errors: +Returns an error for a non-finite or out-of-range Julian date, or a +Kepler solve that fails. + +Rust: `astrophysics::coords::planet_position_low_precision` + """ + ... + +def rise_set_times(right_ascension: float, declination: float, latitude: float, longitude: float, jd: float, standard_altitude: float) -> Optional[tuple[float, float]]: + """ +The rise and set times of a body of fixed equatorial coordinates on a +given day, as Julian dates, or `None` if it never crosses the horizon. + +`standard_altitude` is the altitude counted as the horizon: zero for a +point source ignoring refraction, about -0.0145 radians (-50 +arcminutes) for the Sun's upper limb with mean refraction. + +`None` covers both circumpolar cases -- a body permanently up, and one +permanently down -- which are the same arithmetic: the required hour +angle has no cosine. That is the polar day and the polar night, and +which one it is can be told from the altitude at transit. + +The coordinates are held fixed over the day, which is fine for a star +and an approximation for the Sun, whose declination moves by up to +0.4 degrees between rise and set near an equinox. + +Errors: +Returns an error for a non-finite input, a latitude or declination out +of range, or an out-of-range date. + +Rust: `astrophysics::coords::rise_set_times` + """ + ... + +def tle_parse_lite(line1: str, line2: str) -> TleElements: + """ +Parses a two-line element set into its fields. + +**Parsing only.** The elements are not propagated, and they must not be +propagated by anything in this crate. A TLE's numbers are not osculating +orbital elements: they are *mean* elements in the specific sense defined +by the SGP4/SDP4 theory, with the periodic variations that theory models +already removed. Feeding them to a Kepler propagator -- including +`astrophysics::kepler::propagate_kepler` -- gives an answer +that looks reasonable and is wrong by kilometres within hours, because +the removed terms are exactly what would need adding back. + +SGP4 is therefore not "a better propagator to add later"; it is the +definition of what the numbers mean. Implementing it is a substantial +piece of work with its own deep-space branch, and it is out of scope +here rather than approximated. + +The exponential fields (`bstar` and the second derivative) use the +format's assumed-decimal-point convention: `12345-3` means +`0.12345e-3`. + +Errors: +Returns an error for lines of the wrong length or line number, a field +that will not parse, a checksum mismatch, or an epoch out of range. + +Rust: `astrophysics::coords::tle_parse_lite` + """ + ... + +OBLIQUITY_J2000: float diff --git a/bindings/python/python/numeria/astrophysics/gravitational_waves.pyi b/bindings/python/python/numeria/astrophysics/gravitational_waves.pyi new file mode 100644 index 0000000..cebfcea --- /dev/null +++ b/bindings/python/python/numeria/astrophysics/gravitational_waves.pyi @@ -0,0 +1,74 @@ +""" +Gravitational radiation from a compact binary. Quadrupole-formula results for an inspiralling binary: the emitted luminosity, the wave frequency (twice the orbital frequency), the strain amplitude at a given distance, and the time remaining to merger. The chirp mass `ℳ = (m₁m₂)^(3/5)/(m₁+m₂)^(1/5)` is the combination that governs all of them -- it is the parameter the inspiral waveform actually determines, which is why it is measured far better than either individual mass. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.math import Vec3 + +def gw_luminosity(m1: float, m2: float, separation: float) -> float: + """ +Computes gravitational wave luminosity for a circular binary: L = (32/5) G⁴m₁²m₂²(m₁+m₂) / (c⁵ a⁵). + +Rust: `astrophysics::gravitational_waves::gw_luminosity` + """ + ... + +def gw_frequency(m1: float, m2: float, separation: float) -> float: + """ +Computes the gravitational wave frequency (twice the orbital frequency): f_gw = (1/π)√(G(m₁+m₂)/a³). + +Rust: `astrophysics::gravitational_waves::gw_frequency` + """ + ... + +def gw_strain(m1: float, m2: float, separation: float, distance_to_observer: float) -> float: + """ +Computes the dimensionless gravitational wave strain amplitude: h = 4G²m₁m₂ / (c⁴ a D). + +Rust: `astrophysics::gravitational_waves::gw_strain` + """ + ... + +def inspiral_time(m1: float, m2: float, separation: float) -> float: + """ +Computes the Peters inspiral time for a circular binary: t = (5/256) c⁵ a⁴ / (G³ m₁ m₂ (m₁+m₂)). + +Rust: `astrophysics::gravitational_waves::inspiral_time` + """ + ... + +def chirp_mass(m1: float, m2: float) -> float: + """ +Computes the chirp mass of a binary system: M_c = (m₁ m₂)^(3/5) / (m₁ + m₂)^(1/5). + +Rust: `astrophysics::gravitational_waves::chirp_mass` + """ + ... + +def innermost_stable_circular_orbit(total_mass: float) -> float: + """ +Computes the Schwarzschild ISCO radius: r_isco = 6GM/c². + +Rust: `astrophysics::gravitational_waves::innermost_stable_circular_orbit` + """ + ... + +def find_strongest_source(masses: list[float], positions: list[Vec3 | Sequence[float]]) -> Optional[tuple[int, int, float]]: + """ +Finds the pair of bodies with the highest gravitational wave luminosity, returning their indices and luminosity. + +Rust: `astrophysics::gravitational_waves::find_strongest_source` + """ + ... + +def merger_energy(m1: float, m2: float) -> float: + """ +Estimates the energy radiated during merger: E = η M c², where η = m₁m₂/(m₁+m₂)². + +Rust: `astrophysics::gravitational_waves::merger_energy` + """ + ... diff --git a/bindings/python/python/numeria/astrophysics/habitable_zone.pyi b/bindings/python/python/numeria/astrophysics/habitable_zone.pyi new file mode 100644 index 0000000..f6b6e5b --- /dev/null +++ b/bindings/python/python/numeria/astrophysics/habitable_zone.pyi @@ -0,0 +1,65 @@ +""" +Habitable zone boundaries and tidal locking. Inner and outer edges scale as the square root of the stellar luminosity, with the conventional coefficients: 0.95 AU and 1.37 AU per square root of a solar luminosity. Also the mass-luminosity relation for main-sequence stars, equilibrium temperature for a given albedo, and the tidal locking timescale -- which matters here because low-mass stars have close-in habitable zones, so their habitable planets are likely to be locked. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def habitable_zone_inner(luminosity_solar: float) -> float: + """ +Computes the inner edge of the habitable zone in AU: d_inner = √L × 0.95. + +Rust: `astrophysics::habitable_zone::habitable_zone_inner` + """ + ... + +def habitable_zone_outer(luminosity_solar: float) -> float: + """ +Computes the outer edge of the habitable zone in AU: d_outer = √L × 1.37. + +Rust: `astrophysics::habitable_zone::habitable_zone_outer` + """ + ... + +def habitable_zone(luminosity_solar: float) -> tuple[float, float]: + """ +Returns the (inner, outer) habitable zone boundaries in AU for a given stellar luminosity in solar units. + +Rust: `astrophysics::habitable_zone::habitable_zone` + """ + ... + +def is_in_habitable_zone(luminosity_solar: float, distance_au: float) -> bool: + """ +Returns true if a body at the given distance (AU) lies within the habitable zone. + +Rust: `astrophysics::habitable_zone::is_in_habitable_zone` + """ + ... + +def luminosity_from_mass(mass_solar: float) -> float: + """ +Estimates stellar luminosity from mass using the mass-luminosity relation: L = M^3.5 (in solar units). + +Rust: `astrophysics::habitable_zone::luminosity_from_mass` + """ + ... + +def luminosity_from_temperature_radius(temperature: float, radius_solar: float) -> float: + """ +Computes stellar luminosity from the Stefan-Boltzmann law: L = R² (T/T_sun)⁴ (in solar units). + +Rust: `astrophysics::habitable_zone::luminosity_from_temperature_radius` + """ + ... + +SOLAR_LUMINOSITY: float + +SOLAR_TEMPERATURE: float + +HZ_INNER_COEFFICIENT: float + +HZ_OUTER_COEFFICIENT: float diff --git a/bindings/python/python/numeria/astrophysics/kepler.pyi b/bindings/python/python/numeria/astrophysics/kepler.pyi new file mode 100644 index 0000000..a044238 --- /dev/null +++ b/bindings/python/python/numeria/astrophysics/kepler.pyi @@ -0,0 +1,177 @@ +""" +Kepler's equation, anomaly conversions and two-body propagation. # Three anomalies and why there are three An orbit's position is described by an angle, and three different angles are useful for different things. *True anomaly* is the physical angle from periapsis to the body, seen from the focus -- it is what a telescope measures and what converts directly to a position. *Mean anomaly* advances uniformly in time, `M = n (t - t_p)`, so it is what a clock gives. *Eccentric anomaly* is the intermediate angle on the circumscribing circle that connects the two, and it exists because no closed form connects the other two directly. Kepler's equation `M = E - e sin E` is the link, and it is transcendental. Everything in orbital mechanics that looks like "where will it be at time t" bottoms out in solving it, which is why five centuries of work have gone into doing so quickly. # What is not here `astrophysics::orbital_elements` already provides the element set, the state-to-elements conversion and the geometric quantities read off an orbit; this module adds the time dependence and the inverse conversion, and does not repeat them. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.astrophysics.orbital_elements import OrbitalElements +from numeria.math import Vec3 + +def kepler_solve_elliptic(mean_anomaly: float, e: float, tol: float) -> float: + """ +Solves `M = E - e sin E` for the eccentric anomaly. + +Newton's method from a seed that keeps it in the basin: for nearly +circular orbits `M` itself is already close, and for high +eccentricities the standard `M + e sin M` correction is not -- near +periapsis at `e = 0.99` the function is almost flat in `E` and a naive +seed sends the first step far outside `[0, 2 pi)`. The seed here is +Danby's, which is chosen to converge for every eccentricity below one. + +Returns the anomaly in `[0, 2 pi]`. + +Errors: +Returns an error for an eccentricity outside `[0, 1)`, a non-finite +mean anomaly or tolerance, a non-positive tolerance, or an iteration +that fails to converge. + +Rust: `astrophysics::kepler::kepler_solve_elliptic` + """ + ... + +def kepler_solve_hyperbolic(mean_anomaly: float, e: float, tol: float) -> float: + """ +Solves the hyperbolic Kepler equation `M = e sinh H - H`. + +The hyperbolic form has no periodicity to wrap, and `sinh` grows +exponentially, so a poor seed overflows rather than merely converging +slowly. The seed here is logarithmic for large `M`, which is where the +solution actually lives. + +Errors: +Returns an error for an eccentricity at or below one, a non-finite +mean anomaly or tolerance, a non-positive tolerance, or an iteration +that fails to converge. + +Rust: `astrophysics::kepler::kepler_solve_hyperbolic` + """ + ... + +def true_from_eccentric(eccentric: float, e: float) -> float: + """ +The true anomaly corresponding to an eccentric anomaly. + +`tan(nu/2) = sqrt((1+e)/(1-e)) tan(E/2)`, evaluated through `atan2` so +it stays correct across all four quadrants rather than losing a half +turn where the tangent wraps. + +Errors: +Returns an error for an eccentricity outside `[0, 1)` or a non-finite +anomaly. + +Rust: `astrophysics::kepler::true_from_eccentric` + """ + ... + +def eccentric_from_true(true_anomaly: float, e: float) -> float: + """ +The eccentric anomaly corresponding to a true anomaly. + +Errors: +As `true_from_eccentric`. + +Rust: `astrophysics::kepler::eccentric_from_true` + """ + ... + +def mean_from_eccentric(eccentric: float, e: float) -> float: + """ +The mean anomaly corresponding to an eccentric anomaly: Kepler's +equation read forwards, which needs no solving at all. + +Errors: +As `true_from_eccentric`. + +Rust: `astrophysics::kepler::mean_from_eccentric` + """ + ... + +def orbit_period(a: float, mu: float) -> float: + """ +The orbital period `2 pi sqrt(a^3 / mu)`. + +Errors: +Returns an error for a non-positive semi-major axis or gravitational +parameter, which is to say for an unbound orbit, where there is no +period. + +Rust: `astrophysics::kepler::orbit_period` + """ + ... + +def vis_viva(r: float, a: float, mu: float) -> float: + """ +The vis-viva speed at radius `r` on an orbit of semi-major axis `a`: +`sqrt(mu (2/r - 1/a))`. + +The equation is conservation of energy rearranged, and it holds for +every conic: a positive `a` for an ellipse, negative for a hyperbola, +and the parabolic limit `1/a = 0` giving escape speed. That one formula +covers all three is the reason it is the workhorse of manoeuvre +planning. + +The formula knows about energy, not about geometry: it returns a speed +for any radius up to `2a`, which for a bound orbit reaches past +apoapsis at `a(1+e)`. Radii between the two are not on the orbit and +the number returned there is the speed a body of that energy *would* +have, not one anything reaches. Beyond `2a` the kinetic energy would be +negative and there is no answer at all. + +Errors: +Returns an error for a non-positive radius or gravitational parameter, +a NaN input, or a radius beyond `2a` on a bound orbit, where the speed +would be imaginary. + +Rust: `astrophysics::kepler::vis_viva` + """ + ... + +def state_from_elements(elements: OrbitalElements | Sequence[float], mu: float) -> tuple[Vec3, Vec3]: + """ +The state vectors implied by a set of elements: the inverse of +`OrbitalElements::from_state_vectors`. + +The position and velocity are built in the perifocal frame, where the +orbit is a plane conic with periapsis along the x axis, and then +rotated into the reference frame by the three Euler angles. Doing it +this way rather than by direct formulae is what keeps the retrograde +and equatorial cases right: the rotation is the same in every case, +and only the angles differ. + +Errors: +Returns an error for a non-positive gravitational parameter, a +non-finite element, a negative eccentricity, or a semi-latus rectum +that comes out non-positive -- which happens for a degenerate orbit +with no extent. + +Rust: `astrophysics::kepler::state_from_elements` + """ + ... + +def propagate_kepler(r0: Vec3 | Sequence[float], v0: Vec3 | Sequence[float], dt: float, mu: float) -> tuple[Vec3, Vec3]: + """ +Propagates a two-body state forward by `dt` using Lagrange's f and g +functions. + +The trick is that the new position is a *linear combination of the old +position and velocity*: `r = f r0 + g v0`, with `f` and `g` scalars +depending only on the change in eccentric anomaly. The orbit plane is +therefore preserved exactly by construction, whatever the arithmetic +does -- which is why this is used in preference to integrating the +equations of motion when the two-body assumption holds. + +Elliptic and hyperbolic orbits are handled by their own anomaly +solvers. A parabolic orbit -- eccentricity exactly one -- has neither +and is refused rather than approximated. + +Errors: +Returns an error for a non-positive gravitational parameter, a +non-finite input, a degenerate or parabolic orbit, or an anomaly +solver that does not converge. + +Rust: `astrophysics::kepler::propagate_kepler` + """ + ... diff --git a/bindings/python/python/numeria/astrophysics/lagrange.pyi b/bindings/python/python/numeria/astrophysics/lagrange.pyi new file mode 100644 index 0000000..5315178 --- /dev/null +++ b/bindings/python/python/numeria/astrophysics/lagrange.pyi @@ -0,0 +1,42 @@ +""" +The five Lagrange points of the circular restricted three-body problem. L1, L2 and L3 lie on the line through the two masses and are found by solving a quintic numerically; L4 and L5 sit at the vertices of equilateral triangles with the two masses and are exact. The collinear points are unstable saddles -- a spacecraft there needs station-keeping -- while L4 and L5 are stable for a mass ratio below about 1/24.96, which is why Jupiter's Trojan asteroids stay put. The Hill radius is here as well. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.math import Vec3 + +def hill_radius(distance: float, body_mass: float, primary_mass: float) -> float: + """ +Computes the Hill sphere radius: r_H = d (m / 3M)^(1/3). + +Rust: `astrophysics::lagrange::hill_radius` + """ + ... + +def lagrange_points(primary_pos: Vec3 | Sequence[float], primary_mass: float, body_pos: Vec3 | Sequence[float], body_vel: Vec3 | Sequence[float], body_mass: float) -> list[Vec3]: + """ +Computes all five Lagrange points (L1-L5) for a two-body system in the co-rotating frame. + +Rust: `astrophysics::lagrange::lagrange_points` + """ + ... + +def circular_velocity(mu: float, distance: float) -> float: + """ +Computes the circular orbital velocity: v_c = √(μ/r). + +Rust: `astrophysics::lagrange::circular_velocity` + """ + ... + +def escape_ratio(speed: float, escape_vel: float) -> float: + """ +Computes the ratio of current speed to escape velocity: v / v_esc. + +Rust: `astrophysics::lagrange::escape_ratio` + """ + ... diff --git a/bindings/python/python/numeria/astrophysics/lambert.pyi b/bindings/python/python/numeria/astrophysics/lambert.pyi new file mode 100644 index 0000000..36cf26a --- /dev/null +++ b/bindings/python/python/numeria/astrophysics/lambert.pyi @@ -0,0 +1,98 @@ +""" +Lambert's problem: the orbit connecting two positions in a given time. # The problem and why it is hard Given where a spacecraft is, where it must be, and how long it has to get there, find the transfer orbit. Stated that way it sounds like `astrophysics::kepler::propagate_kepler` run backwards, but it is a genuinely different problem: propagation is an initial-value problem with one answer, and Lambert's is a *boundary*-value problem whose answer need not be unique. It is not, however, a problem of existence. Within a single revolution a transfer exists for every positive flight time: making the trip faster costs more energy without limit, and the minimum-energy transfer is a particular duration rather than a floor on one. What *does* fail is a degenerate geometry -- a transfer angle of zero or exactly `pi`, where the two radii do not determine a plane and infinitely many orbits connect the points. Only the zero-revolution solution is computed here, which is the one interplanetary trajectory design starts from. Multi-revolution transfers exist for longer flight times and are a separate search, with two branches per revolution count; they are not attempted rather than approximated. # The universal-variable formulation Every conic is covered by one iteration, on a variable `z` that is positive for an ellipse, negative for a hyperbola and zero for a parabola. The Stumpff functions `C(z)` and `S(z)` carry the difference, and their series expansions near zero are what keep the parabolic case from losing precision to cancellation -- the closed forms are `0/0` there. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.math import Vec3 + +def stumpff_c(z: float) -> float: + """ +The Stumpff function `C(z)`. + +`(1 - cos sqrt(z))/z` for positive `z` and the hyperbolic analogue for +negative, both of which are `0/0` at the origin. The series +`1/2 - z/24 + z^2/720 - ...` is used near zero, where the closed forms +lose their leading digits to cancellation, and the positive branch is +evaluated as `2 sin^2(sqrt(z)/2)/z` so that it stays accurate at the +other end of the range as well. + +Rust: `astrophysics::lambert::stumpff_c` + """ + ... + +def stumpff_s(z: float) -> float: + """ +The Stumpff function `S(z)`. + +`(sqrt(z) - sin sqrt(z))/z^(3/2)` for positive `z`, with the series +`1/6 - z/120 + z^2/5040 - ...` near the origin for the same reason as +`stumpff_c` -- and worse, since the numerator there is a difference +of two nearly equal quantities that agree to three orders. + +Rust: `astrophysics::lambert::stumpff_s` + """ + ... + +def lambert_universal(r1: Vec3 | Sequence[float], r2: Vec3 | Sequence[float], tof: float, mu: float, prograde: bool) -> tuple[Vec3, Vec3]: + """ +Solves Lambert's problem by universal variables, returning the +departure and arrival velocities. + +`prograde` selects the transfer direction: true takes the short way +round in the sense of increasing right ascension, false the long way. +The two are genuinely different orbits with different flight paths and +different costs, and which one is wanted is not deducible from the +endpoints -- the transfer angle is `theta` one way and `2 pi - theta` +the other. + +The iteration is bisection on `z`. Bisection rather than Newton because +the flight time is monotone in `z`, so bisection cannot fail, and the +derivative a Newton step needs is itself delicate near the parabolic +point. + +Accuracy degrades as the transfer angle approaches `pi`. The +velocities are recovered as `(r2 - f r1)/g`, and near a half turn +`f` approaches one with `r2` near `-r1`, so the numerator is a +difference of nearly equal vectors. Over three thousand randomised +geometries the worst departure velocity was off by a part in 1e8, and +that case had a transfer angle of 179.99 degrees. Exactly `pi` is +refused; the approach to it is merely imprecise. + +Errors: +Returns an error for a non-positive gravitational parameter or flight +time, a position at the origin, or a transfer angle of zero or exactly +`pi`, where the plane is undefined and infinitely many orbits connect +the points. + +Rust: `astrophysics::lambert::lambert_universal` + """ + ... + +def porkchop_data(departures: list[tuple[float, Vec3 | Sequence[float], Vec3 | Sequence[float]]], arrivals: list[tuple[float, Vec3 | Sequence[float], Vec3 | Sequence[float]]], mu: float, prograde: bool) -> list[list[Optional[float]]]: + """ +A porkchop grid of departure characteristic energies. + +Entry `[i][j]` is the departure `C3 = v_infinity^2` for leaving +`departures[i]` and arriving at `arrivals[j]`, with the flight time +taken as the difference of their epochs. `None` marks a pair with no +transfer: a non-positive flight time, a degenerate geometry, or a +duration outside what one revolution allows. + +`C3` rather than delta-v because it is what a launch vehicle's +performance is quoted against: the energy left over after escaping, +which is what the upper stage must supply. The characteristic ridges +and islands of a real porkchop plot come from the two branches of the +transfer -- Type I below a half revolution and Type II above -- meeting +where the transfer angle passes `pi` and the solution degenerates. + +Errors: +Returns an error for an empty grid, a non-positive gravitational +parameter, or more than a million cells. + +Rust: `astrophysics::lambert::porkchop_data` + """ + ... diff --git a/bindings/python/python/numeria/astrophysics/magnetosphere.pyi b/bindings/python/python/numeria/astrophysics/magnetosphere.pyi new file mode 100644 index 0000000..b1bc417 --- /dev/null +++ b/bindings/python/python/numeria/astrophysics/magnetosphere.pyi @@ -0,0 +1,73 @@ +""" +Planetary dipole fields and the magnetopause. The magnetic dipole field in vector form, field-line tracing by integration along the field, and the magnetopause standoff distance -- where magnetic pressure balances the solar wind's dynamic pressure, which is what sets the size of a magnetosphere. Field strength falls as `1/r³`, so the standoff distance depends only weakly (as the sixth root) on the wind pressure. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.math import Vec3 + +class CelestialBodyType: + """ + +Rust: `astrophysics::magnetosphere::CelestialBodyType` + """ + ... + +def magnetic_moment(body_type: CelestialBodyType, mass: float, temperature: float) -> float: + """ +Estimates a celestial body's magnetic dipole moment based on body type, mass, and temperature. + +Rust: `astrophysics::magnetosphere::magnetic_moment` + """ + ... + +def magnetosphere_radius(collision_radius: float, moment: float) -> float: + """ +Computes the magnetosphere standoff radius from the magnetic moment and body radius. + +Rust: `astrophysics::magnetosphere::magnetosphere_radius` + """ + ... + +def dipole_field(center: Vec3 | Sequence[float], moment_vec: Vec3 | Sequence[float], point: Vec3 | Sequence[float]) -> Vec3: + """ +Computes the magnetic dipole field at a point: B = (3(m·r̂)r̂ - m) / r³. + +Rust: `astrophysics::magnetosphere::dipole_field` + """ + ... + +def total_field(centers: list[Vec3 | Sequence[float]], moments: list[Vec3 | Sequence[float]], point: Vec3 | Sequence[float]) -> Vec3: + """ +Computes the superposition of multiple magnetic dipole fields at a point. + +Rust: `astrophysics::magnetosphere::total_field` + """ + ... + +def trace_field_line(centers: list[Vec3 | Sequence[float]], moments: list[Vec3 | Sequence[float]], seed: Vec3 | Sequence[float], forward: bool, step_size: float, max_distance: float, max_points: int, min_field_strength: float, body_radii: list[float]) -> list[tuple[Vec3, float]]: + """ +Traces a magnetic field line from a seed point using adaptive Euler stepping, returning positions and field strengths. + +Rust: `astrophysics::magnetosphere::trace_field_line` + """ + ... + +def generate_seed_points(center: Vec3 | Sequence[float], radius: float, num_seeds: int) -> list[Vec3]: + """ +Generates seed points on a sphere using the golden-angle spiral for uniform distribution. + +Rust: `astrophysics::magnetosphere::generate_seed_points` + """ + ... + +DEFAULT_MAX_LINES_PER_BODY: int + +DEFAULT_POINTS_PER_LINE: int + +DEFAULT_MIN_FIELD_STRENGTH: float + +SOLAR_TEMPERATURE: float diff --git a/bindings/python/python/numeria/astrophysics/maneuvers.pyi b/bindings/python/python/numeria/astrophysics/maneuvers.pyi new file mode 100644 index 0000000..7298411 --- /dev/null +++ b/bindings/python/python/numeria/astrophysics/maneuvers.pyi @@ -0,0 +1,191 @@ +""" +Orbital manoeuvres: combined burns, patched conics, gravity assists and the perturbation that dominates low orbits. # What lives elsewhere The impulsive transfers themselves are already in `propulsion`: `hohmann_delta_v`, `hohmann_transfer_time`, `bi_elliptic_delta_v`, `delta_v_plane_change`, `tsiolkovsky_delta_v` and `delta_v_staged`. The Roche limit is in `astrophysics::tidal` and the Hill radius in `astrophysics::lagrange`. This module adds what those do not cover, and reuses rather than repeats them. # Why delta-v is the currency Every manoeuvre here is priced in velocity change rather than in fuel, because the conversion between them is exponential: Tsiolkovsky's equation says the mass ratio is `e^(dv/v_e)`, so a mission's delta-v budget is a linear quantity that adds up while its mass is not. Ten per cent more delta-v is not ten per cent more spacecraft. The other consequence is the Oberth effect. A burn's *energy* gain is `v dv`, proportional to the speed you already have, so the same delta-v spent deep in a gravity well buys far more energy than the same delta-v spent far from it. That is why escape burns are made at periapsis and why a flyby is worth planning around. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.math import Vec3 + +def combined_maneuver(v1: float, v2: float, plane_change: float) -> float: + """ +The delta-v of a combined speed change and plane change, by the law of +cosines. + +`sqrt(v1^2 + v2^2 - 2 v1 v2 cos(di))`. Doing both at once is always +cheaper than doing them one after the other, because the two vector +changes partly cancel -- the triangle inequality, applied to velocity. +The saving is largest when the plane change is large, which is why an +inclination change is combined with an apoapsis burn wherever the +mission allows. + +Errors: +Returns an error for a negative speed or a non-finite input. + +Rust: `astrophysics::maneuvers::combined_maneuver` + """ + ... + +def sphere_of_influence(distance: float, body_mass: float, primary_mass: float) -> float: + """ +The radius of a body's sphere of influence: +`a (m_body / m_primary)^(2/5)`. + +Inside it the body's gravity dominates the primary's for the purposes +of a patched-conic approximation, and outside it does not. The +two-fifths power is not the equal-force radius, which would be a +square root: it comes from comparing the *perturbing* accelerations +rather than the direct ones, and it is the boundary at which +switching which body you orbit makes the smaller error. + +The sphere is a fiction. Gravity has no boundary, and a real +trajectory feels both bodies throughout; the patched conic is an +approximation whose error is largest exactly at the crossing, where +the neglected body's pull is at its relative peak. + +Errors: +Returns an error for a non-positive distance or mass, or a body more +massive than its primary. + +Rust: `astrophysics::maneuvers::sphere_of_influence` + """ + ... + +def patched_conic_escape(parking_radius: float, mu: float, v_infinity: float) -> float: + """ +The delta-v to leave a circular parking orbit on a hyperbola with the +given excess speed: `sqrt(v_infinity^2 + 2 mu / r) - sqrt(mu / r)`. + +The first term is the speed needed at radius `r` to arrive at infinity +still moving at `v_infinity`; the second is what a circular orbit +already provides. The gap is small compared with either, which is the +Oberth effect in its most practical form: escaping from low orbit +costs about 0.41 of the circular speed, and the deeper the parking +orbit the smaller that fraction becomes. + +Errors: +Returns an error for a non-positive radius or gravitational parameter, +a negative excess speed, or a non-finite input. + +Rust: `astrophysics::maneuvers::patched_conic_escape` + """ + ... + +def gravity_assist_deflection(v_infinity: float, periapsis: float, mu: float) -> float: + """ +The turn angle of a hyperbolic flyby: +`2 arcsin(1 / (1 + r_p v_inf^2 / mu))`. + +A gravity assist changes the direction of the excess velocity, not its +magnitude -- in the *planet's* frame the spacecraft arrives and leaves +at the same speed. The gain is in the sun's frame, where rotating the +excess velocity vector adds or subtracts from the planet's orbital +motion, and the planet loses exactly as much momentum as the +spacecraft gains. + +The turn is largest for a slow approach and a close pass. A fast +spacecraft is barely deflected, which is why an assist buys less the +more energy you already have. + +Errors: +Returns an error for a non-positive periapsis, gravitational parameter +or excess speed, or a non-finite input. + +Rust: `astrophysics::maneuvers::gravity_assist_deflection` + """ + ... + +def oberth_effect_dv(speed: float, delta_v: float, radius: float, mu: float) -> float: + """ +The speed after an impulsive burn of `delta_v` made at radius `r`, +through the energy it buys. + +The point of the function is the comparison it makes possible: the same +delta-v spent at two radii leaves the craft with different energies, +and the difference is `v dv` -- large where `v` is large, which is deep +in the well. Burning at periapsis rather than apoapsis can double the +escape energy for the same fuel. + +Errors: +Returns an error for a non-positive radius or gravitational parameter, +a negative speed, or a non-finite input. + +Rust: `astrophysics::maneuvers::oberth_effect_dv` + """ + ... + +def j2_raan_drift(a: float, e: float, inclination: float, j2: float, body_radius: float, mu: float) -> float: + """ +The nodal regression rate from the Earth's oblateness, in radians per +second. + +`-3/2 n J2 (R/p)^2 cos(i)`, with `p = a(1 - e^2)` and `n` the mean +motion. The `cos i` is what makes the whole thing useful: the drift is +westward for a prograde orbit, zero at exactly ninety degrees, and +eastward beyond it. A retrograde orbit at the right inclination +therefore drifts eastward at precisely the rate the Earth goes round +the sun -- see `sun_synchronous_inclination`. + +J2 dominates every other perturbation in low orbit by three orders of +magnitude, which is why a first-order treatment of it is worth more +than a careful treatment of anything else. + +Errors: +Returns an error for a non-positive semi-major axis, body radius or +gravitational parameter, an eccentricity outside `[0, 1)`, or a +non-finite input. + +Rust: `astrophysics::maneuvers::j2_raan_drift` + """ + ... + +def sun_synchronous_inclination(a: float, e: float, j2: float, body_radius: float, mu: float, drift_per_second: float) -> float: + """ +The inclination at which J2 makes an orbit sun-synchronous. + +The node must drift eastward by one turn a year, which is +`1.991e-7 rad/s`. Solving `j2_raan_drift` for the inclination gives +a value just past ninety degrees -- about 98 degrees for a low orbit -- +and it must be retrograde, since a prograde orbit's node drifts the +wrong way. + +The orbit is sun-synchronous in the sense that it crosses the equator +at the same local solar time every pass, which is what makes imaging +comparable between days. It says nothing about lighting at high +latitudes, where the geometry differs. + +Errors: +Returns an error for a non-positive semi-major axis, body radius or +gravitational parameter, an eccentricity outside `[0, 1)`, or an orbit +for which no inclination gives the required drift -- which happens +when the orbit is too high for J2 to turn it fast enough. + +Rust: `astrophysics::maneuvers::sun_synchronous_inclination` + """ + ... + +def ground_track(r0: Vec3 | Sequence[float], v0: Vec3 | Sequence[float], mu: float, rotation_rate: float, duration: float, samples: int) -> list[tuple[float, float]]: + """ +The ground track of an orbit: `(longitude, latitude)` in radians at +each sample, accounting for the body turning underneath. + +The longitude drift per orbit is what makes a track a spiral rather +than a closed curve: the body turns by `rotation_rate * period` while +the orbit plane stays put, so each pass crosses the equator further +west. A track closes only when the period is a rational fraction of +the rotation, which is what a repeat-ground-track orbit is designed +for. + +The latitude never exceeds the inclination, and reaches it exactly +twice per orbit. That bound is the reason a polar orbit is needed to +see the poles at all. + +Errors: +Returns an error for a bad state, a non-positive gravitational +parameter, no samples, more than a million, or a propagation failure. + +Rust: `astrophysics::maneuvers::ground_track` + """ + ... diff --git a/bindings/python/python/numeria/astrophysics/nbody.pyi b/bindings/python/python/numeria/astrophysics/nbody.pyi new file mode 100644 index 0000000..6ab063a --- /dev/null +++ b/bindings/python/python/numeria/astrophysics/nbody.pyi @@ -0,0 +1,120 @@ +""" +Direct N-body gravitational simulation. Velocity Verlet integration, chosen because it is symplectic: it conserves a nearby "shadow" energy exactly rather than drifting, so orbits stay closed over long integrations where Runge-Kutta of the same order would spiral. Softening replaces `1/r²` with `1/(r² + ε²)` to keep close encounters from producing unbounded accelerations, at the cost of biasing the force at short range. Includes energy and momentum diagnostics, and system generators. Cost is O(N²) per step. For large N use `astrophysics::octree`. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.math import Vec3 + +class Body: + """ + +Rust: `astrophysics::nbody::Body` + """ + def __init__(self, id: int, mass: float, radius: float, position: Vec3 | Sequence[float], velocity: Vec3 | Sequence[float]) -> None: ... + def kinetic_energy(self) -> float: ... + @property + def id(self) -> int: ... + @property + def mass(self) -> float: ... + @property + def radius(self) -> float: ... + @property + def position(self) -> Vec3: ... + @property + def velocity(self) -> Vec3: ... + @property + def acceleration(self) -> Vec3: ... + +class NBodySystem: + """ + +Rust: `astrophysics::nbody::NBodySystem` + """ + def __init__(self, bodies: list[Body], dt: float, softening: float) -> None: ... + def step(self) -> None: ... + def total_energy(self) -> float: ... + def center_of_mass(self) -> Vec3: ... + def total_momentum(self) -> Vec3: ... + @property + def bodies(self) -> list[Body]: ... + @property + def dt(self) -> float: ... + @property + def softening(self) -> float: ... + @property + def time(self) -> float: ... + +def compute_acceleration(bodies: list[Body], idx: int, softening: float) -> Vec3: + """ +Computes gravitational acceleration on body `idx` via direct O(N) pairwise summation with Plummer softening. + +Rust: `astrophysics::nbody::compute_acceleration` + """ + ... + +def init_accelerations(bodies: MutableSequence[Body], softening: float) -> None: + """ +Initializes acceleration vectors for all bodies by computing pairwise gravitational interactions. + +Rust: `astrophysics::nbody::init_accelerations` + """ + ... + +def step_verlet(bodies: MutableSequence[Body], dt: float, softening: float) -> None: + """ +Performs one velocity Verlet integration step (kick-drift-kick) by +delegating to the generic symplectic integrator +`numerical::ode::symplectic::velocity_verlet` over the flattened +phase-space state. + +Rust: `astrophysics::nbody::step_verlet` + """ + ... + +def kinetic_energy(bodies: list[Body]) -> float: + """ +Computes the total kinetic energy of all bodies: KE = Σ ½m_i v_i². + +Rust: `astrophysics::nbody::kinetic_energy` + """ + ... + +def potential_energy(bodies: list[Body], softening: float) -> float: + """ +Computes the total gravitational potential energy: PE = -Σ G m_i m_j / r_ij (with Plummer softening). + +Rust: `astrophysics::nbody::potential_energy` + """ + ... + +def total_energy(bodies: list[Body], softening: float) -> float: + """ +Computes the total mechanical energy as the sum of kinetic and potential energy. + +Rust: `astrophysics::nbody::total_energy` + """ + ... + +def center_of_mass(bodies: list[Body]) -> Vec3: + """ +Computes the center of mass position: R_cm = Σ(m_i r_i) / Σ(m_i). + +Rust: `astrophysics::nbody::center_of_mass` + """ + ... + +def total_momentum(bodies: list[Body]) -> Vec3: + """ +Computes the total linear momentum: p = Σ(m_i v_i). + +Rust: `astrophysics::nbody::total_momentum` + """ + ... + +DEFAULT_DT: float + +DEFAULT_SOFTENING: float diff --git a/bindings/python/python/numeria/astrophysics/orbital_elements.pyi b/bindings/python/python/numeria/astrophysics/orbital_elements.pyi new file mode 100644 index 0000000..4c36f92 --- /dev/null +++ b/bindings/python/python/numeria/astrophysics/orbital_elements.pyi @@ -0,0 +1,155 @@ +""" +Keplerian elements: conversion, propagation, and the anomalies. State vectors to elements and back -- semi-major axis, eccentricity, inclination, longitude of ascending node, argument of periapsis and true anomaly -- via the specific orbital energy, the angular momentum and the eccentricity vector. The three anomalies (true, eccentric and mean) and the conversions between them, with Kepler's equation solved by Newton iteration. Periapsis and apoapsis distances and speeds, orbital period, and propagation forward in time complete the module. For a solver that also handles parabolic and hyperbolic orbits and near e = 1, see `astrophysics::kepler`. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.math import Vec3 + +class OrbitalElements: + """ + +Rust: `astrophysics::orbital_elements::OrbitalElements` + """ + def __init__(self, semi_major_axis: float, eccentricity: float, inclination: float, longitude_ascending_node: float, argument_periapsis: float, true_anomaly: float) -> None: ... + @staticmethod + def from_state_vectors(position: Vec3 | Sequence[float], velocity: Vec3 | Sequence[float], mu: float) -> OrbitalElements: ... + def is_bound(self) -> bool: ... + def period(self, mu: float) -> float: ... + def periapsis(self) -> float: ... + def apoapsis(self) -> Optional[float]: ... + @property + def semi_major_axis(self) -> float: ... + @property + def eccentricity(self) -> float: ... + @property + def inclination(self) -> float: ... + @property + def longitude_ascending_node(self) -> float: ... + @property + def argument_periapsis(self) -> float: ... + @property + def true_anomaly(self) -> float: ... + +def specific_orbital_energy(mu: float, r: float, v: float) -> float: + """ +Computes specific orbital energy: ε = v²/2 - μ/r. + +Rust: `astrophysics::orbital_elements::specific_orbital_energy` + """ + ... + +def specific_angular_momentum(position: Vec3 | Sequence[float], velocity: Vec3 | Sequence[float]) -> Vec3: + """ +Computes specific angular momentum vector: h = r × v. + +Rust: `astrophysics::orbital_elements::specific_angular_momentum` + """ + ... + +def eccentricity_vector(position: Vec3 | Sequence[float], velocity: Vec3 | Sequence[float], mu: float) -> Vec3: + """ +Computes the eccentricity vector: e = (v × h)/μ - r̂, pointing toward periapsis. + +Rust: `astrophysics::orbital_elements::eccentricity_vector` + """ + ... + +def eccentricity(position: Vec3 | Sequence[float], velocity: Vec3 | Sequence[float], mu: float) -> float: + """ +Computes the orbital eccentricity as the magnitude of the eccentricity vector: e = |e_vec|. + +Rust: `astrophysics::orbital_elements::eccentricity` + """ + ... + +def semi_major_axis(mu: float, energy: float) -> float: + """ +Computes the semi-major axis from the vis-viva relation: a = -μ/(2ε). Returns infinity for parabolic orbits. + +Rust: `astrophysics::orbital_elements::semi_major_axis` + """ + ... + +def semi_minor_axis(semi_major: float, ecc: float) -> float: + """ +Computes the semi-minor axis: b = a√(1 - e²). + +Rust: `astrophysics::orbital_elements::semi_minor_axis` + """ + ... + +def periapsis(semi_major: float, ecc: float) -> float: + """ +Computes the periapsis distance: r_p = a(1 - e). + +Rust: `astrophysics::orbital_elements::periapsis` + """ + ... + +def apoapsis(semi_major: float, ecc: float) -> float: + """ +Computes the apoapsis distance: r_a = a(1 + e). + +Rust: `astrophysics::orbital_elements::apoapsis` + """ + ... + +def true_anomaly(position: Vec3 | Sequence[float], velocity: Vec3 | Sequence[float], mu: float) -> float: + """ +Computes the true anomaly ν from state vectors: ν = acos(e · r / (|e||r|)), adjusted for quadrant. + +Rust: `astrophysics::orbital_elements::true_anomaly` + """ + ... + +def inclination(angular_momentum: Vec3 | Sequence[float]) -> float: + """ +Computes the orbital inclination: i = acos(h_z / |h|). + +Rust: `astrophysics::orbital_elements::inclination` + """ + ... + +def longitude_of_ascending_node(angular_momentum: Vec3 | Sequence[float]) -> float: + """ +Computes the longitude of the ascending node Ω from the nodal vector n = (-h_y, h_x, 0). + +Rust: `astrophysics::orbital_elements::longitude_of_ascending_node` + """ + ... + +def argument_of_periapsis(angular_momentum: Vec3 | Sequence[float], ecc_vec: Vec3 | Sequence[float]) -> float: + """ +Computes the argument of periapsis ω: ω = acos(n · e / (|n||e|)), adjusted for quadrant. + +Rust: `astrophysics::orbital_elements::argument_of_periapsis` + """ + ... + +def orbit_points_ellipse(elements: OrbitalElements | Sequence[float], mu: float, num_points: int) -> list[Vec3]: + """ +Generates 3D points along an elliptical orbit using the conic section r = p/(1 + e cos θ). + +Rust: `astrophysics::orbital_elements::orbit_points_ellipse` + """ + ... + +def orbit_points_hyperbola(elements: OrbitalElements | Sequence[float], mu: float, num_points: int) -> list[Vec3]: + """ +Generates 3D points along a hyperbolic orbit trajectory using r = p/(1 + e cos θ) with θ bounded by the asymptotes. + +Rust: `astrophysics::orbital_elements::orbit_points_hyperbola` + """ + ... + +def is_bound(energy: float) -> bool: + """ +Returns true if the specific orbital energy indicates a bound orbit: ε < 0. + +Rust: `astrophysics::orbital_elements::is_bound` + """ + ... diff --git a/bindings/python/python/numeria/astrophysics/tidal.pyi b/bindings/python/python/numeria/astrophysics/tidal.pyi new file mode 100644 index 0000000..2a4123c --- /dev/null +++ b/bindings/python/python/numeria/astrophysics/tidal.pyi @@ -0,0 +1,66 @@ +""" +Tidal forces and the Roche limit. The tidal acceleration is the *difference* in gravitational pull across a body, so it falls as `1/r³` rather than `1/r²` -- which is why the Moon raises larger tides on Earth than the far more massive Sun does. The Roche limit is given in both the rigid and fluid forms; the fluid limit is the larger, because a fluid body deforms and so becomes easier to pull apart. Tidal heating, the locking timescale and the tidal tensor complete the module. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.math import Vec3 + +def tidal_acceleration_magnitude(primary_mass: float, distance: float) -> float: + """ +Computes the Newtonian tidal acceleration magnitude: a_tidal = 2GM/r³. + +Rust: `astrophysics::tidal::tidal_acceleration_magnitude` + """ + ... + +def tidal_acceleration_gr_corrected(primary_mass: float, distance: float, schwarzschild_radius: float) -> float: + """ +Computes tidal acceleration with a GR correction factor: a_tidal / (1 - r_s/r). + +Rust: `astrophysics::tidal::tidal_acceleration_gr_corrected` + """ + ... + +def roche_limit_rigid(primary_radius: float, primary_density: float, satellite_density: float) -> float: + """ +Computes the rigid-body Roche limit: d = R_p (2 ρ_p / ρ_s)^(1/3). + +Rust: `astrophysics::tidal::roche_limit_rigid` + """ + ... + +def roche_limit_fluid(primary_radius: float, primary_density: float, satellite_density: float) -> float: + """ +Computes the fluid-body Roche limit: d = 2.44 R_p (ρ_p / ρ_s)^(1/3). + +Rust: `astrophysics::tidal::roche_limit_fluid` + """ + ... + +def tidal_force_ratio(primary_mass: float, body_mass: float, body_radius: float, distance: float) -> float: + """ +Computes the ratio of tidal force to self-gravity on a body's surface: (a_tidal × R_body) / (GM_body / R_body²). + +Rust: `astrophysics::tidal::tidal_force_ratio` + """ + ... + +def tidal_tensor_eigenvalues(primary_mass: float, distance: float) -> tuple[float, float]: + """ +Computes the tidal tensor eigenvalues (radial, tangential): (2GM/r³, -GM/r³). + +Rust: `astrophysics::tidal::tidal_tensor_eigenvalues` + """ + ... + +def roche_potential(x: float, z: float, m1: float, pos1: Vec3 | Sequence[float], m2: float, pos2: Vec3 | Sequence[float]) -> float: + """ +Computes the Roche potential in the co-rotating frame: Φ = -Gm₁/r₁ - Gm₂/r₂ - ½ω²r_com². + +Rust: `astrophysics::tidal::roche_potential` + """ + ... diff --git a/bindings/python/python/numeria/astrophysics/time_systems.pyi b/bindings/python/python/numeria/astrophysics/time_systems.pyi new file mode 100644 index 0000000..c5c9e9f --- /dev/null +++ b/bindings/python/python/numeria/astrophysics/time_systems.pyi @@ -0,0 +1,110 @@ +""" +Astronomical time: Julian dates and sidereal time. # Why a day is not a day The Earth turns once on its axis in 23h 56m 04s -- a *sidereal* day -- and takes the extra four minutes to face the sun again, because it has moved along its orbit in the meantime. A solar day is therefore longer than a rotation, by almost exactly one part in 366. Everything about pointing a telescope, predicting a satellite pass or reading a ground track depends on keeping the two apart. Sidereal time is the hour angle of the vernal equinox, which is to say how far the Earth has turned relative to the stars. Greenwich mean sidereal time is that quantity at longitude zero, and adding the observer's longitude gives the local value. Right ascension is measured from the same origin, so an object is due south exactly when the local sidereal time equals its right ascension -- which is the whole reason the quantity exists. # What is approximated here `UT1` and `UTC` are treated as the same thing. They differ by up to 0.9 seconds, which is 0.0037 degrees of rotation -- irrelevant for anything in this module and decisive for geodesy. The `TT`/`UTC` offset from leap seconds is likewise ignored; the sun and planet positions here are low-precision approximations for which it does not matter. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def julian_date(year: int, month: int, day: int, hour: int, minute: int, second: float) -> float: + """ +The Julian date of a Gregorian calendar moment. + +Uses the standard Fliegel-Van Flandern arithmetic, shifting January +and February into the previous year so the leap-day irregularity falls +at the end. The count begins at noon, not midnight -- a convention +from before electric light, kept because it puts a single night's +observations inside one Julian day. + +Proleptic Gregorian throughout: dates before the 1582 reform are given +the Gregorian rule rather than the Julian one, which is what almost +every astronomical application wants and is not what a historian +wants. + +Errors: +Returns an error for a month outside 1..=12, a day outside 1..=31, a +time component out of range, or a non-finite second. + +Rust: `astrophysics::time_systems::julian_date` + """ + ... + +def jd_to_calendar(jd: float) -> tuple[int, int, int, int, int, float]: + """ +The Gregorian calendar moment of a Julian date, as +`(year, month, day, hour, minute, second)`. + +The inverse of `julian_date`, proleptic Gregorian throughout to keep +it so, and exact to the limits of the representation: a Julian date near the present carries about 2.5 +million days, so a double resolves it to some 20 microseconds. That is +why serious work splits the date into an integer part and a fraction, +which this does not. + +Errors: +Returns an error for a non-finite Julian date or one outside the range +the arithmetic covers. + +Rust: `astrophysics::time_systems::jd_to_calendar` + """ + ... + +def gmst(jd: float) -> float: + """ +Greenwich mean sidereal time in radians, from a Julian date. + +The IAU 1982 polynomial in Julian centuries from J2000. The linear +coefficient, `8_640_184.812_866` seconds per century, is the whole +content: divided by the century's 36525 days it says the Earth gains +about 236.6 seconds of sidereal time per solar day, which is the four +minutes by which the stars rise earlier each night. + +"Mean" means the equinox is the smoothly precessing one, without +nutation. Apparent sidereal time adds the equation of the equinoxes, +up to about a second of time, which matters for pointing a large +telescope and not for anything here. + +Errors: +Returns an error for a non-finite or out-of-range Julian date. + +Rust: `astrophysics::time_systems::gmst` + """ + ... + +def local_sidereal(jd: float, longitude: float) -> float: + """ +Local mean sidereal time: Greenwich's plus the observer's longitude. + +East longitude is positive. The result is what an object's right +ascension must equal for it to be due south, which is what makes it +the natural clock for an observatory. + +Errors: +As `gmst`, plus a non-finite longitude. + +Rust: `astrophysics::time_systems::local_sidereal` + """ + ... + +def tle_epoch_to_jd(epoch: float) -> float: + """ +The Julian date of a two-line element set's epoch field. + +TLEs carry the epoch as `YYDDD.DDDDDDDD`: a two-digit year and the +fractional day of that year. The two-digit year is resolved by the +convention the format itself uses -- 57 through 99 mean the twentieth +century and 00 through 56 the twenty-first, chosen because Sputnik +went up in 1957 and nothing older has a TLE. + +Errors: +Returns an error for a non-finite epoch, a year outside 0..=99, or a +day of year outside `[1, 367)`. + +Rust: `astrophysics::time_systems::tle_epoch_to_jd` + """ + ... + +J2000: float + +JULIAN_CENTURY: float diff --git a/bindings/python/python/numeria/atmosphere.pyi b/bindings/python/python/numeria/atmosphere.pyi new file mode 100644 index 0000000..a1a11b8 --- /dev/null +++ b/bindings/python/python/numeria/atmosphere.pyi @@ -0,0 +1,167 @@ +""" +The standard atmosphere, humidity, and near-surface wind. The barometric formula and the ISA lapse-rate model give pressure, temperature and density against altitude, plus the pressure and density altitudes an aircraft altimeter reports. Humidity is covered by the Magnus formulation for dew point and relative humidity. Wind includes the power-law shear profile, the wind power density that sets a turbine's available energy (`P/A = ½ρv³`, so a doubling of wind speed is eight times the power), the Beaufort scale, and the Coriolis parameter `f = 2Ω sin φ`. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def wind_chill(temperature_c: float, wind_speed_kmh: float) -> float: + """ +Environment Canada / NWS wind chill formula. +Valid for temperature <= 10°C and wind speed >= 4.8 km/h. +Returns the wind chill temperature in °C. + +Rust: `atmosphere::wind_chill` + """ + ... + +def beaufort_to_speed(beaufort: int) -> float: + """ +Convert Beaufort scale number to approximate wind speed in m/s. +Uses v = 0.836 × B^(3/2). + +Rust: `atmosphere::beaufort_to_speed` + """ + ... + +def speed_to_beaufort(speed_ms: float) -> int: + """ +Convert wind speed in m/s to Beaufort scale number (clamped 0–12). +Inverse of `beaufort_to_speed`. + +Rust: `atmosphere::speed_to_beaufort` + """ + ... + +def wind_shear(v_top: float, v_bottom: float, height_diff: float) -> float: + """ +Vertical wind shear: dv/dz = (v_top - v_bottom) / Δz. +Returns shear in s⁻¹. + +Rust: `atmosphere::wind_shear` + """ + ... + +def wind_power_density(density: float, velocity: float) -> float: + """ +Wind power density: P/A = ½ρv³ (W/m²). + +Rust: `atmosphere::wind_power_density` + """ + ... + +def coriolis_parameter(latitude_rad: float) -> float: + """ +Coriolis parameter: f = 2Ω sin(φ). + +Rust: `atmosphere::coriolis_parameter` + """ + ... + +def coriolis_acceleration(velocity: float, latitude_rad: float) -> float: + """ +Coriolis acceleration: a = 2Ωv sin(φ). + +Rust: `atmosphere::coriolis_acceleration` + """ + ... + +def barometric_pressure(p0: float, molar_mass: float, g: float, height: float, temperature: float) -> float: + """ +Barometric pressure at a given height using the hypsometric equation. +P = P₀ × exp(-Mgh / (RT)) + +Rust: `atmosphere::barometric_pressure` + """ + ... + +def dry_adiabatic_lapse_rate(g: float, cp: float) -> float: + """ +Dry adiabatic lapse rate: Γ = g / cp (K/m). + +Rust: `atmosphere::dry_adiabatic_lapse_rate` + """ + ... + +def temperature_at_altitude(t0: float, lapse_rate: float, altitude: float) -> float: + """ +Temperature at altitude: T = T₀ - Γ × h. + +Rust: `atmosphere::temperature_at_altitude` + """ + ... + +def pressure_altitude(p0: float, pressure: float, lapse_rate: float, t0: float) -> float: + """ +Pressure altitude using the standard atmosphere simplification: +h = (T₀ / Γ) × (1 - (P / P₀)^0.1903) + +Rust: `atmosphere::pressure_altitude` + """ + ... + +def density_altitude(pressure_alt: float, temperature_c: float, standard_temp_c: float) -> float: + """ +Density altitude from pressure altitude and temperature deviation: +DA = PA + 36.576 × (T - T_std) meters. + +Rust: `atmosphere::density_altitude` + """ + ... + +def scale_height(temperature: float, molar_mass: float, g: float) -> float: + """ +Scale height: H = RT / (Mg). + +Rust: `atmosphere::scale_height` + """ + ... + +def dew_point(temperature_c: float, relative_humidity: float) -> float: + """ +Dew point via the Magnus formula. +α = (a×T)/(b+T) + ln(RH), then Td = (b×α)/(a-α). +`relative_humidity` is fractional (0.0–1.0). + +Rust: `atmosphere::dew_point` + """ + ... + +def relative_humidity(temperature_c: float, dew_point_c: float) -> float: + """ +Relative humidity from temperature and dew point (returns fractional 0.0–1.0). +RH = exp((a×Td)/(b+Td) - (a×T)/(b+T)) + +Rust: `atmosphere::relative_humidity` + """ + ... + +def heat_index(temperature_c: float, relative_humidity: float) -> float: + """ +Heat index via the Rothfusz regression (simplified). +Takes temperature in °C and relative_humidity as percentage (0–100). + +Rust: `atmosphere::heat_index` + """ + ... + +def absolute_humidity(relative_humidity: float, temperature_c: float) -> float: + """ +Absolute humidity in g/m³. +AH = (6.112 × e^(17.67T/(T+243.5)) × RH × 2.1674) / (273.15 + T) +`relative_humidity` is fractional (0.0–1.0). + +Rust: `atmosphere::absolute_humidity` + """ + ... + +EARTH_ROTATION_RATE: float + +STANDARD_PRESSURE: float + +STANDARD_TEMPERATURE: float + +DRY_AIR_MOLAR_MASS: float diff --git a/bindings/python/python/numeria/audio/__init__.pyi b/bindings/python/python/numeria/audio/__init__.pyi new file mode 100644 index 0000000..a263724 --- /dev/null +++ b/bindings/python/python/numeria/audio/__init__.pyi @@ -0,0 +1,270 @@ +""" +Audio synthesis, analysis, effects, and I/O. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from . import analysis, effects, envelope, oscillators, physical, spatial, synthesis, tuning, vocoder, wav +from numeria.audio.envelope import Adsr as Adsr +from numeria.audio.envelope import AdsrExp as AdsrExp +from numeria.audio.effects import AllpassFilter as AllpassFilter +from numeria.audio.envelope import Ar as Ar +from numeria.audio.physical import BowedString as BowedString +from numeria.audio.tuning import ChordQuality as ChordQuality +from numeria.audio.effects import Chorus as Chorus +from numeria.audio.effects import CombFilter as CombFilter +from numeria.audio.effects import Compressor as Compressor +from numeria.audio.effects import DeEsser as DeEsser +from numeria.audio.effects import DelayLine as DelayLine +from numeria.audio.effects import Eq as Eq +from numeria.audio.vocoder import Excitation as Excitation +from numeria.audio.effects import Exciter as Exciter +from numeria.audio.effects import Expander as Expander +from numeria.audio.envelope import FadeShape as FadeShape +from numeria.audio.effects import Fdn as Fdn +from numeria.audio.effects import Flanger as Flanger +from numeria.audio.synthesis import FmOperator as FmOperator +from numeria.audio.synthesis import FmSynth as FmSynth +from numeria.audio.effects import Freeverb as Freeverb +from numeria.audio.physical import KellyLochbaum as KellyLochbaum +from numeria.audio.envelope import Lfo as Lfo +from numeria.audio.effects import Limiter as Limiter +from numeria.audio.physical import MassSpringString as MassSpringString +from numeria.audio.physical import Membrane2D as Membrane2D +from numeria.audio.physical import ModalSynth as ModalSynth +from numeria.audio.tuning import Mode as Mode +from numeria.audio.oscillators import NoiseColor as NoiseColor +from numeria.audio.effects import NoiseGate as NoiseGate +from numeria.audio.oscillators import NoiseGen as NoiseGen +from numeria.audio.oscillators import Oscillator as Oscillator +from numeria.audio.effects import PartitionedConvolver as PartitionedConvolver +from numeria.audio.vocoder import PhaseVocoder as PhaseVocoder +from numeria.audio.effects import Phaser as Phaser +from numeria.audio.analysis import PitchMethod as PitchMethod +from numeria.audio.physical import Plate2D as Plate2D +from numeria.audio.effects import SchroederReverb as SchroederReverb +from numeria.audio.analysis import SpectralFeatures as SpectralFeatures +from numeria.audio.effects import StereoWidener as StereoWidener +from numeria.audio.effects import Tremolo as Tremolo +from numeria.audio.effects import Vibrato as Vibrato +from numeria.audio.synthesis import Voice as Voice +from numeria.audio.wav import WavData as WavData +from numeria.audio.oscillators import Waveform as Waveform +from numeria.audio.physical import WaveguideString as WaveguideString +from numeria.audio.physical import WaveguideTube as WaveguideTube +from numeria.audio.oscillators import Wavetable as Wavetable +from numeria.audio.synthesis import additive as additive +from numeria.audio.synthesis import additive_evolving as additive_evolving +from numeria.audio.oscillators import additive_saw as additive_saw +from numeria.audio.oscillators import additive_square as additive_square +from numeria.audio.oscillators import additive_triangle as additive_triangle +from numeria.audio.spatial import air_absorption_filter as air_absorption_filter +from numeria.audio.synthesis import am as am +from numeria.audio.spatial import ambisonics_decode as ambisonics_decode +from numeria.audio.spatial import ambisonics_encode as ambisonics_encode +from numeria.audio.spatial import ambisonics_encode_1st as ambisonics_encode_1st +from numeria.audio.spatial import ambisonics_rotate as ambisonics_rotate +from numeria.audio.envelope import apply_envelope as apply_envelope +from numeria.audio.analysis import audio_fingerprint as audio_fingerprint +from numeria.audio.analysis import autocorrelation_fft as autocorrelation_fft +from numeria.audio.vocoder import autotune as autotune +from numeria.audio.oscillators import band_limited_impulse_train as band_limited_impulse_train +from numeria.audio.physical import banded_waveguide as banded_waveguide +from numeria.audio.spatial import beamforming_delay_sum as beamforming_delay_sum +from numeria.audio.spatial import beamforming_mvdr as beamforming_mvdr +from numeria.audio.analysis import beat_track as beat_track +from numeria.audio.spatial import binaural_simple as binaural_simple +from numeria.audio.tuning import bohlen_pierce as bohlen_pierce +from numeria.audio.analysis import c50 as c50 +from numeria.audio.analysis import c80 as c80 +from numeria.audio.tuning import cents_between as cents_between +from numeria.audio.tuning import cents_to_ratio as cents_to_ratio +from numeria.audio.vocoder import channel_vocoder as channel_vocoder +from numeria.audio.synthesis import chebyshev_waveshaper as chebyshev_waveshaper +from numeria.audio.oscillators import chirp_exponential as chirp_exponential +from numeria.audio.oscillators import chirp_hyperbolic as chirp_hyperbolic +from numeria.audio.oscillators import chirp_linear as chirp_linear +from numeria.audio.analysis import chord_estimate as chord_estimate +from numeria.audio.tuning import chord_tones as chord_tones +from numeria.audio.analysis import chroma as chroma +from numeria.audio.tuning import circle_of_fifths as circle_of_fifths +from numeria.audio.physical import commuted_synthesis as commuted_synthesis +from numeria.audio.tuning import consonance_plomp_levelt as consonance_plomp_levelt +from numeria.audio.effects import convolution_reverb as convolution_reverb +from numeria.audio.vocoder import cross_synthesis as cross_synthesis +from numeria.audio.envelope import crossfade as crossfade +from numeria.audio.analysis import d50 as d50 +from numeria.audio.oscillators import dc as dc +from numeria.audio.effects import dc_offset_remove as dc_offset_remove +from numeria.audio.effects import declick as declick +from numeria.audio.analysis import delta_features as delta_features +from numeria.audio.tuning import dissonance_curve as dissonance_curve +from numeria.audio.spatial import distance_gain as distance_gain +from numeria.audio.effects import distortion_foldback as distortion_foldback +from numeria.audio.effects import distortion_hard_clip as distortion_hard_clip +from numeria.audio.effects import distortion_soft_clip as distortion_soft_clip +from numeria.audio.effects import distortion_tube as distortion_tube +from numeria.audio.effects import dither_tpdf as dither_tpdf +from numeria.audio.spatial import doppler_resample as doppler_resample +from numeria.audio.synthesis import drum_clap as drum_clap +from numeria.audio.synthesis import drum_hihat as drum_hihat +from numeria.audio.synthesis import drum_kick as drum_kick +from numeria.audio.synthesis import drum_snare as drum_snare +from numeria.audio.synthesis import drum_tom as drum_tom +from numeria.audio.analysis import dynamic_time_warping as dynamic_time_warping +from numeria.audio.spatial import early_reflections as early_reflections +from numeria.audio.analysis import edt_from_ir as edt_from_ir +from numeria.audio.analysis import enob as enob +from numeria.audio.envelope import envelope_follower as envelope_follower +from numeria.audio.tuning import equal_temperament as equal_temperament +from numeria.audio.analysis import estimate_snr as estimate_snr +from numeria.audio.envelope import exponential_decay_envelope as exponential_decay_envelope +from numeria.audio.envelope import fade_in as fade_in +from numeria.audio.envelope import fade_out as fade_out +from numeria.audio.analysis import fluctuation_strength as fluctuation_strength +from numeria.audio.synthesis import fm_bessel_sidebands as fm_bessel_sidebands +from numeria.audio.synthesis import fm_simple as fm_simple +from numeria.audio.synthesis import formant_synth as formant_synth +from numeria.audio.analysis import formant_track as formant_track +from numeria.audio.wav import from_interleaved as from_interleaved +from numeria.audio.effects import gain_db as gain_db +from numeria.audio.physical import glottal_pulse_lf as glottal_pulse_lf +from numeria.audio.synthesis import granular as granular +from numeria.audio.effects import haas_delay as haas_delay +from numeria.audio.physical import hammer_string_interaction as hammer_string_interaction +from numeria.audio.synthesis import hard_sync_osc as hard_sync_osc +from numeria.audio.tuning import harmonic_series_scale as harmonic_series_scale +from numeria.audio.analysis import harmonic_to_noise_ratio as harmonic_to_noise_ratio +from numeria.audio.vocoder import harmonizer as harmonizer +from numeria.audio.spatial import ild_spherical_head as ild_spherical_head +from numeria.audio.spatial import image_source_ir as image_source_ir +from numeria.audio.oscillators import impulse as impulse +from numeria.audio.analysis import impulse_response_from_sweep as impulse_response_from_sweep +from numeria.audio.physical import inharmonic_partials as inharmonic_partials +from numeria.audio.analysis import inharmonicity_measure as inharmonicity_measure +from numeria.audio.tuning import interval_name as interval_name +from numeria.audio.spatial import itd_woodworth as itd_woodworth +from numeria.audio.physical import jet_nonlinearity as jet_nonlinearity +from numeria.audio.tuning import just_intonation_5limit as just_intonation_5limit +from numeria.audio.synthesis import karplus_strong as karplus_strong +from numeria.audio.synthesis import karplus_strong_extended as karplus_strong_extended +from numeria.audio.tuning import kirnberger_iii as kirnberger_iii +from numeria.audio.physical import lip_model as lip_model +from numeria.audio.spatial import localize_tdoa as localize_tdoa +from numeria.audio.analysis import loudness_sone as loudness_sone +from numeria.audio.analysis import lpc as lpc +from numeria.audio.analysis import lpc_spectrum as lpc_spectrum +from numeria.audio.analysis import lpc_to_formants as lpc_to_formants +from numeria.audio.analysis import lpc_to_lsp as lpc_to_lsp +from numeria.audio.vocoder import lpc_vocoder as lpc_vocoder +from numeria.audio.analysis import lsp_to_lpc as lsp_to_lpc +from numeria.audio.tuning import meantone_quarter_comma as meantone_quarter_comma +from numeria.audio.effects import measure_lufs as measure_lufs +from numeria.audio.analysis import mfcc as mfcc +from numeria.audio.tuning import midi_to_freq_tuned as midi_to_freq_tuned +from numeria.audio.synthesis import mix as mix +from numeria.audio.oscillators import multisine as multisine +from numeria.audio.tuning import nearest_note as nearest_note +from numeria.audio.effects import noise_shaping_dither as noise_shaping_dither +from numeria.audio.effects import normalize_lufs as normalize_lufs +from numeria.audio.effects import normalize_peak as normalize_peak +from numeria.audio.effects import normalize_rms as normalize_rms +from numeria.audio.analysis import onset_complex_domain as onset_complex_domain +from numeria.audio.analysis import onset_detect as onset_detect +from numeria.audio.analysis import onset_hfc as onset_hfc +from numeria.audio.analysis import onset_strength as onset_strength +from numeria.audio.effects import oversample_process as oversample_process +from numeria.audio.spatial import pan_constant_power as pan_constant_power +from numeria.audio.spatial import pan_linear as pan_linear +from numeria.audio.spatial import pan_minus_4_5_db as pan_minus_4_5_db +from numeria.audio.spatial import pan_vbap_2d as pan_vbap_2d +from numeria.audio.spatial import pan_vbap_3d as pan_vbap_3d +from numeria.audio.envelope import peak_envelope as peak_envelope +from numeria.audio.analysis import peak_pick as peak_pick +from numeria.audio.synthesis import phase_distortion as phase_distortion +from numeria.audio.analysis import pitch_autocorrelation as pitch_autocorrelation +from numeria.audio.analysis import pitch_cepstral as pitch_cepstral +from numeria.audio.analysis import pitch_hps as pitch_hps +from numeria.audio.analysis import pitch_mpm as pitch_mpm +from numeria.audio.effects import pitch_shift_simple as pitch_shift_simple +from numeria.audio.analysis import pitch_to_midi_track as pitch_to_midi_track +from numeria.audio.analysis import pitch_track as pitch_track +from numeria.audio.analysis import pitch_yin as pitch_yin +from numeria.audio.synthesis import pm_simple as pm_simple +from numeria.audio.oscillators import polyblep_saw as polyblep_saw +from numeria.audio.oscillators import polyblep_square as polyblep_square +from numeria.audio.oscillators import polyblep_triangle as polyblep_triangle +from numeria.audio.envelope import portamento as portamento +from numeria.audio.vocoder import psola_pitch_shift as psola_pitch_shift +from numeria.audio.synthesis import pulsar_synthesis as pulsar_synthesis +from numeria.audio.oscillators import pulse_train as pulse_train +from numeria.audio.tuning import pythagorean as pythagorean +from numeria.audio.tuning import pythagorean_comma as pythagorean_comma +from numeria.audio.tuning import ratio_to_cents as ratio_to_cents +from numeria.audio.spatial import ray_tracing_ir as ray_tracing_ir +from numeria.audio.physical import reed_nonlinearity as reed_nonlinearity +from numeria.audio.synthesis import ring_mod as ring_mod +from numeria.audio.envelope import rms_envelope as rms_envelope +from numeria.audio.physical import rosenberg_pulse as rosenberg_pulse +from numeria.audio.analysis import roughness as roughness +from numeria.audio.analysis import rt60_from_ir as rt60_from_ir +from numeria.audio.synthesis import sample_playback as sample_playback +from numeria.audio.tuning import scala_parse as scala_parse +from numeria.audio.tuning import scale_degrees as scale_degrees +from numeria.audio.tuning import schisma as schisma +from numeria.audio.oscillators import schroeder_phase_multisine as schroeder_phase_multisine +from numeria.audio.analysis import sharpness as sharpness +from numeria.audio.analysis import silence_detect as silence_detect +from numeria.audio.analysis import sinad as sinad +from numeria.audio.oscillators import sine_sweep_with_inverse as sine_sweep_with_inverse +from numeria.audio.spatial import sonar_equation as sonar_equation +from numeria.audio.spatial import sonar_range as sonar_range +from numeria.audio.spatial import speaker_baffle_step as speaker_baffle_step +from numeria.audio.spatial import speaker_crossover_lr4 as speaker_crossover_lr4 +from numeria.audio.analysis import spectral_centroid as spectral_centroid +from numeria.audio.analysis import spectral_crest as spectral_crest +from numeria.audio.analysis import spectral_decrease as spectral_decrease +from numeria.audio.analysis import spectral_entropy_mag as spectral_entropy_mag +from numeria.audio.analysis import spectral_features_track as spectral_features_track +from numeria.audio.analysis import spectral_flatness_mag as spectral_flatness_mag +from numeria.audio.analysis import spectral_flux as spectral_flux +from numeria.audio.effects import spectral_gate as spectral_gate +from numeria.audio.analysis import spectral_kurtosis as spectral_kurtosis +from numeria.audio.vocoder import spectral_morph as spectral_morph +from numeria.audio.analysis import spectral_rolloff as spectral_rolloff +from numeria.audio.analysis import spectral_skewness as spectral_skewness +from numeria.audio.analysis import spectral_slope as spectral_slope +from numeria.audio.analysis import spectral_spread as spectral_spread +from numeria.audio.spatial import spherical_head_hrtf as spherical_head_hrtf +from numeria.audio.analysis import sti_approx as sti_approx +from numeria.audio.tuning import stretch_tuning_railsback as stretch_tuning_railsback +from numeria.audio.physical import string_tension_from_freq as string_tension_from_freq +from numeria.audio.synthesis import subtractive as subtractive +from numeria.audio.synthesis import supersaw as supersaw +from numeria.audio.effects import synthesize_ir_exponential as synthesize_ir_exponential +from numeria.audio.tuning import syntonic_comma as syntonic_comma +from numeria.audio.spatial import tdoa_gcc_phat as tdoa_gcc_phat +from numeria.audio.analysis import tempo_estimate as tempo_estimate +from numeria.audio.analysis import thd_n as thd_n +from numeria.audio.spatial import thiele_small_response as thiele_small_response +from numeria.audio.wav import to_interleaved as to_interleaved +from numeria.audio.wav import to_mono as to_mono +from numeria.audio.analysis import transient_detect as transient_detect +from numeria.audio.effects import true_peak as true_peak +from numeria.audio.physical import vocal_tract as vocal_tract +from numeria.audio.synthesis import vowel_formants as vowel_formants +from numeria.audio.wav import wav_info as wav_info +from numeria.audio.wav import wav_read as wav_read +from numeria.audio.wav import wav_read_file as wav_read_file +from numeria.audio.wav import wav_write as wav_write +from numeria.audio.wav import wav_write_file as wav_write_file +from numeria.audio.synthesis import waveshaper as waveshaper +from numeria.audio.tuning import werckmeister_iii as werckmeister_iii +from numeria.audio.vocoder import wsola_time_stretch as wsola_time_stretch +from numeria.audio.tuning import young as young +from numeria.audio.analysis import zero_crossing_rate as zero_crossing_rate + + diff --git a/bindings/python/python/numeria/audio/analysis.pyi b/bindings/python/python/numeria/audio/analysis.pyi new file mode 100644 index 0000000..82a69a6 --- /dev/null +++ b/bindings/python/python/numeria/audio/analysis.pyi @@ -0,0 +1,558 @@ +""" +Audio analysis: pitch detection (YIN, autocorrelation, cepstral, HPS, McLeod), onset/tempo/beat tracking, MFCCs, LPC and formants, LSPs, spectral descriptors, chroma/key/chord estimation, psychoacoustic approximations, distortion metrics, room-acoustics measures from impulse responses, DTW, and constellation fingerprinting. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.spatial.frame import Frame +from numeria.spatial.primitives import Segment + +class PitchMethod: + """ +Frame-wise pitch detection method selector. + +Rust: `audio::analysis::PitchMethod` + """ + ... + +class SpectralFeatures: + """ +One frame of spectral descriptors. + +Rust: `audio::analysis::SpectralFeatures` + """ + def __init__(self, centroid: float, spread: float, skewness: float, kurtosis: float, rolloff85: float, flux: float, flatness: float, crest: float, slope: float, decrease: float, entropy: float) -> None: ... + @property + def centroid(self) -> float: ... + @property + def spread(self) -> float: ... + @property + def skewness(self) -> float: ... + @property + def kurtosis(self) -> float: ... + @property + def rolloff85(self) -> float: ... + @property + def flux(self) -> float: ... + @property + def flatness(self) -> float: ... + @property + def crest(self) -> float: ... + @property + def slope(self) -> float: ... + @property + def decrease(self) -> float: ... + @property + def entropy(self) -> float: ... + +def autocorrelation_fft(x: list[float]) -> list[float]: + """ +Raw (biased, un-normalized) autocorrelation of `x` computed with FFTs; +returns lags 0..x.len(). + +Rust: `audio::analysis::autocorrelation_fft` + """ + ... + +def pitch_yin(x: list[float], fs: float, f_min: float, f_max: float, threshold: float) -> Optional[tuple[float, float]]: + """ +YIN pitch detector. Returns (frequency, confidence in 0..1) or `None` +when no lag drops below `threshold` (unvoiced). + +Rust: `audio::analysis::pitch_yin` + """ + ... + +def pitch_autocorrelation(x: list[float], fs: float, f_min: float, f_max: float) -> Optional[float]: + """ +Autocorrelation pitch: highest normalized-autocorrelation peak in the +lag range; `None` if the peak is weak (< 0.3). + +Rust: `audio::analysis::pitch_autocorrelation` + """ + ... + +def pitch_cepstral(x: list[float], fs: float, f_min: float, f_max: float) -> Optional[float]: + """ +Cepstral pitch: peak of the real cepstrum in the expected quefrency +range. + +Rust: `audio::analysis::pitch_cepstral` + """ + ... + +def pitch_hps(x: list[float], fs: float, n_harmonics: int) -> Optional[float]: + """ +Harmonic product spectrum pitch estimate. + +Rust: `audio::analysis::pitch_hps` + """ + ... + +def pitch_mpm(x: list[float], fs: float) -> Optional[tuple[float, float]]: + """ +McLeod pitch method (NSDF). Returns (frequency, clarity). + +Rust: `audio::analysis::pitch_mpm` + """ + ... + +def pitch_track(x: list[float], fs: float, hop: int, method: PitchMethod) -> list[tuple[float, Optional[float]]]: + """ +Frame-wise pitch track: (time s, f0) per hop, 2048-sample frames. + +Rust: `audio::analysis::pitch_track` + """ + ... + +def pitch_to_midi_track(track: list[tuple[float, Optional[float]]]) -> list[tuple[float, float, int]]: + """ +Segment a pitch track into notes: (start time, duration, MIDI note). +Runs of at least 3 voiced frames on the same rounded MIDI number +become one note. + +Rust: `audio::analysis::pitch_to_midi_track` + """ + ... + +def onset_strength(x: list[float], fs: float, n_fft: int, hop: int) -> list[float]: + """ +Spectral-flux onset strength envelope (one value per STFT frame). + +Rust: `audio::analysis::onset_strength` + """ + ... + +def onset_detect(strength: list[float], threshold: float, min_gap: int) -> list[int]: + """ +Pick onsets from a strength envelope: local maxima above `threshold` +separated by at least `min_gap` frames. + +Rust: `audio::analysis::onset_detect` + """ + ... + +def onset_hfc(x: list[float], fs: float, n_fft: int, hop: int) -> list[float]: + """ +High-frequency-content onset function: Σ k |X_k|² per frame. + +Rust: `audio::analysis::onset_hfc` + """ + ... + +def onset_complex_domain(x: list[float], fs: float, n_fft: int, hop: int) -> list[float]: + """ +Complex-domain onset function: deviation of each frame from the +magnitude/phase prediction of the previous frames. + +Rust: `audio::analysis::onset_complex_domain` + """ + ... + +def tempo_estimate(onsets: list[float], fs: float) -> float: + """ +Tempo (BPM) from an onset-strength envelope sampled at `fs` frames/s, +via the autocorrelation peak in the 40-240 BPM range (preferring the +shortest strong lag, i.e. the fastest consistent pulse). + +Rust: `audio::analysis::tempo_estimate` + """ + ... + +def beat_track(x: list[float], fs: float) -> list[float]: + """ +Beat tracking by dynamic programming (Ellis 2007): onset envelope, +global tempo, then a penalized best-predecessor recursion. Returns +beat times in seconds. + +Rust: `audio::analysis::beat_track` + """ + ... + +def mfcc(x: list[float], fs: float, n_fft: int, hop: int, n_mels: int, n_mfcc: int) -> list[list[float]]: + """ +MFCCs: log-mel spectrogram followed by a DCT-II, keeping `n_mfcc` +coefficients per frame. + +Rust: `audio::analysis::mfcc` + """ + ... + +def delta_features(f: list[list[float]], width: int) -> list[list[float]]: + """ +Regression delta features over ±`width` frames. + +Rust: `audio::analysis::delta_features` + """ + ... + +def lpc(x: list[float], order: int) -> tuple[list[float], float]: + """ +Linear prediction by the autocorrelation method (Levinson-Durbin). +Returns the coefficients of A(z) = 1 + a₁z⁻¹ + … (length order+1, +leading 1) and the residual gain √E. + +Rust: `audio::analysis::lpc` + """ + ... + +def lpc_to_formants(coeffs: list[float], fs: float) -> list[tuple[float, float]]: + """ +Formants (frequency, bandwidth in Hz) from LPC coefficients, via the +roots of A(z). + +Rust: `audio::analysis::lpc_to_formants` + """ + ... + +def formant_track(x: list[float], fs: float, order: int, hop: int) -> list[list[tuple[float, float]]]: + """ +Frame-wise formant tracking (25 ms Hamming frames, pre-emphasis). + +Rust: `audio::analysis::formant_track` + """ + ... + +def lpc_spectrum(coeffs: list[float], gain: float, n: int, fs: float) -> list[float]: + """ +LPC envelope magnitude spectrum: gain/|A(e^{jω})| at `n` frequencies +from 0 to fs/2. + +Rust: `audio::analysis::lpc_spectrum` + """ + ... + +def lpc_to_lsp(coeffs: list[float]) -> list[float]: + """ +Line spectral pairs (radian frequencies in (0, π), sorted) of an LPC +polynomial with leading 1. The order must be even. + +Rust: `audio::analysis::lpc_to_lsp` + """ + ... + +def lsp_to_lpc(lsp: list[float]) -> list[float]: + """ +Reconstruct LPC coefficients (leading 1) from line spectral pairs. + +Rust: `audio::analysis::lsp_to_lpc` + """ + ... + +def spectral_centroid(mag: list[float], freqs: list[float]) -> float: + """ +Amplitude-weighted mean frequency. + +Rust: `audio::analysis::spectral_centroid` + """ + ... + +def spectral_spread(mag: list[float], freqs: list[float]) -> float: + """ +Standard deviation of the spectral distribution. + +Rust: `audio::analysis::spectral_spread` + """ + ... + +def spectral_skewness(mag: list[float], freqs: list[float]) -> float: + """ +Third standardized moment of the spectral distribution. + +Rust: `audio::analysis::spectral_skewness` + """ + ... + +def spectral_kurtosis(mag: list[float], freqs: list[float]) -> float: + """ +Fourth standardized moment of the spectral distribution. + +Rust: `audio::analysis::spectral_kurtosis` + """ + ... + +def spectral_rolloff(mag: list[float], freqs: list[float], pct: float) -> float: + """ +Frequency below which `pct` (0..1) of the spectral energy lies. + +Rust: `audio::analysis::spectral_rolloff` + """ + ... + +def spectral_flux(prev: list[float], cur: list[float]) -> float: + """ +Half-wave rectified spectral flux between consecutive magnitude +frames. + +Rust: `audio::analysis::spectral_flux` + """ + ... + +def spectral_flatness_mag(mag: list[float]) -> float: + """ +Geometric-to-arithmetic mean ratio (1 = white, 0 = tonal). + +Rust: `audio::analysis::spectral_flatness_mag` + """ + ... + +def spectral_crest(mag: list[float]) -> float: + """ +Peak-to-mean spectral ratio. + +Rust: `audio::analysis::spectral_crest` + """ + ... + +def spectral_slope(mag: list[float], freqs: list[float]) -> float: + """ +Linear-regression slope of magnitude vs frequency. + +Rust: `audio::analysis::spectral_slope` + """ + ... + +def spectral_decrease(mag: list[float]) -> float: + """ +Spectral decrease (perceptual measure of how fast magnitude falls off +with bin index). + +Rust: `audio::analysis::spectral_decrease` + """ + ... + +def spectral_entropy_mag(mag: list[float]) -> float: + """ +Normalized Shannon entropy of the magnitude distribution (0..1). + +Rust: `audio::analysis::spectral_entropy_mag` + """ + ... + +def spectral_features_track(x: list[float], fs: float, n_fft: int, hop: int) -> list[SpectralFeatures]: + """ +Frame-wise spectral descriptor track. + +Rust: `audio::analysis::spectral_features_track` + """ + ... + +def zero_crossing_rate(x: list[float], frame: int, hop: int) -> list[float]: + """ +Frame-wise zero-crossing rate (fraction of adjacent sample pairs that +change sign). + +Rust: `audio::analysis::zero_crossing_rate` + """ + ... + +def harmonic_to_noise_ratio(x: list[float], fs: float, f0: float) -> float: + """ +Harmonic-to-noise ratio (dB) from the normalized autocorrelation at +the period of `f0`. + +Rust: `audio::analysis::harmonic_to_noise_ratio` + """ + ... + +def inharmonicity_measure(x: list[float], fs: float, f0: float) -> float: + """ +Piano-style inharmonicity coefficient B fitted from the measured +partial frequencies: f_k ≈ k f0 √(1 + B k²). + +Rust: `audio::analysis::inharmonicity_measure` + """ + ... + +def chroma(x: list[float], fs: float, n_fft: int, hop: int) -> list[list[float]]: + """ +Frame-wise 12-bin chroma (C, C#, …, B), energy-normalized per frame. + +Rust: `audio::analysis::chroma` + """ + ... + +def chord_estimate(chroma_frame: list[float]) -> str: + """ +Template chord match on one chroma frame: returns e.g. "C", "Am", +"Bdim", "Faug". + +Rust: `audio::analysis::chord_estimate` + """ + ... + +def loudness_sone(x: list[float], fs: float) -> float: + """ +Approximate loudness in sones (Zwicker-style power law on the overall +level, full scale taken as 94 dB SPL). + +Rust: `audio::analysis::loudness_sone` + """ + ... + +def sharpness(x: list[float], fs: float) -> float: + """ +Approximate sharpness in acum (Bark-weighted specific-loudness +centroid, von Bismarck weighting). + +Rust: `audio::analysis::sharpness` + """ + ... + +def roughness(x: list[float], fs: float) -> float: + """ +Approximate roughness: fraction of envelope fluctuation energy in the +20-300 Hz modulation range. + +Rust: `audio::analysis::roughness` + """ + ... + +def fluctuation_strength(x: list[float], fs: float) -> float: + """ +Approximate fluctuation strength: fraction of envelope fluctuation +energy in the 1-10 Hz modulation range (maximal near 4 Hz). + +Rust: `audio::analysis::fluctuation_strength` + """ + ... + +def silence_detect(x: list[float], threshold_db: float, min_len: int) -> list[tuple[int, int]]: + """ +Silent regions as (start, end) sample ranges: block RMS below +`threshold_db` (dBFS) for at least `min_len` samples. + +Rust: `audio::analysis::silence_detect` + """ + ... + +def transient_detect(x: list[float], fs: float) -> list[int]: + """ +Transient (attack) sample positions from a high-frequency-content +envelope with an adaptive threshold. + +Rust: `audio::analysis::transient_detect` + """ + ... + +def estimate_snr(x: list[float], noise_segment: list[float]) -> float: + """ +SNR (dB) of a signal given a noise-only reference segment. + +Rust: `audio::analysis::estimate_snr` + """ + ... + +def thd_n(x: list[float], fs: float, f0: float) -> float: + """ +THD+N as a linear ratio: √((P_total − P_fund)/P_fund). + +Rust: `audio::analysis::thd_n` + """ + ... + +def sinad(x: list[float], fs: float, f0: float) -> float: + """ +SINAD in dB: 10 log₁₀(P_fund / (P_total − P_fund)). + +Rust: `audio::analysis::sinad` + """ + ... + +def enob(sinad_db: float) -> float: + """ +Effective number of bits from a SINAD measurement (dB). + +Rust: `audio::analysis::enob` + """ + ... + +def impulse_response_from_sweep(recorded: list[float], inverse_sweep: list[float]) -> list[float]: + """ +Deconvolve a Farina sweep measurement: convolve the recording with the +inverse sweep and align so the direct impulse response starts at 0. + +Rust: `audio::analysis::impulse_response_from_sweep` + """ + ... + +def rt60_from_ir(ir: list[float], fs: float) -> float: + """ +RT60 via Schroeder backward integration, extrapolated from the +−5..−25 dB decay slope. + +Rust: `audio::analysis::rt60_from_ir` + """ + ... + +def edt_from_ir(ir: list[float], fs: float) -> float: + """ +Early decay time: the 0..−10 dB slope extrapolated to 60 dB. + +Rust: `audio::analysis::edt_from_ir` + """ + ... + +def c50(ir: list[float], fs: float) -> float: + """ +Clarity C50 (dB): early (< 50 ms) to late energy ratio. + +Rust: `audio::analysis::c50` + """ + ... + +def c80(ir: list[float], fs: float) -> float: + """ +Clarity C80 (dB): early (< 80 ms) to late energy ratio. + +Rust: `audio::analysis::c80` + """ + ... + +def d50(ir: list[float], fs: float) -> float: + """ +Definition D50: fraction of energy arriving within 50 ms. + +Rust: `audio::analysis::d50` + """ + ... + +def sti_approx(ir: list[float], fs: float) -> float: + """ +Single-band STI approximation from the impulse response's modulation +transfer function at the 14 standard modulation frequencies. + +Rust: `audio::analysis::sti_approx` + """ + ... + +def peak_pick(x: list[float], threshold: float, min_distance: int) -> list[int]: + """ +Local maxima above `threshold`, greedily thinned so accepted peaks are +at least `min_distance` apart (strongest first). Returns sorted +indices. + +Rust: `audio::analysis::peak_pick` + """ + ... + +def dynamic_time_warping(a: list[list[float]], b: list[list[float]]) -> tuple[float, list[tuple[int, int]]]: + """ +Dynamic time warping between two feature sequences (Euclidean local +cost). Returns (total cost, warping path from (0,0) to (n-1,m-1)). + +Rust: `audio::analysis::dynamic_time_warping` + """ + ... + +def audio_fingerprint(x: list[float], fs: float) -> list[int]: + """ +Shazam-style constellation fingerprint: spectrogram peaks paired into +(f_anchor, f_target, Δt) hashes. + +Rust: `audio::analysis::audio_fingerprint` + """ + ... diff --git a/bindings/python/python/numeria/audio/effects.pyi b/bindings/python/python/numeria/audio/effects.pyi new file mode 100644 index 0000000..29c397e --- /dev/null +++ b/bindings/python/python/numeria/audio/effects.pyi @@ -0,0 +1,440 @@ +""" +Audio effects: delays, reverbs (Schroeder, Freeverb, FDN), convolution, modulation effects, dynamics, distortion, EQ, imaging, loudness (ITU-R BS.1770), and dithering. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.dsp.iir import Biquad +from numeria.linalg.matrix import Matrix +from numeria.monte_carlo import Rng + +class AllpassFilter: + """ +Schroeder all-pass diffuser. + +Rust: `audio::effects::AllpassFilter` + """ + def __init__(self, delay_samples: int, gain: float) -> None: ... + def process(self, x: float) -> float: ... + @property + def gain(self) -> float: ... + +class Chorus: + """ +Chorus: LFO-modulated fractional delay mixed with the dry path. + +Rust: `audio::effects::Chorus` + """ + def __init__(self, fs: float) -> None: ... + def process(self, x: float) -> float: ... + @property + def base_ms(self) -> float: ... + @property + def depth_ms(self) -> float: ... + @property + def mix(self) -> float: ... + +class CombFilter: + """ +Feedback comb filter with a one-pole damping low-pass in the loop. + +Rust: `audio::effects::CombFilter` + """ + def __init__(self, delay_samples: int, feedback: float, damping: float) -> None: ... + def process(self, x: float) -> float: ... + @property + def feedback(self) -> float: ... + @property + def damping(self) -> float: ... + +class Compressor: + """ +Feed-forward compressor with soft knee and log-domain smoothing. + +Rust: `audio::effects::Compressor` + """ + def __init__(self, threshold_db: float, ratio: float, attack_ms: float, release_ms: float, fs: float) -> None: ... + def process(self, x: float) -> float: ... + def sidechain(self, x: float, key: float) -> float: ... + def gain_reduction_db(self) -> float: ... + @property + def threshold_db(self) -> float: ... + @property + def ratio(self) -> float: ... + @property + def attack_ms(self) -> float: ... + @property + def release_ms(self) -> float: ... + @property + def knee_db(self) -> float: ... + @property + def makeup_db(self) -> float: ... + +class DeEsser: + """ +De-esser: sibilance-band compressor (band-passed key). + +Rust: `audio::effects::DeEsser` + """ + def __init__(self, freq: float, threshold_db: float, fs: float) -> None: ... + def process(self, x: float) -> float: ... + +class DelayLine: + """ +Circular delay line with fractional read. + +Rust: `audio::effects::DelayLine` + """ + def __init__(self, max_samples: int) -> None: ... + def write(self, x: float) -> None: ... + def read(self, delay_samples: int) -> float: ... + def read_interp(self, delay_frac: float) -> float: ... + def tap(self, d: int) -> float: ... + +class Eq: + """ +A bank of peaking/shelf biquads. + +Rust: `audio::effects::Eq` + """ + @staticmethod + def graphic_10_band(fs: float) -> Eq: ... + @staticmethod + def parametric(bands: list[tuple[float, float, float]], fs: float) -> Eq: ... + def process(self, x: float) -> float: ... + def set_gain(self, i: int, db: float) -> None: ... + @property + def bands(self) -> list[Biquad]: ... + +class Exciter: + """ +Harmonic exciter: high-passed signal through a soft shaper, mixed in. + +Rust: `audio::effects::Exciter` + """ + def __init__(self, freq: float, drive: float, mix: float, fs: float) -> None: ... + def process(self, x: float) -> float: ... + @property + def drive(self) -> float: ... + @property + def mix(self) -> float: ... + +class Expander: + """ +Downward expander (gentler than a gate). + +Rust: `audio::effects::Expander` + """ + def __init__(self, threshold_db: float, ratio: float, fs: float) -> None: ... + def process(self, x: float) -> float: ... + @property + def threshold_db(self) -> float: ... + @property + def ratio(self) -> float: ... + +class Fdn: + """ +Feedback delay network reverb with an orthogonal mixing matrix. + +Rust: `audio::effects::Fdn` + """ + def __init__(self, n: int, delay_samples: list[int], fs: float) -> None: ... + @staticmethod + def householder_matrix(n: int) -> Matrix: ... + @staticmethod + def hadamard_matrix(n: int) -> Matrix: ... + def set_rt60(self, t60_low: float, t60_high: float, fs: float) -> None: ... + def process(self, x: float) -> float: ... + def process_stereo(self, x: float) -> tuple[float, float]: ... + @property + def matrix(self) -> Matrix: ... + @property + def gains(self) -> list[float]: ... + +class Flanger: + """ +Flanger: short modulated delay with feedback. + +Rust: `audio::effects::Flanger` + """ + def __init__(self, fs: float) -> None: ... + def process(self, x: float) -> float: ... + @property + def depth_ms(self) -> float: ... + @property + def feedback(self) -> float: ... + @property + def mix(self) -> float: ... + +class Freeverb: + """ +Jezar's Freeverb topology: 8 combs + 4 all-passes per channel with +a fixed stereo spread. + +Rust: `audio::effects::Freeverb` + """ + def __init__(self, fs: float) -> None: ... + def process(self, x: float) -> tuple[float, float]: ... + +class Limiter: + """ +Brickwall limiter with lookahead. + +Rust: `audio::effects::Limiter` + """ + def __init__(self, ceiling: float, lookahead_ms: float, release_ms: float, fs: float) -> None: ... + def process(self, x: float) -> float: ... + @property + def ceiling(self) -> float: ... + +class NoiseGate: + """ +Downward noise gate. + +Rust: `audio::effects::NoiseGate` + """ + def __init__(self, threshold: float, attack_ms: float, release_ms: float, fs: float) -> None: ... + def process(self, x: float) -> float: ... + @property + def threshold(self) -> float: ... + @property + def attack_ms(self) -> float: ... + @property + def release_ms(self) -> float: ... + +class PartitionedConvolver: + """ +Uniform partitioned (overlap-add, frequency-domain) convolver for +streaming long impulse responses. + +Rust: `audio::effects::PartitionedConvolver` + """ + def __init__(self, ir: list[float], block_size: int) -> None: ... + def process_block(self, x: list[float]) -> list[float]: ... + +class Phaser: + """ +Phaser: cascaded LFO-swept all-pass biquads. + +Rust: `audio::effects::Phaser` + """ + def __init__(self, fs: float) -> None: ... + def process(self, x: float) -> float: ... + @property + def mix(self) -> float: ... + +class SchroederReverb: + """ +Classic Schroeder reverberator: four parallel combs into two series +all-passes. + +Rust: `audio::effects::SchroederReverb` + """ + def __init__(self, fs: float) -> None: ... + def set_rt60(self, t: float) -> None: ... + def set_damping(self, d: float) -> None: ... + def process(self, x: float) -> float: ... + +class StereoWidener: + """ +Mid/side stereo widener. + +Rust: `audio::effects::StereoWidener` + """ + def __init__(self, width: float) -> None: ... + def process(self, l: float, r: float) -> tuple[float, float]: ... + @property + def width(self) -> float: ... + +class Tremolo: + """ +Tremolo (amplitude modulation by an LFO). + +Rust: `audio::effects::Tremolo` + """ + def __init__(self, rate_hz: float, depth: float, fs: float) -> None: ... + def process(self, x: float) -> float: ... + @property + def depth(self) -> float: ... + +class Vibrato: + """ +Vibrato (pitch modulation via modulated delay). + +Rust: `audio::effects::Vibrato` + """ + def __init__(self, rate_hz: float, depth_ms: float, fs: float) -> None: ... + def process(self, x: float) -> float: ... + @property + def depth_ms(self) -> float: ... + +def convolution_reverb(x: list[float], ir: list[float]) -> list[float]: + """ +Convolution reverb via the partitioned FFT convolver (matches direct +convolution; output length x + ir − 1). + +Rust: `audio::effects::convolution_reverb` + """ + ... + +def synthesize_ir_exponential(rt60: float, fs: float, early_reflections: list[tuple[float, float]], rng: Rng) -> list[float]: + """ +Synthetic exponential-decay impulse response with optional discrete +early reflections (time s, gain). + +Rust: `audio::effects::synthesize_ir_exponential` + """ + ... + +def distortion_soft_clip(x: float, drive: float) -> float: + """ +tanh soft clipper. + +Rust: `audio::effects::distortion_soft_clip` + """ + ... + +def distortion_hard_clip(x: float, threshold: float) -> float: + """ +Hard clipper at ±threshold. + +Rust: `audio::effects::distortion_hard_clip` + """ + ... + +def distortion_tube(x: float, drive: float, bias: float) -> float: + """ +Asymmetric "tube" shaper (bias shifts the operating point). + +Rust: `audio::effects::distortion_tube` + """ + ... + +def distortion_foldback(x: float, threshold: float) -> float: + """ +Foldback distortion. + +Rust: `audio::effects::distortion_foldback` + """ + ... + +def oversample_process(x: list[float], factor: int, f: Callable[[float], float]) -> list[float]: + """ +Run a memoryless nonlinearity oversampled by `factor` (anti-aliased: +upsample, apply, decimate). + +Rust: `audio::effects::oversample_process` + """ + ... + +def haas_delay(x: list[float], ms: float, fs: float) -> tuple[list[float], list[float]]: + """ +Haas effect: (dry, delayed) pair for pseudo-stereo width. + +Rust: `audio::effects::haas_delay` + """ + ... + +def pitch_shift_simple(x: list[float], semitones: float, fs: float) -> list[float]: + """ +Delay-line (Doppler) pitch shifter with two crossfaded taps. + +Rust: `audio::effects::pitch_shift_simple` + """ + ... + +def gain_db(x: MutableSequence[float], db: float) -> None: + """ +Apply a gain in dB in place. + +Rust: `audio::effects::gain_db` + """ + ... + +def normalize_peak(x: MutableSequence[float], target_db: float) -> None: + """ +Normalize the peak to `target_db` (dBFS) in place. + +Rust: `audio::effects::normalize_peak` + """ + ... + +def normalize_rms(x: MutableSequence[float], target_db: float) -> None: + """ +Normalize the RMS to `target_db` in place. + +Rust: `audio::effects::normalize_rms` + """ + ... + +def measure_lufs(x: list[float], fs: float) -> float: + """ +Integrated loudness (LUFS) per ITU-R BS.1770-4: K-weighting, 400 ms +blocks with 75% overlap, absolute −70 LUFS and relative −10 LU +gating. + +Rust: `audio::effects::measure_lufs` + """ + ... + +def normalize_lufs(x: MutableSequence[float], target_lufs: float, fs: float) -> None: + """ +Normalize integrated loudness to `target_lufs` in place. + +Rust: `audio::effects::normalize_lufs` + """ + ... + +def true_peak(x: list[float], fs: float) -> float: + """ +Inter-sample true peak (4× oversampled), linear. + +Rust: `audio::effects::true_peak` + """ + ... + +def dither_tpdf(x: list[float], bits: int, rng: Rng) -> list[float]: + """ +TPDF dither to `bits` (quantized output in −1..1). + +Rust: `audio::effects::dither_tpdf` + """ + ... + +def noise_shaping_dither(x: list[float], bits: int, rng: Rng) -> list[float]: + """ +First-order noise-shaped dither (error feedback pushes quantization +noise upward in frequency). + +Rust: `audio::effects::noise_shaping_dither` + """ + ... + +def dc_offset_remove(x: MutableSequence[float]) -> None: + """ +Remove the mean in place. + +Rust: `audio::effects::dc_offset_remove` + """ + ... + +def declick(x: list[float], threshold: float) -> list[float]: + """ +Replace samples whose second difference exceeds `threshold` with a +linear interpolation of their neighbors (simple click repair). + +Rust: `audio::effects::declick` + """ + ... + +def spectral_gate(x: list[float], noise_profile: list[float], threshold_db: float, n_fft: int, hop: int) -> list[float]: + """ +Spectral gate denoiser: attenuate STFT bins that fall below the +noise profile (per-bin magnitude) plus `threshold_db`. + +Rust: `audio::effects::spectral_gate` + """ + ... diff --git a/bindings/python/python/numeria/audio/envelope.pyi b/bindings/python/python/numeria/audio/envelope.pyi new file mode 100644 index 0000000..d07374f --- /dev/null +++ b/bindings/python/python/numeria/audio/envelope.pyi @@ -0,0 +1,167 @@ +""" +Envelopes, LFOs, followers, fades, and glides. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.statistics.distributions import Exponential +from numeria.audio.oscillators import Oscillator + +class Adsr: + """ +Linear (optionally exponential-curved) ADSR envelope; times in +seconds, sustain as a level in \\[0, 1\\]. + +Rust: `audio::envelope::Adsr` + """ + def __init__(self, attack: float, decay: float, sustain: float, release: float, fs: float) -> None: ... + def gate_on(self) -> None: ... + def gate_off(self) -> None: ... + def set_curve(self, exp: bool) -> None: ... + def next(self) -> float: ... + def is_active(self) -> bool: ... + @property + def attack(self) -> float: ... + @property + def decay(self) -> float: ... + @property + def sustain(self) -> float: ... + @property + def release(self) -> float: ... + +class AdsrExp: + """ +Exponential ADSR driven by RC time constants (τ per segment). + +Rust: `audio::envelope::AdsrExp` + """ + def __init__(self, attack_tau: float, decay_tau: float, sustain: float, release_tau: float, fs: float) -> None: ... + def gate_on(self) -> None: ... + def gate_off(self) -> None: ... + def next(self) -> float: ... + def is_active(self) -> bool: ... + @property + def attack_tau(self) -> float: ... + @property + def decay_tau(self) -> float: ... + @property + def sustain(self) -> float: ... + @property + def release_tau(self) -> float: ... + +class Ar: + """ +Simple linear attack-release envelope (a one-shot AD when the gate +is released immediately). + +Rust: `audio::envelope::Ar` + """ + def __init__(self, attack: float, release: float, fs: float) -> None: ... + def gate_on(self) -> None: ... + def gate_off(self) -> None: ... + def next(self) -> float: ... + def is_active(self) -> bool: ... + @property + def attack(self) -> float: ... + @property + def release(self) -> float: ... + +class FadeShape: + """ +Fade curve shapes. + +Rust: `audio::envelope::FadeShape` + """ + ... + +class Lfo: + """ +Low-frequency oscillator: scaled/offset wrapper over `Oscillator`. + +Rust: `audio::envelope::Lfo` + """ + def next(self) -> float: ... + def sync(self) -> None: ... + @property + def depth(self) -> float: ... + @property + def offset(self) -> float: ... + +def envelope_follower(x: list[float], attack_ms: float, release_ms: float, fs: float) -> list[float]: + """ +Peak envelope follower with attack/release time constants (ms). + +Rust: `audio::envelope::envelope_follower` + """ + ... + +def peak_envelope(x: list[float], window: int) -> list[float]: + """ +Sliding-window peak magnitude (centered). + +Rust: `audio::envelope::peak_envelope` + """ + ... + +def rms_envelope(x: list[float], window: int) -> list[float]: + """ +Sliding-window RMS (centered). + +Rust: `audio::envelope::rms_envelope` + """ + ... + +def exponential_decay_envelope(n: int, tau: float, fs: float) -> list[float]: + """ +e^(−t/τ) sampled for n samples. + +Rust: `audio::envelope::exponential_decay_envelope` + """ + ... + +def apply_envelope(x: MutableSequence[float], env: list[float]) -> None: + """ +Multiply a signal by an envelope in place. + +Rust: `audio::envelope::apply_envelope` + """ + ... + +def fade_in(x: MutableSequence[float], n: int, shape: FadeShape) -> None: + """ +Fade in the first n samples in place. + +Rust: `audio::envelope::fade_in` + """ + ... + +def fade_out(x: MutableSequence[float], n: int, shape: FadeShape) -> None: + """ +Fade out the last n samples in place. + +Rust: `audio::envelope::fade_out` + """ + ... + +def crossfade(a: list[float], b: list[float], shape: FadeShape) -> list[float]: + """ +Full-length crossfade from a to b. + +Panics: +Panics if the inputs differ in length. + +Rust: `audio::envelope::crossfade` + """ + ... + +def portamento(from_hz: float, to_hz: float, n: int, fs: float, exponential: bool) -> list[float]: + """ +Pitch glide trajectory (Hz per sample): linear or exponential +(constant cents/second) from one frequency to another. + +Rust: `audio::envelope::portamento` + """ + ... diff --git a/bindings/python/python/numeria/audio/oscillators.pyi b/bindings/python/python/numeria/audio/oscillators.pyi new file mode 100644 index 0000000..b7ba2c6 --- /dev/null +++ b/bindings/python/python/numeria/audio/oscillators.pyi @@ -0,0 +1,218 @@ +""" +Audio-rate oscillators and test signals: PolyBLEP anti-aliased classics, additive resynthesis, mipmapped wavetables, colored noise, chirps, and measurement sweeps. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.statistics.distributions import Exponential + +class NoiseColor: + """ +Noise spectra for `NoiseGen`. + +Rust: `audio::oscillators::NoiseColor` + """ + ... + +class NoiseGen: + """ +Deterministic colored-noise generator. + +Rust: `audio::oscillators::NoiseGen` + """ + def __init__(self, seed: int, color: NoiseColor) -> None: ... + def next(self) -> float: ... + +class Oscillator: + """ +Phase-accumulating audio oscillator (PolyBLEP for saw/square, +polyBLAMP triangle, seeded noise, optional wavetables). + +Rust: `audio::oscillators::Oscillator` + """ + def __init__(self, kind: Waveform, freq: float, fs: float) -> None: ... + def next(self) -> float: ... + def set_freq(self, freq: float) -> None: ... + def set_phase(self, phase: float) -> None: ... + def block(self, n: int) -> list[float]: ... + def fm(self, mod_hz: float) -> float: ... + def hard_sync(self, reset: bool) -> None: ... + @property + def phase(self) -> float: ... + @property + def freq(self) -> float: ... + @property + def fs(self) -> float: ... + @property + def kind(self) -> Waveform: ... + +class Waveform: + """ +Waveform selector for `Oscillator`. + +Rust: `audio::oscillators::Waveform` + """ + ... + +class Wavetable: + """ +Mipmapped single-cycle wavetable: table m is band-limited so that it +can play at up to `base_freqs[m]` without aliasing. + +Rust: `audio::oscillators::Wavetable` + """ + def __init__(self, tables: list[list[float]], base_freqs: list[float]) -> None: ... + @staticmethod + def from_fn(f: Callable[[float], float], size: int, n_mips: int, fs: float) -> Wavetable: ... + @staticmethod + def from_harmonics(amps: list[float], size: int, n_mips: int, fs: float) -> Wavetable: ... + def lookup(self, phase: float, freq: float) -> float: ... + @staticmethod + def saw(fs: float) -> Wavetable: ... + @staticmethod + def square(fs: float) -> Wavetable: ... + @staticmethod + def triangle(fs: float) -> Wavetable: ... + @property + def tables(self) -> list[list[float]]: ... + @property + def base_freqs(self) -> list[float]: ... + +def polyblep_saw(phase: float, dt: float) -> float: + """ +Anti-aliased sawtooth (−1..1) at phase t with increment dt. + +Rust: `audio::oscillators::polyblep_saw` + """ + ... + +def polyblep_square(phase: float, dt: float, duty: float) -> float: + """ +Anti-aliased pulse with the given duty cycle. + +Rust: `audio::oscillators::polyblep_square` + """ + ... + +def polyblep_triangle(phase: float, dt: float) -> float: + """ +Anti-aliased triangle (corner smoothing by polyBLAMP; triangle +aliasing is already −12 dB/oct so the correction is mild). + +Rust: `audio::oscillators::polyblep_triangle` + """ + ... + +def additive_saw(phase: float, n_harmonics: int) -> float: + """ +Band-limited saw from its Fourier series (n harmonics). + +Rust: `audio::oscillators::additive_saw` + """ + ... + +def additive_square(phase: float, n_harmonics: int) -> float: + """ +Band-limited square from its Fourier series. + +Rust: `audio::oscillators::additive_square` + """ + ... + +def additive_triangle(phase: float, n_harmonics: int) -> float: + """ +Band-limited triangle from its Fourier series. + +Rust: `audio::oscillators::additive_triangle` + """ + ... + +def chirp_linear(f0: float, f1: float, duration: float, fs: float) -> list[float]: + """ +Linear chirp from f0 to f1 over `duration` seconds. + +Rust: `audio::oscillators::chirp_linear` + """ + ... + +def chirp_exponential(f0: float, f1: float, duration: float, fs: float) -> list[float]: + """ +Exponential (logarithmic-sweep) chirp. + +Rust: `audio::oscillators::chirp_exponential` + """ + ... + +def chirp_hyperbolic(f0: float, f1: float, duration: float, fs: float) -> list[float]: + """ +Hyperbolic chirp (linear period sweep). + +Rust: `audio::oscillators::chirp_hyperbolic` + """ + ... + +def sine_sweep_with_inverse(f0: float, f1: float, duration: float, fs: float) -> tuple[list[float], list[float]]: + """ +Farina exponential sweep and its inverse filter: convolving the two +yields (a delayed) impulse, the standard impulse-response +measurement pair. + +Rust: `audio::oscillators::sine_sweep_with_inverse` + """ + ... + +def impulse(n: int, pos: int) -> list[float]: + """ +Unit impulse at `pos` in an n-sample buffer. + +Rust: `audio::oscillators::impulse` + """ + ... + +def dc(n: int, level: float) -> list[float]: + """ +Constant (DC) buffer. + +Rust: `audio::oscillators::dc` + """ + ... + +def multisine(freqs: list[float], amps: list[float], phases: list[float], n: int, fs: float) -> list[float]: + """ +Sum of sinusoids with per-tone amplitude and phase. + +Panics: +Panics if the parameter arrays differ in length. + +Rust: `audio::oscillators::multisine` + """ + ... + +def schroeder_phase_multisine(n_tones: int, n: int, fs: float) -> list[float]: + """ +Schroeder-phase multisine of `n_tones` bin-aligned harmonics of +fs/n: near-minimal crest factor for broadband excitation. + +Rust: `audio::oscillators::schroeder_phase_multisine` + """ + ... + +def pulse_train(freq: float, width: float, n: int, fs: float) -> list[float]: + """ +Rectangular pulse train: `width` seconds high per period. + +Rust: `audio::oscillators::pulse_train` + """ + ... + +def band_limited_impulse_train(freq: float, n: int, fs: float) -> list[float]: + """ +Band-limited impulse train (all cosine harmonics up to Nyquist, +unit DC component). + +Rust: `audio::oscillators::band_limited_impulse_train` + """ + ... diff --git a/bindings/python/python/numeria/audio/physical.pyi b/bindings/python/python/numeria/audio/physical.pyi new file mode 100644 index 0000000..5ac5890 --- /dev/null +++ b/bindings/python/python/numeria/audio/physical.pyi @@ -0,0 +1,247 @@ +""" +Physical modeling synthesis: digital waveguides (plucked/struck/bowed strings, clarinet and flute bores), modal synthesis (bars, membranes, plates, bells, glasses), finite-difference membranes and Kirchhoff plates, a brute-force mass-spring string for validation, the Kelly-Lochbaum vocal tract, and glottal source models. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.dsp.iir import Biquad + +class BowedString: + """ +Bowed string: two waveguide segments joined at the bow point with a +stick-slip friction curve producing Helmholtz motion. + +Rust: `audio::physical::BowedString` + """ + def __init__(self, freq: float, fs: float) -> None: ... + def next(self) -> float: ... + @property + def bow_velocity(self) -> float: ... + @property + def bow_force(self) -> float: ... + +class KellyLochbaum: + """ +Kelly-Lochbaum piecewise-cylindrical vocal tract lattice. + +Rust: `audio::physical::KellyLochbaum` + """ + def set_areas(self, area_function: list[float]) -> None: ... + def next(self, glottal: float) -> float: ... + @property + def glottal_reflection(self) -> float: ... + @property + def lip_reflection(self) -> float: ... + +class MassSpringString: + """ +Brute-force lumped mass-spring string with fixed ends, for validating +the waveguide against a direct Newtonian simulation. + +Rust: `audio::physical::MassSpringString` + """ + def __init__(self, freq: float, n: int, fs: float) -> None: ... + def pluck(self, pos: float, amp: float) -> None: ... + def next(self) -> float: ... + @property + def masses(self) -> list[float]: ... + @property + def positions(self) -> list[float]: ... + @property + def velocities(self) -> list[float]: ... + @property + def k(self) -> float: ... + @property + def damping(self) -> float: ... + +class Membrane2D: + """ +Circular drum head on a masked finite-difference grid, audio-rate. + +Rust: `audio::physical::Membrane2D` + """ + @staticmethod + def drum(radius_m: float, tension: float, density: float, res: int, fs: float) -> Membrane2D: ... + def strike(self, x: float, y: float, vel: float) -> None: ... + def next(self) -> float: ... + +class ModalSynth: + """ +Bank of two-pole resonators driven by an excitation buffer. + +Rust: `audio::physical::ModalSynth` + """ + @staticmethod + def from_modes(modes: list[tuple[float, float, float]], fs: float) -> ModalSynth: ... + @staticmethod + def bar(length: float, width: float, thickness: float, young: float, rho: float, fs: float) -> ModalSynth: ... + @staticmethod + def membrane_circular(radius: float, tension: float, sigma: float, fs: float) -> ModalSynth: ... + @staticmethod + def plate(a: float, b: float, thickness: float, young: float, rho: float, nu: float, fs: float) -> ModalSynth: ... + @staticmethod + def bell(radius: float, thickness: float, young: float, rho: float, nu: float, fs: float) -> ModalSynth: ... + @staticmethod + def glass(f0: float, fs: float) -> ModalSynth: ... + def excite(self, impulse: list[float]) -> None: ... + def strike(self, hardness: float) -> None: ... + def next(self) -> float: ... + @property + def modes(self) -> list[tuple[float, float, float]]: ... + +class Plate2D: + """ +Simply supported Kirchhoff plate (u_tt = -κ² ∇⁴u) on a +finite-difference grid, audio-rate. + +Rust: `audio::physical::Plate2D` + """ + def __init__(self, a: float, b: float, thickness: float, young: float, rho: float, nu: float, res: int, fs: float) -> None: ... + def strike(self, x: float, y: float, vel: float) -> None: ... + def next(self) -> float: ... + @property + def damping(self) -> float: ... + +class WaveguideString: + """ +Bidirectional digital waveguide string with bridge damping filter, +optional stiffness allpass, and an internal tuning allpass keeping the +pitch exact at the fundamental. + +Rust: `audio::physical::WaveguideString` + """ + def __init__(self, freq: float, fs: float) -> None: ... + def set_freq(self, freq: float) -> None: ... + def freq(self) -> float: ... + def pluck(self, pos: float, amp: float, width: float) -> None: ... + def strike(self, pos: float, vel: float) -> None: ... + def bow(self, force: float, velocity: float, pos: float) -> None: ... + def next(self) -> float: ... + def output_at(self, pos: float) -> float: ... + @property + def damping(self) -> float: ... + @property + def stiffness_allpass(self) -> Biquad: ... + @property + def bridge_filter(self) -> Biquad: ... + @property + def pluck_pos(self) -> float: ... + +class WaveguideTube: + """ +Single-reed (clarinet) or jet (flute) waveguide wind instrument. + +Rust: `audio::physical::WaveguideTube` + """ + @staticmethod + def clarinet(freq: float, fs: float) -> WaveguideTube: ... + @staticmethod + def flute(freq: float, fs: float) -> WaveguideTube: ... + def set_breath(self, p: float) -> None: ... + def next(self) -> float: ... + +def banded_waveguide(freq: float, bands: list[tuple[float, float]], fs: float) -> list[WaveguideString]: + """ +One waveguide per band: (center frequency, T60 seconds) pairs, as used +in banded waveguide synthesis of stiff/inharmonic objects. + +Rust: `audio::physical::banded_waveguide` + """ + ... + +def commuted_synthesis(body_ir: list[float], excitation: list[float], string: WaveguideString, n: int) -> list[float]: + """ +Commuted synthesis: the body impulse response is convolved into the +excitation and fed through the string, avoiding a body filter at +synthesis time. + +Rust: `audio::physical::commuted_synthesis` + """ + ... + +def hammer_string_interaction(string: WaveguideString, hammer_mass: float, hammer_vel: float, stiffness_exp: float, k: float) -> list[float]: + """ +Piano hammer-string contact: a hammer of mass `hammer_mass` (kg) with +initial velocity `hammer_vel` compresses a nonlinear felt spring +F = k ξ^p against the string. Returns the contact force history +(one sample per tick until separation); the string is excited in place. + +Rust: `audio::physical::hammer_string_interaction` + """ + ... + +def reed_nonlinearity(delta_p: float, stiffness: float, closing_p: float) -> float: + """ +Single-reed reflection coefficient as a function of the pressure +difference across the reed (STK-style linear table, clamped to ±1). + +Rust: `audio::physical::reed_nonlinearity` + """ + ... + +def jet_nonlinearity(x: float) -> float: + """ +Flute jet nonlinearity x - x³, clamped to ±1. + +Rust: `audio::physical::jet_nonlinearity` + """ + ... + +def lip_model(delta_p: float, lip_tension: float) -> float: + """ +Brass lip valve: pressure-controlled transmission coefficient; the +lips open on positive mouth-bore pressure difference. + +Rust: `audio::physical::lip_model` + """ + ... + +def vocal_tract(area_function: list[float], fs: float) -> KellyLochbaum: + """ +Build a Kelly-Lochbaum lattice from a tract area function (cm² or any +consistent unit); each section is one sample of travel at `fs`. + +Rust: `audio::physical::vocal_tract` + """ + ... + +def glottal_pulse_lf(t: float, t0: float, te: float, tp: float, ta: float) -> float: + """ +Simplified Liljencrants-Fant glottal flow *derivative* pulse over one +period t ∈ [0, t0): exponentially growing sinusoid up to `te` (peak of +the sinusoid at `tp`), then an exponential return phase with time +constant `ta`. + +Rust: `audio::physical::glottal_pulse_lf` + """ + ... + +def rosenberg_pulse(phase: float, open_quotient: float) -> float: + """ +Rosenberg glottal flow pulse: raised-cosine rise over the first 2/3 of +the open phase, cosine fall over the last 1/3, zero when closed. +`phase` in [0, 1), `open_quotient` in (0, 1]. + +Rust: `audio::physical::rosenberg_pulse` + """ + ... + +def string_tension_from_freq(freq: float, length: float, mu: float) -> float: + """ +Tension (N) needed for a string of `length` (m) and line density `mu` +(kg/m) to sound at `freq`: T = μ (2 L f)². + +Rust: `audio::physical::string_tension_from_freq` + """ + ... + +def inharmonic_partials(f0: float, b: float, n: int) -> list[float]: + """ +Piano-style stretched partials f_k = k f0 √(1 + B k²). + +Rust: `audio::physical::inharmonic_partials` + """ + ... diff --git a/bindings/python/python/numeria/audio/spatial.pyi b/bindings/python/python/numeria/audio/spatial.pyi new file mode 100644 index 0000000..1540843 --- /dev/null +++ b/bindings/python/python/numeria/audio/spatial.pyi @@ -0,0 +1,265 @@ +""" +Spatial audio: panning laws, VBAP, ambisonics, simple binaural cues, Doppler, distance/air attenuation, geometric room acoustics (image source and ray tracing), microphone arrays (beamforming, TDOA localization), sonar, and loudspeaker system responses. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.monte_carlo import Rng +from numeria.dsp.iir import Sos +from numeria.math import Vec3 + +def pan_linear(x: float, pos: float) -> tuple[float, float]: + """ +Linear pan law; `pos` in [-1 (left), 1 (right)]. + +Rust: `audio::spatial::pan_linear` + """ + ... + +def pan_constant_power(x: float, pos: float) -> tuple[float, float]: + """ +Constant-power (-3 dB center) pan law. + +Rust: `audio::spatial::pan_constant_power` + """ + ... + +def pan_minus_4_5_db(x: float, pos: float) -> tuple[float, float]: + """ +-4.5 dB-center compromise pan law (geometric mean of the linear and +constant-power laws). + +Rust: `audio::spatial::pan_minus_4_5_db` + """ + ... + +def pan_vbap_2d(angle: float, speaker_angles: list[float]) -> list[float]: + """ +2D VBAP: gains for `speaker_angles` (radians, unsorted) reproducing a +source at `angle`; only the flanking pair is nonzero. + +Rust: `audio::spatial::pan_vbap_2d` + """ + ... + +def pan_vbap_3d(dir: Vec3 | Sequence[float], speakers: list[Vec3 | Sequence[float]]) -> list[float]: + """ +3D VBAP over speaker triplets: picks the triplet giving all-positive +gains with the best conditioning, normalized to unit power. + +Rust: `audio::spatial::pan_vbap_3d` + """ + ... + +def ambisonics_encode_1st(x: float, azimuth: float, elevation: float) -> list[float]: + """ +First-order B-format (FuMa WXYZ) encoding of one sample. + +Rust: `audio::spatial::ambisonics_encode_1st` + """ + ... + +def ambisonics_encode(x: float, az: float, el: float, order: int) -> list[float]: + """ +Higher-order ambisonic encoding (ACN channel order, SN3D weights) of +one sample; (order+1)² channels. + +Rust: `audio::spatial::ambisonics_encode` + """ + ... + +def ambisonics_decode(b: list[float], speakers: list[tuple[float, float]], order: int) -> list[float]: + """ +Basic projection decode of an ACN/SN3D signal set to speakers at +(azimuth, elevation) pairs. + +Rust: `audio::spatial::ambisonics_decode` + """ + ... + +def ambisonics_rotate(b: list[float], yaw: float, pitch: float, roll: float, order: int) -> list[float]: + """ +Rotate an ACN/SN3D ambisonic frame by yaw/pitch/roll, via projection +onto a Fibonacci sphere sampling (exact for band-limited fields as the +sampling is dense; 256 points). + +Rust: `audio::spatial::ambisonics_rotate` + """ + ... + +def itd_woodworth(azimuth: float, head_radius: float, c: float) -> float: + """ +Woodworth interaural time difference (s) for a spherical head of +radius `head_radius`; `azimuth` in radians from the median plane. + +Rust: `audio::spatial::itd_woodworth` + """ + ... + +def spherical_head_hrtf(azimuth: float, freq: float, head_radius: float, c: float) -> complex: + """ +Duda-Martens (Brown-Duda) spherical-head shadowing filter response at +one ear; `azimuth` is measured from that ear's axis (0 = ipsilateral). + +Rust: `audio::spatial::spherical_head_hrtf` + """ + ... + +def ild_spherical_head(azimuth: float, freq: float, head_radius: float) -> float: + """ +Interaural level difference (dB, positive = louder in the near ear) +from the spherical-head model. + +Rust: `audio::spatial::ild_spherical_head` + """ + ... + +def binaural_simple(x: list[float], azimuth: float, elevation: float, fs: float) -> tuple[list[float], list[float]]: + """ +Simple binaural rendering: ITD (fractional delay) plus first-order +head-shadow filtering per ear. + +Rust: `audio::spatial::binaural_simple` + """ + ... + +def doppler_resample(x: list[float], source_path: Callable[[float], Vec3 | Sequence[float]], listener: Vec3 | Sequence[float], c: float, fs: float) -> list[float]: + """ +Doppler by retarded-time resampling: the source moves along +`source_path(t)`; each output sample reads the emission-time signal +value with 1/r distance attenuation. + +Rust: `audio::spatial::doppler_resample` + """ + ... + +def distance_gain(d: float, ref_d: float, rolloff: float) -> float: + """ +Inverse-distance gain with reference distance and rolloff exponent. + +Rust: `audio::spatial::distance_gain` + """ + ... + +def air_absorption_filter(d: float, humidity: float, temp: float, fs: float) -> Sos: + """ +Atmospheric absorption over distance `d` approximated as a 2nd-order +Butterworth lowpass whose cutoff gives 3 dB of ISO 9613-style +high-frequency loss at that range. + +Rust: `audio::spatial::air_absorption_filter` + """ + ... + +def image_source_ir(room: Vec3 | Sequence[float], source: Vec3 | Sequence[float], listener: Vec3 | Sequence[float], absorption: list[float], max_order: int, fs: float, c: float) -> list[float]: + """ +Shoebox image-source impulse response (Allen-Berkley). `absorption` +holds wall absorption coefficients in the order +[-x, +x, -y, +y, -z, +z]. + +Rust: `audio::spatial::image_source_ir` + """ + ... + +def ray_tracing_ir(room_mesh: Mesh, source: Vec3 | Sequence[float], listener: Vec3 | Sequence[float], absorption: list[float], n_rays: int, max_bounces: int, fs: float, c: float, rng: Rng) -> list[float]: + """ +Stochastic ray-traced energy impulse response in an arbitrary closed +mesh; `absorption[i]` indexes by triangle material. Amplitude is the +square root of collected energy per sample bin. + +Rust: `audio::spatial::ray_tracing_ir` + """ + ... + +def early_reflections(room: Vec3 | Sequence[float], source: Vec3 | Sequence[float], listener: Vec3 | Sequence[float], c: float) -> list[tuple[float, float, Vec3]]: + """ +Direct sound plus the six first-order reflections of a shoebox room: +(arrival time s, 1/(4πd) gain, unit direction of arrival). + +Rust: `audio::spatial::early_reflections` + """ + ... + +def beamforming_delay_sum(mics: list[Vec3 | Sequence[float]], signals: list[list[float]], steer: Vec3 | Sequence[float], fs: float, c: float) -> list[float]: + """ +Delay-and-sum beamformer steered toward the unit direction `steer` +(plane-wave model): aligns and averages the mic signals. + +Rust: `audio::spatial::beamforming_delay_sum` + """ + ... + +def beamforming_mvdr(mics: list[Vec3 | Sequence[float]], signals: list[list[float]], steer: Vec3 | Sequence[float], freq: float, fs: float, c: float, diagonal_loading: float) -> list[float]: + """ +Narrowband frequency-domain MVDR beamformer at `freq`: per-block +spatial covariance with diagonal loading, steering toward `steer`. + +Rust: `audio::spatial::beamforming_mvdr` + """ + ... + +def tdoa_gcc_phat(a: list[float], b: list[float], fs: float) -> float: + """ +GCC-PHAT time difference of arrival: delay of `b` relative to `a` in +seconds (positive = b lags a). + +Rust: `audio::spatial::tdoa_gcc_phat` + """ + ... + +def localize_tdoa(mics: list[Vec3 | Sequence[float]], tdoas: list[float], c: float) -> Vec3: + """ +Least-squares source localization from TDOAs relative to `mics[0]` +(`tdoas[i]` is the extra delay at mic i+1), by Gauss-Newton. + +Rust: `audio::spatial::localize_tdoa` + """ + ... + +def sonar_range(t_echo: float, c: float) -> float: + """ +Round-trip echo time to range. + +Rust: `audio::spatial::sonar_range` + """ + ... + +def sonar_equation(sl: float, tl: float, ts: float, nl: float, di: float) -> float: + """ +Active sonar equation: echo excess = SL - 2 TL + TS - (NL - DI), dB. + +Rust: `audio::spatial::sonar_equation` + """ + ... + +def speaker_crossover_lr4(fc: float, fs: float) -> tuple[Sos, Sos]: + """ +Linkwitz-Riley 4th-order crossover: (lowpass, highpass), each two +cascaded 2nd-order Butterworth sections; the pair sums to allpass. + +Rust: `audio::spatial::speaker_crossover_lr4` + """ + ... + +def speaker_baffle_step(width_m: float, fs: float) -> Sos: + """ +Baffle-step compensation target: the +6 dB diffraction step of a +baffle of width `width_m`, centered at f3 = 115/width, as a high +shelf. + +Rust: `audio::spatial::speaker_baffle_step` + """ + ... + +def thiele_small_response(fs_driver: float, qts: float, vas: float, box_volume: float, f: float) -> float: + """ +Sealed-box (2nd-order highpass) response magnitude in dB of a driver +with free-air resonance `fs_driver`, total Q `qts`, and compliance +volume `vas` in a box of `box_volume` (same units), at frequency `f`. + +Rust: `audio::spatial::thiele_small_response` + """ + ... diff --git a/bindings/python/python/numeria/audio/synthesis.pyi b/bindings/python/python/numeria/audio/synthesis.pyi new file mode 100644 index 0000000..4f0d529 --- /dev/null +++ b/bindings/python/python/numeria/audio/synthesis.pyi @@ -0,0 +1,283 @@ +""" +Sound synthesis: additive, FM (DX7-style operator routing), Karplus-Strong, subtractive, granular, formant, waveshaping, drums, and note/sequence rendering. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.audio.envelope import Adsr +from numeria.monte_carlo import Rng +from numeria.dsp.iir import Sos +from numeria.audio.oscillators import Waveform + +class FmOperator: + """ +One FM operator: frequency ratio, modulation index (as output +amplitude when used as a modulator), envelope, and self-feedback. + +Rust: `audio::synthesis::FmOperator` + """ + @property + def ratio(self) -> float: ... + @property + def index(self) -> float: ... + @property + def feedback(self) -> float: ... + @property + def phase(self) -> float: ... + +class FmSynth: + """ +DX7-style FM synth: `algorithm[i]` lists the operators that modulate +operator i (an empty list means it is a carrier unless someone else +consumes it; operators that appear in no modulation list sum into +the output). + +Rust: `audio::synthesis::FmSynth` + """ + @staticmethod + def dx7_algorithm(n: int) -> list[list[int]]: ... + def note_on(self, freq: float) -> None: ... + def note_off(self) -> None: ... + def next(self) -> float: ... + def render(self, freq: float, duration: float) -> list[float]: ... + @property + def algorithm(self) -> list[list[int]]: ... + @property + def fs(self) -> float: ... + +class Voice: + """ +Voice types for `vowel_formants`. + +Rust: `audio::synthesis::Voice` + """ + ... + +def additive(harmonics: list[tuple[float, float, float]], freq: float, n: int, fs: float) -> list[float]: + """ +Additive synthesis from (ratio, amplitude, phase) partials of a +fundamental `freq`. + +Rust: `audio::synthesis::additive` + """ + ... + +def additive_evolving(harmonics: list[tuple[float, list[float]]], freq: float, n: int, fs: float) -> list[float]: + """ +Additive synthesis with a per-partial amplitude envelope +(each envelope is resampled to n output samples). + +Rust: `audio::synthesis::additive_evolving` + """ + ... + +def fm_simple(carrier: float, modulator: float, index: float, n: int, fs: float) -> list[float]: + """ +Two-operator FM: sin(2πf_c·t + I·sin(2πf_m·t)). + +Rust: `audio::synthesis::fm_simple` + """ + ... + +def fm_bessel_sidebands(index: float, n_sidebands: int) -> list[float]: + """ +Bessel sideband amplitudes |J_k(I)| for k = 0..n_sidebands. + +Rust: `audio::synthesis::fm_bessel_sidebands` + """ + ... + +def pm_simple(carrier: float, modulator: float, index: float, n: int, fs: float) -> list[float]: + """ +Phase modulation (identical spectrum to `fm_simple` for a sine +modulator). + +Rust: `audio::synthesis::pm_simple` + """ + ... + +def am(carrier: float, modulator: float, depth: float, n: int, fs: float) -> list[float]: + """ +Amplitude modulation (1 + depth·sin(2πf_m t))·sin(2πf_c t). + +Rust: `audio::synthesis::am` + """ + ... + +def ring_mod(a: list[float], b: list[float]) -> list[float]: + """ +Ring modulation a·b. + +Rust: `audio::synthesis::ring_mod` + """ + ... + +def karplus_strong(freq: float, duration: float, fs: float, decay: float, blend: float, rng: Rng) -> list[float]: + """ +Karplus-Strong plucked string: noise burst through the averaging +loop. `decay` scales the loop gain, `blend` the averaging strength. + +Rust: `audio::synthesis::karplus_strong` + """ + ... + +def karplus_strong_extended(freq: float, duration: float, fs: float, pick_pos: float, pick_width: float, decay: float, dynamics: float) -> list[float]: + """ +Extended Karplus-Strong: pick position comb, pick-direction width +low-pass, and a dynamics low-pass on the excitation. + +Rust: `audio::synthesis::karplus_strong_extended` + """ + ... + +def subtractive(source: Waveform, freq: float, filter: Sos, env: Adsr, filter_env_amount: float, n: int) -> list[float]: + """ +Subtractive synthesis: raw oscillator through a filter with an +amplitude envelope (`filter_env_amount` scales a per-sample cutoff +bias applied as post-gain tilt on the filtered signal — a simple +stand-in for a modulated-cutoff filter). + +Rust: `audio::synthesis::subtractive` + """ + ... + +def granular(grain_source: list[float], grain_size: float, density: float, pitch_shift: float, position: float, spread: float, n: int, fs: float, rng: Rng) -> list[float]: + """ +Granular synthesis: Hann-windowed grains read from a source buffer +at `position` (0..1, with `spread` jitter), pitch shifted by +resampled playback, `density` grains per second. + +Rust: `audio::synthesis::granular` + """ + ... + +def vowel_formants(vowel: str, voice: Voice) -> list[tuple[float, float, float]]: + """ +Classic (Peterson-Barney-style) formant tables for the vowels +a, e, i, o, u: (frequency, bandwidth, amplitude) triples. + +Rust: `audio::synthesis::vowel_formants` + """ + ... + +def formant_synth(f0: float, formants: list[tuple[float, float, float]], n: int, fs: float) -> list[float]: + """ +Formant synthesis: a pulse-train glottal source through parallel +resonators (freq, bandwidth, amp). + +Rust: `audio::synthesis::formant_synth` + """ + ... + +def pulsar_synthesis(f0: float, formant: float, duty: float, n: int, fs: float) -> list[float]: + """ +Pulsar synthesis: a formant-frequency sinusoid burst repeated at f0 +with the given duty cycle. + +Rust: `audio::synthesis::pulsar_synthesis` + """ + ... + +def phase_distortion(phase: float, amount: float, kind: int) -> float: + """ +Casio CZ-style phase distortion: warp the phase ramp before the +cosine lookup. `kind` 0 = knee (saw-like), 1 = resonant sweep. + +Rust: `audio::synthesis::phase_distortion` + """ + ... + +def waveshaper(x: float, f: Callable[[float], float]) -> float: + """ +Apply an arbitrary waveshaper. + +Rust: `audio::synthesis::waveshaper` + """ + ... + +def chebyshev_waveshaper(x: float, harmonic_amps: list[float]) -> float: + """ +Chebyshev waveshaper: Σ a_k·T_k(x) turns a pure cosine at amplitude +1 into exactly the requested harmonic mix. + +Rust: `audio::synthesis::chebyshev_waveshaper` + """ + ... + +def hard_sync_osc(master_freq: float, slave_freq: float, n: int, fs: float) -> list[float]: + """ +Hard-synced sawtooth: a slave saw retriggered at the master rate. + +Rust: `audio::synthesis::hard_sync_osc` + """ + ... + +def supersaw(freq: float, detune: float, n_voices: int, n: int, fs: float) -> list[float]: + """ +Detuned saw stack (JP-8000 style supersaw): `detune` is the maximum +relative detune of the outer voices. + +Rust: `audio::synthesis::supersaw` + """ + ... + +def sample_playback(sample: list[float], rate_ratio: float, loop_start: int, loop_end: int, n: int) -> list[float]: + """ +Sample playback with loop points and linear-interpolated rate +conversion. + +Rust: `audio::synthesis::sample_playback` + """ + ... + +def drum_kick(fs: float, pitch_start: float, pitch_end: float, decay: float) -> list[float]: + """ +Kick drum: exponential pitch sweep with an exponential amplitude +decay. + +Rust: `audio::synthesis::drum_kick` + """ + ... + +def drum_snare(fs: float) -> list[float]: + """ +Snare: tone plus band-passed noise, both decaying. + +Rust: `audio::synthesis::drum_snare` + """ + ... + +def drum_hihat(fs: float) -> list[float]: + """ +Hi-hat: short bright filtered noise burst. + +Rust: `audio::synthesis::drum_hihat` + """ + ... + +def drum_clap(fs: float) -> list[float]: + """ +Clap: a few staggered noise bursts. + +Rust: `audio::synthesis::drum_clap` + """ + ... + +def drum_tom(fs: float, pitch: float) -> list[float]: + """ +Tom: pitch-swept sine, longer than a kick. + +Rust: `audio::synthesis::drum_tom` + """ + ... + +def mix(tracks: list[list[float]], gains: list[float]) -> list[float]: + """ +Mix tracks with per-track gains (output as long as the longest track). + +Rust: `audio::synthesis::mix` + """ + ... diff --git a/bindings/python/python/numeria/audio/tuning.pyi b/bindings/python/python/numeria/audio/tuning.pyi new file mode 100644 index 0000000..3b52a27 --- /dev/null +++ b/bindings/python/python/numeria/audio/tuning.pyi @@ -0,0 +1,238 @@ +""" +Musical tuning: temperaments, interval math, Scala parsing, consonance models, stretch tuning, and pitch-class utilities. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +class ChordQuality: + """ +Chord qualities. + +Rust: `audio::tuning::ChordQuality` + """ + ... + +class Mode: + """ +Diatonic modes and common scales. + +Rust: `audio::tuning::Mode` + """ + ... + +def equal_temperament(n_divisions: int, base_hz: float, base_midi: int) -> list[float]: + """ +MIDI frequencies (128 entries) for an equal temperament with +`n_divisions` steps per octave anchored at (`base_midi`, `base_hz`). + +Rust: `audio::tuning::equal_temperament` + """ + ... + +def just_intonation_5limit() -> list[float]: + """ +5-limit just intonation ratios from the tonic. + +Rust: `audio::tuning::just_intonation_5limit` + """ + ... + +def pythagorean() -> list[float]: + """ +Pythagorean (3-limit) chromatic scale ratios. + +Rust: `audio::tuning::pythagorean` + """ + ... + +def meantone_quarter_comma() -> list[float]: + """ +Quarter-comma meantone: fifths flattened so major thirds are pure 5/4. + +Rust: `audio::tuning::meantone_quarter_comma` + """ + ... + +def werckmeister_iii() -> list[float]: + """ +Werckmeister III well temperament (1691), as ratios from C. + +Rust: `audio::tuning::werckmeister_iii` + """ + ... + +def kirnberger_iii() -> list[float]: + """ +Kirnberger III well temperament, as ratios from C. + +Rust: `audio::tuning::kirnberger_iii` + """ + ... + +def young() -> list[float]: + """ +Thomas Young's 1799 well temperament (Young II), as ratios from C. + +Rust: `audio::tuning::young` + """ + ... + +def bohlen_pierce() -> list[float]: + """ +Bohlen-Pierce scale: 13 equal divisions of the tritave (3:1); returns +the 14 ratios including both endpoints. + +Rust: `audio::tuning::bohlen_pierce` + """ + ... + +def harmonic_series_scale(n: int) -> list[float]: + """ +Harmonic-series scale: partials n..2n reduced to ratios from 1 to 2. + +Rust: `audio::tuning::harmonic_series_scale` + """ + ... + +def scala_parse(scl: str) -> list[float]: + """ +Parse a Scala `.scl` file body into cents values (one per scale +degree, ending with the octave entry). Ratios like `3/2` and cents +like `701.955` are both accepted. + +Rust: `audio::tuning::scala_parse` + """ + ... + +def cents_between(f1: float, f2: float) -> float: + """ +Signed interval from `f1` to `f2` in cents. + +Rust: `audio::tuning::cents_between` + """ + ... + +def ratio_to_cents(r: float) -> float: + """ +Frequency ratio to cents. + +Rust: `audio::tuning::ratio_to_cents` + """ + ... + +def cents_to_ratio(c: float) -> float: + """ +Cents to frequency ratio. + +Rust: `audio::tuning::cents_to_ratio` + """ + ... + +def nearest_note(freq: float, a4: float) -> tuple[int, float]: + """ +Nearest 12-TET MIDI note to `freq` for the given A4: returns +(midi, cents deviation from that note). + +Rust: `audio::tuning::nearest_note` + """ + ... + +def interval_name(ratio: float) -> str: + """ +Name of the just interval closest to `ratio` (within 6 cents), or +"unknown". + +Rust: `audio::tuning::interval_name` + """ + ... + +def consonance_plomp_levelt(f1: float, f2: float) -> float: + """ +Plomp-Levelt consonance of two pure tones: 1 at unison, minimum near +a quarter of a critical band apart. + +Rust: `audio::tuning::consonance_plomp_levelt` + """ + ... + +def dissonance_curve(base: float, partials: list[tuple[float, float]], ratio_range: tuple[float, float], n: int) -> list[tuple[float, float]]: + """ +Sethares dissonance curve: total pairwise Plomp-Levelt dissonance of +two copies of a `partials` timbre (`(ratio, amplitude)` relative to +`base` Hz) as the second copy sweeps through `ratio_range`. Returns +`n` points of (interval ratio, dissonance). + +Rust: `audio::tuning::dissonance_curve` + """ + ... + +def stretch_tuning_railsback(midi: float, b: float) -> float: + """ +Piano stretch tuning deviation (cents from 12-TET) for a constant +string inharmonicity coefficient `b`: octaves are widened so partial 2 +of the lower note matches the fundamental of its octave. + +Rust: `audio::tuning::stretch_tuning_railsback` + """ + ... + +def syntonic_comma() -> float: + """ +The syntonic comma 81/80. + +Rust: `audio::tuning::syntonic_comma` + """ + ... + +def pythagorean_comma() -> float: + """ +The Pythagorean comma 3¹²/2¹⁹. + +Rust: `audio::tuning::pythagorean_comma` + """ + ... + +def schisma() -> float: + """ +The schisma 32805/32768 (Pythagorean comma / syntonic comma). + +Rust: `audio::tuning::schisma` + """ + ... + +def midi_to_freq_tuned(midi: int, a4: float, temperament: list[float]) -> float: + """ +Frequency of a MIDI note in a 12-tone `temperament` (ratios from the +tonic C), anchored so that A4 (MIDI 69) sounds at `a4`. + +Rust: `audio::tuning::midi_to_freq_tuned` + """ + ... + +def circle_of_fifths(start: int, n: int) -> list[int]: + """ +Pitch classes reached by successive fifths from `start`. + +Rust: `audio::tuning::circle_of_fifths` + """ + ... + +def scale_degrees(root: int, mode: Mode) -> list[int]: + """ +Pitch classes of a scale on `root` (semitones 0-11, ascending). + +Rust: `audio::tuning::scale_degrees` + """ + ... + +def chord_tones(root: int, quality: ChordQuality) -> list[int]: + """ +Pitch classes of a chord on `root` (semitones 0-11). + +Rust: `audio::tuning::chord_tones` + """ + ... diff --git a/bindings/python/python/numeria/audio/vocoder.pyi b/bindings/python/python/numeria/audio/vocoder.pyi new file mode 100644 index 0000000..a59401a --- /dev/null +++ b/bindings/python/python/numeria/audio/vocoder.pyi @@ -0,0 +1,107 @@ +""" +Phase vocoder and related voice/spectral processors: time stretching, pitch shifting, robotization, channel and LPC vocoders, WSOLA, PSOLA, spectral morphing, cross synthesis, harmonizing, and autotune. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +class Excitation: + """ +Excitation source for the LPC vocoder. + +Rust: `audio::vocoder::Excitation` + """ + ... + +class PhaseVocoder: + """ +Classic phase vocoder with optional identity phase locking +(Laroche-Dolson). + +Rust: `audio::vocoder::PhaseVocoder` + """ + def __init__(self, n_fft: int, hop: int, fs: float) -> None: ... + def phase_lock(self, on: bool) -> None: ... + def time_stretch(self, x: list[float], ratio: float) -> list[float]: ... + def pitch_shift(self, x: list[float], semitones: float) -> list[float]: ... + def pitch_shift_formant_preserving(self, x: list[float], semitones: float) -> list[float]: ... + def freeze(self, x: list[float], at_sample: int, duration: int) -> list[float]: ... + def robotize(self, x: list[float]) -> list[float]: ... + def whisperize(self, x: list[float]) -> list[float]: ... + +def channel_vocoder(carrier: list[float], modulator: list[float], n_bands: int, fs: float) -> list[float]: + """ +Classic channel vocoder: the modulator's band envelopes are imposed on +the carrier through a log-spaced bandpass bank. + +Rust: `audio::vocoder::channel_vocoder` + """ + ... + +def lpc_vocoder(x: list[float], order: int, frame: int, hop: int, excitation: Excitation, fs: float) -> list[float]: + """ +LPC analysis/resynthesis vocoder: per-frame all-pole envelopes driven +by a synthetic excitation. + +Rust: `audio::vocoder::lpc_vocoder` + """ + ... + +def wsola_time_stretch(x: list[float], ratio: float, fs: float) -> list[float]: + """ +WSOLA time stretching: overlap-add of ~30 ms segments aligned by a +local cross-correlation search, preserving pitch. + +Rust: `audio::vocoder::wsola_time_stretch` + """ + ... + +def psola_pitch_shift(x: list[float], fs: float, f0_track: list[tuple[float, Optional[float]]], ratio: float) -> list[float]: + """ +TD-PSOLA pitch shifting driven by a pitch track (as produced by +`audio::analysis::pitch_track`). + +Rust: `audio::vocoder::psola_pitch_shift` + """ + ... + +def spectral_morph(a: list[float], b: list[float], t: float, n_fft: int, hop: int) -> list[float]: + """ +Interpolate magnitudes between two sounds (phases from `a`); +`t` in 0..1. + +Rust: `audio::vocoder::spectral_morph` + """ + ... + +def cross_synthesis(source: list[float], filter: list[float], n_fft: int, hop: int) -> list[float]: + """ +Cross synthesis: the source's phases (fine structure) with the +filter's smoothed magnitude envelope. + +Rust: `audio::vocoder::cross_synthesis` + """ + ... + +def harmonizer(x: list[float], fs: float, intervals: list[int]) -> list[float]: + """ +Mix the dry signal with pitch-shifted copies at the given semitone +intervals. + +Rust: `audio::vocoder::harmonizer` + """ + ... + +def autotune(x: list[float], fs: float, scale: list[int], strength: float) -> list[float]: + """ +Pull detected pitch toward the nearest pitch class in `scale` +(semitones 0-11); `strength` 0..1 is full correction at 1. Voiced +regions are retuned with phase-coherent PSOLA grains; unvoiced +regions pass through. + +Rust: `audio::vocoder::autotune` + """ + ... diff --git a/bindings/python/python/numeria/audio/wav.pyi b/bindings/python/python/numeria/audio/wav.pyi new file mode 100644 index 0000000..a8dcf78 --- /dev/null +++ b/bindings/python/python/numeria/audio/wav.pyi @@ -0,0 +1,107 @@ +""" +WAV (RIFF) reading and writing: PCM 8/16/24/32-bit, IEEE float 32/64-bit, and WAVE_FORMAT_EXTENSIBLE containers. Samples are normalized to −1..1 per channel. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +class WavData: + """ +Decoded audio: sample rate, channel count, and per-channel samples +in −1..1. + +Rust: `audio::wav::WavData` + """ + def __init__(self, fs: int, channels: int, samples: list[list[float]]) -> None: ... + @property + def fs(self) -> int: ... + @property + def channels(self) -> int: ... + @property + def samples(self) -> list[list[float]]: ... + +def wav_read(bytes: list[int]) -> WavData: + """ +Decode a WAV byte stream. + +Errors: +`InvalidArgument` for malformed containers or unsupported encodings. + +Rust: `audio::wav::wav_read` + """ + ... + +def wav_write(data: WavData, bits: int, float: bool) -> list[int]: + """ +Encode to WAV bytes: PCM at 8/16/24/32 bits, or IEEE float at 32/64. + +Panics: +Panics for unsupported bit depths or mismatched channel lengths. + +Rust: `audio::wav::wav_write` + """ + ... + +def wav_read_file(path: str) -> WavData: + """ +Read a WAV file from disk. + +Errors: +I/O errors from the filesystem; decode failures become +`InvalidData`. + +Rust: `audio::wav::wav_read_file` + """ + ... + +def wav_write_file(path: str, data: WavData, bits: int, float: bool) -> None: + """ +Write a WAV file to disk. + +Errors: +I/O errors from the filesystem. + +Rust: `audio::wav::wav_write_file` + """ + ... + +def wav_info(bytes: list[int]) -> tuple[int, int, int, int]: + """ +Header summary (fs, channels, bits, frames) without decoding samples. + +Errors: +`InvalidArgument` for malformed containers. + +Rust: `audio::wav::wav_info` + """ + ... + +def to_mono(d: WavData) -> list[float]: + """ +Average all channels down to one. + +Rust: `audio::wav::to_mono` + """ + ... + +def to_interleaved(d: WavData) -> list[float]: + """ +Interleave channels (frame-major). + +Rust: `audio::wav::to_interleaved` + """ + ... + +def from_interleaved(x: list[float], channels: int) -> list[list[float]]: + """ +Split an interleaved stream into per-channel vectors. + +Panics: +Panics if `channels == 0`. + +Rust: `audio::wav::from_interleaved` + """ + ... diff --git a/bindings/python/python/numeria/biophysics/__init__.pyi b/bindings/python/python/numeria/biophysics/__init__.pyi new file mode 100644 index 0000000..afc9695 --- /dev/null +++ b/bindings/python/python/numeria/biophysics/__init__.pyi @@ -0,0 +1,171 @@ +""" +Biophysics: the elementary membrane, transport and mechanics relations here, with the population-scale models in submodules. The roadmap calls this area `bio`; it lives under the existing `biophysics` module instead, so that there is one home for the subject rather than two. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from . import epidemiology, neuro, phylo, population, seq_align +from numeria.statistics.distributions import Exponential + +def nernst_potential(temperature: float, z: float, c_out: float, c_in: float) -> float: + """ +Nernst equation: E = (RT/(zF)) × ln(c_out/c_in) + +Rust: `biophysics::nernst_potential` + """ + ... + +def goldman_potential(temperature: float, pk: float, pna: float, pcl: float, k_out: float, k_in: float, na_out: float, na_in: float, cl_out: float, cl_in: float) -> float: + """ +Goldman-Hodgkin-Katz voltage equation for K⁺, Na⁺, and Cl⁻. +`Vm = (RT/F) × ln((Pk[K]o + Pna[Na]o + Pcl[Cl]i) / (Pk[K]i + Pna[Na]i + Pcl[Cl]o))` + +Rust: `biophysics::goldman_potential` + """ + ... + +def resting_membrane_potential_typical() -> float: + """ +Typical resting membrane potential for a neuron: -70 mV + +Rust: `biophysics::resting_membrane_potential_typical` + """ + ... + +def michaelis_menten(vmax: float, km: float, substrate: float) -> float: + """ +Michaelis-Menten kinetics: `v = Vmax × [S] / (Km + [S])` + +Rust: `biophysics::michaelis_menten` + """ + ... + +def michaelis_menten_inhibited(vmax: float, km: float, substrate: float, inhibitor: float, ki: float) -> float: + """ +Competitive inhibition: `v = Vmax × [S] / (Km(1 + [I]/Ki) + [S])` + +Rust: `biophysics::michaelis_menten_inhibited` + """ + ... + +def lineweaver_burk(vmax: float, km: float, substrate: float) -> tuple[float, float]: + """ +Lineweaver-Burk transform: returns `(1/[S], 1/v)` for double-reciprocal plot + +Rust: `biophysics::lineweaver_burk` + """ + ... + +def hill_equation(vmax: float, k: float, substrate: float, n: float) -> float: + """ +Hill equation for cooperative binding: `v = Vmax × [S]^n / (K^n + [S]^n)` + +Rust: `biophysics::hill_equation` + """ + ... + +def hill_coefficient_from_data(s1: float, v1: float, s2: float, v2: float, vmax: float) -> float: + """ +Derive Hill coefficient from two (substrate, velocity) data points. +n = log((v1/(Vmax-v1)) / (v2/(Vmax-v2))) / log(s1/s2) + +Rust: `biophysics::hill_coefficient_from_data` + """ + ... + +def exponential_growth(n0: float, rate: float, time: float) -> float: + """ +Exponential growth: N = N₀ × e^(rt) + +Rust: `biophysics::exponential_growth` + """ + ... + +def logistic_growth(n0: float, k: float, r: float, time: float) -> float: + """ +Logistic growth: N = K / (1 + ((K - N₀)/N₀) × e^(-rt)) + +Rust: `biophysics::logistic_growth` + """ + ... + +def doubling_time_population(rate: float) -> float: + """ +Doubling time: td = ln(2) / r + +Rust: `biophysics::doubling_time_population` + """ + ... + +def lotka_volterra_prey(prey: float, predator: float, alpha: float, beta: float) -> float: + """ +Lotka-Volterra prey rate: dx/dt = αx - βxy + +Rust: `biophysics::lotka_volterra_prey` + """ + ... + +def lotka_volterra_predator(prey: float, predator: float, delta: float, gamma: float) -> float: + """ +Lotka-Volterra predator rate: dy/dt = δxy - γy + +Rust: `biophysics::lotka_volterra_predator` + """ + ... + +def cardiac_output(stroke_volume: float, heart_rate: float) -> float: + """ +Cardiac output: CO = SV × HR (L/min when SV in L/beat and HR in bpm) + +Rust: `biophysics::cardiac_output` + """ + ... + +def mean_arterial_pressure(systolic: float, diastolic: float) -> float: + """ +Mean arterial pressure: MAP = DBP + (SBP - DBP)/3 + +Rust: `biophysics::mean_arterial_pressure` + """ + ... + +def vascular_resistance(pressure_drop: float, flow: float) -> float: + """ +Vascular resistance: R = ΔP / Q + +Rust: `biophysics::vascular_resistance` + """ + ... + +def poiseuille_blood_flow(radius: float, pressure_drop: float, viscosity: float, length: float) -> float: + """ +Poiseuille flow in a vessel: Q = πr⁴ΔP / (8μL) + +Rust: `biophysics::poiseuille_blood_flow` + """ + ... + +def sigmoid(x: float, x50: float, slope: float) -> float: + """ +Sigmoid function: f = 1 / (1 + exp(-slope × (x - x50))) + +Rust: `biophysics::sigmoid` + """ + ... + +def ld50_probit(dose: float, ld50: float, slope: float) -> float: + """ +LD50 probit model: probability = sigmoid(ln(dose), ln(ld50), slope) + +Rust: `biophysics::ld50_probit` + """ + ... + +FARADAY_BIO: float + +GAS_CONSTANT: float + +BODY_TEMP: float diff --git a/bindings/python/python/numeria/biophysics/epidemiology.pyi b/bindings/python/python/numeria/biophysics/epidemiology.pyi new file mode 100644 index 0000000..084089a --- /dev/null +++ b/bindings/python/python/numeria/biophysics/epidemiology.pyi @@ -0,0 +1,426 @@ +""" +Compartment models of epidemics, their stochastic counterparts, and the quantities estimated from case data. # Units and conventions Compartments are *fractions* of the population and sum to one, so a model is independent of the population size and the numbers can be read as probabilities. The stochastic models work in whole individuals instead, because the questions they answer -- will this outbreak die out, how long until it does -- are questions about integers and have no meaning in a continuum. Rates are per unit time in whatever unit the caller uses for `t_end`; the recovery rate `gamma` is the reciprocal of the mean infectious period, so a two-week illness with time in days is `gamma = 1/14`. # What the basic reproduction number is and is not `R0 = beta / gamma` is the expected number of secondary cases from one case in a *wholly susceptible* population. It is a property of the pathogen and the contact structure together, not of the pathogen alone, and it stops describing the epidemic the moment susceptibles are depleted -- which is what the effective reproduction number is for. Two populations with the same `R0` and different contact heterogeneity do not have the same epidemic; see `epidemic_threshold_network`, where the threshold is set by the largest eigenvalue of the contact graph rather than by any average. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.graph.core import Graph +from numeria.monte_carlo import Rng + +class EpidemicSample: + """ +One sample of a compartment trajectory: `(time, S, E, I, R)`. + +Models without an exposed class report `E = 0`, so a caller can plot any +of them the same way. + +Rust: `biophysics::epidemiology::EpidemicSample` + """ + def __init__(self, t: float, s: float, e: float, i: float, r: float) -> None: ... + def total(self) -> float: ... + @property + def t(self) -> float: ... + @property + def s(self) -> float: ... + @property + def e(self) -> float: ... + @property + def i(self) -> float: ... + @property + def r(self) -> float: ... + +def sir(beta: float, gamma: float, s0: float, i0: float, t_end: float) -> list[EpidemicSample]: + """ +The classical SIR model. + +Errors: +Returns an error for negative rates, a bad initial condition, or a +non-positive end time. + +Rust: `biophysics::epidemiology::sir` + """ + ... + +def sis(beta: float, gamma: float, s0: float, i0: float, t_end: float) -> list[EpidemicSample]: + """ +SIS: recovery returns an individual to the susceptible pool, so there is +no removed class and the disease can persist indefinitely. + +The distinction from SIR is not a detail. With no removed class the +epidemic has an *endemic equilibrium* at `1 - 1/R0` rather than burning +out, which is why the same pathogen parameters give a one-off wave in one +model and a permanent prevalence in the other. + +Errors: +Returns an error on the same conditions as `sir`. + +Rust: `biophysics::epidemiology::sis` + """ + ... + +def sirs(beta: float, gamma: float, omega: float, s0: float, i0: float, t_end: float) -> list[EpidemicSample]: + """ +SIRS: immunity wanes at rate `omega`, returning the removed to the +susceptible pool. + +Errors: +Returns an error on the same conditions as `sir`. + +Rust: `biophysics::epidemiology::sirs` + """ + ... + +def seir(beta: float, sigma: float, gamma: float, s0: float, e0: float, i0: float, t_end: float) -> list[EpidemicSample]: + """ +SEIR: an exposed class that is infected but not yet infectious, entered +at the infection rate and left at rate `sigma`. + +The latent period does not change the final size at all -- that depends +on `R0` alone -- but it slows the *growth rate*, which is what makes two +pathogens with the same `R0` and different incubation periods look so +different in the first month. + +Errors: +Returns an error on the same conditions as `sir`. + +Rust: `biophysics::epidemiology::seir` + """ + ... + +def seirs(beta: float, sigma: float, gamma: float, omega: float, s0: float, e0: float, i0: float, t_end: float) -> list[EpidemicSample]: + """ +SEIRS: SEIR with waning immunity. + +Errors: +Returns an error on the same conditions as `sir`. + +Rust: `biophysics::epidemiology::seirs` + """ + ... + +def msir(beta: float, gamma: float, delta: float, m0: float, s0: float, i0: float, t_end: float) -> list[tuple[float, float, float, float, float]]: + """ +MSIR: an additional class of infants protected by maternal antibodies, +which are lost at rate `delta`. + +Returns `(time, M, S, I, R)`. The maternal class is why measles +vaccination is not given at birth: the antibodies that protect the infant +also neutralise the vaccine. + +Errors: +Returns an error on the same conditions as `sir`. + +Rust: `biophysics::epidemiology::msir` + """ + ... + +def r0_sir(beta: float, gamma: float) -> float: + """ +`R0 = beta / gamma` for the SIR model. + +Errors: +Returns an error for a non-positive recovery rate, for which the +infectious period is unbounded and `R0` is not defined. + +Rust: `biophysics::epidemiology::r0_sir` + """ + ... + +def herd_immunity_threshold(r0: float) -> float: + """ +The herd immunity threshold `1 - 1/R0`: the immune fraction at which the +effective reproduction number falls to one. + +This is the threshold for the epidemic to stop *growing*, not the +fraction that ends up infected. An epidemic that reaches the threshold +keeps going and overshoots it, because the people already infectious at +that moment go on to infect others; see `final_size_equation`, whose +answer is always larger. + +Errors: +Returns an error for `R0` below one, where no immunity is needed. + +Rust: `biophysics::epidemiology::herd_immunity_threshold` + """ + ... + +def final_size_equation(r0: float) -> float: + """ +The final size of an epidemic: the fraction ever infected, from the +implicit relation `1 - z = exp(-R0 z)`. + +Solved by bisection, which is unconditionally safe here because +`f(z) = 1 - z - exp(-R0 z)` vanishes at zero, is *positive* just above it +for every `R0 > 1` -- its slope there is `R0 - 1` -- and is `-exp(-R0)` +at one. So the sought root is bracketed with `f` positive at the low end +and negative at the high end, which is the opposite of the usual +arrangement and the easy thing to get backwards. Newton's method on the +same equation converges too, but from a poor start it can step outside +`[0, 1]`, where the epidemic fraction has no meaning. + +Errors: +Returns an error for a negative `R0`. + +Rust: `biophysics::epidemiology::final_size_equation` + """ + ... + +def extinction_probability_epidemic(r0: float, i0: int) -> float: + """ +The probability that an introduction of `i0` infectious individuals dies +out rather than becoming an epidemic. + +From the branching-process approximation, valid while susceptibles are +undepleted: each case's offspring are geometric with mean `R0`, the +extinction probability of one chain is `1/R0`, and independent chains +multiply. So even a pathogen with `R0 = 3` fails to establish about a +third of the time from a single case -- epidemics are rarer than their +reproduction numbers suggest, and the ones that happen are the survivors +of many that did not. + +Errors: +Returns an error for a negative `R0` or no introductions. + +Rust: `biophysics::epidemiology::extinction_probability_epidemic` + """ + ... + +def epidemic_threshold_network(g: Graph) -> float: + """ +The epidemic threshold of a contact network: the reciprocal of the +largest eigenvalue of its adjacency matrix. + +A disease spreads on the network when `beta / gamma` exceeds this. The +mean degree is *not* the right quantity: a network with a few very +highly connected nodes has a spectral radius far above its mean degree, +and its epidemic threshold is correspondingly lower. That is why a +scale-free contact structure sustains an epidemic that a homogeneous +network with the same average contact rate would not. + +Errors: +Returns an error for an empty graph or one with no edges, whose spectral +radius is zero and whose threshold is unbounded. + +Rust: `biophysics::epidemiology::epidemic_threshold_network` + """ + ... + +def sir_with_vaccination(beta: float, gamma: float, coverage: float, i0: float, t_end: float) -> list[EpidemicSample]: + """ +SIR with a fraction `coverage` vaccinated before the epidemic begins. + +Vaccination moves people straight from susceptible to removed, so it acts +exactly like a reduced initial susceptible fraction -- which is why the +effect of a vaccination campaign on the final size is entirely captured +by `R0 (1 - coverage)`, and why the threshold coverage is the herd +immunity threshold. + +Errors: +Returns an error for a coverage outside zero to one, or on the same +conditions as `sir`. + +Rust: `biophysics::epidemiology::sir_with_vaccination` + """ + ... + +def sir_with_demography(beta: float, gamma: float, mu: float, s0: float, i0: float, t_end: float) -> list[EpidemicSample]: + """ +SIR with births and deaths at rate `mu`, both at the same rate so the +population is constant. + +Demography is what turns a one-off epidemic into an endemic disease: the +birth of new susceptibles replenishes the fuel, and the trajectory spirals +into an equilibrium at `S* = 1/R0` rather than burning out. The damped +oscillation on the way there is the source of the multi-year cycles seen +in measles before vaccination. + +Errors: +Returns an error for a negative rate, or on the same conditions as +`sir`. + +Rust: `biophysics::epidemiology::sir_with_demography` + """ + ... + +def two_strain(beta1: float, gamma1: float, beta2: float, gamma2: float, s0: float, i1: float, i2: float, t_end: float) -> list[tuple[float, float, float, float, float]]: + """ +Two strains competing for the same susceptible pool, with complete +cross-immunity. + +Returns `(time, S, I1, I2, R)`. Both strains end at zero: they compete +for one susceptible pool and this model does not replenish it, so the +epidemic ends when the susceptibles do. + +Which strain infects more is *not* settled by `R0` alone. Competitive +exclusion -- the fitter strain driving the other out however far behind +it starts -- is a statement about a system with susceptible +replenishment, where there is an indefinite future to be excluded from. +Here the race is finite, and a strain with a thousandfold head start can +out-infect a rival with nearly twice its reproduction number before the +susceptibles are gone. From equal starts the fitter strain does win. + +Errors: +Returns an error for negative rates or a bad initial condition. + +Rust: `biophysics::epidemiology::two_strain` + """ + ... + +def age_structured(contact: list[list[float]], sizes: list[float], gamma: float, i0: list[float], t_end: float) -> list[tuple[float, list[float], list[float], list[float]]]: + """ +An age-structured SIR with a contact matrix. + +`contact[i][j]` is the rate at which a member of group `i` is contacted +by a member of group `j`, and `sizes` gives each group's share of the +population. Returns the trajectory as `(time, S, I, R)` with one entry +per group. + +Structure changes the threshold, not just the detail. `R0` is the largest +eigenvalue of the next-generation matrix, not the average contact rate +times the infectious period, and the two differ whenever contact is +assortative -- which it always is by age. + +Errors: +Returns an error for a non-square or negative contact matrix, group sizes +that do not sum to one, or a bad initial condition. + +Rust: `biophysics::epidemiology::age_structured` + """ + ... + +def r0_age_structured(contact: list[list[float]], sizes: list[float], gamma: float) -> float: + """ +`R0` for an age-structured model: the largest eigenvalue of the +next-generation matrix `K[a][b] = contact[a][b] * sizes[a] / (gamma * +sizes[b])`. + +Errors: +Returns an error on the same conditions as `age_structured`, or for a +non-positive recovery rate. + +Rust: `biophysics::epidemiology::r0_age_structured` + """ + ... + +def sir_stochastic_gillespie(beta: float, gamma: float, n: int, i0: int, t_end: float, rng: Rng) -> list[tuple[float, int, int, int]]: + """ +An exact stochastic SIR by Gillespie's direct method, in whole +individuals. + +Returns `(time, S, I, R)` after each event. The deterministic model +cannot answer the question this one is for: with `R0 > 1` the +deterministic epidemic always takes off, while the stochastic one dies +out with probability `(1/R0)^i0` -- and that difference is not a +correction, it is the whole behaviour at small numbers. + +Errors: +Returns an error for negative rates, an empty population, or a +non-positive end time. + +Rust: `biophysics::epidemiology::sir_stochastic_gillespie` + """ + ... + +def network_sir(g: Graph, beta: float, gamma: float, patient_zero: int, rng: Rng) -> list[tuple[int, int, int]]: + """ +An SIR epidemic on a contact network. + +Each infectious node infects each susceptible neighbour at rate `beta` +and recovers at rate `gamma`. Returns the `(S, I, R)` counts after each +event. Unlike the well-mixed model the epidemic here is limited by the +*local* structure: a node cannot reinfect its own neighbourhood, so the +final size is smaller than the well-mixed prediction at the same `R0`. + +Errors: +Returns an error for negative rates, an empty graph, or a patient zero +outside it. + +Rust: `biophysics::epidemiology::network_sir` + """ + ... + +def effective_r_estimate(incidence: list[float], serial_interval: list[float], window: int) -> list[float]: + """ +The effective reproduction number over time, by the Cori method. + +`R_t` is the ratio of today's incidence to the total infectiousness +present, where the latter is past incidence weighted by the serial +interval distribution. Returns one estimate per day from `window` +onward, and `NaN` before that -- there is no data yet, and reporting a +number there would be worse than reporting nothing. + +The distinction from a naive ratio of consecutive counts matters: that +ratio is a *growth rate*, and converting it to a reproduction number +requires knowing the generation time. Two epidemics doubling at the same +speed have very different `R_t` if one has a serial interval of three +days and the other of ten. + +Errors: +Returns an error for a negative incidence, a serial interval that is not +a distribution, or a window longer than the record. + +Rust: `biophysics::epidemiology::effective_r_estimate` + """ + ... + +def serial_interval_fit(intervals: list[float]) -> tuple[float, float]: + """ +Fits a gamma distribution to observed serial intervals by the method of +moments, returning `(shape, scale)`. + +Errors: +Returns an error for fewer than two observations, a non-positive +interval, or observations with no spread. + +Rust: `biophysics::epidemiology::serial_interval_fit` + """ + ... + +def seir_fit_to_incidence(incidence: list[float], dt: float, population: float, guess: tuple[float, float, float]) -> tuple[float, float, float]: + """ +Fits `(beta, sigma, gamma)` of an SEIR model to an incidence series by +Nelder-Mead on the sum of squared errors. + +Fitting three rates to one incidence curve is close to the edge of what +the data supports: the growth rate constrains a *combination* of `beta` +and `sigma`, so the two trade off against each other along a valley in +the objective and are only weakly separated by the shape of the peak. +The returned fit reproduces the curve; it should not be read as three +independently identified parameters. + +The initial infectious fraction is taken from the first observation +rather than estimated, so if that first point is noisy or the epidemic +was already under way when reporting began, the resulting time offset +appears as a residual that no choice of rates can remove. Fitting it as a +fourth parameter would trade that bias for a worse identifiability +problem than the one already described. + +Errors: +Returns an error for fewer than five points, a negative incidence, or a +non-positive population. + +Rust: `biophysics::epidemiology::seir_fit_to_incidence` + """ + ... + +def wallinga_teunis(incidence: list[float], serial_interval: list[float]) -> list[float]: + """ +The Wallinga-Teunis case reproduction number. + +Where the Cori method asks "how many people is each *current* case +infecting", this asks "how many did each *past* case go on to infect", +by assigning each case's infector probabilistically among the earlier +cases in proportion to the serial interval. The two answer different +questions and disagree near the end of a record, where Wallinga-Teunis +is biased down because the infections have not happened yet. + +Errors: +Returns an error for a negative incidence or a serial interval that is +not a distribution. + +Rust: `biophysics::epidemiology::wallinga_teunis` + """ + ... diff --git a/bindings/python/python/numeria/biophysics/neuro.pyi b/bindings/python/python/numeria/biophysics/neuro.pyi new file mode 100644 index 0000000..6449d9d --- /dev/null +++ b/bindings/python/python/numeria/biophysics/neuro.pyi @@ -0,0 +1,724 @@ +""" +Computational neuroscience: single neurons, spike trains, synapses and the small networks built from them. # Units The conductance-based models use the squid axon's units throughout: millivolts, milliseconds, microfarads and microamps per square centimetre, and millisiemens per square centimetre. A rate is therefore a count per millisecond unless a function says otherwise, and the spike frequencies reported by the F-I curves are converted to hertz where that is the useful number. The reduced models -- FitzHugh-Nagumo and the drift-diffusion process -- carry no units at all. # What a spike is here Every model that fires does so by one of two mechanisms, and the difference decides what can be asked of it. Hodgkin-Huxley, Morris-Lecar and FitzHugh-Nagumo generate the spike from their own dynamics: the upstroke is a solution of the equations and the threshold is not a parameter but an emergent property of the vector field. The integrate-and-fire family -- LIF, Izhikevich, AdEx -- *stipulates* the spike: the equations describe only the approach, and a rule replaces the voltage when it crosses a number. The second kind is far cheaper and reproduces firing statistics well; it has no answer to questions about the spike's shape, because the shape was never computed. Spikes are detected in a trace by an upward crossing of a fixed level, which is the right test for a model whose spikes are tall and brief. # Equilibrium potentials `biophysics::nernst_potential` and `biophysics::goldman_potential` already provide the reversal potentials these models take as constants, and are not repeated here. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.statistical_mechanics.kinetics import Inhibition +from numeria.linalg.matrix import Matrix +from numeria.statistics.distributions import Poisson +from numeria.monte_carlo import Rng + +class MorrisLecar: + """ +Morris-Lecar's parameters, in the squid axon's units. + +Rust: `biophysics::neuro::MorrisLecar` + """ + def __init__(self, c_m: float, g_l: float, g_ca: float, g_k: float, v_l: float, v_ca: float, v_k: float, v1: float, v2: float, v3: float, v4: float, phi: float) -> None: ... + @staticmethod + def hopf() -> MorrisLecar: ... + @staticmethod + def saddle_node() -> MorrisLecar: ... + @property + def c_m(self) -> float: ... + @property + def g_l(self) -> float: ... + @property + def g_ca(self) -> float: ... + @property + def g_k(self) -> float: ... + @property + def v_l(self) -> float: ... + @property + def v_ca(self) -> float: ... + @property + def v_k(self) -> float: ... + @property + def v1(self) -> float: ... + @property + def v2(self) -> float: ... + @property + def v3(self) -> float: ... + @property + def v4(self) -> float: ... + @property + def phi(self) -> float: ... + +def spike_times(trace: list[tuple[float, float]], level: float) -> list[float]: + """ +The times at which a voltage trace crosses `level` upward, interpolated +between samples. + +The level is the caller's because the models here peak at very +different voltages: Hodgkin-Huxley and Izhikevich overshoot well past +zero, while `adex` tops out at `v_t + 10 * slope`, which is usually +still negative. A detector fixed at zero would report that an AdEx +neuron never fires. + +Rust: `biophysics::neuro::spike_times` + """ + ... + +def hh_steady_state(v: float) -> tuple[float, float, float]: + """ +The steady-state gating variables at a holding potential. + +A gate settles at `alpha / (alpha + beta)`; starting a run anywhere +else adds a transient that has nothing to do with the stimulus. + +Rust: `biophysics::neuro::hh_steady_state` + """ + ... + +def hodgkin_huxley(i_ext: Callable[[float], float], t_end: float, dt: float) -> list[tuple[float, float, float, float, float]]: + """ +The Hodgkin-Huxley membrane, integrated with fixed-step RK4. + +Returns `(t, V, m, h, n)` per step. The run starts from the gating +variables' steady state at `HH_V_REST`, so an unstimulated axon stays +where it is instead of relaxing through a spurious transient. + +The action potential is not built in. Sodium activation `m` is fast and +its cube makes the inward current explosive; inactivation `h` and +potassium activation `n` are ten times slower and end it. That +separation of timescales is the whole mechanism, and it is why the +threshold is a property of the trajectory rather than a parameter. + +A strongly *hyperpolarising* current is the one thing this integrator +cannot take. Below about -25 uA/cm^2 the voltage falls far enough that +`beta_m`, which grows exponentially as the membrane hyperpolarises, +reaches thousands per millisecond and a fixed step of 0.01 ms is no +longer stable. That is reported as a breakdown rather than returned as +a trace full of nonsense. Depolarising currents have no such limit: +hundreds of uA/cm^2 integrate cleanly, and simply drive the model into +depolarisation block. + +Errors: +Returns an error for a non-positive `t_end`, a `dt` outside `(0, 0.05]` +-- above which fixed-step RK4 loses the upstroke -- a `dt` that is not +smaller than `t_end`, or an integration that diverges. + +Rust: `biophysics::neuro::hodgkin_huxley` + """ + ... + +def hh_spike_times(trace: list[tuple[float, float, float, float, float]]) -> list[float]: + """ +The spike times in a Hodgkin-Huxley trace. + +Rust: `biophysics::neuro::hh_spike_times` + """ + ... + +def hh_spike_threshold_estimate() -> float: + """ +The smallest sustained current, in uA/cm^2, that makes the model fire. + +Found by bisection on "does a 120 ms step produce a spike". This is the +rheobase, and it is not the same thing as a voltage threshold: a brief +pulse well above this current can fail to fire, and the model has no +single voltage at which firing becomes inevitable. + +Rust: `biophysics::neuro::hh_spike_threshold_estimate` + """ + ... + +def hh_fi_curve(currents: list[float]) -> list[tuple[float, float]]: + """ +The firing rate in hertz against sustained current, for each current in +`currents`. + +Hodgkin-Huxley's F-I curve is discontinuous: at the rheobase the rate +jumps to about 50 Hz rather than rising from zero, because the +oscillation is born through a subcritical Hopf bifurcation with a +finite frequency. A neuron whose rate can be tuned smoothly to +arbitrarily low values -- a type I neuron -- needs a different +bifurcation, which `morris_lecar` can be parameterised to show. + +The first 30 ms of each run are discarded so the onset transient does +not enter the rate. + +Errors: +Returns an error if `currents` is empty or holds a value that is not +finite. + +Rust: `biophysics::neuro::hh_fi_curve` + """ + ... + +def fitzhugh_nagumo_neuron(a: float, b: float, tau: float, current: float, v0: float, w0: float, t_end: float, dt: float) -> list[tuple[float, float, float]]: + """ +FitzHugh-Nagumo, the two-variable caricature of an excitable membrane. + +`dv/dt = v - v^3/3 - w + I`, `dw/dt = (v + a - b w) / tau`. Returns +`(t, v, w)` per step, dimensionless throughout. + +The point of the reduction is that two variables can be drawn: the +cubic `v` nullcline and the straight `w` nullcline cross at a fixed +point, and whether that crossing sits on the cubic's middle branch +decides whether the neuron rests or oscillates. Excitability -- a small +push decaying, a slightly larger one taking a long excursion -- is +visible in the phase plane in a way it is not in four dimensions. + +Errors: +Returns an error for a non-positive `tau`, or a run length or step size +out of range. + +Rust: `biophysics::neuro::fitzhugh_nagumo_neuron` + """ + ... + +def morris_lecar(params: MorrisLecar, current: float, v0: float, w0: float, t_end: float, dt: float) -> list[tuple[float, float, float]]: + """ +Morris-Lecar, a calcium-potassium membrane with one gating variable. + +Returns `(t, V, w)` per step. The calcium current is instantaneous, +which is what removes the second gate: only potassium activation `w` +has its own equation. + +Errors: +Returns an error for a non-positive capacitance or slope, or a run +length or step size out of range. + +Rust: `biophysics::neuro::morris_lecar` + """ + ... + +def izhikevich(a: float, b: float, c: float, d: float, current: float, t_end: float, dt: float) -> list[tuple[float, float]]: + """ +Izhikevich's two-variable spiking model. + +`v' = 0.04 v^2 + 5 v + 140 - u + I` and `u' = a (b v - u)`, with the +reset `v <- c`, `u <- u + d` once `v` reaches 30 mV. Returns `(t, v)` +per step, with the spike sample set to the 30 mV peak so a trace can be +plotted without the reset looking like a downstroke. + +The quadratic term is what makes it a spike generator rather than a +leaky integrator: above the unstable fixed point `v` runs away in finite +time, and the reset is what stops it. Two parameters then buy most of +the qualitative variety real neurons show -- see +`izhikevich_presets`. + +The published implementation advances `v` in two half-steps for +stability, and that is what is done here; `dt` is the reporting step. + +Errors: +Returns an error for a non-positive `a`, or a run length or step size +out of range. + +Rust: `biophysics::neuro::izhikevich` + """ + ... + +def izhikevich_presets() -> list[tuple[str, list[float]]]: + """ +The five firing patterns Izhikevich's paper names, as `(a, b, c, d)`. + +Regular spiking, intrinsically bursting, chattering, fast spiking and +low-threshold spiking. `c` and `d` set what happens after a spike, so +they are what separates a regular spiker from a burster; `a` and `b` +set the recovery variable's speed and its coupling to voltage. + +Rust: `biophysics::neuro::izhikevich_presets` + """ + ... + +def adex(c_m: float, g_l: float, e_l: float, slope: float, v_t: float, tau_w: float, a: float, b: float, v_reset: float, current: float, t_end: float, dt: float) -> list[tuple[float, float, float]]: + """ +The adaptive exponential integrate-and-fire neuron. + +`C dV/dt = -g_L (V - E_L) + g_L dt_slope exp((V - v_t)/dt_slope) - w + I` +with `tau_w dw/dt = a (V - E_L) - w`, and the reset `V <- v_reset`, +`w <- w + b` at the peak. Returns `(t, V, w)` per step. + +The exponential term is fitted to the sodium activation curve, so the +upstroke's *shape* near threshold is right even though the spike itself +is still stipulated. The adaptation current `w` is what the leaky +integrator lacks: it accumulates over a spike train and slows it, which +is the commonest firing pattern in cortex and cannot be produced by a +model with one variable. + +The recorded spike sample sits at the peak `v_t + 10 * slope`, which is +where `spike_times` should be pointed to count them. + +Errors: +Returns an error for a non-positive capacitance, conductance, slope or +adaptation time constant, or a run length or step size out of range. + +Rust: `biophysics::neuro::adex` + """ + ... + +def lif_neuron(current: float, tau: float, v_th: float, v_reset: float, refractory: float, noise: float, t_end: float, dt: float, rng: Rng) -> list[float]: + """ +The leaky integrate-and-fire neuron's spike times. + +`tau dV/dt = -(V - V_rest) + R I`, with a spike and a reset to +`v_reset` whenever `V` reaches `v_th`, and an absolute refractory +period during which the voltage is clamped. Gaussian current noise of +standard deviation `noise` is added per unit time, scaled so the +result does not depend on `dt`. + +The voltage between spikes carries no information the times do not, so +only the times are returned. + +Errors: +Returns an error for a non-positive `tau`, a negative refractory period +or noise, a threshold at or below the reset, or a run length or step +size out of range. + +Rust: `biophysics::neuro::lif_neuron` + """ + ... + +def lif_fi_exact(current: float, tau: float, v_th: float, v_reset: float, refractory: float) -> float: + """ +The leaky integrate-and-fire firing rate in the noiseless case, exactly. + +`1 / (t_ref + tau ln((I - V_reset)/(I - V_th)))` for a current above +threshold, and zero otherwise. The rest potential is taken as zero, so +`I` is measured in the same units as the voltages. + +The logarithm is what makes the curve saturate: doubling a large +current barely changes the rate, because the refractory period comes to +dominate. Below `v_th` the neuron never fires however long you wait -- +the exact zero, not a very small number. + +Errors: +Returns an error for a non-positive `tau`, a negative refractory +period, or a threshold at or below the reset. + +Rust: `biophysics::neuro::lif_fi_exact` + """ + ... + +def interspike_intervals(spikes: list[float]) -> list[float]: + """ +The gaps between successive spikes. + +Errors: +Returns an error if the times are not sorted, since an unsorted train +would silently produce negative intervals. + +Rust: `biophysics::neuro::interspike_intervals` + """ + ... + +def cv_isi(spikes: list[float]) -> float: + """ +The coefficient of variation of the interspike intervals. + +One for a Poisson process, because an exponential distribution's +standard deviation equals its mean; near zero for a regular pacemaker; +and above one for a bursting cell, whose intervals come in two very +different sizes. It is a measure of *irregularity*, not of rate: it is +unchanged by running the clock faster. + +Errors: +Returns an error for fewer than three spikes, an unsorted train, or a +mean interval of zero. + +Rust: `biophysics::neuro::cv_isi` + """ + ... + +def fano_factor(counts: list[int]) -> float: + """ +The Fano factor of a set of counts: variance over mean. + +One for a Poisson process. Unlike `cv_isi` this is measured over a +window, so the two can disagree: a train with regular intervals but a +drifting rate has a low CV and a high Fano factor, because the +irregularity is between windows rather than within them. + +Errors: +Returns an error for fewer than two counts or a mean of zero. + +Rust: `biophysics::neuro::fano_factor` + """ + ... + +def poisson_spike_train(rate: float, t_end: float, rng: Rng) -> list[float]: + """ +A homogeneous Poisson spike train on `[0, t_end)`. + +Generated by accumulating exponential waiting times, which is exact -- +there is no time step and so no chance of two spikes in one bin. + +Errors: +Returns an error for a non-positive rate or run length, or an expected +count above ten million. + +Rust: `biophysics::neuro::poisson_spike_train` + """ + ... + +def psth(trains: list[list[float]], bin: float, t_end: float) -> list[float]: + """ +The peri-stimulus time histogram: the mean firing rate in each bin, +across trials. + +Dividing by the bin width and the trial count is what makes this a +rate rather than a count, and is what lets histograms with different +binnings be compared. The bin width is the whole choice in a PSTH: too +wide and a transient response is smeared into the background, too +narrow and every bin is zero or one. + +Errors: +Returns an error for no trials, a non-positive bin width or window, or +a spike time outside `[0, t_end)`. + +Rust: `biophysics::neuro::psth` + """ + ... + +def raster_data(trains: list[list[float]]) -> list[tuple[float, int]]: + """ +Every spike as a `(time, trial)` pair, sorted by time. + +The raster is the raw data a PSTH averages away, and the two answer +different questions: a response present on every trial and one present +on half the trials at twice the rate give the same histogram. + +Rust: `biophysics::neuro::raster_data` + """ + ... + +def spike_triggered_average(stimulus: list[float], dt: float, spikes: list[float], window: int) -> list[float]: + """ +The spike-triggered average: the mean stimulus in the `window` samples +before a spike. + +Returned oldest sample first, so the last entry is the stimulus at the +spike itself. Spikes too early for a full window are skipped, and the +count of those that contributed decides the divisor. + +This estimates the neuron's linear filter only if the stimulus is white: +any correlation in the stimulus appears in the average and will be +mistaken for structure in the neuron. The usual remedy is to whiten by +the stimulus autocorrelation, which is a different calculation from +this one. + +Errors: +Returns an error for an empty stimulus, a non-positive sampling step, a +zero window, a window longer than the stimulus, or no usable spike. + +Rust: `biophysics::neuro::spike_triggered_average` + """ + ... + +def tuning_curve_fit_von_mises(angles: list[float], rates: list[float]) -> tuple[float, float, float]: + """ +Fits `r(theta) = amplitude * exp(kappa * cos(theta - preferred))` to a +set of angles and rates, returning `(preferred, kappa, amplitude)`. + +Taking logarithms turns the von Mises form into +`ln r = ln A + (kappa cos mu) cos theta + (kappa sin mu) sin theta`, +which is linear in three coefficients and so is solved exactly rather +than searched for. `preferred` comes back in `(-pi, pi]`. + +The price of the linearisation is that it fits the log rate, so it +weights a doubling at a low rate as heavily as a doubling at the peak. +With noiseless data that costs nothing and the fit is exact; with noisy +data it biases toward the flanks. + +Errors: +Returns an error for fewer than three points, mismatched lengths, a +non-positive rate, or angles that do not determine the fit -- all equal, +or spread over too little of the circle. + +Rust: `biophysics::neuro::tuning_curve_fit_von_mises` + """ + ... + +def synapse_exp(g_max: float, tau: float, spikes: list[float], t: float) -> float: + """ +The conductance of an exponential synapse at time `t`, given the +presynaptic spike times. + +Each spike adds `g_max` instantaneously and it decays as +`exp(-(t - t_spike)/tau)`. Conductances sum, so a burst arriving within +a time constant produces more than one spike's worth -- which is what +makes a synapse a low-pass filter of its input rather than a repeater. + +Errors: +Returns an error for a non-positive `tau` or an unsorted spike train. + +Rust: `biophysics::neuro::synapse_exp` + """ + ... + +def alpha_synapse(g_max: float, tau: float, spikes: list[float], t: float) -> float: + """ +The conductance of an alpha synapse at time `t`. + +`g_max * x * exp(1 - x)` with `x = (t - t_spike)/tau`, which peaks at +exactly `g_max` one time constant after the spike. The rise is what +distinguishes it from `synapse_exp`: a real conductance cannot jump, +and the delay to peak matters when the question is whether two inputs +coincide. + +Errors: +Returns an error for a non-positive `tau` or an unsorted spike train. + +Rust: `biophysics::neuro::alpha_synapse` + """ + ... + +def stdp_window(delta: float, a_plus: float, a_minus: float, tau_plus: float, tau_minus: float) -> float: + """ +The spike-timing-dependent plasticity window: the weight change for a +post-minus-pre interval of `delta`. + +Positive `delta` -- the postsynaptic spike came second -- potentiates by +`a_plus exp(-delta/tau_plus)`; negative depresses by +`-a_minus exp(delta/tau_minus)`. Exactly simultaneous spikes give zero, +which is the discontinuity at the origin the rule is known for: a +millisecond either way is the difference between strengthening and +weakening. + +Errors: +Returns an error for a non-positive time constant or a negative +amplitude. + +Rust: `biophysics::neuro::stdp_window` + """ + ... + +def stdp_train(pre: list[float], post: list[float], a_plus: float, a_minus: float, tau_plus: float, tau_minus: float) -> float: + """ +The total weight change from every pre-post pair in two trains. + +This is the all-to-all rule: each presynaptic spike is paired with each +postsynaptic spike. It is the simplest interpretation and not the only +one -- nearest-neighbour pairing gives noticeably less potentiation at +high rates, because a burst's later spikes no longer each count against +every earlier one. + +Errors: +Returns an error for a bad window parameter, an unsorted train, or more +than ten million pairs. + +Rust: `biophysics::neuro::stdp_train` + """ + ... + +def izhikevich_network(n_exc: int, n_inh: int, t_end: float, rng: Rng) -> list[tuple[float, int]]: + """ +Izhikevich's randomly connected network of excitatory and inhibitory +neurons, returning every spike as `(time in ms, neuron index)`. + +Excitatory neurons are regular spikers scattered toward chattering by a +squared random factor, inhibitory ones toward fast spiking, exactly as +in the published network; each neuron receives a random thalamic drive +each millisecond, with the excitatory population driven harder. All +weights are all-to-all with random excitatory strengths and stronger +fixed inhibitory ones. + +The behaviour worth looking for is that the population synchronises +into gamma-band rhythms without any oscillator being built in: the +rhythm is a property of the excitatory-inhibitory loop, not of the +cells. Inhibition being both stronger and faster than excitation is +what produces it. + +Errors: +Returns an error for no excitatory or no inhibitory neurons, more than +four thousand in total, or a non-positive run length. + +Rust: `biophysics::neuro::izhikevich_network` + """ + ... + +def hopfield_store(patterns: list[list[int]]) -> Matrix: + """ +The Hebbian weight matrix storing a set of +-1 patterns. + +`w_ij = (1/n) sum_p x_i^p x_j^p` with a zero diagonal. The rule is +local and one-shot: each pattern is written by a single pass and never +revisited, which is why the network cannot unlearn and why capacity is +the limiting resource rather than training time. + +Errors: +Returns an error for no patterns, patterns of differing or zero length, +or an entry that is not exactly +1 or -1. + +Rust: `biophysics::neuro::hopfield_store` + """ + ... + +def hopfield_recall(w: Matrix | Sequence[Sequence[float]], probe: list[int], steps: int) -> list[int]: + """ +Recalls from a probe by sweeping the units in index order, stopping +early once a whole sweep changes nothing. `steps` counts sweeps. + +The updates are sequential rather than simultaneous, and the difference +is not cosmetic. Flipping one unit at a time against the current state +can only lower the energy `-1/2 x' W x` when the weights are symmetric +with a zero diagonal, so recall converges to a fixed point. Updating +every unit at once against the *old* state has no such guarantee: it +can raise the energy and settle into a two-cycle that oscillates +forever between two states, neither of them stored. What it converges *to* need not be a stored +pattern: mixtures of three stored patterns are also minima, and so are +the negatives of everything stored, since flipping every unit leaves +the energy unchanged. + +Errors: +Returns an error for a non-square matrix, a probe of the wrong length, +or a probe entry that is not exactly +1 or -1. + +Rust: `biophysics::neuro::hopfield_recall` + """ + ... + +def hopfield_energy(w: Matrix | Sequence[Sequence[float]], state: list[int]) -> float: + """ +The energy of a state under a Hopfield weight matrix. + +Errors: +Returns an error for a non-square matrix or a state of the wrong length. + +Rust: `biophysics::neuro::hopfield_energy` + """ + ... + +def hopfield_capacity_check(n: int, stored: int, trials: int, rng: Rng) -> float: + """ +The fraction of stored patterns recalled exactly from themselves, over +`trials` random pattern sets of size `stored`. + +Recall from the pattern itself is the easiest possible test, so this +measures storage rather than error correction. It falls off sharply +near `0.138 n` patterns: below that the stored patterns are stable, and +above it the crosstalk between them overwhelms the signal and the +network forgets everything at once rather than degrading gracefully. + +Errors: +Returns an error for a network or trial count of zero, no patterns to +store, or a request above five hundred units. + +Rust: `biophysics::neuro::hopfield_capacity_check` + """ + ... + +def wilson_cowan(c_ee: float, c_ei: float, c_ie: float, c_ii: float, p_e: float, p_i: float, tau_e: float, tau_i: float, slope: float, threshold: float, e0: float, i0: float, t_end: float, dt: float) -> list[tuple[float, float, float]]: + """ +The Wilson-Cowan equations for coupled excitatory and inhibitory +populations, returning `(t, E, I)`. + +`tau_e dE/dt = -E + S(c_ee E - c_ei I + p_e)` and the matching +equation for `I`, with `S` the logistic function. `E` and `I` are +fractions of each population active, so they stay in `[0, 1]`. + +This is a mean-field model: it describes what a population does on +average and says nothing about individual spikes or their timing. +Oscillations here are oscillations of the *rate*, which is a different +claim from the synchrony a spiking network shows, and the two need not +coincide. + +Errors: +Returns an error for a non-positive time constant or slope, initial +activity outside `[0, 1]`, or a run length or step size out of range. + +Rust: `biophysics::neuro::wilson_cowan` + """ + ... + +def length_constant(r_m: float, r_i: float, diameter: float) -> float: + """ +The passive cable's length constant `sqrt(d R_m / (4 R_i))`. + +With `r_m` in ohm-cm^2, `r_i` in ohm-cm and the diameter in cm, the +answer is in cm. The square root is the reason thin processes are +electrically short: halving the diameter shortens the reach only by +`sqrt(2)`, but that is enough that a dendritic spine's neck is a +different electrical world from its parent branch. + +Errors: +Returns an error for a non-positive resistance or diameter. + +Rust: `biophysics::neuro::length_constant` + """ + ... + +def cable_equation_1d(length: float, lambda_: float, v_injected: float, points: int) -> list[float]: + """ +The steady-state voltage along a finite passive cable with current +injected at one end and the far end sealed. + +Returns `points` samples of `V(x)` over `[0, length]`, solved from the +discretised cable equation `lambda^2 V'' = V` rather than from the +closed form, so the boundary conditions are imposed rather than +assumed. The analytic answer for a sealed end is +`V(x) = V(0) cosh((L - x)/lambda) / cosh(L/lambda)`. + +A sealed end is not a neutral choice. Current that reaches it has +nowhere to go, so the voltage there is *higher* than an infinite cable +would give -- an end effect that grows as the cable shortens relative +to its length constant. + +Errors: +Returns an error for a non-positive length or length constant, fewer +than three points, or a singular system. + +Rust: `biophysics::neuro::cable_equation_1d` + """ + ... + +def reaction_time_ddm(drift: float, threshold: float, noise: float, dt: float, trials: int, rng: Rng) -> list[tuple[float, bool]]: + """ +Simulated reaction times from the drift-diffusion model, as +`(time, chose the positive bound)`. + +Evidence accumulates from zero with constant `drift` and Gaussian noise +until it reaches `+threshold` or `-threshold`. The model's appeal is +that one mechanism produces both the choice and its latency, and it +predicts the awkward fact that errors and correct responses have +nearly the same distribution of times when the starting point is +unbiased. + +Errors: +Returns an error for a non-positive threshold, noise or step, no +trials, or a run that exhausts the fifty-million-step budget shared +across all trials. + +Rust: `biophysics::neuro::reaction_time_ddm` + """ + ... + +def ddm_analytic_accuracy(drift: float, threshold: float, noise: float) -> float: + """ +The exact probability that unbiased evidence reaches the positive +bound: `1 / (1 + exp(-2 * drift * threshold / noise^2))`. + +This is the gambler's-ruin answer for Brownian motion with drift +between symmetric absorbing barriers, and it depends on the three +parameters only through `drift * threshold / noise^2`. Doubling the +drift and the noise variance together therefore changes the accuracy +not at all, only the time taken. + +Errors: +Returns an error for a non-positive threshold or noise. + +Rust: `biophysics::neuro::ddm_analytic_accuracy` + """ + ... + +HH_C_M: float + +HH_G_NA: float + +HH_G_K: float + +HH_G_L: float + +HH_E_NA: float + +HH_E_K: float + +HH_E_L: float + +HH_V_REST: float diff --git a/bindings/python/python/numeria/biophysics/phylo.pyi b/bindings/python/python/numeria/biophysics/phylo.pyi new file mode 100644 index 0000000..1550b30 --- /dev/null +++ b/bindings/python/python/numeria/biophysics/phylo.pyi @@ -0,0 +1,293 @@ +""" +Phylogenetics: trees, the distance and character methods that build them, and the statistics read off them. # What a tree is here `PhyloTree` stores a parent index and a branch length per node, with leaves first and internal nodes after. That representation makes the two operations everything else needs -- walking to the root, and finding a common ancestor -- direct, at the cost of making "children of" a search. Trees in this module are rooted; an unrooted method such as neighbour joining produces a tree whose root is an artefact of the construction and carries no meaning, which is noted where it matters. # Distances are not times A branch length is a number of substitutions per site, not an elapsed time, and converting between them needs a rate that no method here estimates. UPGMA is the exception and it is an *assumption* rather than an inference: it produces an ultrametric tree, in which every leaf is equidistant from the root, which is true only under a strict molecular clock. Neighbour joining makes no such assumption, and the difference shows immediately on data where rates vary between lineages. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.linalg.matrix import Matrix +from numeria.monte_carlo import Rng + +class DistanceMethod: + """ +Which distance method a bootstrap replicate should use. + +Rust: `biophysics::phylo::DistanceMethod` + """ + ... + +class PhyloTree: + """ +A rooted phylogenetic tree. + +Nodes `0..leaf_count` are leaves; the rest are internal. The root is the +unique node whose parent is `None`. + +Rust: `biophysics::phylo::PhyloTree` + """ + def __init__(self, parent: list[Optional[int]], branch_length: list[float], labels: list[str]) -> None: ... + def len(self) -> int: ... + def is_empty(self) -> bool: ... + def root(self) -> int: ... + def children(self, node: int) -> list[int]: ... + def leaves(self) -> list[int]: ... + def is_binary(self) -> bool: ... + def path_to_root(self, node: int) -> list[int]: ... + def depth(self, node: int) -> float: ... + def height(self) -> float: ... + def total_length(self) -> float: ... + def mrca(self, a: int, b: int) -> int: ... + def distance(self, a: int, b: int) -> float: ... + def is_ultrametric(self, tolerance: float) -> bool: ... + def splits(self) -> list[list[str]]: ... + def bipartitions(self) -> list[list[str]]: ... + def robinson_foulds(self, other: PhyloTree) -> int: ... + def to_newick(self) -> str: ... + @staticmethod + def from_newick(text: str) -> PhyloTree: ... + @property + def parent(self) -> list[Optional[int]]: ... + @property + def branch_length(self) -> list[float]: ... + @property + def labels(self) -> list[str]: ... + +def upgma(dist: Matrix | Sequence[Sequence[float]], labels: list[str]) -> PhyloTree: + """ +UPGMA: unweighted pair group method with arithmetic mean. + +Repeatedly joins the two closest clusters and places their common +ancestor at half their distance, so every leaf ends up the same distance +from the root. That ultrametricity is *assumed*, not measured: UPGMA +returns a clocklike tree whether or not the data are clocklike, and on +data where one lineage evolves faster it will place that lineage's +long branch too close to the root -- the classic long-branch artefact. +Use `neighbor_joining` unless a clock is justified. + +The distance between merged clusters is the mean over all pairs of +members, which is what makes the merge heights non-decreasing and the +result a valid ultrametric tree. + +Errors: +Returns an error for a non-square, asymmetric, negative or non-finite +matrix, a label count that disagrees with it, or fewer than two taxa. + +Rust: `biophysics::phylo::upgma` + """ + ... + +def neighbor_joining(dist: Matrix | Sequence[Sequence[float]], labels: list[str]) -> PhyloTree: + """ +Saitou and Nei's neighbour joining. + +Joins the pair minimising `Q(i,j) = (n-2) d(i,j) - r_i - r_j`, where +`r_i` is the row sum, rather than the pair that is simply closest. The +correction is what makes the method consistent without a clock: two +taxa can be close together merely because both evolve slowly, and `Q` +discounts exactly that. Given an additive matrix the method recovers +the true tree exactly. + +The result is an **unrooted** tree returned in rooted form: the final +node has three children and is a placeholder, not an inferred ancestor. +Do not read `PhyloTree::height` or `PhyloTree::depth` off it as +times, and expect `PhyloTree::is_binary` to be false at that node. + +Non-additive data can imply a negative branch. Since a negative length +has no meaning as a number of substitutions, it is clamped to zero -- +the standard remedy, and a sign that the data do not fit a tree. + +Errors: +Returns an error for a malformed matrix (see `upgma`) or fewer than +three taxa. + +Rust: `biophysics::phylo::neighbor_joining` + """ + ... + +def distance_matrix_jc69(seqs: list[list[int]]) -> Matrix: + """ +The matrix of Jukes-Cantor corrected pairwise distances. + +Sites where either sequence is not one of A, C, G, T are skipped for +that pair, so different pairs may rest on different numbers of sites. + +Errors: +Returns an error for fewer than two sequences, sequences of differing +or zero length, a pair with no comparable site, or a pair whose observed +difference has saturated at three quarters, where the correction gives +no finite answer. + +Rust: `biophysics::phylo::distance_matrix_jc69` + """ + ... + +def parsimony_fitch(tree: PhyloTree, characters: list[int]) -> int: + """ +Fitch's parsimony score: the fewest character changes the tree needs. + +`characters` holds one state per leaf, in the order `PhyloTree::leaves` +returns them. Working from the tips down, each node takes the +intersection of its children's state sets, or -- when that is empty -- +their union at the cost of one change. + +The score counts changes, not their positions: a site can be explained +by several equally parsimonious assignments, and parsimony picks none of +them. It is also biased when rates vary a lot between branches, where it +can be positively misled (long-branch attraction) into preferring the +wrong topology however much data you add. + +Errors: +Returns an error if the character count differs from the leaf count or +more than 32 distinct states appear. + +Rust: `biophysics::phylo::parsimony_fitch` + """ + ... + +def likelihood_jc69(tree: PhyloTree, seqs: list[list[int]]) -> float: + """ +The log-likelihood of an alignment on a tree under Jukes-Cantor, by +Felsenstein's pruning algorithm. + +`seqs` holds one aligned sequence per leaf, in the order +`PhyloTree::leaves` returns them, and branch lengths are expected +substitutions per site. Under JC69 a branch of length `t` leaves a site +unchanged with probability `1/4 + 3/4 e^(-4t/3)` and sends it to each +other base with `1/4 - 1/4 e^(-4t/3)`; pruning sums over every ancestral +assignment in one pass up the tree rather than enumerating `4^nodes` of +them. + +The result is a *log* likelihood because the likelihood itself +underflows: a thousand sites each contributing a factor near `0.25` +gives a number around `1e-600`, which is not representable. + +Sites where a leaf carries an ambiguous or missing base contribute a +factor of one from that leaf -- the site still informs the others. + +Errors: +Returns an error if the sequence count differs from the leaf count, the +sequences are empty or of differing length. + +Rust: `biophysics::phylo::likelihood_jc69` + """ + ... + +def bootstrap_trees(seqs: list[list[int]], labels: list[str], replicates: int, method: DistanceMethod, rng: Rng) -> tuple[PhyloTree, list[float]]: + """ +Bootstrap support for the splits of a distance tree. + +Builds a reference tree from the whole alignment, then resamples the +*columns* with replacement `replicates` times, rebuilds, and reports the +fraction of replicates recovering each branch of the reference. The +returned vector is aligned with `reference.bipartitions()`. + +Branches are compared as unrooted bipartitions rather than rooted +clades. Neighbour joining's root is an artefact, so two replicates that +found the same tree can report a clade and its complement; treating +those as different answers would understate support for no reason. + +Columns are the sampling unit because sites are what the model treats as +independent draws; resampling taxa instead would answer a different +question. High support means the signal is spread across the alignment +rather than resting on a handful of sites -- it is not a probability +that the split is true, and a consistently wrong method will support a +wrong split at 100%. + +Replicates whose resampled alignment yields no usable distance matrix +(a saturated pair, say) are skipped, and the divisor counts only those +that succeeded. + +Errors: +Returns an error for fewer than three sequences, unaligned or empty +sequences, a label count that disagrees, zero replicates, or a whole +alignment that yields no tree. + +Rust: `biophysics::phylo::bootstrap_trees` + """ + ... + +def birth_death_tree(lambda_: float, mu: float, n_leaves: int, rng: Rng) -> PhyloTree: + """ +A birth-death tree, pruned to the lineages that survive. + +Runs the forward process -- each lineage speciating at rate `lambda` and +dying at rate `mu` -- until `n_leaves` lineages are alive at once, then +removes the extinct ones and suppresses the resulting single-child +nodes. What comes back is the *reconstructed* tree, the only one a +phylogeny of living species could ever show. + +That pruning is why extinction leaves a signature rather than +disappearing. Near the present, lineages have not yet had time to die, +so the reconstructed tree grows at the full rate `lambda` there while +deeper down it grows at `lambda - mu`. The surviving tree therefore +looks as though speciation accelerated toward the present -- the "pull +of the present", which shows up as a positive `gamma_statistic` and an +upturn in the `lineage_through_time` curve. + +The tree is stopped at the first event *after* the target count is +reached, so the interval during which `n_leaves` lineages coexist has a +length rather than collapsing to zero. + +The tree is ultrametric by construction -- every tip sits at the same +stopping time. + +Errors: +Returns an error for a non-positive `lambda`, a negative or non-finite +`mu`, `mu >= lambda`, fewer than three leaves, or if every attempt died +out before reaching the target. + +Rust: `biophysics::phylo::birth_death_tree` + """ + ... + +def gamma_statistic(tree: PhyloTree) -> float: + """ +Pybus and Harvey's gamma statistic. + +Standard normal under a constant-rate pure-birth process, so it is a +direct test of that null: negative gamma means the internal branching +events sit closer to the root than a constant rate predicts -- an early +burst, or a diversification rate that slowed -- and positive gamma means +they crowd toward the present. + +Extinction pushes gamma *positive* on a reconstructed tree even at a +constant rate: recent lineages have not yet had time to die, so nodes +crowd toward the present. A positive value is therefore not by itself +evidence of an accelerating rate. The bias runs the other way from the +slowdown test, which is why a significantly negative gamma is taken as +conservative evidence of a slowdown. + +The statistic reads times off the tree, so it is meaningful only for an +ultrametric one; a tree with unequal tip depths is rejected rather than +silently misread. + +Errors: +Returns an error for fewer than three tips, a tree that is not +ultrametric to `1e-8` relative, or one of zero height. + +Rust: `biophysics::phylo::gamma_statistic` + """ + ... + +def lineage_through_time(tree: PhyloTree) -> list[tuple[float, int]]: + """ +The lineage-through-time curve: `(time, lineage count)` at the root, at +every branching, and at the present. + +Time is measured from the root. Plotted with a log count axis, a +constant-rate pure-birth tree gives a straight line of slope `lambda`, +which is what makes the curve's departures readable: a bend downward +toward the tips is a slowdown, and the upturn near the present on a tree +with extinction is the pull of the present rather than a real burst. + +For a tree whose tips are not all at the same depth, the final point +uses the deepest tip and the count there is the leaf total. + +Errors: +Returns an error for a tree with fewer than two tips. + +Rust: `biophysics::phylo::lineage_through_time` + """ + ... diff --git a/bindings/python/python/numeria/biophysics/population.pyi b/bindings/python/python/numeria/biophysics/population.pyi new file mode 100644 index 0000000..a1fe2ba --- /dev/null +++ b/bindings/python/python/numeria/biophysics/population.pyi @@ -0,0 +1,659 @@ +""" +Population dynamics and population genetics: growth laws, interacting species, age-structured projection, discrete maps, and the drift, selection and coalescent theory that describes gene frequencies. # Two kinds of model, and why they disagree The deterministic models here describe a population large enough that averages are the whole story. The genetic models mostly do not: drift is the *variance* introduced by finite sampling, and it vanishes from any model that tracks only the mean. A Wright-Fisher population's expected allele frequency never changes at all, and yet every such population eventually fixes one allele or the other -- so the mean is not merely an approximation here, it is silent about the outcome. Where a function reports an expectation, it says so. # Units Times are in whatever unit the caller uses for rates. Genetic models work in generations, and `n` is the number of *diploid* individuals unless a function says otherwise, so a population of `n` carries `2n` gene copies -- the factor that makes heterozygosity decay as `1 - 1/(2n)` rather than `1 - 1/n`. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.linalg.matrix import Matrix +from numeria.monte_carlo import Rng +from numeria.statistics.inference import TestResult +from numeria.fractals.ifs import Variation + +class Competition: + """ +Which of the four outcomes a two-species Lotka-Volterra competition has. + +Rust: `biophysics::population::Competition` + """ + ... + +def logistic_growth(r: float, k: float, n0: float, t: float) -> float: + """ +Logistic growth in closed form: `N = K N0 e^(rt) / (K + N0 (e^(rt) - 1))`. + +Evaluated from the analytic solution rather than integrated, so it is +exact at every time and costs nothing at large `t`. + +Errors: +Returns an error for a non-positive carrying capacity or a negative +initial population. + +Rust: `biophysics::population::logistic_growth` + """ + ... + +def gompertz(r: float, k: float, n0: float, t: float) -> float: + """ +Gompertz growth: `N = K exp(ln(N0/K) e^(-rt))`. + +Differs from the logistic in where it turns: the inflection is at `K/e`, +about 37 per cent of capacity, rather than at half. That asymmetry is why +it fits tumour and organ growth better than the logistic does -- those +slow down earlier than a symmetric curve allows. + +Errors: +Returns an error for a non-positive capacity or initial population. + +Rust: `biophysics::population::gompertz` + """ + ... + +def richards(r: float, k: float, nu: float, n0: float, t: float) -> float: + """ +Richards growth, which contains both: `nu = 1` is logistic and the limit +`nu -> 0` is Gompertz. + +`N = K (1 + q e^(-r t))^(-1/nu)` with `q = (K/N0)^nu - 1`, solving +`dN/dt = (r/nu) N (1 - (N/K)^nu)`. + +The `r/nu` in that equation is not decoration, and writing the solution +with `e^(-r nu t)` instead -- which solves the tidier-looking +`dN/dt = r N (1 - (N/K)^nu)` -- destroys the Gompertz limit. Under that +convention the effective rate is `r nu`, so letting `nu -> 0` at fixed +`r` freezes the curve at its initial value rather than approaching +anything. Here `r` is the intrinsic rate in both limits, which is what +makes the family a genuine interpolation rather than two special cases +with a gap between them. + +Errors: +Returns an error for a non-positive capacity, initial population or +shape. + +Rust: `biophysics::population::richards` + """ + ... + +def allee_effect_ode(r: float, a: float, k: float, n0: float, t_end: float) -> list[tuple[float, float]]: + """ +Growth with a strong Allee effect: +`dN/dt = r N (N/A - 1) (1 - N/K)`. + +Below the threshold `A` the growth rate is *negative* and the population +collapses however far it is from the capacity. That is the qualitative +difference from logistic growth, where any positive population recovers: +here there is a point of no return, which is why a species can be +committed to extinction while individuals are still alive. + +Errors: +Returns an error for a threshold not below the capacity, a negative +initial population, or a non-positive end time. + +Rust: `biophysics::population::allee_effect_ode` + """ + ... + +def lotka_volterra(alpha: float, beta: float, delta: float, gamma: float, x0: float, y0: float, t_end: float) -> tuple[list[tuple[float, float, float]], list[float]]: + """ +The Lotka-Volterra predator-prey system, with its conserved quantity. + +`dx/dt = alpha x - beta x y`, `dy/dt = delta x y - gamma y`. Returns +`(time, prey, predator)` together with +`V = delta x - gamma ln x + beta y - alpha ln y`, which is constant along +every orbit. + +That constant is the reason the orbits are closed curves rather than a +limit cycle: the system is conservative, so its amplitude is set by where +it started and never forgets. A model that damped onto a single cycle +would be a different system, and returning the invariant lets a caller +see the integrator's drift rather than take it on trust. + +Errors: +Returns an error for non-positive rates or a non-positive initial +population, for which the invariant is undefined. + +Rust: `biophysics::population::lotka_volterra` + """ + ... + +def rosenzweig_macarthur(r: float, k: float, attack: float, handling: float, efficiency: float, mortality: float, x0: float, y0: float, t_end: float) -> list[tuple[float, float, float]]: + """ +The Rosenzweig-MacArthur predator-prey model: logistic prey with a +saturating (Holling type II) predator response. + +`dx/dt = r x (1 - x/K) - a x y / (1 + a h x)`, +`dy/dt = e a x y / (1 + a h x) - m y`. + +The saturating response is what produces the *paradox of enrichment*: +raising the prey's carrying capacity destabilises the coexistence +equilibrium into a limit cycle of growing amplitude, so enriching the +system makes extinction more likely rather than less. The plain +Lotka-Volterra model, whose response is linear, cannot show this. + +Errors: +Returns an error for non-positive parameters or a non-positive initial +population. + +Rust: `biophysics::population::rosenzweig_macarthur` + """ + ... + +def enrichment_critical_capacity(attack: float, handling: float, efficiency: float, mortality: float) -> float: + """ +The prey density at which the Rosenzweig-MacArthur coexistence +equilibrium loses stability, `K = (1 + a h x*) / (a h - ...)`, expressed +as the critical carrying capacity. + +The equilibrium prey density is `x* = m / (a (e - m h))`, independent of +`K`, and the equilibrium is stable while `K < x* + 1/(a h)` and unstable +above -- the Hopf bifurcation of the paradox of enrichment. + +Errors: +Returns an error for parameters that admit no coexistence equilibrium: +the predator must gain more from a prey item than it spends handling it. + +Rust: `biophysics::population::enrichment_critical_capacity` + """ + ... + +def coexistence_condition(k1: float, k2: float, alpha12: float, alpha21: float) -> Competition: + """ +The outcome of two-species competition, from the competition +coefficients and capacities alone. + +Coexistence requires each species to limit *itself* more than it limits +the other -- `alpha12 < K1/K2` and `alpha21 < K2/K1`. If both +inequalities reverse, both exclusion equilibria are stable and the winner +is decided by the starting densities rather than by the parameters. This +is the content of the competitive exclusion principle, and it is a +statement about niche overlap rather than about which species is +"stronger". + +Errors: +Returns an error for non-positive capacities or negative coefficients. + +Rust: `biophysics::population::coexistence_condition` + """ + ... + +def competition_lv(r1: float, r2: float, k1: float, k2: float, alpha12: float, alpha21: float, n1: float, n2: float, t_end: float) -> list[tuple[float, float, float]]: + """ +Two-species Lotka-Volterra competition, integrated. + +Errors: +Returns an error for non-positive capacities or rates, or negative +initial densities. + +Rust: `biophysics::population::competition_lv` + """ + ... + +def metapopulation_levins(c: float, e: float, p0: float, t_end: float) -> list[tuple[float, float]]: + """ +The Levins metapopulation model: `dp/dt = c p (1 - p) - e p`. + +The equilibrium occupancy is `1 - e/c`, and the population persists only +while colonisation outpaces extinction. Note what it says about habitat +loss: destroying a fraction `D` of patches replaces the equilibrium with +`1 - D - e/c`, so a metapopulation goes extinct while a fraction `e/c` of +its habitat still remains -- the extinction debt. + +Errors: +Returns an error for negative rates or an occupancy outside zero to one. + +Rust: `biophysics::population::metapopulation_levins` + """ + ... + +def leslie_matrix(fecundity: list[float], survival: list[float]) -> Matrix: + """ +The Leslie projection matrix from age-specific fecundity and survival. + +`fecundity[i]` is the expected offspring of an individual in class `i` +over one time step, and `survival[i]` the probability of surviving from +class `i` to `i + 1`, so `survival` is one shorter than `fecundity`. + +Errors: +Returns an error for empty input, a mismatched length, a negative +fecundity, or a survival outside zero to one. + +Rust: `biophysics::population::leslie_matrix` + """ + ... + +def leslie_growth_rate(l: Matrix | Sequence[Sequence[float]]) -> tuple[float, list[float]]: + """ +The asymptotic growth rate and stable age distribution of a Leslie +matrix, by power iteration. + +Returns `(lambda, distribution)` with the distribution normalised to sum +to one. Perron-Frobenius guarantees the dominant eigenvalue of a +primitive non-negative matrix is real, positive and simple, which is what +makes power iteration the right method here rather than a general +eigensolver. + +The strong ergodic theorem is the substance: *whatever* age distribution +a population starts with, it converges to this one and then grows by +`lambda` per step. The transient depends on the start; the asymptote does +not. + +Errors: +Returns an error for a non-square matrix or one whose iteration does not +converge -- which happens when the matrix is imprimitive, for instance a +species that reproduces at exactly one age, whose age classes then cycle +forever instead of settling. + +Rust: `biophysics::population::leslie_growth_rate` + """ + ... + +def stable_age_distribution(l: Matrix | Sequence[Sequence[float]]) -> list[float]: + """ +The stable age distribution alone. + +Errors: +Returns an error on the same conditions as `leslie_growth_rate`. + +Rust: `biophysics::population::stable_age_distribution` + """ + ... + +def euler_lotka_solve(lx: list[float], mx: list[float]) -> float: + """ +Solves the Euler-Lotka equation `sum l_x m_x r^(-x) = 1` for the growth +rate `r` per time step. + +`lx[i]` is survivorship to age `i + 1` and `mx[i]` the fecundity there, +so the first entry describes age one. The left side is strictly +decreasing in `r`, so bisection cannot fail; it is the same growth rate +`leslie_growth_rate` finds, reached from the life table rather than +from the matrix. + +Errors: +Returns an error for mismatched lengths, a survivorship outside zero to +one, or a population with no reproduction at all. + +Rust: `biophysics::population::euler_lotka_solve` + """ + ... + +def ricker_map(r: float, k: float, n0: float, steps: int) -> list[float]: + """ +The Ricker map `N -> N exp(r (1 - N/K))`, iterated. + +Overcompensating density dependence: a population far above capacity +crashes below it rather than settling, and as `r` grows the fixed point +period-doubles into chaos. That a deterministic single-species model with +no environmental variation produces apparently random fluctuations is the +point -- population data need not be noisy to look noisy. + +Errors: +Returns an error for a non-positive capacity or negative start. + +Rust: `biophysics::population::ricker_map` + """ + ... + +def beverton_holt(ratio: float, k: float, n0: float, steps: int) -> list[float]: + """ +The Beverton-Holt map `N -> R N / (1 + (R - 1) N / K)`, iterated. + +Compensating rather than overcompensating: however far above capacity the +population starts it approaches `K` monotonically and never overshoots, +so unlike Ricker it has no route to chaos at any `R`. The two models +differ in nothing but the shape of the density dependence, and that +single difference is the whole distinction between a stable fishery model +and a chaotic one. + +It also has a closed-form solution, which is what the tests check against. + +Errors: +Returns an error for a growth ratio at or below one, a non-positive +capacity, or a negative start. + +Rust: `biophysics::population::beverton_holt` + """ + ... + +def bifurcation_ricker(r_lo: float, r_hi: float, samples: int, transient: int, keep: int) -> list[tuple[float, list[float]]]: + """ +The attractor of the Ricker map at each of a range of growth rates: the +bifurcation diagram. + +Returns `(r, attractor points)` per rate, with the transient discarded +and the remaining points deduplicated so a period-`p` cycle reports `p` +values. + +Errors: +Returns an error for an empty or descending range, or bad map parameters. + +Rust: `biophysics::population::bifurcation_ricker` + """ + ... + +def wright_fisher(n: int, p0: float, generations: int, rng: Rng) -> list[float]: + """ +A Wright-Fisher allele-frequency trajectory: each generation resamples +`2n` gene copies binomially from the previous frequency. + +The expected frequency never changes -- drift is a martingale -- and yet +every trajectory eventually fixes at zero or one. That is the whole point +of the model, and the reason no deterministic account of it is possible: +the mean is constant while the outcome is certain to be extreme. + +Errors: +Returns an error for no individuals or a frequency outside zero to one. + +Rust: `biophysics::population::wright_fisher` + """ + ... + +def moran_process(n: int, i0: int, fitness: float, rng: Rng) -> tuple[bool, int]: + """ +A Moran process: one birth and one death per step, with the mutant type +having relative fitness `r`. + +Returns `(fixed, steps)` -- whether the mutant fixed rather than being +lost, and how many steps it took. Unlike Wright-Fisher the population +overlaps generations, and the fixation probability has an exact closed +form; see `fixation_probability_moran`. + +Errors: +Returns an error for an empty population, a starting count above it, or a +non-positive fitness. + +Rust: `biophysics::population::moran_process` + """ + ... + +def fixation_probability_moran(n: int, i: int, r: float) -> float: + """ +The exact fixation probability of `i` mutants of relative fitness `r` in +a Moran population of `n`: `(1 - r^-i) / (1 - r^-n)`. + +At `r = 1` it degenerates to `i/n` -- a neutral mutant fixes with +probability equal to its initial frequency, which is the cleanest +statement of what drift alone does. A single advantageous mutant with +`r = 1.01` fixes with probability about `1/100` rather than the certainty +a deterministic model would predict: even a beneficial mutation is +usually lost. + +Errors: +Returns an error for an empty population, a count above it, or a +non-positive fitness. + +Rust: `biophysics::population::fixation_probability_moran` + """ + ... + +def genetic_drift_heterozygosity(n: int, h0: float, t: float) -> float: + """ +The expected heterozygosity after `t` generations of drift: +`H_t = H_0 (1 - 1/(2N))^t`. + +The `2N` rather than `N` is the diploid gene copy count, and getting it +wrong halves the predicted rate of decay. Variation is lost at a rate set +by the population size alone -- no selection is involved -- which is why +small populations lose diversity even when nothing is wrong with them. + +Errors: +Returns an error for an empty population or a heterozygosity outside zero +to one. + +Rust: `biophysics::population::genetic_drift_heterozygosity` + """ + ... + +def hardy_weinberg(p: float) -> tuple[float, float, float]: + """ +Hardy-Weinberg genotype frequencies `(p^2, 2pq, q^2)`. + +Errors: +Returns an error for an allele frequency outside zero to one. + +Rust: `biophysics::population::hardy_weinberg` + """ + ... + +def hw_chi_square_test(observed: list[float]) -> TestResult: + """ +A chi-squared test of observed genotype counts against Hardy-Weinberg +proportions, with the allele frequency estimated from the same data. + +One degree of freedom, not two: estimating `p` from the counts costs one, +which is why the standard `k - 1` rule does not apply here. Reported +through `chi_squared_gof`, whose degrees of freedom are corrected +afterwards. + +Errors: +Returns an error for a negative count or an empty sample. + +Rust: `biophysics::population::hw_chi_square_test` + """ + ... + +def selection_one_locus(p0: float, w: list[float], generations: int) -> list[float]: + """ +One generation at a time of selection at a single diploid locus, with +genotype fitnesses `[w_AA, w_Aa, w_aa]`. + +Returns the allele frequency each generation. Which allele wins is not +decided by fitness alone: with heterozygote advantage neither fixes and +the population settles at a polymorphic equilibrium, while with +heterozygote *disadvantage* both fixations are stable and the outcome +depends on where it started. Directional selection is only one of three +possibilities. + +Errors: +Returns an error for a frequency outside zero to one, a negative fitness, +or a population with no viable genotype. + +Rust: `biophysics::population::selection_one_locus` + """ + ... + +def balanced_polymorphism(w: list[float]) -> float: + """ +The polymorphic equilibrium of a locus with heterozygote advantage: +`p* = (w_Aa - w_aa) / (2 w_Aa - w_AA - w_aa)`. + +Errors: +Returns an error unless the heterozygote is strictly the fittest, in +which case there is no interior equilibrium to report. + +Rust: `biophysics::population::balanced_polymorphism` + """ + ... + +def mutation_selection_balance(mu: float, s: float, h: float) -> float: + """ +The equilibrium frequency of a deleterious allele maintained by +mutation. + +For a fully recessive allele the balance is `sqrt(mu/s)`; with any +dominance `h > 0` it is `mu/(h s)` instead. The difference is large: at +`mu = 1e-6` and `s = 0.1` a recessive allele sits at 0.32 per cent while +one with `h = 0.1` sits at 0.01 per cent, some thirty times rarer. +Selection acts on heterozygotes far more often than on the rare +homozygote, so even slight dominance dominates the balance. + +Errors: +Returns an error for a non-positive selection coefficient, a negative +mutation rate, or a dominance outside zero to one. + +Rust: `biophysics::population::mutation_selection_balance` + """ + ... + +def kin_selection_hamilton(r: float, b: float, c: float) -> bool: + """ +Hamilton's rule: an altruistic act spreads when `r b > c`. + +Errors: +Returns an error for a relatedness outside zero to one. + +Rust: `biophysics::population::kin_selection_hamilton` + """ + ... + +def price_equation_decompose(trait_values: list[float], fitness: list[float], offspring_trait: list[float]) -> tuple[float, float]: + """ +The Price equation, decomposing the change in a mean trait into +selection and transmission. + +Returns `(selection, transmission)` with +`selection = Cov(w, z) / w_bar` and +`transmission = E[w dz] / w_bar`, whose sum is exactly the change in the +mean trait. This is an *identity*, not a model -- it assumes nothing +about inheritance or fitness and holds for any population whatever, which +is what makes it useful for deciding whether an observed change was +selection at all. + +Errors: +Returns an error for mismatched lengths, an empty population, a negative +fitness, or a mean fitness of zero. + +Rust: `biophysics::population::price_equation_decompose` + """ + ... + +def coalescent_time_expected(n: int, k: int) -> float: + """ +The expected time, in generations, during which a sample of `n` lineages +has exactly `k` ancestors: `E[T_k] = 4N / (k (k - 1))`. + +The `4N` is the diploid gene-copy convention: there are `2N` copies, and +the coalescence rate for `k` lineages is `C(k,2) / (2N)`. The +distribution's shape is the striking part -- `T_2` alone is `2N` +generations, longer than every other interval put together, so the +genealogy of a sample is dominated by its deepest branch and estimates of +ancient history rest on very little independent information. + +Errors: +Returns an error for fewer than two lineages or an empty population. + +Rust: `biophysics::population::coalescent_time_expected` + """ + ... + +def coalescent_tmrca_expected(n: int, k: int) -> float: + """ +The expected time to the most recent common ancestor of a sample of `k`: +`4N (1 - 1/k)` generations. + +Bounded above by `4N` however large the sample: adding sequences barely +deepens the tree, because new lineages coalesce almost immediately with +the ones already there. Sampling more individuals buys resolution near +the tips and almost nothing at the root. + +Errors: +Returns an error for fewer than two lineages or an empty population. + +Rust: `biophysics::population::coalescent_tmrca_expected` + """ + ... + +def coalescent_simulate(n: int, samples: int, rng: Rng) -> list[float]: + """ +One realisation of the coalescent: the waiting times, in generations, +while the sample has `k, k-1, ..., 2` ancestors. + +Returns the intervals in that order, so the total tree height is their +sum. Each is exponential with rate `C(k,2)/(2N)`. + +The tree *topology* belongs with the phylogenetics module; this reports +the times, which is what the diversity statistics here need. + +Errors: +Returns an error for fewer than two lineages or an empty population. + +Rust: `biophysics::population::coalescent_simulate` + """ + ... + +def watterson_theta(segregating: float, n: int) -> float: + """ +Watterson's estimator of `theta = 4 N mu` from the number of segregating +sites: `theta_W = S / a_n`. + +The division by `a_n` rather than by `n` is the whole content: the number +of segregating sites grows only logarithmically with the sample, because +each additional sequence adds a shorter and shorter branch to the +genealogy. Dividing by the sample size would make the estimate fall +steadily as more data arrived. + +Errors: +Returns an error for fewer than two sequences or a negative site count. + +Rust: `biophysics::population::watterson_theta` + """ + ... + +def nucleotide_diversity(sequences: list[list[int]]) -> float: + """ +Nucleotide diversity `pi`: the mean number of differences between a pair +of sequences. + +Errors: +Returns an error for fewer than two sequences or sequences of differing +length. + +Rust: `biophysics::population::nucleotide_diversity` + """ + ... + +def segregating_sites(sequences: list[list[int]]) -> int: + """ +The number of segregating sites in an alignment. + +Errors: +Returns an error for fewer than two sequences or sequences of differing +length. + +Rust: `biophysics::population::segregating_sites` + """ + ... + +def tajima_d(sequences: list[list[int]]) -> float: + """ +Tajima's D: the standardised difference between nucleotide diversity and +Watterson's estimator. + +Both estimate the same `theta` under neutrality and constant size, so +their difference is zero in expectation and any departure is evidence +that one of those assumptions fails. The sign carries the interpretation: +negative means an excess of rare variants -- a recent expansion or a +selective sweep -- and positive means an excess of intermediate ones, +as under balancing selection or population structure. It cannot +distinguish demography from selection, which is why a significant D is a +question rather than an answer. + +Errors: +Returns an error for fewer than four sequences, below which the variance +is not defined, or for an alignment with no variation. + +Rust: `biophysics::population::tajima_d` + """ + ... + +def fst(subpop_freqs: list[float]) -> float: + """ +Wright's `F_ST` from subpopulation allele frequencies: +`(H_T - H_S) / H_T`. + +Zero when the subpopulations have identical frequencies and one when each +is fixed for a different allele. It measures how much of the total +heterozygosity is *lost* by subdivision, so it is a statement about +variance in frequency rather than about how different the populations +look. + +Errors: +Returns an error for fewer than two subpopulations, a frequency outside +zero to one, or a set of populations all fixed for the same allele, for +which there is no heterozygosity to partition. + +Rust: `biophysics::population::fst` + """ + ... diff --git a/bindings/python/python/numeria/biophysics/seq_align.pyi b/bindings/python/python/numeria/biophysics/seq_align.pyi new file mode 100644 index 0000000..7c259f2 --- /dev/null +++ b/bindings/python/python/numeria/biophysics/seq_align.pyi @@ -0,0 +1,502 @@ +""" +Sequence alignment and the elementary sequence analysis around it. # What an alignment score means Every function here returns a score under an explicit `Scoring`, and the score is only comparable between alignments computed under the *same* one. That is not pedantry: a gap penalty is a free parameter, and the choice of it decides whether two sequences align as one long homology with an insertion or as two short unrelated fragments. Where a function returns an alignment as well as a score, the score is always the score of that alignment under that scoring -- which the tests check directly, since a dynamic program that reports a maximum it did not achieve is the commonest way for one of these to be wrong. # Global, local and affine The three classical algorithms differ in one line of the recurrence each, and the differences matter more than the similarity suggests. Needleman-Wunsch aligns the sequences end to end; Smith-Waterman clamps the score at zero so a poor prefix cannot drag a good local match below the surface; Gotoh separates opening a gap from extending one, which is what lets a single long insertion cost less than many short ones. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +class Scoring: + """ +A substitution and gap scoring scheme. + +Rust: `biophysics::seq_align::Scoring` + """ + def __init__(self, match_score: int, mismatch: int, gap: int, matrix: Optional[SubstitutionMatrix]) -> None: ... + @staticmethod + def simple(match_score: int, mismatch: int, gap: int) -> Scoring: ... + def substitution(self, a: int, b: int) -> int: ... + @property + def match_score(self) -> int: ... + @property + def mismatch(self) -> int: ... + @property + def gap(self) -> int: ... + @property + def matrix(self) -> Optional[SubstitutionMatrix]: ... + +class SubstitutionMatrix: + """ +A named substitution matrix over an alphabet. + +Rust: `biophysics::seq_align::SubstitutionMatrix` + """ + def __init__(self, alphabet: list[int], scores: list[int]) -> None: ... + def lookup(self, a: int, b: int) -> Optional[int]: ... + def is_symmetric(self) -> bool: ... + @property + def alphabet(self) -> list[int]: ... + @property + def scores(self) -> list[int]: ... + +def needleman_wunsch(a: list[int], b: list[int], score: Scoring) -> tuple[int, str, str]: + """ +Global alignment by Needleman-Wunsch. + +Returns the optimal score and the two aligned strings, with `-` for gaps. +The alignment spans both sequences end to end, which is the right model +when the sequences are known to be homologous over their whole length and +the wrong one when only a domain is shared -- for that, see +`smith_waterman`. + +Errors: +Returns an error for a non-negative gap penalty, or sequences long enough +that the quadratic table would not fit; use `hirschberg` for those. + +Rust: `biophysics::seq_align::needleman_wunsch` + """ + ... + +def smith_waterman(a: list[int], b: list[int], score: Scoring) -> tuple[int, int, int, str, str]: + """ +Local alignment by Smith-Waterman. + +Returns `(score, start in a, start in b, aligned a, aligned b)`. + +The single change from Needleman-Wunsch -- clamping each cell at zero -- +is what makes it local: a prefix that aligns badly is discarded rather +than carried, so a strong internal match is found whatever surrounds it. +The score is therefore never negative, and an alignment of two unrelated +sequences reports a small positive score rather than a large negative +one, which is why local scores need a significance model and global ones +less so. + +Errors: +Returns an error on the same conditions as `needleman_wunsch`. + +Rust: `biophysics::seq_align::smith_waterman` + """ + ... + +def gotoh_affine(a: list[int], b: list[int], match_score: int, mismatch: int, gap_open: int, gap_extend: int) -> tuple[int, str, str]: + """ +Global alignment with affine gap penalties, by Gotoh's algorithm. + +A gap of length `k` costs `open + k * extend` rather than `k * gap`, so a +single long insertion is cheap relative to many short ones. That is the +biologically right shape -- one indel event of twenty residues is far +more likely than twenty separate ones -- and it is why affine gaps are +the default in practice despite costing three tables instead of one. + +With `open = 0` the model degenerates to linear gaps and the result must +agree with `needleman_wunsch` at `gap = extend`, which the tests check. + +Errors: +Returns an error for a positive gap penalty or an oversized table. + +Rust: `biophysics::seq_align::gotoh_affine` + """ + ... + +def banded_alignment(a: list[int], b: list[int], band: int, score: Scoring) -> int: + """ +The global alignment score restricted to a diagonal band. + +Only cells with `|i - j| <= band` are computed, so the cost is +`O(n * band)` rather than `O(n * m)`. The result is the true optimum only +when the optimal alignment stays inside the band -- which is why this is +a heuristic for similar sequences rather than a general algorithm, and +why a band wide enough to contain the whole table must reproduce +`needleman_wunsch` exactly. + +Errors: +Returns an error for a non-negative gap penalty, or a band too narrow to +reach the far corner. + +Rust: `biophysics::seq_align::banded_alignment` + """ + ... + +def hirschberg(a: list[int], b: list[int], score: Scoring) -> tuple[str, str]: + """ +Global alignment in linear space, by Hirschberg's divide and conquer. + +The score of a global alignment can be computed in `O(min(n, m))` space +by keeping two rows, but the *traceback* seems to need the whole table. +Hirschberg's observation is that the optimal alignment must cross the +middle row somewhere, that the crossing point can be found from two +linear-space score passes -- one forward, one backward -- and that the +problem then splits in two. The cost is a constant factor more time for +an asymptotic saving in space, which is the trade that makes whole-genome +alignment possible at all. + +The alignment it returns is optimal, so its score must equal +`needleman_wunsch`'s; the tests check exactly that. + +Errors: +Returns an error for a non-negative gap penalty. + +Rust: `biophysics::seq_align::hirschberg` + """ + ... + +def alignment_score(top: str, bottom: str, score: Scoring) -> int: + """ +The score of an alignment already made, under a scoring scheme. + +Used to check that a dynamic program achieved the score it reported -- +the commonest way for one of these to be wrong is to report a maximum it +did not actually reach. + +Gaps are charged linearly, so this agrees with `needleman_wunsch` and +with `gotoh_affine` only when the latter's open cost is zero. + +Errors: +Returns an error for alignments of differing length or a column of two +gaps, which no alignment should contain. + +Rust: `biophysics::seq_align::alignment_score` + """ + ... + +def alignment_score_affine(top: str, bottom: str, match_score: int, mismatch: int, gap_open: int, gap_extend: int) -> int: + """ +The score of an alignment under affine gap penalties. + +Errors: +Returns an error on the same conditions as `alignment_score`. + +Rust: `biophysics::seq_align::alignment_score_affine` + """ + ... + +def blosum62() -> SubstitutionMatrix: + """ +The BLOSUM62 substitution matrix. + +Derived from blocks of aligned protein segments no more than 62 per cent +identical, which is what the number means -- a *higher* BLOSUM number is +built from more similar sequences and suits closer homologues, the +opposite of the intuition the name suggests. The diagonal is not +constant: a tryptophan match scores 11 and a leucine match 4, because +tryptophan is rare and its conservation is correspondingly more +informative. + +Rust: `biophysics::seq_align::blosum62` + """ + ... + +def pam250() -> SubstitutionMatrix: + """ +The PAM250 substitution matrix. + +Extrapolated from one per cent accepted mutations by raising the +substitution probability matrix to the 250th power, so it describes very +distant relationships -- the opposite end of the range from BLOSUM62. The +extrapolation is its weakness: errors in the one-per-cent estimates +compound over 250 multiplications, which is the reason BLOSUM, built +directly from distant alignments, generally does better at finding remote +homologues. + +Rust: `biophysics::seq_align::pam250` + """ + ... + +def gc_content(seq: list[int]) -> float: + """ +The fraction of G and C bases. + +Errors: +Returns an error for an empty sequence. + +Rust: `biophysics::seq_align::gc_content` + """ + ... + +def reverse_complement(seq: list[int]) -> list[int]: + """ +The reverse complement of a DNA sequence. + +An involution: applying it twice returns the original, which is what +makes it a symmetry of double-stranded DNA rather than a transformation +of it. Unrecognised bases are passed through as `N`. + +Rust: `biophysics::seq_align::reverse_complement` + """ + ... + +def transcribe(seq: list[int]) -> list[int]: + """ +DNA to RNA: thymine becomes uracil. + +Rust: `biophysics::seq_align::transcribe` + """ + ... + +def codon_to_amino(codon: list[int]) -> int: + """ +The amino acid a codon encodes, or `*` for a stop and `X` for anything +unrecognised. + +Rust: `biophysics::seq_align::codon_to_amino` + """ + ... + +def translate(seq: list[int]) -> list[int]: + """ +Translates a nucleotide sequence in frame zero, stopping at the first +stop codon. + +Rust: `biophysics::seq_align::translate` + """ + ... + +def orf_find(seq: list[int], min_len: int) -> list[tuple[int, int, int]]: + """ +Open reading frames, as `(start, end, strand)` with the strand `+1` or +`-1` and positions on the forward strand. + +Searches all six frames. `min_len` is in amino acids, excluding the stop. + +Errors: +Returns an error for a zero minimum length, which would report every +start codon. + +Rust: `biophysics::seq_align::orf_find` + """ + ... + +def codon_usage(seq: list[int]) -> list[tuple[str, float]]: + """ +Codon usage counts as fractions, for codons appearing in frame zero. + +Errors: +Returns an error for a sequence shorter than one codon. + +Rust: `biophysics::seq_align::codon_usage` + """ + ... + +def melting_temperature_wallace(seq: list[int]) -> float: + """ +The Wallace rule melting temperature: `2 (A + T) + 4 (G + C)` degrees. + +Valid only for short oligonucleotides, roughly 14 to 20 bases. It ignores +concentration, salt and stacking entirely, which is why it disagrees with +`tm_nearest_neighbor` by ten degrees or more on anything longer -- the +stacking energy that the nearest-neighbour model accounts for is not a +correction at that length, it is most of the answer. + +Errors: +Returns an error for an empty sequence. + +Rust: `biophysics::seq_align::melting_temperature_wallace` + """ + ... + +def tm_nearest_neighbor(seq: list[int], concentration: float) -> float: + """ +The nearest-neighbour melting temperature, in degrees Celsius. + +`Tm = dH / (dS + R ln(C/4)) - 273.15`, with the enthalpy and entropy +summed over adjacent base pairs from the SantaLucia unified parameters. +The concentration enters logarithmically, so a hundredfold change moves +the melting point by only a few degrees -- which is why primer design +tolerates approximate concentrations and not approximate sequences. + +Errors: +Returns an error for a sequence shorter than two bases, a non-positive +concentration, or a base outside A, C, G and T. + +Rust: `biophysics::seq_align::tm_nearest_neighbor` + """ + ... + +def hamming_seqs(a: list[int], b: list[int]) -> Optional[int]: + """ +The Hamming distance, or `None` if the sequences differ in length. + +Rust: `biophysics::seq_align::hamming_seqs` + """ + ... + +def p_distance(a: list[int], b: list[int]) -> float: + """ +The proportion of differing sites. + +Errors: +Returns an error for empty or mismatched sequences. + +Rust: `biophysics::seq_align::p_distance` + """ + ... + +def jukes_cantor_distance(p: float) -> float: + """ +The Jukes-Cantor corrected distance +`d = -3/4 ln(1 - 4p/3)`. + +The correction is for *multiple hits*: two sequences that have diverged +far enough will differ at three quarters of their sites by chance alone, +because a random base matches one time in four. So the observed +proportion saturates at 0.75 while the true number of substitutions grows +without bound, and the logarithm is what recovers the latter from the +former. Above the saturation point the distance is not merely large -- +it is undefined, and reporting a large finite number there would be +worse than refusing. + +Errors: +Returns an error for a proportion outside `[0, 3/4)`. + +Rust: `biophysics::seq_align::jukes_cantor_distance` + """ + ... + +def kimura_2p(transitions: float, transversions: float) -> float: + """ +Kimura's two-parameter distance from transition and transversion +proportions. + +Distinguishing the two matters because transitions -- purine to purine or +pyrimidine to pyrimidine -- happen several times more often than +transversions despite there being twice as many transversions available. +Treating all changes alike, as Jukes-Cantor does, therefore +underestimates the divergence of sequences that have accumulated mostly +transitions. + +Errors: +Returns an error for proportions outside the range where the formula's +logarithms are defined. + +Rust: `biophysics::seq_align::kimura_2p` + """ + ... + +def kmer_index(seq: list[int], k: int) -> list[tuple[list[int], list[int]]]: + """ +Every `k`-mer and the positions it occurs at, sorted by k-mer. + +Errors: +Returns an error for a zero `k` or one longer than the sequence. + +Rust: `biophysics::seq_align::kmer_index` + """ + ... + +def minimizers(seq: list[int], k: int, w: int) -> list[tuple[int, int]]: + """ +The minimizers of a sequence: the smallest-hashing k-mer in each window +of `w` consecutive k-mers, deduplicated by position. + +The property that makes minimizers useful is not that they are a sample +but that they are a *consistent* one: two sequences that share a +substring of length at least `w + k - 1` are guaranteed to select the +same minimizer from it, so a shared region is found without comparing +every k-mer. Random sampling has no such guarantee. + +Errors: +Returns an error for a zero `k` or `w`, or a sequence too short to hold a +window. + +Rust: `biophysics::seq_align::minimizers` + """ + ... + +def burrows_wheeler_search(text: list[int], pattern: list[int]) -> list[int]: + """ +Exact pattern search over the Burrows-Wheeler transform, by backward +search on an FM-index. + +Backward search narrows an interval of the suffix array one pattern +character at a time, so the cost depends on the *pattern* length and not +on the text's -- which is the whole point of the index. Returns the +matching positions in the original text, sorted. + +Errors: +Returns an error for an empty pattern or text. + +Rust: `biophysics::seq_align::burrows_wheeler_search` + """ + ... + +def msa_center_star(sequences: list[list[int]], score: Scoring) -> list[str]: + """ +A centre-star multiple alignment. + +Picks the sequence with the best total pairwise score as the centre, +aligns every other to it, and merges the results by inserting gaps so +that all agree with the centre. The result is not optimal -- optimal +multiple alignment is NP-hard in the number of sequences -- and its +quality depends entirely on the centre being a reasonable +representative, which is why it degrades on a divergent family. + +Errors: +Returns an error for fewer than two sequences, an empty sequence, or a +bad scoring. + +Rust: `biophysics::seq_align::msa_center_star` + """ + ... + +def profile_from_msa(msa: list[str]) -> list[tuple[int, list[float]]]: + """ +The residue frequency profile of an alignment, as `(residue, column +frequencies)` sorted by residue. + +Errors: +Returns an error for an empty alignment or rows of differing length. + +Rust: `biophysics::seq_align::profile_from_msa` + """ + ... + +def consensus(msa: list[str]) -> str: + """ +The consensus sequence: the commonest residue in each column, with gaps +broken in favour of a residue. + +Errors: +Returns an error on the same conditions as `profile_from_msa`. + +Rust: `biophysics::seq_align::consensus` + """ + ... + +def pssm_score(profile: list[tuple[int, list[float]]], seq: list[int]) -> list[float]: + """ +Scores a sequence against a position-specific scoring matrix, sliding it +along and reporting the log-odds score at each offset. + +The background is uniform over the profile's residues. A count of zero +would give a log-odds of negative infinity, so a pseudocount is added -- +without one, a single unobserved residue vetoes an otherwise perfect +match, which is an artefact of finite sampling rather than a fact about +the motif. + +Errors: +Returns an error for an empty profile or a sequence shorter than it. + +Rust: `biophysics::seq_align::pssm_score` + """ + ... + +def de_bruijn_assembly_lite(reads: list[list[int]], k: int) -> list[list[int]]: + """ +A de Bruijn assembly: the unambiguous paths through the k-mer graph of a +read set. + +Each read contributes its `k`-mers; nodes are `(k-1)`-mers and edges are +`k`-mers. Contigs are grown along vertices with exactly one way in and +one way out, and stop wherever the graph branches -- which is exactly +where a repeat longer than `k` sits. That is the fundamental limit of +short-read assembly, not a shortcoming of this implementation: a repeat +longer than the read length cannot be resolved by any amount of coverage. + +Errors: +Returns an error for a `k` below two, or no reads long enough. + +Rust: `biophysics::seq_align::de_bruijn_assembly_lite` + """ + ... diff --git a/bindings/python/python/numeria/cfd/__init__.pyi b/bindings/python/python/numeria/cfd/__init__.pyi new file mode 100644 index 0000000..bc01e0f --- /dev/null +++ b/bindings/python/python/numeria/cfd/__init__.pyi @@ -0,0 +1,314 @@ +""" +Computational fluid dynamics: staggered grids, advection schemes, and (in later modules) incompressible solvers, shallow water, SPH, LBM, level sets, and turbulence models. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from . import advection, boundary_layer, grid, lbm, level_set, multiphase, porous, potential_flow, riemann, shallow_water, sph, stable_fluids, turbulence, vortex +from numeria.cfd.grid import CellField2 as CellField2 +from numeria.cfd.lbm import Collision as Collision +from numeria.cfd.riemann import Cons as Cons +from numeria.cfd.potential_flow import Element as Element +from numeria.cfd.riemann import Euler1D as Euler1D +from numeria.cfd.riemann import Euler2D as Euler2D +from numeria.cfd.riemann import EulerBc as EulerBc +from numeria.cfd.multiphase import FlowPattern as FlowPattern +from numeria.cfd.grid import FluidBc as FluidBc +from numeria.cfd.riemann import FluxKind as FluxKind +from numeria.cfd.level_set import FreeSurfaceFluid2 as FreeSurfaceFluid2 +from numeria.cfd.turbulence import KEpsilon as KEpsilon +from numeria.cfd.turbulence import KEpsilonVariant as KEpsilonVariant +from numeria.cfd.turbulence import KOmegaSst as KOmegaSst +from numeria.cfd.sph import Kernel as Kernel +from numeria.cfd.sph import Kind as Kind +from numeria.cfd.lbm import LbmD2Q9 as LbmD2Q9 +from numeria.cfd.lbm import LbmD3Q19 as LbmD3Q19 +from numeria.cfd.lbm import LbmD3Q27 as LbmD3Q27 +from numeria.cfd.level_set import LevelSet2 as LevelSet2 +from numeria.cfd.level_set import LevelSet3 as LevelSet3 +from numeria.cfd.advection import Limiter as Limiter +from numeria.cfd.grid import MacGrid2 as MacGrid2 +from numeria.cfd.grid import MacGrid3 as MacGrid3 +from numeria.cfd.potential_flow import PanelMethod as PanelMethod +from numeria.cfd.sph import Plane as Plane +from numeria.cfd.potential_flow import Plane2 as Plane2 +from numeria.cfd.potential_flow import PotentialFlow2 as PotentialFlow2 +from numeria.cfd.stable_fluids import PressureSolver as PressureSolver +from numeria.cfd.riemann import Prim as Prim +from numeria.cfd.multiphase import SaturatedFluid as SaturatedFluid +from numeria.cfd.advection import Scheme as Scheme +from numeria.cfd.level_set import Segment2 as Segment2 +from numeria.cfd.shallow_water import ShallowWater2D as ShallowWater2D +from numeria.cfd.turbulence import SpalartAllmaras as SpalartAllmaras +from numeria.cfd.sph import SpatialHash as SpatialHash +from numeria.cfd.sph import Sph as Sph +from numeria.cfd.sph import SphParticle as SphParticle +from numeria.cfd.sph import SphScheme as SphScheme +from numeria.cfd.stable_fluids import StableFluid2 as StableFluid2 +from numeria.cfd.stable_fluids import StableFluid3 as StableFluid3 +from numeria.cfd.porous import VanGenuchten as VanGenuchten +from numeria.cfd.level_set import Vof2 as Vof2 +from numeria.cfd.vortex import VortexKernel as VortexKernel +from numeria.cfd.vortex import VortexMethod2 as VortexMethod2 +from numeria.cfd.vortex import VortexMethod3 as VortexMethod3 +from numeria.cfd.vortex import VortexParticle as VortexParticle +from numeria.cfd.level_set import WenoOrUpwind as WenoOrUpwind +from numeria.cfd.potential_flow import WingGeometry as WingGeometry +from numeria.cfd.potential_flow import added_mass_cylinder as added_mass_cylinder +from numeria.cfd.potential_flow import added_mass_sphere as added_mass_sphere +from numeria.cfd.advection import advect_bfecc_2d as advect_bfecc_2d +from numeria.cfd.advection import advect_flux_limited_2d as advect_flux_limited_2d +from numeria.cfd.advection import advect_lax_wendroff_1d as advect_lax_wendroff_1d +from numeria.cfd.advection import advect_maccormack_2d as advect_maccormack_2d +from numeria.cfd.advection import advect_muscl_1d as advect_muscl_1d +from numeria.cfd.advection import advect_semi_lagrangian_2d as advect_semi_lagrangian_2d +from numeria.cfd.advection import advect_upwind_1d as advect_upwind_1d +from numeria.cfd.advection import advect_upwind_2d as advect_upwind_2d +from numeria.cfd.advection import advect_velocity_semi_lagrangian as advect_velocity_semi_lagrangian +from numeria.cfd.advection import advect_weno5_1d as advect_weno5_1d +from numeria.cfd.advection import advection_diffusion_1d as advection_diffusion_1d +from numeria.cfd.porous import advection_dispersion_1d as advection_dispersion_1d +from numeria.cfd.porous import bioclogging_porosity_change as bioclogging_porosity_change +from numeria.cfd.vortex import biot_savart_ring as biot_savart_ring +from numeria.cfd.vortex import biot_savart_segment as biot_savart_segment +from numeria.cfd.boundary_layer import blasius_cf as blasius_cf +from numeria.cfd.boundary_layer import blasius_drag_plate as blasius_drag_plate +from numeria.cfd.boundary_layer import blasius_profile as blasius_profile +from numeria.cfd.boundary_layer import blasius_solve as blasius_solve +from numeria.cfd.boundary_layer import blasius_thickness as blasius_thickness +from numeria.cfd.riemann import blast_wave_woodward_colella as blast_wave_woodward_colella +from numeria.cfd.multiphase import boiling_heat_flux_rohsenow as boiling_heat_flux_rohsenow +from numeria.cfd.multiphase import breakup_rate_luo_svendsen as breakup_rate_luo_svendsen +from numeria.cfd.porous import brinkman_velocity_profile as brinkman_velocity_profile +from numeria.cfd.porous import brooks_corey as brooks_corey +from numeria.cfd.multiphase import bubble_drag_coefficient as bubble_drag_coefficient +from numeria.cfd.multiphase import bubble_rise_velocity as bubble_rise_velocity +from numeria.cfd.porous import buckley_leverett as buckley_leverett +from numeria.cfd.advection import burgers_exact_cole_hopf as burgers_exact_cole_hopf +from numeria.cfd.advection import burgers_step as burgers_step +from numeria.cfd.vortex import burgers_vortex as burgers_vortex +from numeria.cfd.porous import capillary_pressure_leverett as capillary_pressure_leverett +from numeria.cfd.level_set import capillary_wave_dispersion as capillary_wave_dispersion +from numeria.cfd.porous import carman_kozeny_fibers as carman_kozeny_fibers +from numeria.cfd.multiphase import cavitation_number as cavitation_number +from numeria.cfd.turbulence import channel_flow_dns_reference as channel_flow_dns_reference +from numeria.cfd.multiphase import chisholm as chisholm +from numeria.cfd.multiphase import coalescence_rate_prince_blanch as coalescence_rate_prince_blanch +from numeria.cfd.multiphase import condensation_nusselt_film as condensation_nusselt_film +from numeria.cfd.potential_flow import conformal_map_flow as conformal_map_flow +from numeria.cfd.riemann import cons_to_prim as cons_to_prim +from numeria.cfd.level_set import contact_angle_young as contact_angle_young +from numeria.cfd.boundary_layer import couette_flow as couette_flow +from numeria.cfd.multiphase import critical_heat_flux_zuber as critical_heat_flux_zuber +from numeria.cfd.vortex import crow_instability_growth as crow_instability_growth +from numeria.cfd.potential_flow import cylinder_cp_exact as cylinder_cp_exact +from numeria.cfd.potential_flow import cylinder_flow as cylinder_flow +from numeria.cfd.sph import dam_break_2d as dam_break_2d +from numeria.cfd.sph import dam_break_exact_front as dam_break_exact_front +from numeria.cfd.porous import darcy_flow_rate as darcy_flow_rate +from numeria.cfd.porous import darcy_velocity as darcy_velocity +from numeria.cfd.turbulence import decaying_isotropic_turbulence as decaying_isotropic_turbulence +from numeria.cfd.turbulence import delta_criterion as delta_criterion +from numeria.cfd.porous import dispersion_coefficient as dispersion_coefficient +from numeria.cfd.shallow_water import dispersion_deep as dispersion_deep +from numeria.cfd.shallow_water import dispersion_full as dispersion_full +from numeria.cfd.shallow_water import dispersion_shallow as dispersion_shallow +from numeria.cfd.turbulence import dissipation_rate_from_spectrum as dissipation_rate_from_spectrum +from numeria.cfd.multiphase import drift_flux_velocity as drift_flux_velocity +from numeria.cfd.sph import droplet_oscillation as droplet_oscillation +from numeria.cfd.level_set import droplet_shape_pendant as droplet_shape_pendant +from numeria.cfd.multiphase import droplet_terminal_velocity as droplet_terminal_velocity +from numeria.cfd.porous import dupuit_unconfined as dupuit_unconfined +from numeria.cfd.turbulence import dynamic_smagorinsky_cs as dynamic_smagorinsky_cs +from numeria.cfd.porous import effective_thermal_conductivity_porous as effective_thermal_conductivity_porous +from numeria.cfd.boundary_layer import ekman_depth as ekman_depth +from numeria.cfd.boundary_layer import ekman_spiral as ekman_spiral +from numeria.cfd.potential_flow import elliptic_wing_cl as elliptic_wing_cl +from numeria.cfd.turbulence import energy_spectrum_1d as energy_spectrum_1d +from numeria.cfd.turbulence import energy_spectrum_2d as energy_spectrum_2d +from numeria.cfd.turbulence import energy_spectrum_3d as energy_spectrum_3d +from numeria.cfd.multiphase import eotvos as eotvos +from numeria.cfd.porous import ergun_pressure_drop as ergun_pressure_drop +from numeria.cfd.multiphase import evaporation_rate_hertz_knudsen as evaporation_rate_hertz_knudsen +from numeria.cfd.boundary_layer import falkner_skan_separation_beta as falkner_skan_separation_beta +from numeria.cfd.boundary_layer import falkner_skan_solve as falkner_skan_solve +from numeria.cfd.boundary_layer import first_cell_height as first_cell_height +from numeria.cfd.boundary_layer import flat_plate_heat_transfer_laminar as flat_plate_heat_transfer_laminar +from numeria.cfd.stable_fluids import flow_past_cylinder as flow_past_cylinder +from numeria.cfd.multiphase import flow_pattern_taitel_dukler as flow_pattern_taitel_dukler +from numeria.cfd.multiphase import fluidization_minimum_velocity as fluidization_minimum_velocity +from numeria.cfd.riemann import flux as flux +from numeria.cfd.riemann import flux_ausm_plus as flux_ausm_plus +from numeria.cfd.riemann import flux_hll as flux_hll +from numeria.cfd.riemann import flux_hllc as flux_hllc +from numeria.cfd.riemann import flux_roe as flux_roe +from numeria.cfd.riemann import flux_rusanov as flux_rusanov +from numeria.cfd.porous import forchheimer as forchheimer +from numeria.cfd.multiphase import friedel_correlation as friedel_correlation +from numeria.cfd.shallow_water import gerstner_wave as gerstner_wave +from numeria.cfd.potential_flow import ground_effect_factor as ground_effect_factor +from numeria.cfd.porous import groundwater_flow_2d as groundwater_flow_2d +from numeria.cfd.boundary_layer import head_entrainment_method as head_entrainment_method +from numeria.cfd.vortex import helicity_density as helicity_density +from numeria.cfd.vortex import hill_spherical_vortex as hill_spherical_vortex +from numeria.cfd.multiphase import hindered_settling_exponent as hindered_settling_exponent +from numeria.cfd.porous import hydraulic_conductivity as hydraulic_conductivity +from numeria.cfd.sph import hydrostatic_tank as hydrostatic_tank +from numeria.cfd.potential_flow import induced_drag as induced_drag +from numeria.cfd.turbulence import inertial_range_exponent as inertial_range_exponent +from numeria.cfd.turbulence import integral_scale as integral_scale +from numeria.cfd.potential_flow import inverse_joukowski as inverse_joukowski +from numeria.cfd.riemann import isentropic_vortex_exact as isentropic_vortex_exact +from numeria.cfd.shallow_water import jonswap_spectrum as jonswap_spectrum +from numeria.cfd.potential_flow import joukowski_airfoil as joukowski_airfoil +from numeria.cfd.potential_flow import joukowski_airfoil_flow as joukowski_airfoil_flow +from numeria.cfd.potential_flow import joukowski_transform as joukowski_transform +from numeria.cfd.potential_flow import karman_trefftz_airfoil as karman_trefftz_airfoil +from numeria.cfd.vortex import kelvin_helmholtz_growth_exact as kelvin_helmholtz_growth_exact +from numeria.cfd.shallow_water import kelvin_wake_angle as kelvin_wake_angle +from numeria.cfd.sph import kernel_grad as kernel_grad +from numeria.cfd.sph import kernel_laplacian as kernel_laplacian +from numeria.cfd.sph import kernel_support as kernel_support +from numeria.cfd.sph import kernel_w as kernel_w +from numeria.cfd.turbulence import kolmogorov_scales as kolmogorov_scales +from numeria.cfd.turbulence import kolmogorov_spectrum as kolmogorov_spectrum +from numeria.cfd.vortex import lamb_oseen_velocity as lamb_oseen_velocity +from numeria.cfd.turbulence import lambda2_criterion as lambda2_criterion +from numeria.cfd.boundary_layer import law_of_the_wall as law_of_the_wall +from numeria.cfd.riemann import lax_problem as lax_problem +from numeria.cfd.lbm import lbm_cavity_step as lbm_cavity_step +from numeria.cfd.lbm import lbm_cylinder as lbm_cylinder +from numeria.cfd.lbm import lbm_lid_cavity as lbm_lid_cavity +from numeria.cfd.lbm import lbm_poiseuille_2d as lbm_poiseuille_2d +from numeria.cfd.lbm import lbm_thermal as lbm_thermal +from numeria.cfd.lbm import lbm_to_physical as lbm_to_physical +from numeria.cfd.stable_fluids import lid_driven_cavity as lid_driven_cavity +from numeria.cfd.potential_flow import lifting_line as lifting_line +from numeria.cfd.turbulence import log_law_fit as log_law_fit +from numeria.cfd.multiphase import martinelli_parameter as martinelli_parameter +from numeria.cfd.potential_flow import method_of_images_wall as method_of_images_wall +from numeria.cfd.boundary_layer import michel_transition_criterion as michel_transition_criterion +from numeria.cfd.level_set import minnaert_frequency as minnaert_frequency +from numeria.cfd.multiphase import mixture_density as mixture_density +from numeria.cfd.multiphase import mixture_viscosity_dukler as mixture_viscosity_dukler +from numeria.cfd.multiphase import mixture_viscosity_mcadams as mixture_viscosity_mcadams +from numeria.cfd.multiphase import morton_number as morton_number +from numeria.cfd.stable_fluids import multigrid_vcycle as multigrid_vcycle +from numeria.cfd.potential_flow import naca4 as naca4 +from numeria.cfd.potential_flow import naca5 as naca5 +from numeria.cfd.riemann import normal_shock_relations as normal_shock_relations +from numeria.cfd.riemann import nozzle_area_ratio as nozzle_area_ratio +from numeria.cfd.riemann import nozzle_mach_from_area as nozzle_mach_from_area +from numeria.cfd.riemann import oblique_shock_angle as oblique_shock_angle +from numeria.cfd.porous import ogata_banks as ogata_banks +from numeria.cfd.level_set import ohnesorge as ohnesorge +from numeria.cfd.potential_flow import oswald_efficiency_estimate as oswald_efficiency_estimate +from numeria.cfd.turbulence import pao_spectrum as pao_spectrum +from numeria.cfd.multiphase import particle_response_time as particle_response_time +from numeria.cfd.advection import peclet_cell as peclet_cell +from numeria.cfd.porous import peclet_porous as peclet_porous +from numeria.cfd.porous import permeability_kozeny_carman as permeability_kozeny_carman +from numeria.cfd.shallow_water import pierson_moskowitz as pierson_moskowitz +from numeria.cfd.boundary_layer import pohlhausen_profile as pohlhausen_profile +from numeria.cfd.vortex import point_vortex_hamiltonian as point_vortex_hamiltonian +from numeria.cfd.vortex import point_vortex_step as point_vortex_step +from numeria.cfd.lbm import poiseuille_exact as poiseuille_exact +from numeria.cfd.sph import poiseuille_sph as poiseuille_sph +from numeria.cfd.multiphase import population_balance_1d as population_balance_1d +from numeria.cfd.riemann import prandtl_meyer as prandtl_meyer +from numeria.cfd.stable_fluids import pressure_poisson_cg as pressure_poisson_cg +from numeria.cfd.riemann import prim_to_cons as prim_to_cons +from numeria.cfd.turbulence import q_criterion as q_criterion +from numeria.cfd.riemann import quasi_1d_nozzle as quasi_1d_nozzle +from numeria.cfd.riemann import rankine_hugoniot as rankine_hugoniot +from numeria.cfd.potential_flow import rankine_oval as rankine_oval +from numeria.cfd.vortex import rankine_vortex as rankine_vortex +from numeria.cfd.stable_fluids import rayleigh_benard as rayleigh_benard +from numeria.cfd.level_set import rayleigh_plesset as rayleigh_plesset +from numeria.cfd.turbulence import re_lambda as re_lambda +from numeria.cfd.porous import relative_permeability_corey as relative_permeability_corey +from numeria.cfd.turbulence import reynolds_stress as reynolds_stress +from numeria.cfd.porous import richards_equation_1d as richards_equation_1d +from numeria.cfd.turbulence import richardson_cascade_time as richardson_cascade_time +from numeria.cfd.riemann import riemann_exact as riemann_exact +from numeria.cfd.riemann import riemann_exact_star as riemann_exact_star +from numeria.cfd.advection import rk3_ssp as rk3_ssp +from numeria.cfd.multiphase import rosin_rammler as rosin_rammler +from numeria.cfd.turbulence import rotation_tensor as rotation_tensor +from numeria.cfd.multiphase import sauter_mean_diameter as sauter_mean_diameter +from numeria.cfd.multiphase import sedimentation_richardson_zaki as sedimentation_richardson_zaki +from numeria.cfd.riemann import sedov_1d as sedov_1d +from numeria.cfd.multiphase import settling_velocity as settling_velocity +from numeria.cfd.riemann import shu_osher as shu_osher +from numeria.cfd.level_set import single_vortex_deformation_test as single_vortex_deformation_test +from numeria.cfd.turbulence import smagorinsky_nu_t as smagorinsky_nu_t +from numeria.cfd.riemann import sod_exact as sod_exact +from numeria.cfd.riemann import sod_shock_tube as sod_shock_tube +from numeria.cfd.riemann import sound_speed as sound_speed +from numeria.cfd.boundary_layer import spalding as spalding +from numeria.cfd.multiphase import spray_penetration_hiroyasu as spray_penetration_hiroyasu +from numeria.cfd.shallow_water import stokes_drift as stokes_drift +from numeria.cfd.boundary_layer import stokes_first_problem as stokes_first_problem +from numeria.cfd.multiphase import stokes_number as stokes_number +from numeria.cfd.boundary_layer import stokes_second_problem as stokes_second_problem +from numeria.cfd.turbulence import strain_tensor as strain_tensor +from numeria.cfd.boundary_layer import stratford_separation_criterion as stratford_separation_criterion +from numeria.cfd.vortex import strouhal_from_re as strouhal_from_re +from numeria.cfd.turbulence import structure_function as structure_function +from numeria.cfd.shallow_water import swe_1d_exact_dam_break as swe_1d_exact_dam_break +from numeria.cfd.shallow_water import swe_1d_step_hll as swe_1d_step_hll +from numeria.cfd.turbulence import synthetic_eddy_method as synthetic_eddy_method +from numeria.cfd.turbulence import synthetic_turbulence_kraichnan as synthetic_turbulence_kraichnan +from numeria.cfd.level_set import taylor_bubble_velocity as taylor_bubble_velocity +from numeria.cfd.stable_fluids import taylor_green_exact as taylor_green_exact +from numeria.cfd.stable_fluids import taylor_green_vortex as taylor_green_vortex +from numeria.cfd.turbulence import taylor_microscale as taylor_microscale +from numeria.cfd.porous import theis_drawdown as theis_drawdown +from numeria.cfd.boundary_layer import thermal_bl_ratio as thermal_bl_ratio +from numeria.cfd.lbm import thermal_step as thermal_step +from numeria.cfd.porous import thiem_steady as thiem_steady +from numeria.cfd.potential_flow import thin_airfoil_cl as thin_airfoil_cl +from numeria.cfd.potential_flow import thin_airfoil_cl_flat as thin_airfoil_cl_flat +from numeria.cfd.boundary_layer import thwaites_method as thwaites_method +from numeria.cfd.boundary_layer import thwaites_separation_point as thwaites_separation_point +from numeria.cfd.vortex import tip_vortex_decay as tip_vortex_decay +from numeria.cfd.advection import total_variation as total_variation +from numeria.cfd.boundary_layer import transition_re_x_estimate as transition_re_x_estimate +from numeria.cfd.shallow_water import tsunami_runup_1d as tsunami_runup_1d +from numeria.cfd.turbulence import turbulence_intensity as turbulence_intensity +from numeria.cfd.boundary_layer import turbulent_bl_power_law as turbulent_bl_power_law +from numeria.cfd.boundary_layer import turbulent_cf_prandtl as turbulent_cf_prandtl +from numeria.cfd.boundary_layer import turbulent_cf_schlichting as turbulent_cf_schlichting +from numeria.cfd.turbulence import turbulent_diffusivity as turbulent_diffusivity +from numeria.cfd.boundary_layer import turbulent_thickness_1_7 as turbulent_thickness_1_7 +from numeria.cfd.multiphase import two_phase_pressure_drop_lockhart_martinelli as two_phase_pressure_drop_lockhart_martinelli +from numeria.cfd.turbulence import two_point_correlation as two_point_correlation +from numeria.cfd.boundary_layer import u_tau as u_tau +from numeria.cfd.boundary_layer import van_driest_damping as van_driest_damping +from numeria.cfd.multiphase import void_fraction_drift_flux as void_fraction_drift_flux +from numeria.cfd.multiphase import void_fraction_homogeneous as void_fraction_homogeneous +from numeria.cfd.multiphase import void_fraction_lockhart_martinelli as void_fraction_lockhart_martinelli +from numeria.cfd.turbulence import von_karman_spectrum as von_karman_spectrum +from numeria.cfd.turbulence import vortex_identify_q as vortex_identify_q +from numeria.cfd.potential_flow import vortex_lattice as vortex_lattice +from numeria.cfd.vortex import vortex_line_trace as vortex_line_trace +from numeria.cfd.vortex import vortex_pair_velocity as vortex_pair_velocity +from numeria.cfd.vortex import vortex_ring_self_velocity as vortex_ring_self_velocity +from numeria.cfd.vortex import vortex_shedding_frequency as vortex_shedding_frequency +from numeria.cfd.turbulence import vreman_nu_t as vreman_nu_t +from numeria.cfd.turbulence import wale_nu_t as wale_nu_t +from numeria.cfd.shallow_water import wave_breaking_criterion as wave_breaking_criterion +from numeria.cfd.shallow_water import wave_field_from_spectrum as wave_field_from_spectrum +from numeria.cfd.shallow_water import wave_speed_shallow as wave_speed_shallow +from numeria.cfd.riemann import wave_speeds_einfeldt as wave_speeds_einfeldt +from numeria.cfd.level_set import weber_breakup_regime as weber_breakup_regime +from numeria.cfd.advection import weno5_reconstruct as weno5_reconstruct +from numeria.cfd.boundary_layer import y_plus as y_plus +from numeria.cfd.level_set import young_laplace_pressure as young_laplace_pressure +from numeria.cfd.level_set import zalesak_disk as zalesak_disk +from numeria.cfd.level_set import zalesak_rotate as zalesak_rotate + + diff --git a/bindings/python/python/numeria/cfd/advection.pyi b/bindings/python/python/numeria/cfd/advection.pyi new file mode 100644 index 0000000..faa0273 --- /dev/null +++ b/bindings/python/python/numeria/cfd/advection.pyi @@ -0,0 +1,172 @@ +""" +Advection schemes: classic 1D finite-volume methods (upwind, Lax-Wendroff, MUSCL with slope limiters, WENO5), 2D semi-Lagrangian transport with BFECC/MacCormack error compensation, SSP-RK3, Burgers solvers, and the Cole-Hopf exact solution. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.cfd.grid import CellField2 +from numeria.cfd.grid import MacGrid2 + +class Limiter: + """ +Slope limiters for MUSCL-type schemes. + +Rust: `cfd::advection::Limiter` + """ + ... + +class Scheme: + """ +Spatial scheme selector for Burgers / advection-diffusion steps. + +Rust: `cfd::advection::Scheme` + """ + ... + +def advect_upwind_1d(q: list[float], u: float, dx: float, dt: float) -> list[float]: + """ +First-order upwind advection of a periodic 1D field by constant +velocity `u` for one step. + +Rust: `cfd::advection::advect_upwind_1d` + """ + ... + +def advect_lax_wendroff_1d(q: list[float], u: float, dx: float, dt: float) -> list[float]: + """ +Second-order Lax-Wendroff advection (dispersive near discontinuities). + +Rust: `cfd::advection::advect_lax_wendroff_1d` + """ + ... + +def advect_muscl_1d(q: list[float], u: float, dx: float, dt: float, limiter: Limiter) -> list[float]: + """ +MUSCL advection with a slope limiter (TVD for Minmod/VanLeer/…). + +Rust: `cfd::advection::advect_muscl_1d` + """ + ... + +def weno5_reconstruct(q: list[float]) -> tuple[list[float], list[float]]: + """ +WENO5 reconstruction of face states at every i+1/2: returns +(left-biased, right-biased) values, both length n (periodic). + +Rust: `cfd::advection::weno5_reconstruct` + """ + ... + +def advect_weno5_1d(q: list[float], u: float, dx: float, dt: float) -> list[float]: + """ +WENO5 upwind advection (Euler step in time). + +Rust: `cfd::advection::advect_weno5_1d` + """ + ... + +def advect_semi_lagrangian_2d(q: CellField2, grid: MacGrid2, dt: float) -> CellField2: + """ +Semi-Lagrangian advection of a cell field through a MAC velocity +field (RK2 backtrace, bilinear sampling). Unconditionally stable. + +Rust: `cfd::advection::advect_semi_lagrangian_2d` + """ + ... + +def advect_bfecc_2d(q: CellField2, grid: MacGrid2, dt: float) -> CellField2: + """ +Back-and-forth error compensation and correction (BFECC): second +order, limited to the local min/max to avoid new extrema. + +Rust: `cfd::advection::advect_bfecc_2d` + """ + ... + +def advect_maccormack_2d(q: CellField2, grid: MacGrid2, dt: float) -> CellField2: + """ +Unsplit MacCormack advection with min/max limiting. + +Rust: `cfd::advection::advect_maccormack_2d` + """ + ... + +def advect_upwind_2d(q: CellField2, grid: MacGrid2, dt: float) -> CellField2: + """ +First-order upwind advection on the 2D grid using face velocities. + +Rust: `cfd::advection::advect_upwind_2d` + """ + ... + +def advect_velocity_semi_lagrangian(grid: MacGrid2, dt: float) -> None: + """ +Advect the MAC velocity field itself semi-Lagrangianly (each face +component backtraced from its own staggered position). + +Rust: `cfd::advection::advect_velocity_semi_lagrangian` + """ + ... + +def advect_flux_limited_2d(q: CellField2, grid: MacGrid2, dt: float, limiter: Limiter) -> CellField2: + """ +Dimensionally split flux-limited (MUSCL) advection on the 2D grid. + +Rust: `cfd::advection::advect_flux_limited_2d` + """ + ... + +def rk3_ssp(q: list[float], rhs: Callable[[list[float]], list[float]], dt: float) -> list[float]: + """ +Strong-stability-preserving third-order Runge-Kutta step for +dq/dt = rhs(q). + +Rust: `cfd::advection::rk3_ssp` + """ + ... + +def burgers_step(u: list[float], dx: float, dt: float, nu: float, scheme: Scheme) -> list[float]: + """ +One explicit step of viscous Burgers u_t + (u²/2)_x = ν u_xx on a +periodic domain. + +Rust: `cfd::advection::burgers_step` + """ + ... + +def burgers_exact_cole_hopf(x: float, t: float, nu: float, u0: Callable[[float], float]) -> float: + """ +Exact viscous Burgers solution by the Cole-Hopf transform: +u(x,t) = ∫ ((x−y)/t) e^{−G/2ν} dy / ∫ e^{−G/2ν} dy with +G(y) = (x−y)²/(2t) + ∫₀^y u₀. + +Rust: `cfd::advection::burgers_exact_cole_hopf` + """ + ... + +def advection_diffusion_1d(q: list[float], u: float, d: float, dx: float, dt: float, scheme: Scheme) -> list[float]: + """ +One explicit step of 1D advection-diffusion q_t + u q_x = D q_xx. + +Rust: `cfd::advection::advection_diffusion_1d` + """ + ... + +def peclet_cell(u: float, dx: float, d: float) -> float: + """ +Cell Péclet number u dx / D. + +Rust: `cfd::advection::peclet_cell` + """ + ... + +def total_variation(q: list[float]) -> float: + """ +Total variation Σ |q_{i+1} − q_i| (periodic). + +Rust: `cfd::advection::total_variation` + """ + ... diff --git a/bindings/python/python/numeria/cfd/boundary_layer.pyi b/bindings/python/python/numeria/cfd/boundary_layer.pyi new file mode 100644 index 0000000..51c4d6f --- /dev/null +++ b/bindings/python/python/numeria/cfd/boundary_layer.pyi @@ -0,0 +1,268 @@ +""" +Boundary layers: Blasius and Falkner-Skan similarity solutions (shooting), Thwaites and Head integral methods, turbulent wall laws, transition and separation criteria, rotating and oscillating layers, and flat-plate heat transfer. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.math import Vec2 + +def blasius_solve(eta_max: float, n: int) -> list[tuple[float, float, float, float]]: + """ +Blasius flat-plate similarity solution: rows (η, f, f′, f″) with +f‴ + ½ f f″ = 0, solved by RK4 shooting on f″(0). + +Rust: `cfd::boundary_layer::blasius_solve` + """ + ... + +def blasius_profile(y: float, x: float, u_inf: float, nu: float) -> float: + """ +Blasius streamwise velocity u(y) at station x. + +Rust: `cfd::boundary_layer::blasius_profile` + """ + ... + +def blasius_thickness(x: float, u_inf: float, nu: float) -> tuple[float, float, float]: + """ +Blasius thicknesses (δ99, δ*, θ) at station x. + +Rust: `cfd::boundary_layer::blasius_thickness` + """ + ... + +def blasius_cf(x: float, u_inf: float, nu: float) -> float: + """ +Local Blasius skin friction 0.664/√Re_x. + +Rust: `cfd::boundary_layer::blasius_cf` + """ + ... + +def blasius_drag_plate(l: float, width: float, u_inf: float, nu: float, rho: float) -> float: + """ +Total laminar drag of one side of a flat plate. + +Rust: `cfd::boundary_layer::blasius_drag_plate` + """ + ... + +def falkner_skan_solve(beta: float, eta_max: float, n: int) -> list[tuple[float, float, float]]: + """ +Falkner-Skan wedge-flow similarity: rows (η, f, f′) for +f‴ + f f″ + β(1 − f′²) = 0. + +Rust: `cfd::boundary_layer::falkner_skan_solve` + """ + ... + +def falkner_skan_separation_beta() -> float: + """ +Falkner-Skan separation parameter β = −0.1988. + +Rust: `cfd::boundary_layer::falkner_skan_separation_beta` + """ + ... + +def thwaites_method(u_e: Callable[[float], float], x: list[float], nu: float) -> list[tuple[float, float, float, float]]: + """ +Thwaites integral method along stations `x` with edge velocity +`u_e(x)`: rows (θ, λ, H, cf). + +Rust: `cfd::boundary_layer::thwaites_method` + """ + ... + +def thwaites_separation_point(u_e: Callable[[float], float], x: list[float], nu: float) -> Optional[float]: + """ +First station where Thwaites' λ drops below −0.09 (separation). + +Rust: `cfd::boundary_layer::thwaites_separation_point` + """ + ... + +def pohlhausen_profile(eta: float, lambda_: float) -> float: + """ +Pohlhausen quartic velocity profile u/U at η = y/δ with shape +parameter λ. + +Rust: `cfd::boundary_layer::pohlhausen_profile` + """ + ... + +def turbulent_bl_power_law(y: float, delta: float, n: float) -> float: + """ +Turbulent 1/n power-law profile. + +Rust: `cfd::boundary_layer::turbulent_bl_power_law` + """ + ... + +def turbulent_cf_prandtl(re_x: float) -> float: + """ +Local turbulent skin friction (Prandtl 1/5-power law) 0.0592 Re⁻⅕. + +Rust: `cfd::boundary_layer::turbulent_cf_prandtl` + """ + ... + +def turbulent_cf_schlichting(re_x: float) -> float: + """ +Schlichting's local turbulent skin friction (2 log₁₀Re − 0.65)⁻²·³. + +Rust: `cfd::boundary_layer::turbulent_cf_schlichting` + """ + ... + +def turbulent_thickness_1_7(x: float, re_x: float) -> float: + """ +Turbulent boundary-layer thickness δ = 0.37 x / Re_x^{1/5}. + +Rust: `cfd::boundary_layer::turbulent_thickness_1_7` + """ + ... + +def law_of_the_wall(y_plus: float, kappa: float, b: float) -> float: + """ +Logarithmic law of the wall u⁺ = ln(y⁺)/κ + B. + +Rust: `cfd::boundary_layer::law_of_the_wall` + """ + ... + +def spalding(y_plus: float) -> float: + """ +Spalding's composite wall profile: u⁺(y⁺) by inverting +y⁺ = u⁺ + e^{−κB}(e^{κu⁺} − 1 − κu⁺ − (κu⁺)²/2 − (κu⁺)³/6). + +Rust: `cfd::boundary_layer::spalding` + """ + ... + +def van_driest_damping(y_plus: float, a: float) -> float: + """ +Van Driest near-wall damping 1 − e^{−y⁺/A}. + +Rust: `cfd::boundary_layer::van_driest_damping` + """ + ... + +def y_plus(y: float, u_tau: float, nu: float) -> float: + """ +Wall coordinate y⁺ = y u_τ/ν. + +Rust: `cfd::boundary_layer::y_plus` + """ + ... + +def u_tau(tau_w: float, rho: float) -> float: + """ +Friction velocity √(τ_w/ρ). + +Rust: `cfd::boundary_layer::u_tau` + """ + ... + +def first_cell_height(y_plus_target: float, u_inf: float, nu: float, re_l: float, l: float) -> float: + """ +First-cell height for a target y⁺ on a plate of length `l` +(turbulent flat-plate friction estimate). + +Rust: `cfd::boundary_layer::first_cell_height` + """ + ... + +def transition_re_x_estimate(turbulence_intensity: float) -> float: + """ +Mayle-style transition estimate: Re_θt ≈ 400 Ti^{−5/8} (Ti in +percent), converted to Re_x with the Blasius relation θ = +0.664 x/√Re_x. + +Rust: `cfd::boundary_layer::transition_re_x_estimate` + """ + ... + +def michel_transition_criterion(re_theta: float, re_x: float) -> bool: + """ +Michel's transition criterion: Re_θ > 1.174 (1 + 22400/Re_x) Re_x^0.46. + +Rust: `cfd::boundary_layer::michel_transition_criterion` + """ + ... + +def head_entrainment_method(u_e: Callable[[float], float], x: list[float], nu: float, theta0: float, h0: float) -> list[tuple[float, float, float]]: + """ +Head's entrainment integral method for turbulent boundary layers: +rows (θ, H, cf) marched along `x`. + +Rust: `cfd::boundary_layer::head_entrainment_method` + """ + ... + +def stratford_separation_criterion(cp: float, x: float, dcp_dx: float, re_x: float) -> bool: + """ +Stratford's turbulent separation criterion: +Cp √(x dCp/dx) ≥ 0.39 (10⁻⁶ Re_x)^{0.1}. + +Rust: `cfd::boundary_layer::stratford_separation_criterion` + """ + ... + +def ekman_spiral(z: float, u_g: float, f: float, nu: float) -> Vec2: + """ +Ekman spiral velocity (u, v) at height z for geostrophic wind `u_g`. + +Rust: `cfd::boundary_layer::ekman_spiral` + """ + ... + +def ekman_depth(nu: float, f: float) -> float: + """ +Ekman layer depth π√(2ν/f). + +Rust: `cfd::boundary_layer::ekman_depth` + """ + ... + +def stokes_second_problem(y: float, t: float, u0: float, omega: float, nu: float) -> float: + """ +Stokes' second problem (oscillating plate): u(y, t). + +Rust: `cfd::boundary_layer::stokes_second_problem` + """ + ... + +def stokes_first_problem(y: float, t: float, u0: float, nu: float) -> float: + """ +Stokes' first problem (impulsively started plate): u = u0 erfc(η). + +Rust: `cfd::boundary_layer::stokes_first_problem` + """ + ... + +def couette_flow(y: float, h: float, u_wall: float, dp_dx: float, mu: float) -> float: + """ +Plane Couette-Poiseuille flow u(y) between plates 0 and h. + +Rust: `cfd::boundary_layer::couette_flow` + """ + ... + +def flat_plate_heat_transfer_laminar(re_x: float, pr: float) -> float: + """ +Laminar flat-plate local Nusselt number 0.332 Re_x^½ Pr^⅓. + +Rust: `cfd::boundary_layer::flat_plate_heat_transfer_laminar` + """ + ... + +def thermal_bl_ratio(pr: float) -> float: + """ +Thermal to velocity boundary-layer thickness ratio ≈ Pr^{−1/3}. + +Rust: `cfd::boundary_layer::thermal_bl_ratio` + """ + ... diff --git a/bindings/python/python/numeria/cfd/grid.pyi b/bindings/python/python/numeria/cfd/grid.pyi new file mode 100644 index 0000000..b7a5127 --- /dev/null +++ b/bindings/python/python/numeria/cfd/grid.pyi @@ -0,0 +1,113 @@ +""" +Staggered (MAC) grids and cell-centered scalar fields for incompressible flow solvers. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.math import Vec2 +from numeria.math import Vec3 + +class CellField2: + """ +Cell-centered scalar field on the same layout as `MacGrid2` +pressure cells; node (i, j) sits at world ((i+0.5) dx, (j+0.5) dx). + +Rust: `cfd::grid::CellField2` + """ + def __init__(self, nx: int, ny: int, dx: float) -> None: ... + @staticmethod + def from_fn(nx: int, ny: int, dx: float, f: Callable[[float, float], float]) -> CellField2: ... + def at(self, i: int, j: int) -> float: ... + def sample(self, p: Vec2 | Sequence[float]) -> float: ... + def sample_cubic(self, p: Vec2 | Sequence[float]) -> float: ... + @property + def nx(self) -> int: ... + @property + def ny(self) -> int: ... + @property + def dx(self) -> float: ... + @property + def data(self) -> list[float]: ... + +class FluidBc: + """ +Boundary condition for the velocity field. + +Rust: `cfd::grid::FluidBc` + """ + ... + +class MacGrid2: + """ +2D marker-and-cell grid: `u` on vertical faces ((nx+1) × ny), `v` on +horizontal faces (nx × (ny+1)), pressure and solid flags at cell +centers. Cell (i, j) spans [i·dx, (i+1)·dx) × [j·dx, (j+1)·dx). + +Rust: `cfd::grid::MacGrid2` + """ + def __init__(self, nx: int, ny: int, dx: float) -> None: ... + def u_idx(self, i: int, j: int) -> int: ... + def v_idx(self, i: int, j: int) -> int: ... + def c_idx(self, i: int, j: int) -> int: ... + def u_at(self, i: int, j: int) -> float: ... + def v_at(self, i: int, j: int) -> float: ... + def velocity_at(self, p: Vec2 | Sequence[float]) -> Vec2: ... + def divergence(self) -> list[float]: ... + def curl(self) -> list[float]: ... + def max_velocity(self) -> float: ... + def cfl_dt(self, cfl: float) -> float: ... + def set_solid_box(self, x0: float, y0: float, x1: float, y1: float) -> None: ... + def set_solid_circle(self, cx: float, cy: float, r: float) -> None: ... + def apply_bc(self, bc: FluidBc) -> None: ... + def kinetic_energy(self) -> float: ... + def enstrophy(self) -> float: ... + @property + def nx(self) -> int: ... + @property + def ny(self) -> int: ... + @property + def dx(self) -> float: ... + @property + def u(self) -> list[float]: ... + @property + def v(self) -> list[float]: ... + @property + def p(self) -> list[float]: ... + @property + def solid(self) -> list[bool]: ... + +class MacGrid3: + """ +3D MAC grid (faces staggered per axis). + +Rust: `cfd::grid::MacGrid3` + """ + def __init__(self, nx: int, ny: int, nz: int, dx: float) -> None: ... + def u_at(self, i: int, j: int, k: int) -> float: ... + def v_at(self, i: int, j: int, k: int) -> float: ... + def w_at(self, i: int, j: int, k: int) -> float: ... + def velocity_at(self, p: Vec3 | Sequence[float]) -> Vec3: ... + def divergence(self) -> list[float]: ... + def max_velocity(self) -> float: ... + def cfl_dt(self, cfl: float) -> float: ... + @property + def nx(self) -> int: ... + @property + def ny(self) -> int: ... + @property + def nz(self) -> int: ... + @property + def dx(self) -> float: ... + @property + def u(self) -> list[float]: ... + @property + def v(self) -> list[float]: ... + @property + def w(self) -> list[float]: ... + @property + def p(self) -> list[float]: ... + @property + def solid(self) -> list[bool]: ... diff --git a/bindings/python/python/numeria/cfd/lbm.pyi b/bindings/python/python/numeria/cfd/lbm.pyi new file mode 100644 index 0000000..ac30fff --- /dev/null +++ b/bindings/python/python/numeria/cfd/lbm.pyi @@ -0,0 +1,168 @@ +""" +Lattice Boltzmann method: D2Q9 with BGK/TRT/MRT/cumulant-style collisions, bounce-back solids, Zou-He open boundaries, Guo forcing, D3Q19 and D3Q27 lattices, and classic benchmarks. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.astrophysics.nbody import Body +from numeria.math import Vec2 +from numeria.math import Vec3 + +class Collision: + """ +Collision operator selector. + +Rust: `cfd::lbm::Collision` + """ + ... + +class LbmD2Q9: + """ +D2Q9 lattice Boltzmann solver. + +Rust: `cfd::lbm::LbmD2Q9` + """ + def __init__(self, nx: int, ny: int, tau: float) -> None: ... + def init_equilibrium(self, rho: float, u: Vec2 | Sequence[float]) -> None: ... + def viscosity(self) -> float: ... + def density(self) -> list[float]: ... + def velocity(self) -> list[Vec2]: ... + def mach_max(self) -> float: ... + def collide(self) -> None: ... + def stream(self) -> None: ... + def bounce_back(self) -> None: ... + def zou_he_velocity_inlet(self, u: float) -> None: ... + def zou_he_pressure_outlet(self, rho: float) -> None: ... + def periodic(self) -> None: ... + def step(self) -> None: ... + def run(self, n: int) -> None: ... + def drag_on_solid(self) -> Vec2: ... + def vorticity(self) -> list[float]: ... + @property + def nx(self) -> int: ... + @property + def ny(self) -> int: ... + @property + def f(self) -> list[list[float]]: ... + @property + def tau(self) -> float: ... + @property + def solid(self) -> list[bool]: ... + @property + def force(self) -> Vec2: ... + @property + def collision(self) -> Collision: ... + @property + def periodic_x(self) -> bool: ... + @property + def periodic_y(self) -> bool: ... + +class LbmD3Q19: + """ +D3Q19 BGK lattice Boltzmann solver (periodic + bounce-back). + +Rust: `cfd::lbm::LbmD3Q19` + """ + def __init__(self, nx: int, ny: int, nz: int, tau: float) -> None: ... + def viscosity(self) -> float: ... + def velocity(self) -> list[Vec3]: ... + def step(self) -> None: ... + @property + def nx(self) -> int: ... + @property + def ny(self) -> int: ... + @property + def nz(self) -> int: ... + @property + def f(self) -> list[list[float]]: ... + @property + def tau(self) -> float: ... + @property + def solid(self) -> list[bool]: ... + @property + def force(self) -> Vec3: ... + +class LbmD3Q27: + """ +D3Q27 lattice constants (velocities and weights), for custom solvers. + +Rust: `cfd::lbm::LbmD3Q27` + """ + @staticmethod + def velocities() -> list[tuple[int, int, int]]: ... + @staticmethod + def weight(e: tuple[int, int, int]) -> float: ... + +def lbm_poiseuille_2d(nx: int, ny: int, tau: float, force: float) -> LbmD2Q9: + """ +Body-force-driven Poiseuille channel: solid walls at j = 0 and +j = ny−1, periodic in x. + +Rust: `cfd::lbm::lbm_poiseuille_2d` + """ + ... + +def poiseuille_exact(y: float, h: float, force: float, nu: float) -> float: + """ +Exact Poiseuille profile u(y) for channel half-width walls at y = 0 +and y = h. + +Rust: `cfd::lbm::poiseuille_exact` + """ + ... + +def lbm_cylinder(nx: int, ny: int, re: float) -> LbmD2Q9: + """ +Flow past a cylinder at Reynolds number `re` (Zou-He inlet/outlet). + +Rust: `cfd::lbm::lbm_cylinder` + """ + ... + +def lbm_lid_cavity(n: int, re: float) -> LbmD2Q9: + """ +Lid-driven cavity at Reynolds number `re` (moving top wall via +bounce-back with wall velocity). + +Rust: `cfd::lbm::lbm_lid_cavity` + """ + ... + +def lbm_cavity_step(lbm: LbmD2Q9, u_lid: float) -> None: + """ +Apply the moving-lid boundary to a cavity solver for one step: after +the regular step, impose the lid velocity on the top row (Zou-He). + +Rust: `cfd::lbm::lbm_cavity_step` + """ + ... + +def lbm_thermal(nx: int, ny: int, tau_f: float, tau_g: float) -> tuple[LbmD2Q9, LbmD2Q9]: + """ +Double-distribution thermal LBM: returns (flow lattice, temperature +lattice); the temperature field advects with the flow and feeds back +as a Boussinesq force. Step both manually with `thermal_step`. + +Rust: `cfd::lbm::lbm_thermal` + """ + ... + +def thermal_step(flow: LbmD2Q9, temp: LbmD2Q9, buoyancy: float) -> None: + """ +One coupled Boussinesq step of the double-distribution system. + +Rust: `cfd::lbm::thermal_step` + """ + ... + +def lbm_to_physical(u_lattice: float, dx: float, dt: float) -> float: + """ +Convert a lattice velocity to physical units given the lattice +spacing and time step. + +Rust: `cfd::lbm::lbm_to_physical` + """ + ... diff --git a/bindings/python/python/numeria/cfd/level_set.pyi b/bindings/python/python/numeria/cfd/level_set.pyi new file mode 100644 index 0000000..ce95e1e --- /dev/null +++ b/bindings/python/python/numeria/cfd/level_set.pyi @@ -0,0 +1,225 @@ +""" +Interface capturing: level sets (upwind/WENO advection, Sussman reinitialization, fast marching, marching squares/tetrahedra), volume of fluid with PLIC, a simple free-surface fluid, and bubble/droplet physics relations. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.cfd.grid import CellField2 +from numeria.cfd.grid import MacGrid2 +from numeria.math import Vec2 +from numeria.math import Vec3 + +class FreeSurfaceFluid2: + """ +Level-set free-surface liquid on a stable-fluids solver (CSF surface +tension, gravity restricted to the liquid). + +Rust: `cfd::level_set::FreeSurfaceFluid2` + """ + def __init__(self, nx: int, ny: int, dx: float) -> None: ... + def step(self, dt: float) -> None: ... + @staticmethod + def dam_break(nx: int, ny: int, dx: float) -> FreeSurfaceFluid2: ... + @staticmethod + def droplet_fall(nx: int, ny: int, dx: float) -> FreeSurfaceFluid2: ... + @staticmethod + def rising_bubble(nx: int, ny: int, dx: float) -> FreeSurfaceFluid2: ... + @staticmethod + def sloshing_tank(nx: int, ny: int, dx: float, amplitude: float, omega: float) -> FreeSurfaceFluid2: ... + @property + def surface_tension(self) -> float: ... + @property + def density_ratio(self) -> float: ... + +class LevelSet2: + """ +2D signed distance level set (φ < 0 inside). + +Rust: `cfd::level_set::LevelSet2` + """ + def __init__(self, phi: CellField2, band: Optional[float]) -> None: ... + @staticmethod + def from_sdf(f: Callable[[float, float], float], nx: int, ny: int, dx: float) -> LevelSet2: ... + @staticmethod + def circle(nx: int, ny: int, dx: float, cx: float, cy: float, r: float) -> LevelSet2: ... + @staticmethod + def box_(nx: int, ny: int, dx: float, x0: float, y0: float, x1: float, y1: float) -> LevelSet2: ... + def advect(self, grid: MacGrid2, dt: float, scheme: WenoOrUpwind) -> None: ... + def reinitialize(self, iters: int) -> None: ... + def fast_marching(self) -> None: ... + def curvature(self) -> CellField2: ... + def normal(self, p: Vec2 | Sequence[float]) -> Vec2: ... + def area(self) -> float: ... + def perimeter(self) -> float: ... + def interface_segments(self) -> list[Segment2]: ... + def heaviside(self, eps: float) -> CellField2: ... + def delta(self, eps: float) -> CellField2: ... + def union(self, other: LevelSet2) -> None: ... + def intersect(self, other: LevelSet2) -> None: ... + def subtract(self, other: LevelSet2) -> None: ... + def extend_velocity(self, vel: CellField2, band: float) -> None: ... + def volume_correction(self, target_area: float) -> None: ... + @property + def phi(self) -> CellField2: ... + @property + def band(self) -> Optional[float]: ... + +class LevelSet3: + """ +3D level set with mesh extraction. + +Rust: `cfd::level_set::LevelSet3` + """ + def __init__(self, phi: ScalarField3, band: Optional[float]) -> None: ... + @staticmethod + def from_sdf(f: Callable[[float, float, float], float], n: int, dx: float) -> LevelSet3: ... + @staticmethod + def sphere(n: int, dx: float, c: Vec3 | Sequence[float], r: float) -> LevelSet3: ... + def to_mesh(self) -> Mesh: ... + @property + def phi(self) -> ScalarField3: ... + @property + def band(self) -> Optional[float]: ... + +class Segment2: + """ +A line segment of the reconstructed interface. + +Rust: `cfd::level_set::Segment2` + """ + def __init__(self, a: Vec2 | Sequence[float], b: Vec2 | Sequence[float]) -> None: ... + @property + def a(self) -> Vec2: ... + @property + def b(self) -> Vec2: ... + +class Vof2: + """ +Volume-of-fluid interface tracking with PLIC reconstruction. + +Rust: `cfd::level_set::Vof2` + """ + def __init__(self, fraction: CellField2) -> None: ... + @staticmethod + def init_from_sdf(f: Callable[[float, float], float], nx: int, ny: int, dx: float) -> Vof2: ... + def reconstruct_normals_youngs(self) -> list[Vec2]: ... + def reconstruct_normals_elvira(self) -> list[Vec2]: ... + def advect_plic(self, grid: MacGrid2, dt: float) -> None: ... + def interface_segments(self) -> list[Segment2]: ... + def total_volume(self) -> float: ... + def curvature_height_function(self) -> list[float]: ... + @property + def fraction(self) -> CellField2: ... + +class WenoOrUpwind: + """ +Advection scheme for the level set. + +Rust: `cfd::level_set::WenoOrUpwind` + """ + ... + +def zalesak_disk(n: int) -> LevelSet2: + """ +Zalesak's slotted disk on an n × n unit grid. + +Rust: `cfd::level_set::zalesak_disk` + """ + ... + +def zalesak_rotate(ls: LevelSet2, revolutions: float) -> float: + """ +Rigidly rotate a level set about the domain center for the given +revolutions; returns the relative area error. + +Rust: `cfd::level_set::zalesak_rotate` + """ + ... + +def single_vortex_deformation_test(n: int, t_period: float) -> float: + """ +Single-vortex deformation test (LeVeque): stretch for t_period/2, +reverse, and return the relative area error at the end. + +Rust: `cfd::level_set::single_vortex_deformation_test` + """ + ... + +def rayleigh_plesset(r0: float, p_inf: Callable[[float], float], p_v: float, sigma: float, mu: float, rho: float, t_end: float, dt: float) -> list[tuple[float, float, float]]: + """ +Rayleigh-Plesset bubble dynamics: returns (t, R, Ṙ) samples +(RK4, incompressible liquid). + +Rust: `cfd::level_set::rayleigh_plesset` + """ + ... + +def minnaert_frequency(r: float, p: float, rho: float, gamma: float) -> float: + """ +Minnaert resonance frequency of a gas bubble. + +Rust: `cfd::level_set::minnaert_frequency` + """ + ... + +def weber_breakup_regime(we: float) -> str: + """ +Droplet breakup regime by Weber number. + +Rust: `cfd::level_set::weber_breakup_regime` + """ + ... + +def ohnesorge(mu: float, rho: float, sigma: float, l: float) -> float: + """ +Ohnesorge number μ/√(ρσL). + +Rust: `cfd::level_set::ohnesorge` + """ + ... + +def capillary_wave_dispersion(k: float, sigma: float, rho: float, g: float) -> float: + """ +Deep-water gravity-capillary dispersion ω = √(gk + σk³/ρ). + +Rust: `cfd::level_set::capillary_wave_dispersion` + """ + ... + +def young_laplace_pressure(sigma: float, r1: float, r2: float) -> float: + """ +Young-Laplace pressure jump σ(1/R₁ + 1/R₂). + +Rust: `cfd::level_set::young_laplace_pressure` + """ + ... + +def contact_angle_young(sigma_sv: float, sigma_sl: float, sigma_lv: float) -> float: + """ +Young's contact angle from the interfacial tensions. + +Rust: `cfd::level_set::contact_angle_young` + """ + ... + +def droplet_shape_pendant(b: float, beta: float, n: int) -> list[Vec2]: + """ +Pendant droplet profile from the axisymmetric Young-Laplace +equations (Bashforth-Adams): apex radius of curvature `b`, capillary +shape factor `beta` = Δρ g b²/σ; returns (r, z) points hanging below +the apex. + +Rust: `cfd::level_set::droplet_shape_pendant` + """ + ... + +def taylor_bubble_velocity(d: float, g: float) -> float: + """ +Taylor bubble (slug) rise velocity 0.35 √(g D). + +Rust: `cfd::level_set::taylor_bubble_velocity` + """ + ... diff --git a/bindings/python/python/numeria/cfd/multiphase.pyi b/bindings/python/python/numeria/cfd/multiphase.pyi new file mode 100644 index 0000000..e971117 --- /dev/null +++ b/bindings/python/python/numeria/cfd/multiphase.pyi @@ -0,0 +1,353 @@ +""" +Multiphase flow correlations: mixture properties, drift-flux and void fraction models, two-phase pressure drop, flow-pattern maps, bubble and droplet dynamics, population balance, boiling and condensation, sprays, and dispersed-particle transport. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.sim.cloth_sim import Particle + +class FlowPattern: + """ +Horizontal / near-horizontal two-phase flow patterns. + +Rust: `cfd::multiphase::FlowPattern` + """ + ... + +class SaturatedFluid: + """ +Saturated-fluid property bundle for boiling correlations. + +Rust: `cfd::multiphase::SaturatedFluid` + """ + def __init__(self, mu_l: float, h_fg: float, rho_l: float, rho_g: float, sigma: float, cp_l: float, pr_l: float) -> None: ... + @staticmethod + def water_1atm() -> SaturatedFluid: ... + @property + def mu_l(self) -> float: ... + @property + def h_fg(self) -> float: ... + @property + def rho_l(self) -> float: ... + @property + def rho_g(self) -> float: ... + @property + def sigma(self) -> float: ... + @property + def cp_l(self) -> float: ... + @property + def pr_l(self) -> float: ... + +def mixture_density(alpha: float, rho_g: float, rho_l: float) -> float: + """ +Mixture density rho_m = alpha rho_g + (1 - alpha) rho_l for void +fraction `alpha`. + +Rust: `cfd::multiphase::mixture_density` + """ + ... + +def mixture_viscosity_mcadams(x: float, mu_g: float, mu_l: float) -> float: + """ +McAdams homogeneous mixture viscosity from quality `x`: +1/mu_m = x/mu_g + (1-x)/mu_l. + +Rust: `cfd::multiphase::mixture_viscosity_mcadams` + """ + ... + +def mixture_viscosity_dukler(x: float, rho_g: float, rho_l: float, mu_g: float, mu_l: float) -> float: + """ +Dukler mixture viscosity: mu_m = rho_m (x mu_g/rho_g + (1-x) mu_l/rho_l). + +Rust: `cfd::multiphase::mixture_viscosity_dukler` + """ + ... + +def drift_flux_velocity(j_g: float, j_l: float, c0: float, v_gj: float) -> float: + """ +Drift-flux gas velocity v_g = C0 j + v_gj with total superficial +velocity j = j_g + j_l. + +Rust: `cfd::multiphase::drift_flux_velocity` + """ + ... + +def void_fraction_homogeneous(x: float, rho_g: float, rho_l: float) -> float: + """ +Homogeneous (no-slip) void fraction from quality `x`. + +Rust: `cfd::multiphase::void_fraction_homogeneous` + """ + ... + +def void_fraction_drift_flux(j_g: float, j_l: float, c0: float, v_gj: float) -> float: + """ +Drift-flux void fraction alpha = j_g / (C0 j + v_gj). + +Rust: `cfd::multiphase::void_fraction_drift_flux` + """ + ... + +def void_fraction_lockhart_martinelli(x: float, rho_g: float, rho_l: float, mu_g: float, mu_l: float) -> float: + """ +Lockhart-Martinelli void fraction (turbulent-turbulent): +alpha = 1 - 1/sqrt(1 + 20/X + 1/X^2) with the Martinelli parameter from +quality and fluid properties. + +Rust: `cfd::multiphase::void_fraction_lockhart_martinelli` + """ + ... + +def martinelli_parameter(x: float, rho_g: float, rho_l: float, mu_g: float, mu_l: float) -> float: + """ +Turbulent-turbulent Martinelli parameter +X_tt = ((1-x)/x)^0.9 (rho_g/rho_l)^0.5 (mu_l/mu_g)^0.1. + +Rust: `cfd::multiphase::martinelli_parameter` + """ + ... + +def two_phase_pressure_drop_lockhart_martinelli(dp_l: float, dp_g: float, c: float) -> float: + """ +Lockhart-Martinelli two-phase pressure gradient from the single-phase +liquid and gas gradients: dp_tp = dp_l phi_l^2 with +phi_l^2 = 1 + C/X + 1/X^2, X^2 = dp_l/dp_g. + +Rust: `cfd::multiphase::two_phase_pressure_drop_lockhart_martinelli` + """ + ... + +def chisholm(re_l: float, re_g: float) -> float: + """ +Chisholm C coefficient from the flow regimes of each phase +(turbulent-turbulent 20, viscous-turbulent 12, turbulent-viscous 10, +viscous-viscous 5). + +Rust: `cfd::multiphase::chisholm` + """ + ... + +def friedel_correlation(x: float, rho_g: float, rho_l: float, mu_g: float, mu_l: float, sigma: float, d: float, mass_flux: float) -> float: + """ +Simplified Friedel two-phase multiplier phi_lo^2 for the +liquid-only pressure gradient, using the homogeneous density, Froude and +Weber corrections. + +Rust: `cfd::multiphase::friedel_correlation` + """ + ... + +def flow_pattern_taitel_dukler(j_g: float, j_l: float, d: float, rho_g: float, rho_l: float, mu_g: float, mu_l: float, inclination: float) -> FlowPattern: + """ +Simplified Taitel-Dukler flow-pattern map for a pipe of diameter `d` at +inclination `inclination` (radians from horizontal), from superficial +velocities `j_g`, `j_l`. + +Rust: `cfd::multiphase::flow_pattern_taitel_dukler` + """ + ... + +def eotvos(delta_rho: float, g: float, d: float, sigma: float) -> float: + """ +Eotvos (Bond) number Eo = delta_rho g d^2 / sigma. + +Rust: `cfd::multiphase::eotvos` + """ + ... + +def morton_number(g: float, mu: float, rho: float, sigma: float) -> float: + """ +Morton number Mo = g mu^4 / (rho sigma^3) (continuous-phase properties, +density difference folded into g for near-unit density ratios). + +Rust: `cfd::multiphase::morton_number` + """ + ... + +def bubble_drag_coefficient(re: float, eo: float, mo: float) -> float: + """ +Tomiyama drag coefficient for a contaminated bubble: +Cd = max(24/Re (1 + 0.15 Re^0.687), 8 Eo / (3 (Eo + 4))). + +Rust: `cfd::multiphase::bubble_drag_coefficient` + """ + ... + +def bubble_rise_velocity(d: float, rho_l: float, rho_g: float, mu_l: float, sigma: float) -> float: + """ +Terminal rise velocity of a bubble of diameter `d`: force balance with +the Tomiyama contaminated drag law, solved by bisection. Reduces to the +Stokes settling formula for tiny bubbles and to the Eotvos-limited cap +regime for large ones. + +Rust: `cfd::multiphase::bubble_rise_velocity` + """ + ... + +def droplet_terminal_velocity(d: float, rho_d: float, rho_c: float, mu_c: float) -> float: + """ +Stokes terminal velocity of a small droplet in a continuous phase: +u = g d^2 (rho_d - rho_c) / (18 mu_c). + +Rust: `cfd::multiphase::droplet_terminal_velocity` + """ + ... + +def sauter_mean_diameter(diameters: list[float]) -> float: + """ +Sauter mean diameter d32 = sum d^3 / sum d^2. + +Rust: `cfd::multiphase::sauter_mean_diameter` + """ + ... + +def rosin_rammler(d: float, d_mean: float, n: float) -> float: + """ +Rosin-Rammler cumulative mass fraction below diameter `d`: +F = 1 - exp(-(d/d_mean)^n). + +Rust: `cfd::multiphase::rosin_rammler` + """ + ... + +def breakup_rate_luo_svendsen(alpha: float, eps: float, d: float, sigma: float, rho_c: float) -> float: + """ +Simplified Luo-Svendsen breakup rate for a bubble/droplet of diameter +`d` in turbulence of dissipation `eps` at dispersed-phase fraction +`alpha`: rate ~ 0.923 (1-alpha) (eps/d^2)^{1/3} +exp(-12 sigma / (2.05 rho_c eps^{2/3} d^{5/3})). + +Rust: `cfd::multiphase::breakup_rate_luo_svendsen` + """ + ... + +def coalescence_rate_prince_blanch(d1: float, d2: float, eps: float, rho_c: float, sigma: float) -> float: + """ +Simplified Prince-Blanch coalescence kernel for bubbles of diameters +`d1`, `d2` (turbulent collision frequency times a film-drainage +efficiency). + +Rust: `cfd::multiphase::coalescence_rate_prince_blanch` + """ + ... + +def population_balance_1d(n: list[float], sizes: list[float], breakup: Callable[[float], float], coalescence: Callable[[float, float], float], dt: float) -> list[float]: + """ +One explicit step of a discrete population balance on size classes +`sizes` (diameters) with number densities `n`: binary breakage into two +equal-volume daughters and pairwise coalescence into the nearest class by +volume. Number densities update; volume moves between resolved classes. + +Rust: `cfd::multiphase::population_balance_1d` + """ + ... + +def cavitation_number(p: float, p_v: float, rho: float, u: float) -> float: + """ +Cavitation number sigma_c = (p - p_v) / (rho u^2 / 2). + +Rust: `cfd::multiphase::cavitation_number` + """ + ... + +def boiling_heat_flux_rohsenow(delta_t: float, fluid: SaturatedFluid, c_sf: float) -> float: + """ +Rohsenow nucleate-boiling heat flux for wall superheat `delta_t` (K) +with surface constant `c_sf` (0.013 for water on polished surfaces). + +Rust: `cfd::multiphase::boiling_heat_flux_rohsenow` + """ + ... + +def critical_heat_flux_zuber(fluid: SaturatedFluid) -> float: + """ +Zuber critical heat flux: +q_chf = 0.131 h_fg rho_g^{1/2} (sigma g (rho_l - rho_g))^{1/4}. + +Rust: `cfd::multiphase::critical_heat_flux_zuber` + """ + ... + +def condensation_nusselt_film(fluid: SaturatedFluid, k_l: float, delta_t: float, height: float) -> float: + """ +Nusselt laminar film condensation coefficient on a vertical plate of +height `height` with wall subcooling `delta_t` and liquid conductivity +`k_l`: h = 0.943 [rho_l (rho_l - rho_g) g h_fg k^3 / (mu dT L)]^{1/4}. + +Rust: `cfd::multiphase::condensation_nusselt_film` + """ + ... + +def evaporation_rate_hertz_knudsen(p_sat: float, p: float, t: float, m: float) -> float: + """ +Hertz-Knudsen maximum evaporation mass flux (kg/m^2/s) for molar mass +`m` (kg/mol) at temperature `t`: J = (p_sat - p) sqrt(m / (2 pi R T)). + +Rust: `cfd::multiphase::evaporation_rate_hertz_knudsen` + """ + ... + +def spray_penetration_hiroyasu(delta_p: float, rho_l: float, rho_a: float, d_nozzle: float, t: float) -> float: + """ +Hiroyasu spray tip penetration for injection pressure drop `delta_p` +into gas of density `rho_a` through a nozzle of diameter `d_nozzle`, +at time `t` after start of injection. + +Rust: `cfd::multiphase::spray_penetration_hiroyasu` + """ + ... + +def particle_response_time(rho_p: float, d_p: float, mu: float) -> float: + """ +Particle response time tau_p = rho_p d^2 / (18 mu). + +Rust: `cfd::multiphase::particle_response_time` + """ + ... + +def stokes_number(rho_p: float, d_p: float, u: float, mu: float, l: float) -> float: + """ +Stokes number St = tau_p u / l. + +Rust: `cfd::multiphase::stokes_number` + """ + ... + +def settling_velocity(d: float, rho_p: float, rho_f: float, mu: float, g: float) -> float: + """ +Terminal settling velocity of a sphere with the Schiller-Naumann drag +Cd = 24/Re (1 + 0.15 Re^0.687), solved by bisection. + +Rust: `cfd::multiphase::settling_velocity` + """ + ... + +def fluidization_minimum_velocity(d: float, rho_p: float, rho_f: float, mu: float, porosity: float) -> float: + """ +Minimum fluidization velocity from the Ergun equation balanced against +the bed weight at voidage `porosity`, solved by bisection. + +Rust: `cfd::multiphase::fluidization_minimum_velocity` + """ + ... + +def sedimentation_richardson_zaki(u_t: float, porosity: float, n: float) -> float: + """ +Richardson-Zaki hindered settling velocity u = u_t phi^n where `phi` is +the fluid voidage. + +Rust: `cfd::multiphase::sedimentation_richardson_zaki` + """ + ... + +def hindered_settling_exponent(re: float) -> float: + """ +Richardson-Zaki exponent as a function of particle Reynolds number. + +Rust: `cfd::multiphase::hindered_settling_exponent` + """ + ... diff --git a/bindings/python/python/numeria/cfd/porous.pyi b/bindings/python/python/numeria/cfd/porous.pyi new file mode 100644 index 0000000..f2ae82b --- /dev/null +++ b/bindings/python/python/numeria/cfd/porous.pyi @@ -0,0 +1,261 @@ +""" +Porous-media flow: Darcy's law and extensions, unsaturated flow (Richards equation with Van Genuchten retention), well hydraulics, solute transport, and two-phase relations (Leverett, Corey, Buckley-Leverett). +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.cfd.grid import CellField2 + +class VanGenuchten: + """ +Van Genuchten soil retention parameters (`alpha` in 1/m, `k_s` in m/s). + +Rust: `cfd::porous::VanGenuchten` + """ + def __init__(self, theta_r: float, theta_s: float, alpha: float, n: float, k_s: float) -> None: ... + def theta(self, h: float) -> float: ... + def effective_saturation(self, h: float) -> float: ... + def k(self, h: float) -> float: ... + def capacity(self, h: float) -> float: ... + def head_from_theta(self, theta: float) -> float: ... + @staticmethod + def sand() -> VanGenuchten: ... + @staticmethod + def loam() -> VanGenuchten: ... + @staticmethod + def clay() -> VanGenuchten: ... + @property + def theta_r(self) -> float: ... + @property + def theta_s(self) -> float: ... + @property + def alpha(self) -> float: ... + @property + def n(self) -> float: ... + @property + def k_s(self) -> float: ... + +def darcy_velocity(k: float, mu: float, grad_p: float) -> float: + """ +Darcy velocity (specific discharge) q = -(k/mu) grad p; returns the +magnitude for a pressure gradient `grad_p` (Pa/m). + +Rust: `cfd::porous::darcy_velocity` + """ + ... + +def darcy_flow_rate(k: float, a: float, mu: float, dp: float, l: float) -> float: + """ +Volumetric flow rate through area `a` over length `l` under pressure +difference `dp`: Q = k A dp / (mu L). + +Rust: `cfd::porous::darcy_flow_rate` + """ + ... + +def permeability_kozeny_carman(porosity: float, d_particle: float) -> float: + """ +Kozeny-Carman permeability of a packed bed of spheres of diameter +`d_particle` (Ergun-consistent constant 150): +k = phi^3 d^2 / (150 (1-phi)^2). + +Rust: `cfd::porous::permeability_kozeny_carman` + """ + ... + +def carman_kozeny_fibers(porosity: float, d_fiber: float) -> float: + """ +Approximate Kozeny-type permeability of a random fiber mat with fiber +diameter `d_fiber` (Kozeny constant ~ 5 with the fiber specific surface +4/d): k = phi^3 d^2 / (80 (1-phi)^2). + +Rust: `cfd::porous::carman_kozeny_fibers` + """ + ... + +def ergun_pressure_drop(u: float, d: float, porosity: float, mu: float, rho: float, l: float) -> float: + """ +Ergun pressure drop over bed length `l` for superficial velocity `u`: +dP/L = 150 mu u (1-phi)^2/(phi^3 d^2) + 1.75 rho u^2 (1-phi)/(phi^3 d). + +Rust: `cfd::porous::ergun_pressure_drop` + """ + ... + +def forchheimer(k: float, beta: float, mu: float, rho: float, u: float) -> float: + """ +Forchheimer pressure gradient magnitude: dp/dx = mu u / k + beta rho u^2. + +Rust: `cfd::porous::forchheimer` + """ + ... + +def brinkman_velocity_profile(y: float, h: float, k: float, mu: float, dp_dx: float) -> float: + """ +Brinkman flow in a porous channel of height `h` driven by `dp_dx`: +u(y) = -(k/mu) dp/dx [1 - cosh((y - h/2)/sqrt(k)) / cosh(h/(2 sqrt(k)))]. + +Rust: `cfd::porous::brinkman_velocity_profile` + """ + ... + +def hydraulic_conductivity(k: float, rho: float, g: float, mu: float) -> float: + """ +Hydraulic conductivity K = k rho g / mu (m/s). + +Rust: `cfd::porous::hydraulic_conductivity` + """ + ... + +def brooks_corey(h: float, h_b: float, lambda_: float) -> float: + """ +Brooks-Corey effective saturation for entry pressure head `h_b` (m) and +pore-size index `lambda`. + +Rust: `cfd::porous::brooks_corey` + """ + ... + +def richards_equation_1d(theta0: list[float], soil: VanGenuchten | Sequence[float], dz: float, dt: float, steps: int, bc_top: float, bc_bottom: float) -> list[list[float]]: + """ +Explicit finite-volume Richards equation in 1D (z positive downward), +theta-form so mass is conserved exactly. Boundary conditions are water +fluxes (m/s, positive downward): `bc_top` enters the first cell, +`bc_bottom` leaves the last. Returns the water-content profile after each +step (`steps + 1` rows including the initial state). + +Rust: `cfd::porous::richards_equation_1d` + """ + ... + +def theis_drawdown(q: float, t_coeff: float, s: float, r: float, t: float) -> float: + """ +Theis transient drawdown at radius `r` and time `t` for pumping rate `q`, +transmissivity `t_coeff` and storativity `s`: +s_d = Q/(4 pi T) W(u), u = r^2 S/(4 T t), W = E1. + +Rust: `cfd::porous::theis_drawdown` + """ + ... + +def thiem_steady(q: float, t_coeff: float, r1: float, r2: float, h1: float) -> float: + """ +Thiem steady-state head at radius `r2` given head `h1` at `r1`: +h2 = h1 + Q/(2 pi T) ln(r2/r1). + +Rust: `cfd::porous::thiem_steady` + """ + ... + +def dupuit_unconfined(h1: float, h2: float, l: float, x: float) -> float: + """ +Dupuit unconfined flow between heads `h1` and `h2` over length `l`: +the water-table height at distance `x`. + +Rust: `cfd::porous::dupuit_unconfined` + """ + ... + +def groundwater_flow_2d(k_field: CellField2, bc: list[tuple[int, int, float]], recharge: CellField2) -> CellField2: + """ +Steady 2D groundwater flow: solve div(K grad h) = -recharge with +Dirichlet head fixed at the listed `(i, j, head)` cells and no-flow +elsewhere on the boundary. Gauss-Seidel with harmonic-mean face +conductivities. + +Rust: `cfd::porous::groundwater_flow_2d` + """ + ... + +def peclet_porous(u: float, l: float, d: float) -> float: + """ +Peclet number for porous transport: Pe = u l / D. + +Rust: `cfd::porous::peclet_porous` + """ + ... + +def dispersion_coefficient(alpha_l: float, u: float, d_m: float) -> float: + """ +Hydrodynamic dispersion coefficient D = alpha_L u + D_m. + +Rust: `cfd::porous::dispersion_coefficient` + """ + ... + +def advection_dispersion_1d(c: list[float], u: float, d: float, dx: float, dt: float, retardation: float, decay: float) -> list[float]: + """ +One explicit step of the 1D advection-dispersion-reaction equation with +retardation factor R and first-order decay: +R dc/dt + u dc/dx = D d2c/dx2 - R lambda c (upwind advection). + +Rust: `cfd::porous::advection_dispersion_1d` + """ + ... + +def ogata_banks(x: float, t: float, u: float, d: float) -> float: + """ +Ogata-Banks solution for continuous injection at x = 0 into an initially +clean semi-infinite column: c/c0 at (x, t). + +Rust: `cfd::porous::ogata_banks` + """ + ... + +def capillary_pressure_leverett(sw: float, porosity: float, k: float, sigma: float, theta: float) -> float: + """ +Leverett J-function scaling of capillary pressure: +Pc = sigma cos(theta) sqrt(phi/k) J(Sw), with J = 0.5 Sw^{-1/2}. + +Rust: `cfd::porous::capillary_pressure_leverett` + """ + ... + +def relative_permeability_corey(sw: float, sw_r: float, so_r: float, n: float) -> tuple[float, float]: + """ +Corey relative permeabilities `(k_rw, k_ro)` with residual saturations +and exponent `n`. + +Rust: `cfd::porous::relative_permeability_corey` + """ + ... + +def buckley_leverett(x: float, t: float, u_total: float, porosity: float, mu_w: float, mu_o: float, sw_r: float, so_r: float) -> float: + """ +Buckley-Leverett water saturation at position `x` and time `t` for total +(Darcy) velocity `u_total` injected into a column at connate water +saturation, using quadratic Corey curves. Returns Sw(x, t) including the +Welge shock front. + +Rust: `cfd::porous::buckley_leverett` + """ + ... + +def bioclogging_porosity_change(phi0: float, biomass: float, rho_biofilm: float) -> float: + """ +Porosity reduction by biofilm growth: phi = phi0 - biomass/rho_biofilm. + +Rust: `cfd::porous::bioclogging_porosity_change` + """ + ... + +def effective_thermal_conductivity_porous(k_s: float, k_f: float, porosity: float) -> float: + """ +Effective thermal conductivity of a saturated porous medium (geometric +mean mixing): k_eff = k_s^(1-phi) k_f^phi. + +Rust: `cfd::porous::effective_thermal_conductivity_porous` + """ + ... + +def gravity_number(k: float, rho: float, mu: float, u: float) -> float: + """ +Gravity number: ratio of gravity to viscous forces in porous flow +(used in the tests for scaling sanity). + +Rust: `cfd::porous::gravity_number` + """ + ... diff --git a/bindings/python/python/numeria/cfd/potential_flow.pyi b/bindings/python/python/numeria/cfd/potential_flow.pyi new file mode 100644 index 0000000..ad809eb --- /dev/null +++ b/bindings/python/python/numeria/cfd/potential_flow.pyi @@ -0,0 +1,276 @@ +""" +Incompressible potential flow: elementary singularities, complex potentials, Joukowski and Karman-Trefftz airfoils, NACA sections, the Hess-Smith panel method, thin-airfoil and lifting-line theory, a simple vortex lattice, and added-mass results. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.math import Vec2 + +class Element: + """ +Elementary potential-flow element. + +Rust: `cfd::potential_flow::Element` + """ + def complex_potential(self, z: complex) -> complex: ... + def complex_velocity(self, z: complex) -> complex: ... + +class PanelMethod: + """ +Hess-Smith source/vortex panel method. + +Rust: `cfd::potential_flow::PanelMethod` + """ + def __init__(self, airfoil: list[Vec2 | Sequence[float]]) -> None: ... + def solve(self) -> None: ... + def velocity_at(self, p: Vec2 | Sequence[float]) -> Vec2: ... + def cp_distribution(self) -> list[tuple[float, float]]: ... + def cl(self) -> float: ... + def cm_quarter_chord(self) -> float: ... + def streamlines(self, seeds: list[Vec2 | Sequence[float]], steps: int, dt: float) -> list[list[Vec2]]: ... + def pressure_center(self) -> float: ... + @property + def alpha(self) -> float: ... + @property + def u_inf(self) -> float: ... + @property + def sources(self) -> list[float]: ... + @property + def gamma(self) -> float: ... + +class Plane2: + """ +A wall line for the method of images. + +Rust: `cfd::potential_flow::Plane2` + """ + def __init__(self, point: Vec2 | Sequence[float], normal: Vec2 | Sequence[float]) -> None: ... + @property + def point(self) -> Vec2: ... + @property + def normal(self) -> Vec2: ... + +class PotentialFlow2: + """ +Superposition of potential-flow elements. + +Rust: `cfd::potential_flow::PotentialFlow2` + """ + def __init__(self, elements: list[Element]) -> None: ... + def velocity(self, p: Vec2 | Sequence[float]) -> Vec2: ... + def potential(self, p: Vec2 | Sequence[float]) -> float: ... + def stream_function(self, p: Vec2 | Sequence[float]) -> float: ... + def complex_potential(self, z: complex) -> complex: ... + def complex_velocity(self, z: complex) -> complex: ... + def pressure_coefficient(self, p: Vec2 | Sequence[float], u_inf: float) -> float: ... + def stagnation_points(self) -> list[Vec2]: ... + def streamlines(self, seeds: list[Vec2 | Sequence[float]], steps: int, dt: float) -> list[list[Vec2]]: ... + def lift_kutta_joukowski(self, u_inf: float, rho: float) -> float: ... + @property + def elements(self) -> list[Element]: ... + +class WingGeometry: + """ +Simple wing planform for the vortex lattice. + +Rust: `cfd::potential_flow::WingGeometry` + """ + def __init__(self, span: float, root_chord: float, tip_chord: float, sweep: float) -> None: ... + @property + def span(self) -> float: ... + @property + def root_chord(self) -> float: ... + @property + def tip_chord(self) -> float: ... + @property + def sweep(self) -> float: ... + +def cylinder_flow(u_inf: float, r: float, gamma: float) -> PotentialFlow2: + """ +Flow past a circular cylinder with circulation Γ. + +Rust: `cfd::potential_flow::cylinder_flow` + """ + ... + +def rankine_oval(u: float, m: float, a: float) -> PotentialFlow2: + """ +Rankine oval: source and sink of strength m at (±a, 0) in a stream. + +Rust: `cfd::potential_flow::rankine_oval` + """ + ... + +def cylinder_cp_exact(theta: float, gamma: float, u: float, r: float) -> float: + """ +Exact surface pressure coefficient of the rotating cylinder +(Γ counterclockwise-positive, matching `Element::Vortex`). + +Rust: `cfd::potential_flow::cylinder_cp_exact` + """ + ... + +def joukowski_transform(z: complex, c: float) -> complex: + """ +Joukowski map ζ = z + c²/z. + +Rust: `cfd::potential_flow::joukowski_transform` + """ + ... + +def inverse_joukowski(zeta: complex, c: float) -> complex: + """ +Inverse Joukowski map (branch with |z| ≥ c). + +Rust: `cfd::potential_flow::inverse_joukowski` + """ + ... + +def joukowski_airfoil(center: complex, c: float, n_points: int) -> list[Vec2]: + """ +Joukowski airfoil: image of the circle through (c, 0) centered at +`center`. + +Rust: `cfd::potential_flow::joukowski_airfoil` + """ + ... + +def joukowski_airfoil_flow(center: complex, c: float, alpha: float, u_inf: float) -> tuple[list[Vec2], list[float], float]: + """ +Joukowski airfoil with the Kutta condition: returns (surface points, +surface cp, lift coefficient). + +Rust: `cfd::potential_flow::joukowski_airfoil_flow` + """ + ... + +def karman_trefftz_airfoil(center: complex, c: float, n_exp: float, n_points: int) -> list[Vec2]: + """ +Karman-Trefftz airfoil (finite trailing-edge angle set by `n_exp` +slightly below 2). + +Rust: `cfd::potential_flow::karman_trefftz_airfoil` + """ + ... + +def naca4(code: str, n_points: int, closed_te: bool) -> list[Vec2]: + """ +NACA 4-digit airfoil ("2412" etc.), chord 1, from the trailing edge +over the top and back along the bottom. + +Rust: `cfd::potential_flow::naca4` + """ + ... + +def naca5(code: str, n_points: int) -> list[Vec2]: + """ +NACA 5-digit airfoil ("23012" etc.). + +Rust: `cfd::potential_flow::naca5` + """ + ... + +def thin_airfoil_cl(alpha: float, camber_slope: Callable[[float], float]) -> float: + """ +Thin-airfoil lift coefficient for a camber-line slope dz/dx given on +x ∈ [0, 1]: cl = 2π(α − α_L0). + +Rust: `cfd::potential_flow::thin_airfoil_cl` + """ + ... + +def thin_airfoil_cl_flat(alpha: float) -> float: + """ +Flat-plate thin-airfoil lift 2πα. + +Rust: `cfd::potential_flow::thin_airfoil_cl_flat` + """ + ... + +def lifting_line(span: float, chord: Callable[[float], float], alpha: Callable[[float], float], n_terms: int, u_inf: float) -> tuple[float, float, list[float]]: + """ +Prandtl lifting line with a Fourier sine series: returns +(CL, CDi, circulation at the collocation stations). + +Rust: `cfd::potential_flow::lifting_line` + """ + ... + +def elliptic_wing_cl(ar: float, alpha: float) -> float: + """ +Elliptic-wing lift slope: CL = 2πα/(1 + 2/AR). + +Rust: `cfd::potential_flow::elliptic_wing_cl` + """ + ... + +def induced_drag(cl: float, ar: float, e: float) -> float: + """ +Induced drag CL²/(π e AR). + +Rust: `cfd::potential_flow::induced_drag` + """ + ... + +def oswald_efficiency_estimate(ar: float, sweep: float) -> float: + """ +Raymer's straight-wing Oswald efficiency estimate with a sweep +correction. + +Rust: `cfd::potential_flow::oswald_efficiency_estimate` + """ + ... + +def vortex_lattice(wing: WingGeometry | Sequence[float], alpha: float, n_span: int, n_chord: int, u_inf: float) -> tuple[float, float, list[float]]: + """ +Single-lattice-row vortex lattice method (horseshoe vortices at the +quarter chord, collocation at 3/4 chord): returns (CL, CDi, +circulation per strip). + +Rust: `cfd::potential_flow::vortex_lattice` + """ + ... + +def ground_effect_factor(h_over_b: float) -> float: + """ +McCormick ground-effect induced-drag factor (16 h/b)²/(1 + (16 h/b)²). + +Rust: `cfd::potential_flow::ground_effect_factor` + """ + ... + +def conformal_map_flow(map: Callable[[complex], complex], base: PotentialFlow2, z: complex) -> Vec2: + """ +Velocity of a base flow seen through a conformal map at the physical +point z (numerical dW/dζ via the chain rule). + +Rust: `cfd::potential_flow::conformal_map_flow` + """ + ... + +def method_of_images_wall(elements: list[Element], wall: Plane2) -> PotentialFlow2: + """ +Mirror every element across a wall (method of images). + +Rust: `cfd::potential_flow::method_of_images_wall` + """ + ... + +def added_mass_cylinder(rho: float, r: float) -> float: + """ +Added mass of an accelerating cylinder per unit length ρπr². + +Rust: `cfd::potential_flow::added_mass_cylinder` + """ + ... + +def added_mass_sphere(rho: float, r: float) -> float: + """ +Added mass of a sphere (2/3)ρπr³. + +Rust: `cfd::potential_flow::added_mass_sphere` + """ + ... diff --git a/bindings/python/python/numeria/cfd/riemann.pyi b/bindings/python/python/numeria/cfd/riemann.pyi new file mode 100644 index 0000000..ce06163 --- /dev/null +++ b/bindings/python/python/numeria/cfd/riemann.pyi @@ -0,0 +1,338 @@ +""" +1D/2D compressible Euler equations: exact and approximate Riemann solvers (HLL, HLLC, Roe with entropy fix, Rusanov, AUSM+), MUSCL finite-volume drivers, classic shock-tube problems, and gas-dynamic shock/expansion relations. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.statistics.distributions import Normal + +class Cons: + """ +Conserved state (density, momentum, total energy). + +Rust: `cfd::riemann::Cons` + """ + def __init__(self, rho: float, mom: float, e: float) -> None: ... + @property + def rho(self) -> float: ... + @property + def mom(self) -> float: ... + @property + def e(self) -> float: ... + +class Euler1D: + """ +1D finite-volume Euler solver (order 1, or 2 with minmod MUSCL). + +Rust: `cfd::riemann::Euler1D` + """ + def __init__(self, n: int, dx: float, gamma: float) -> None: ... + def set_riemann_problem(self, l: Prim | Sequence[float], r: Prim | Sequence[float], x_split: float) -> None: ... + def step(self, cfl: float) -> float: ... + def run_until(self, t: float) -> None: ... + def primitives(self) -> list[Prim]: ... + def total_mass(self) -> float: ... + def total_energy(self) -> float: ... + def shock_position(self) -> Optional[float]: ... + @property + def cells(self) -> list[Cons]: ... + @property + def dx(self) -> float: ... + @property + def gamma(self) -> float: ... + @property + def flux(self) -> FluxKind: ... + @property + def order(self) -> int: ... + @property + def bc(self) -> EulerBc: ... + @property + def time(self) -> float: ... + +class Euler2D: + """ +2D finite-volume Euler solver (dimensional splitting, MUSCL + HLLC). + +Rust: `cfd::riemann::Euler2D` + """ + def __init__(self, nx: int, ny: int, dx: float, gamma: float) -> None: ... + def set_cell(self, i: int, j: int, rho: float, u: float, v: float, p: float) -> None: ... + def step(self, cfl: float) -> float: ... + def kelvin_helmholtz_init(self) -> None: ... + def rayleigh_taylor_init(self, g: float) -> None: ... + def double_mach_reflection_init(self) -> None: ... + def shock_bubble_init(self) -> None: ... + def schlieren(self) -> list[float]: ... + @property + def nx(self) -> int: ... + @property + def ny(self) -> int: ... + @property + def dx(self) -> float: ... + @property + def gamma(self) -> float: ... + @property + def rho(self) -> list[float]: ... + @property + def momx(self) -> list[float]: ... + @property + def momy(self) -> list[float]: ... + @property + def e(self) -> list[float]: ... + @property + def solid(self) -> list[bool]: ... + @property + def periodic(self) -> bool: ... + @property + def gravity(self) -> float: ... + @property + def time(self) -> float: ... + +class EulerBc: + """ +Boundary condition for `Euler1D`. + +Rust: `cfd::riemann::EulerBc` + """ + ... + +class FluxKind: + """ +Numerical flux selector. + +Rust: `cfd::riemann::FluxKind` + """ + ... + +class Prim: + """ +Primitive state (density, velocity, pressure). + +Rust: `cfd::riemann::Prim` + """ + def __init__(self, rho: float, u: float, p: float) -> None: ... + @property + def rho(self) -> float: ... + @property + def u(self) -> float: ... + @property + def p(self) -> float: ... + +def prim_to_cons(p: Prim | Sequence[float], gamma: float) -> Cons: + """ +Primitive → conserved. + +Rust: `cfd::riemann::prim_to_cons` + """ + ... + +def cons_to_prim(c: Cons | Sequence[float], gamma: float) -> Prim: + """ +Conserved → primitive. + +Rust: `cfd::riemann::cons_to_prim` + """ + ... + +def flux(c: Cons | Sequence[float], gamma: float) -> Cons: + """ +Physical flux F(U). + +Rust: `cfd::riemann::flux` + """ + ... + +def sound_speed(p: Prim | Sequence[float], gamma: float) -> float: + """ +Speed of sound √(γp/ρ). + +Rust: `cfd::riemann::sound_speed` + """ + ... + +def riemann_exact_star(l: Prim | Sequence[float], r: Prim | Sequence[float], gamma: float) -> tuple[float, float]: + """ +Star-region pressure and velocity of the exact Riemann problem +(Newton iteration, Toro ch. 4). + +Rust: `cfd::riemann::riemann_exact_star` + """ + ... + +def riemann_exact(l: Prim | Sequence[float], r: Prim | Sequence[float], gamma: float, x_over_t: float) -> Prim: + """ +Sample the exact Riemann solution at similarity coordinate ξ = x/t. + +Rust: `cfd::riemann::riemann_exact` + """ + ... + +def wave_speeds_einfeldt(l: Cons | Sequence[float], r: Cons | Sequence[float], gamma: float) -> tuple[float, float]: + """ +Einfeldt (Roe-averaged) wave speed bounds (S_L, S_R). + +Rust: `cfd::riemann::wave_speeds_einfeldt` + """ + ... + +def flux_hll(l: Cons | Sequence[float], r: Cons | Sequence[float], gamma: float) -> Cons: + """ +HLL flux. + +Rust: `cfd::riemann::flux_hll` + """ + ... + +def flux_hllc(l: Cons | Sequence[float], r: Cons | Sequence[float], gamma: float) -> Cons: + """ +HLLC flux (restores the contact wave). + +Rust: `cfd::riemann::flux_hllc` + """ + ... + +def flux_roe(l: Cons | Sequence[float], r: Cons | Sequence[float], gamma: float) -> Cons: + """ +Roe flux with a Harten entropy fix. + +Rust: `cfd::riemann::flux_roe` + """ + ... + +def flux_rusanov(l: Cons | Sequence[float], r: Cons | Sequence[float], gamma: float) -> Cons: + """ +Rusanov (local Lax-Friedrichs) flux. + +Rust: `cfd::riemann::flux_rusanov` + """ + ... + +def flux_ausm_plus(l: Cons | Sequence[float], r: Cons | Sequence[float], gamma: float) -> Cons: + """ +AUSM+ flux (Liou 1996). + +Rust: `cfd::riemann::flux_ausm_plus` + """ + ... + +def sod_shock_tube(n: int) -> Euler1D: + """ +Sod's shock tube on n cells (γ = 1.4, unit domain). + +Rust: `cfd::riemann::sod_shock_tube` + """ + ... + +def lax_problem(n: int) -> Euler1D: + """ +Lax's problem. + +Rust: `cfd::riemann::lax_problem` + """ + ... + +def shu_osher(n: int) -> Euler1D: + """ +Shu-Osher shock/entropy-wave interaction. + +Rust: `cfd::riemann::shu_osher` + """ + ... + +def blast_wave_woodward_colella(n: int) -> Euler1D: + """ +Woodward-Colella interacting blast waves (reflective walls). + +Rust: `cfd::riemann::blast_wave_woodward_colella` + """ + ... + +def sedov_1d(n: int) -> Euler1D: + """ +Sedov point blast in 1D planar symmetry. + +Rust: `cfd::riemann::sedov_1d` + """ + ... + +def sod_exact(x: float, t: float) -> Prim: + """ +Exact Sod solution at (x, t) on the unit domain split at 0.5. + +Rust: `cfd::riemann::sod_exact` + """ + ... + +def rankine_hugoniot(p1: float, rho1: float, mach: float, gamma: float) -> Prim: + """ +Post-shock primitive state behind a normal shock of Mach `mach` +moving into gas at (p1, rho1) at rest (lab frame). + +Rust: `cfd::riemann::rankine_hugoniot` + """ + ... + +def normal_shock_relations(mach: float, gamma: float) -> tuple[float, float, float, float]: + """ +Normal shock relations: (p2/p1, ρ2/ρ1, T2/T1, M2). + +Rust: `cfd::riemann::normal_shock_relations` + """ + ... + +def oblique_shock_angle(mach: float, deflection: float, gamma: float) -> Optional[tuple[float, float]]: + """ +Oblique shock wave angles (weak, strong) in radians for a flow +deflection; `None` if the deflection exceeds the maximum attached +angle. + +Rust: `cfd::riemann::oblique_shock_angle` + """ + ... + +def prandtl_meyer(mach: float, gamma: float) -> float: + """ +Prandtl-Meyer function ν(M) in radians. + +Rust: `cfd::riemann::prandtl_meyer` + """ + ... + +def nozzle_area_ratio(mach: float, gamma: float) -> float: + """ +Isentropic area ratio A/A* for a given Mach number. + +Rust: `cfd::riemann::nozzle_area_ratio` + """ + ... + +def nozzle_mach_from_area(ratio: float, gamma: float, supersonic: bool) -> float: + """ +Invert the area ratio for the subsonic or supersonic branch. + +Rust: `cfd::riemann::nozzle_mach_from_area` + """ + ... + +def quasi_1d_nozzle(area: Callable[[float], float], n: int, p0: float, t0: float, p_exit: float, gamma: float) -> list[Prim]: + """ +Quasi-1D isentropic nozzle solution: `area(x)` on x ∈ [0, 1] with a +single interior throat; chooses the supersonic branch downstream when +`p_exit` is below the critical exit pressure. Returns primitives at +`n` stations (R = 287 J/kg·K). + +Rust: `cfd::riemann::quasi_1d_nozzle` + """ + ... + +def isentropic_vortex_exact(x: float, y: float, t: float, gamma: float) -> tuple[float, float, float, float]: + """ +Isentropic vortex (strength 5) advected by a uniform (1, 1) +background on a 10 × 10 periodic domain: returns (ρ, u, v, p). + +Rust: `cfd::riemann::isentropic_vortex_exact` + """ + ... diff --git a/bindings/python/python/numeria/cfd/shallow_water.pyi b/bindings/python/python/numeria/cfd/shallow_water.pyi new file mode 100644 index 0000000..a2ad0f7 --- /dev/null +++ b/bindings/python/python/numeria/cfd/shallow_water.pyi @@ -0,0 +1,181 @@ +""" +Shallow water equations: well-balanced HLL finite volumes with hydrostatic reconstruction and wet/dry handling (1D and 2D), the Stoker dam-break solution, water-wave dispersion relations, ocean spectra, and Gerstner waves. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.monte_carlo import Rng +from numeria.sim.fluid_sim import ShallowWater1D +from numeria.math import Vec2 +from numeria.math import Vec3 + +class ShallowWater2D: + """ +2D shallow water solver on a square grid: HLL fluxes with Audusse +hydrostatic reconstruction (well-balanced over bathymetry), Manning +friction, Coriolis, and wet/dry tolerance. + +Rust: `cfd::shallow_water::ShallowWater2D` + """ + def __init__(self, nx: int, ny: int, dx: float, g: float) -> None: ... + def set_bathymetry(self, f: Callable[[float, float], float]) -> None: ... + def set_dam_break(self, x_split: float, h_l: float, h_r: float) -> None: ... + def set_gaussian_bump(self, cx: float, cy: float, amp: float, sigma: float, base_depth: float) -> None: ... + def add_source(self, i: int, j: int, rate: float) -> None: ... + def step(self, cfl: float) -> float: ... + def run_until(self, t: float) -> None: ... + def wet_cells(self) -> int: ... + def total_volume(self) -> float: ... + def total_energy(self) -> float: ... + def froude_field(self) -> list[float]: ... + def max_depth(self) -> float: ... + @property + def nx(self) -> int: ... + @property + def ny(self) -> int: ... + @property + def dx(self) -> float: ... + @property + def h(self) -> list[float]: ... + @property + def hu(self) -> list[float]: ... + @property + def hv(self) -> list[float]: ... + @property + def bathymetry(self) -> list[float]: ... + @property + def g(self) -> float: ... + @property + def manning_n(self) -> float: ... + @property + def coriolis(self) -> float: ... + @property + def dry_tol(self) -> float: ... + @property + def time(self) -> float: ... + +def swe_1d_exact_dam_break(x: float, t: float, h_l: float, h_r: float, g: float) -> tuple[float, float]: + """ +Stoker's exact wet-bed dam-break solution: (h, u) at position x +(dam at x = 0) and time t. + +Rust: `cfd::shallow_water::swe_1d_exact_dam_break` + """ + ... + +def swe_1d_step_hll(h: MutableSequence[float], hu: MutableSequence[float], b: list[float], dx: float, g: float, dt: float, dry: float, reflective: bool) -> None: + """ +1D well-balanced SWE step on columns with bathymetry (helper used by +the tsunami run-up model and `ShallowWater1D::step_hll`). + +Rust: `cfd::shallow_water::swe_1d_step_hll` + """ + ... + +def tsunami_runup_1d(slope: float, a: float, sigma: float, x0: float, n: int, domain: float, t_end: float, n_samples: int) -> list[float]: + """ +Tsunami run-up on a plane beach of slope `slope`: an offshore +Gaussian wave of amplitude `a` (width `sigma`, centered at world +x = x0) propagates onto the beach. Returns the shoreline position +sampled at each of `n_samples` uniform times up to `t_end`. + +Rust: `cfd::shallow_water::tsunami_runup_1d` + """ + ... + +def wave_speed_shallow(h: float, g: float) -> float: + """ +Shallow-water wave speed √(gh). + +Rust: `cfd::shallow_water::wave_speed_shallow` + """ + ... + +def dispersion_shallow(k: float, h: float, g: float) -> float: + """ +Shallow-water dispersion ω = k√(gh). + +Rust: `cfd::shallow_water::dispersion_shallow` + """ + ... + +def dispersion_deep(k: float, g: float) -> float: + """ +Deep-water dispersion ω = √(gk). + +Rust: `cfd::shallow_water::dispersion_deep` + """ + ... + +def dispersion_full(k: float, h: float, g: float) -> float: + """ +Full linear dispersion ω = √(gk tanh(kh)). + +Rust: `cfd::shallow_water::dispersion_full` + """ + ... + +def stokes_drift(a: float, k: float, h: float, g: float) -> float: + """ +Stokes drift at the surface: U = a²ωk cosh(2kh)/(2 sinh²(kh)). + +Rust: `cfd::shallow_water::stokes_drift` + """ + ... + +def wave_breaking_criterion(h_over_lambda: float) -> bool: + """ +Miche/steepness breaking criterion: waves break when H/λ exceeds +1/7. + +Rust: `cfd::shallow_water::wave_breaking_criterion` + """ + ... + +def jonswap_spectrum(f: float, hs: float, tp: float, gamma: float) -> float: + """ +JONSWAP spectrum S(f) (m²/Hz) for significant wave height `hs`, peak +period `tp`, and peak-enhancement `gamma` (≈3.3), normalized so that +∫S df = hs²/16. + +Rust: `cfd::shallow_water::jonswap_spectrum` + """ + ... + +def pierson_moskowitz(f: float, u10: float) -> float: + """ +Pierson-Moskowitz spectrum for wind speed `u10` (m/s) at 10 m. + +Rust: `cfd::shallow_water::pierson_moskowitz` + """ + ... + +def wave_field_from_spectrum(spectrum: Callable[[float], float], n: int, fs: float, rng: Rng) -> list[float]: + """ +Synthesize a sea-surface elevation time series from a one-sided +spectrum (random phases): `n` samples at rate `fs`. + +Rust: `cfd::shallow_water::wave_field_from_spectrum` + """ + ... + +def gerstner_wave(p: Vec2 | Sequence[float], t: float, waves: list[tuple[float, float, float, Vec2 | Sequence[float]]]) -> Vec3: + """ +Gerstner (trochoidal) wave displacement of the surface point whose +rest position is `p`, summing waves given as (amplitude, wavelength, +speed, direction). + +Rust: `cfd::shallow_water::gerstner_wave` + """ + ... + +def kelvin_wake_angle() -> float: + """ +Kelvin ship-wake half angle arcsin(1/3) ≈ 19.47°. + +Rust: `cfd::shallow_water::kelvin_wake_angle` + """ + ... diff --git a/bindings/python/python/numeria/cfd/sph.pyi b/bindings/python/python/numeria/cfd/sph.pyi new file mode 100644 index 0000000..ec2a7d4 --- /dev/null +++ b/bindings/python/python/numeria/cfd/sph.pyi @@ -0,0 +1,205 @@ +""" +Smoothed-particle hydrodynamics: standard kernel family, spatial hashing, weakly compressible (WCSPH) and predictive-corrective solvers with boundary particles, and classic free-surface benchmarks. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.astrophysics.nbody import Body +from numeria.sim.cloth_sim import Particle +from numeria.math import Vec3 + +class Kernel: + """ +SPH smoothing kernels. + +Rust: `cfd::sph::Kernel` + """ + ... + +class Kind: + """ +Particle type. + +Rust: `cfd::sph::Kind` + """ + ... + +class Plane: + """ +An infinite plane given by a point and unit normal. + +Rust: `cfd::sph::Plane` + """ + def __init__(self, point: Vec3 | Sequence[float], normal: Vec3 | Sequence[float]) -> None: ... + def signed_distance(self, p: Vec3 | Sequence[float]) -> float: ... + @property + def point(self) -> Vec3: ... + @property + def normal(self) -> Vec3: ... + +class SpatialHash: + """ +Uniform-cell spatial hash for neighbor queries. + +Rust: `cfd::sph::SpatialHash` + """ + def __init__(self, cell: float) -> None: ... + def rebuild(self, positions: list[Vec3 | Sequence[float]]) -> None: ... + def neighbors(self, p: Vec3 | Sequence[float], out: MutableSequence[int]) -> None: ... + +class Sph: + """ +SPH fluid solver. + +Rust: `cfd::sph::Sph` + """ + @staticmethod + def new_2d(h: float, rest_density: float) -> Sph: ... + @staticmethod + def new_3d(h: float, rest_density: float) -> Sph: ... + def add_block(self, min: Vec3 | Sequence[float], max: Vec3 | Sequence[float], spacing: float) -> None: ... + def add_boundary_box(self, min: Vec3 | Sequence[float], max: Vec3 | Sequence[float], spacing: float, layers: int) -> None: ... + def add_boundary_from_mesh(self, mesh: Mesh, spacing: float) -> None: ... + def compute_density(self) -> None: ... + def compute_pressure(self) -> None: ... + def compute_forces(self) -> None: ... + def step(self, dt: float) -> None: ... + def stable_dt(self) -> float: ... + def xsph_correction(self, eps: float) -> None: ... + def shifting(self) -> None: ... + def kinetic_energy(self) -> float: ... + def potential_energy(self) -> float: ... + def total_momentum(self) -> Vec3: ... + def max_density_error(self) -> float: ... + def surface_particles(self) -> list[int]: ... + def to_density_field(self, min: Vec3 | Sequence[float], max: Vec3 | Sequence[float], res: int) -> ScalarField3: ... + def pressure_on_wall(self, wall: Plane) -> float: ... + @property + def particles(self) -> list[SphParticle]: ... + @property + def h(self) -> float: ... + @property + def rest_density(self) -> float: ... + @property + def gamma(self) -> float: ... + @property + def c0(self) -> float: ... + @property + def viscosity(self) -> float: ... + @property + def surface_tension(self) -> float: ... + @property + def gravity(self) -> Vec3: ... + @property + def kernel(self) -> Kernel: ... + @property + def dim(self) -> int: ... + @property + def scheme(self) -> SphScheme: ... + +class SphParticle: + """ +One SPH particle. + +Rust: `cfd::sph::SphParticle` + """ + def __init__(self, pos: Vec3 | Sequence[float], vel: Vec3 | Sequence[float], mass: float, rho: float, p: float, kind: Kind) -> None: ... + @property + def pos(self) -> Vec3: ... + @property + def vel(self) -> Vec3: ... + @property + def mass(self) -> float: ... + @property + def rho(self) -> float: ... + @property + def p(self) -> float: ... + @property + def kind(self) -> Kind: ... + +class SphScheme: + """ +Pressure scheme. + +Rust: `cfd::sph::SphScheme` + """ + ... + +def kernel_support(k: Kernel) -> float: + """ +Support radius (as a multiple of h) of each kernel. + +Rust: `cfd::sph::kernel_support` + """ + ... + +def kernel_w(k: Kernel, r: float, h: float, dim: int) -> float: + """ +Kernel value W(r, h). + +Rust: `cfd::sph::kernel_w` + """ + ... + +def kernel_grad(k: Kernel, r_vec: Vec3 | Sequence[float], h: float, dim: int) -> Vec3: + """ +Kernel gradient ∇W (points from the neighbor toward decreasing W). + +Rust: `cfd::sph::kernel_grad` + """ + ... + +def kernel_laplacian(k: Kernel, r: float, h: float, dim: int) -> float: + """ +Radial Laplacian ∇²W = W'' + (dim−1)/r · W′ (the Müller viscosity +kernel returns its purpose-built positive Laplacian). + +Rust: `cfd::sph::kernel_laplacian` + """ + ... + +def dam_break_2d(h: float, width: float, height: float) -> Sph: + """ +2D dam break: a water column of width `width` and height `height` in +a tank 4×width long. + +Rust: `cfd::sph::dam_break_2d` + """ + ... + +def dam_break_exact_front(t: float, h0: float, g: float) -> float: + """ +Ritter's dry-bed dam-break front position x = 2 t √(g h0) (measured +from the dam). + +Rust: `cfd::sph::dam_break_exact_front` + """ + ... + +def hydrostatic_tank(h: float, width: float, depth: float) -> Sph: + """ +Still tank of the given depth (hydrostatic pressure benchmark). + +Rust: `cfd::sph::hydrostatic_tank` + """ + ... + +def droplet_oscillation(h: float, radius: float, tension: float) -> Sph: + """ +Zero-gravity droplet with surface tension (Rayleigh oscillation +benchmark). + +Rust: `cfd::sph::droplet_oscillation` + """ + ... + +def poiseuille_sph(h: float, gap: float, force: float) -> Sph: + """ +Body-force-driven planar channel (Poiseuille) flow between two walls. + +Rust: `cfd::sph::poiseuille_sph` + """ + ... diff --git a/bindings/python/python/numeria/cfd/stable_fluids.pyi b/bindings/python/python/numeria/cfd/stable_fluids.pyi new file mode 100644 index 0000000..a131ad9 --- /dev/null +++ b/bindings/python/python/numeria/cfd/stable_fluids.pyi @@ -0,0 +1,155 @@ +""" +Stable-fluids incompressible solver on a MAC grid: MacCormack advection, implicit viscosity, buoyancy, vorticity confinement, and a pressure projection with a choice of Poisson solvers, plus classic benchmark configurations. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.cfd.grid import CellField2 +from numeria.sim.fluid_sim import EulerFluid2D +from numeria.cfd.grid import FluidBc +from numeria.statistics.distributions import Poisson +from numeria.math import Vec2 + +class PressureSolver: + """ +Pressure Poisson solver choice. + +Rust: `cfd::stable_fluids::PressureSolver` + """ + ... + +class StableFluid2: + """ +2D stable-fluids solver. + +Rust: `cfd::stable_fluids::StableFluid2` + """ + def __init__(self, nx: int, ny: int, dx: float) -> None: ... + def set_pressure_solver(self, s: PressureSolver) -> None: ... + def step(self, dt: float) -> None: ... + def diffuse(self, dt: float) -> None: ... + def apply_vorticity_confinement(self, dt: float) -> None: ... + def project(self) -> None: ... + def add_density(self, x: float, y: float, amount: float) -> None: ... + def add_heat(self, x: float, y: float, amount: float) -> None: ... + def add_velocity(self, x: float, y: float, v: Vec2 | Sequence[float]) -> None: ... + def divergence_max(self) -> float: ... + def streamlines(self, seeds: list[Vec2 | Sequence[float]], steps: int, dt: float) -> list[list[Vec2]]: ... + def particles_advect(self, pts: MutableSequence[Vec2 | Sequence[float]], dt: float) -> None: ... + def drag_on_solid(self) -> Vec2: ... + def lift_on_solid(self) -> float: ... + def vorticity_field(self) -> CellField2: ... + def stream_function(self) -> CellField2: ... + @property + def density(self) -> CellField2: ... + @property + def temperature(self) -> CellField2: ... + @property + def viscosity(self) -> float: ... + @property + def buoyancy(self) -> float: ... + @property + def vorticity_confinement(self) -> float: ... + @property + def bc(self) -> FluidBc: ... + @property + def lid_velocity(self) -> float: ... + @property + def thermal_diffusivity(self) -> float: ... + +class StableFluid3: + """ +Minimal 3D stable-fluids solver (semi-Lagrangian advection + CG +projection). + +Rust: `cfd::stable_fluids::StableFluid3` + """ + def __init__(self, nx: int, ny: int, nz: int, dx: float) -> None: ... + def step(self, dt: float) -> None: ... + def project(self) -> None: ... + @property + def density(self) -> list[float]: ... + @property + def buoyancy(self) -> float: ... + @property + def temperature(self) -> list[float]: ... + +def pressure_poisson_cg(div: list[float], solid: list[bool], nx: int, ny: int, dx: float, tol: float, max_iter: int) -> list[float]: + """ +Solve ∇²p = rhs with Neumann boundaries (and solid cells) by +conjugate gradients; the mean of `rhs` over fluid cells is removed +for compatibility and the solution has zero mean. + +Rust: `cfd::stable_fluids::pressure_poisson_cg` + """ + ... + +def poisson_neumann_cg_rect(rhs: list[float], nx: int, ny: int, dx: float, dy: float, tol: float, max_iter: int) -> list[float]: + """ +CG solve of the node-centered Neumann pressure system used by +`sim::fluid_sim::EulerFluid2D` (column-major layout +`i*ny + j`, mirror boundary nodes, anisotropic spacing). This is the +Part 3 rewire target for that solver's Poisson step. + +Rust: `cfd::stable_fluids::poisson_neumann_cg_rect` + """ + ... + +def multigrid_vcycle(rhs: list[float], levels: int, pre: int, post: int) -> list[float]: + """ +One geometric multigrid V-cycle for the Dirichlet Poisson problem +∇²u = rhs on the unit square (rhs is an n × n interior-node grid with +n = √len, spacing h = 1/(n+1)); returns the V-cycle approximation to +the solution from a zero initial guess. Iterate on the residual for a +full solve. + +Rust: `cfd::stable_fluids::multigrid_vcycle` + """ + ... + +def lid_driven_cavity(n: int, re: float, t_end: float) -> StableFluid2: + """ +Lid-driven cavity at Reynolds number `re` on an n × n unit box, +stepped to `t_end` (lid speed 1). + +Rust: `cfd::stable_fluids::lid_driven_cavity` + """ + ... + +def flow_past_cylinder(nx: int, ny: int, re: float) -> StableFluid2: + """ +Uniform inflow past a circular cylinder (diameter ~ny/5 cells) at +Reynolds number `re` (inflow speed 1). + +Rust: `cfd::stable_fluids::flow_past_cylinder` + """ + ... + +def rayleigh_benard(nx: int, ny: int, ra: float, pr: float) -> StableFluid2: + """ +Rayleigh-Bénard convection cell: Rayleigh number `ra`, Prandtl `pr`, +hot floor and cold ceiling encoded in the initial temperature. + +Rust: `cfd::stable_fluids::rayleigh_benard` + """ + ... + +def taylor_green_vortex(n: int, nu: float) -> StableFluid2: + """ +Taylor-Green vortex in a free-slip unit box: +u = sin(πx) cos(πy), v = −cos(πx) sin(πy), decaying as e^{−2π²νt}. + +Rust: `cfd::stable_fluids::taylor_green_vortex` + """ + ... + +def taylor_green_exact(x: float, y: float, t: float, nu: float) -> Vec2: + """ +Exact Taylor-Green velocity at (x, y, t). + +Rust: `cfd::stable_fluids::taylor_green_exact` + """ + ... diff --git a/bindings/python/python/numeria/cfd/turbulence.pyi b/bindings/python/python/numeria/cfd/turbulence.pyi new file mode 100644 index 0000000..8e31249 --- /dev/null +++ b/bindings/python/python/numeria/cfd/turbulence.pyi @@ -0,0 +1,384 @@ +""" +Turbulence modelling and statistics. Kolmogorov scaling, energy spectra, LES subgrid models (Smagorinsky, dynamic Smagorinsky, WALE, Vreman), vortex identification criteria, RANS models (k-epsilon, k-omega SST, Spalart-Allmaras), synthetic turbulence generation, and canonical spectra (von Karman, Pao). +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.linalg import Mat3 +from numeria.cfd.stable_fluids import StableFluid3 + +class KEpsilon: + """ +Homogeneous (0D) k-epsilon model state advanced by production balance. + +Rust: `cfd::turbulence::KEpsilon` + """ + def __init__(self, k0: float, eps0: float, nu: float, variant: KEpsilonVariant) -> None: ... + @staticmethod + def init_from_intensity(ti: float, u_mean: float, length: float, nu: float) -> KEpsilon: ... + def nu_t(self) -> float: ... + def production(self, s_mag: float) -> float: ... + def step(self, s_mag: float, dt: float) -> None: ... + def wall_function(self, u_tau: float, y: float) -> tuple[float, float]: ... + @property + def k(self) -> float: ... + @property + def epsilon(self) -> float: ... + @property + def nu(self) -> float: ... + @property + def variant(self) -> KEpsilonVariant: ... + @property + def c_mu(self) -> float: ... + @property + def c1(self) -> float: ... + @property + def c2(self) -> float: ... + @property + def sigma_k(self) -> float: ... + @property + def sigma_eps(self) -> float: ... + +class KEpsilonVariant: + """ +Which k-epsilon variant to use. + +Rust: `cfd::turbulence::KEpsilonVariant` + """ + ... + +class KOmegaSst: + """ +Homogeneous k-omega SST model (Menter 1994), blended toward k-omega near +`blend = 1` and k-epsilon at `blend = 0`. + +Rust: `cfd::turbulence::KOmegaSst` + """ + def __init__(self, k0: float, omega0: float, nu: float) -> None: ... + def nu_t(self, s_mag: float, f2: float) -> float: ... + def step(self, s_mag: float, f1: float, dt: float) -> None: ... + @property + def k(self) -> float: ... + @property + def omega(self) -> float: ... + @property + def nu(self) -> float: ... + @property + def a1(self) -> float: ... + @property + def beta_star(self) -> float: ... + +class SpalartAllmaras: + """ +Homogeneous Spalart-Allmaras one-equation model (no wall term). + +Rust: `cfd::turbulence::SpalartAllmaras` + """ + def __init__(self, nu_tilde0: float, nu: float) -> None: ... + def nu_t(self) -> float: ... + def step(self, omega_mag: float, d: float, dt: float) -> None: ... + @property + def nu_tilde(self) -> float: ... + @property + def nu(self) -> float: ... + +def kolmogorov_spectrum(k: float, dissipation: float) -> float: + """ +Kolmogorov -5/3 inertial-range energy spectrum E(k) = C eps^{2/3} k^{-5/3}. + +Rust: `cfd::turbulence::kolmogorov_spectrum` + """ + ... + +def kolmogorov_scales(nu: float, dissipation: float) -> tuple[float, float, float]: + """ +Kolmogorov length, time and velocity scales `(eta, tau, u)` from +kinematic viscosity and dissipation rate. + +Rust: `cfd::turbulence::kolmogorov_scales` + """ + ... + +def taylor_microscale(u_rms: float, nu: float, dissipation: float) -> float: + """ +Taylor microscale lambda = sqrt(15 nu u'^2 / eps). + +Rust: `cfd::turbulence::taylor_microscale` + """ + ... + +def integral_scale(u_rms: float, dissipation: float) -> float: + """ +Integral length scale estimate L = u'^3 / eps. + +Rust: `cfd::turbulence::integral_scale` + """ + ... + +def re_lambda(u_rms: float, nu: float, dissipation: float) -> float: + """ +Taylor-microscale Reynolds number Re_lambda = u' lambda / nu. + +Rust: `cfd::turbulence::re_lambda` + """ + ... + +def energy_spectrum_1d(u: list[float], dx: float) -> tuple[list[float], list[float]]: + """ +One-dimensional energy spectrum of a periodic velocity signal sampled at +spacing `dx`. Returns `(k, e_k)` with k in rad per unit length. The sum of +`e_k` times dk equals half the mean-square fluctuation. + +Rust: `cfd::turbulence::energy_spectrum_1d` + """ + ... + +def energy_spectrum_2d(u: list[float], v: list[float], n: int, dx: float) -> tuple[list[float], list[float]]: + """ +Shell-averaged 2D energy spectrum of a periodic (u, v) field on an +`n` x `n` grid with spacing `dx`. Returns `(k, e_k)` binned on integer +wavenumber shells. + +Rust: `cfd::turbulence::energy_spectrum_2d` + """ + ... + +def energy_spectrum_3d(u: list[float], v: list[float], w: list[float], n: int, dx: float) -> tuple[list[float], list[float]]: + """ +Shell-averaged 3D energy spectrum of periodic (u, v, w) on an n^3 grid +(index `(k*n + j)*n + i`) with spacing `dx`. + +Rust: `cfd::turbulence::energy_spectrum_3d` + """ + ... + +def dissipation_rate_from_spectrum(k: list[float], e: list[float], nu: float) -> float: + """ +Dissipation rate from a spectrum: eps = 2 nu integral k^2 E(k) dk +(trapezoidal). + +Rust: `cfd::turbulence::dissipation_rate_from_spectrum` + """ + ... + +def structure_function(u: list[float], sep: int, order: int) -> float: + """ +Longitudinal structure function of order `p` at separation `r` (in +samples times dx) for a periodic 1D signal: <|u(x+r) - u(x)|^p>. + +Rust: `cfd::turbulence::structure_function` + """ + ... + +def two_point_correlation(u: list[float], sep: int) -> float: + """ +Two-point autocorrelation of a periodic 1D signal at separation `sep` +(samples), normalized to R(0) = 1. + +Rust: `cfd::turbulence::two_point_correlation` + """ + ... + +def strain_tensor(g: Mat3) -> Mat3: + """ +Symmetric strain-rate tensor S = (grad u + grad u^T)/2 from a velocity +gradient tensor g where `g.data[i][j] = du_i/dx_j`. + +Rust: `cfd::turbulence::strain_tensor` + """ + ... + +def rotation_tensor(g: Mat3) -> Mat3: + """ +Antisymmetric rotation-rate tensor W = (grad u - grad u^T)/2. + +Rust: `cfd::turbulence::rotation_tensor` + """ + ... + +def smagorinsky_nu_t(g: Mat3, delta: float, cs: float) -> float: + """ +Smagorinsky eddy viscosity nu_t = (Cs Delta)^2 |S|, |S| = sqrt(2 S:S). + +Rust: `cfd::turbulence::smagorinsky_nu_t` + """ + ... + +def dynamic_smagorinsky_cs(l: Mat3, m: Mat3) -> float: + """ +Germano-Lilly dynamic Smagorinsky coefficient from resolved and +test-filtered fields. `l` is the Leonard stress tensor and `m` the model +difference tensor; returns Cs^2 = / clipped at zero. + +Rust: `cfd::turbulence::dynamic_smagorinsky_cs` + """ + ... + +def wale_nu_t(g: Mat3, delta: float, cw: float) -> float: + """ +WALE subgrid eddy viscosity (Nicoud & Ducros 1999) with constant `cw` +(typically 0.325 to 0.5). + +Rust: `cfd::turbulence::wale_nu_t` + """ + ... + +def vreman_nu_t(g: Mat3, delta: float, c: float) -> float: + """ +Vreman subgrid eddy viscosity (Vreman 2004) with constant `c` +(approximately 2.5 Cs^2, so about 0.07). + +Rust: `cfd::turbulence::vreman_nu_t` + """ + ... + +def q_criterion(g: Mat3) -> float: + """ +Q-criterion: Q = (|W|^2 - |S|^2)/2. Positive Q marks vortical regions. + +Rust: `cfd::turbulence::q_criterion` + """ + ... + +def lambda2_criterion(g: Mat3) -> float: + """ +Lambda-2 criterion: middle eigenvalue of S^2 + W^2. Negative values mark +vortex cores. + +Rust: `cfd::turbulence::lambda2_criterion` + """ + ... + +def delta_criterion(g: Mat3) -> float: + """ +Delta criterion: Delta = (Q/3)^3 + (det(g)/2)^2 > 0 marks complex +eigenvalues of the velocity gradient (swirling motion). + +Rust: `cfd::turbulence::delta_criterion` + """ + ... + +def vortex_identify_q(u: list[float], v: list[float], n: int, dx: float, threshold: float) -> list[bool]: + """ +Identify vortical cells in a 2D periodic velocity field on an n x n grid +(spacing `dx`) by the 2D Q-criterion; returns flags where Q > `threshold`. + +Rust: `cfd::turbulence::vortex_identify_q` + """ + ... + +def turbulence_intensity(u: list[float]) -> float: + """ +Turbulence intensity: rms fluctuation over mean speed. + +Rust: `cfd::turbulence::turbulence_intensity` + """ + ... + +def reynolds_stress(u: list[float], v: list[float]) -> float: + """ +Reynolds stress component from paired samples. + +Rust: `cfd::turbulence::reynolds_stress` + """ + ... + +def synthetic_turbulence_kraichnan(n: int, l: float, n_modes: int, k_peak: float, u_rms: float, seed: int) -> tuple[list[float], list[float]]: + """ +Divergence-free synthetic 2D turbulence (Kraichnan-style random Fourier +modes) on an n x n periodic grid of size `l`. Each of `n_modes` modes has a +random wavevector with magnitude near `k_peak` and an amplitude direction +perpendicular to it. Returns `(u, v)`. + +Rust: `cfd::turbulence::synthetic_turbulence_kraichnan` + """ + ... + +def synthetic_eddy_method(n_pts: int, l: float, n_eddies: int, eddy_size: float, u_rms: float, seed: int) -> list[float]: + """ +Synthetic eddy method: superpose `n_eddies` compact Gaussian eddies with +random centers and signs in a periodic box of size `l`, sampled at `n_pts` +points along a line. Returns a fluctuation signal scaled to `u_rms`. + +Rust: `cfd::turbulence::synthetic_eddy_method` + """ + ... + +def von_karman_spectrum(k: float, k_energy: float, ke: float, k_eta: float) -> float: + """ +Von Karman model spectrum with integral-scale wavenumber `ke` and +Kolmogorov cutoff `k_eta`: +E(k) ~ (k/ke)^4 / (1 + (k/ke)^2)^{17/6} * exp(-2 (k/k_eta)^2) scaled so +the peak region carries energy `k_energy` overall (approximate). + +Rust: `cfd::turbulence::von_karman_spectrum` + """ + ... + +def pao_spectrum(k: float, dissipation: float, nu: float) -> float: + """ +Pao dissipation-range spectrum: +E(k) = C eps^{2/3} k^{-5/3} exp(-1.5 C (k eta)^{4/3}). + +Rust: `cfd::turbulence::pao_spectrum` + """ + ... + +def log_law_fit(y: list[float], u: list[float], nu: float) -> tuple[float, float]: + """ +Fit the log law u+ = (1/kappa) ln y+ + B to a profile `(y, u)` for a fluid +of viscosity `nu`, returning `(u_tau, b)`. The friction velocity comes from +the slope of u versus ln y: u_tau = kappa * slope. + +Rust: `cfd::turbulence::log_law_fit` + """ + ... + +def channel_flow_dns_reference(re_tau: float, n: int) -> tuple[list[float], list[float]]: + """ +Reference mean-velocity profile for turbulent channel flow at friction +Reynolds number `re_tau`: Reichardt's composite profile evaluated at +`n` points from the wall to the centerline. Returns `(y_plus, u_plus)`. + +Rust: `cfd::turbulence::channel_flow_dns_reference` + """ + ... + +def inertial_range_exponent(k: list[float], e: list[float], k_lo: float, k_hi: float) -> float: + """ +Log-log slope of a spectrum over the wavenumber band `[k_lo, k_hi]` +(least squares). For an inertial range this returns about -5/3. + +Rust: `cfd::turbulence::inertial_range_exponent` + """ + ... + +def richardson_cascade_time(l: float, u_l: float) -> float: + """ +Eddy-turnover (cascade) time at scale `l` with velocity `u_l`: +tau = l / u_l. + +Rust: `cfd::turbulence::richardson_cascade_time` + """ + ... + +def turbulent_diffusivity(nu_t: float, pr_t: float) -> float: + """ +Turbulent diffusivity nu_t / Pr_t. + +Rust: `cfd::turbulence::turbulent_diffusivity` + """ + ... + +def decaying_isotropic_turbulence(n: int, nu: float, t_end: float) -> StableFluid3: + """ +Set up a decaying isotropic turbulence run: a StableFluid3 on an n^3 +periodic box seeded with divergence-free random modes, advanced to +`t_end`. Returns the fluid for inspection. Keep `n` small (16 or so). + +Rust: `cfd::turbulence::decaying_isotropic_turbulence` + """ + ... diff --git a/bindings/python/python/numeria/cfd/vortex.pyi b/bindings/python/python/numeria/cfd/vortex.pyi new file mode 100644 index 0000000..acabd09 --- /dev/null +++ b/bindings/python/python/numeria/cfd/vortex.pyi @@ -0,0 +1,232 @@ +""" +Vortex methods: regularized Biot-Savart particle methods in 2D and 3D, classical vortex solutions (Lamb-Oseen, Rankine, Burgers, Hill), point vortex dynamics with a symplectic integrator, and vortex phenomenology (shedding, Crow instability, tip vortices). +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.statistics.distributions import Gamma +from numeria.cfd.grid import MacGrid2 +from numeria.math import Vec2 +from numeria.math import Vec3 + +class VortexKernel: + """ +Regularization kernel for the Biot-Savart sum. + +Rust: `cfd::vortex::VortexKernel` + """ + ... + +class VortexMethod2: + """ +2D vortex blob method: particles carry scalar circulation. + +Rust: `cfd::vortex::VortexMethod2` + """ + def __init__(self, particles: list[tuple[Vec2 | Sequence[float], float]], delta: float) -> None: ... + def velocity_at(self, p: Vec2 | Sequence[float]) -> Vec2: ... + def step(self, dt: float, nu: float) -> None: ... + @staticmethod + def vortex_sheet(n: int, gamma_total: float, length: float, delta: float) -> VortexMethod2: ... + @staticmethod + def kelvin_helmholtz_roll_up(n: int, delta_u: float, wavelength: float, amp: float) -> VortexMethod2: ... + @staticmethod + def point_vortex_pair(gamma: float, d: float) -> VortexMethod2: ... + @staticmethod + def lamb_oseen_init(gamma: float, r_c: float, n: int) -> VortexMethod2: ... + def merge_near(self, eps: float) -> None: ... + def to_grid(self, nx: int, ny: int) -> MacGrid2: ... + @property + def particles(self) -> list[tuple[Vec2, float]]: ... + @property + def delta(self) -> float: ... + +class VortexMethod3: + """ +3D vortex particle method with direct Biot-Savart summation. + +Rust: `cfd::vortex::VortexMethod3` + """ + def __init__(self, particles: list[VortexParticle], kernel: VortexKernel) -> None: ... + def velocity_at(self, p: Vec3 | Sequence[float]) -> Vec3: ... + def step(self, dt: float, nu: float) -> None: ... + @staticmethod + def vortex_ring(center: Vec3 | Sequence[float], radius: float, strength: float, core: float, n: int) -> VortexMethod3: ... + @staticmethod + def two_rings_leapfrog(radius: float, strength: float, core: float, gap: float, n: int) -> VortexMethod3: ... + def helicity(self) -> float: ... + def enstrophy(self) -> float: ... + def kinetic_energy(self) -> float: ... + def impulse(self) -> Vec3: ... + def remesh(self, spacing: float) -> None: ... + @property + def particles(self) -> list[VortexParticle]: ... + @property + def kernel(self) -> VortexKernel: ... + +class VortexParticle: + """ +A vector-valued vortex particle: strength is circulation times length +(the integral of vorticity over the particle's volume). + +Rust: `cfd::vortex::VortexParticle` + """ + def __init__(self, pos: Vec3 | Sequence[float], strength: Vec3 | Sequence[float], core: float) -> None: ... + @property + def pos(self) -> Vec3: ... + @property + def strength(self) -> Vec3: ... + @property + def core(self) -> float: ... + +def biot_savart_segment(p: Vec3 | Sequence[float], a: Vec3 | Sequence[float], b: Vec3 | Sequence[float], gamma: float) -> Vec3: + """ +Velocity induced at `p` by a straight vortex segment from `a` to `b` +carrying circulation `gamma`. + +Rust: `cfd::vortex::biot_savart_segment` + """ + ... + +def biot_savart_ring(p: Vec3 | Sequence[float], center: Vec3 | Sequence[float], radius: float, gamma: float, normal: Vec3 | Sequence[float], n_seg: int) -> Vec3: + """ +Velocity induced at `p` by a circular vortex ring discretized into +`n_seg` straight segments. + +Rust: `cfd::vortex::biot_savart_ring` + """ + ... + +def vortex_ring_self_velocity(gamma: float, r: float, core: float) -> float: + """ +Kelvin's formula for the self-induced translation speed of a thin vortex +ring: U = Gamma/(4 pi R) (ln(8R/a) - 1/4). + +Rust: `cfd::vortex::vortex_ring_self_velocity` + """ + ... + +def lamb_oseen_velocity(r: float, t: float, gamma: float, nu: float) -> float: + """ +Lamb-Oseen azimuthal velocity at radius `r` and time `t`: +v = Gamma/(2 pi r) (1 - exp(-r^2/(4 nu t))). + +Rust: `cfd::vortex::lamb_oseen_velocity` + """ + ... + +def rankine_vortex(r: float, r_core: float, gamma: float) -> float: + """ +Rankine vortex: solid-body rotation inside `r_core`, potential outside. + +Rust: `cfd::vortex::rankine_vortex` + """ + ... + +def burgers_vortex(r: float, gamma: float, nu: float, strain: float) -> float: + """ +Burgers vortex azimuthal velocity: the steady balance of diffusion +against axial strain `strain` (units 1/s): +v = Gamma/(2 pi r) (1 - exp(-strain r^2/(4 nu))). + +Rust: `cfd::vortex::burgers_vortex` + """ + ... + +def hill_spherical_vortex(p: Vec3 | Sequence[float], u: float, a: float) -> Vec3: + """ +Hill's spherical vortex of radius `a` in the co-moving frame: far-field +velocity is -u along z, the sphere surface is a stream surface, and the +poles are stagnation points. + +Rust: `cfd::vortex::hill_spherical_vortex` + """ + ... + +def vortex_pair_velocity(gamma: float, d: float) -> float: + """ +Translation speed of a counter-rotating vortex pair: Gamma/(2 pi d). + +Rust: `cfd::vortex::vortex_pair_velocity` + """ + ... + +def point_vortex_hamiltonian(pos: list[Vec2 | Sequence[float]], gammas: list[float]) -> float: + """ +Point-vortex Hamiltonian H = -(1/4 pi) sum_{i None: + """ +One implicit-midpoint step of the point-vortex system (symplectic; the +Hamiltonian error stays bounded over long integrations). + +Rust: `cfd::vortex::point_vortex_step` + """ + ... + +def kelvin_helmholtz_growth_exact(k: float, delta_u: float) -> float: + """ +Inviscid Kelvin-Helmholtz growth rate for a velocity jump `delta_u`: +sigma = k delta_u / 2. + +Rust: `cfd::vortex::kelvin_helmholtz_growth_exact` + """ + ... + +def vortex_shedding_frequency(strouhal: float, u: float, d: float) -> float: + """ +Shedding frequency f = St U / D. + +Rust: `cfd::vortex::vortex_shedding_frequency` + """ + ... + +def strouhal_from_re(re: float) -> float: + """ +Roshko-style Strouhal-Reynolds correlation for a circular cylinder. + +Rust: `cfd::vortex::strouhal_from_re` + """ + ... + +def crow_instability_growth(b: float, gamma: float, core: float) -> float: + """ +Crow instability growth rate for a trailing-vortex pair of spacing `b` +(approximate peak rate ~0.83 Gamma/(2 pi b^2), weakly dependent on core). + +Rust: `cfd::vortex::crow_instability_growth` + """ + ... + +def tip_vortex_decay(gamma: float, r_core0: float, nu: float, t: float) -> float: + """ +Peak swirl velocity of a decaying tip vortex at time `t` (Lamb-Oseen core +growth from initial core radius `r_core0`). + +Rust: `cfd::vortex::tip_vortex_decay` + """ + ... + +def helicity_density(u: Vec3 | Sequence[float], omega: Vec3 | Sequence[float]) -> float: + """ +Helicity density u . omega. + +Rust: `cfd::vortex::helicity_density` + """ + ... + +def vortex_line_trace(omega_field: Callable[[Vec3 | Sequence[float]], Vec3 | Sequence[float]], seed: Vec3 | Sequence[float], steps: int, ds: float) -> list[Vec3]: + """ +Trace a vortex line through a vorticity field by RK4 along the normalized +field direction, with arc-length step `ds`. + +Rust: `cfd::vortex::vortex_line_trace` + """ + ... diff --git a/bindings/python/python/numeria/chemistry.pyi b/bindings/python/python/numeria/chemistry.pyi new file mode 100644 index 0000000..1cccb8f --- /dev/null +++ b/bindings/python/python/numeria/chemistry.pyi @@ -0,0 +1,155 @@ +""" +Reaction kinetics, chemical thermodynamics and electrochemistry. Rate laws for first- and second-order decay and the Arrhenius temperature dependence `k = A exp(−Eₐ/RT)`; the Gibbs free energy and its relation to the equilibrium constant, `ΔG° = −RT ln K`, with the van 't Hoff equation for how K moves with temperature; and Hess's law. Electrochemistry covers the Nernst equation, cell potentials and Faraday electrolysis. Solution chemistry covers pH and pOH, molarity, dilution and osmotic pressure. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def arrhenius_rate(pre_exponential: float, activation_energy: float, temperature: float) -> float: + """ +Arrhenius equation: k = A × exp(-Ea / (RT)) + +Rust: `chemistry::arrhenius_rate` + """ + ... + +def half_life_first_order(rate_constant: float) -> float: + """ +First-order half-life: t½ = ln(2) / k + +Rust: `chemistry::half_life_first_order` + """ + ... + +def concentration_first_order(c0: float, rate_constant: float, time: float) -> float: + """ +First-order concentration decay: `[A] = [A]₀ × e^(-kt)` + +Rust: `chemistry::concentration_first_order` + """ + ... + +def concentration_second_order(c0: float, rate_constant: float, time: float) -> float: + """ +Second-order integrated rate law: `1/[A] = 1/[A]₀ + kt`, returns `[A]` + +Rust: `chemistry::concentration_second_order` + """ + ... + +def reaction_rate(k: float, concentrations: list[float], orders: list[float]) -> float: + """ +General rate law: `r = k × Π([Ci]^ni)` + +Rust: `chemistry::reaction_rate` + """ + ... + +def gibbs_free_energy(enthalpy: float, temperature: float, entropy: float) -> float: + """ +Gibbs free energy: ΔG = ΔH - TΔS + +Rust: `chemistry::gibbs_free_energy` + """ + ... + +def equilibrium_constant_from_gibbs(delta_g: float, temperature: float) -> float: + """ +Equilibrium constant from Gibbs energy: K = exp(-ΔG / (RT)) + +Rust: `chemistry::equilibrium_constant_from_gibbs` + """ + ... + +def vant_hoff(k1: float, delta_h: float, t1: float, t2: float) -> float: + """ +Van't Hoff equation: ln(K2/K1) = -ΔH/R × (1/T2 - 1/T1), returns K2 + +Rust: `chemistry::vant_hoff` + """ + ... + +def hess_law(enthalpies: list[float], coefficients: list[float]) -> float: + """ +Hess's law: ΔH_rxn = Σ(ci × ΔHi) + +Rust: `chemistry::hess_law` + """ + ... + +def nernst_potential(e_standard: float, temperature: float, n_electrons: float, reaction_quotient: float) -> float: + """ +Nernst equation: E = E° - (RT / (nF)) × ln(Q) + +Rust: `chemistry::nernst_potential` + """ + ... + +def cell_potential(e_cathode: float, e_anode: float) -> float: + """ +Cell potential: E_cell = E_cathode - E_anode + +Rust: `chemistry::cell_potential` + """ + ... + +def faraday_electrolysis(current: float, time: float, molar_mass: float, n_electrons: float) -> float: + """ +Faraday's law of electrolysis: m = (I × t × M) / (n × F) + +Rust: `chemistry::faraday_electrolysis` + """ + ... + +def ph(h_concentration: float) -> float: + """ +pH = -log₁₀([H⁺]) + +Rust: `chemistry::ph` + """ + ... + +def poh(oh_concentration: float) -> float: + """ +pOH = -log₁₀([OH⁻]) + +Rust: `chemistry::poh` + """ + ... + +def h_from_ph(ph: float) -> float: + """ +[H⁺] = 10^(-pH) + +Rust: `chemistry::h_from_ph` + """ + ... + +def osmotic_pressure(molarity: float, temperature: float, i_factor: float) -> float: + """ +Osmotic pressure: Π = iMRT + +Rust: `chemistry::osmotic_pressure` + """ + ... + +def molarity(moles: float, volume_liters: float) -> float: + """ +Molarity: M = n / V + +Rust: `chemistry::molarity` + """ + ... + +def dilution(c1: float, v1: float, v2: float) -> float: + """ +Dilution: C2 = C1 × V1 / V2 + +Rust: `chemistry::dilution` + """ + ... + +FARADAY: float diff --git a/bindings/python/python/numeria/classical.pyi b/bindings/python/python/numeria/classical.pyi new file mode 100644 index 0000000..793d342 --- /dev/null +++ b/bindings/python/python/numeria/classical.pyi @@ -0,0 +1,447 @@ +""" +Newtonian mechanics: kinematics, dynamics, and the harmonic oscillator. Linear and rotational motion under constant acceleration, forces and momentum, work, energy and power, collisions in one dimension from perfectly elastic to perfectly inelastic, moments of inertia for the standard bodies, and circular motion. The oscillator section runs from the undamped period through the damped response -- damping ratio, logarithmic decrement, quality factor -- to the driven steady state and its resonance, and ends with the normal frequencies of two coupled oscillators. For the same problem solved numerically, or with more than two masses, see `resonance`. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.resonance.oscillator import Damping +from numeria.statistics.distributions import Normal +from numeria.math import Vec3 + +def displacement(initial_velocity: float, acceleration: float, time: float) -> float: + """ +Position after uniform acceleration: x = x0 + v0*t + 0.5*a*t^2 + +Rust: `classical::displacement` + """ + ... + +def velocity(initial_velocity: float, acceleration: float, time: float) -> float: + """ +Velocity after uniform acceleration: v = v0 + a*t + +Rust: `classical::velocity` + """ + ... + +def velocity_squared(initial_velocity: float, acceleration: float, displacement: float) -> float: + """ +Velocity squared: v^2 = v0^2 + 2*a*d + +Rust: `classical::velocity_squared` + """ + ... + +def position_3d(pos: Vec3 | Sequence[float], vel: Vec3 | Sequence[float], acc: Vec3 | Sequence[float], t: float) -> Vec3: + """ +3D position under constant acceleration. + +Rust: `classical::position_3d` + """ + ... + +def velocity_3d(vel: Vec3 | Sequence[float], acc: Vec3 | Sequence[float], t: float) -> Vec3: + """ +3D velocity under constant acceleration. + +Rust: `classical::velocity_3d` + """ + ... + +def projectile_range(speed: float, angle_rad: float, g: float) -> float: + """ +Range of a projectile on flat ground: R = v^2 * sin(2θ) / g + +Rust: `classical::projectile_range` + """ + ... + +def projectile_max_height(speed: float, angle_rad: float, g: float) -> float: + """ +Maximum height of a projectile: H = v^2 * sin^2(θ) / (2g) + +Rust: `classical::projectile_max_height` + """ + ... + +def projectile_time_of_flight(speed: float, angle_rad: float, g: float) -> float: + """ +Time of flight for a projectile on flat ground: T = 2v*sin(θ) / g + +Rust: `classical::projectile_time_of_flight` + """ + ... + +def force(mass: float, acceleration: float) -> float: + """ +Force = mass * acceleration (Newton's second law) + +Rust: `classical::force` + """ + ... + +def force_3d(mass: float, acceleration: Vec3 | Sequence[float]) -> Vec3: + """ +F = ma as vectors + +Rust: `classical::force_3d` + """ + ... + +def acceleration(force: float, mass: float) -> float: + """ +Acceleration from force: a = F / m + +Rust: `classical::acceleration` + """ + ... + +def weight(mass: float, g: float) -> float: + """ +Weight: W = m * g + +Rust: `classical::weight` + """ + ... + +def momentum(mass: float, velocity: float) -> float: + """ +Linear momentum: p = m * v + +Rust: `classical::momentum` + """ + ... + +def momentum_3d(mass: float, velocity: Vec3 | Sequence[float]) -> Vec3: + """ +3D momentum. + +Rust: `classical::momentum_3d` + """ + ... + +def impulse(force: float, delta_t: float) -> float: + """ +Impulse: J = F * Δt + +Rust: `classical::impulse` + """ + ... + +def elastic_collision_1d(m1: float, v1: float, m2: float, v2: float) -> tuple[float, float]: + """ +Final velocities after a 1D elastic collision between two masses. +Returns (v1_final, v2_final). + +Rust: `classical::elastic_collision_1d` + """ + ... + +def inelastic_collision_1d(m1: float, v1: float, m2: float, v2: float) -> float: + """ +Final velocity after a perfectly inelastic collision (objects stick together). + +Rust: `classical::inelastic_collision_1d` + """ + ... + +def coefficient_of_restitution(v1i: float, v2i: float, v1f: float, v2f: float) -> float: + """ +Coefficient of restitution: e = -(v1f - v2f) / (v1i - v2i) + +Rust: `classical::coefficient_of_restitution` + """ + ... + +def kinetic_energy(mass: float, speed: float) -> float: + """ +Kinetic energy: KE = 0.5 * m * v^2 + +Rust: `classical::kinetic_energy` + """ + ... + +def potential_energy_gravity(mass: float, g: float, height: float) -> float: + """ +Gravitational potential energy: PE = m * g * h + +Rust: `classical::potential_energy_gravity` + """ + ... + +def potential_energy_spring(spring_constant: float, displacement: float) -> float: + """ +Elastic potential energy: PE = 0.5 * k * x^2 + +Rust: `classical::potential_energy_spring` + """ + ... + +def work(force: float, displacement: float, angle_rad: float) -> float: + """ +Work: W = F * d * cos(θ) + +Rust: `classical::work` + """ + ... + +def power(work: float, time: float) -> float: + """ +Power: P = W / t + +Rust: `classical::power` + """ + ... + +def power_instantaneous(force: float, velocity: float) -> float: + """ +Power (instantaneous): P = F * v + +Rust: `classical::power_instantaneous` + """ + ... + +def angular_velocity(delta_theta: float, delta_t: float) -> float: + """ +Angular velocity: ω = Δθ / Δt + +Rust: `classical::angular_velocity` + """ + ... + +def angular_acceleration(delta_omega: float, delta_t: float) -> float: + """ +Angular acceleration: α = Δω / Δt + +Rust: `classical::angular_acceleration` + """ + ... + +def torque(radius: float, force: float, angle_rad: float) -> float: + """ +Torque: τ = r * F * sin(θ) + +Rust: `classical::torque` + """ + ... + +def torque_3d(r: Vec3 | Sequence[float], f: Vec3 | Sequence[float]) -> Vec3: + """ +Torque as cross product: τ = r × F + +Rust: `classical::torque_3d` + """ + ... + +def moment_of_inertia_point(mass: float, radius: float) -> float: + """ +Moment of inertia of a point mass: I = m * r^2 + +Rust: `classical::moment_of_inertia_point` + """ + ... + +def moment_of_inertia_solid_sphere(mass: float, radius: float) -> float: + """ +Moment of inertia of a solid sphere: I = (2/5) * m * r^2 + +Rust: `classical::moment_of_inertia_solid_sphere` + """ + ... + +def moment_of_inertia_solid_cylinder(mass: float, radius: float) -> float: + """ +Moment of inertia of a solid cylinder about its axis: I = (1/2) * m * r^2 + +Rust: `classical::moment_of_inertia_solid_cylinder` + """ + ... + +def rotational_kinetic_energy(moment_of_inertia: float, angular_velocity: float) -> float: + """ +Rotational kinetic energy: KE = 0.5 * I * ω^2 + +Rust: `classical::rotational_kinetic_energy` + """ + ... + +def angular_momentum(moment_of_inertia: float, angular_velocity: float) -> float: + """ +Angular momentum: L = I * ω + +Rust: `classical::angular_momentum` + """ + ... + +def centripetal_acceleration(speed: float, radius: float) -> float: + """ +Centripetal acceleration: a = v^2 / r + +Rust: `classical::centripetal_acceleration` + """ + ... + +def centripetal_force(mass: float, speed: float, radius: float) -> float: + """ +Centripetal force: F = m * v^2 / r + +Rust: `classical::centripetal_force` + """ + ... + +def friction_force(coefficient: float, normal_force: float) -> float: + """ +Friction force: f = μ * N + +Rust: `classical::friction_force` + """ + ... + +def shm_period_spring(mass: float, spring_constant: float) -> float: + """ +Period of a mass-spring system: T = 2π * sqrt(m / k) + +Rust: `classical::shm_period_spring` + """ + ... + +def shm_period_pendulum(length: float, g: float) -> float: + """ +Period of a simple pendulum: T = 2π * sqrt(L / g) + +Rust: `classical::shm_period_pendulum` + """ + ... + +def shm_position(amplitude: float, angular_freq: float, time: float, phase: float) -> float: + """ +Position of SHM: x(t) = A * cos(ωt + φ) + +Rust: `classical::shm_position` + """ + ... + +def shm_velocity(amplitude: float, angular_freq: float, time: float, phase: float) -> float: + """ +Velocity of SHM: v(t) = -A * ω * sin(ωt + φ) + +Rust: `classical::shm_velocity` + """ + ... + +def damped_frequency(natural_freq: float, damping_ratio: float) -> float: + """ +Damped angular frequency: ωd = ω₀√(1 - ζ²), returns 0 if overdamped (ζ ≥ 1) + +Rust: `classical::damped_frequency` + """ + ... + +def damped_amplitude(initial_amplitude: float, damping_coeff: float, time: float) -> float: + """ +Damped amplitude: A(t) = A₀ × e^(-γt) + +Rust: `classical::damped_amplitude` + """ + ... + +def damped_position(amplitude: float, damping_coeff: float, angular_freq: float, time: float, phase: float) -> float: + """ +Damped oscillation position: x(t) = A₀e^(-γt)cos(ωdt + φ) + +Rust: `classical::damped_position` + """ + ... + +def damping_ratio(damping_coeff: float, mass: float, spring_constant: float) -> float: + """ +Damping ratio: ζ = c / (2√(mk)) + +Rust: `classical::damping_ratio` + """ + ... + +def critical_damping(mass: float, spring_constant: float) -> float: + """ +Critical damping coefficient: c_crit = 2√(mk) + +Rust: `classical::critical_damping` + """ + ... + +def logarithmic_decrement(damping_ratio: float) -> float: + """ +Logarithmic decrement: δ = 2πζ / √(1 - ζ²) + +Rust: `classical::logarithmic_decrement` + """ + ... + +def quality_factor(damping_ratio: float) -> float: + """ +Quality factor: Q = 1 / (2ζ) + +Rust: `classical::quality_factor` + """ + ... + +def decay_time(damping_coeff: float) -> float: + """ +Decay time constant: τ = 1/γ (time for amplitude to drop to 1/e) + +Rust: `classical::decay_time` + """ + ... + +def driven_amplitude(f0: float, omega: float, omega0: float, gamma: float) -> float: + """ +Driven oscillation amplitude: A = f₀ / √((ω₀²-ω²)² + (2γω)²) +where f₀ = F₀/m (driving force per unit mass) + +Rust: `classical::driven_amplitude` + """ + ... + +def driven_phase(omega: float, omega0: float, gamma: float) -> float: + """ +Phase lag of driven oscillation: φ = atan2(2γω, ω₀²-ω²) + +Rust: `classical::driven_phase` + """ + ... + +def resonance_frequency(natural_freq: float, damping_coeff: float) -> float: + """ +Resonance frequency: ωr = √(ω₀² - 2γ²), returns 0 if overdamped + +Rust: `classical::resonance_frequency` + """ + ... + +def resonance_amplitude(f0: float, omega0: float, gamma: float) -> float: + """ +Peak amplitude at resonance: A_max = f₀ / (2γ√(ω₀² - γ²)) + +Rust: `classical::resonance_amplitude` + """ + ... + +def coupled_normal_frequencies(k: float, k_coupling: float, m: float) -> tuple[float, float]: + """ +Normal-mode frequencies of two identical masses coupled by a spring: +ω₁ = √(k/m), ω₂ = √((k + 2k_c)/m) + +Rust: `classical::coupled_normal_frequencies` + """ + ... + +def beat_frequency_coupled(freq1: float, freq2: float) -> float: + """ +Beat frequency of coupled oscillators: f_beat = |f1 - f2| + +Rust: `classical::beat_frequency_coupled` + """ + ... diff --git a/bindings/python/python/numeria/codes/__init__.pyi b/bindings/python/python/numeria/codes/__init__.pyi new file mode 100644 index 0000000..e133a9c --- /dev/null +++ b/bindings/python/python/numeria/codes/__init__.pyi @@ -0,0 +1,12 @@ +""" +Error detection, error correction, compression, and the arithmetic cryptography is built on. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from . import block, checksum, compression, convolutional, crypto_math, reed_solomon + + diff --git a/bindings/python/python/numeria/codes/block.pyi b/bindings/python/python/numeria/codes/block.pyi new file mode 100644 index 0000000..24e7af0 --- /dev/null +++ b/bindings/python/python/numeria/codes/block.pyi @@ -0,0 +1,259 @@ +""" +Binary linear block codes. A linear code of length `n` and dimension `k` is a `k`-dimensional subspace of `GF(2)^n`. Everything follows from that one sentence. The subspace is described either by a basis -- the rows of a generator matrix `G` -- or by the equations that cut it out -- the rows of a parity check matrix `H`, with `C = { x : H x' = 0 }`. Encoding is a matrix product. Decoding is the observation that `H (c + e)' = H e'`, so the syndrome depends only on the error and not on what was sent: correcting is choosing the lightest error pattern with the observed syndrome. Linearity is also what makes the minimum distance computable at all. The distance between two codewords is the weight of their difference, which is another codeword, so the minimum distance over all `2^k (2^k - 1) / 2` pairs is just the minimum weight over the `2^k - 1` non-zero codewords. The `_small` routines enumerate the whole code and are exponential in `k` by construction; they are for the classical codes, which are small. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.monte_carlo import Rng + +class Gf2Matrix: + """ +A matrix over `GF(2)`, one bit per entry, packed sixty-four to a word. + +Packing is not only for space: a row operation becomes a handful of word +XORs rather than a loop over bits, so elimination on a code-sized matrix +costs what a floating-point elimination on a matrix sixty-four times +smaller would. + +Rust: `codes::block::Gf2Matrix` + """ + def __init__(self, rows: int, cols: int, data: list[int]) -> None: ... + def words_per_row(self) -> int: ... + @staticmethod + def zeros(rows: int, cols: int) -> Gf2Matrix: ... + @staticmethod + def identity(n: int) -> Gf2Matrix: ... + @staticmethod + def from_rows(rows: list[list[bool]]) -> Gf2Matrix: ... + def get(self, r: int, c: int) -> bool: ... + def set(self, r: int, c: int, value: bool) -> None: ... + def row(self, r: int) -> list[bool]: ... + def to_rows(self) -> list[list[bool]]: ... + def rref(self) -> tuple[Gf2Matrix, list[int]]: ... + def rank(self) -> int: ... + def transpose(self) -> Gf2Matrix: ... + def mul(self, other: Gf2Matrix) -> Gf2Matrix: ... + def mul_vec(self, x: list[bool]) -> list[bool]: ... + def vec_mul(self, x: list[bool]) -> list[bool]: ... + def solve(self, b: list[bool]) -> Optional[list[bool]]: ... + def kernel_basis(self) -> list[list[bool]]: ... + @property + def rows(self) -> int: ... + @property + def cols(self) -> int: ... + @property + def data(self) -> list[int]: ... + +class LinearCode: + """ +A binary linear code, held by both of its descriptions. + +`g` is `k` by `n` and its rows are a basis of the code; `h` is `n - k` by +`n` and its rows are a basis of the dual, so `G H'` is zero and a word is +a codeword exactly when its syndrome vanishes. + +Rust: `codes::block::LinearCode` + """ + def __init__(self, g: Gf2Matrix, h: Gf2Matrix, n: int, k: int, d: int) -> None: ... + @staticmethod + def from_generator(g: Gf2Matrix) -> LinearCode: ... + def codewords(self) -> list[list[bool]]: ... + def encode(self, msg: list[bool]) -> list[bool]: ... + def syndrome(self, recv: list[bool]) -> list[bool]: ... + def contains(self, x: list[bool]) -> bool: ... + def minimum_distance_small(self) -> int: ... + def weight_enumerator(self) -> list[int]: ... + def dual(self) -> LinearCode: ... + def is_self_dual(self) -> bool: ... + def decode_syndrome(self, recv: list[bool]) -> tuple[list[bool], int]: ... + def standard_array_decode_small(self, recv: list[bool]) -> tuple[list[bool], int]: ... + @staticmethod + def hamming(r: int) -> LinearCode: ... + @staticmethod + def extended_hamming(r: int) -> LinearCode: ... + @staticmethod + def repetition(n: int) -> LinearCode: ... + @staticmethod + def parity_check(n: int) -> LinearCode: ... + @staticmethod + def golay23() -> LinearCode: ... + @staticmethod + def golay24() -> LinearCode: ... + @staticmethod + def reed_muller(r: int, m: int) -> LinearCode: ... + @property + def g(self) -> Gf2Matrix: ... + @property + def h(self) -> Gf2Matrix: ... + @property + def n(self) -> int: ... + @property + def k(self) -> int: ... + @property + def d(self) -> int: ... + +def weight(v: list[bool]) -> int: + """ +The number of ones in a bit vector: its Hamming weight. + +Rust: `codes::block::weight` + """ + ... + +def xor(a: list[bool], b: list[bool]) -> list[bool]: + """ +The bitwise difference of two equal-length vectors. + +Panics: +Panics unless the lengths agree. + +Rust: `codes::block::xor` + """ + ... + +def hamming_74_encode(nibble: int) -> int: + """ +Hamming(7, 4) encoding: four data bits in, seven out. + +The classical layout, with the parity bits at the powers of two: position +one, two and four, counting from one at the least significant bit of the +result. Parity bit `b` covers exactly the positions whose index has bit +`b` set, so the three parity checks of a corrupted word spell out the +binary numeral of the corrupted position. + +Panics: +Panics if `nibble` has anything above its low four bits. + +Rust: `codes::block::hamming_74_encode` + """ + ... + +def hamming_74_decode(byte: int) -> tuple[int, bool]: + """ +Hamming(7, 4) decoding: correct any single error and return the four data +bits, with a flag saying whether a correction was made. + +Panics: +Panics if `byte` has its top bit set, which is outside the seven-bit code. + +Rust: `codes::block::hamming_74_decode` + """ + ... + +def singleton_bound(n: int, k: int) -> int: + """ +The Singleton bound: `d <= n - k + 1`. + +Deleting `d - 1` positions must leave the codewords distinct, since they +differ in at least `d`, so the code embeds in `GF(2)^(n - d + 1)` and +`k <= n - d + 1`. Returns the largest distance the parameters allow. + +Panics: +Panics unless `k <= n`. + +Rust: `codes::block::singleton_bound` + """ + ... + +def hamming_bound(n: int, d: int) -> float: + """ +The Hamming, or sphere-packing, bound on how many codewords a binary code +of length `n` and distance `d` can have. + +Spheres of radius `t = (d - 1) / 2` around distinct codewords are +disjoint, so their total volume fits inside `2^n`. A code meeting it with +equality is *perfect* -- the spheres tile the space -- which the Hamming +and Golay codes do and almost nothing else does. + +Rust: `codes::block::hamming_bound` + """ + ... + +def gilbert_varshamov(n: int, d: int) -> float: + """ +The Gilbert-Varshamov bound: a code of length `n` and distance `d` with at +least this many codewords exists. + +A lower bound, and a constructive one: keep adding any word at distance +`d` or more from everything chosen so far, and you can only be stuck once +the balls of radius `d - 1` cover the space. Where the Hamming bound says +what is impossible, this says what is unavoidable, and the best known +binary codes sit between them. + +Rust: `codes::block::gilbert_varshamov` + """ + ... + +def plotkin_bound(n: int, d: int) -> float: + """ +The Plotkin bound, for codes whose distance is more than half their +length. + +When `2d > n` the average distance between codewords cannot reach `d` +unless there are very few of them, and the count is capped at +`2 * floor(d / (2d - n))`. Outside that regime the bound says nothing and +this returns infinity. + +Rust: `codes::block::plotkin_bound` + """ + ... + +def ldpc_regular(n: int, wc: int, wr: int, rng: Rng) -> Gf2Matrix: + """ +A regular low-density parity check matrix by Gallager's construction: +`wc` ones in every column and `wr` in every row. + +The first band of rows partitions the columns into consecutive runs of +`wr`; each later band is a column permutation of that one. The result is +sparse by construction, which is the whole point -- belief propagation +costs one message per one in the matrix, and its accuracy depends on the +Tanner graph having few short cycles, which a sparse random matrix +mostly does. + +Panics: +Panics unless `wr` divides `n` and `wc` is between one and `n / wr`. + +Rust: `codes::block::ldpc_regular` + """ + ... + +def ldpc_decode_bp(h: Gf2Matrix, llr: list[float], iters: int) -> tuple[list[bool], bool]: + """ +Belief propagation decoding of an LDPC code, in the log-likelihood domain. + +`llr[i]` is the log of the ratio of the probability that bit `i` is zero +to the probability that it is one, so a positive value leans towards zero. +Each round every check tells each of its bits what the other bits imply, +and every bit tells each of its checks what the other checks imply; the +exclusions are what keep a message from being fed its own output back. + +Returns the hard decisions and whether every parity check is satisfied. +A `true` is strong evidence of a correct decode but not proof: the +algorithm can settle on a different codeword. + +Panics: +Panics unless `llr` has one entry per column. + +Rust: `codes::block::ldpc_decode_bp` + """ + ... + +def ldpc_decode_bitflip(h: Gf2Matrix, recv: list[bool], iters: int) -> list[bool]: + """ +Gallager's bit-flipping decoder: repeatedly flip whichever bits sit in the +most unsatisfied checks. + +Hard decisions only, so it throws away the channel's confidence and pays +for it -- roughly two decibels against belief propagation on the same +code. What it buys is that a round is a handful of parity computations +with no transcendental functions anywhere. + +Panics: +Panics unless `recv` has one entry per column. + +Rust: `codes::block::ldpc_decode_bitflip` + """ + ... diff --git a/bindings/python/python/numeria/codes/checksum.pyi b/bindings/python/python/numeria/codes/checksum.pyi new file mode 100644 index 0000000..212a2d5 --- /dev/null +++ b/bindings/python/python/numeria/codes/checksum.pyi @@ -0,0 +1,317 @@ +""" +Checksums and check digits: cheap ways to notice that data changed. None of these corrects anything, and none of them resists an adversary. What they do is turn a class of likely accidents into a mismatch, and the useful question about each is which class. A single parity bit catches any odd number of flipped bits and nothing else. A Fletcher or Adler sum catches reordering, which a plain sum does not, because the second accumulator weights each byte by its position. A CRC of width `w` catches every burst of `w` bits or fewer, every odd number of bit errors when the polynomial has `x + 1` as a factor, and all but `2^-w` of everything else. The decimal check digits catch every single-digit error and, except for Luhn, every transposition of adjacent digits. For an adversary, none of this is relevant: all of it is linear or nearly so, and a forger can adjust the data to hit any checksum they like. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def parity(bits: list[bool]) -> bool: + """ +Checksums and check digits: cheap ways to notice that data changed. + +None of these corrects anything, and none of them resists an adversary. +What they do is turn a class of likely accidents into a mismatch, and the +useful question about each is which class. A single parity bit catches any +odd number of flipped bits and nothing else. A Fletcher or Adler sum +catches reordering, which a plain sum does not, because the second +accumulator weights each byte by its position. A CRC of width `w` catches +every burst of `w` bits or fewer, every odd number of bit errors when the +polynomial has `x + 1` as a factor, and all but `2^-w` of everything else. +The decimal check digits catch every single-digit error and, except for +Luhn, every transposition of adjacent digits. + +For an adversary, none of this is relevant: all of it is linear or nearly +so, and a forger can adjust the data to hit any checksum they like. +Even parity: `true` when an odd number of bits are set, so that appending +it makes the total even. + +Detects any odd number of bit errors and no even number, which is the +whole of what a single bit can promise. + +Rust: `codes::checksum::parity` + """ + ... + +def parity_u64(x: int) -> bool: + """ +Parity of the set bits of a word. + +Rust: `codes::checksum::parity_u64` + """ + ... + +def checksum_fletcher16(data: list[int]) -> int: + """ +The Fletcher-16 checksum: a running byte sum and a running sum of that +sum, both modulo 255, packed into sixteen bits. + +The second accumulator is what makes it more than a sum: it weights each +byte by how many bytes follow it, so swapping two bytes changes the +result, which a plain sum cannot notice. Modulo 255 rather than 256 +because a modulus with a factor of two lets the high bits of a byte fall +out of the low accumulator entirely. + +Rust: `codes::checksum::checksum_fletcher16` + """ + ... + +def checksum_fletcher32(data: list[int]) -> int: + """ +The Fletcher-32 checksum, over sixteen-bit words modulo 65535. + +Odd-length input is padded with a zero byte, which is the usual +convention and the reason Fletcher-32 cannot distinguish `"ab"` from +`"ab\\0"`. + +Rust: `codes::checksum::checksum_fletcher32` + """ + ... + +def adler32(data: list[int]) -> int: + """ +Adler-32, as used by zlib: Fletcher's idea with a prime modulus. + +The accumulators start at one and zero and run modulo 65521, the largest +prime below `2^16`. The prime modulus spreads the values more evenly than +Fletcher's 65535, and the leading one makes the checksum of an empty +input distinguishable from the checksum of a run of zero bytes. + +Rust: `codes::checksum::adler32` + """ + ... + +def crc(data: list[int], poly: int, width: int, init: int, xor_out: int, reflect_io: bool) -> int: + """ +A cyclic redundancy check, in the parametric form every named CRC is an +instance of. + +The message is treated as a polynomial over `GF(2)`, shifted left by +`width` and divided by `poly`; the remainder is the check value. Because +the code is linear, the difference between a message and a corrupted one +has its own remainder, so a corruption goes unnoticed exactly when its +error pattern is itself a multiple of `poly` -- which no burst shorter +than `width + 1` can be, since `poly` has degree `width`. + +`init` seeds the register, so a run of leading zero bytes changes the +result; `xor_out` is applied at the end; `reflect` reverses the bits of +each input byte and of the final register, which is what the +bit-at-a-time hardware of a serial line does naturally. The named CRCs in +wide use all reflect input and output together or neither, so one flag +covers them. + +Panics: +Panics unless `width` is between 8 and 64. + +Rust: `codes::checksum::crc` + """ + ... + +def crc32_ieee(data: list[int]) -> int: + """ +CRC-32 as used by Ethernet, zip, PNG and gzip. + +Polynomial `0x04C11DB7`, register seeded to all ones, reflected in and +out, complemented at the end. The check value of `"123456789"` is +`0xCBF43926`. + +Rust: `codes::checksum::crc32_ieee` + """ + ... + +def crc16_ccitt(data: list[int]) -> int: + """ +CRC-16/CCITT-FALSE: polynomial `0x1021`, seeded to all ones, unreflected, +no final xor. The check value of `"123456789"` is `0x29B1`. + +The name records a long-standing confusion: the true CCITT parameters +seed the register to zero, and this variant -- which is the one actually +deployed, in XMODEM's successors and in many microcontroller libraries -- +does not. + +Rust: `codes::checksum::crc16_ccitt` + """ + ... + +def crc8(data: list[int]) -> int: + """ +CRC-8/SMBUS: polynomial `0x07`, zero seed, unreflected. The check value +of `"123456789"` is `0xF4`. + +Rust: `codes::checksum::crc8` + """ + ... + +def crc_table(poly: int) -> list[int]: + """ +The 256-entry lookup table for a reflected 32-bit CRC. + +`poly` is the *reversed* polynomial -- `0xEDB88320` for CRC-32 -- because +a reflected CRC shifts right, and the table holds the remainder of each +possible byte. Processing a byte becomes one table lookup instead of +eight conditional shifts; the table is the loop unrolled once and cached. + +Rust: `codes::checksum::crc_table` + """ + ... + +def crc32_with_table(data: list[int], table: list[int]) -> int: + """ +CRC-32 driven by a precomputed table rather than bit by bit. + +The same value as `crc32_ieee`, computed eight bits at a time. Pass the +table from `crc_table` with the reversed polynomial. + +Rust: `codes::checksum::crc32_with_table` + """ + ... + +def luhn_check(digits: list[int]) -> bool: + """ +The Luhn checksum test, as used on payment card numbers. + +Doubling every second digit from the right and casting out nines catches +every single-digit error and every transposition of adjacent digits +except `09` against `90`, which it maps to the same sum. That one blind +spot is why Verhoeff and Damm exist. + +The check digit is the last element of `digits`. + +Panics: +Panics if any entry is above nine. + +Rust: `codes::checksum::luhn_check` + """ + ... + +def luhn_generate(payload: list[int]) -> int: + """ +The Luhn check digit that completes `payload`. + +Panics: +Panics if any entry is above nine. + +Rust: `codes::checksum::luhn_generate` + """ + ... + +def isbn10_check(digits: list[int]) -> bool: + """ +ISBN-10, whose check digit is a weighted sum modulo eleven. + +Weights ten down to one, and the modulus is prime, which is what lets it +catch every transposition -- swapping two digits changes the sum by a +non-zero multiple of their difference, and a prime modulus has no zero +divisors to hide that. The price is that the check digit sometimes has to +be ten, written `X`; pass it as the value `10`. + +Panics: +Panics unless there are ten entries, each at most nine, except the last +which may be ten. + +Rust: `codes::checksum::isbn10_check` + """ + ... + +def isbn13_check(digits: list[int]) -> bool: + """ +ISBN-13, the same numbering embedded in the EAN-13 scheme: alternating +weights of one and three modulo ten. + +The modulus is composite, so unlike ISBN-10 it misses transpositions of +adjacent digits differing by five -- but it never needs an `X`, which is +what the change bought. + +Panics: +Panics unless there are thirteen digits, each at most nine. + +Rust: `codes::checksum::isbn13_check` + """ + ... + +def verhoeff_check(digits: list[int]) -> bool: + """ +The Verhoeff check, which catches every single-digit error and every +transposition of adjacent digits. + +It works by giving up on arithmetic modulo ten and using the dihedral +group of order ten instead, which is not commutative -- so swapping two +digits genuinely changes the product, with no cases left over. A +position-dependent permutation of order eight is applied first, which is +what extends the guarantee past the two digits nearest the check digit. + +The check digit is the last element of `digits`. + +Panics: +Panics if any entry is above nine. + +Rust: `codes::checksum::verhoeff_check` + """ + ... + +def verhoeff_generate(payload: list[int]) -> int: + """ +The Verhoeff check digit that completes `payload`. + +Panics: +Panics if any entry is above nine. + +Rust: `codes::checksum::verhoeff_generate` + """ + ... + +def damm_check(digits: list[int]) -> bool: + """ +The Damm check, with the same guarantees as Verhoeff and none of its +tables. + +One quasigroup operation folded across the digits, with no permutation +and no inverse: the check digit is simply the interim value, because the +table's diagonal is zero. Total anti-symmetry -- that `(a * b) * c` and +`(a * c) * b` differ whenever `b` and `c` do -- is exactly the property +that catches transpositions, and it is built into the table rather than +arranged around it. + +The check digit is the last element of `digits`. + +Panics: +Panics if any entry is above nine. + +Rust: `codes::checksum::damm_check` + """ + ... + +def damm_generate(payload: list[int]) -> int: + """ +The Damm check digit that completes `payload`. + +Panics: +Panics if any entry is above nine. + +Rust: `codes::checksum::damm_generate` + """ + ... + +def hamming_distance_bits(a: int, b: int) -> int: + """ +The number of bit positions in which two words differ. + +The distance a code needs to survive: a code whose words are all at least +`d` apart detects `d - 1` errors and corrects `(d - 1) / 2`, because a +received word within that radius of a codeword is within that radius of +no other. + +Rust: `codes::checksum::hamming_distance_bits` + """ + ... + +def hamming_distance_bytes(a: list[int], b: list[int]) -> Optional[int]: + """ +The bitwise Hamming distance between two byte strings, or `None` if they +are different lengths. + +Rust: `codes::checksum::hamming_distance_bytes` + """ + ... diff --git a/bindings/python/python/numeria/codes/compression.pyi b/bindings/python/python/numeria/codes/compression.pyi new file mode 100644 index 0000000..4b59899 --- /dev/null +++ b/bindings/python/python/numeria/codes/compression.pyi @@ -0,0 +1,414 @@ +""" +Lossless compression, and the string machinery it is built on. Every method here is one of two ideas. *Entropy coding* -- Huffman, Shannon-Fano, arithmetic -- assumes the symbols are drawn independently from a known distribution and spends about `-log2 p` bits on a symbol of probability `p`. It cannot beat the entropy, and Shannon's theorem says nothing can. *Modelling* -- run lengths, LZ77, LZW, the Burrows-Wheeler transform -- changes what the symbols are, so that a stream with obvious structure and high byte entropy becomes one with low entropy that an entropy coder can then finish off. Real compressors are a modelling stage followed by an entropy stage, and the two halves are here separately. The suffix array and its longest-common-prefix array sit underneath: they are what makes the Burrows-Wheeler transform computable in near-linear time, and they answer questions about repetition in their own right. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.optimization.game_theory import Move + +class BitWriter: + """ +Packs bits into bytes, most significant bit first. + +Rust: `codes::compression::BitWriter` + """ + def __init__(self, ) -> None: ... + def push(self, bit: bool) -> None: ... + def push_bits(self, code: int, len: int) -> None: ... + def bit_len(self) -> int: ... + def finish(self) -> list[int]: ... + +class Lz77Token: + """ +One LZ77 token: a back reference and the literal that follows it. + +Rust: `codes::compression::Lz77Token` + """ + def __init__(self, offset: int, length: int, next: int) -> None: ... + @property + def offset(self) -> int: ... + @property + def length(self) -> int: ... + @property + def next(self) -> int: ... + +def huffman_build(freqs: list[int]) -> list[tuple[int, int]]: + """ +Optimal prefix code lengths and codewords for the given symbol +frequencies, one entry per symbol. + +Returns `(codeword, length)` pairs; a symbol of zero frequency gets +`(0, 0)` and must not be encoded. The codes are canonical, so a decoder +needs only the lengths. + +Huffman's construction repeatedly merges the two least frequent symbols. +It is optimal, and the proof is short: in some optimal code the two rarest +symbols are siblings at the greatest depth, so merging them and solving +the smaller problem loses nothing. Optimal means no prefix code has a +smaller expected length -- not that it reaches the entropy, which it +cannot when the probabilities are not powers of two. + +Panics: +Panics on an empty frequency table. + +Rust: `codes::compression::huffman_build` + """ + ... + +def canonical_huffman(lengths: list[int]) -> list[int]: + """ +Canonical codewords for the given code lengths. + +Symbols are ordered by length and then by index, and codewords are +assigned in increasing numeric order, doubling at each length increase. +Any two prefix codes with the same length multiset compress identically, +so a decoder can be handed the lengths alone -- which is why every real +format transmits lengths rather than a tree. + +Panics: +Panics if the lengths do not satisfy Kraft's inequality, since no prefix +code has them. + +Rust: `codes::compression::canonical_huffman` + """ + ... + +def kraft_sum(lengths: list[int]) -> float: + """ +The Kraft sum of a set of code lengths: `sum 2^-l`. + +At most one for any prefix code, and exactly one when the code wastes +nothing -- which Huffman's always does, since a tree with an only child +could shorten that child by a bit. + +Rust: `codes::compression::kraft_sum` + """ + ... + +def huffman_encode(data: list[int]) -> tuple[list[int], list[tuple[int, int]], int]: + """ +Huffman-codes a byte string, returning the packed bits, the code table, +and the number of bits that matter. + +Rust: `codes::compression::huffman_encode` + """ + ... + +def huffman_decode(bits: list[int], table: list[tuple[int, int]], n: int) -> list[int]: + """ +Decodes `n` symbols from a Huffman-coded bit string. + +Panics: +Panics if the bits do not spell out `n` valid codewords. + +Rust: `codes::compression::huffman_decode` + """ + ... + +def shannon_fano(freqs: list[int]) -> list[tuple[int, int]]: + """ +Shannon-Fano coding: split the frequency-sorted symbols into two halves of +as nearly equal weight as possible, and recurse. + +The older construction, and never better than Huffman: it decides the top +of the tree first and cannot revise, while Huffman builds from the leaves +and so is optimal. The gap is usually small and occasionally a whole bit +per symbol. + +Panics: +Panics on an empty frequency table. + +Rust: `codes::compression::shannon_fano` + """ + ... + +def average_code_length(table: list[tuple[int, int]], freqs: list[int]) -> float: + """ +The average code length of a prefix code against the given frequencies, in +bits per symbol. + +Rust: `codes::compression::average_code_length` + """ + ... + +def arithmetic_encode(data: list[int], model: list[int]) -> list[int]: + """ +Arithmetic coding against a fixed model of symbol frequencies. + +Where a prefix code must spend a whole number of bits on every symbol, +arithmetic coding narrows a single interval by a factor of each symbol's +probability and writes out one number identifying it. The cost of a +message is therefore `-log2` of its probability to within two bits *in +total*, not per symbol, which is what makes it beat Huffman whenever some +symbol is much more likely than a half. + +Panics: +Panics unless the model has one non-negative count per symbol value, the +total is between one and 65536, and every byte that occurs has a positive +count. + +Rust: `codes::compression::arithmetic_encode` + """ + ... + +def arithmetic_decode(bits: list[int], model: list[int], n: int) -> list[int]: + """ +Decodes `n` symbols from an arithmetic-coded stream. + +Panics: +Panics under the same conditions as `arithmetic_encode`. + +Rust: `codes::compression::arithmetic_decode` + """ + ... + +def lz77_compress(data: list[int], window: int, lookahead: int) -> list[Lz77Token]: + """ +LZ77: replace repeats with references to earlier text. + +The window bounds how far back a reference may point and the lookahead how +long a match may be. A match is allowed to run past its own start -- an +offset of one with length twenty is a run of twenty identical bytes -- and +the decompressor copying one byte at a time handles that for free, which +is why run-length encoding falls out of LZ77 rather than needing to be +added to it. + +Panics: +Panics if the window or lookahead is zero. + +Rust: `codes::compression::lz77_compress` + """ + ... + +def lz77_decompress(tokens: list[Lz77Token]) -> list[int]: + """ +Rebuilds the original from LZ77 tokens. + +Panics: +Panics if a token points further back than the output so far. + +Rust: `codes::compression::lz77_decompress` + """ + ... + +def lzw_compress(data: list[int]) -> list[int]: + """ +LZW: build a dictionary of every phrase seen plus one byte, and emit +dictionary indices. + +The decoder rebuilds the same dictionary from the same output, so nothing +has to be transmitted with the data -- which is what made it practical for +modems and printers with no memory to spare. + +Rust: `codes::compression::lzw_compress` + """ + ... + +def lzw_decompress(codes: list[int]) -> list[int]: + """ +Rebuilds the original from LZW codes. + +Panics: +Panics on a code the dictionary cannot yet contain. + +Rust: `codes::compression::lzw_decompress` + """ + ... + +def rle_compress(data: list[int]) -> list[int]: + """ +Run-length encoding in the PackBits scheme. + +A control byte below 128 means "the next `n + 1` bytes are literal"; one +at or above means "repeat the next byte `257 - n` times". Incompressible +data grows by one byte in every 128, which is the price of never needing +an escape character. + +Rust: `codes::compression::rle_compress` + """ + ... + +def rle_decompress(data: list[int]) -> list[int]: + """ +Rebuilds the original from PackBits run-length encoding. + +Panics: +Panics if the stream is truncated part way through a run or literal. + +Rust: `codes::compression::rle_decompress` + """ + ... + +def suffix_array(data: list[int]) -> list[int]: + """ +The suffix array: the starting positions of the suffixes, in the order +those suffixes sort. + +Built by prefix doubling. After round `k` the suffixes are sorted by their +first `2^k` characters, and the next round sorts by pairs of the ranks +already computed -- so each round doubles the prefix length examined and +`log n` rounds settle it. Not the linear-time construction, but the +simplest one whose correctness is visible. + +Rust: `codes::compression::suffix_array` + """ + ... + +def lcp_array(data: list[int], sa: list[int]) -> list[int]: + """ +The longest common prefix of each adjacent pair in the suffix array, by +Kasai's algorithm. + +`lcp[i]` is the overlap between the suffixes at `sa[i - 1]` and `sa[i]`, +with `lcp[0]` zero. Kasai's insight is that walking the suffixes in +*text* order lets the previous answer be reused: dropping the first +character of a suffix shortens its overlap with its neighbour by at most +one, so the total work is linear rather than quadratic. + +Panics: +Panics unless the suffix array matches the data's length. + +Rust: `codes::compression::lcp_array` + """ + ... + +def longest_repeated_substring(data: list[int]) -> tuple[int, int]: + """ +The longest substring that occurs at least twice, as `(start, length)`. + +The largest entry of the longest-common-prefix array, because two +occurrences of the same substring are two suffixes sharing that prefix, +and suffixes sharing a long prefix are adjacent in the suffix array. +Length zero when nothing repeats. + +Rust: `codes::compression::longest_repeated_substring` + """ + ... + +def bwt(data: list[int]) -> tuple[list[int], int]: + """ +The Burrows-Wheeler transform: the last column of the sorted rotations, +and which row the original occupies. + +The transform is reversible and sorts nothing about the data itself -- it +is a permutation of the bytes. What it does is bring together the bytes +that precede similar contexts, so English text comes out in long runs of +the same letter, and a run-length or move-to-front stage that could do +nothing with the original then has plenty to work with. + +Rust: `codes::compression::bwt` + """ + ... + +def ibwt(data: list[int], idx: int) -> list[int]: + """ +Inverts the Burrows-Wheeler transform. + +The last column plus the row index is enough, because sorting the last +column gives the first, and the `i`-th occurrence of a byte in the last +column is the `i`-th in the first -- rotations sharing a first byte stay +in the same relative order. That correspondence is the whole inverse. + +Panics: +Panics if the index is outside the data. + +Rust: `codes::compression::ibwt` + """ + ... + +def mtf_encode(data: list[int]) -> list[int]: + """ +Move-to-front coding: emit each byte's position in a list, then move it to +the front. + +It turns locality into small numbers. A stretch using only a few distinct +bytes -- which is what the Burrows-Wheeler transform produces -- becomes a +stretch of values near zero, and a stretch of one repeated byte becomes a +run of zeros, which an entropy coder or a run-length stage can then +exploit. + +Rust: `codes::compression::mtf_encode` + """ + ... + +def mtf_decode(data: list[int]) -> list[int]: + """ +Inverts move-to-front coding. + +Rust: `codes::compression::mtf_decode` + """ + ... + +def delta_encode(data: list[int]) -> list[int]: + """ +Differences between consecutive bytes, modulo 256, with the first byte +kept as it is. + +Worth doing when the data is a slowly varying signal: a smooth ramp has +high byte entropy and near-zero difference entropy. + +Rust: `codes::compression::delta_encode` + """ + ... + +def delta_decode(data: list[int]) -> list[int]: + """ +Inverts delta coding. + +Rust: `codes::compression::delta_decode` + """ + ... + +def entropy_bytes(data: list[int]) -> float: + """ +The Shannon entropy of the byte histogram, in bits per byte. + +The floor for any coder that treats the bytes as independent draws. +Between zero, for a constant stream, and eight, for a uniform one. It is +not a floor for compression in general: a stream of a million alternating +bytes has an entropy of one bit per byte and compresses to nothing, since +the bytes are not independent. + +Rust: `codes::compression::entropy_bytes` + """ + ... + +def compression_bound(data: list[int]) -> float: + """ +The size in bytes that the byte entropy allows, which no memoryless coder +can beat. + +Rust: `codes::compression::compression_bound` + """ + ... + +def kolmogorov_estimate_by_compressors(data: list[int]) -> float: + """ +The size the module's own best pipeline achieves, as a stand-in for the +incompressible content of the data. + +Kolmogorov complexity is not computable, and this is not an approximation +to it in any rigorous sense -- it is an upper bound that happens to behave +sensibly, which is what the practical literature uses it for. The pipeline +is Burrows-Wheeler, then move-to-front, then run lengths, then Huffman: +each stage exposes structure the next can spend. + +Rust: `codes::compression::kolmogorov_estimate_by_compressors` + """ + ... + +def normalized_compression_distance(a: list[int], b: list[int]) -> float: + """ +The normalized compression distance between two byte strings. + +`(C(ab) - min(C(a), C(b))) / max(C(a), C(b))`: if knowing `a` makes `b` +cheap to describe, they are close. Near zero for identical inputs and near +one for unrelated ones, and it needs no notion of what the data means, +which is why it gets used on genomes and on music alike. + +Rust: `codes::compression::normalized_compression_distance` + """ + ... diff --git a/bindings/python/python/numeria/codes/convolutional.pyi b/bindings/python/python/numeria/codes/convolutional.pyi new file mode 100644 index 0000000..329e40e --- /dev/null +++ b/bindings/python/python/numeria/codes/convolutional.pyi @@ -0,0 +1,293 @@ +""" +Convolutional and turbo codes, and the channels they run over. A convolutional code has no block length. The encoder is a shift register: each input bit is combined with the last few, and the output depends on a sliding window rather than on a partition of the message. That makes the code a walk through a *trellis* -- a graph whose vertices are the register states and whose edges are the possible inputs -- and decoding the problem of finding the walk that best matches what arrived. Viterbi's algorithm is dynamic programming on that graph, and it is optimal: it returns the maximum-likelihood sequence, not an approximation to it. Turbo codes take two such encoders, feed the second an interleaved copy of the message, and decode by having the two halves exchange opinions. What each passes the other is *extrinsic* information -- what it concluded about a bit from everything except that bit's own channel evidence -- and keeping the exchange extrinsic is the whole trick. Feeding back a decoder's full opinion would let it hear its own guess reflected as independent confirmation, and the iteration would converge confidently to nonsense. The capacity functions at the end say where the limits are. A rate-`1/2` binary code cannot work below about `0.187` decibels of `Eb/N0`, whatever it is; turbo codes reached within a few tenths of that, which is why they ended a thirty-year search. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.monte_carlo import Rng + +class ConvolutionalCode: + """ +A rate `1/n` convolutional code, given by its constraint length and +generator polynomials. + +The generators are the taps of the shift register, conventionally written +in octal: the NASA standard's `171` and `133` are `0o171` and `0o133`, +seven bits each for a constraint length of seven. Bit `k - 1` of a +generator is the current input and bit zero the oldest bit in memory. + +Rust: `codes::convolutional::ConvolutionalCode` + """ + def __init__(self, k: int, polys: list[int]) -> None: ... + @staticmethod + def nasa_standard() -> ConvolutionalCode: ... + def memory(self) -> int: ... + def trellis_states(self) -> int: ... + def outputs(self) -> int: ... + def step(self, state: int, input: bool) -> tuple[list[bool], int]: ... + def encode(self, bits: list[bool]) -> list[bool]: ... + def viterbi_decode(self, recv_hard: list[bool]) -> list[bool]: ... + def viterbi_soft(self, llr: list[float]) -> list[bool]: ... + def free_distance_estimate(self) -> int: ... + def puncture(self, encoded: list[bool], pattern: list[bool]) -> list[bool]: ... + def depuncture_llr(self, punctured: list[float], pattern: list[bool], full_len: int) -> list[float]: ... + @property + def k(self) -> int: ... + @property + def polys(self) -> list[int]: ... + +class RscCode: + """ +A rate-`1/2` recursive systematic convolutional encoder: the message +passes through unchanged, and one parity stream is generated with +feedback. + +Feedback is what makes a turbo code work. Without it, a low-weight input +gives a low-weight output whichever order the bits arrive in, so +interleaving buys nothing; with it, a weight-one input drives the register +forever and only very particular inputs produce light parity. The +interleaver can then almost always break whatever pattern was light for +the first encoder. + +Rust: `codes::convolutional::RscCode` + """ + def __init__(self, k: int, feedback: int, feedforward: int) -> None: ... + @staticmethod + def standard() -> RscCode: ... + def memory(self) -> int: ... + def trellis_states(self) -> int: ... + def step(self, state: int, input: bool) -> tuple[bool, int]: ... + def terminating_input(self, state: int) -> bool: ... + def encode(self, bits: list[bool]) -> tuple[list[bool], int]: ... + def encode_terminated(self, bits: list[bool]) -> tuple[list[bool], list[bool]]: ... + def bcjr_extrinsic(self, ys: list[float], yp: list[float], la: list[float]) -> list[float]: ... + def ends_at_zero(self, bits: list[bool]) -> bool: ... + @property + def k(self) -> int: ... + @property + def feedback(self) -> int: ... + @property + def feedforward(self) -> int: ... + +class TurboCode: + """ +A turbo code: two recursive systematic encoders sharing a message, the +second seeing it through an interleaver. + +Rust: `codes::convolutional::TurboCode` + """ + def __init__(self, rsc: RscCode, interleaver: list[int]) -> None: ... + def len(self) -> int: ... + def is_empty(self) -> bool: ... + def encode(self, msg: list[bool]) -> tuple[list[bool], list[bool], list[bool]]: ... + def decode_bcjr(self, ys: list[float], yp1: list[float], yp2: list[float], iters: int) -> list[bool]: ... + @property + def rsc(self) -> RscCode: ... + @property + def interleaver(self) -> list[int]: ... + +def interleaver_block(n: int, rows: int) -> list[int]: + """ +A block interleaver: write the sequence into a rectangle row by row, read +it out column by column. + +Returns the permutation `pi` with `pi[i]` the source index of output `i`. +It spreads any run of `rows` consecutive positions to distance `rows` +apart, which is what turns a burst into scattered single errors that a +random-error code can handle. + +Panics: +Panics unless `rows` divides `n` and both are positive. + +Rust: `codes::convolutional::interleaver_block` + """ + ... + +def interleaver_random(n: int, rng: Rng) -> list[int]: + """ +A uniformly random interleaver. + +Rust: `codes::convolutional::interleaver_random` + """ + ... + +def qpp_interleaver(n: int, f1: int, f2: int) -> list[int]: + """ +A quadratic permutation polynomial interleaver: `pi(i) = f1 i + f2 i^2` +modulo `n`, the family LTE uses. + +It is a permutation exactly when `f1` is coprime to `n` and every prime +dividing `n` also divides `f2` -- conditions cheap enough to check, which +is the point: an LTE receiver reconstructs the interleaver from two +integers instead of storing a table of six thousand entries. + +Panics: +Panics unless the parameters give a permutation. + +Rust: `codes::convolutional::qpp_interleaver` + """ + ... + +def awgn_channel(bits: list[bool], snr_db: float, rng: Rng) -> list[float]: + """ +Transmits bits over an additive white Gaussian noise channel with binary +phase shift keying, returning the received samples. + +A zero bit is sent as `+1` and a one as `-1`, so the received value is +`±1` plus a Gaussian of variance `1 / (2 * 10^(snr_db/10))`. That variance +is the one that makes `snr_db` the symbol energy to noise density ratio +`Es/N0` in decibels. + +Rust: `codes::convolutional::awgn_channel` + """ + ... + +def awgn_sigma(snr_db: float) -> float: + """ +The noise standard deviation for a given `Es/N0` in decibels, with unit +symbol energy. + +Rust: `codes::convolutional::awgn_sigma` + """ + ... + +def llr_from_awgn(samples: list[float], sigma: float) -> list[float]: + """ +The log-likelihood ratios a Gaussian channel implies, positive for a zero +bit. + +Rust: `codes::convolutional::llr_from_awgn` + """ + ... + +def bsc_channel(bits: list[bool], p: float, rng: Rng) -> list[bool]: + """ +Transmits bits over a binary symmetric channel that flips each with +probability `p`. + +Panics: +Panics unless `p` is in `[0, 1]`. + +Rust: `codes::convolutional::bsc_channel` + """ + ... + +def ber_simulation(code: ConvolutionalCode, snr_db_range: list[float], n_bits: int, rng: Rng) -> list[tuple[float, float]]: + """ +Bit error rates against signal to noise ratio, for a convolutional code +decoded softly. + +`snr_db_range` is `Eb/N0` in decibels -- energy per *information* bit, +which is the only fair way to compare codes of different rates, since a +stronger code spends more channel symbols on each message bit and must be +charged for them. + +Panics: +Panics if `n_bits` is zero. + +Rust: `codes::convolutional::ber_simulation` + """ + ... + +def binary_entropy(p: float) -> float: + """ +Binary entropy in bits. + +Rust: `codes::convolutional::binary_entropy` + """ + ... + +def capacity_bsc(p: float) -> float: + """ +The capacity of a binary symmetric channel: `1 - H(p)` bits per use. + +Panics: +Panics unless `p` is in `[0, 1]`. + +Rust: `codes::convolutional::capacity_bsc` + """ + ... + +def capacity_bec(e: float) -> float: + """ +The capacity of a binary erasure channel: `1 - e` bits per use. + +The one channel whose capacity needs no argument: a fraction `e` of the +symbols never arrive, and the rest arrive perfectly. + +Panics: +Panics unless `e` is in `[0, 1]`. + +Rust: `codes::convolutional::capacity_bec` + """ + ... + +def channel_capacity_awgn(snr: float) -> float: + """ +The capacity of a real additive white Gaussian noise channel with the +given signal to noise ratio: `0.5 log2(1 + snr)` bits per use. + +`snr` here is the ratio of signal power to noise *variance*. That is not +`Es/N0`: a real channel has variance `N0/2`, so the ratio to pass is +twice `Es/N0`. Comparing this against `channel_capacity_bpsk`, which +takes `Es/N0`, without that factor is the easy way to conclude that +restricting the input alphabet raises capacity. + +Panics: +Panics if the ratio is negative. + +Rust: `codes::convolutional::channel_capacity_awgn` + """ + ... + +def channel_capacity_bpsk(snr: float) -> float: + """ +The capacity of a Gaussian channel whose input is restricted to `+/-1`. + +Restricting the input costs something: at high signal to noise the +unrestricted channel's capacity grows without bound while this saturates +at one bit per use, because one bit is all a binary symbol can carry. The +expectation has no closed form and is integrated numerically. + +Panics: +Panics if the ratio is negative. + +Rust: `codes::convolutional::channel_capacity_bpsk` + """ + ... + +def shannon_limit_bpsk(rate: float) -> float: + """ +The lowest `Eb/N0`, in decibels, at which a binary code of the given rate +can work. + +Found by bisecting `channel_capacity_bpsk` for the point where capacity +equals the rate, then converting from `Es/N0` to `Eb/N0` by dividing out +the rate. At rate one half the answer is about `0.187` decibels; as the +rate falls towards zero it approaches `-1.59`, which is `10 log10(ln 2)` +and is the limit for any code at any rate. + +Panics: +Panics unless the rate is in `(0, 1)`. + +Rust: `codes::convolutional::shannon_limit_bpsk` + """ + ... + +def shannon_limit_unconstrained(rate: float) -> float: + """ +The same limit for a channel with no restriction on the input alphabet: +`(2^(2R) - 1) / (2R)`, in decibels. + +Always at or below `shannon_limit_bpsk`, since removing a restriction +cannot make a channel worse, and equal to it in the limit of low rate. + +Panics: +Panics unless the rate is positive. + +Rust: `codes::convolutional::shannon_limit_unconstrained` + """ + ... diff --git a/bindings/python/python/numeria/codes/crypto_math.pyi b/bindings/python/python/numeria/codes/crypto_math.pyi new file mode 100644 index 0000000..e75c18f --- /dev/null +++ b/bindings/python/python/numeria/codes/crypto_math.pyi @@ -0,0 +1,410 @@ +""" +The arithmetic underneath public-key cryptography, for study rather than for use. **None of this is safe to deploy.** Every routine here branches and indexes on secret values, so the time it takes and the memory it touches leak what it is working on; a modular exponentiation that skips a squaring when a bit is zero tells anyone timing it how many bits are set. Real implementations are written to take the same time and the same path whatever the key, use blinding to break the correlation between input and timing, and are audited for the dozen further side channels that remain. Nothing here does any of that, and the key sizes the tests use are small enough to factor over lunch. What it is for is seeing why the constructions work. RSA rests on the fact that exponentiating by `e` and then by `d` returns you to where you started whenever `ed = 1` modulo the group order -- so anyone who can compute the group order can find `d`, and the security assumption is exactly that factoring `n` is hard. Diffie-Hellman and elliptic curve Diffie-Hellman rest on the same shape in a different group. Shamir's scheme rests on a polynomial of degree `k - 1` being determined by `k` points and by no fewer. Each of those is a theorem, and the tests here check the theorem rather than the ciphertext. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.monte_carlo import Rng + +class EcCurve: + """ +A short Weierstrass curve `y^2 = x^3 + a x + b` over the prime field +`F_p`. + +The points form a group under the chord-and-tangent construction: three +points on a line sum to the identity, so adding two points means drawing +the line through them, finding the third intersection, and reflecting it. +That the construction is associative is the one non-obvious fact, and it +is what makes the whole subject possible. + +Rust: `codes::crypto_math::EcCurve` + """ + def __init__(self, a: int, b: int, p: int) -> None: ... + def is_on_curve(self, pt: EcPoint) -> bool: ... + def negate(self, pt: EcPoint) -> EcPoint: ... + def add(self, p1: EcPoint, p2: EcPoint) -> EcPoint: ... + def double(self, pt: EcPoint) -> EcPoint: ... + def scalar_mul(self, k: int, pt: EcPoint) -> EcPoint: ... + def all_points(self) -> list[EcPoint]: ... + def order_naive_small(self) -> int: ... + def point_order_small(self, pt: EcPoint) -> int: ... + def random_point(self, rng: Rng) -> EcPoint: ... + @staticmethod + def secp256k1() -> EcCurve: ... + @staticmethod + def secp256k1_generator() -> tuple[EcPoint, int]: ... + @staticmethod + def p256() -> EcCurve: ... + @staticmethod + def p256_generator() -> tuple[EcPoint, int]: ... + @property + def a(self) -> int: ... + @property + def b(self) -> int: ... + @property + def p(self) -> int: ... + +class EcPoint: + """ +A point on a short Weierstrass curve, or the point at infinity. + +Rust: `codes::crypto_math::EcPoint` + """ + ... + +def rsa_keygen(bits: int, rng: Rng) -> tuple[int, int, int]: + """ +Generates an RSA modulus and exponent pair: `(n, e, d)`. + +Two primes of about `bits / 2` each are drawn, `n` is their product, and +`d` inverts `e` modulo the Carmichael function of `n` -- the exponent of +the multiplicative group, which is the least value that works and so gives +the smallest `d`. The public exponent is 65537, whose binary form has two +set bits and therefore encrypts in seventeen squarings. + +Panics: +Panics unless `bits` is between 16 and 2048. Anything in that range is far +too small to protect anything. + +Rust: `codes::crypto_math::rsa_keygen` + """ + ... + +def rsa_keygen_with_primes(bits: int, rng: Rng) -> tuple[int, int, int, int, int]: + """ +Generates a key and keeps the primes, which the Chinese remainder form of +decryption needs. + +Panics: +Panics unless `bits` is between 16 and 2048. + +Rust: `codes::crypto_math::rsa_keygen_with_primes` + """ + ... + +def rsa_encrypt(m: int, e: int, n: int) -> int: + """ +Textbook RSA encryption: `m^e` modulo `n`. + +Deterministic, and therefore not a secure encryption scheme on its own -- +the same message always gives the same ciphertext, so an attacker who can +guess the plaintext can confirm the guess. Real use pads the message with +randomness first. + +Rust: `codes::crypto_math::rsa_encrypt` + """ + ... + +def rsa_decrypt(c: int, d: int, n: int) -> int: + """ +Textbook RSA decryption: `c^d` modulo `n`. + +Rust: `codes::crypto_math::rsa_decrypt` + """ + ... + +def rsa_crt_decrypt(c: int, d: int, p: int, q: int) -> int: + """ +Decryption through the Chinese remainder theorem, given the two primes. + +Working modulo `p` and `q` separately and recombining costs about a +quarter of the work, since modular exponentiation is cubic in the operand +size and the operands are half as long. Every real implementation does +this, which is also why a fault during one of the two halves famously +reveals the factorisation. + +Panics: +Panics if `p` and `q` are not coprime, so that the recombination has no +inverse. + +Rust: `codes::crypto_math::rsa_crt_decrypt` + """ + ... + +def diffie_hellman_demo(p: int, g: int, rng: Rng) -> tuple[tuple[int, int], tuple[int, int], int]: + """ +A Diffie-Hellman exchange in full: both parties' key pairs and the shared +secret they arrive at. + +Returns `((a, A), (b, B), s)` where `A = g^a`, `B = g^b` and +`s = B^a = A^b`, all modulo `p`. The exchange works because +exponentiation commutes; it is secure only if recovering `a` from `g^a` is +hard, which needs `p` to be a large safe prime and `g` to generate a large +subgroup. Neither is checked here. + +Panics: +Panics unless `p` is at least three. + +Rust: `codes::crypto_math::diffie_hellman_demo` + """ + ... + +def ecdh_demo(curve: EcCurve, g: EcPoint, order: int, rng: Rng) -> tuple[tuple[int, EcPoint], tuple[int, EcPoint], EcPoint]: + """ +An elliptic curve Diffie-Hellman exchange in full. + +Returns `((a, aG), (b, bG), s)`. The same construction as the +multiplicative version, in a group where the best known attack is +square-root time rather than sub-exponential -- which is why a 256-bit +curve stands against a 3072-bit modulus. + +Panics: +Panics if the base point is not on the curve. + +Rust: `codes::crypto_math::ecdh_demo` + """ + ... + +def ec_count_points_small(curve: EcCurve) -> int: + """ +The number of points on a small curve, including infinity. + +Panics: +Panics if the field has more than a million elements. + +Rust: `codes::crypto_math::ec_count_points_small` + """ + ... + +def hasse_bound_check(count: int, p: int) -> bool: + """ +Whether a point count satisfies Hasse's theorem. + +The count lies within `2 sqrt(p)` of `p + 1`. That is a remarkably tight +bound -- the group is always about as large as the field, never a constant +factor away -- and it is what makes a curve's security predictable from +its field size alone. + +Rust: `codes::crypto_math::hasse_bound_check` + """ + ... + +def shamir_split(secret: int, k: int, n: int, prime: int, rng: Rng) -> list[tuple[int, int]]: + """ +Splits a secret into `n` shares of which any `k` suffice. + +The secret is the constant term of a random polynomial of degree `k - 1` +over `F_prime`, and a share is that polynomial's value at a non-zero +point. Any `k` points determine the polynomial by interpolation, and any +`k - 1` leave the constant term uniformly distributed -- so fewer than `k` +shares give not merely a hard problem but no information at all. That is +what makes the scheme *perfect*, and it is rare. + +Panics: +Panics unless `1 <= k <= n`, `n` is below the prime, and the secret is a +non-negative residue below it. + +Rust: `codes::crypto_math::shamir_split` + """ + ... + +def shamir_reconstruct(shares: list[tuple[int, int]], prime: int) -> int: + """ +Recovers the secret from any `k` shares by Lagrange interpolation at zero. + +Panics: +Panics on an empty share list, on a repeated abscissa, or if the modulus +is not prime enough for the required inverses to exist. + +Rust: `codes::crypto_math::shamir_reconstruct` + """ + ... + +def one_time_pad(data: list[int], key: list[int]) -> list[int]: + """ +Exclusive-or of the data with a repeating key. + +With a key as long as the message, drawn uniformly and never reused, this +is the one cipher with a proof of perfect secrecy: the ciphertext is +independent of the plaintext, so an adversary with unlimited computation +learns nothing. With a short key repeated, it is a Vigenere cipher and +`vigenere_break` undoes it. The gap between those two is entirely the +key. + +Panics: +Panics on an empty key. + +Rust: `codes::crypto_math::one_time_pad` + """ + ... + +def lfsr(taps: int, state: int, n: int) -> list[bool]: + """ +A Fibonacci linear feedback shift register: `n` output bits from a state +and a tap mask. + +The new bit is the parity of the tapped positions, and the register shifts +right. The output is a linear recurrence over `GF(2)`, which is what makes +it fast, and also what makes it hopeless as a cipher on its own: +`berlekamp_massey_attack` recovers the whole register from twice its +length in output. + +Tap bit zero, or the step map is not reversible and the register cannot +reach every state -- see `lfsr_period`. + +Panics: +Panics on a zero tap mask. + +Rust: `codes::crypto_math::lfsr` + """ + ... + +def lfsr_period(taps: int, width: int) -> int: + """ +The period of a shift register of the given width, by running it until it +repeats. + +A width-`w` register has at most `2^w - 1` states before it must repeat, +and reaches that only for a *primitive* tap polynomial. The all-zero state +is absorbing, which is why the maximum is one short of the state count. + +The step map is a bijection only when bit zero is tapped: without it, the +outgoing bit does not influence the feedback, two states share an image, +and the register runs into a cycle it can never leave and never started +on. Returns zero in that case, meaning the register never comes back. + +Rust: `codes::crypto_math::lfsr_period` + """ + ... + +def berlekamp_massey_attack(stream: list[bool]) -> tuple[int, int]: + """ +Recovers the shortest linear recurrence a bit stream satisfies, as +`(length, taps)`. + +The Berlekamp-Massey algorithm, over `GF(2)`. Given `2L` bits of output +from a register of length `L` it returns that register, which is why a +bare shift register is not a cipher: the keystream reveals the key +generator in time linear in its size. + +Rust: `codes::crypto_math::berlekamp_massey_attack` + """ + ... + +def hash_avalanche_test(h: Callable[[list[int]], int], trials: int, rng: Rng) -> float: + """ +How close a hash comes to flipping half its output bits when one input bit +changes. + +Returns the mean fraction of output bits that flip. A good hash sits at a +half: every output bit should be an unbiased, independent-looking function +of every input bit, so that no partial information about the input +survives. A value far from a half is a structural weakness a distinguisher +can be built from. + +Panics: +Panics if `trials` is zero. + +Rust: `codes::crypto_math::hash_avalanche_test` + """ + ... + +def birthday_bound(n_bits: int) -> float: + """ +The number of samples at which a collision becomes likely for an output of +`n_bits`. + +About `2^(n/2)`, up to a constant: with `k` samples there are about +`k^2 / 2` pairs and each collides with probability `2^-n`, so the count of +collisions reaches one near the square root. It is why a 128-bit hash +offers 64 bits of collision resistance, not 128. + +Rust: `codes::crypto_math::birthday_bound` + """ + ... + +def frequency_analysis(text: list[int]) -> list[float]: + """ +The frequency of each letter, ignoring everything else, as fractions +summing to one. + +Rust: `codes::crypto_math::frequency_analysis` + """ + ... + +def index_of_coincidence(text: list[int]) -> float: + """ +The index of coincidence: the chance that two letters drawn at random from +the text are the same. + +About `0.066` for English and `0.038` for a uniform jumble. Because it is +unchanged by a substitution -- relabelling the letters does not change how +often two match -- it tells a monoalphabetic cipher from a polyalphabetic +one without any guess about the key, which is what makes it the first +measurement to take. + +Rust: `codes::crypto_math::index_of_coincidence` + """ + ... + +def kasiski_examination(text: list[int]) -> list[int]: + """ +Candidate key lengths from repeated trigrams, as Kasiski proposed. + +A trigram repeating in the ciphertext usually means the same plaintext +trigram met the same stretch of key, so the gap between the two is a +multiple of the key length. Returns the lengths that divide the most gaps, +best first. + +Rust: `codes::crypto_math::kasiski_examination` + """ + ... + +def caesar_break(text: list[int]) -> int: + """ +The Caesar shift that best matches English letter frequencies. + +Scored by the dot product of the observed and expected distributions, +which is largest when the two line up -- the same statistic as chi-squared +scoring, with the arithmetic the other way up. + +Rust: `codes::crypto_math::caesar_break` + """ + ... + +def vigenere_break(text: list[int], max_key: int) -> str: + """ +The most likely Vigenere key, searching lengths up to `max_key`. + +The key length is chosen by the average index of coincidence of the +columns -- at the true length each column is a Caesar shift of English and +so looks like English, and at any other length the columns are jumbled -- +and each column is then solved as its own Caesar shift. + +Panics: +Panics if `max_key` is zero. + +Rust: `codes::crypto_math::vigenere_break` + """ + ... + +def perfect_shuffle_permutation(n: int, out: bool) -> list[int]: + """ +The permutation a perfect riffle shuffle applies to `n` cards. + +An *out* shuffle keeps the top card on top; an *in* shuffle pushes it to +second. Eight out-shuffles restore a 52-card deck and 52 in-shuffles do, +which is the standard demonstration that a deterministic shuffle is no +shuffle at all. + +Panics: +Panics unless `n` is positive and even. + +Rust: `codes::crypto_math::perfect_shuffle_permutation` + """ + ... + +def permutation_cipher_period(perm: list[int]) -> int: + """ +How many times a permutation must be applied before everything returns +home: the least common multiple of its cycle lengths. + +Panics: +Panics unless the input is a permutation of `0..n`. + +Rust: `codes::crypto_math::permutation_cipher_period` + """ + ... diff --git a/bindings/python/python/numeria/codes/reed_solomon.pyi b/bindings/python/python/numeria/codes/reed_solomon.pyi new file mode 100644 index 0000000..e57e3d9 --- /dev/null +++ b/bindings/python/python/numeria/codes/reed_solomon.pyi @@ -0,0 +1,170 @@ +""" +Reed-Solomon and BCH codes over finite fields. Reed-Solomon works on symbols rather than bits, which is why it appears wherever errors arrive in clumps: a scratch on a disc, a fading burst on a radio link, a smudge across a printed barcode. A byte is wrong whether one bit of it flipped or all eight, so a burst that would defeat a bit-level code costs an `RS(255, 223)` codeword at most one of its sixteen correctable symbols per byte touched. The construction is one idea. Fix a field, treat the message as the coefficients of a polynomial, and multiply by a generator whose roots are consecutive powers of a primitive element. A codeword is then exactly a polynomial vanishing at those `n - k` points, so evaluating the received word there gives zero if nothing went wrong and, if something did, a set of *syndromes* that depend only on the errors. Berlekamp-Massey turns those syndromes into a polynomial whose roots say where the errors are, Chien search finds the roots, and Forney's formula says how large each error was. Every step is field arithmetic; none of it looks at the message. Because the generator has exactly `n - k` roots, the code meets the Singleton bound with equality -- `d = n - k + 1`. Reed-Solomon codes are the standard example of a maximum distance separable code, and there is no slack anywhere in the parameters. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +class BchCode: + """ +A binary BCH code: cyclic, with a designed distance, over `GF(2^m)`. + +The generator is the least common multiple of the minimal polynomials of +`alpha^1` through `alpha^(2t)`. Those `2t` consecutive roots force a +distance of at least `2t + 1` by the BCH bound, which is what "designed +distance" means -- the true distance can be larger, and often is. + +Rust: `codes::reed_solomon::BchCode` + """ + def __init__(self, m: int, t: int) -> None: ... + def encode(self, msg: list[bool]) -> list[bool]: ... + def decode(self, recv: list[bool]) -> tuple[list[bool], int]: ... + @property + def m(self) -> int: ... + @property + def t(self) -> int: ... + @property + def n(self) -> int: ... + @property + def k(self) -> int: ... + @property + def generator(self) -> list[int]: ... + +class Gf256: + """ +The field `GF(2^8)`, with logarithm and antilogarithm tables. + +Multiplication in a field of characteristic two is not the processor's +multiplication, so it is done through logarithms: every non-zero element +is a power of a primitive element, and a product of powers adds their +exponents. The `exp` table is doubled in length so the sum of two +exponents never needs reducing modulo 255 at the point of use. + +Rust: `codes::reed_solomon::Gf256` + """ + def __init__(self, prim_poly: int) -> None: ... + @staticmethod + def add(a: int, b: int) -> int: ... + def mul(self, a: int, b: int) -> int: ... + def div(self, a: int, b: int) -> int: ... + def inv(self, a: int) -> int: ... + def pow(self, a: int, e: int) -> int: ... + def alpha(self, e: int) -> int: ... + def poly_eval(self, poly: list[int], x: int) -> int: ... + def poly_mul(self, a: list[int], b: list[int]) -> list[int]: ... + def poly_rem(self, a: list[int], b: list[int]) -> list[int]: ... + @property + def log(self) -> list[int]: ... + @property + def exp(self) -> list[int]: ... + +class Gf2m: + """ +A general binary extension field `GF(2^m)`, elements held as bit patterns. + +Rust: `codes::reed_solomon::Gf2m` + """ + def __init__(self, m: int, prim: int) -> None: ... + @staticmethod + def with_degree(m: int) -> Gf2m: ... + def order(self) -> int: ... + @staticmethod + def add(a: int, b: int) -> int: ... + def mul(self, a: int, b: int) -> int: ... + def pow(self, a: int, e: int) -> int: ... + def inv(self, a: int) -> int: ... + def trace(self, a: int) -> int: ... + def all_elements(self) -> list[int]: ... + def minimal_polynomial(self, e: int) -> list[int]: ... + @property + def m(self) -> int: ... + @property + def prim(self) -> int: ... + +class GfP: + """ +A prime field `GF(p)`, for the places a power of two is the wrong shape. + +Rust: `codes::reed_solomon::GfP` + """ + def __init__(self, p: int) -> None: ... + def add(self, a: int, b: int) -> int: ... + def sub(self, a: int, b: int) -> int: ... + def mul(self, a: int, b: int) -> int: ... + def pow(self, a: int, e: int) -> int: ... + def inv(self, a: int) -> int: ... + @property + def p(self) -> int: ... + +class ReedSolomon: + """ +A Reed-Solomon code over `GF(256)`, systematic, with the parity symbols +appended. + +Rust: `codes::reed_solomon::ReedSolomon` + """ + def __init__(self, n: int, k: int) -> None: ... + def correction_capacity(self) -> int: ... + def distance(self) -> int: ... + def encode(self, msg: list[int]) -> list[int]: ... + def syndromes(self, recv: list[int]) -> list[int]: ... + def decode(self, recv: list[int]) -> tuple[list[int], int]: ... + def correct(self, recv: list[int]) -> tuple[list[int], int]: ... + def decode_erasures(self, recv: list[int], erasure_pos: list[int]) -> list[int]: ... + @property + def n(self) -> int: ... + @property + def k(self) -> int: ... + +def rs_ccsds() -> ReedSolomon: + """ +`RS(255, 223)`, the CCSDS telemetry standard: sixteen correctable symbol +errors in a 255-byte frame, used on essentially every deep space mission +since Voyager. + +Rust: `codes::reed_solomon::rs_ccsds` + """ + ... + +def rs_qr_code(version: int) -> ReedSolomon: + """ +The Reed-Solomon block a QR code of the given version uses at its lowest +error correction level. + +Panics: +Panics unless the version is between one and four, the range tabulated +here. + +Rust: `codes::reed_solomon::rs_qr_code` + """ + ... + +def rs_dvd() -> ReedSolomon: + """ +`RS(32, 28)`, the outer code of the cross-interleaved scheme on a compact +disc and its descendants. + +Rust: `codes::reed_solomon::rs_dvd` + """ + ... + +def cyclic_code_generators(n: int) -> list[list[int]]: + """ +The generator polynomials of every binary cyclic code of length `n`, as +the divisors of `x^n - 1` over `GF(2)`. + +A cyclic code of length `n` is exactly an ideal in `GF(2)[x] / (x^n - 1)`, +and every such ideal is generated by a divisor of `x^n - 1`. So the +cyclic codes of a given length are in bijection with those divisors, and +listing them lists the codes. Returned constant term first. + +Panics: +Panics unless `n` is odd and at most 31 -- an even `n` makes `x^n - 1` +non-squarefree in characteristic two, and the enumeration is exponential. + +Rust: `codes::reed_solomon::cyclic_code_generators` + """ + ... diff --git a/bindings/python/python/numeria/color_science.pyi b/bindings/python/python/numeria/color_science.pyi new file mode 100644 index 0000000..5c5468e --- /dev/null +++ b/bindings/python/python/numeria/color_science.pyi @@ -0,0 +1,127 @@ +""" +Colour: the standard spaces, the transforms between them, and perceptual measures. RGB to and from HSV, HSL and CIE XYZ, with the sRGB transfer function kept separate from the linear values -- the distinction that most colour bugs come from, since averaging or blending is only meaningful in linear light. Also spectral colour (wavelength to RGB), the Planckian locus (blackbody temperature to RGB, and the correlated colour temperature back), relative luminance and the WCAG contrast ratio. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def wavelength_to_rgb(wavelength_nm: float) -> tuple[float, float, float]: + """ +Convert a visible light wavelength (380-780 nm) to linear RGB in [0, 1]. + +Uses a standard piecewise approximation with intensity falloff at the +edges of the visible spectrum. + +Rust: `color_science::wavelength_to_rgb` + """ + ... + +def blackbody_to_rgb(temperature_k: float) -> tuple[float, float, float]: + """ +Convert a blackbody temperature (1000-40000 K) to linear RGB in [0, 1]. + +Uses the Tanner Helland approximation. + +Rust: `color_science::blackbody_to_rgb` + """ + ... + +def rgb_to_hsv(r: float, g: float, b: float) -> tuple[float, float, float]: + """ +Convert linear RGB `[0,1]` to HSV. H in `[0,360)`, S and V in `[0,1]`. + +Rust: `color_science::rgb_to_hsv` + """ + ... + +def hsv_to_rgb(h: float, s: float, v: float) -> tuple[float, float, float]: + """ +Convert HSV to linear RGB. H in `[0,360)`, S and V in `[0,1]`. + +Rust: `color_science::hsv_to_rgb` + """ + ... + +def rgb_to_hsl(r: float, g: float, b: float) -> tuple[float, float, float]: + """ +Convert linear RGB `[0,1]` to HSL. H in `[0,360)`, S and L in `[0,1]`. + +Rust: `color_science::rgb_to_hsl` + """ + ... + +def hsl_to_rgb(h: float, s: float, l: float) -> tuple[float, float, float]: + """ +Convert HSL to linear RGB. H in `[0,360)`, S and L in `[0,1]`. + +Rust: `color_science::hsl_to_rgb` + """ + ... + +def linear_to_srgb(c: float) -> float: + """ +Apply sRGB gamma correction to a linear channel value. + +Rust: `color_science::linear_to_srgb` + """ + ... + +def srgb_to_linear(c: float) -> float: + """ +Convert an sRGB gamma-encoded channel value back to linear. + +Rust: `color_science::srgb_to_linear` + """ + ... + +def rgb_to_xyz(r: float, g: float, b: float) -> tuple[float, float, float]: + """ +Convert linear sRGB to CIE XYZ (D65 illuminant). + +Rust: `color_science::rgb_to_xyz` + """ + ... + +def xyz_to_rgb(x: float, y: float, z: float) -> tuple[float, float, float]: + """ +Convert CIE XYZ (D65 illuminant) to linear sRGB. + +Rust: `color_science::xyz_to_rgb` + """ + ... + +def correlated_color_temperature(x: float, y: float) -> float: + """ +Compute correlated color temperature from CIE xy chromaticity using +McCamy's approximation. + +Rust: `color_science::correlated_color_temperature` + """ + ... + +def color_difference_euclidean(r1: float, g1: float, b1: float, r2: float, g2: float, b2: float) -> float: + """ +Euclidean color difference in RGB space. + +Rust: `color_science::color_difference_euclidean` + """ + ... + +def luminance(r: float, g: float, b: float) -> float: + """ +Relative luminance per ITU-R BT.709. + +Rust: `color_science::luminance` + """ + ... + +def contrast_ratio(l1: float, l2: float) -> float: + """ +WCAG contrast ratio between two relative luminance values. + +Rust: `color_science::contrast_ratio` + """ + ... diff --git a/bindings/python/python/numeria/continuum_mechanics.pyi b/bindings/python/python/numeria/continuum_mechanics.pyi new file mode 100644 index 0000000..181b006 --- /dev/null +++ b/bindings/python/python/numeria/continuum_mechanics.pyi @@ -0,0 +1,177 @@ +""" +Stress and strain as tensors, and the yield criteria built on them. The stress tensor with its invariants, principal stresses, and the split into hydrostatic and deviatoric parts -- the split that matters because metals yield on the deviatoric part alone, which is why the von Mises criterion ignores hydrostatic pressure entirely. Strain in both the small-strain and Green-Lagrange forms, the isotropic 3-D Hooke's law `σᵢⱼ = λ δᵢⱼ ε_kk + 2μ εᵢⱼ` and its compliance inverse, plane stress and plane strain, and the von Mises, Tresca, Mohr-Coulomb and Drucker-Prager yield criteria. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.linalg import Mat3 + +def stress_tensor(sxx: float, syy: float, szz: float, sxy: float, sxz: float, syz: float) -> Mat3: + """ +Constructs a symmetric 3x3 Cauchy stress tensor from six independent components. + +Rust: `continuum_mechanics::stress_tensor` + """ + ... + +def stress_invariants(stress: Mat3) -> tuple[float, float, float]: + """ +Computes the three stress invariants (I1, I2, I3) of a symmetric stress tensor. + +I1 = tr(sigma), I2 = (tr^2 - tr(sigma^2))/2, I3 = det(sigma). + +Rust: `continuum_mechanics::stress_invariants` + """ + ... + +def principal_stresses(stress: Mat3) -> list[float]: + """ +Computes the three principal stresses (eigenvalues) of a symmetric 3x3 tensor, +returned in descending order: sigma1 >= sigma2 >= sigma3. + +Uses the analytical cubic solution via the characteristic equation det(sigma - lambda*I) = 0. + +Rust: `continuum_mechanics::principal_stresses` + """ + ... + +def hydrostatic_stress(stress: Mat3) -> float: + """ +Hydrostatic (mean) stress: sigma_h = tr(sigma) / 3. + +Rust: `continuum_mechanics::hydrostatic_stress` + """ + ... + +def deviatoric_stress(stress: Mat3) -> Mat3: + """ +Deviatoric stress tensor: s = sigma - sigma_h * I. + +Rust: `continuum_mechanics::deviatoric_stress` + """ + ... + +def von_mises_from_tensor(stress: Mat3) -> float: + """ +Von Mises equivalent stress from a full stress tensor: sigma_vm = sqrt(3/2 * s:s). + +Rust: `continuum_mechanics::von_mises_from_tensor` + """ + ... + +def max_shear_stress(stress: Mat3) -> float: + """ +Maximum shear stress: tau_max = (sigma1 - sigma3) / 2. + +Rust: `continuum_mechanics::max_shear_stress` + """ + ... + +def strain_tensor(exx: float, eyy: float, ezz: float, exy: float, exz: float, eyz: float) -> Mat3: + """ +Constructs a symmetric 3x3 strain tensor from six independent components. + +Rust: `continuum_mechanics::strain_tensor` + """ + ... + +def volumetric_strain(strain: Mat3) -> float: + """ +Volumetric strain: epsilon_v = tr(epsilon). + +Rust: `continuum_mechanics::volumetric_strain` + """ + ... + +def deviatoric_strain(strain: Mat3) -> Mat3: + """ +Deviatoric strain tensor: e = epsilon - (epsilon_v / 3) * I. + +Rust: `continuum_mechanics::deviatoric_strain` + """ + ... + +def strain_from_displacement_gradient(grad_u: Mat3) -> Mat3: + """ +Small (infinitesimal) strain from the displacement gradient: epsilon = (grad_u + grad_u^T) / 2. + +Rust: `continuum_mechanics::strain_from_displacement_gradient` + """ + ... + +def green_lagrange_strain(deformation_gradient: Mat3) -> Mat3: + """ +Green-Lagrange finite strain tensor: E = (F^T F - I) / 2. + +Rust: `continuum_mechanics::green_lagrange_strain` + """ + ... + +def hooke_3d(strain: Mat3, youngs: float, poisson: float) -> Mat3: + """ +3D isotropic linear elastic (Hooke's law): +sigma_ij = lambda * tr(epsilon) * delta_ij + 2 * mu * epsilon_ij. + +Rust: `continuum_mechanics::hooke_3d` + """ + ... + +def compliance_matrix_isotropic(youngs: float, poisson: float) -> list[float]: + """ +Returns the six key elastic constants for an isotropic material: +[C11, C12, C44, lambda, mu (shear modulus), K (bulk modulus)]. + +Rust: `continuum_mechanics::compliance_matrix_isotropic` + """ + ... + +def plane_stress(strain_xx: float, strain_yy: float, strain_xy: float, youngs: float, poisson: float) -> tuple[float, float, float]: + """ +Plane stress: returns (sigma_xx, sigma_yy, tau_xy) given in-plane strains. +Uses the constitutive relation sigma = E/(1-nu^2) * [1,nu; nu,1] * epsilon for normal, +and tau_xy = G * gamma_xy where G = E/(2(1+nu)). + +Rust: `continuum_mechanics::plane_stress` + """ + ... + +def plane_strain(strain_xx: float, strain_yy: float, strain_xy: float, youngs: float, poisson: float) -> tuple[float, float, float]: + """ +Plane strain: returns (sigma_xx, sigma_yy, tau_xy) given in-plane strains. +This is the 3D Hooke's law with epsilon_zz = 0 and the z-normal stress is nonzero but not returned. + +Rust: `continuum_mechanics::plane_strain` + """ + ... + +def tresca_stress(stress: Mat3) -> float: + """ +Tresca equivalent stress: max(|s1-s2|, |s2-s3|, |s3-s1|). + +Rust: `continuum_mechanics::tresca_stress` + """ + ... + +def mohr_coulomb(normal_stress: float, shear_stress: float, cohesion: float, friction_angle: float) -> float: + """ +Mohr-Coulomb failure criterion: tau - c - sigma * tan(phi). +Returns positive if the stress state violates the criterion (failure). +`friction_angle` is in radians. + +Rust: `continuum_mechanics::mohr_coulomb` + """ + ... + +def drucker_prager(stress: Mat3, cohesion: float, friction_angle: float) -> float: + """ +Drucker-Prager failure criterion: sqrt(J2) + alpha * I1 - k. +Returns positive if the stress state violates the criterion (failure). +Alpha and k are derived from cohesion c and friction angle phi (radians) +using the inscribed-cone approximation (matching Mohr-Coulomb for compression). + +Rust: `continuum_mechanics::drucker_prager` + """ + ... diff --git a/bindings/python/python/numeria/control_systems/__init__.pyi b/bindings/python/python/numeria/control_systems/__init__.pyi new file mode 100644 index 0000000..630a45f --- /dev/null +++ b/bindings/python/python/numeria/control_systems/__init__.pyi @@ -0,0 +1,175 @@ +""" +Linear control: system response, stability margins and PID tuning. First- and second-order step and impulse responses in closed form, and the parameters that characterise them -- natural frequency, damping ratio, rise and settling time, percent overshoot, bandwidth. Stability is assessed through the gain and phase margins, which say how much extra gain or delay the loop tolerates before it oscillates. Steady-state error is given by system type. PID tuning uses the Ziegler-Nichols rules. They are a starting point rather than an answer: they were derived for a quarter-amplitude decay and typically give an aggressive loop that wants detuning. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from . import kalman +from numeria.resonance.oscillator import Damping +from numeria.control_systems.kalman import ExtendedKalmanFilter as ExtendedKalmanFilter +from numeria.control_systems.kalman import KalmanFilter as KalmanFilter + +class PidController: + """ + +Rust: `control_systems::PidController` + """ + def __init__(self, kp: float, ki: float, kd: float, output_min: float, output_max: float) -> None: ... + def update(self, setpoint: float, measured: float, dt: float) -> float: ... + def reset(self) -> None: ... + @property + def kp(self) -> float: ... + @property + def ki(self) -> float: ... + @property + def kd(self) -> float: ... + +def first_order_step_response(gain: float, tau: float, t: float) -> float: + """ +First-order step response: y(t) = K(1 - e^(-t/τ)) + +Rust: `control_systems::first_order_step_response` + """ + ... + +def first_order_impulse_response(gain: float, tau: float, t: float) -> float: + """ +First-order impulse response: h(t) = (K/τ)·e^(-t/τ) + +Rust: `control_systems::first_order_impulse_response` + """ + ... + +def second_order_step_response(gain: float, omega_n: float, zeta: float, t: float) -> float: + """ +Second-order step response for underdamped, critically damped, and overdamped systems. + +Rust: `control_systems::second_order_step_response` + """ + ... + +def second_order_natural_frequency(k: float, m: float) -> float: + """ +Natural frequency of a second-order system: ωn = √(k/m) + +Rust: `control_systems::second_order_natural_frequency` + """ + ... + +def second_order_damping_ratio(c: float, k: float, m: float) -> float: + """ +Damping ratio of a second-order system: ζ = c/(2√(km)) + +Rust: `control_systems::second_order_damping_ratio` + """ + ... + +def rise_time_first_order(tau: float) -> float: + """ +Rise time of a first-order system (10% to 90%): tr = 2.2τ + +Rust: `control_systems::rise_time_first_order` + """ + ... + +def settling_time_first_order(tau: float) -> float: + """ +Settling time of a first-order system (2% criterion): ts = 4τ + +Rust: `control_systems::settling_time_first_order` + """ + ... + +def settling_time_second_order(zeta: float, omega_n: float) -> float: + """ +Settling time of a second-order system (2% criterion): ts = 4/(ζωn) + +Rust: `control_systems::settling_time_second_order` + """ + ... + +def overshoot_percent(zeta: float) -> float: + """ +Peak overshoot percentage: Mp = 100·exp(-πζ/√(1-ζ²)) + +Rust: `control_systems::overshoot_percent` + """ + ... + +def bandwidth_first_order(tau: float) -> float: + """ +Bandwidth of a first-order system: ωb = 1/τ + +Rust: `control_systems::bandwidth_first_order` + """ + ... + +def gain_margin_db(open_loop_gain_at_phase_crossover: float) -> float: + """ +Gain margin in dB: GM = -20·log₁₀(|G(jω)|) at the phase crossover frequency + +Rust: `control_systems::gain_margin_db` + """ + ... + +def phase_margin(phase_at_gain_crossover: float) -> float: + """ +Phase margin in degrees: PM = 180° + φ(ωgc) + +Rust: `control_systems::phase_margin` + """ + ... + +def steady_state_error_type0(gain: float) -> float: + """ +Steady-state error for a type-0 system with step input: ess = 1/(1 + K) + +Rust: `control_systems::steady_state_error_type0` + """ + ... + +def steady_state_error_type1(gain: float) -> float: + """ +Steady-state error for a type-1 system with ramp input: ess = 1/K + +Rust: `control_systems::steady_state_error_type1` + """ + ... + +def is_stable_first_order(tau: float) -> bool: + """ +Check stability of a first-order system: stable when τ > 0. + +Rust: `control_systems::is_stable_first_order` + """ + ... + +def is_stable_second_order(zeta: float, omega_n: float) -> bool: + """ +Check stability of a second-order system: stable when ζ > 0 and ωn > 0. + +Rust: `control_systems::is_stable_second_order` + """ + ... + +def routh_criterion_2nd(a0: float, a1: float, a2: float) -> bool: + """ +Routh stability criterion for a 2nd-order polynomial: stable when all coefficients > 0. + +Rust: `control_systems::routh_criterion_2nd` + """ + ... + +def transfer_function_poles(denominator: list[float]) -> list[complex]: + """ +Poles of a transfer function: the complex roots of the denominator +polynomial (coefficients highest degree first), via +`numerical::roots::polynomial_roots`. A system is BIBO-stable when +every pole has a negative real part. + +Rust: `control_systems::transfer_function_poles` + """ + ... diff --git a/bindings/python/python/numeria/control_systems/kalman.pyi b/bindings/python/python/numeria/control_systems/kalman.pyi new file mode 100644 index 0000000..338bbf8 --- /dev/null +++ b/bindings/python/python/numeria/control_systems/kalman.pyi @@ -0,0 +1,55 @@ +""" +Kalman filtering. Linear filter: predict x ← F·x, P ← F·P·Fᵀ + Q; update with gain K = P·Hᵀ·S⁻¹, S = H·P·Hᵀ + R, using the Joseph-form covariance update P ← (I−KH)·P·(I−KH)ᵀ + K·R·Kᵀ so P stays symmetric PSD. The gain solve goes through `linalg::lu`. Reference: Bar-Shalom, Li & Kirubarajan, *Estimation with Applications to Tracking*. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.linalg.matrix import Matrix + +class ExtendedKalmanFilter: + """ +Extended Kalman filter: nonlinear transition f and observation h +with user-supplied Jacobians, linearized at the current estimate. + +Rust: `control_systems::kalman::ExtendedKalmanFilter` + """ + def predict(self) -> None: ... + def update(self, z: list[float]) -> None: ... + @property + def x(self) -> list[float]: ... + @property + def p(self) -> Matrix: ... + @property + def q(self) -> Matrix: ... + @property + def r(self) -> Matrix: ... + +class KalmanFilter: + """ +Linear Kalman filter with state x, covariance P, transition F, +observation H, process noise Q, and measurement noise R. + +Rust: `control_systems::kalman::KalmanFilter` + """ + def __init__(self, x: list[float], p: Matrix | Sequence[Sequence[float]], f: Matrix | Sequence[Sequence[float]], h: Matrix | Sequence[Sequence[float]], q: Matrix | Sequence[Sequence[float]], r: Matrix | Sequence[Sequence[float]]) -> None: ... + def predict(self) -> None: ... + def update(self, z: list[float]) -> None: ... + @staticmethod + def constant_velocity_1d(dt: float, process_noise: float, measurement_noise: float) -> KalmanFilter: ... + @staticmethod + def constant_velocity_3d(dt: float, q: float, r: float) -> KalmanFilter: ... + @property + def x(self) -> list[float]: ... + @property + def p(self) -> Matrix: ... + @property + def f(self) -> Matrix: ... + @property + def h(self) -> Matrix: ... + @property + def q(self) -> Matrix: ... + @property + def r(self) -> Matrix: ... diff --git a/bindings/python/python/numeria/core/__init__.pyi b/bindings/python/python/numeria/core/__init__.pyi new file mode 100644 index 0000000..24201f1 --- /dev/null +++ b/bindings/python/python/numeria/core/__init__.pyi @@ -0,0 +1,12 @@ +""" +Pure numeric building blocks: compensated summation, forward-mode automatic differentiation, and interval arithmetic. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from . import compensated, dual, interval + + diff --git a/bindings/python/python/numeria/core/compensated.pyi b/bindings/python/python/numeria/core/compensated.pyi new file mode 100644 index 0000000..daee494 --- /dev/null +++ b/bindings/python/python/numeria/core/compensated.pyi @@ -0,0 +1,50 @@ +""" +Compensated (error-free-transformation) summation. Formulas: Neumaier's improved Kahan-Babuska summation (A. Neumaier, "Rundungsfehleranalyse einiger Verfahren zur Summation endlicher Summen", ZAMM 54, 1974) and recursive pairwise summation (Higham, *Accuracy and Stability of Numerical Algorithms*, ch. 4). +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def sum_neumaier(xs: list[float]) -> float: + """ +Compensated (error-free-transformation) summation. + +Formulas: Neumaier's improved Kahan-Babuska summation +(A. Neumaier, "Rundungsfehleranalyse einiger Verfahren zur Summation +endlicher Summen", ZAMM 54, 1974) and recursive pairwise summation +(Higham, *Accuracy and Stability of Numerical Algorithms*, ch. 4). +Neumaier compensated sum of a slice. + +Computes `Σ xᵢ` with a running compensation term that captures the +low-order bits lost in each addition, giving results accurate to +O(1) ulp independent of length for well-scaled data. + +Rust: `core::compensated::sum_neumaier` + """ + ... + +def sum_pairwise(xs: list[float]) -> float: + """ +Recursive pairwise sum of a slice: `Σ xᵢ` with O(log n) error growth. + +Splits the slice in half and sums each half recursively; runs of up +to 32 elements are summed naively as the base case. + +Rust: `core::compensated::sum_pairwise` + """ + ... + +def dot_compensated(a: list[float], b: list[float]) -> float: + """ +Compensated dot product `Σ aᵢ·bᵢ` via Neumaier accumulation of the +individual products. + +Panics: +Panics if `a` and `b` have different lengths. + +Rust: `core::compensated::dot_compensated` + """ + ... diff --git a/bindings/python/python/numeria/core/dual.pyi b/bindings/python/python/numeria/core/dual.pyi new file mode 100644 index 0000000..383734e --- /dev/null +++ b/bindings/python/python/numeria/core/dual.pyi @@ -0,0 +1,51 @@ +""" +Forward-mode automatic differentiation with dual numbers. A dual number x = re + ε·eps with ε² = 0 propagates exact first derivatives through arithmetic: f(a + ε·a') = f(a) + ε·f'(a)·a'. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +class Dual: + """ +Dual number: `re` carries the value, `eps` the derivative. + +Rust: `core::dual::Dual` + """ + def __init__(self, re: float, eps: float) -> None: ... + @staticmethod + def variable(x: float) -> Dual: ... + @staticmethod + def constant(c: float) -> Dual: ... + def sin(self) -> Dual: ... + def cos(self) -> Dual: ... + def tan(self) -> Dual: ... + def exp(self) -> Dual: ... + def ln(self) -> Dual: ... + def sqrt(self) -> Dual: ... + def powf(self, p: float) -> Dual: ... + def powi(self, n: int) -> Dual: ... + def atan(self) -> Dual: ... + def sinh(self) -> Dual: ... + def cosh(self) -> Dual: ... + def tanh(self) -> Dual: ... + def abs(self) -> Dual: ... + def __add__(self, rhs: Dual | Sequence[float]) -> Dual: ... + def __sub__(self, rhs: Dual | Sequence[float]) -> Dual: ... + def __mul__(self, rhs: Dual | Sequence[float]) -> Dual: ... + def __truediv__(self, rhs: Dual | Sequence[float]) -> Dual: ... + def __neg__(self) -> Dual: ... + @property + def re(self) -> float: ... + @property + def eps(self) -> float: ... + +def derivative(f: Callable[[Dual | Sequence[float]], Dual | Sequence[float]], x: float) -> float: + """ +Exact derivative f'(x) of a scalar function via forward-mode AD. + +Rust: `core::dual::derivative` + """ + ... diff --git a/bindings/python/python/numeria/core/interval.pyi b/bindings/python/python/numeria/core/interval.pyi new file mode 100644 index 0000000..7457e02 --- /dev/null +++ b/bindings/python/python/numeria/core/interval.pyi @@ -0,0 +1,57 @@ +""" +Rigorous interval arithmetic with outward rounding. Every operation returns an interval guaranteed to contain the true real result for all inputs in the operand intervals: computed bounds are widened outward with `f64::next_down`/`next_up` (plus a small ulp margin for transcendental functions whose libm error is ≤ 1 ulp but unproven). Reference: Moore, Kearfott & Cloud, *Introduction to Interval Analysis* (SIAM, 2009). +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +class Interval: + """ +Closed interval [lo, hi]. + +Rust: `core::interval::Interval` + """ + def __init__(self, lo: float, hi: float) -> None: ... + @staticmethod + def point(x: float) -> Interval: ... + def width(self) -> float: ... + def midpoint(self) -> float: ... + def contains(self, x: float) -> bool: ... + def intersect(self, other: Interval | Sequence[float]) -> Optional[Interval]: ... + def hull(self, other: Interval | Sequence[float]) -> Interval: ... + def sqrt(self) -> Interval: ... + def exp(self) -> Interval: ... + def sin(self) -> Interval: ... + def cos(self) -> Interval: ... + def powi(self, n: int) -> Interval: ... + def __add__(self, rhs: Interval | Sequence[float]) -> Interval: ... + def __sub__(self, rhs: Interval | Sequence[float]) -> Interval: ... + def __mul__(self, rhs: Interval | Sequence[float]) -> Interval: ... + def __truediv__(self, rhs: Interval | Sequence[float]) -> Interval: ... + def __neg__(self) -> Interval: ... + @property + def lo(self) -> float: ... + @property + def hi(self) -> float: ... + +def interval_newton(f: Callable[[Interval | Sequence[float]], Interval | Sequence[float]], df: Callable[[Interval | Sequence[float]], Interval | Sequence[float]], x0: Interval | Sequence[float], tol: float, max_iter: int) -> list[Interval]: + """ +Rigorous interval Newton method: encloses every root of f in `x0`. + +`f` and `df` must be interval extensions of the function and its +derivative. Boxes where 0 ∉ f(X) are discarded; where f'(X) +excludes 0 the Newton contraction N(X) = m − f(m)/f'(X) ∩ X is +applied; otherwise the box is bisected. Boxes narrower than `tol` +that still satisfy 0 ∈ f(X) are reported (overlapping neighbors +merged). Every real root in `x0` is contained in some returned +interval; spurious near-root boxes may also appear at width ~tol. + +Panics: +Panics unless tol > 0. + +Rust: `core::interval::interval_newton` + """ + ... diff --git a/bindings/python/python/numeria/curves.pyi b/bindings/python/python/numeria/curves.pyi new file mode 100644 index 0000000..e63d507 --- /dev/null +++ b/bindings/python/python/numeria/curves.pyi @@ -0,0 +1,213 @@ +""" +Plane curves: conics, Bézier curves, and parametric families. The conic sections with their eccentricities, foci and the discriminant that classifies a general quadratic; quadratic and cubic Bézier curves in 2-D and 3-D; and parametric circles, ellipses, spirals, Lissajous figures, cycloids and helices. Arc length and signed curvature close the module. For subdivision surfaces and B-spline or NURBS patches see `mesh::surfaces`; for space curves with Frenet frames see `patterns::knots`. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.patterns.tilings import Archimedean +from numeria.spatial.primitives import Circle +from numeria.math import Vec3 + +def circle_area(radius: float) -> float: + """ +Area of a circle: A = πr² + +Rust: `curves::circle_area` + """ + ... + +def circle_circumference(radius: float) -> float: + """ +Circle circumference: C = 2πr + +Rust: `curves::circle_circumference` + """ + ... + +def circle_equation(x: float, y: float, cx: float, cy: float, r: float) -> float: + """ +Returns (x-cx)^2 + (y-cy)^2 - r^2. Zero means the point lies on the circle. + +Rust: `curves::circle_equation` + """ + ... + +def ellipse_circumference_approx(a: float, b: float) -> float: + """ +Ramanujan approximation: pi * (3(a+b) - sqrt((3a+b)(a+3b))) + +Rust: `curves::ellipse_circumference_approx` + """ + ... + +def ellipse_equation(x: float, y: float, a: float, b: float) -> float: + """ +Returns x^2/a^2 + y^2/b^2 - 1. Zero means the point lies on the ellipse. + +Rust: `curves::ellipse_equation` + """ + ... + +def ellipse_eccentricity(a: float, b: float) -> float: + """ +Eccentricity e = sqrt(1 - b^2/a^2) for a > b. + +Rust: `curves::ellipse_eccentricity` + """ + ... + +def parabola_focus(a: float) -> float: + """ +Focus distance f = 1/(4a) for parabola y = ax^2. + +Rust: `curves::parabola_focus` + """ + ... + +def parabola_equation(x: float, a: float) -> float: + """ +Parabola equation: y = ax² + +Rust: `curves::parabola_equation` + """ + ... + +def hyperbola_eccentricity(a: float, b: float) -> float: + """ +Eccentricity e = sqrt(1 + b^2/a^2). + +Rust: `curves::hyperbola_eccentricity` + """ + ... + +def hyperbola_asymptote_slope(a: float, b: float) -> float: + """ +Asymptote slope of a hyperbola: m = b/a + +Rust: `curves::hyperbola_asymptote_slope` + """ + ... + +def conic_discriminant(a: float, b: float, c: float) -> float: + """ +Discriminant B^2 - 4AC for general conic Ax^2 + Bxy + Cy^2 + ... +Negative => ellipse, zero => parabola, positive => hyperbola. + +Rust: `curves::conic_discriminant` + """ + ... + +def bezier_quadratic(t: float, p0: tuple[float, float], p1: tuple[float, float], p2: tuple[float, float]) -> tuple[float, float]: + """ +Quadratic Bezier curve point: B(t) = (1-t)²P₀ + 2(1-t)tP₁ + t²P₂ + +Rust: `curves::bezier_quadratic` + """ + ... + +def bezier_cubic(t: float, p0: tuple[float, float], p1: tuple[float, float], p2: tuple[float, float], p3: tuple[float, float]) -> tuple[float, float]: + """ +Cubic Bezier curve point: B(t) = (1-t)³P₀ + 3(1-t)²tP₁ + 3(1-t)t²P₂ + t³P₃ + +Rust: `curves::bezier_cubic` + """ + ... + +def bezier_quadratic_3d(t: float, p0: Vec3 | Sequence[float], p1: Vec3 | Sequence[float], p2: Vec3 | Sequence[float]) -> Vec3: + """ +Quadratic Bezier curve in 3D: B(t) = (1-t)²P₀ + 2(1-t)tP₁ + t²P₂ + +Rust: `curves::bezier_quadratic_3d` + """ + ... + +def bezier_cubic_3d(t: float, p0: Vec3 | Sequence[float], p1: Vec3 | Sequence[float], p2: Vec3 | Sequence[float], p3: Vec3 | Sequence[float]) -> Vec3: + """ +Cubic Bezier curve in 3D: B(t) = (1-t)³P₀ + 3(1-t)²tP₁ + 3(1-t)t²P₂ + t³P₃ + +Rust: `curves::bezier_cubic_3d` + """ + ... + +def bezier_sample(p0: tuple[float, float], p1: tuple[float, float], p2: tuple[float, float], p3: tuple[float, float], n: int) -> list[tuple[float, float]]: + """ +Sample n+1 evenly spaced points along a cubic Bezier curve (t from 0 to 1). + +Rust: `curves::bezier_sample` + """ + ... + +def parametric_circle(t: float, r: float) -> tuple[float, float]: + """ +Parametric circle: (x, y) = (r·cos(t), r·sin(t)) + +Rust: `curves::parametric_circle` + """ + ... + +def parametric_ellipse(t: float, a: float, b: float) -> tuple[float, float]: + """ +Parametric ellipse: (x, y) = (a·cos(t), b·sin(t)) + +Rust: `curves::parametric_ellipse` + """ + ... + +def parametric_spiral(t: float, a: float, b: float) -> tuple[float, float]: + """ +Archimedean spiral: r = a + b*t. + +Rust: `curves::parametric_spiral` + """ + ... + +def parametric_lissajous(t: float, a: float, b: float, delta: float) -> tuple[float, float]: + """ +Lissajous figure: (sin(a*t + delta), sin(b*t)). + +Rust: `curves::parametric_lissajous` + """ + ... + +def parametric_cycloid(t: float, r: float) -> tuple[float, float]: + """ +Cycloid: (r(t - sin(t)), r(1 - cos(t))). + +Rust: `curves::parametric_cycloid` + """ + ... + +def parametric_helix(t: float, radius: float, pitch: float) -> tuple[float, float, float]: + """ +Helix: (r cos(t), r sin(t), pitch * t / (2*pi)). + +Rust: `curves::parametric_helix` + """ + ... + +def arc_length_parametric(fx: Callable[[float], float], fy: Callable[[float], float], t0: float, t1: float, n: int) -> float: + """ +Numerical arc length via piecewise linear approximation with n segments. + +Rust: `curves::arc_length_parametric` + """ + ... + +def arc_length_circle(radius: float, angle: float) -> float: + """ +Arc length of a circular arc: s = rθ + +Rust: `curves::arc_length_circle` + """ + ... + +def curvature_2d(dxdt: float, dydt: float, d2xdt2: float, d2ydt2: float) -> float: + """ +Curvature kappa = |x'y'' - y'x''| / (x'^2 + y'^2)^(3/2). + +Rust: `curves::curvature_2d` + """ + ... diff --git a/bindings/python/python/numeria/discrete/__init__.pyi b/bindings/python/python/numeria/discrete/__init__.pyi new file mode 100644 index 0000000..a222a39 --- /dev/null +++ b/bindings/python/python/numeria/discrete/__init__.pyi @@ -0,0 +1,12 @@ +""" +Discrete mathematics: primes and factorization, elementary and analytic number theory, counting and enumeration, integer partitions, integer sequences, and union-find. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from . import combinatorics, disjoint_set, number_theory, partitions, primes, sequences + + diff --git a/bindings/python/python/numeria/discrete/combinatorics.pyi b/bindings/python/python/numeria/discrete/combinatorics.pyi new file mode 100644 index 0000000..a4697bc --- /dev/null +++ b/bindings/python/python/numeria/discrete/combinatorics.pyi @@ -0,0 +1,723 @@ +""" +Counting, enumeration, and the permutation group. Three kinds of function live here. Counting functions return a `BigInt` whenever the value outgrows 64 bits, which is almost immediately -- the Bell numbers pass `u64::MAX` at n = 25 and the Catalan numbers at n = 33. Enumeration functions return iterators that generate one object at a time rather than materialising the whole family. The permutation functions treat a `&[usize]` as the one-line form of a bijection on `0..n`, so `p[i]` is the image of `i`. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.linalg.matrix import Matrix +from numeria.exact.polynomial import PolyQ +from numeria.monte_carlo import Rng + +def binomial_u64(n: int, k: int) -> Optional[int]: + """ +`C(n, k)` in `u64`, exactly when the result fits, otherwise `None`. + +Multiplies and divides alternately so the running value is always an exact +binomial coefficient and therefore an integer: after step `i` the value is +`C(n, i + 1)`. + +The running product before the division is `C(n, i+1) * (i+1)`, which is up +to `k` times the answer, so doing this in `u64` would report overflow for +results that fit. It runs in `u128` instead and tests the *coefficient* +against `u64::MAX`. Since `k` is folded to `min(k, n-k)`, the coefficient +only increases along the loop, so passing the bound once is final. + +Rust: `discrete::combinatorics::binomial_u64` + """ + ... + +def binomial_mod_p(n: int, k: int, p: int) -> int: + """ +`C(n, k) mod p` for prime `p`, by Lucas's theorem. + +Lucas reduces the coefficient to a product of coefficients of the base-`p` +digits, each of which is below `p` and so computable directly. A digit of +`k` exceeding the matching digit of `n` makes the whole product zero. + +Panics: +Panics if `p` is zero or one. The result is only correct for prime `p`. + +Rust: `discrete::combinatorics::binomial_mod_p` + """ + ... + +def multinomial(ks: list[int]) -> int: + """ +The multinomial `(sum ks)! / prod(ks!)`. + +Built as a product of binomials rather than a ratio of factorials, so +every intermediate is itself an integer count. + +Rust: `discrete::combinatorics::multinomial` + """ + ... + +def permutations_count(n: int, k: int) -> Optional[int]: + """ +The falling factorial `n * (n-1) * ... * (n-k+1)`, or `None` on overflow. + +Rust: `discrete::combinatorics::permutations_count` + """ + ... + +def permutations_iter(items: list[int]) -> list[list[int]]: + """ +All permutations of `items`, by Heap's algorithm. + +Heap's algorithm reaches each of the `n!` arrangements with a single +transposition per step, so generating the whole family costs `O(n!)` swaps +rather than `O(n * n!)` copies -- the copies here are only to hand out +owned results. The order is Heap's, not lexicographic. + +Rust: `discrete::combinatorics::permutations_iter` + """ + ... + +def permutations_lex_next(p: MutableSequence[int]) -> bool: + """ +Advances `p` to the next permutation in lexicographic order in place. + +Returns `false` when `p` is already the last (descending) arrangement, in +which case `p` is left untouched. This is the standard pivot-and-reverse +step: find the rightmost ascent, swap its left element with the smallest +larger element to its right, then reverse the now-descending suffix. + +Rust: `discrete::combinatorics::permutations_lex_next` + """ + ... + +def nth_permutation(n_items: int, index: int) -> list[int]: + """ +The permutation of `0..n_items` at the given lexicographic `index`, by the +factorial number system. + +Digit `i` of the factoradic expansion says how many of the still-unused +symbols to skip, which is exactly what selecting the `index`-th +lexicographic arrangement does. + +Panics: +Panics if `index` is negative or at least `n_items!`. + +Rust: `discrete::combinatorics::nth_permutation` + """ + ... + +def permutation_index(p: list[int]) -> int: + """ +The lexicographic index of `p` among the permutations of its own symbols. + +Inverse of `nth_permutation`: counts, at each position, how many unused +symbols are smaller than the one chosen, and weights that by the factorial +of the remaining length. + +Rust: `discrete::combinatorics::permutation_index` + """ + ... + +def combinations_iter(n: int, k: int) -> list[list[int]]: + """ +The `k`-subsets of `0..n`, each sorted ascending, in lexicographic order. + +Rust: `discrete::combinatorics::combinations_iter` + """ + ... + +def combinations_with_replacement_iter(n: int, k: int) -> list[list[int]]: + """ +The `k`-multisets over `0..n`, each non-decreasing, in lexicographic order. + +Same shape as `combinations_iter` with the strict ceiling relaxed: +entries may repeat, so position `j` is capped at `n - 1` rather than at +`j + n - k`. + +Rust: `discrete::combinatorics::combinations_with_replacement_iter` + """ + ... + +def gray_code_iter(n_bits: int) -> list[int]: + """ +The `2^n_bits` reflected binary Gray codes in order. + +`g(i) = i XOR (i >> 1)`, whose consecutive values differ in exactly one +bit. + +Panics: +Panics if `n_bits` exceeds 63. + +Rust: `discrete::combinatorics::gray_code_iter` + """ + ... + +def subsets_iter(n: int) -> list[int]: + """ +The `2^n` subsets of `0..n` as bitmasks, in increasing numeric order. + +Panics: +Panics if `n` exceeds 63. + +Rust: `discrete::combinatorics::subsets_iter` + """ + ... + +def derangements_count(n: int) -> int: + """ +The number of permutations of `n` symbols with no fixed point. + +Uses the recurrence `D(n) = (n-1) (D(n-1) + D(n-2))`, which is exact in +integers, rather than the alternating factorial sum, which alternates in +sign and would need cancellation. + +Rust: `discrete::combinatorics::derangements_count` + """ + ... + +def is_derangement(p: list[int]) -> bool: + """ +True when `p` is a permutation with no fixed point. + +Rust: `discrete::combinatorics::is_derangement` + """ + ... + +def is_permutation(p: list[int]) -> bool: + """ +True when `p` is a bijection on `0..p.len()`. + +Rust: `discrete::combinatorics::is_permutation` + """ + ... + +def random_permutation(n: int, rng: Rng) -> list[int]: + """ +A uniformly random permutation of `0..n`, by Fisher-Yates. + +Each step picks uniformly from the untouched suffix, which gives every one +of the `n!` arrangements the same probability. + +Rust: `discrete::combinatorics::random_permutation` + """ + ... + +def random_derangement(n: int, rng: Rng) -> list[int]: + """ +A uniformly random derangement of `0..n`, by rejection. + +The density of derangements tends to `1/e`, so the expected number of +draws is about 2.72 regardless of `n` -- rejection is the cheap method +here, not a fallback. Returns the empty permutation for `n = 0` and panics +for `n = 1`, which has no derangement. + +Panics: +Panics if `n` is 1. + +Rust: `discrete::combinatorics::random_derangement` + """ + ... + +def permutation_compose(a: list[int], b: list[int]) -> list[int]: + """ +The composition `a` after `b`: `(a . b)(i) = a[b[i]]`. + +Panics: +Panics if the two permutations have different lengths. + +Rust: `discrete::combinatorics::permutation_compose` + """ + ... + +def permutation_inverse(p: list[int]) -> list[int]: + """ +The inverse permutation. + +Rust: `discrete::combinatorics::permutation_inverse` + """ + ... + +def permutation_cycle_type(p: list[int]) -> list[int]: + """ +The cycle lengths of `p`, sorted descending. + +This is the conjugacy class invariant: two permutations are conjugate in +the symmetric group exactly when their cycle types agree. Fixed points +count as cycles of length one, so the entries sum to `p.len()`. + +Rust: `discrete::combinatorics::permutation_cycle_type` + """ + ... + +def permutation_order(p: list[int]) -> int: + """ +The order of `p` in the symmetric group: the lcm of its cycle lengths. + +Returns a `BigInt` because the maximum order over `S_n` (Landau's +function) passes `u64::MAX` well before `n = 130`. + +Rust: `discrete::combinatorics::permutation_order` + """ + ... + +def permutation_sign(p: list[int]) -> int: + """ +The sign of `p`: `+1` for an even permutation, `-1` for an odd one. + +A cycle of length `L` is a product of `L - 1` transpositions, so the sign +is `(-1)^(n - number of cycles)`. + +Rust: `discrete::combinatorics::permutation_sign` + """ + ... + +def permutation_to_cycles(p: list[int]) -> list[list[int]]: + """ +The disjoint cycles of `p`, each starting at its smallest element, ordered +by that element. Fixed points appear as one-element cycles. + +Rust: `discrete::combinatorics::permutation_to_cycles` + """ + ... + +def permutation_from_cycles(n: int, cycles: list[list[int]]) -> list[int]: + """ +The permutation of `0..n` with the given disjoint cycles. + +Symbols not mentioned are fixed. Each cycle maps every element to the next +one listed and the last back to the first. + +Panics: +Panics if a symbol is at least `n` or appears in two cycles. + +Rust: `discrete::combinatorics::permutation_from_cycles` + """ + ... + +def permutation_matrix(p: list[int]) -> Matrix: + """ +The permutation matrix `P` with `P[p[j], j] = 1`. + +With this convention `P` applied to a coordinate vector moves the entry at +`j` to `p[j]`, so `permutation_matrix(compose(a, b))` is the product of the +two matrices in the same order. + +Rust: `discrete::combinatorics::permutation_matrix` + """ + ... + +def stirling_first(n: int, k: int) -> int: + """ +Unsigned Stirling numbers of the first kind: the number of permutations of +`n` symbols with exactly `k` cycles. + +Recurrence `c(n, k) = c(n-1, k-1) + (n-1) c(n-1, k)`: the new symbol is +either its own cycle or inserted after one of the `n-1` existing symbols. + +Rust: `discrete::combinatorics::stirling_first` + """ + ... + +def stirling_second(n: int, k: int) -> int: + """ +Stirling numbers of the second kind: the number of ways to partition `n` +labelled objects into exactly `k` non-empty unlabelled blocks. + +Recurrence `S(n, k) = S(n-1, k-1) + k S(n-1, k)`: the new object either +opens a block of its own or joins one of the `k` existing ones. + +Rust: `discrete::combinatorics::stirling_second` + """ + ... + +def bell_number(n: int) -> int: + """ +The `n`-th Bell number: the number of partitions of an `n`-element set. + +Rust: `discrete::combinatorics::bell_number` + """ + ... + +def bell_triangle(n: int) -> list[list[int]]: + """ +The first `n + 1` rows of the Bell (Peirce) triangle. + +Row 0 is `[1]`; each later row starts with the last entry of the previous +row and each subsequent entry is the sum of its left neighbour and the +entry above that neighbour. Row `i` begins with the `i`-th Bell number. + +Rust: `discrete::combinatorics::bell_triangle` + """ + ... + +def catalan(n: int) -> int: + """ +The `n`-th Catalan number, `C(2n, n) / (n + 1)`. + +Rust: `discrete::combinatorics::catalan` + """ + ... + +def catalan_mod(n: int, m: int) -> int: + """ +The `n`-th Catalan number modulo `m`, for any `m`. + +Uses the convolution recurrence `C(n+1) = sum_i C(i) C(n-i)` rather than +the closed form. The closed form needs a division by `n + 1`, which has no +modular meaning when `n + 1` shares a factor with `m`; the convolution is +pure addition and multiplication and so is valid for every modulus. +Costs `O(n^2)`. + +Panics: +Panics if `m` is zero. + +Rust: `discrete::combinatorics::catalan_mod` + """ + ... + +def eulerian_number(n: int, k: int) -> int: + """ +Eulerian number `A(n, k)`: permutations of `n` symbols with exactly `k` +ascents. + +Recurrence `A(n, k) = (k+1) A(n-1, k) + (n-k) A(n-1, k-1)`. + +Rust: `discrete::combinatorics::eulerian_number` + """ + ... + +def narayana(n: int, k: int) -> int: + """ +Narayana number `N(n, k) = C(n, k) C(n, k-1) / n`, the number of Dyck paths +of semilength `n` with exactly `k` peaks. Defined for `1 <= k <= n`. + +Rust: `discrete::combinatorics::narayana` + """ + ... + +def motzkin(n: int) -> int: + """ +The `n`-th Motzkin number: lattice paths from `(0,0)` to `(n,0)` with steps +up, down and level that never dip below the axis. + +Recurrence `M(n+1) = M(n) + sum_i M(i) M(n-1-i)`. + +Rust: `discrete::combinatorics::motzkin` + """ + ... + +def schroeder(n: int) -> int: + """ +The `n`-th large Schroeder number: lattice paths from `(0,0)` to `(n,n)` +with steps east, north and diagonal that stay weakly below the diagonal. + +Recurrence `3(2n-1) S(n-1) = (n+1) S(n) + (n-2) S(n-2)`, rearranged; done +here by the equivalent convolution `S(n) = S(n-1) + sum_i S(i) S(n-1-i)`. + +Rust: `discrete::combinatorics::schroeder` + """ + ... + +def delannoy(m: int, n: int) -> int: + """ +The Delannoy number `D(m, n)`: lattice paths from `(0,0)` to `(m,n)` with +east, north and diagonal steps. + +Rust: `discrete::combinatorics::delannoy` + """ + ... + +def lah_number(n: int, k: int) -> int: + """ +The unsigned Lah number `L(n, k) = C(n-1, k-1) n! / k!`: the number of ways +to partition `n` labelled objects into `k` non-empty ordered lists. + +Rust: `discrete::combinatorics::lah_number` + """ + ... + +def ballot_number(p: int, q: int) -> int: + """ +The ballot number: the number of ways to count `p` votes for A and `q` for +B so that A is never behind. + +Equal to `C(p+q, q) (p - q + 1) / (p + 1)`; zero when `q > p`. + +Rust: `discrete::combinatorics::ballot_number` + """ + ... + +def dyck_paths_iter(n: int) -> list[list[bool]]: + """ +The Dyck paths of semilength `n`, as step vectors of `2n` booleans where +`true` is an up step. + +Every prefix has at least as many up steps as down steps and the whole path +balances, so there are `catalan(n)` of them. Generated in lexicographic +order with `false < true`. + +Rust: `discrete::combinatorics::dyck_paths_iter` + """ + ... + +def set_partitions_iter(n: int) -> list[list[int]]: + """ +The set partitions of `0..n`, as restricted growth strings. + +Entry `i` of the string is the index of the block containing `i`. The +restriction is that a string starts at 0 and never jumps by more than one +above the running maximum, which makes the correspondence with partitions +exactly one-to-one -- block indices are forced to appear in order of their +smallest element, so relabelling the blocks cannot produce a duplicate. +There are `bell_number(n)` of them. + +Rust: `discrete::combinatorics::set_partitions_iter` + """ + ... + +def compositions_iter(n: int) -> list[list[int]]: + """ +The compositions of `n`: the ordered tuples of positive integers summing to +`n`. There are `2^(n-1)` for `n >= 1`, and one (the empty tuple) for `n = 0`. + +Generated from the `n - 1` gap positions: a composition is exactly a choice +of which of the `n - 1` gaps between `n` units to cut. + +Rust: `discrete::combinatorics::compositions_iter` + """ + ... + +def necklaces_count(n: int, k: int) -> int: + """ +The number of necklaces: `k`-colourings of `n` beads in a cycle, counted up +to rotation. + +Burnside over the cyclic group: the rotation by `j` fixes a colouring +exactly when the colouring is constant on the `gcd(j, n)` orbits, so the +count is `(1/n) sum_{d | n} phi(d) k^(n/d)`. + +Rust: `discrete::combinatorics::necklaces_count` + """ + ... + +def bracelets_count(n: int, k: int) -> int: + """ +The number of bracelets: `k`-colourings of `n` beads in a cycle, counted up +to rotation *and* reflection. + +Burnside over the dihedral group. The reflections contribute +`k^((n+1)/2)` each for odd `n`, and for even `n` split into `n/2` axes +through two beads (`k^(n/2 + 1)`) and `n/2` axes through two gaps +(`k^(n/2)`). + +Rust: `discrete::combinatorics::bracelets_count` + """ + ... + +def burnside_orbit_count(group_element_fixed_counts: list[int]) -> int: + """ +Burnside's lemma: the number of orbits is the average number of points +fixed by a group element. + +Takes one fixed-point count per group element, so the slice length is the +group order. + +Panics: +Panics on an empty slice, and if the average is not an integer -- which +cannot happen for a genuine group action, so a non-zero remainder means the +caller's counts are not a group's. + +Rust: `discrete::combinatorics::burnside_orbit_count` + """ + ... + +def polya_enumeration(cycle_index: PolyQ, colors: int) -> int: + """ +Polya enumeration: the number of colourings with `colors` colours, given a +cycle index. + +The cycle index of a group acting on `n` points is a polynomial in `n` +variables `a_1..a_n`. Polya's theorem with unweighted colours substitutes +the same value -- the number of colours -- for every variable, and the +result of that substitution is a polynomial in one variable. That single +variable form is what `cycle_index_cyclic`, `cycle_index_dihedral` and +`cycle_index_symmetric` return and what this function evaluates, so the +specialisation happens once at construction rather than at every call. + +Panics: +Panics if the value at `colors` is not an integer, which cannot happen for +a cycle index of a genuine group. + +Rust: `discrete::combinatorics::polya_enumeration` + """ + ... + +def cycle_index_cyclic(n: int) -> PolyQ: + """ +The cycle index of the cyclic group `C_n` acting on `n` points, with every +variable already set to the colour count: `(1/n) sum_{d | n} phi(d) x^(n/d)`. + +Rust: `discrete::combinatorics::cycle_index_cyclic` + """ + ... + +def cycle_index_dihedral(n: int) -> PolyQ: + """ +The cycle index of the dihedral group `D_n` acting on `n` points, with +every variable set to the colour count. + +Half the cyclic index plus the reflection average. + +Rust: `discrete::combinatorics::cycle_index_dihedral` + """ + ... + +def cycle_index_symmetric(n: int) -> PolyQ: + """ +The cycle index of the symmetric group `S_n` acting on `n` points, with +every variable set to the colour count. + +Averaging over all of `S_n` collapses to the rising factorial +`x (x+1) ... (x+n-1) / n!`, which is `C(x + n - 1, n)` -- the count of +`n`-multisets, exactly what "colourings up to any relabelling of the +points" means. + +Rust: `discrete::combinatorics::cycle_index_symmetric` + """ + ... + +def pigeonhole_min_overlap(items: int, boxes: int) -> int: + """ +The guaranteed occupancy of the fullest box: `ceil(items / boxes)`. + +The pigeonhole principle in its quantitative form -- some box holds at +least this many, and a balanced distribution shows the bound is attained. + +Panics: +Panics if `boxes` is zero. + +Rust: `discrete::combinatorics::pigeonhole_min_overlap` + """ + ... + +def ramsey_known(s: int, t: int) -> Optional[int]: + """ +The Ramsey number `R(s, t)` when it is known exactly, otherwise `None`. + +Only nine non-trivial values are known; everything beyond `R(4,5) = 25` +and the `R(3, t)` ladder is open, so this returns `None` rather than a +bound. + +Rust: `discrete::combinatorics::ramsey_known` + """ + ... + +def is_latin_square(sq: list[list[int]]) -> bool: + """ +True when every row and every column of `sq` is a permutation of `0..n`. + +Rust: `discrete::combinatorics::is_latin_square` + """ + ... + +def latin_square_random(n: int, rng: Rng) -> list[list[int]]: + """ +A random Latin square of order `n`. + +Built from the cyclic square `(i + j) mod n` by applying an independent +random permutation to the rows, to the columns, and to the symbols. Each +of those three operations preserves the Latin property, so the result is +always valid. It samples the isotopy class of the cyclic square rather +than all Latin squares uniformly, which the caller should not assume +otherwise. + +Rust: `discrete::combinatorics::latin_square_random` + """ + ... + +def magic_square(n: int) -> Optional[list[list[int]]]: + """ +A magic square of order `n`, or `None` for `n = 2`, which has none. + +Three constructions by residue: the Siamese method for odd `n`, the +complement pattern for `n` divisible by four, and Strachey's LUX method for +`n` congruent to 2 mod 4. Entries are `1..=n^2` and every row, column and +both diagonals sum to `n(n^2+1)/2`. + +Rust: `discrete::combinatorics::magic_square` + """ + ... + +def de_bruijn_sequence(k: int, n: int) -> list[int]: + """ +A de Bruijn sequence `B(k, n)`: a cyclic sequence of length `k^n` over the +alphabet `0..k` in which every `n`-tuple appears exactly once. + +Built by the Frank-Kessler-Maiorana algorithm, which concatenates the +Lyndon words over the alphabet whose length divides `n`, in lexicographic +order. + +Panics: +Panics if `k` is zero or `n` is zero. + +Rust: `discrete::combinatorics::de_bruijn_sequence` + """ + ... + +def perfect_shuffles_order(n_cards: int, out: bool) -> int: + """ +The number of perfect shuffles that restore a deck of `n_cards`. + +An out-shuffle keeps the top and bottom cards fixed and permutes the rest +by doubling their position modulo `n_cards - 1`, so its order is the +multiplicative order of 2 there. An in-shuffle moves every card, doubling +position modulo `n_cards + 1`. + +Panics: +Panics if `n_cards` is odd or below two: a perfect shuffle needs two equal +halves. + +Rust: `discrete::combinatorics::perfect_shuffles_order` + """ + ... + +def josephus(n: int, k: int) -> int: + """ +The survivor of the Josephus problem: `n` people in a circle, every `k`-th +eliminated, returned as a zero-based position. + +Recurrence `J(1) = 0`, `J(i) = (J(i-1) + k) mod i`: after the first +elimination the problem is the same one on `i - 1` people with the origin +shifted by `k`. + +Panics: +Panics if `n` or `k` is zero. + +Rust: `discrete::combinatorics::josephus` + """ + ... + +def tower_of_hanoi_moves(n: int, from_: int, to: int) -> list[tuple[int, int]]: + """ +The moves solving the Tower of Hanoi for `n` discs, as `(from, to)` pegs. + +Exactly `2^n - 1` moves, the known minimum. + +Panics: +Panics if `from` and `to` are equal or either is outside `0..3`. + +Rust: `discrete::combinatorics::tower_of_hanoi_moves` + """ + ... + +def twelvefold_way(n: int, k: int, injective: Optional[bool], surjective: Optional[bool], distinguishable_balls: bool, distinguishable_boxes: bool) -> int: + """ +The twelvefold way: `n` balls into `k` boxes under the six combinations of +distinguishability and the three restrictions. + +A restriction applies when its argument is `Some(true)`; `Some(false)` and +`None` both mean "no restriction", so `Some(false)` does not ask for a +map that fails to be injective. + +Rust: `discrete::combinatorics::twelvefold_way` + """ + ... diff --git a/bindings/python/python/numeria/discrete/disjoint_set.pyi b/bindings/python/python/numeria/discrete/disjoint_set.pyi new file mode 100644 index 0000000..e96959b --- /dev/null +++ b/bindings/python/python/numeria/discrete/disjoint_set.pyi @@ -0,0 +1,31 @@ +""" +Union-find over `0..n` with path compression and union by size. Shared infrastructure: graph minimum spanning trees, percolation cluster labelling, and single-linkage clustering all reduce to the same "merge these two, are these two together" question. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +class DisjointSet: + """ +Union-find over `0..n` with path compression and union by size. + +Shared infrastructure: graph minimum spanning trees, percolation cluster +labelling, and single-linkage clustering all reduce to the same +"merge these two, are these two together" question. +Disjoint-set forest over the elements `0..n`. + +Rust: `discrete::disjoint_set::DisjointSet` + """ + def __init__(self, n: int) -> None: ... + def len(self) -> int: ... + def is_empty(self) -> bool: ... + def count(self) -> int: ... + def find(self, x: int) -> int: ... + def union(self, a: int, b: int) -> bool: ... + def connected(self, a: int, b: int) -> bool: ... + def set_size(self, x: int) -> int: ... + def sets(self) -> list[list[int]]: ... + def labels(self) -> list[int]: ... diff --git a/bindings/python/python/numeria/discrete/number_theory.pyi b/bindings/python/python/numeria/discrete/number_theory.pyi new file mode 100644 index 0000000..4420a32 --- /dev/null +++ b/bindings/python/python/numeria/discrete/number_theory.pyi @@ -0,0 +1,616 @@ +""" +Elementary and analytic number theory. Divisibility and the Euclidean algorithm, modular arithmetic and the Chinese remainder theorem, the classical arithmetic functions (`phi`, `mu`, `sigma_k`, Carmichael's `lambda`) together with their sieves, multiplicative order, discrete logarithms, quadratic residues, and a collection of Diophantine and digit problems. Factorization comes from `discrete::primes`; nothing here re-implements it. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def gcd_u64(a: int, b: int) -> int: + """ +Greatest common divisor, by the binary (Stein) algorithm. + +`gcd(0, n) == n`, so `gcd(0, 0) == 0`. + +Rust: `discrete::number_theory::gcd_u64` + """ + ... + +def lcm_u64(a: int, b: int) -> int: + """ +Least common multiple; zero whenever either argument is zero. + +Panics: +Panics if the least common multiple does not fit in a `u64`. + +Rust: `discrete::number_theory::lcm_u64` + """ + ... + +def extended_gcd_i64(a: int, b: int) -> tuple[int, int, int]: + """ +Extended Euclidean algorithm: `(g, x, y)` with `a*x + b*y == g` and +`g == gcd(|a|, |b|) >= 0`. + +Panics: +Panics on `a == i64::MIN` or `b == i64::MIN`, whose negation is not +representable. + +Rust: `discrete::number_theory::extended_gcd_i64` + """ + ... + +def mod_pow_u64(base: int, exp: int, m: int) -> int: + """ +Modular exponentiation `base^exp mod m`. + +Shares the implementation in `discrete::primes::mod_pow_u64`. + +Rust: `discrete::number_theory::mod_pow_u64` + """ + ... + +def mod_inverse_u64(a: int, m: int) -> Optional[int]: + """ +The inverse of `a` modulo `m`, or `None` when `gcd(a, m) != 1`. + +The residue is returned in `[0, m)`; the modulus `0` has no residues +and yields `None`, while modulus `1` yields `0`. + +Rust: `discrete::number_theory::mod_inverse_u64` + """ + ... + +def crt(residues: list[tuple[int, int]]) -> Optional[tuple[int, int]]: + """ +Chinese remainder theorem for general (not necessarily coprime) +moduli. + +Takes `(remainder, modulus)` pairs and returns the unique class +`(r, m)` with `m == lcm` of the moduli and `r` in `[0, m)` satisfying +every congruence. Returns `None` when the system is inconsistent, +when any modulus is zero, or when the combined modulus overflows a +`u64`. An empty system is solved by `(0, 1)`. + +Rust: `discrete::number_theory::crt` + """ + ... + +def euler_phi(n: int) -> int: + """ +Euler's totient: the count of integers in `[1, n]` coprime to `n`. + +`euler_phi(0)` is defined as `0`. + +Rust: `discrete::number_theory::euler_phi` + """ + ... + +def phi_sieve(n: int) -> list[int]: + """ +`euler_phi` for every index up to `n`, by a sieve. + +Entry `i` of the returned vector is `euler_phi(i)`, so its length is +`n + 1`. + +Rust: `discrete::number_theory::phi_sieve` + """ + ... + +def mobius(n: int) -> int: + """ +The Moebius function: `0` when `n` is not squarefree, otherwise +`(-1)^k` for `k` distinct prime factors. + +`mobius(0)` is defined as `0` and `mobius(1) == 1`. + +Rust: `discrete::number_theory::mobius` + """ + ... + +def mobius_sieve(n: int) -> list[int]: + """ +`mobius` for every index up to `n`, by a linear sieve. + +Entry `i` of the returned vector is `mobius(i)`, so its length is +`n + 1`. + +Rust: `discrete::number_theory::mobius_sieve` + """ + ... + +def divisors(n: int) -> list[int]: + """ +Every divisor of `n`, ascending. Empty for `n == 0`. + +Rust: `discrete::number_theory::divisors` + """ + ... + +def divisor_count(n: int) -> int: + """ +The number of divisors, `sigma_0(n)`. Zero for `n == 0`. + +Rust: `discrete::number_theory::divisor_count` + """ + ... + +def divisor_sum(n: int) -> int: + """ +The sum of divisors, `sigma_1(n)`. Zero for `n == 0`. + +Panics: +Panics if the sum does not fit in a `u64`. + +Rust: `discrete::number_theory::divisor_sum` + """ + ... + +def sigma_k(n: int, k: int) -> int: + """ +The divisor power sum `sigma_k(n) = sum_{d | n} d^k`. + +`k == 0` counts divisors. Zero for `n == 0`. + +Panics: +Panics if the sum does not fit in a `u64`. + +Rust: `discrete::number_theory::sigma_k` + """ + ... + +def is_perfect(n: int) -> bool: + """ +Whether `n` equals the sum of its proper divisors. + +Panics: +Panics if the divisor sum does not fit in a `u64`. + +Rust: `discrete::number_theory::is_perfect` + """ + ... + +def is_abundant(n: int) -> bool: + """ +Whether the proper divisors of `n` sum to more than `n`. + +Panics: +Panics if the divisor sum does not fit in a `u64`. + +Rust: `discrete::number_theory::is_abundant` + """ + ... + +def is_deficient(n: int) -> bool: + """ +Whether the proper divisors of `n` sum to less than `n`. + +Panics: +Panics if the divisor sum does not fit in a `u64`. + +Rust: `discrete::number_theory::is_deficient` + """ + ... + +def amicable_pairs(limit: int) -> list[tuple[int, int]]: + """ +All amicable pairs `(a, b)` with `a < b <= limit`. + +A pair is amicable when each number is the sum of the other's proper +divisors. Aliquot sums are built by one `O(limit log limit)` sieve. + +Rust: `discrete::number_theory::amicable_pairs` + """ + ... + +def multiplicative_order(a: int, n: int) -> Optional[int]: + """ +The least `k > 0` with `a^k == 1 (mod n)`, or `None` when `a` and `n` +are not coprime. + +The trivial group modulo `1` gives `Some(1)`. + +Rust: `discrete::number_theory::multiplicative_order` + """ + ... + +def primitive_root(p: int) -> Optional[int]: + """ +The least primitive root modulo the prime `p`, or `None` when `p` is +not prime. + +A primitive root generates the whole multiplicative group, so its +order is `p - 1`. + +Rust: `discrete::number_theory::primitive_root` + """ + ... + +def all_primitive_roots(p: int) -> list[int]: + """ +Every primitive root modulo the prime `p`, ascending. + +There are `euler_phi(p - 1)` of them; the list is empty when `p` is +not prime. + +Rust: `discrete::number_theory::all_primitive_roots` + """ + ... + +def discrete_log_bsgs(base: int, target: int, modulus: int) -> Optional[int]: + """ +Discrete logarithm by baby-step giant-step: the least `x >= 0` with +`base^x == target (mod modulus)`, or `None` when none exists. + +The modulus is arbitrary — a leading reduction strips the common +factors of `base` and `modulus` before the classical coprime search, +so `base` need not be invertible. Time and memory are both +`O(sqrt(modulus))`. + +Rust: `discrete::number_theory::discrete_log_bsgs` + """ + ... + +def discrete_log_pohlig_hellman(base: int, target: int, p: int, factorization: list[tuple[int, int]]) -> Optional[int]: + """ +Discrete logarithm modulo a prime by the Pohlig-Hellman reduction. + +`factorization` is the factorization of the order of `base` — for a +primitive root, that of `p - 1`, as produced by +`discrete::primes::factorize`. The logarithm is recovered in +each prime-power subgroup and glued by the CRT, which costs +`O(sum e_i (log n + sqrt(q_i)))` instead of `O(sqrt(p))`. + +Returns `None` when `p` is not an odd prime, when the factorization +does not describe the order of `base`, or when no logarithm exists. + +Rust: `discrete::number_theory::discrete_log_pohlig_hellman` + """ + ... + +def legendre_symbol(a: int, p: int) -> int: + """ +The Legendre symbol `(a/p)`: `0` when `p` divides `a`, `1` when `a` +is a nonzero quadratic residue, `-1` otherwise. + +Panics: +Panics unless `p` is an odd prime. + +Rust: `discrete::number_theory::legendre_symbol` + """ + ... + +def jacobi_symbol(a: int, n: int) -> int: + """ +The Jacobi symbol `(a/n)` for odd `n > 0`, by reciprocity. + +Equal to the Legendre symbol when `n` is prime. A value of `1` for +composite `n` does not imply that `a` is a residue. + +Panics: +Panics if `n` is even or zero. + +Rust: `discrete::number_theory::jacobi_symbol` + """ + ... + +def tonelli_shanks(a: int, p: int) -> Optional[int]: + """ +A square root of `a` modulo the prime `p` by Tonelli-Shanks, or +`None` when `a` is a non-residue. + +The smaller of the two roots is returned, so the result is always in +`[0, p/2]`. + +Panics: +Panics unless `p` is prime. + +Rust: `discrete::number_theory::tonelli_shanks` + """ + ... + +def quadratic_residues(p: int) -> list[int]: + """ +The nonzero quadratic residues modulo the odd prime `p`, ascending. + +There are exactly `(p - 1) / 2` of them. The list is empty when `p` +is not an odd prime. + +Rust: `discrete::number_theory::quadratic_residues` + """ + ... + +def carmichael_lambda(n: int) -> int: + """ +Carmichael's `lambda(n)`: the exponent of the group of units modulo +`n`, that is the least `k` with `a^k == 1 (mod n)` for every `a` +coprime to `n`. + +Always a divisor of `euler_phi(n)`. `lambda(0)` is defined as `0`. + +Rust: `discrete::number_theory::carmichael_lambda` + """ + ... + +def is_carmichael(n: int) -> bool: + """ +Whether `n` is a Carmichael number: composite, yet `a^(n-1) == 1 +(mod n)` for every `a` coprime to `n`. + +Decided by Korselt's criterion — `n` odd, squarefree, and `p - 1` +divides `n - 1` for every prime `p` dividing `n`. + +Rust: `discrete::number_theory::is_carmichael` + """ + ... + +def digit_sum(n: int, base: int) -> int: + """ +The sum of the digits of `n` written in `base`. + +Panics: +Panics if `base < 2`. + +Rust: `discrete::number_theory::digit_sum` + """ + ... + +def digital_root(n: int, base: int) -> int: + """ +The digital root: repeated digit sums until a single digit remains. + +Equal to `1 + (n - 1) mod (base - 1)` for positive `n`, which is the +closed form used here. + +Panics: +Panics if `base < 2`. + +Rust: `discrete::number_theory::digital_root` + """ + ... + +def is_palindrome(n: int, base: int) -> bool: + """ +Whether the digits of `n` in `base` read the same both ways. + +Panics: +Panics if `base < 2`. + +Rust: `discrete::number_theory::is_palindrome` + """ + ... + +def reverse_digits(n: int, base: int) -> int: + """ +`n` with its digits in `base` reversed. + +Panics: +Panics if `base < 2`, or if the reversed value overflows a `u64`. + +Rust: `discrete::number_theory::reverse_digits` + """ + ... + +def happy_number(n: int) -> bool: + """ +Whether iterating the sum of squared decimal digits reaches `1`. + +Cycle detection is by Floyd's algorithm; `0` is not happy. + +Rust: `discrete::number_theory::happy_number` + """ + ... + +def collatz_trajectory(n: int) -> list[int]: + """ +The Collatz trajectory of `n`, from `n` down to the terminal `1`. + +Empty for `n == 0`. + +Panics: +Panics if some `3x + 1` step overflows a `u64`. + +Rust: `discrete::number_theory::collatz_trajectory` + """ + ... + +def collatz_stopping_time(n: int) -> int: + """ +The total stopping time: the number of Collatz steps from `n` to `1`. + +Zero for `n == 0` and `n == 1`. + +Panics: +Panics if some `3x + 1` step overflows a `u64`. + +Rust: `discrete::number_theory::collatz_stopping_time` + """ + ... + +def sum_of_two_squares(n: int) -> Optional[tuple[int, int]]: + """ +A representation `n = a^2 + b^2` with `a <= b`, or `None` when none +exists. + +By Fermat's two-square theorem a representation exists exactly when +every prime `p == 3 (mod 4)` divides `n` to an even power; that test +runs first, so non-representable inputs cost only a factorization. + +Rust: `discrete::number_theory::sum_of_two_squares` + """ + ... + +def sum_of_four_squares(n: int) -> tuple[int, int, int, int]: + """ +A representation `n = a^2 + b^2 + c^2 + d^2` with the parts +ascending. + +Lagrange's four-square theorem guarantees one exists for every `n`. +The search fixes the largest part first, which leaves a small +remainder for the inner two-square search. + +Panics: +Panics if no representation is found, which would contradict +Lagrange's theorem. + +Rust: `discrete::number_theory::sum_of_four_squares` + """ + ... + +def pythagorean_triples_primitive(limit: int) -> list[tuple[int, int, int]]: + """ +Every primitive Pythagorean triple `(a, b, c)` with `a < b < c` and +hypotenuse `c <= limit`, ascending. + +Generated by the Berggren ternary tree rooted at `(3, 4, 5)`: every +primitive triple is reached exactly once, so no gcd filtering or +deduplication is needed. + +Rust: `discrete::number_theory::pythagorean_triples_primitive` + """ + ... + +def gaussian_integer_factor(re: int, im: int) -> list[tuple[int, int]]: + """ +Factor a Gaussian integer into Gaussian primes. + +The product of the returned list reproduces the input exactly: a +leading unit (`-1`, `i` or `-i`) is included whenever one is needed, +and the empty list is returned for the input `1` and for `0`. Rational +primes `p == 3 (mod 4)` stay inert and appear as `(p, 0)`; `2` splits +as powers of `1 + i`; primes `p == 1 (mod 4)` split into the conjugate +pair coming from `p = a^2 + b^2`. + +Panics: +Panics if the norm `re^2 + im^2` does not fit in a `u64`. + +Rust: `discrete::number_theory::gaussian_integer_factor` + """ + ... + +def frobenius_number(coins: list[int]) -> Optional[int]: + """ +The Frobenius number of a coin system: the largest amount that cannot +be paid exactly. + +`None` when the coins share a common factor (infinitely many amounts +are then unreachable) or when the list holds no positive coin. A coin +of value `1` makes every non-negative amount payable and reports `0`. +Two coprime coins use the closed form `ab - a - b`; more coins use a +Dijkstra search over the residues of the smallest coin, so memory is +`O(min(coins))`. + +Rust: `discrete::number_theory::frobenius_number` + """ + ... + +def egyptian_fractions_greedy(r: Fraction) -> list[int]: + """ +The greedy (Fibonacci-Sylvester) Egyptian-fraction expansion of a +positive rational: denominators `d` with `sum 1/d == r`. + +Each step subtracts the largest unit fraction not exceeding the +remainder, which strictly reduces the numerator and therefore +terminates. An empty list is returned for `r <= 0`. + +Rust: `discrete::number_theory::egyptian_fractions_greedy` + """ + ... + +def zeckendorf(n: int) -> list[int]: + """ +The Zeckendorf representation of `n`: the unique set of +non-consecutive Fibonacci numbers summing to `n`, ascending. + +Uses the Fibonacci numbers `1, 2, 3, 5, 8, ...`, each at most once. +Empty for `n == 0`. + +Rust: `discrete::number_theory::zeckendorf` + """ + ... + +def lucas_sequence_u(p: int, q: int, n: int, m: int) -> int: + """ +The Lucas sequence `U_n(P, Q) mod m`, where `U_0 = 0`, `U_1 = 1` and +`U_n = P*U_{n-1} - Q*U_{n-2}`. + +`U_n(1, -1)` is the Fibonacci sequence. Evaluated by the recurrence, +so the cost is linear in `n`. Returns `0` for `m <= 1`. + +Rust: `discrete::number_theory::lucas_sequence_u` + """ + ... + +def quadratic_diophantine_solve(a: int, b: int, c: int) -> list[tuple[int, int]]: + """ +Every integer solution of `a*x^2 + b*y^2 == c`, ascending. + +Only the definite case is enumerable: with `a > 0`, `b > 0` and +`c >= 0` the solution set is finite and is returned in full. An +indefinite form (a Pell-type equation) has infinitely many solutions, +so an empty list is returned there instead. + +Rust: `discrete::number_theory::quadratic_diophantine_solve` + """ + ... + +def linear_diophantine(a: int, b: int, c: int) -> Optional[tuple[int, int, int, int]]: + """ +Solve `a*x + b*y == c` over the integers. + +Returns `(x0, y0, dx, dy)`: a particular solution together with the +homogeneous step, so that `(x0 + t*dx, y0 + t*dy)` is a solution for +every integer `t` and every solution has this form. `None` when +`gcd(a, b)` does not divide `c`, when both coefficients are zero, or +when the particular solution overflows an `i64`. + +Rust: `discrete::number_theory::linear_diophantine` + """ + ... + +def stern_brocot_nth(n: int) -> Fraction: + """ +The `n`-th positive rational in breadth-first order on the +Stern-Brocot tree, counting the root `1/1` as `n == 1`. + +The bits of `n` below its leading bit spell the descent: `0` goes +left, `1` goes right, and each node is the mediant of its bounding +ancestors. Every positive rational appears exactly once, already in +lowest terms. + +Panics: +Panics if `n == 0`. + +Rust: `discrete::number_theory::stern_brocot_nth` + """ + ... + +def farey_next(a: Fraction, n: int) -> Fraction: + """ +The next fraction after `a` in the Farey sequence of order `n`. + +The successor `r/s` is the unique fraction with `s <= n` and +`r*q - p*s == 1` for `a = p/q`, found by solving `p*s == -1 (mod q)` +and taking the largest admissible `s`. + +Panics: +Panics if `n == 0`, if `a` does not fit in `i64`, or if the +denominator of `a` exceeds `n`. + +Rust: `discrete::number_theory::farey_next` + """ + ... + +def dirichlet_convolution(f: list[int], g: list[int]) -> list[int]: + """ +The Dirichlet convolution `(f * g)(n) = sum_{d | n} f(d) g(n/d)`. + +Both slices are indexed by the argument, so element `i` holds the +value at `i` and element `0` is unused (it is zero on output). The +result has the length of the shorter input. + +Rust: `discrete::number_theory::dirichlet_convolution` + """ + ... diff --git a/bindings/python/python/numeria/discrete/partitions.pyi b/bindings/python/python/numeria/discrete/partitions.pyi new file mode 100644 index 0000000..1842dbb --- /dev/null +++ b/bindings/python/python/numeria/discrete/partitions.pyi @@ -0,0 +1,180 @@ +""" +Integer partitions, Young diagrams, and the RSK correspondence. A partition of `n` is a weakly decreasing list of positive integers summing to `n`. It is stored as `Vec` in that order, so `p[0]` is the largest part. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def partition_count(n: int) -> int: + """ +The number of partitions of `n`, by Euler's pentagonal number theorem. + +The theorem gives `p(n) = sum_k (-1)^(k+1) [p(n - g_k) + p(n - g'_k)]` over +the generalised pentagonal numbers `g_k = k(3k-1)/2`. There are only +`O(sqrt n)` of those below `n`, so each value costs `O(sqrt n)` additions +and the whole table costs `O(n^1.5)` -- far less than the `O(n^2)` of the +naive "partitions of n into parts at most m" table. + +Rust: `discrete::partitions::partition_count` + """ + ... + +def partition_count_table(n: int) -> list[int]: + """ +`p(0)` through `p(n)`. + +Rust: `discrete::partitions::partition_count_table` + """ + ... + +def partitions_iter(n: int) -> list[list[int]]: + """ +The partitions of `n`, each weakly decreasing, in reverse lexicographic +order (starting at `[n]` and ending at all ones). + +Rust: `discrete::partitions::partitions_iter` + """ + ... + +def partitions_into_k(n: int, k: int) -> int: + """ +The number of partitions of `n` into exactly `k` positive parts. + +Recurrence `P(n, k) = P(n-1, k-1) + P(n-k, k)`: either the smallest part is +a one, which removes it, or every part is at least two, which subtracts one +from each. + +Rust: `discrete::partitions::partitions_into_k` + """ + ... + +def partition_count_into_at_most_k(n: int, k: int) -> int: + """ +The number of partitions of `n` into at most `k` parts. + +By conjugation this also counts the partitions of `n` whose largest part is +at most `k`. + +Rust: `discrete::partitions::partition_count_into_at_most_k` + """ + ... + +def partitions_distinct(n: int) -> int: + """ +The number of partitions of `n` into distinct parts. + +Product `prod_{i=1..n} (1 + x^i)` accumulated as a coefficient table. + +Rust: `discrete::partitions::partitions_distinct` + """ + ... + +def partitions_odd(n: int) -> int: + """ +The number of partitions of `n` into odd parts. + +Euler's theorem says this equals `partitions_distinct`; the two are +computed independently here so that agreement is evidence rather than a +tautology. + +Rust: `discrete::partitions::partitions_odd` + """ + ... + +def partition_conjugate(p: list[int]) -> list[int]: + """ +The conjugate partition: the column lengths of the Young diagram. + +`conjugate(p)[j]` counts the parts of `p` exceeding `j`. Conjugation is an +involution and preserves the sum. + +Rust: `discrete::partitions::partition_conjugate` + """ + ... + +def young_diagram(p: list[int]) -> list[list[bool]]: + """ +The Young diagram of `p` in English notation: row `i` has `p[i]` true +cells, padded with false to the width of the first row. + +Rust: `discrete::partitions::young_diagram` + """ + ... + +def hook_lengths(p: list[int]) -> list[list[int]]: + """ +The hook length of every cell of the Young diagram, in the same ragged +shape as `p`. + +The hook of a cell is the cell itself, the cells to its right in the row +(the arm), and the cells below it in the column (the leg). + +Rust: `discrete::partitions::hook_lengths` + """ + ... + +def standard_tableaux_count(p: list[int]) -> int: + """ +The number of standard Young tableaux of shape `p`, by the hook length +formula `n! / prod(hooks)`. + +Panics: +Panics if `p` is not weakly decreasing, since the hook lengths would then +be meaningless. + +Rust: `discrete::partitions::standard_tableaux_count` + """ + ... + +def rsk_correspondence(perm: list[int]) -> tuple[list[list[int]], list[list[int]]]: + """ +The Robinson-Schensted correspondence: a permutation of `0..n` maps to a +pair of standard Young tableaux of the same shape. + +`P` is built by row insertion (each value bumps the leftmost strictly +larger entry down a row) and `Q` records which cell was created at each +step, so `Q` is standard by construction. The map is a bijection between +`S_n` and such pairs, which is the combinatorial content of the identity +`sum_shapes f(shape)^2 = n!`. + +Entries of `P` are the permutation's own values; entries of `Q` are the +step indices `0..n`. + +Rust: `discrete::partitions::rsk_correspondence` + """ + ... + +def durfee_square(p: list[int]) -> int: + """ +The side of the Durfee square: the largest `s` with `p[s-1] >= s`, that is, +the largest square that fits in the top-left of the Young diagram. + +Rust: `discrete::partitions::durfee_square` + """ + ... + +def hardy_ramanujan_estimate(n: int) -> float: + """ +The Hardy-Ramanujan asymptotic for the partition count, +`exp(pi sqrt(2n/3)) / (4 n sqrt 3)`. + +The relative error decays like `1/sqrt(n)`, so this is an order-of-magnitude +estimate rather than a value to round. + +Rust: `discrete::partitions::hardy_ramanujan_estimate` + """ + ... + +def goldbach_conjecture_verify(up_to: int) -> bool: + """ +True when every even number from 4 to `up_to` is a sum of two primes. + +Verification, not proof: the conjecture is open. Returns `true` vacuously +for `up_to < 4`. + +Rust: `discrete::partitions::goldbach_conjecture_verify` + """ + ... diff --git a/bindings/python/python/numeria/discrete/primes.pyi b/bindings/python/python/numeria/discrete/primes.pyi new file mode 100644 index 0000000..6289515 --- /dev/null +++ b/bindings/python/python/numeria/discrete/primes.pyi @@ -0,0 +1,281 @@ +""" +Primes: sieves, primality testing, factorization, and prime counting. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.monte_carlo import Rng + +def sieve_eratosthenes(n: int) -> list[int]: + """ +All primes up to and including `n`, by the sieve of Eratosthenes. + +Rust: `discrete::primes::sieve_eratosthenes` + """ + ... + +def sieve_segmented(lo: int, hi: int) -> list[int]: + """ +Primes in `[lo, hi)`, sieving only that window. + +The window is marked using the primes up to `sqrt(hi)`, so memory scales +with the window rather than with `hi`. + +Rust: `discrete::primes::sieve_segmented` + """ + ... + +def sieve_linear(n: int) -> tuple[list[int], list[int]]: + """ +Primes up to `n` together with the smallest prime factor of every +integer up to `n`, by the linear (Gries-Misra) sieve. + +Each composite is struck exactly once, by its smallest prime factor. + +Rust: `discrete::primes::sieve_linear` + """ + ... + +def mod_pow_u64(base: int, exp: int, m: int) -> int: + """ +Modular exponentiation on `u64`. + +Rust: `discrete::primes::mod_pow_u64` + """ + ... + +def is_prime_u64(n: int) -> bool: + """ +Deterministic primality for every `u64`. + +Miller-Rabin over the first twelve prime bases is proven correct for +all 64-bit inputs, so this is a decision procedure rather than a +probabilistic test. + +Rust: `discrete::primes::is_prime_u64` + """ + ... + +def is_prime_bigint(n: int, rounds: int, rng: Rng) -> bool: + """ +Probabilistic primality for a `BigInt`: `rounds` Miller-Rabin bases +followed by a strong Lucas test, which together form BPSW. + +No composite is known to pass BPSW, though none is proven not to; a +composite passing `rounds` independent Miller-Rabin bases alone has +probability at most `4^-rounds`. + +Panics: +Panics if `n` is negative. + +Rust: `discrete::primes::is_prime_bigint` + """ + ... + +def next_prime(n: int) -> int: + """ +The smallest prime strictly greater than `n`. + +Panics: +Panics if the search would overflow `u64`. + +Rust: `discrete::primes::next_prime` + """ + ... + +def prev_prime(n: int) -> Optional[int]: + """ +The largest prime strictly less than `n`, or `None` below 3. + +Rust: `discrete::primes::prev_prime` + """ + ... + +def random_prime(bits: int, rng: Rng) -> int: + """ +A random prime with exactly `bits` bits. + +Panics: +Panics if `bits` is below 2. + +Rust: `discrete::primes::random_prime` + """ + ... + +def pollard_rho(n: int) -> Optional[int]: + """ +A non-trivial factor of a composite `n` by Pollard's rho with +Brent's cycle detection, or `None` if the attempt fails. + +Rust: `discrete::primes::pollard_rho` + """ + ... + +def pollard_rho_bigint(n: int, rng: Rng) -> Optional[int]: + """ +Pollard's rho over `BigInt`, for factors beyond `u64`. + +Rust: `discrete::primes::pollard_rho_bigint` + """ + ... + +def pollard_p_minus_1(n: int, bound: int) -> Optional[int]: + """ +Pollard's p-1 method: finds a factor `p` of `n` when `p - 1` is +`bound`-smooth. Returns `None` when no such factor separates. + +Rust: `discrete::primes::pollard_p_minus_1` + """ + ... + +def trial_division(n: int, limit: int) -> tuple[list[tuple[int, int]], int]: + """ +Trial division up to `limit`: the factors found and the unfactored +remainder. + +Rust: `discrete::primes::trial_division` + """ + ... + +def fermat_factor(n: int) -> Optional[tuple[int, int]]: + """ +Fermat's method: write an odd `n` as a difference of squares. + +Effective only when `n` has two factors close to its square root; +returns `None` once the search passes a generous bound. + +Rust: `discrete::primes::fermat_factor` + """ + ... + +def factorize(n: int) -> list[tuple[int, int]]: + """ +The complete prime factorization of `n`, ascending by prime. + +Small factors go by trial division, the rest by Pollard's rho. + +Rust: `discrete::primes::factorize` + """ + ... + +def factorize_bigint(n: int, rng: Rng) -> list[tuple[int, int]]: + """ +The factorization of a `BigInt` into primes. + +Complete in every case that terminates, which is every case observed. +Unlike `factorize` this cannot promise it: splitting a large composite +has no guaranteed-terminating fallback the way trial division is one below +`2^64`, so a cofactor that survives Pollard rho and Pollard p-1 is returned +as a single entry even though it is known composite. A caller that needs +certainty should test each returned base with `is_prime_bigint`. Rho is +tried three times and each call draws sixteen fresh random polynomials, so +reaching that state means forty-eight independent attempts all failed. + +Panics: +Panics if `n` is not positive. + +Rust: `discrete::primes::factorize_bigint` + """ + ... + +def prime_count_meissel(n: int) -> int: + """ +The exact count of primes up to `n`, without sieving to `n`. + +Uses the Lucy_Hedgehog recurrence over the distinct values of +`n / i`: starting from a count of all integers, each prime up to +`sqrt(n)` sieves its multiples out of every partial count at once. The +state has `O(sqrt n)` entries and the whole computation is +`O(n^(3/4))`, so `pi(10^9)` is reachable without a `10^9`-bit sieve. + +Rust: `discrete::primes::prime_count_meissel` + """ + ... + +def prime_count_li_approx(x: float) -> float: + """ +The logarithmic integral estimate of `pi(x)`, by series. + +Rust: `discrete::primes::prime_count_li_approx` + """ + ... + +def riemann_r(x: float) -> float: + """ +Riemann's refinement `R(x) = sum_{k>=1} mu(k)/k * li(x^(1/k))`. + +Rust: `discrete::primes::riemann_r` + """ + ... + +def nth_prime(n: int) -> int: + """ +The `n`th prime, one-based: `nth_prime(1) == 2`. + +Panics: +Panics if `n` is zero. + +Rust: `discrete::primes::nth_prime` + """ + ... + +def prime_gaps(n: int) -> list[int]: + """ +The gaps between consecutive primes up to `n`. + +Rust: `discrete::primes::prime_gaps` + """ + ... + +def twin_primes(n: int) -> list[tuple[int, int]]: + """ +Twin prime pairs `(p, p+2)` with `p + 2 <= n`. + +Rust: `discrete::primes::twin_primes` + """ + ... + +def goldbach_partitions(n: int) -> list[tuple[int, int]]: + """ +Every way to write an even `n` as an ordered sum of two primes with +`p <= q`. + +Rust: `discrete::primes::goldbach_partitions` + """ + ... + +def primes_in_arithmetic_progression(a: int, d: int, count: int) -> list[int]: + """ +The first `count` primes in the arithmetic progression `a, a+d, ...`. + +Panics: +Panics if `d` is zero. + +Rust: `discrete::primes::primes_in_arithmetic_progression` + """ + ... + +def mersenne_lucas_lehmer(p: int) -> bool: + """ +The Lucas-Lehmer test: is the Mersenne number `2^p - 1` prime? + +`p` must itself be prime for the test to be meaningful; composite `p` +gives a composite Mersenne number and the function returns false. + +Rust: `discrete::primes::mersenne_lucas_lehmer` + """ + ... + +def wilson_check(p: int) -> bool: + """ +Wilson's theorem: `p` is prime exactly when `(p-1)! = -1 (mod p)`. + +Correct but exponentially slower than `is_prime_u64`; included for +the identity rather than for use. + +Rust: `discrete::primes::wilson_check` + """ + ... diff --git a/bindings/python/python/numeria/discrete/sequences.pyi b/bindings/python/python/numeria/discrete/sequences.pyi new file mode 100644 index 0000000..7185a83 --- /dev/null +++ b/bindings/python/python/numeria/discrete/sequences.pyi @@ -0,0 +1,296 @@ +""" +Integer sequences, linear recurrences, and generating functions. Two halves. The first recovers a sequence from an analytic or algebraic description: Taylor coefficients from a function by Cauchy's integral, and the minimal linear recurrence from a prefix by Berlekamp-Massey. The second is the named sequences themselves. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def ogf_coefficients(f: Callable[[complex], complex], n: int, radius: float) -> list[float]: + """ +The first `n` Taylor coefficients of `f` about the origin, by Cauchy's +integral evaluated on a circle of the given radius. + +`a_k = (1 / 2 pi i) * contour integral of f(z) / z^(k+1)`. Sampling the +circle at `N` equally spaced points turns that into a discrete Fourier +transform, so all `N` coefficients come out of one FFT rather than `n` +separate quadratures. + +The radius is the accuracy knob and the caller owns it: it must be inside +the disc of convergence, and the error in `a_k` scales like +`(radius / R)^N` for the true radius of convergence `R`. A radius near `R` +resolves high-order coefficients but amplifies the low-order ones by +`radius^-k`; a small radius does the reverse. + +Returns the real parts, so this is for series with real coefficients. + +Panics: +Panics if `n` is zero or `radius` is not positive. + +Rust: `discrete::sequences::ogf_coefficients` + """ + ... + +def egf_to_ogf(coeffs: list[float]) -> list[float]: + """ +Converts exponential generating function coefficients to ordinary ones by +multiplying term `k` by `k!`. + +The factorial overflows `f64` past `k = 170`, so the tail beyond that is +infinite rather than silently wrong. + +Rust: `discrete::sequences::egf_to_ogf` + """ + ... + +def linear_recurrence(init: list[int], coeffs: list[int], n: int) -> int: + """ +The `n`-th term of the linear recurrence +`a_k = coeffs[0] a_{k-1} + coeffs[1] a_{k-2} + ...`, with `init` giving +`a_0 .. a_{order-1}`. + +Panics: +Panics unless `init` and `coeffs` have the same non-zero length. + +Rust: `discrete::sequences::linear_recurrence` + """ + ... + +def linear_recurrence_mod(init: list[int], coeffs: list[int], n: int, m: int) -> int: + """ +The `n`-th term of the same recurrence, modulo `m`, by matrix +exponentiation. + +Costs `O(order^3 log n)` rather than `O(order * n)`, which is what makes an +index like `10^18` reachable. + +Panics: +Panics unless `init` and `coeffs` have the same non-zero length, or if `m` +is zero. + +Rust: `discrete::sequences::linear_recurrence_mod` + """ + ... + +def find_linear_recurrence(seq: list[Fraction]) -> Optional[list[Fraction]]: + """ +The shortest linear recurrence generating `seq`, by Berlekamp-Massey over +the rationals. + +Returns `c` with `a_n = c[0] a_{n-1} + c[1] a_{n-2} + ...`, or `None` when +the sequence is too short to determine one. A recurrence of order `L` is +only pinned down by `2L` terms, so a candidate found from fewer is a guess; +this reports `None` in that case rather than returning it. The empty vector +is returned for the all-zero sequence, whose recurrence has order zero. + +Rust: `discrete::sequences::find_linear_recurrence` + """ + ... + +def berlekamp_massey_gf2(seq: list[bool]) -> list[bool]: + """ +The connection polynomial of the shortest linear feedback shift register +generating `seq` over GF(2), returned as taps `t` with +`a_n = t[0] a_{n-1} XOR t[1] a_{n-2} XOR ...`. + +Same algorithm as `find_linear_recurrence` with the field replaced by +GF(2), where every non-zero discrepancy is one and subtraction is XOR, so +there is no division to do. + +Rust: `discrete::sequences::berlekamp_massey_gf2` + """ + ... + +def fibonacci_mod(n: int, m: int) -> int: + """ +`F(n) mod m`, by fast doubling. + +The identities `F(2k) = F(k) (2 F(k+1) - F(k))` and +`F(2k+1) = F(k)^2 + F(k+1)^2` halve the index each step, so this is +`O(log n)` multiplications rather than `O(n)` additions. + +Panics: +Panics if `m` is zero. + +Rust: `discrete::sequences::fibonacci_mod` + """ + ... + +def pisano_period(m: int) -> int: + """ +The Pisano period: the period of the Fibonacci sequence modulo `m`. + +Found by advancing until the pair `(0, 1)` recurs, which is the state that +starts the sequence, so the first recurrence is the full period. + +Panics: +Panics if `m` is zero. + +Rust: `discrete::sequences::pisano_period` + """ + ... + +def lucas(n: int) -> int: + """ +The `n`-th Lucas number: `L(0) = 2`, `L(1) = 1`, `L(n) = L(n-1) + L(n-2)`. + +Rust: `discrete::sequences::lucas` + """ + ... + +def pell_number(n: int) -> int: + """ +The `n`-th Pell number: `P(0) = 0`, `P(1) = 1`, `P(n) = 2 P(n-1) + P(n-2)`. + +Rust: `discrete::sequences::pell_number` + """ + ... + +def jacobsthal(n: int) -> int: + """ +The `n`-th Jacobsthal number: `J(0) = 0`, `J(1) = 1`, +`J(n) = J(n-1) + 2 J(n-2)`. + +Rust: `discrete::sequences::jacobsthal` + """ + ... + +def tribonacci(n: int) -> int: + """ +The `n`-th tribonacci number: `0, 0, 1, 1, 2, 4, 7, 13, ...`. + +Rust: `discrete::sequences::tribonacci` + """ + ... + +def look_and_say(seed: str, iterations: int) -> str: + """ +The look-and-say sequence: each step reads the previous term aloud. + +`"1"` becomes `"11"` (one 1), which becomes `"21"` (two 1s), and so on. + +Panics: +Panics if `seed` is empty or contains a non-digit. + +Rust: `discrete::sequences::look_and_say` + """ + ... + +def conway_constant_estimate(iters: int) -> float: + """ +Conway's constant, estimated from the growth of look-and-say lengths. + +The true value 1.303577... is the unique real root above one of Conway's +degree-71 polynomial. Lengths grow at that rate asymptotically, but the +single-step ratio does not settle onto it smoothly: it is still swinging +between 1.3137 and 1.3510 at twenty iterations, so reading off one ratio +would be worse at twenty steps than at sixteen. The swing has period four, +so this takes the geometric mean across a four-step window instead, which +cancels most of it and reaches four digits by thirty iterations. + +Fewer than four iterations are run as four, since the window needs them. + +Rust: `discrete::sequences::conway_constant_estimate` + """ + ... + +def thue_morse(n: int) -> bool: + """ +The `n`-th Thue-Morse bit: the parity of the number of ones in `n`. + +Rust: `discrete::sequences::thue_morse` + """ + ... + +def thue_morse_sequence(n: int) -> list[bool]: + """ +The first `n` bits of the Thue-Morse sequence. + +Rust: `discrete::sequences::thue_morse_sequence` + """ + ... + +def kolakoski(n: int) -> list[int]: + """ +The first `n` terms of the Kolakoski sequence over `{1, 2}`. + +The sequence is its own run-length encoding: it starts `1, 2, 2, 1, 1, 2`, +whose run lengths are `1, 2, 2, 1, 1, 2` again. Generated by reading the +sequence back as it is written -- term `k` says how long run `k` is. + +Rust: `discrete::sequences::kolakoski` + """ + ... + +def recaman(n: int) -> list[int]: + """ +The first `n` terms of Recaman's sequence. + +`a(0) = 0`; each step subtracts the index if the result is positive and +has not appeared before, and otherwise adds it. + +Rust: `discrete::sequences::recaman` + """ + ... + +def ulam_sequence(a: int, b: int, n: int) -> list[int]: + """ +The first `n` terms of the Ulam sequence starting `a, b`. + +After the seeds, each term is the smallest integer larger than the last +that is the sum of two distinct earlier terms in exactly one way. + +Panics: +Panics unless `0 < a < b`. + +Rust: `discrete::sequences::ulam_sequence` + """ + ... + +def aliquot_sequence(n: int, max_steps: int) -> list[int]: + """ +The aliquot sequence from `n`: repeatedly replace a number by the sum of +its proper divisors. + +Stops early at zero, which is terminal, and at a repeat, which means the +sequence has entered a cycle (a perfect number, an amicable pair, or a +longer sociable chain). The returned vector includes `n` itself and the +repeated value, so a cycle is visible in the output. + +Rust: `discrete::sequences::aliquot_sequence` + """ + ... + +def ackermann_small(m: int, n: int) -> Optional[int]: + """ +The Ackermann function, for arguments whose value is representable. + +`A(m, n)` is computed by the closed forms rather than the recursion, which +would not terminate in practice: `A(0,n) = n+1`, `A(1,n) = n+2`, +`A(2,n) = 2n+3`, `A(3,n) = 2^(n+3) - 3`, and `A(4,n)` is a tower of twos. +Returns `None` when the value cannot be built -- `A(4, 2)` already has +19729 digits and `A(5, 0) = A(4, 1)` is the largest value below it that +this returns. + +Rust: `discrete::sequences::ackermann_small` + """ + ... + +def sequence_identify(terms: list[int]) -> list[str]: + """ +Names of the known sequences whose opening terms match `terms`. + +Every candidate family is generated and compared term by term, so a name is +returned only on an exact match of the whole input. A linear recurrence +found by `find_linear_recurrence` is reported as well, which covers the +families not listed by name. + +The result is a list because short prefixes are genuinely ambiguous: +`1, 1, 2` opens the Fibonacci numbers, the Catalan numbers, and the +partition counts alike. + +Rust: `discrete::sequences::sequence_identify` + """ + ... diff --git a/bindings/python/python/numeria/dsp/__init__.pyi b/bindings/python/python/numeria/dsp/__init__.pyi new file mode 100644 index 0000000..638d087 --- /dev/null +++ b/bindings/python/python/numeria/dsp/__init__.pyi @@ -0,0 +1,84 @@ +""" +Digital signal processing: window functions, FIR/IIR filter design, resampling, and phase utilities. The window generators and first-order RC filters that used to live in `signal_processing` moved here; the old paths re-export them. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from . import fir, iir, phase, resample, windows +from numeria.dsp.iir import Biquad as Biquad +from numeria.dsp.fir import FirState as FirState +from numeria.dsp.iir import IirKind as IirKind +from numeria.dsp.iir import Sos as Sos +from numeria.dsp.iir import Svf as Svf +from numeria.dsp.windows import WindowKind as WindowKind +from numeria.dsp.windows import WindowMetrics as WindowMetrics +from numeria.dsp.iir import a_weighting_filter as a_weighting_filter +from numeria.dsp.iir import bessel as bessel +from numeria.dsp.iir import bilinear_transform as bilinear_transform +from numeria.dsp.windows import blackman_window as blackman_window +from numeria.dsp.iir import butterworth as butterworth +from numeria.dsp.iir import butterworth_order as butterworth_order +from numeria.dsp.iir import c_weighting_filter as c_weighting_filter +from numeria.dsp.iir import chebyshev1 as chebyshev1 +from numeria.dsp.iir import chebyshev2 as chebyshev2 +from numeria.dsp.resample import cic_decimate as cic_decimate +from numeria.dsp.iir import dc_blocker as dc_blocker +from numeria.dsp.resample import decimate as decimate +from numeria.dsp.iir import elliptic as elliptic +from numeria.dsp.iir import filtfilt as filtfilt +from numeria.dsp.fir import filtfilt_fir as filtfilt_fir +from numeria.dsp.fir import fir_apply as fir_apply +from numeria.dsp.fir import fir_apply_fft as fir_apply_fft +from numeria.dsp.fir import fir_bandpass as fir_bandpass +from numeria.dsp.fir import fir_bandstop as fir_bandstop +from numeria.dsp.fir import fir_differentiator as fir_differentiator +from numeria.dsp.fir import fir_freq_response as fir_freq_response +from numeria.dsp.fir import fir_gaussian as fir_gaussian +from numeria.dsp.fir import fir_group_delay as fir_group_delay +from numeria.dsp.fir import fir_highpass as fir_highpass +from numeria.dsp.fir import fir_hilbert as fir_hilbert +from numeria.dsp.fir import fir_kaiser_design as fir_kaiser_design +from numeria.dsp.fir import fir_least_squares as fir_least_squares +from numeria.dsp.fir import fir_lowpass as fir_lowpass +from numeria.dsp.fir import fir_parks_mcclellan as fir_parks_mcclellan +from numeria.dsp.fir import fir_raised_cosine as fir_raised_cosine +from numeria.dsp.fir import fir_root_raised_cosine as fir_root_raised_cosine +from numeria.dsp.fir import fir_savitzky_golay as fir_savitzky_golay +from numeria.dsp.iir import first_order_highpass as first_order_highpass +from numeria.dsp.iir import first_order_lowpass as first_order_lowpass +from numeria.dsp.iir import group_delay as group_delay +from numeria.dsp.phase import group_delay_from_phase as group_delay_from_phase +from numeria.dsp.resample import half_band_filter as half_band_filter +from numeria.dsp.windows import hamming_window as hamming_window +from numeria.dsp.windows import hann_window as hann_window +from numeria.dsp.iir import iir_apply as iir_apply +from numeria.dsp.iir import impulse_response as impulse_response +from numeria.dsp.windows import kaiser_beta_for_attenuation as kaiser_beta_for_attenuation +from numeria.dsp.iir import one_pole_lowpass as one_pole_lowpass +from numeria.dsp.phase import phase_difference as phase_difference +from numeria.dsp.phase import phase_locked_loop as phase_locked_loop +from numeria.dsp.phase import phase_vs_reference as phase_vs_reference +from numeria.dsp.iir import rbj_q_from_bandwidth as rbj_q_from_bandwidth +from numeria.dsp.windows import rectangular_window as rectangular_window +from numeria.dsp.resample import resample_cubic as resample_cubic +from numeria.dsp.resample import resample_linear as resample_linear +from numeria.dsp.resample import resample_rational as resample_rational +from numeria.dsp.resample import resample_sinc as resample_sinc +from numeria.dsp.resample import resample_to_rate as resample_to_rate +from numeria.dsp.resample import sinc_interpolate as sinc_interpolate +from numeria.dsp.iir import state_variable_filter as state_variable_filter +from numeria.dsp.iir import step_response as step_response +from numeria.dsp.iir import tf_to_zpk as tf_to_zpk +from numeria.dsp.phase import unwrap_phase as unwrap_phase +from numeria.dsp.phase import unwrap_phase_2d as unwrap_phase_2d +from numeria.dsp.resample import upsample as upsample +from numeria.dsp.windows import window as window +from numeria.dsp.windows import window_metrics as window_metrics +from numeria.dsp.phase import wrap_phase as wrap_phase +from numeria.dsp.phase import zero_crossing_times as zero_crossing_times +from numeria.dsp.iir import zpk_to_sos as zpk_to_sos + + diff --git a/bindings/python/python/numeria/dsp/fir.pyi b/bindings/python/python/numeria/dsp/fir.pyi new file mode 100644 index 0000000..a5a03b9 --- /dev/null +++ b/bindings/python/python/numeria/dsp/fir.pyi @@ -0,0 +1,236 @@ +""" +FIR filter design and application. All frequencies are normalized to the sample rate (cycles/sample), so cutoffs live in (0, 0.5). Designs are linear-phase; windowed-sinc designs follow Oppenheim & Schafer §7.5, the equiripple design is the Parks-McClellan / Remez exchange (type I), and Savitzky-Golay follows the least-squares polynomial derivation. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.manifold.spacetime import Causal +from numeria.dsp.windows import WindowKind + +class FirState: + """ +Streaming FIR state: one-sample-at-a-time processing with an +internal circular delay line. + +Rust: `dsp::fir::FirState` + """ + def __init__(self, h: list[float]) -> None: ... + def process(self, x: float) -> float: ... + def reset(self) -> None: ... + +def fir_lowpass(n_taps: int, cutoff: float, w: WindowKind) -> list[float]: + """ +Windowed-sinc low-pass FIR; unit DC gain. `cutoff` in (0, 0.5). + +Panics: +Panics if `n_taps == 0` or the cutoff is out of range. + +Rust: `dsp::fir::fir_lowpass` + """ + ... + +def fir_highpass(n_taps: int, cutoff: float, w: WindowKind) -> list[float]: + """ +Windowed-sinc high-pass FIR via spectral inversion; unit Nyquist gain. + +Panics: +Panics unless `n_taps` is odd (type I linear phase is required for a +high-pass) and the cutoff is in range. + +Rust: `dsp::fir::fir_highpass` + """ + ... + +def fir_bandpass(n_taps: int, lo: float, hi: float, w: WindowKind) -> list[float]: + """ +Windowed-sinc band-pass FIR (difference of two low-passes); unit gain +at the band center (lo + hi)/2. + +Panics: +Panics unless `0 < lo < hi < 0.5`. + +Rust: `dsp::fir::fir_bandpass` + """ + ... + +def fir_bandstop(n_taps: int, lo: float, hi: float, w: WindowKind) -> list[float]: + """ +Windowed-sinc band-stop FIR; unit DC gain. + +Panics: +Panics unless `n_taps` is odd and `0 < lo < hi < 0.5`. + +Rust: `dsp::fir::fir_bandstop` + """ + ... + +def fir_kaiser_design(pass_: float, stop: float, ripple_db: float, atten_db: float) -> list[float]: + """ +Kaiser-window low-pass design from a passband/stopband spec: +passband edge, stopband edge (normalized), maximum passband ripple +and minimum stopband attenuation in dB. Chooses the tap count and β +by Kaiser's formulas. + +Panics: +Panics unless `0 < pass < stop < 0.5`. + +Rust: `dsp::fir::fir_kaiser_design` + """ + ... + +def fir_parks_mcclellan(n_taps: int, bands: list[tuple[float, float]], desired: list[float], weights: list[float]) -> list[float]: + """ +Equiripple (Parks-McClellan / Remez exchange) linear-phase type I +design. `bands` are disjoint ascending (lo, hi) pairs in [0, 0.5]; +`desired` and `weights` give one amplitude and weight per band. + +Errors: +Returns `SolveError::InvalidArgument` for a malformed spec and +`SolveError::NoConvergence` if the exchange fails to settle. + +Panics: +Panics unless `n_taps` is odd and ≥ 3. + +Rust: `dsp::fir::fir_parks_mcclellan` + """ + ... + +def fir_least_squares(n_taps: int, bands: list[tuple[float, float]], desired: list[float]) -> list[float]: + """ +Least-squares linear-phase type I design over the given bands +(transition regions are "don't care"). + +Panics: +Panics unless `n_taps` is odd and the spec lengths match. + +Rust: `dsp::fir::fir_least_squares` + """ + ... + +def fir_differentiator(n_taps: int) -> list[float]: + """ +Windowed ideal differentiator (antisymmetric, Blackman window). The +output of `fir_apply` approximates dx/dn (per-sample derivative) +delayed by (n_taps−1)/2. + +Panics: +Panics unless `n_taps` is odd. + +Rust: `dsp::fir::fir_differentiator` + """ + ... + +def fir_hilbert(n_taps: int) -> list[float]: + """ +Windowed ideal Hilbert transformer (antisymmetric, Blackman window): +shifts every positive-frequency component by −90°. + +Panics: +Panics unless `n_taps` is odd. + +Rust: `dsp::fir::fir_hilbert` + """ + ... + +def fir_raised_cosine(span: int, sps: int, beta: float) -> list[float]: + """ +Raised-cosine (Nyquist) pulse: `span` symbols long at `sps` samples +per symbol with roll-off `beta` ∈ [0, 1]. Length span·sps + 1, peak 1, +zero ISI at symbol spacing. + +Panics: +Panics if `span` or `sps` is zero, or beta is outside [0, 1]. + +Rust: `dsp::fir::fir_raised_cosine` + """ + ... + +def fir_root_raised_cosine(span: int, sps: int, beta: float) -> list[float]: + """ +Root-raised-cosine pulse (same span/sps/beta conventions as +`fir_raised_cosine`); convolving it with itself gives a raised +cosine. Normalized to unit energy. + +Panics: +Panics if `span` or `sps` is zero, or beta is outside [0, 1]. + +Rust: `dsp::fir::fir_root_raised_cosine` + """ + ... + +def fir_gaussian(n_taps: int, bt: float) -> list[float]: + """ +Gaussian pulse-shaping filter with bandwidth-time product `bt` +(bandwidth normalized to the sample rate). Unit DC gain. + +Panics: +Panics if `n_taps == 0` or `bt <= 0`. + +Rust: `dsp::fir::fir_gaussian` + """ + ... + +def fir_savitzky_golay(window: int, order: int, deriv: int) -> list[float]: + """ +Savitzky-Golay convolution kernel: fits a polynomial of `order` over +a centered odd `window` and evaluates its `deriv`-th derivative (unit +sample spacing). Feeding it to `fir_apply` estimates the derivative +delayed by (window−1)/2 samples. + +Panics: +Panics unless `window` is odd and `deriv <= order < window`. + +Rust: `dsp::fir::fir_savitzky_golay` + """ + ... + +def fir_apply(h: list[float], x: list[float]) -> list[float]: + """ +Causal FIR filtering by direct convolution; output has the same +length as the input (group delay is not compensated). + +Rust: `dsp::fir::fir_apply` + """ + ... + +def fir_apply_fft(h: list[float], x: list[float]) -> list[float]: + """ +Causal FIR filtering via overlap-save FFT blocks; identical output to +`fir_apply` but O(n log n) for long kernels. + +Rust: `dsp::fir::fir_apply_fft` + """ + ... + +def filtfilt_fir(h: list[float], x: list[float]) -> list[float]: + """ +Zero-phase filtering: filter forward, reverse, filter again, reverse. +The effective magnitude response is |H|². + +Rust: `dsp::fir::filtfilt_fir` + """ + ... + +def fir_freq_response(h: list[float], n: int) -> tuple[list[float], list[complex]]: + """ +Frequency response of an FIR at n points spanning [0, 0.5] (normalized +frequency); returns (frequencies, complex response). + +Panics: +Panics if `n < 2`. + +Rust: `dsp::fir::fir_freq_response` + """ + ... + +def fir_group_delay(h: list[float]) -> float: + """ +Group delay in samples: (n−1)/2 for (anti)symmetric linear-phase +kernels, otherwise the energy-weighted center of the impulse response. + +Rust: `dsp::fir::fir_group_delay` + """ + ... diff --git a/bindings/python/python/numeria/dsp/iir.pyi b/bindings/python/python/numeria/dsp/iir.pyi new file mode 100644 index 0000000..8ee474b --- /dev/null +++ b/bindings/python/python/numeria/dsp/iir.pyi @@ -0,0 +1,317 @@ +""" +Infinite impulse response filters: RBJ biquads, second-order-section cascades, and classical designs (Butterworth, Chebyshev I/II, elliptic, Bessel) via analog prototypes, frequency transformation, and the bilinear transform. Frequencies are in Hz against an explicit sample rate. The elliptic prototype follows Orfanidis' lecture notes (the same construction as scipy's `ellipap`); Chebyshev and Butterworth prototypes are the textbook pole formulas. The pre-Part-3 first-order RC filters remain here unchanged. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +class Biquad: + """ +One second-order section in transposed direct form II, with the RBJ +cookbook designs as constructors. Coefficients are normalized +(a0 = 1); `a1`, `a2` are the denominator terms. + +Rust: `dsp::iir::Biquad` + """ + @staticmethod + def from_coeffs(b0: float, b1: float, b2: float, a1: float, a2: float) -> Biquad: ... + @staticmethod + def identity() -> Biquad: ... + @staticmethod + def lowpass(fc: float, fs: float, q: float) -> Biquad: ... + @staticmethod + def highpass(fc: float, fs: float, q: float) -> Biquad: ... + @staticmethod + def bandpass(fc: float, fs: float, q: float) -> Biquad: ... + @staticmethod + def notch(fc: float, fs: float, q: float) -> Biquad: ... + @staticmethod + def allpass(fc: float, fs: float, q: float) -> Biquad: ... + @staticmethod + def peaking(fc: float, fs: float, q: float, gain_db: float) -> Biquad: ... + @staticmethod + def lowshelf(fc: float, fs: float, slope: float, gain_db: float) -> Biquad: ... + @staticmethod + def highshelf(fc: float, fs: float, slope: float, gain_db: float) -> Biquad: ... + def process(self, x: float) -> float: ... + def process_block(self, x: list[float]) -> list[float]: ... + def reset(self) -> None: ... + def prime(self, v: float) -> None: ... + def freq_response(self, f: float, fs: float) -> complex: ... + def coeffs(self) -> tuple[list[float], list[float]]: ... + def is_stable(self) -> bool: ... + @property + def b0(self) -> float: ... + @property + def b1(self) -> float: ... + @property + def b2(self) -> float: ... + @property + def a1(self) -> float: ... + @property + def a2(self) -> float: ... + +class IirKind: + """ +Filter band selectors for the classical designs; frequencies in Hz. + +Rust: `dsp::iir::IirKind` + """ + ... + +class Sos: + """ +A cascade of biquads with an overall gain. + +Rust: `dsp::iir::Sos` + """ + def __init__(self, sections: list[Biquad], gain: float) -> None: ... + def process(self, x: float) -> float: ... + def process_block(self, x: list[float]) -> list[float]: ... + def reset(self) -> None: ... + def freq_response(self, f: float, fs: float) -> complex: ... + def to_tf(self) -> tuple[list[float], list[float]]: ... + def poles(self) -> list[complex]: ... + def zeros(self) -> list[complex]: ... + @property + def sections(self) -> list[Biquad]: ... + @property + def gain(self) -> float: ... + +class Svf: + """ +Chamberlin state-variable filter producing simultaneous low-pass, +high-pass, band-pass, and notch outputs. + +Rust: `dsp::iir::Svf` + """ + def process(self, x: float) -> tuple[float, float, float, float]: ... + def reset(self) -> None: ... + +def bilinear_transform(s_zeros: list[complex], s_poles: list[complex], gain: float, fs: float, prewarp_hz: Optional[float] = None) -> Sos: + """ +Bilinear transform of an analog (z, p, k) description to a digital +`Sos` at sample rate fs. `prewarp` optionally pins one analog +frequency (Hz) to its digital location. + +Rust: `dsp::iir::bilinear_transform` + """ + ... + +def zpk_to_sos(zeros: list[complex], poles: list[complex], gain: float) -> Sos: + """ +Group a digital (z, p, k) set into second-order sections. Complex +values must come in conjugate pairs. + +Rust: `dsp::iir::zpk_to_sos` + """ + ... + +def tf_to_zpk(b: list[float], a: list[float]) -> tuple[list[complex], list[complex], float]: + """ +Digital (zeros, poles, gain) from transfer-function coefficient +arrays in z⁻¹ order (b\\[0\\] + b\\[1\\]z⁻¹ + …), using +`numerical::polynomial_roots`. + +Panics: +Panics if either polynomial is degenerate (all zero). + +Rust: `dsp::iir::tf_to_zpk` + """ + ... + +def butterworth(order: int, kind: IirKind, fs: float) -> Sos: + """ +Butterworth digital filter (maximally flat magnitude). + +Panics: +Panics if `order == 0` or the band edges are invalid for fs. + +Rust: `dsp::iir::butterworth` + """ + ... + +def chebyshev1(order: int, ripple_db: float, kind: IirKind, fs: float) -> Sos: + """ +Chebyshev type I (equiripple passband, `ripple_db` peak-to-peak). + +Panics: +Panics if `order == 0`. + +Rust: `dsp::iir::chebyshev1` + """ + ... + +def chebyshev2(order: int, atten_db: float, kind: IirKind, fs: float) -> Sos: + """ +Chebyshev type II (monotone passband, equiripple stopband at +−`atten_db`). The cutoff marks the stopband edge. + +Panics: +Panics if `order == 0`. + +Rust: `dsp::iir::chebyshev2` + """ + ... + +def elliptic(order: int, ripple_db: float, atten_db: float, kind: IirKind, fs: float) -> Sos: + """ +Elliptic (Cauer) filter: `ripple_db` passband ripple and `atten_db` +stopband attenuation. + +Panics: +Panics if `order == 0`. + +Rust: `dsp::iir::elliptic` + """ + ... + +def bessel(order: int, kind: IirKind, fs: float) -> Sos: + """ +Bessel-Thomson filter (maximally flat group delay), −3 dB at the +cutoff. + +Panics: +Panics if `order == 0`. + +Rust: `dsp::iir::bessel` + """ + ... + +def butterworth_order(pass_: float, stop: float, ripple_db: float, atten_db: float, fs: float) -> int: + """ +Minimum Butterworth order meeting a low-pass spec: passband edge, +stopband edge (Hz), maximum passband ripple and minimum stopband +attenuation (dB). + +Panics: +Panics unless `0 < pass < stop < fs/2`. + +Rust: `dsp::iir::butterworth_order` + """ + ... + +def filtfilt(sos: Sos, x: list[float]) -> list[float]: + """ +Zero-phase filtering: odd-reflection padding, steady-state priming +of every section, forward pass, backward pass (the same edge- +transient suppression goal as Gustafsson's method). + +Rust: `dsp::iir::filtfilt` + """ + ... + +def iir_apply(b: list[float], a: list[float], x: list[float]) -> list[float]: + """ +Direct-form II transposed filtering with arbitrary-order (b, a) +coefficient arrays in z⁻¹ order. + +Panics: +Panics if `a` is empty or `a[0] == 0`. + +Rust: `dsp::iir::iir_apply` + """ + ... + +def impulse_response(sos: Sos, n: int) -> list[float]: + """ +Impulse response of a cascade (n samples). + +Rust: `dsp::iir::impulse_response` + """ + ... + +def step_response(sos: Sos, n: int) -> list[float]: + """ +Step response of a cascade (n samples). + +Rust: `dsp::iir::step_response` + """ + ... + +def group_delay(sos: Sos, n_points: int, fs: float) -> tuple[list[float], list[float]]: + """ +Group delay in samples over `n_points` frequencies spanning +(0, fs/2): τ(ω) = −dφ/dω from the unwrapped phase. + +Rust: `dsp::iir::group_delay` + """ + ... + +def one_pole_lowpass(fc: float, fs: float) -> tuple[float, float]: + """ +One-pole low-pass coefficients (b0, a1) for +y\\[n\\] = b0·x\\[n\\] + a1·y\\[n−1\\], with a1 = e^(−2π·fc/fs). +The pre-Part-3 `first_order_lowpass` is this filter with +α = dt/(RC + dt). + +Rust: `dsp::iir::one_pole_lowpass` + """ + ... + +def dc_blocker(r: float) -> Biquad: + """ +DC-blocking filter: H(z) = (1 − z⁻¹)/(1 − r·z⁻¹), r slightly below 1. + +Rust: `dsp::iir::dc_blocker` + """ + ... + +def state_variable_filter(fc: float, fs: float, q: float) -> Svf: + """ +Build a Chamberlin SVF at cutoff fc with resonance q. + +Rust: `dsp::iir::state_variable_filter` + """ + ... + +def a_weighting_filter(fs: float) -> Sos: + """ +IEC 61672 A-weighting as a digital cascade (bilinear transform of the +standard analog poles), normalized to exactly 0 dB at 1 kHz. + +Rust: `dsp::iir::a_weighting_filter` + """ + ... + +def c_weighting_filter(fs: float) -> Sos: + """ +IEC 61672 C-weighting, normalized to 0 dB at 1 kHz. + +Rust: `dsp::iir::c_weighting_filter` + """ + ... + +def rbj_q_from_bandwidth(bw_octaves: float, fc: float, fs: float) -> float: + """ +RBJ Q for a given bandwidth in octaves at center fc: +1/Q = 2·sinh(ln2/2 · BW · ω/sin ω). + +Rust: `dsp::iir::rbj_q_from_bandwidth` + """ + ... + +def first_order_lowpass(signal: list[float], dt: float, rc: float) -> list[float]: + """ +First-order RC low-pass filter: α = dt / (RC + dt) + +Panics: +Panics if `dt <= 0` or `rc < 0`. + +Rust: `dsp::iir::first_order_lowpass` + """ + ... + +def first_order_highpass(signal: list[float], dt: float, rc: float) -> list[float]: + """ +First-order RC high-pass filter: α = RC / (RC + dt) + +Panics: +Panics if `dt <= 0` or `rc < 0`. + +Rust: `dsp::iir::first_order_highpass` + """ + ... diff --git a/bindings/python/python/numeria/dsp/phase.pyi b/bindings/python/python/numeria/dsp/phase.pyi new file mode 100644 index 0000000..f867171 --- /dev/null +++ b/bindings/python/python/numeria/dsp/phase.pyi @@ -0,0 +1,91 @@ +""" +Phase utilities: unwrapping (1D and Itoh 2D), phase-locked loops, interpolated zero crossings, and phase measurement against a reference tone. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def wrap_phase(p: float) -> float: + """ +Wrap an angle into (−π, π]. + +Rust: `dsp::phase::wrap_phase` + """ + ... + +def unwrap_phase(p: list[float]) -> list[float]: + """ +1D phase unwrapping: remove 2π jumps between consecutive samples. + +Rust: `dsp::phase::unwrap_phase` + """ + ... + +def unwrap_phase_2d(p: list[float], w: int, h: int) -> list[float]: + """ +2D phase unwrapping by Itoh's method: unwrap each row, then unwrap +the columns of the row-unwrapped field. Exact for residue-free +(consistent) phase maps. + +Panics: +Panics unless `p.len() == w * h`. + +Rust: `dsp::phase::unwrap_phase_2d` + """ + ... + +def phase_difference(a: list[float], b: list[float]) -> list[float]: + """ +Wrapped per-sample phase difference a − b. + +Panics: +Panics if the lengths differ. + +Rust: `dsp::phase::phase_difference` + """ + ... + +def group_delay_from_phase(phase: list[float], freqs: list[float]) -> list[float]: + """ +Group delay −dφ/dω from unwrapped phase samples on an angular +frequency grid (central differences; one-sided at the ends). + +Panics: +Panics if the lengths differ or fewer than 2 points. + +Rust: `dsp::phase::group_delay_from_phase` + """ + ... + +def phase_locked_loop(x: list[float], fs: float, f0: float, bandwidth: float) -> tuple[list[float], list[float]]: + """ +Second-order phase-locked loop tracking a real tone near f0: +returns the NCO phase track and the instantaneous frequency estimate +(Hz) per sample. `bandwidth` is the loop bandwidth in Hz. + +Panics: +Panics unless the rates are positive. + +Rust: `dsp::phase::phase_locked_loop` + """ + ... + +def zero_crossing_times(x: list[float], fs: float) -> list[float]: + """ +Linearly interpolated zero-crossing times (seconds), both directions. + +Rust: `dsp::phase::zero_crossing_times` + """ + ... + +def phase_vs_reference(x: list[float], ref_freq: float, fs: float) -> float: + """ +Phase (radians) of the signal's component at `ref_freq` relative to +cos(2π·f·t) starting at the first sample, via single-bin correlation. + +Rust: `dsp::phase::phase_vs_reference` + """ + ... diff --git a/bindings/python/python/numeria/dsp/resample.pyi b/bindings/python/python/numeria/dsp/resample.pyi new file mode 100644 index 0000000..acbaef7 --- /dev/null +++ b/bindings/python/python/numeria/dsp/resample.pyi @@ -0,0 +1,127 @@ +""" +Sample-rate conversion: integer up/down sampling, polyphase rational resampling, windowed-sinc/linear/cubic interpolation, CIC decimation, and half-band filters. Anti-aliasing and interpolation kernels are symmetric windowed-sinc filters applied centered ("same" alignment), so resampled signals keep zero net delay. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def upsample(x: list[float], factor: int) -> list[float]: + """ +Integer upsampling: zero-stuff by `factor`, then interpolate with a +Kaiser-windowed sinc low-pass at the original Nyquist. Output length +is `x.len() * factor`. + +Panics: +Panics if `factor == 0`. + +Rust: `dsp::resample::upsample` + """ + ... + +def decimate(x: list[float], factor: int) -> list[float]: + """ +Integer decimation: anti-alias low-pass at the new Nyquist, then +keep every `factor`-th sample. Output length ⌈n/factor⌉. + +Panics: +Panics if `factor == 0`. + +Rust: `dsp::resample::decimate` + """ + ... + +def resample_rational(x: list[float], up: int, down: int) -> list[float]: + """ +Rational resampling by up/down with a single polyphase Kaiser-sinc +kernel. Output length ⌈n·up/down⌉. + +Panics: +Panics if `up == 0` or `down == 0`. + +Rust: `dsp::resample::resample_rational` + """ + ... + +def resample_to_rate(x: list[float], fs_in: float, fs_out: float) -> list[float]: + """ +Resample from `fs_in` to `fs_out`, approximating the ratio with a +rational up/down (denominator ≤ 1000) and delegating to +`resample_rational`. + +Panics: +Panics unless both rates are positive. + +Rust: `dsp::resample::resample_to_rate` + """ + ... + +def sinc_interpolate(x: list[float], t: float, half_width: int) -> float: + """ +Windowed-sinc (Hann) interpolation of the sample stream at fractional +index t (samples), using `half_width` taps on each side. + +Rust: `dsp::resample::sinc_interpolate` + """ + ... + +def resample_sinc(x: list[float], ratio: float, half_width: int) -> list[float]: + """ +Arbitrary-ratio resampling by windowed-sinc interpolation; output +length ⌈n·ratio⌉. + +Panics: +Panics if `ratio <= 0`. + +Rust: `dsp::resample::resample_sinc` + """ + ... + +def resample_linear(x: list[float], ratio: float) -> list[float]: + """ +Linear-interpolation resampling (cheap, −12 dB/oct images). + +Panics: +Panics if `ratio <= 0`. + +Rust: `dsp::resample::resample_linear` + """ + ... + +def resample_cubic(x: list[float], ratio: float) -> list[float]: + """ +Catmull-Rom cubic resampling. + +Panics: +Panics if `ratio <= 0`. + +Rust: `dsp::resample::resample_cubic` + """ + ... + +def cic_decimate(x: list[float], factor: int, stages: int) -> list[float]: + """ +Cascaded integrator-comb decimation: `stages` integrators, decimate +by `factor`, `stages` combs; output scaled by factor^stages so DC +gain is one. + +Panics: +Panics if `factor == 0` or `stages == 0`. + +Rust: `dsp::resample::cic_decimate` + """ + ... + +def half_band_filter(n_taps: int) -> list[float]: + """ +Half-band FIR: odd length, every second tap zero (except the 0.5 +center), cutoff 0.25 — the workhorse for factor-2 stages. + +Panics: +Panics unless `n_taps` is odd and ≥ 7. + +Rust: `dsp::resample::half_band_filter` + """ + ... diff --git a/bindings/python/python/numeria/dsp/windows.pyi b/bindings/python/python/numeria/dsp/windows.pyi new file mode 100644 index 0000000..503d2f3 --- /dev/null +++ b/bindings/python/python/numeria/dsp/windows.pyi @@ -0,0 +1,99 @@ +""" +Window functions for spectral analysis and FIR design. `window` generates any of the standard windows in symmetric form (filter design; endpoints at k = 0 and k = n−1) or periodic form (spectral analysis; the implied period is n). `window_metrics` measures the figures of merit from Harris (1978), *On the Use of Windows for Harmonic Analysis with the DFT*. The pre-Part-3 generators (`hann_window`, …) are kept and wrap `window` with their original symmetric convention. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +class WindowKind: + """ +Window families for `window`. Parameterized variants carry their +shape parameter: Kaiser β, Tukey taper fraction α ∈ [0, 1], Gaussian +σ (relative to the half-width), Dolph-Chebyshev sidelobe attenuation +in (positive) dB. + +Rust: `dsp::windows::WindowKind` + """ + ... + +class WindowMetrics: + """ +Figures of merit for a window (Harris 1978). + +Rust: `dsp::windows::WindowMetrics` + """ + def __init__(self, coherent_gain: float, enbw: float, scallop_loss_db: float, main_lobe_bins: float, max_sidelobe_db: float) -> None: ... + @property + def coherent_gain(self) -> float: ... + @property + def enbw(self) -> float: ... + @property + def scallop_loss_db(self) -> float: ... + @property + def main_lobe_bins(self) -> float: ... + @property + def max_sidelobe_db(self) -> float: ... + +def window(kind: WindowKind, n: int, periodic: bool) -> list[float]: + """ +Generate a window of length n. `periodic` selects the DFT-even form +(denominator n, for spectral analysis); symmetric windows use +denominator n−1 (for FIR design). + +Rust: `dsp::windows::window` + """ + ... + +def window_metrics(w: list[float]) -> WindowMetrics: + """ +Measure a window's figures of merit by direct evaluation of its DTFT +on a fine frequency grid (64 points per bin). + +Rust: `dsp::windows::window_metrics` + """ + ... + +def kaiser_beta_for_attenuation(db: float) -> float: + """ +Kaiser window β for a target stopband attenuation in dB +(Kaiser's empirical formula). + +Rust: `dsp::windows::kaiser_beta_for_attenuation` + """ + ... + +def hann_window(n: int) -> list[float]: + """ +Generate a Hann window of length n: `w[k] = 0.5·(1 - cos(2πk/(n-1)))` + +Rust: `dsp::windows::hann_window` + """ + ... + +def hamming_window(n: int) -> list[float]: + """ +Generate a Hamming window of length n: `w[k] = 0.54 - 0.46·cos(2πk/(n-1))` + +Rust: `dsp::windows::hamming_window` + """ + ... + +def blackman_window(n: int) -> list[float]: + """ +Generate a Blackman window of length n: +`w[k] = 0.42 - 0.5·cos(2πk/(n-1)) + 0.08·cos(4πk/(n-1))` + +Rust: `dsp::windows::blackman_window` + """ + ... + +def rectangular_window(n: int) -> list[float]: + """ +Generate a rectangular (uniform) window of length n: `w[k] = 1` for all k + +Rust: `dsp::windows::rectangular_window` + """ + ... diff --git a/bindings/python/python/numeria/electromagnetism.pyi b/bindings/python/python/numeria/electromagnetism.pyi new file mode 100644 index 0000000..2313076 --- /dev/null +++ b/bindings/python/python/numeria/electromagnetism.pyi @@ -0,0 +1,490 @@ +""" +Classical electromagnetism, from Coulomb's law to radiating dipoles. Electrostatics (Coulomb force and field, potential, Gauss flux, capacitance), magnetostatics (the force on a moving charge, the field of a wire, solenoid and toroid, dipole moments and torques), induction (Faraday and motional EMF, self and mutual inductance), and circuits from Ohm's law through RC transients to the AC steady state -- complex reactance, RLC impedance, resonance, quality factor and bandwidth, power factor, and transformer ratios. The wave section covers the free-space relations: propagation speed, the Poynting magnitude, energy density, the impedance of free space `Z₀ = μ₀c ≈ 376.73 Ω`, dipole radiation and the Larmor power. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.math import Vec3 + +def coulomb_force(q1: float, q2: float, distance: float) -> float: + """ +Coulomb's law: F = k_e * |q1 * q2| / r^2 + +Rust: `electromagnetism::coulomb_force` + """ + ... + +def coulomb_force_signed(q1: float, q2: float, distance: float) -> float: + """ +Coulomb force (signed, 1D): positive = repulsive, negative = attractive + +Rust: `electromagnetism::coulomb_force_signed` + """ + ... + +def coulomb_force_vec(q1: float, pos1: Vec3 | Sequence[float], q2: float, pos2: Vec3 | Sequence[float]) -> Vec3: + """ +Coulomb force vector from charge at pos1 to charge at pos2. + +Rust: `electromagnetism::coulomb_force_vec` + """ + ... + +def electric_field_point_charge(charge: float, distance: float) -> float: + """ +Electric field due to a point charge: E = k_e * q / r^2 + +Rust: `electromagnetism::electric_field_point_charge` + """ + ... + +def electric_field_vec(charge: float, charge_pos: Vec3 | Sequence[float], field_point: Vec3 | Sequence[float]) -> Vec3: + """ +Electric field vector at a point due to a charge at a given position. + +Rust: `electromagnetism::electric_field_vec` + """ + ... + +def electric_potential(charge: float, distance: float) -> float: + """ +Electric potential due to a point charge: V = k_e * q / r + +Rust: `electromagnetism::electric_potential` + """ + ... + +def electric_potential_energy(q1: float, q2: float, distance: float) -> float: + """ +Electric potential energy: U = k_e * q1 * q2 / r + +Rust: `electromagnetism::electric_potential_energy` + """ + ... + +def electric_flux_gauss(enclosed_charge: float) -> float: + """ +Electric flux through a surface (Gauss's law): Φ = q_enclosed / ε_0 + +Rust: `electromagnetism::electric_flux_gauss` + """ + ... + +def capacitance_parallel_plate(area: float, separation: float) -> float: + """ +Capacitance of a parallel plate capacitor: C = ε_0 * A / d + +Rust: `electromagnetism::capacitance_parallel_plate` + """ + ... + +def capacitor_energy(capacitance: float, voltage: float) -> float: + """ +Energy stored in a capacitor: U = 0.5 * C * V^2 + +Rust: `electromagnetism::capacitor_energy` + """ + ... + +def ohms_law_voltage(current: float, resistance: float) -> float: + """ +Ohm's law: V = I * R + +Rust: `electromagnetism::ohms_law_voltage` + """ + ... + +def ohms_law_current(voltage: float, resistance: float) -> float: + """ +Ohm's law: I = V / R + +Rust: `electromagnetism::ohms_law_current` + """ + ... + +def ohms_law_resistance(voltage: float, current: float) -> float: + """ +Ohm's law: R = V / I + +Rust: `electromagnetism::ohms_law_resistance` + """ + ... + +def electrical_power(voltage: float, current: float) -> float: + """ +Electrical power: P = V * I + +Rust: `electromagnetism::electrical_power` + """ + ... + +def electrical_power_from_current(current: float, resistance: float) -> float: + """ +Electrical power: P = I^2 * R + +Rust: `electromagnetism::electrical_power_from_current` + """ + ... + +def resistors_series(resistances: list[float]) -> float: + """ +Resistors in series: R_total = R1 + R2 + ... + +Rust: `electromagnetism::resistors_series` + """ + ... + +def resistors_parallel(resistances: list[float]) -> float: + """ +Resistors in parallel: 1/R_total = 1/R1 + 1/R2 + ... + +Rust: `electromagnetism::resistors_parallel` + """ + ... + +def capacitors_series(capacitances: list[float]) -> float: + """ +Capacitors in series: 1/C_total = 1/C1 + 1/C2 + ... + +Rust: `electromagnetism::capacitors_series` + """ + ... + +def capacitors_parallel(capacitances: list[float]) -> float: + """ +Capacitors in parallel: C_total = C1 + C2 + ... + +Rust: `electromagnetism::capacitors_parallel` + """ + ... + +def rc_time_constant(resistance: float, capacitance: float) -> float: + """ +RC time constant: τ = R * C + +Rust: `electromagnetism::rc_time_constant` + """ + ... + +def rc_charging_voltage(v0: float, resistance: float, capacitance: float, time: float) -> float: + """ +Voltage across charging capacitor: V(t) = V0 * (1 - e^(-t/RC)) + +Rust: `electromagnetism::rc_charging_voltage` + """ + ... + +def magnetic_force_on_charge(charge: float, velocity: float, b_field: float, angle_rad: float) -> float: + """ +Magnetic force on a moving charge: F = q * v * B * sin(θ) + +Rust: `electromagnetism::magnetic_force_on_charge` + """ + ... + +def lorentz_force(charge: float, e_field: Vec3 | Sequence[float], velocity: Vec3 | Sequence[float], b_field: Vec3 | Sequence[float]) -> Vec3: + """ +Lorentz force: F = q * (E + v × B) + +Rust: `electromagnetism::lorentz_force` + """ + ... + +def magnetic_field_wire(current: float, distance: float) -> float: + """ +Magnetic field from a long straight wire: B = μ_0 * I / (2π * r) + +Rust: `electromagnetism::magnetic_field_wire` + """ + ... + +def force_between_wires(i1: float, i2: float, distance: float) -> float: + """ +Magnetic force between two parallel wires per unit length: F/L = μ_0 * I1 * I2 / (2π * d) + +Rust: `electromagnetism::force_between_wires` + """ + ... + +def cyclotron_radius(mass: float, velocity: float, charge: float, b_field: float) -> float: + """ +Cyclotron radius: r = m*v / (|q|*B) + +Rust: `electromagnetism::cyclotron_radius` + """ + ... + +def cyclotron_frequency(charge: float, b_field: float, mass: float) -> float: + """ +Cyclotron frequency: f = |q|*B / (2π*m) + +Rust: `electromagnetism::cyclotron_frequency` + """ + ... + +def faraday_emf(num_turns: float, delta_flux: float, delta_time: float) -> float: + """ +Faraday's law (magnitude): EMF = -N * dΦ/dt + +Rust: `electromagnetism::faraday_emf` + """ + ... + +def motional_emf(b_field: float, length: float, velocity: float) -> float: + """ +Motional EMF: ε = B * L * v + +Rust: `electromagnetism::motional_emf` + """ + ... + +def inductor_energy(inductance: float, current: float) -> float: + """ +Inductance energy: U = 0.5 * L * I^2 + +Rust: `electromagnetism::inductor_energy` + """ + ... + +def wavelength_from_frequency(frequency: float) -> float: + """ +Relationship between wavelength and frequency: c = λ * f + +Rust: `electromagnetism::wavelength_from_frequency` + """ + ... + +def frequency_from_wavelength(wavelength: float) -> float: + """ +Frequency from wavelength: f = c / λ + +Rust: `electromagnetism::frequency_from_wavelength` + """ + ... + +def poynting_magnitude(e_field: float, b_field: float) -> float: + """ +Poynting vector magnitude (EM wave intensity): S = E * B / μ_0 + +Rust: `electromagnetism::poynting_magnitude` + """ + ... + +def solenoid_field(mu0: float, turns_per_length: float, current: float) -> float: + """ +Solenoid magnetic field: B = μ₀nI + +Rust: `electromagnetism::solenoid_field` + """ + ... + +def toroid_field(mu0: float, total_turns: float, current: float, radius: float) -> float: + """ +Toroid magnetic field: B = μ₀NI/(2πr) + +Rust: `electromagnetism::toroid_field` + """ + ... + +def magnetic_flux(b_field: float, area: float, angle: float) -> float: + """ +Magnetic flux: Φ = BA cos(θ) + +Rust: `electromagnetism::magnetic_flux` + """ + ... + +def magnetic_energy_density(b_field: float) -> float: + """ +Magnetic energy density: u = B²/(2μ₀) + +Rust: `electromagnetism::magnetic_energy_density` + """ + ... + +def mutual_inductance_coaxial(mu0: float, n1: float, n2: float, area: float, length: float) -> float: + """ +Mutual inductance of coaxial solenoids: M = μ₀n₁n₂AL + +Rust: `electromagnetism::mutual_inductance_coaxial` + """ + ... + +def self_inductance_solenoid(mu0: float, turns: float, area: float, length: float) -> float: + """ +Self-inductance of a solenoid: L = μ₀N²A/l + +Rust: `electromagnetism::self_inductance_solenoid` + """ + ... + +def magnetic_dipole_moment(current: float, area: float) -> float: + """ +Magnetic dipole moment: m = IA + +Rust: `electromagnetism::magnetic_dipole_moment` + """ + ... + +def torque_on_dipole(moment: float, b_field: float, angle: float) -> float: + """ +Torque on a magnetic dipole: τ = mB sin(θ) + +Rust: `electromagnetism::torque_on_dipole` + """ + ... + +def capacitive_reactance(frequency: float, capacitance: float) -> float: + """ +Capacitive reactance: Xc = 1/(2πfC) + +Rust: `electromagnetism::capacitive_reactance` + """ + ... + +def inductive_reactance(frequency: float, inductance: float) -> float: + """ +Inductive reactance: XL = 2πfL + +Rust: `electromagnetism::inductive_reactance` + """ + ... + +def impedance_rlc_series(resistance: float, inductive_reactance: float, capacitive_reactance: float) -> float: + """ +Impedance of a series RLC circuit: Z = √(R² + (XL - XC)²) + +Rust: `electromagnetism::impedance_rlc_series` + """ + ... + +def resonant_frequency_lc(inductance: float, capacitance: float) -> float: + """ +Resonant frequency of an LC circuit: f₀ = 1/(2π√(LC)) + +Rust: `electromagnetism::resonant_frequency_lc` + """ + ... + +def power_factor(resistance: float, impedance: float) -> float: + """ +Power factor: cos(φ) = R/Z + +Rust: `electromagnetism::power_factor` + """ + ... + +def rms_voltage(peak: float) -> float: + """ +RMS voltage: V_rms = V_peak/√2 + +Rust: `electromagnetism::rms_voltage` + """ + ... + +def rms_current(peak: float) -> float: + """ +RMS current: I_rms = I_peak/√2 + +Rust: `electromagnetism::rms_current` + """ + ... + +def ac_power_average(vrms: float, irms: float, power_factor: float) -> float: + """ +Average AC power: P = V_rms × I_rms × cos(φ) + +Rust: `electromagnetism::ac_power_average` + """ + ... + +def quality_factor_rlc(inductance: float, capacitance: float, resistance: float) -> float: + """ +Quality factor of an RLC circuit: Q = (1/R)√(L/C) + +Rust: `electromagnetism::quality_factor_rlc` + """ + ... + +def bandwidth_rlc(resonant_freq: float, quality: float) -> float: + """ +Bandwidth of an RLC circuit: BW = f₀/Q + +Rust: `electromagnetism::bandwidth_rlc` + """ + ... + +def em_wave_speed(permittivity: float, permeability: float) -> float: + """ +EM wave speed in a medium: v = 1/√(εμ) + +Rust: `electromagnetism::em_wave_speed` + """ + ... + +def refractive_index_from_em(permittivity_rel: float, permeability_rel: float) -> float: + """ +Refractive index from relative permittivity and permeability: n = √(ε_r × μ_r) + +Rust: `electromagnetism::refractive_index_from_em` + """ + ... + +def characteristic_impedance(permeability: float, permittivity: float) -> float: + """ +Characteristic impedance of a medium: η = √(μ/ε) + +Rust: `electromagnetism::characteristic_impedance` + """ + ... + +def free_space_impedance() -> float: + """ +Free-space impedance: η₀ = √(μ₀/ε₀) ≈ 377 Ω + +Rust: `electromagnetism::free_space_impedance` + """ + ... + +def energy_density_em(e_field: float, b_field: float) -> float: + """ +Total EM energy density: u = ε₀E²/2 + B²/(2μ₀) + +Rust: `electromagnetism::energy_density_em` + """ + ... + +def radiation_intensity_dipole(power: float, angle: float) -> float: + """ +Radiation intensity of a Hertzian dipole: I(θ) = (3P/(8π)) × sin²(θ) + +Rust: `electromagnetism::radiation_intensity_dipole` + """ + ... + +def larmor_power(charge: float, acceleration: float) -> float: + """ +Larmor radiated power: P = q²a²/(6πε₀c³) + +Rust: `electromagnetism::larmor_power` + """ + ... + +def transformer_voltage(v_primary: float, n_primary: float, n_secondary: float) -> float: + """ +Transformer secondary voltage: V₂ = V₁ × N₂/N₁ + +Rust: `electromagnetism::transformer_voltage` + """ + ... + +def transformer_current(i_primary: float, n_primary: float, n_secondary: float) -> float: + """ +Transformer secondary current: I₂ = I₁ × N₁/N₂ + +Rust: `electromagnetism::transformer_current` + """ + ... diff --git a/bindings/python/python/numeria/electronics.pyi b/bindings/python/python/numeria/electronics.pyi new file mode 100644 index 0000000..4489332 --- /dev/null +++ b/bindings/python/python/numeria/electronics.pyi @@ -0,0 +1,148 @@ +""" +Semiconductor device physics. Carrier statistics -- the intrinsic concentration, Fermi-Dirac occupancy, the thermal voltage `kT/q` -- and transport by drift and diffusion, linked by the Einstein relation `D/μ = kT/q`. Then the devices: the PN junction's built-in potential and depletion width, the Shockley diode equation, MOSFET drain current in the linear and saturation regimes, and solar cells through open-circuit voltage, fill factor and efficiency. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def intrinsic_carrier_concentration(nc: float, nv: float, band_gap: float, temperature: float) -> float: + """ +Intrinsic carrier concentration: ni = sqrt(Nc * Nv) * exp(-Eg / (2kT)) + +Rust: `electronics::intrinsic_carrier_concentration` + """ + ... + +def fermi_dirac(energy: float, fermi_level: float, temperature: float) -> float: + """ +Fermi-Dirac distribution: f(E) = 1 / (1 + exp((E - Ef) / (kT))) + +Rust: `electronics::fermi_dirac` + """ + ... + +def thermal_voltage(temperature: float) -> float: + """ +Thermal voltage: Vt = kT / q + +Rust: `electronics::thermal_voltage` + """ + ... + +def conductivity(carrier_density: float, mobility: float, charge: float) -> float: + """ +Electrical conductivity: sigma = n * q * mu + +Rust: `electronics::conductivity` + """ + ... + +def resistivity(conductivity: float) -> float: + """ +Resistivity: rho = 1 / sigma + +Rust: `electronics::resistivity` + """ + ... + +def drift_velocity(mobility: float, electric_field: float) -> float: + """ +Drift velocity: vd = mu * E + +Rust: `electronics::drift_velocity` + """ + ... + +def diffusion_coefficient_einstein(mobility: float, temperature: float) -> float: + """ +Einstein relation for diffusion coefficient: D = mu * kT / q + +Rust: `electronics::diffusion_coefficient_einstein` + """ + ... + +def built_in_potential(na: float, nd: float, ni: float, temperature: float) -> float: + """ +Built-in potential of a p-n junction: Vbi = (kT/q) * ln(Na * Nd / ni^2) + +Rust: `electronics::built_in_potential` + """ + ... + +def depletion_width(epsilon: float, vbi: float, na: float, nd: float, charge: float) -> float: + """ +Depletion region width: W = sqrt(2 * epsilon * Vbi * (1/Na + 1/Nd) / q) + +Rust: `electronics::depletion_width` + """ + ... + +def diode_current(is_: float, voltage: float, temperature: float, n: float) -> float: + """ +Shockley diode equation: I = Is * (exp(V / (n * Vt)) - 1) + +Rust: `electronics::diode_current` + """ + ... + +def diode_reverse_saturation(area: float, ni: float, dn: float, dp: float, ln: float, lp: float, charge: float) -> float: + """ +Reverse saturation current (symmetric approximation): +Is = q * A * ni^2 * (Dn/Ln + Dp/Lp) + +Rust: `electronics::diode_reverse_saturation` + """ + ... + +def mosfet_drain_current_linear(mu: float, cox: float, w: float, l: float, vgs: float, vth: float, vds: float) -> float: + """ +MOSFET drain current in the linear region: +Id = mu * Cox * (W/L) * ((Vgs - Vth) * Vds - Vds^2 / 2) + +Rust: `electronics::mosfet_drain_current_linear` + """ + ... + +def mosfet_drain_current_saturation(mu: float, cox: float, w: float, l: float, vgs: float, vth: float) -> float: + """ +MOSFET drain current in the saturation region: +Id = (mu * Cox / 2) * (W/L) * (Vgs - Vth)^2 + +Rust: `electronics::mosfet_drain_current_saturation` + """ + ... + +def solar_cell_current(photocurrent: float, dark_current: float, voltage: float, temperature: float) -> float: + """ +Solar cell current: I = Iph - I0 * (exp(V / Vt) - 1) + +Rust: `electronics::solar_cell_current` + """ + ... + +def open_circuit_voltage(photocurrent: float, dark_current: float, temperature: float) -> float: + """ +Open-circuit voltage: Voc = Vt * ln(Iph / I0 + 1) + +Rust: `electronics::open_circuit_voltage` + """ + ... + +def fill_factor(voc: float, isc: float, pmax: float) -> float: + """ +Fill factor: FF = Pmax / (Voc * Isc) + +Rust: `electronics::fill_factor` + """ + ... + +def solar_cell_efficiency(pmax: float, incident_power: float) -> float: + """ +Solar cell efficiency: eta = Pmax / Pin + +Rust: `electronics::solar_cell_efficiency` + """ + ... diff --git a/bindings/python/python/numeria/exact/__init__.pyi b/bindings/python/python/numeria/exact/__init__.pyi new file mode 100644 index 0000000..d41a4da --- /dev/null +++ b/bindings/python/python/numeria/exact/__init__.pyi @@ -0,0 +1,14 @@ +""" +Exact arithmetic: arbitrary-precision integers, exact rationals, arbitrary-precision binary floating point, polynomials, and continued fractions. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from . import bigfloat, bigint, contfrac, polynomial, rational, symbolic +from numeria.exact.bigfloat import BigFloat as BigFloat +from numeria.exact.symbolic import Expr as Expr + + diff --git a/bindings/python/python/numeria/exact/bigfloat.pyi b/bindings/python/python/numeria/exact/bigfloat.pyi new file mode 100644 index 0000000..f60b853 --- /dev/null +++ b/bindings/python/python/numeria/exact/bigfloat.pyi @@ -0,0 +1,135 @@ +""" +Arbitrary-precision binary floating point. A `BigFloat` is the exact dyadic rational `mantissa * 2^exponent` together with a working `precision` measured in bits. # Canonical form Every value produced by this module is normalized: either the mantissa is zero (and the exponent is zero), or the mantissa's magnitude has *exactly* `precision` significant bits. Normalization is applied on construction and after every operation, so `precision` is the true working precision rather than an upper bound, and the leading bit of the mantissa is always set. # Rounding All rounding is **round-to-nearest, ties-to-even** — the IEEE-754 default — applied exactly once per operation. `add`, `sub`, `mul`, `div` and `sqrt` form the exact result (or an exact result plus a sticky low bit that cannot change the rounding decision) and round it once, so they are correctly rounded: the returned value is the closest `precision`-bit dyadic to the true mathematical result. As a consequence they reproduce IEEE-754 `f64` arithmetic bit for bit when used at `precision = 53` on operands in the normal range. The transcendental functions (`BigFloat::exp`, `BigFloat::ln`, `BigFloat::sin`, `BigFloat::cos`, `BigFloat::atan`, `BigFloat::pow`) and the constants (`BigFloat::pi`, `BigFloat::e`, `BigFloat::ln2`) evaluate their series at 64 guard bits above the target precision and round once at the end. They are not *proved* correctly rounded (that would need the table-maker's dilemma resolved), but the guard digits put the error far below one ulp of the requested precision. Formulas: Gauss-Legendre AGM iteration for π (Brent 1976, Salamin 1976), Machin's `π/4 = 4·atan(1/5) − atan(1/239)` as an independent cross-check, `ln 2 = 2·atanh(1/3)`, exponential and circular Taylor series after range reduction, and `ln x = 2^s · 2·atanh((m−1)/(m+1))` after repeated square roots. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +class BigFloat: + """ +An arbitrary-precision binary float: the exact value +`mantissa * 2^exponent`, carried at `precision` bits. + +See the module documentation for the canonical form and the +rounding rules. Comparison, `PartialEq` and `Ord` are by *numeric +value*: two `BigFloat`s that represent the same number compare equal +even when their `precision` fields differ. + +Rust: `exact::bigfloat::BigFloat` + """ + def __init__(self, mantissa: int, exponent: int, precision: int) -> None: ... + @staticmethod + def zero(precision: int) -> BigFloat: ... + @staticmethod + def one(precision: int) -> BigFloat: ... + @staticmethod + def from_i64(n: int, precision: int) -> BigFloat: ... + @staticmethod + def from_bigint(n: int, precision: int) -> BigFloat: ... + @staticmethod + def from_f64(x: float, precision: int) -> BigFloat: ... + @staticmethod + def from_str(s: str, precision: int) -> BigFloat: ... + def is_zero(self) -> bool: ... + def is_negative(self) -> bool: ... + def is_positive(self) -> bool: ... + def signum(self) -> int: ... + def neg(self) -> BigFloat: ... + def abs(self) -> BigFloat: ... + def mul_pow2(self, k: int) -> BigFloat: ... + def round_to(self, precision: int) -> BigFloat: ... + def add_prec(self, other: BigFloat, precision: int) -> BigFloat: ... + def sub_prec(self, other: BigFloat, precision: int) -> BigFloat: ... + def mul_prec(self, other: BigFloat, precision: int) -> BigFloat: ... + def div_prec(self, other: BigFloat, precision: int) -> BigFloat: ... + def sqrt_prec(self, precision: int) -> BigFloat: ... + def add(self, other: BigFloat) -> BigFloat: ... + def sub(self, other: BigFloat) -> BigFloat: ... + def mul(self, other: BigFloat) -> BigFloat: ... + def div(self, other: BigFloat) -> BigFloat: ... + def sqrt(self) -> BigFloat: ... + def to_f64(self) -> float: ... + def to_string_decimal(self, digits: int) -> str: ... + @staticmethod + def pi(precision: int) -> BigFloat: ... + @staticmethod + def e(precision: int) -> BigFloat: ... + @staticmethod + def ln2(precision: int) -> BigFloat: ... + def exp(self) -> BigFloat: ... + def ln(self) -> BigFloat: ... + def sin_cos(self) -> tuple[BigFloat, BigFloat]: ... + def sin(self) -> BigFloat: ... + def cos(self) -> BigFloat: ... + def atan(self) -> BigFloat: ... + def powi(self, n: int) -> BigFloat: ... + def pow(self, exponent: BigFloat) -> BigFloat: ... + @staticmethod + def agm(a: BigFloat, b: BigFloat) -> BigFloat: ... + @property + def mantissa(self) -> int: ... + @property + def exponent(self) -> int: ... + @property + def precision(self) -> int: ... + +def pi_digits(n_decimal: int) -> str: + """ +The decimal expansion of π truncated to `n_decimal` places, e.g. +`"3.14159"` for `n_decimal == 5`. + +Rust: `exact::bigfloat::pi_digits` + """ + ... + +def e_digits(n: int) -> str: + """ +The decimal expansion of `e` truncated to `n` places. + +Rust: `exact::bigfloat::e_digits` + """ + ... + +def sqrt2_digits(n: int) -> str: + """ +The decimal expansion of `√2` truncated to `n` places. + +Rust: `exact::bigfloat::sqrt2_digits` + """ + ... + +def machin_pi(precision: int) -> BigFloat: + """ +π to `precision` bits from Machin's formula, +`π = 16·atan(1/5) − 4·atan(1/239)`. + +This is deliberately a different algorithm from `BigFloat::pi` (a +linearly convergent arctangent series against a quadratically +convergent AGM iteration) so that the two can cross-check each other. + +Panics: +Panics if `precision < 2`. + +Rust: `exact::bigfloat::machin_pi` + """ + ... + +def compensated_to_bigfloat_check(xs: list[float]) -> float: + """ +The exact error of `core::compensated::sum_neumaier` on `xs`. + +Every `f64` is a dyadic rational, so the true sum `Σ xᵢ` is computed +exactly in `BigFloat` at a precision wide enough to hold every bit of +every operand. The return value is `sum_neumaier(xs) − Σ xᵢ`, +evaluated exactly and then rounded once to `f64`; it is exactly `0.0` +whenever the compensated sum is perfect. + +Panics: +Panics if any element is infinite or NaN. + +Rust: `exact::bigfloat::compensated_to_bigfloat_check` + """ + ... diff --git a/bindings/python/python/numeria/exact/bigint.pyi b/bindings/python/python/numeria/exact/bigint.pyi new file mode 100644 index 0000000..37331dc --- /dev/null +++ b/bindings/python/python/numeria/exact/bigint.pyi @@ -0,0 +1,345 @@ +""" +Arbitrary-precision signed integers. Magnitudes are little-endian vectors of `u64` limbs in base 2^64, held in a canonical form: no trailing zero limbs, and the limb vector is empty exactly when the value is zero. Every operation restores that form, so equality is structural and `is_zero` is a length check. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.monte_carlo import Rng + +def zero() -> int: + """ + +Rust: `exact::bigint::BigInt::zero` + """ + ... + +def one() -> int: + """ + +Rust: `exact::bigint::BigInt::one` + """ + ... + +def from_u64(n: int) -> int: + """ + +Rust: `exact::bigint::BigInt::from_u64` + """ + ... + +def from_i64(n: int) -> int: + """ + +Rust: `exact::bigint::BigInt::from_i64` + """ + ... + +def from_str_radix(s: str, radix: int) -> int: + """ +Parse in `radix` (2..=36), accepting a leading `+` or `-` and +either case of letter digit. + +Errors: +Returns `GeomError::InvalidArgument` for an unsupported radix, an +empty digit string, or an out-of-range character. + +Rust: `exact::bigint::BigInt::from_str_radix` + """ + ... + +def to_string_radix(n: int, radix: int) -> str: + """ +Render in `radix` (2..=36) using lower-case letter digits. + +Panics: +Panics if `radix` is outside 2..=36. + +Rust: `exact::bigint::BigInt::to_string_radix` + """ + ... + +def to_f64(n: int) -> float: + """ +Nearest `f64`, saturating to infinity beyond the exponent range. + +Rust: `exact::bigint::BigInt::to_f64` + """ + ... + +def to_i64(n: int) -> Optional[int]: + """ +The value as an `i64`, or `None` if it does not fit. + +Rust: `exact::bigint::BigInt::to_i64` + """ + ... + +def bits(n: int) -> int: + """ +Number of bits in the magnitude; zero has zero bits. + +Rust: `exact::bigint::BigInt::bits` + """ + ... + +def is_zero(n: int) -> bool: + """ + +Rust: `exact::bigint::BigInt::is_zero` + """ + ... + +def is_negative(n: int) -> bool: + """ + +Rust: `exact::bigint::BigInt::is_negative` + """ + ... + +def is_even(n: int) -> bool: + """ + +Rust: `exact::bigint::BigInt::is_even` + """ + ... + +def abs(n: int) -> int: + """ + +Rust: `exact::bigint::BigInt::abs` + """ + ... + +def neg(n: int) -> int: + """ + +Rust: `exact::bigint::BigInt::neg` + """ + ... + +def add(n: int, other: int) -> int: + """ + +Rust: `exact::bigint::BigInt::add` + """ + ... + +def sub(n: int, other: int) -> int: + """ + +Rust: `exact::bigint::BigInt::sub` + """ + ... + +def mul(n: int, other: int) -> int: + """ + +Rust: `exact::bigint::BigInt::mul` + """ + ... + +def div_rem(n: int, other: int) -> tuple[int, int]: + """ +Truncated division: the quotient rounds toward zero and the +remainder takes the sign of the dividend, matching Rust's `/` and +`%` on primitive integers. + +Panics: +Panics if `other` is zero. + +Rust: `exact::bigint::BigInt::div_rem` + """ + ... + +def rem_euclid(n: int, m: int) -> int: + """ +Euclidean remainder: always in `0..|m|`. + +Panics: +Panics if `m` is zero. + +Rust: `exact::bigint::BigInt::rem_euclid` + """ + ... + +def pow(n: int, e: int) -> int: + """ +`self` raised to `e` by binary exponentiation. + +Rust: `exact::bigint::BigInt::pow` + """ + ... + +def mod_pow(n: int, e: int, m: int) -> int: + """ +Modular exponentiation by a 4-bit sliding window, reducing after +every multiply. The result is the least non-negative residue. + +Panics: +Panics if `m` is zero or `e` is negative. + +Rust: `exact::bigint::BigInt::mod_pow` + """ + ... + +def gcd(n: int, other: int) -> int: + """ +Greatest common divisor, always non-negative. `gcd(0, 0)` is 0. + +Rust: `exact::bigint::BigInt::gcd` + """ + ... + +def lcm(n: int, other: int) -> int: + """ +Least common multiple, always non-negative. Zero if either side is +zero. + +Rust: `exact::bigint::BigInt::lcm` + """ + ... + +def extended_gcd(n: int, other: int) -> tuple[int, int, int]: + """ +Extended Euclid: returns `(g, x, y)` with `self*x + other*y == g` +and `g == gcd(self, other) >= 0`. + +Rust: `exact::bigint::BigInt::extended_gcd` + """ + ... + +def mod_inverse(n: int, m: int) -> Optional[int]: + """ +Modular inverse, or `None` when `gcd(self, m) != 1`. + +Rust: `exact::bigint::BigInt::mod_inverse` + """ + ... + +def shl(n: int, bits: int) -> int: + """ +Shift left by `bits`, preserving sign. + +Rust: `exact::bigint::BigInt::shl` + """ + ... + +def shr(n: int, bits: int) -> int: + """ +Shift the magnitude right by `bits`, preserving sign. This +truncates toward zero rather than flooring, so it matches +`div_rem` by a power of two rather than an arithmetic shift. + +Rust: `exact::bigint::BigInt::shr` + """ + ... + +def bit(n: int, i: int) -> bool: + """ +Bit `i` of the magnitude, counting from the least significant. + +Rust: `exact::bigint::BigInt::bit` + """ + ... + +def and_(n: int, other: int) -> int: + """ +Bitwise AND of the magnitudes; the result takes `self`'s sign. + +Rust: `exact::bigint::BigInt::and` + """ + ... + +def or_(n: int, other: int) -> int: + """ +Bitwise OR of the magnitudes; the result takes `self`'s sign, or +`other`'s when `self` is zero. + +Rust: `exact::bigint::BigInt::or` + """ + ... + +def xor(n: int, other: int) -> int: + """ +Bitwise XOR of the magnitudes; the result takes `self`'s sign, or +`other`'s when `self` is zero. + +Rust: `exact::bigint::BigInt::xor` + """ + ... + +def sqrt(n: int) -> int: + """ +Integer square root: the largest `r` with `r*r <= self`. + +Panics: +Panics if `self` is negative. + +Rust: `exact::bigint::BigInt::sqrt` + """ + ... + +def nth_root(bigint: int, n: int) -> int: + """ +Integer `n`th root: the largest `r` with `r^n <= self`. + +Panics: +Panics if `n` is zero, or if `self` is negative with even `n`. + +Rust: `exact::bigint::BigInt::nth_root` + """ + ... + +def is_perfect_square(n: int) -> bool: + """ + +Rust: `exact::bigint::BigInt::is_perfect_square` + """ + ... + +def random_bits(bits: int, rng: Rng) -> int: + """ +A uniformly random non-negative integer with exactly `bits` bits of +magnitude (the top bit is set), or zero when `bits` is zero. + +Rust: `exact::bigint::BigInt::random_bits` + """ + ... + +def random_below(bound: int, rng: Rng) -> int: + """ +A uniformly random integer in `0..bound` by rejection sampling. + +Panics: +Panics if `bound` is not positive. + +Rust: `exact::bigint::BigInt::random_below` + """ + ... + +def factorial(n: int) -> int: + """ +`n!`. + +Rust: `exact::bigint::BigInt::factorial` + """ + ... + +def binomial(n: int, k: int) -> int: + """ +The binomial coefficient `n choose k`, zero when `k > n`. + +Rust: `exact::bigint::BigInt::binomial` + """ + ... + +def fibonacci(n: int) -> int: + """ +The `n`th Fibonacci number by fast doubling, with `F(0) = 0`. + +Rust: `exact::bigint::BigInt::fibonacci` + """ + ... diff --git a/bindings/python/python/numeria/exact/contfrac.pyi b/bindings/python/python/numeria/exact/contfrac.pyi new file mode 100644 index 0000000..8260eab --- /dev/null +++ b/bindings/python/python/numeria/exact/contfrac.pyi @@ -0,0 +1,153 @@ +""" +Continued fractions: expansions, convergents, the periodic expansion of a square root, Pell's equation, generalized continued fractions by the modified Lentz algorithm, and the Gauss-map statistics. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def continued_fraction_f64(x: float, max_terms: int) -> list[int]: + """ +The simple continued-fraction expansion of a float, `[a0; a1, a2, ...]`. + +Stops after `max_terms`, or earlier once the remaining fractional part +is too small to yield a meaningful term. Only the leading terms of the +result describe the intended real number: an `f64` carries about 53 +bits, so terms beyond roughly the twentieth describe the rounding of +the input rather than the number itself. + +Panics: +Panics if `x` is not finite. + +Rust: `exact::contfrac::continued_fraction_f64` + """ + ... + +def convergents(cf: list[int]) -> list[Fraction]: + """ +The convergents `h_k / k_k` of a simple continued fraction. + +Uses the standard recurrence `h_k = a_k h_{k-1} + h_{k-2}`, and the +same for the denominators. + +Rust: `exact::contfrac::convergents` + """ + ... + +def periodic_cf_sqrt(n: int) -> tuple[list[int], list[int]]: + """ +The periodic continued fraction of `sqrt(n)`, as `(head, period)` with +`sqrt(n) = [head; period repeated]`. + +For a perfect square the period is empty. Otherwise the expansion is +purely periodic after the first term and the period always ends with +`2*a0`, which is the termination test used here. + +Panics: +Panics if `n` is zero. + +Rust: `exact::contfrac::periodic_cf_sqrt` + """ + ... + +def pell_fundamental_solution(d: int) -> Optional[tuple[int, int]]: + """ +The fundamental solution of Pell's equation `x^2 - d y^2 = 1`. + +Returns `None` when `d` is a perfect square, where the equation has +only the trivial solution. Otherwise the smallest solution with +`y > 0` is a convergent of the continued fraction of `sqrt(d)`. + +Panics: +Panics if `d` is zero. + +Rust: `exact::contfrac::pell_fundamental_solution` + """ + ... + +def generalized_cf_eval(a: Callable[[int], float], b: Callable[[int], float], n: int) -> float: + """ +Evaluate a generalized continued fraction +`b(0) + a(1)/(b(1) + a(2)/(b(2) + ...))` to `n` levels by the modified +Lentz algorithm. + +Lentz builds the value from the top down with multiplicative updates, +so it never forms the deep nested quotient directly and cannot lose the +tail to cancellation. Zero intermediates are nudged to a tiny value, +which is the "modified" part. + +Panics: +Panics if `n` is zero. + +Rust: `exact::contfrac::generalized_cf_eval` + """ + ... + +def cf_e(n: int) -> list[int]: + """ +The first `n` terms of the continued fraction of `e`. + +`e = [2; 1, 2, 1, 1, 4, 1, 1, 6, 1, ...]`: after the leading 2 the +terms run in blocks of `1, 2k, 1`. + +Rust: `exact::contfrac::cf_e` + """ + ... + +def cf_pi_terms(n: int) -> list[int]: + """ +The first `n` terms of the continued fraction of `pi`. + +`pi` has no known pattern, so the terms are read off a high-precision +value computed here in fixed point rather than from an `f64`, which +would only support about twenty correct terms. The working precision +is chosen generously against the number of terms requested. + +Rust: `exact::contfrac::cf_pi_terms` + """ + ... + +def gauss_map_orbit(x: float, n: int) -> list[float]: + """ +The orbit of `x` under the Gauss map `G(x) = frac(1/x)`, `n` steps. + +The Gauss map is the shift on continued-fraction expansions: the +integer parts of the reciprocals along the orbit are exactly the +partial quotients. + +Panics: +Panics if `x` is not finite. + +Rust: `exact::contfrac::gauss_map_orbit` + """ + ... + +def khinchin_estimate(x: float, n: int) -> float: + """ +The geometric mean of the first `n` continued-fraction terms of `x`. + +For almost every irrational this tends to Khinchin's constant, +about 2.685452001. Convergence is very slow, so a short orbit only +lands in the neighbourhood. + +Panics: +Panics if `x` is not finite. + +Rust: `exact::contfrac::khinchin_estimate` + """ + ... + +def levy_constant_estimate(x: float, n: int) -> float: + """ +Estimate Levy's constant from the growth of the convergent +denominators of `x`: `q_n^(1/n)` tends to `exp(pi^2 / (12 ln 2))`, +about 3.275822918. + +Panics: +Panics if `x` is not finite. + +Rust: `exact::contfrac::levy_constant_estimate` + """ + ... diff --git a/bindings/python/python/numeria/exact/polynomial.pyi b/bindings/python/python/numeria/exact/polynomial.pyi new file mode 100644 index 0000000..353609a --- /dev/null +++ b/bindings/python/python/numeria/exact/polynomial.pyi @@ -0,0 +1,180 @@ +""" +Dense univariate polynomials with `f64` coefficients (`Poly`) and with exact rational coefficients (`PolyQ`). Both types store coefficients from lowest to highest degree, so `c[i]` multiplies `x^i`, and both keep that vector trimmed: the last entry of a non-empty coefficient vector is never zero. The zero polynomial is the empty vector, and `Poly::degree` reports `0` for it (use `Poly::is_zero` to tell the zero polynomial from a non-zero constant). `Poly` carries the numerical machinery -- root finding, Sturm sequences, Chebyshev fitting, Pade approximants -- while `PolyQ` carries the exact machinery: subresultant GCDs, content and primitive parts, rational root factoring, and Eisenstein's criterion. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +class Poly: + """ +A polynomial with `f64` coefficients, ordered from the constant term up. + +Rust: `exact::polynomial::Poly` + """ + def __init__(self, c: list[float]) -> None: ... + @staticmethod + def zero() -> Poly: ... + @staticmethod + def constant(a: float) -> Poly: ... + @staticmethod + def monomial(k: int, coeff: float) -> Poly: ... + def is_zero(self) -> bool: ... + def degree(self) -> int: ... + def leading(self) -> float: ... + def eval(self, x: float) -> float: ... + def eval_complex(self, z: complex) -> complex: ... + def add(self, other: Poly | Sequence[float]) -> Poly: ... + def sub(self, other: Poly | Sequence[float]) -> Poly: ... + def neg(self) -> Poly: ... + def mul(self, other: Poly | Sequence[float]) -> Poly: ... + def mul_scalar(self, k: float) -> Poly: ... + def div_rem(self, divisor: Poly | Sequence[float]) -> Optional[tuple[Poly, Poly]]: ... + def derivative(self) -> Poly: ... + def integral(self, c0: float) -> Poly: ... + def compose(self, inner: Poly | Sequence[float]) -> Poly: ... + def scale_arg(self, k: float) -> Poly: ... + def shift_arg(self, h: float) -> Poly: ... + def gcd(self, other: Poly | Sequence[float], tol: float) -> Poly: ... + def roots(self) -> list[complex]: ... + @staticmethod + def from_roots(roots: list[float]) -> Poly: ... + def resultant(self, other: Poly | Sequence[float]) -> float: ... + def discriminant(self) -> float: ... + def sturm_sequence(self) -> list[Poly]: ... + def count_real_roots(self, a: float, b: float) -> int: ... + def root_bound(self) -> float: ... + def isolate_real_roots(self) -> list[tuple[float, float]]: ... + def refine_root(self, interval: tuple[float, float], tol: float) -> float: ... + @staticmethod + def interpolate_lagrange(xs: list[float], ys: list[float]) -> Poly: ... + @staticmethod + def interpolate_newton(xs: list[float], ys: list[float]) -> Poly: ... + @staticmethod + def chebyshev_eval(coeffs: list[float], x: float) -> float: ... + @staticmethod + def chebyshev_eval_on(coeffs: list[float], a: float, b: float, x: float) -> float: ... + @staticmethod + def chebyshev_basis(n: int) -> list[Poly]: ... + def to_chebyshev_basis(self) -> list[float]: ... + @staticmethod + def from_chebyshev_basis(coeffs: list[float]) -> Poly: ... + def pade(self, m: int, n: int) -> Optional[tuple[Poly, Poly]]: ... + @staticmethod + def wilkinson(n: int) -> Poly: ... + @staticmethod + def cyclotomic(n: int) -> PolyQ: ... + def is_squarefree(self, tol: float) -> bool: ... + def squarefree_part(self, tol: float) -> Poly: ... + @property + def c(self) -> list[float]: ... + +class PolyQ: + """ +A polynomial with exact rational coefficients, ordered from the constant +term up. + +Rust: `exact::polynomial::PolyQ` + """ + def __init__(self, c: list[Fraction]) -> None: ... + @staticmethod + def zero() -> PolyQ: ... + @staticmethod + def constant(a: Fraction) -> PolyQ: ... + @staticmethod + def from_i64s(c: list[int]) -> PolyQ: ... + def is_zero(self) -> bool: ... + def degree(self) -> int: ... + def leading(self) -> Fraction: ... + def to_poly(self) -> Poly: ... + def eval(self, x: Fraction) -> Fraction: ... + def add(self, other: PolyQ) -> PolyQ: ... + def sub(self, other: PolyQ) -> PolyQ: ... + def neg(self) -> PolyQ: ... + def mul(self, other: PolyQ) -> PolyQ: ... + def mul_scalar(self, k: Fraction) -> PolyQ: ... + def div_scalar(self, k: Fraction) -> Optional[PolyQ]: ... + def monic(self) -> PolyQ: ... + def div_rem(self, divisor: PolyQ) -> Optional[tuple[PolyQ, PolyQ]]: ... + def derivative(self) -> PolyQ: ... + def integral(self, c0: Fraction) -> PolyQ: ... + def compose(self, inner: PolyQ) -> PolyQ: ... + def scale_arg(self, k: Fraction) -> PolyQ: ... + def shift_arg(self, h: Fraction) -> PolyQ: ... + @staticmethod + def from_roots(roots: list[Fraction]) -> PolyQ: ... + def content(self) -> Fraction: ... + def primitive_part(self) -> PolyQ: ... + def pseudo_div(self, b: PolyQ) -> Optional[tuple[PolyQ, PolyQ]]: ... + def gcd_exact(self, other: PolyQ) -> PolyQ: ... + def is_squarefree(self) -> bool: ... + def squarefree_part(self) -> PolyQ: ... + def resultant(self, other: PolyQ) -> Fraction: ... + def discriminant(self) -> Fraction: ... + def factor_rational_roots(self) -> list[tuple[Fraction, int]]: ... + def eisenstein_check(self, p: int) -> bool: ... + @property + def c(self) -> list[Fraction]: ... + +def polynomial_multiply_fft(a: Poly | Sequence[float], b: Poly | Sequence[float]) -> Poly: + """ +Product of two polynomials through the FFT: transform, multiply +pointwise, transform back. + +Mathematically identical to `Poly::mul`, and asymptotically faster, +at the cost of rounding on the order of `eps * n * max|a| * max|b|`. + +Rust: `exact::polynomial::polynomial_multiply_fft` + """ + ... + +def bernstein_basis(n: int, i: int, t: float) -> float: + """ +The Bernstein basis polynomial `B_{i,n}(t) = C(n, i) t^i (1 - t)^(n - i)`. + +Returns `0.0` when `i > n`. + +Rust: `exact::polynomial::bernstein_basis` + """ + ... + +def to_bernstein(p: Poly | Sequence[float], a: float, b: float) -> list[float]: + """ +Bernstein coefficients of `p` on `[a, b]`. + +The returned `w` of length `deg(p) + 1` satisfies +`p(a + (b - a) t) = sum_i w[i] * bernstein_basis(n, i, t)` for all `t`, +which is the control polygon of `p` viewed as a Bezier curve. + +Panics: +Panics unless `a < b`. + +Rust: `exact::polynomial::to_bernstein` + """ + ... + +def newton_identities(power_sums: list[float]) -> list[float]: + """ +Elementary symmetric functions from power sums, by Newton's identities. + +Given `p_1 .. p_n` in `power_sums`, returns `e_0 .. e_n` (so the result +is one longer, and starts at `e_0 = 1`), using +`k e_k = sum_{i=1}^{k} (-1)^(i-1) e_{k-i} p_i`. + +Rust: `exact::polynomial::newton_identities` + """ + ... + +def vieta(roots: list[complex]) -> list[complex]: + """ +Coefficients of the monic polynomial with the given complex roots, low +degree first (Vieta's formulas). + +Entry `n - k` is `(-1)^k e_k`, the signed `k`-th elementary symmetric +function of the roots; the leading entry is `1`. + +Rust: `exact::polynomial::vieta` + """ + ... diff --git a/bindings/python/python/numeria/exact/rational.pyi b/bindings/python/python/numeria/exact/rational.pyi new file mode 100644 index 0000000..beb01bd --- /dev/null +++ b/bindings/python/python/numeria/exact/rational.pyi @@ -0,0 +1,351 @@ +""" +Exact rational arithmetic over `BigInt`. Every `Rational` is kept in lowest terms with a strictly positive denominator, so equality is structural and there is exactly one representation of each value. Zero is `0/1`. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.linalg.matrix import Matrix + +def farey_sequence(n: int) -> list[Fraction]: + """ +The Farey sequence F_n: every reduced fraction in `[0, 1]` with +denominator at most `n`, in ascending order. + +Panics: +Panics if `n` is zero. + +Rust: `exact::rational::farey_sequence` + """ + ... + +def stern_brocot_path(r: Fraction) -> list[bool]: + """ +The path from the Stern-Brocot root `1/1` down to `r`, as a sequence of +branch choices: `true` for the right (larger) child, `false` for the +left. + +The root itself has an empty path. Only positive rationals have one. + +Panics: +Panics unless `r` is strictly positive. + +Rust: `exact::rational::stern_brocot_path` + """ + ... + +def best_rational_approximations(x: float, max_den: int) -> list[Fraction]: + """ +Every continued-fraction convergent of `x` with denominator at most +`max_den`, in increasing order of denominator. + +The last element is the best rational approximation to `x` under that +bound, in the strong sense that no fraction with a smaller denominator +is closer. + +Panics: +Panics if `max_den` is zero or `x` is not finite. + +Rust: `exact::rational::best_rational_approximations` + """ + ... + +def solve_exact_rational(a: list[list[Fraction]], b: list[Fraction]) -> Optional[list[Fraction]]: + """ +Solve `A x = b` exactly for rational data, by clearing denominators and +running Bareiss fraction-free elimination. + +Returns `None` if the matrix is not square, the shapes disagree, or the +system is singular. + +Rust: `exact::rational::solve_exact_rational` + """ + ... + +def solve_exact(a: Matrix | Sequence[Sequence[float]], b: list[float]) -> Optional[list[Fraction]]: + """ +Solve `A x = b` exactly for an `f64` system. + +Each coefficient is converted to the rational it exactly equals — every +finite `f64` is dyadic — so the result is the exact solution of the +system as stored. Where the `f64` inputs are themselves roundings of +intended values, use `solve_exact_rational` to keep those values +exact instead. + +Returns `None` for a non-square or singular system, mismatched shapes, +or any non-finite entry. + +Rust: `exact::rational::solve_exact` + """ + ... + +def determinant_exact(a: list[list[Fraction]]) -> Fraction: + """ +The exact determinant of a rational matrix, via Bareiss on the +denominator-cleared integer matrix. + +Panics: +Panics if the matrix is not square. + +Rust: `exact::rational::determinant_exact` + """ + ... + +def hilbert_matrix_inverse_exact(n: int) -> list[list[Fraction]]: + """ +The exact inverse of the `n x n` Hilbert matrix `H_ij = 1/(i+j+1)`. + +Uses the closed form +`(-1)^(i+j) (i+j+1) C(n+i, n-j-1) C(n+j, n-i-1) C(i+j, i)^2`, +whose entries are all integers. + +Panics: +Panics if `n` is zero. + +Rust: `exact::rational::hilbert_matrix_inverse_exact` + """ + ... + +def hilbert_matrix_exact(n: int) -> list[list[Fraction]]: + """ +The `n x n` Hilbert matrix as exact rationals. + +Panics: +Panics if `n` is zero. + +Rust: `exact::rational::hilbert_matrix_exact` + """ + ... + +def new(n: int, d: int) -> Optional[Fraction]: + """ +The rational `n/d`, or `None` when `d` is zero. + +Rust: `exact::rational::Rational::new` + """ + ... + +def from_i64(n: int, d: int) -> Fraction: + """ +The rational `n/d` from machine integers. + +Panics: +Panics if `d` is zero. + +Rust: `exact::rational::Rational::from_i64` + """ + ... + +def from_int(n: int) -> Fraction: + """ +The integer `n` as a rational. + +Rust: `exact::rational::Rational::from_int` + """ + ... + +def zero() -> Fraction: + """ + +Rust: `exact::rational::Rational::zero` + """ + ... + +def one() -> Fraction: + """ + +Rust: `exact::rational::Rational::one` + """ + ... + +def from_f64_exact(x: float) -> Optional[Fraction]: + """ +The exact value of an IEEE-754 double, or `None` for NaN and the +infinities. + +Every finite `f64` is a dyadic rational `m * 2^e`, so the reduced +denominator is always a power of two. + +Rust: `exact::rational::Rational::from_f64_exact` + """ + ... + +def from_f64_approx(x: float, max_den: int) -> Fraction: + """ +The best rational approximation to `x` with denominator at most +`max_den`, found by walking the continued fraction (equivalently, +descending the Stern-Brocot tree). + +Panics: +Panics if `max_den` is zero or `x` is not finite. + +Rust: `exact::rational::Rational::from_f64_approx` + """ + ... + +def is_zero(q: Fraction) -> bool: + """ + +Rust: `exact::rational::Rational::is_zero` + """ + ... + +def is_negative(q: Fraction) -> bool: + """ + +Rust: `exact::rational::Rational::is_negative` + """ + ... + +def is_integer(q: Fraction) -> bool: + """ + +Rust: `exact::rational::Rational::is_integer` + """ + ... + +def abs(q: Fraction) -> Fraction: + """ + +Rust: `exact::rational::Rational::abs` + """ + ... + +def neg(q: Fraction) -> Fraction: + """ + +Rust: `exact::rational::Rational::neg` + """ + ... + +def recip(q: Fraction) -> Optional[Fraction]: + """ +The reciprocal, or `None` for zero. + +Rust: `exact::rational::Rational::recip` + """ + ... + +def add(q: Fraction, other: Fraction) -> Fraction: + """ + +Rust: `exact::rational::Rational::add` + """ + ... + +def sub(q: Fraction, other: Fraction) -> Fraction: + """ + +Rust: `exact::rational::Rational::sub` + """ + ... + +def mul(q: Fraction, other: Fraction) -> Fraction: + """ + +Rust: `exact::rational::Rational::mul` + """ + ... + +def div(q: Fraction, other: Fraction) -> Optional[Fraction]: + """ +Quotient, or `None` when `other` is zero. + +Rust: `exact::rational::Rational::div` + """ + ... + +def pow(q: Fraction, e: int) -> Fraction: + """ +`self` raised to a signed integer power. + +Panics: +Panics when raising zero to a negative power. + +Rust: `exact::rational::Rational::pow` + """ + ... + +def to_f64(q: Fraction) -> float: + """ +Nearest `f64`. + +When both parts are individually representable the quotient is a +single correctly-rounded division. Otherwise -- a tiny value like +`1e-300` has a denominator of about `2^1049`, far past the `f64` +range even though the quotient is fine -- the numerator is scaled +by a power of two first so the quotient itself lands in range, and +the scale is undone afterwards. + +Rust: `exact::rational::Rational::to_f64` + """ + ... + +def floor(q: Fraction) -> int: + """ +Greatest integer not exceeding the value. + +Rust: `exact::rational::Rational::floor` + """ + ... + +def ceil(q: Fraction) -> int: + """ +Least integer not below the value. + +Rust: `exact::rational::Rational::ceil` + """ + ... + +def round(q: Fraction) -> int: + """ +Nearest integer, with halves rounded away from zero. + +Rust: `exact::rational::Rational::round` + """ + ... + +def fract(q: Fraction) -> Fraction: + """ +The fractional part `self - floor(self)`, always in `[0, 1)`. + +Rust: `exact::rational::Rational::fract` + """ + ... + +def to_continued_fraction(q: Fraction) -> list[int]: + """ +The continued-fraction expansion `[a0; a1, a2, ...]`. + +The expansion is finite for every rational and, apart from the +integer case, never ends in a 1, which makes it canonical. + +Rust: `exact::rational::Rational::to_continued_fraction` + """ + ... + +def from_continued_fraction(cf: list[int]) -> Fraction: + """ +Rebuild a rational from a continued fraction. + +Errors: +Returns `GeomError::Empty` for an empty expansion, and +`GeomError::InvalidArgument` if a non-leading term is not +positive, which cannot arise from `Self::to_continued_fraction`. + +Rust: `exact::rational::Rational::from_continued_fraction` + """ + ... + +def mediant(a: Fraction, b: Fraction) -> Fraction: + """ +The mediant `(a.num + b.num) / (a.den + b.den)`. + +The mediant of two fractions always lies strictly between them, the +property the Stern-Brocot tree and Farey sequences are built on. + +Rust: `exact::rational::Rational::mediant` + """ + ... diff --git a/bindings/python/python/numeria/exact/symbolic.pyi b/bindings/python/python/numeria/exact/symbolic.pyi new file mode 100644 index 0000000..3080317 --- /dev/null +++ b/bindings/python/python/numeria/exact/symbolic.pyi @@ -0,0 +1,109 @@ +""" +A small computer algebra system over expression trees. Expressions are built from constants, exact rationals, named variables, n-ary sums and products, powers, and the usual elementary functions. The design is numeric-first: everything can be evaluated, differentiated exactly, simplified enough to make cancellation visible, and compiled to a stack machine for repeated evaluation. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.exact.polynomial import Poly +from numeria.monte_carlo import Rng + +class CompiledExpr: + """ +An expression flattened to a stack program. + +Rust: `exact::symbolic::CompiledExpr` + """ + def vars(self) -> list[str]: ... + def len(self) -> int: ... + def is_empty(self) -> bool: ... + def eval(self, vals: list[float]) -> float: ... + +class Expr: + """ +A symbolic expression. + +Rust: `exact::symbolic::Expr` + """ + @staticmethod + def c(v: float) -> Expr: ... + @staticmethod + def var(name: str) -> Expr: ... + @staticmethod + def zero() -> Expr: ... + @staticmethod + def one() -> Expr: ... + @staticmethod + def add(terms: list[Expr]) -> Expr: ... + @staticmethod + def mul(factors: list[Expr]) -> Expr: ... + @staticmethod + def pow(base: Expr, exp: Expr) -> Expr: ... + def as_number(self) -> Optional[float]: ... + def node_count(self) -> int: ... + def depth(self) -> int: ... + def variables(self) -> list[str]: ... + def substitute(self, var: str, replacement: Expr) -> Expr: ... + def eval(self, vars: list[tuple[str, float]]) -> float: ... + def to_latex(self) -> str: ... + @staticmethod + def parse(s: str) -> Expr: ... + def diff(self, var: str) -> Expr: ... + def gradient(self, vars: list[str]) -> list[Expr]: ... + def simplify(self) -> Expr: ... + def expand(self) -> Expr: ... + def as_polynomial(self, var: str) -> Optional[Poly]: ... + def taylor(self, var: str, at: float, order: int) -> Optional[Poly]: ... + def compile(self) -> CompiledExpr: ... + def integrate_simple(self, var: str) -> Optional[Expr]: ... + def limit_numeric(self, var: str, at: float, side: Side) -> Optional[float]: ... + def equivalent_numeric(self, other: Expr, trials: int, rng: Rng) -> bool: ... + +class Side: + """ +Which side a one-sided limit approaches from. + +Rust: `exact::symbolic::Side` + """ + ... + +def hessian(e: Expr, vars: list[str]) -> list[list[Expr]]: + """ +The Hessian matrix of second partial derivatives, simplified. + +Rust: `exact::symbolic::hessian` + """ + ... + +def solve_univariate_numeric(e: Expr, var: str, bracket: tuple[float, float]) -> list[float]: + """ +Real roots of `e` in a bracket, by scanning for sign changes and +bisecting each one. + +Only sign-changing roots are found; a root of even multiplicity, where +the curve touches the axis without crossing, is invisible to this +method. + +Panics: +Panics if the bracket is empty or reversed. + +Rust: `exact::symbolic::solve_univariate_numeric` + """ + ... + +def critical_points(e: Expr, var: str, range: tuple[float, float], n: int) -> list[tuple[float, float]]: + """ +Critical points of `e` in a range: the points where the derivative +changes sign, paired with the value of `e` there. + +`n` is unused beyond selecting the search resolution and is kept for +signature compatibility. + +Panics: +Panics if the range is empty or reversed. + +Rust: `exact::symbolic::critical_points` + """ + ... diff --git a/bindings/python/python/numeria/fem/__init__.pyi b/bindings/python/python/numeria/fem/__init__.pyi new file mode 100644 index 0000000..015f6ee --- /dev/null +++ b/bindings/python/python/numeria/fem/__init__.pyi @@ -0,0 +1,12 @@ +""" +Finite elements, finite-difference time domain, and spectral methods. Three ways of turning a differential equation into a linear system, kept in one place because the interesting content is how they differ. A finite *difference* replaces the derivative with a difference quotient and asks the equation to hold at grid points. A finite *element* never differentiates the solution twice at all: it multiplies by a test function, integrates by parts, and asks the resulting integral identity to hold for every test function in a finite dimensional space. That change of question is what buys the method its two best properties -- it needs one less derivative of the solution to make sense, so a kink in the coefficient is admissible rather than fatal, and the answer it produces is the *best* approximation in the space with respect to the energy the operator defines. A spectral method is the same Galerkin idea with global smooth basis functions instead of local piecewise ones, which trades the sparsity of the matrix for a convergence rate limited only by the smoothness of the solution. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from . import fdtd, fem1d, fem2d, spectral_pde + + diff --git a/bindings/python/python/numeria/fem/fdtd.pyi b/bindings/python/python/numeria/fem/fdtd.pyi new file mode 100644 index 0000000..92ce5a4 --- /dev/null +++ b/bindings/python/python/numeria/fem/fdtd.pyi @@ -0,0 +1,252 @@ +""" +Finite-difference time domain: Maxwell's equations on a Yee grid. # Why the grid is staggered Maxwell's curl equations couple the two fields' time derivatives to each other's spatial derivatives. Yee's arrangement puts `E` and `H` half a cell apart in space *and* half a step apart in time, so that every derivative in the scheme is a centred difference straddling the point it is evaluated at. Nothing is interpolated and nothing is averaged: the update is second-order accurate while using the narrowest possible stencil, and it is explicit, so a step costs one pass over the arrays. The arrangement also makes the discrete divergence of `B` exactly conserved -- the update adds a discrete curl, and the discrete divergence of a discrete curl is identically zero on this grid. A collocated scheme has to enforce that separately or watch it drift. # The Courant limit is not a guideline With `S = c dt / dx`, the scheme's numerical dispersion relation admits a real wavenumber for every real frequency only while `S <= 1` in one dimension, or `S <= 1/sqrt(d)` in `d` dimensions. Past that the scheme has a mode that grows geometrically, and it grows from rounding noise if nothing else. This is not accuracy degrading gently; it is a hard threshold, and `fdtd_courant_check` reports which side of it a set of parameters falls on. # The magic time step At exactly `S = 1` in one dimension the numerical dispersion relation becomes the exact one, and the update degenerates into a shift: a pulse moves one cell per step with its shape unchanged, to machine precision, forever. One dimension is the only place this happens -- in two or three the dispersion error depends on the propagation angle and cannot be cancelled at all angles at once, which is why a two-dimensional simulation is run at a Courant number safely below the limit rather than at it. # Fields are normalised The updates here track `E` and `eta_0 H` rather than `E` and `H`, which removes the free-space impedance from every line of the update and leaves the Courant number as the only coefficient. It also makes the two fields comparable in magnitude, which matters because the conserved energy adds their squares -- in unnormalised units one term would be `1e5` times the other and the sum would be numerical nonsense. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +class Boundary1d: + """ +What to do at the ends of a one-dimensional grid. + +Rust: `fem::fdtd::Boundary1d` + """ + ... + +class Fdtd1d: + """ +The result of a one-dimensional run: the electric field at every +step, and the magnetic field alongside it. + +Rust: `fem::fdtd::Fdtd1d` + """ + def __init__(self, e: list[list[float]], h: list[list[float]]) -> None: ... + def energy(self, eps_r: list[float], n: int) -> Optional[float]: ... + @property + def e(self) -> list[list[float]]: ... + @property + def h(self) -> list[list[float]]: ... + +class Fdtd2d: + """ +The state of a two-dimensional run. + +The final snapshot alone is close to useless for a driven problem -- +it is whatever phase the oscillation happened to land on -- so the +envelope is carried alongside it. That is a deliberate departure from +returning a bare field: what a steady-state calculation is *for* is +the amplitude, and reconstructing it from one snapshot is not +possible. + +Rust: `fem::fdtd::Fdtd2d` + """ + def __init__(self, nx: int, ny: int, ez: list[float], envelope: list[float]) -> None: ... + @property + def nx(self) -> int: ... + @property + def ny(self) -> int: ... + @property + def ez(self) -> list[float]: ... + @property + def envelope(self) -> list[float]: ... + +def fdtd_courant_check(dx: float, dt: float, c: float) -> bool: + """ +Whether a set of parameters satisfies the one-dimensional Courant +condition `c dt <= dx`. + +Equality is admissible and is in fact the best possible choice in one +dimension: see the module note on the magic time step. + +Rust: `fem::fdtd::fdtd_courant_check` + """ + ... + +def fdtd_courant_check_2d(dx: float, dy: float, dt: float, c: float) -> bool: + """ +The two-dimensional Courant condition, `c dt <= 1 / sqrt(1/dx^2 + +1/dy^2)`. + +On a square grid that is `dx / (c sqrt(2))`, and unlike the +one-dimensional case the bound is not a good place to sit: the +dispersion error at the limit vanishes along the diagonals and is +worst along the axes, so no single Courant number is exact for every +direction. + +Rust: `fem::fdtd::fdtd_courant_check_2d` + """ + ... + +def fdtd_1d(eps_r: list[float], source: Callable[[int], float], source_cell: int, courant: float, steps: int, boundary: Boundary1d) -> Fdtd1d: + """ +Marches the one-dimensional Yee scheme. + +`eps_r` gives the relative permittivity of each cell, `courant` is +`c dt / dx` in vacuum, and `source` is added to `E` at `source_cell` +at every step -- a soft source, which a wave passes through rather +than reflecting off, unlike overwriting the cell. + +Errors: + +`SolveError::InvalidArgument` for a grid shorter than three cells, +a non-positive or non-finite permittivity, a source cell outside the +grid, a Courant number outside `(0, 1]`, or a Courant number above +the limit the grid's *fastest* medium sets -- past the limit the +scheme is unconditionally unstable and running it would produce +numbers rather than an answer. + +Rust: `fem::fdtd::fdtd_1d` + """ + ... + +def photonic_crystal_bandgap_1d(eps_a: float, eps_b: float, d_a: float, d_b: float, omega_max: float, samples: int) -> list[tuple[float, float]]: + """ +The photonic band gaps of an infinite `a`/`b` bilayer stack, found +from the Bloch dispersion relation. + +A period of the stack has a transfer matrix, and Bloch's theorem says +the propagating states are those whose transfer matrix has unit +modulus eigenvalues. For a two-layer period that reduces to + + +with `k_i = omega n_i / c`. A frequency for which the right-hand side +exceeds one in magnitude has no real `K`: nothing propagates, and +that is a gap. The prefactor `(n_a/n_b + n_b/n_a)/2` is at least one +with equality only when the two indices agree, which is the whole +reason a gap exists at all -- a homogeneous "stack" has none. + +Frequencies are angular and the speed of light is taken as one, so a +frequency is really `omega L / c` in disguise; scaling every +thickness by a factor scales every gap edge by its reciprocal. + +Returns the gaps below `omega_max` as `(low, high)` pairs, ascending, +with the edges refined by bisection rather than left at the sampling +resolution. + +Errors: + +`SolveError::InvalidArgument` for non-positive permittivities or +thicknesses, a non-positive frequency ceiling, or fewer than two +samples. + +Rust: `fem::fdtd::photonic_crystal_bandgap_1d` + """ + ... + +def fdtd_2d_tm(eps_r: list[float], source_pos: tuple[int, int], source: Callable[[int], float], nx: int, ny: int, steps: int, pml: tuple[int, int], courant: float, reflection: float) -> Fdtd2d: + """ +Marches the two-dimensional transverse-magnetic Yee scheme with a +Berenger split-field perfectly matched layer. + +`eps_r` is row-major over `nx * ny` cells. `source` gives the value +added softly at `source_pos` on each step, exactly as in +`fdtd_1d` -- a continuous sinusoid at `f` cycles per step is +`|s| (TAU * f * s as f64).sin()`, and a pulse is anything with +compact support. Taking the waveform rather than a frequency is what +lets a caller switch the drive off, which is the only way to measure +what a boundary reflects: with a source still running, the field near +it is the source's own and says nothing about the layer. + +Ramp a continuous drive on rather than switching it: a step +broadcasts across the whole band the grid can carry, and none of it +is what was asked for. + +Why the field is split: + +A lossy layer absorbs, but an ordinary lossy layer also *reflects*, +because its impedance differs from the vacuum it adjoins. Berenger's +construction splits `E_z` into the two parts that the two spatial +derivatives feed, and damps each with the loss belonging to its own +axis. The resulting medium is matched at every angle and every +frequency, which no single isotropic conductivity can be: what is +left is only the reflection from grading the profile over a finite +depth, and that is what the `reflection` target controls. + +The layer is backed by a conductor. That is not a flaw -- anything +that reaches the backing has crossed the graded layer twice and comes +back attenuated by the round-trip factor the grading was designed +for. + +`pml` gives the depth on each axis separately, `(x, y)`. A depth of +zero on an axis leaves plain conducting walls there, which is what a +waveguide wants: absorbing its side walls would stop it being a +waveguide, while absorbing its ends stops the switch-on transient +rattling around forever and swamping the field being measured. + +Errors: + +`SolveError::InvalidArgument` for a grid smaller than the layers +need, a permittivity array of the wrong length or with a non-positive +entry, a source outside the grid, a non-finite frequency, a +reflection target outside `(0, 1)`, or a Courant number above the +two-dimensional limit for the fastest medium present. + +Rust: `fem::fdtd::fdtd_2d_tm` + """ + ... + +def waveguide_cutoff_check_fdtd(width: int, length: int, mode: int, omega: float, courant: float, steps: int) -> float: + """ +Infers a parallel-plate waveguide's cutoff frequency from the +evanescent decay it shows when driven below that cutoff. + +The guide is `width` cells between conducting plates, driven in its +`mode`-th transverse pattern at angular frequency `omega` in radians +per unit *time*, with the cell size and the speed of light both one +-- so a step advances the phase by `omega * S`, not by `omega`. +Below cutoff nothing propagates: the field falls off as +`exp(-alpha x)`, and measuring `alpha` down the guide gives the +cutoff back. + +Which cutoff comes back: + +Not the textbook `m pi c / a`. The grid has its own dispersion +relation, + + +and an evanescent `k_x = i alpha` turns the first term on the right +into `-sinh^2(alpha / 2)`. Solving for where `alpha` vanishes gives +the *numerical* cutoff + + +which is what this returns and what the simulation actually has. It +approaches the continuum value as the guide is resolved more finely, +from below -- the grid is always a little slow -- and the difference +is second order in the cell size. Reporting the continuum figure +would be reporting what the answer ought to be rather than what it +is. + +Errors: + +`SolveError::InvalidArgument` for a mode outside `1..width`, a +guide too short to measure a decay in, or a frequency at or above the +numerical cutoff, where there is no decay to measure; +`SolveError::NoConvergence` if the measured profile is not a clean +exponential, which is the honest answer when a mode is close to the +grid's resolution limit: three half-waves across ten cells decays +within a couple of cells, leaving too little of the profile above the +numerical floor to fit a slope to. Widening the guide fixes it. + +Rust: `fem::fdtd::waveguide_cutoff_check_fdtd` + """ + ... + +def waveguide_cutoff_numerical(width: int, mode: int, courant: float) -> float: + """ +The numerical cutoff a parallel-plate guide of this width has on a +grid at this Courant number, `(2/S) arcsin(S sin(k_y/2))`. + +The continuum answer is `m pi / a`; this is what the grid actually +gives, and it is always the smaller of the two. + +Errors: + +`SolveError::InvalidArgument` for a mode outside `1..width` or a +Courant number outside the plane limit. + +Rust: `fem::fdtd::waveguide_cutoff_numerical` + """ + ... diff --git a/bindings/python/python/numeria/fem/fem1d.pyi b/bindings/python/python/numeria/fem/fem1d.pyi new file mode 100644 index 0000000..ddf5725 --- /dev/null +++ b/bindings/python/python/numeria/fem/fem1d.pyi @@ -0,0 +1,145 @@ +""" +One-dimensional finite elements for `-(p u')' + q u = f`. # The weak form The strong form asks for a function whose second derivative satisfies the equation pointwise. Multiplying by a test function `v` that vanishes wherever `u` is prescribed, integrating over the interval and integrating the second-derivative term by parts gives ```text a(u, v) = integral p u' v' + q u v dx = integral f v dx = L(v) ``` for every admissible `v`. Two things happened in that line. The solution now needs only one derivative rather than two, so a discontinuous `p` -- a layered material -- is admissible instead of fatal. And the boundary term `[p u' v]` that integration by parts produced is where flux conditions enter: prescribe nothing and the method silently imposes zero flux, which is why Neumann conditions are called *natural* and Dirichlet conditions, which have to be built into the space, are called *essential*. # Why the answer is the best one available Galerkin's method asks for the identity to hold not for every `v` but for every `v` in a finite dimensional subspace, and looks for `u_h` in that same subspace. Subtracting the two statements gives Galerkin orthogonality, `a(u - u_h, v_h) = 0` for every `v_h` in the space: the error is `a`-orthogonal to everything representable. When `a` is symmetric and positive definite it is an inner product, orthogonality of the error is exactly the characterisation of an orthogonal projection, and so ```text ||u - u_h||_a <= ||u - v_h||_a for every v_h in the space ``` with a constant of one. The finite element solution is not merely a good approximation in the energy norm; it is *the* best one. Nothing in a finite difference scheme corresponds to this. It is checked directly against the nodal interpolant in the property tests. Equivalently, `u_h` minimises the energy `J(v) = a(v,v)/2 - L(v)` over the space -- the Ritz view -- which is why refining a mesh can only lower the computed energy: the coarse space sits inside the fine one. # A variable coefficient is averaged, not sampled Linear elements have a constant derivative on each element, so the quadrature in the stiffness term integrates `p` against a constant and reproduces its element *average* exactly. That has a consequence worth knowing: the discrete bilinear form still agrees with the true one on the element space itself, so `u_h` is the exact `a`-orthogonal projection of the true solution rather than an approximation of one, and the Pythagoras identity ```text ||u - v_h||_a^2 = ||u - u_h||_a^2 + ||u_h - v_h||_a^2 ``` holds to rounding for every `v_h` in the space. It is not an accident of a smooth `p`: a `p` that jumps *within* an element is averaged the same way, which is the sense in which a finite element method handles a discontinuous coefficient gracefully rather than exactly. # Nodal exactness, and its limits For the pure Poisson problem `-u'' = f` with Dirichlet data, the linear element solution is exact *at the nodes*, to machine precision, on any mesh. The Green's function of `-d^2/dx^2` is piecewise linear with its kink at the source point, so for a mesh node it lies in the element space itself; pairing it against the orthogonal error gives `(u - u_h)(x_i) = 0`. This is a property of the operator, not a lucky cancellation, and it fails the moment either ingredient goes: - a variable `p` makes the Green's function piecewise `int dx/p`, which is not piecewise linear, and nodal exactness disappears; - a reaction term `q` does the same; - for quadratic elements the piecewise linear Green's function of a *vertex* is still in the space, so vertices stay exact, but the one belonging to a midside node kinks in the middle of an element and is not. Quadratic elements are exact at element vertices and merely third-order accurate at the midsides. Nodal exactness also needs the load `integral f phi_i` integrated exactly. Assembly here uses five-point Gauss-Legendre per element, exact through degree nine, so it holds to rounding for polynomial data and to quadrature error otherwise. # Sign conventions Flux conditions are stated with the *outward* normal, so the same `Bc::Neumann` value means the same physical thing at both ends: `p du/dn = g`, which is `-p u'(a) = g` on the left and `p u'(b) = g` on the right. `Bc::Robin` is `p du/dn + alpha u = g` in the same convention, and keeps the stiffness matrix symmetric. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.statistics.distributions import Poisson + +class Bc: + """ +A boundary condition at one end of the interval. + +Flux conditions use the outward normal, so a given value means the +same physical thing at either end. + +Rust: `fem::fem1d::Bc` + """ + ... + +class Fem1dSolution: + """ +A finite element solution, with the mesh it lives on. + +The solver functions return bare nodal values to match the shape of +the rest of the crate; wrapping them here is what makes it possible to +ask for the value *between* nodes, which is what an error norm needs. + +Rust: `fem::fem1d::Fem1dSolution` + """ + def __init__(self, a: float, b: float, degree: int, values: list[float]) -> None: ... + def elements(self) -> int: ... + def h(self) -> float: ... + def nodes(self) -> list[float]: ... + def eval(self, x: float) -> float: ... + def eval_derivative(self, x: float) -> float: ... + @property + def a(self) -> float: ... + @property + def b(self) -> float: ... + @property + def degree(self) -> int: ... + @property + def values(self) -> list[float]: ... + +def fem_1d_poisson(f: Callable[[float], float], a: float, b: float, bc: tuple[Bc, Bc], n: int) -> list[float]: + """ +Solves `-u'' = f` with linear elements on a uniform mesh of `n` +elements, returning the `n + 1` nodal values. + +With exact load integration this is nodally exact -- see the module +documentation for why that is a property of the Laplacian rather than +of the discretisation. + +Errors: + +`SolveError::InvalidArgument` for an empty mesh, a degenerate +interval, or non-finite data; `SolveError::Singular` when both ends +carry a pure flux condition, which leaves the solution undetermined up +to an additive constant. + +Rust: `fem::fem1d::fem_1d_poisson` + """ + ... + +def fem_1d_general(p: Callable[[float], float], q: Callable[[float], float], f: Callable[[float], float], a: float, b: float, bc: tuple[Bc, Bc], n: int) -> list[float]: + """ +Solves `-(p u')' + q u = f` with linear elements, returning the +`n + 1` nodal values. + +Errors: + +As `fem_1d_poisson`, and additionally +`SolveError::InvalidArgument` if `p` is not positive at a quadrature +point. A negative `q` large enough to make the operator indefinite is +reported as `SolveError::Singular`. + +Rust: `fem::fem1d::fem_1d_general` + """ + ... + +def fem_1d_quadratic(p: Callable[[float], float], q: Callable[[float], float], f: Callable[[float], float], a: float, b: float, bc: tuple[Bc, Bc], n: int) -> list[float]: + """ +Solves `-(p u')' + q u = f` with quadratic elements, returning the +`2n + 1` nodal values: element vertices at the even indices and +midsides at the odd ones. + +Errors: + +As `fem_1d_general`. + +Rust: `fem::fem1d::fem_1d_quadratic` + """ + ... + +def fem_1d_error_l2(u_h: Fem1dSolution, u_exact: Callable[[float], float]) -> float: + """ +The `L2` norm of the error against an exact solution. + +Rust: `fem::fem1d::fem_1d_error_l2` + """ + ... + +def fem_1d_error_h1_seminorm(u_h: Fem1dSolution, du_exact: Callable[[float], float]) -> float: + """ +The `H1` seminorm of the error: the `L2` norm of the derivative +difference alone. + +For the Poisson problem this is the energy norm, up to the factor the +coefficient `p` contributes, and so it is the norm in which the finite +element solution is the best approximation available. + +Rust: `fem::fem1d::fem_1d_error_h1_seminorm` + """ + ... + +def fem_1d_error_h1(u_h: Fem1dSolution, u_exact: Callable[[float], float], du_exact: Callable[[float], float]) -> float: + """ +The full `H1` norm of the error, `sqrt(L2^2 + seminorm^2)`. + +Rust: `fem::fem1d::fem_1d_error_h1` + """ + ... + +def convergence_rate(errors: list[float], hs: list[float]) -> float: + """ +The observed order of convergence: the least-squares slope of +`ln(error)` against `ln(h)`. + +A method converging as `C h^k` returns `k`. Fitting all the points +rather than taking the ratio of the last two is deliberate -- a single +ratio is a difference of two noisy logarithms and inherits the noise +of both. + +Errors: + +`SolveError::InvalidArgument` unless there are at least two pairs of +matching length, all strictly positive and finite, with at least two +distinct spacings. + +Rust: `fem::fem1d::convergence_rate` + """ + ... diff --git a/bindings/python/python/numeria/fem/fem2d.pyi b/bindings/python/python/numeria/fem/fem2d.pyi new file mode 100644 index 0000000..ad8a9a6 --- /dev/null +++ b/bindings/python/python/numeria/fem/fem2d.pyi @@ -0,0 +1,376 @@ +""" +Triangular finite elements in the plane. # The linear triangle On a triangle the three barycentric coordinates are themselves the linear shape functions, and their gradients are constant. That single fact does most of the work: the stiffness integral `integral grad(phi_i) . grad(phi_j)` has a constant integrand, so it is the gradient product times the triangle's area, with no quadrature involved and no error introduced. The whole element matrix for the Laplacian is ```text K_ij = (b_i b_j + c_i c_j) / (4 A) ``` where `b` and `c` are the edge-opposite coordinate differences and `A` is the signed area. The two-dimensional method inherits everything the one-dimensional one has -- Galerkin orthogonality, energy minimisation, best approximation in the energy norm -- because none of those arguments mentions the dimension. # What the mesh has to guarantee Two conditions matter and they are different in kind. *Conformity* is structural: two triangles meet along a whole shared edge or at a single shared vertex, never at a vertex hanging in the middle of a neighbour's edge. Without it the assembled function is not continuous and the space is not a subspace of `H1`, so the theory does not apply at all. It is checked here by counting: every edge belongs to one triangle or two, never more. *Shape* is quantitative. The interpolation error carries a factor of `1/sin(theta_min)`, so a mesh of slivers converges at the same rate with a much worse constant. `FemMesh2::quality_min_angle` reports the worst angle in the mesh, and uniform refinement leaves it exactly unchanged -- the four children of a triangle are all similar to their parent, which is the property that makes repeated refinement safe and that a red-green or longest-edge scheme has to work to recover. # Delaunay and the maximum principle The off-diagonal stiffness entry for an interior edge is `-(cot alpha + cot beta)/2`, the two angles opposite the edge in the triangles sharing it. It is nonpositive exactly when those angles sum to no more than `pi` -- which is the Delaunay condition. So a Delaunay triangulation gives an M-matrix, and an M-matrix gives a discrete maximum principle: a nonnegative load produces a nonnegative solution, and a harmonic one attains its extremes on the boundary. On a badly shaped non-Delaunay mesh the discrete solution can overshoot its own boundary data while still converging, which is exactly the kind of defect a plausibility check on a picture would miss. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.linalg.sparse import CsrMatrix +from numeria.statistics.distributions import Poisson +from numeria.math import Vec2 + +class FemMesh2: + """ +A conforming triangulation of a planar region. + +Rust: `fem::fem2d::FemMesh2` + """ + def __init__(self, nodes: list[Vec2 | Sequence[float]], tris: list[list[int]]) -> None: ... + @staticmethod + def rect(w: float, h: float, nx: int, ny: int) -> FemMesh2: ... + @staticmethod + def disk(r: float, n: int) -> FemMesh2: ... + @staticmethod + def from_delaunay(points: list[Vec2 | Sequence[float]]) -> FemMesh2: ... + def refine_uniform(self) -> FemMesh2: ... + def quality_min_angle(self) -> float: ... + def area(self) -> float: ... + def edge_count(self) -> int: ... + @property + def nodes(self) -> list[Vec2]: ... + @property + def tris(self) -> list[list[int]]: ... + @property + def boundary(self) -> list[int]: ... + +def fem_2d_poisson(mesh: FemMesh2, f: Callable[[Vec2 | Sequence[float]], float], dirichlet: Callable[[Vec2 | Sequence[float]], Optional[float]]) -> list[float]: + """ +Solves `-div(grad u) = f` on the mesh with the given Dirichlet data. + +`dirichlet` is consulted at every boundary node; returning `None` +leaves that node free, which imposes the natural zero-flux condition +there. Returning `None` everywhere leaves the constant in the kernel +and is reported as `SolveError::Singular`. + +The system is symmetric positive definite once the data is applied, so +it is solved by Jacobi-preconditioned conjugate gradients. + +Errors: + +`SolveError::InvalidArgument` for non-finite data, +`SolveError::Singular` if nothing pins the solution, and +`SolveError::NoConvergence` if the iteration stalls. + +Rust: `fem::fem2d::fem_2d_poisson` + """ + ... + +def fem_2d_reaction_diffusion(mesh: FemMesh2, c: Callable[[Vec2 | Sequence[float]], float], f: Callable[[Vec2 | Sequence[float]], float], dirichlet: Callable[[Vec2 | Sequence[float]], Optional[float]]) -> list[float]: + """ +Solves `-div(grad u) + c u = f` on the mesh with Dirichlet data. + +A positive `c` is a reaction term and keeps the problem coercive; a +negative one is the Helmholtz operator `-lap - k^2`, which loses +positive definiteness once `k^2` passes the first eigenvalue of the +domain. See `fem_2d_helmholtz` for that case, which needs a +different solver. + +Errors: + +As `fem_2d_poisson`, and `SolveError::NotPositiveDefinite` if the +reaction term makes the system indefinite. + +Rust: `fem::fem2d::fem_2d_reaction_diffusion` + """ + ... + +def stiffness_matrix(mesh: FemMesh2) -> CsrMatrix: + """ +The assembled stiffness matrix of the Laplacian, with no boundary +conditions applied. + +The off-diagonal entry for an edge is minus half the sum of the +cotangents of the two angles opposite it -- the identity that ties the +M-matrix property to the Delaunay condition, since a cotangent turns +negative exactly when its angle turns obtuse. Every row sums to zero, +because the three shape functions of a triangle sum to the constant +one and so their gradients sum to zero. + +Rust: `fem::fem2d::stiffness_matrix` + """ + ... + +def mass_matrix(mesh: FemMesh2) -> CsrMatrix: + """ +The assembled consistent mass matrix. + +`A/6` on the diagonal and `A/12` off it, per triangle. Its entries sum +to the area of the mesh, since the shape functions form a partition of +unity; the *lumped* alternative, which puts each row's total on its +diagonal, is what an explicit time integrator wants and is a different +matrix with the same total. + +Rust: `fem::fem2d::mass_matrix` + """ + ... + +def element_gradient(mesh: FemMesh2, values: list[float], tri: int) -> Optional[Vec2]: + """ +The gradient of a nodal field on one triangle, which is constant +there because the field is linear. + +Returns `None` for an out-of-range triangle index or a mismatched +value count. + +Rust: `fem::fem2d::element_gradient` + """ + ... + +def dirichlet_energy(mesh: FemMesh2, values: list[float]) -> float: + """ +The Dirichlet energy `integral |grad u|^2` of a nodal field, computed +exactly. + +It is exact rather than quadrature-limited because the gradient is +constant on each triangle, so the integral is a sum of area times a +squared length. This is the energy norm the finite element solution +minimises, and the quantity that must fall when the mesh is refined. + +Errors: + +`SolveError::DimensionMismatch` if the value count does not match +the node count. + +Rust: `fem::fem2d::dirichlet_energy` + """ + ... + +def interpolate(mesh: FemMesh2, values: list[float], p: Vec2 | Sequence[float]) -> Optional[float]: + """ +Evaluates a nodal field at an arbitrary point by locating the +containing triangle and interpolating barycentrically. + +Returns `None` if the point lies outside every triangle, or if the +value count does not match the mesh. The search is linear in the +triangle count -- there is no spatial index here, so this is for +sampling an answer rather than for an inner loop. + +Rust: `fem::fem2d::interpolate` + """ + ... + +def fem_2d_helmholtz(mesh: FemMesh2, k: float, f: Callable[[Vec2 | Sequence[float]], float], dirichlet: Callable[[Vec2 | Sequence[float]], Optional[float]]) -> list[float]: + """ +Solves the Helmholtz problem `-lap u - k^2 u = f` with Dirichlet data. + +This is the same assembly as `fem_2d_reaction_diffusion` with a +negative reaction term, but it needs a different solver and the reason +is structural rather than numerical. Once `k^2` passes the first +Dirichlet eigenvalue of the domain the operator stops being positive +definite, and conjugate gradients -- which is a minimisation method -- +has nothing left to minimise. A dense LU factorisation is used +instead, which costs `O(n^3)` in the node count and confines this +function to modest meshes. + +At `k^2` exactly equal to an eigenvalue the operator is singular: the +homogeneous problem has a nonzero solution, so the inhomogeneous one +has either none or a whole line of them. That is resonance, not a +numerical accident, and it is reported as `SolveError::Singular`. +Approaching an eigenvalue the response grows like the reciprocal of +the distance to it. + +Errors: + +`SolveError::InvalidArgument` for non-finite data, +`SolveError::Singular` at or extremely close to a resonance. + +Rust: `fem::fem2d::fem_2d_helmholtz` + """ + ... + +def fem_eigenvalues_drum(mesh: FemMesh2, count: int) -> list[float]: + """ +The `count` smallest eigenvalues of the Dirichlet Laplacian on the +mesh -- the squared frequencies of a drum clamped at its rim. + +The discrete problem is the generalised one `K phi = lambda M phi` +over the interior nodes, solved by transforming it to a standard +symmetric problem through the Cholesky factor of the mass matrix. +Using the consistent mass matrix rather than a lumped one matters +here: lumping shifts the eigenvalues downwards, and it is precisely +their being *upper* bounds that makes them useful. + +That bound is the property worth knowing. The discrete eigenvalues +come from the Rayleigh quotient minimised over a subspace of the true +admissible space, and a minimum over less is never smaller, so every +computed eigenvalue is an upper bound on the true one and refining the +mesh can only lower it. A method whose eigenvalues approach the answer +from below has a defect, however good its error looks. + +The dense eigensolver is `O(n^3)` in the interior node count, so this +is for meshes of hundreds of nodes rather than thousands. + +Errors: + +`SolveError::InvalidArgument` if `count` is zero or exceeds the +number of interior nodes; `SolveError::NotPositiveDefinite` if the +mass matrix fails to factor, and whatever the eigensolver reports. + +Rust: `fem::fem2d::fem_eigenvalues_drum` + """ + ... + +def fem_eigenmodes_drum(mesh: FemMesh2, count: int) -> tuple[list[float], list[list[float]]]: + """ +The `count` lowest drum modes: eigenvalues and the matching nodal +eigenvectors, the latter given over all nodes with zeros on the +clamped boundary. + +Eigenvectors are normalised so that the mass-weighted norm +`phi^T M phi` is one, which is the discrete form of normalising the +mode shape in `L2`. + +Errors: + +As `fem_eigenvalues_drum`. + +Rust: `fem::fem2d::fem_eigenmodes_drum` + """ + ... + +def fem_2d_elasticity_plane_stress(mesh: FemMesh2, e: float, nu: float, loads: list[tuple[int, Vec2 | Sequence[float]]], fixed: list[tuple[int, Vec2 | Sequence[float]]]) -> list[Vec2]: + """ +Solves the plane-stress elasticity problem on the mesh. + +`loads` are point forces applied at nodes and `fixed` prescribes +displacements at nodes, both components at once. Unit thickness is +assumed throughout, so a force is a force per unit thickness. + +The constant-strain triangle: + +Displacement is linear on each triangle, so strain -- its gradient -- +is constant there, and so is stress. That makes the element matrix +`A B^T D B` with no quadrature, exactly as for the Laplacian, and it +makes the stress field piecewise constant and discontinuous across +every edge. The discontinuity is not a bug to be smoothed away +silently: its size is an error estimate, and averaging it to the +nodes before showing it to anyone is how a coarse mesh comes to look +convincing. + +What has to be pinned: + +The stiffness matrix has a three-dimensional kernel: two translations +and one infinitesimal rotation. Prescribing fewer than three +independent degrees of freedom leaves the body free to move without +straining, and the system is singular no matter how many loads are +applied. This is checked directly. + +Errors: + +`SolveError::InvalidArgument` for a non-positive modulus, a +Poisson's ratio outside `(-1, 0.5)`, an out-of-range node index, or +non-finite data; `SolveError::Singular` if the constraints leave a +rigid body motion free. + +Rust: `fem::fem2d::fem_2d_elasticity_plane_stress` + """ + ... + +def element_strain(mesh: FemMesh2, u: list[Vec2 | Sequence[float]], tri: int) -> Optional[list[float]]: + """ +The constant strain `(eps_x, eps_y, gamma)` of one triangle, given a +nodal displacement field. + +`gamma` is the engineering shear strain, twice the tensor component. +Returns `None` for an out-of-range index or a mismatched field. + +Rust: `fem::fem2d::element_strain` + """ + ... + +def element_stress(mesh: FemMesh2, u: list[Vec2 | Sequence[float]], e: float, nu: float, tri: int) -> Optional[list[float]]: + """ +The constant stress `(sigma_x, sigma_y, tau)` of one triangle. + +Returns `None` for an out-of-range index, a mismatched field, or +material constants outside their admissible ranges. + +Rust: `fem::fem2d::element_stress` + """ + ... + +def strain_energy(mesh: FemMesh2, u: list[Vec2 | Sequence[float]], e: float, nu: float) -> float: + """ +The total strain energy `(1/2) integral sigma : eps`. + +At equilibrium this is half the work the applied loads do, which is +Clapeyron's theorem and follows from nothing more than the stiffness +matrix being symmetric. + +Errors: + +`SolveError::DimensionMismatch` for a mismatched field and +`SolveError::InvalidArgument` for invalid material constants. + +Rust: `fem::fem2d::strain_energy` + """ + ... + +def von_mises_stress(mesh: FemMesh2, u: list[Vec2 | Sequence[float]], e: float, nu: float) -> list[float]: + """ +The von Mises equivalent stress of each triangle, given a nodal +displacement field. + +One value per triangle, not per node: the strain of a linear +displacement field is constant on an element and discontinuous across +its edges. That discontinuity is not a bug to be smoothed away +silently -- its size is an error estimate, and averaging it to the +nodes before showing it to anyone is how a coarse mesh comes to look +convincing. + +In plane stress the out-of-plane stress is zero rather than free, so +the equivalent stress is +`sqrt(sx^2 - sx sy + sy^2 + 3 tau^2)`. A consequence worth noticing: +equal biaxial tension `sx = sy = s` gives `|s|`, not zero. The +three-dimensional intuition that hydrostatic stress cannot yield a +material does not survive into plane stress, because a state that is +hydrostatic *in plane* has a free surface out of it and so is not +hydrostatic at all. + +Errors: + +`SolveError::DimensionMismatch` if the displacement count does not +match the node count, and `SolveError::InvalidArgument` for invalid +material constants. + +Rust: `fem::fem2d::von_mises_stress` + """ + ... + +def fem_2d_heat_transient(mesh: FemMesh2, initial: list[float], alpha: float, dt: float, steps: int, theta: float, source: Callable[[Vec2 | Sequence[float]], float], dirichlet: Callable[[Vec2 | Sequence[float]], Optional[float]]) -> list[list[float]]: + """ +Marches the heat equation `u_t = alpha lap u + f` with the +`theta` scheme, returning `steps + 1` snapshots starting from the +initial field. + +The step solves +`(M + theta alpha dt K) u_next = (M - (1-theta) alpha dt K) u + dt F`. +`theta = 0` is forward Euler, `1` backward Euler, `1/2` +Crank-Nicolson. + +Stability, and the difference between A-stable and L-stable: + +Applied to a discrete eigenmode the scheme multiplies its amplitude +by `(1 - (1-theta) a) / (1 + theta a)` each step, with +`a = alpha lambda dt`. For `theta >= 1/2` that factor has magnitude +below one for every positive `a`, which is A-stability, and forward +Euler instead needs `a < 2`. + +Crank-Nicolson is A-stable but *not* L-stable: as `a` grows its factor +tends to `-1`, not to zero. A mode too stiff to resolve therefore +survives while flipping sign every step, which is why a discontinuous +initial condition rings under Crank-Nicolson and why the usual remedy +is to take the first couple of steps with backward Euler, whose +factor does tend to zero. That contrast is asserted in the tests. + +Errors: + +`SolveError::InvalidArgument` for a mismatched initial field, a +non-positive step, a `theta` outside `[0, 1]`, a negative diffusivity +or non-finite data; whatever the linear solver reports otherwise. + +Rust: `fem::fem2d::fem_2d_heat_transient` + """ + ... diff --git a/bindings/python/python/numeria/fem/spectral_pde.pyi b/bindings/python/python/numeria/fem/spectral_pde.pyi new file mode 100644 index 0000000..180fb30 --- /dev/null +++ b/bindings/python/python/numeria/fem/spectral_pde.pyi @@ -0,0 +1,165 @@ +""" +Spectral methods: global basis functions instead of local ones. # What changes when the basis stops being local A finite element expands the solution in functions that are nonzero on one or two cells. The matrix is sparse, and the accuracy is whatever the polynomial degree gives -- `h^2`, `h^3`, a fixed power of the mesh size no matter how smooth the answer is. A spectral method expands in functions that are nonzero everywhere and smooth: complex exponentials on a periodic domain, Chebyshev polynomials on an interval. The matrix becomes dense, and in exchange the error stops obeying any fixed power of `N` at all. For an analytic function it falls geometrically -- adding a few points multiplies the error by a constant factor rather than reducing it by a fixed order -- and for a function with `k` continuous derivatives it falls as `N^-k`. The method is only as good as the solution is smooth, and it is *exactly* as good as that. Both halves are measured in the tests rather than asserted. # Two Poisson solvers that are not the same solver `transforms::fft::fft_poisson_2d` already solves the periodic Poisson problem with an FFT, but it is not a spectral method. It divides by the eigenvalue of the *five-point* Laplacian, `(2 cos kx + 2 cos ky - 4)/h^2`, which makes the discrete residual vanish to rounding -- exactly what a pressure projection in a fluid solver wants, since there the finite-difference divergence is the thing that must be zero. Against the continuum it is second-order accurate and no better. `spectral_poisson_periodic` divides by the true symbol `-k^2`. Its discrete residual is not zero, and its error against the continuum solution is nil for anything the grid can represent and geometrically small otherwise. The two answers differ by `O(h^2)`, and which one is wanted depends on whether the discrete operator or the differential one is the thing being solved. # Chebyshev points cluster, and they have to Interpolating at equally spaced points on an interval diverges as the degree grows, even for functions as tame as `1/(1+25x^2)` -- Runge's phenomenon, and it is not a rounding problem but a property of the Lebesgue constant, which grows like `2^N/(N log N)`. The Chebyshev points `cos(j pi / N)` cluster towards the ends at a density that makes the Lebesgue constant grow only logarithmically, which is what makes high-degree interpolation usable at all. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.linalg.matrix import Matrix +from numeria.statistics.distributions import Poisson + +def chebyshev_points(n: int, a: float, b: float) -> list[float]: + """ +The `n + 1` Chebyshev-Gauss-Lobatto points on `[a, b]`. + +Ordered descending on `[-1, 1]` -- `x_j = cos(j pi / n)` runs from `1` +to `-1` -- which is the convention Trefethen's differentiation matrix +assumes, and mapped affinely onto `[a, b]`. Getting the order +backwards flips the sign of every derivative, silently. + +Errors: + +`SolveError::InvalidArgument` for `n == 0` or a degenerate +interval. + +Rust: `fem::spectral_pde::chebyshev_points` + """ + ... + +def cheb_diff_matrix(n: int, a: float, b: float) -> Matrix: + """ +The Chebyshev differentiation matrix on `[a, b]`, `(n+1)` square. + +Multiplying a vector of values at `chebyshev_points` by this matrix +gives the derivative of the degree-`n` polynomial through those +values, at the same points. For data that *is* a polynomial of degree +at most `n` the result is the exact derivative, to rounding, however +large `n` is. + +The off-diagonal entries are Trefethen's +`(c_i / c_j) (-1)^{i+j} / (x_i - x_j)`, with `c` equal to two at the +ends and one inside. The diagonal is *not* set from its closed form +but as minus the sum of the rest of its row -- the negative sum trick. +The two agree analytically, and differ in floating point by +cancellation that grows with `n`; taking the sum makes the matrix +annihilate constants exactly instead of nearly, which matters because +the constant is the one thing every derivative operator must kill and +the error in it pollutes everything else. + +Errors: + +`SolveError::InvalidArgument` for `n == 0` or a degenerate +interval. + +Rust: `fem::spectral_pde::cheb_diff_matrix` + """ + ... + +def cheb_differentiate(d: Matrix | Sequence[Sequence[float]], values: list[float]) -> list[float]: + """ +Differentiates values sampled at `chebyshev_points`. + +Errors: + +`SolveError::DimensionMismatch` if the sample count is not +`n + 1` for the matrix's `n`. + +Rust: `fem::spectral_pde::cheb_differentiate` + """ + ... + +def chebyshev_collocation_bvp(p: Callable[[float], float], q: Callable[[float], float], f: Callable[[float], float], a: float, b: float, bc: tuple[float, float], n: int) -> list[float]: + """ +Solves `-(p u')' + q u = f` on `[a, b]` with Dirichlet ends by +Chebyshev collocation. + +The operator is assembled as `-D diag(p) D + diag(q)` and the +equation is imposed at the interior collocation points, with the two +end rows replaced by the boundary conditions. Returns the `n + 1` +values at `chebyshev_points`. + +Only Dirichlet conditions are offered. A flux condition in a +collocation method means replacing an end row by a row of the +differentiation matrix, which works but changes the conditioning +enough to deserve its own treatment rather than a flag here. + +The matrix is dense and the cost is `O(n^3)`, which is the trade the +method makes: far fewer unknowns for the same accuracy, each of them +coupled to all the others. + +Errors: + +`SolveError::InvalidArgument` for a degenerate interval, `n < 2`, +a non-positive `p`, or non-finite data; `SolveError::Singular` if +the collocation matrix is singular, which a reaction term negative +enough to hit an eigenvalue will do. + +Rust: `fem::spectral_pde::chebyshev_collocation_bvp` + """ + ... + +def spectral_poisson_periodic(f: list[float], length: float) -> list[float]: + """ +Solves `u'' = f` on a periodic interval of the given length, using +the true spectral symbol `-k^2`. + +`f` is sampled at `n` equally spaced points starting at the left end; +the point at the right end is the same as the first and is not +included. The solution is fixed by taking it mean-free, which is the +only choice available: a periodic Poisson problem determines `u` only +up to a constant, and it has no solution at all unless `f` itself has +zero mean. A nonzero mean in the data is silently dropped -- the +alternative is refusing perfectly good data over a rounding-level +mean -- and `spectral_poisson_periodic` returns the solution of the +mean-free part. + +Compare `transforms::fft::fft_poisson_2d`, which divides by +the five-point Laplacian's eigenvalue instead. See the module note: +they solve different problems and both are right. + +Errors: + +`SolveError::InvalidArgument` for fewer than two samples, a +non-positive length, or non-finite data. + +Rust: `fem::spectral_pde::spectral_poisson_periodic` + """ + ... + +def spectral_second_derivative(u: list[float], length: float) -> list[float]: + """ +Differentiates a periodic sample twice with the spectral symbol, +which is the exact inverse of `spectral_poisson_periodic` on +mean-free data. + +Errors: + +As `spectral_poisson_periodic`. + +Rust: `fem::spectral_pde::spectral_second_derivative` + """ + ... + +def spectral_convergence_demo(f: Callable[[float], float], df: Callable[[float], float], a: float, b: float, sizes: list[int]) -> list[float]: + """ +The largest error in the Chebyshev derivative of `f` at each degree +in `sizes`. + +The point of the function is the *shape* of what it returns, not any +one entry. For an analytic `f` the sequence falls geometrically and a +log-log fit against `n` finds no fixed slope at all; for an `f` with +`k` continuous derivatives it falls as `n^-k` and the fit finds +exactly `k`. Plotting one without the other is what makes spectral +accuracy look like magic rather than like a statement about +smoothness. + +Errors: + +`SolveError::InvalidArgument` if any size is below one or the +interval is degenerate. + +Rust: `fem::spectral_pde::spectral_convergence_demo` + """ + ... diff --git a/bindings/python/python/numeria/fields.pyi b/bindings/python/python/numeria/fields.pyi new file mode 100644 index 0000000..cf75667 --- /dev/null +++ b/bindings/python/python/numeria/fields.pyi @@ -0,0 +1,60 @@ +""" +Uniform-grid scalar fields. Minimal backfill of the Part 2 `ScalarField2`/`ScalarField3` types that later roadmap phases build on: row-major storage with grid spacing and bilinear/trilinear sampling. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +class ScalarField2: + """ +Uniform-grid scalar fields. + +Minimal backfill of the Part 2 `ScalarField2`/`ScalarField3` types +that later roadmap phases build on: row-major storage with grid +spacing and bilinear/trilinear sampling. +2D scalar field on an nx×ny uniform grid with spacing dx +(row-major: index = y·nx + x; physical position of node (i, j) is +(i·dx, j·dx)). + +Rust: `fields::ScalarField2` + """ + def __init__(self, nx: int, ny: int, dx: float) -> None: ... + @staticmethod + def from_fn(nx: int, ny: int, dx: float, f: Callable[[float, float], float]) -> ScalarField2: ... + def get(self, i: int, j: int) -> float: ... + def set(self, i: int, j: int, v: float) -> None: ... + def sample(self, x: float, y: float) -> float: ... + def min_max(self) -> tuple[float, float]: ... + @property + def nx(self) -> int: ... + @property + def ny(self) -> int: ... + @property + def dx(self) -> float: ... + @property + def data(self) -> list[float]: ... + +class ScalarField3: + """ +3D scalar field on an nx×ny×nz uniform grid with spacing dx +(index = (k·ny + j)·nx + i). + +Rust: `fields::ScalarField3` + """ + def __init__(self, nx: int, ny: int, nz: int, dx: float) -> None: ... + def get(self, i: int, j: int, k: int) -> float: ... + def set(self, i: int, j: int, k: int, v: float) -> None: ... + def sample(self, x: float, y: float, z: float) -> float: ... + @property + def nx(self) -> int: ... + @property + def ny(self) -> int: ... + @property + def nz(self) -> int: ... + @property + def dx(self) -> float: ... + @property + def data(self) -> list[float]: ... diff --git a/bindings/python/python/numeria/finance/__init__.pyi b/bindings/python/python/numeria/finance/__init__.pyi new file mode 100644 index 0000000..0f461c4 --- /dev/null +++ b/bindings/python/python/numeria/finance/__init__.pyi @@ -0,0 +1,12 @@ +""" +Quantitative finance: derivative pricing, interest rates, portfolio construction and risk measurement. # What the models are and are not Every pricing model here is a statement about a *hypothetical* market: continuous trading, no transaction costs, a known volatility, and a price process of a stated form. None of those is true. What the models buy is not a prediction of price but a consistent way to quote one instrument in terms of another -- which is why the quantity traders actually exchange is implied volatility, the number that makes the formula reproduce the market price, rather than the price itself. The tests in this module lean hard on that internal consistency. Put-call parity is a no-arbitrage identity independent of the model; a binomial tree must converge to Black-Scholes as its steps grow; Monte Carlo must agree with the closed form within its own standard error; and the Greeks must match finite differences of the price they are derivatives of. Those are checkable. Whether the model describes a real market is not, and nothing here claims it. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from . import options, portfolio, rates, risk + + diff --git a/bindings/python/python/numeria/finance/options.pyi b/bindings/python/python/numeria/finance/options.pyi new file mode 100644 index 0000000..144a7c0 --- /dev/null +++ b/bindings/python/python/numeria/finance/options.pyi @@ -0,0 +1,494 @@ +""" +Option pricing: closed forms, lattices, Monte Carlo and a PDE solver. # Conventions Rates and volatilities are continuously compounded and annualised; time is in years. `q` is a continuous dividend yield, which also serves as a foreign interest rate for a currency option and as a convenience yield for a commodity. A `call: bool` argument names the payoff: `max(S - K, 0)` when true and `max(K - S, 0)` when false. # Why there are so many methods for one number They price different things, and where they overlap they check each other. `black_scholes` is exact but only for a European payoff on a lognormal process. A lattice (`binomial_crr`, `trinomial`) handles early exercise, at the cost of converging to the closed form only in the limit -- and it converges by oscillating around the answer, not by approaching it from one side. Monte Carlo (`monte_carlo_european` and the path-dependent payoffs) handles anything you can simulate, and pays for that with an error that falls like the square root of the path count, which is why the variance reduction here is not an optimisation but the difference between usable and not. # The volatility argument is the whole problem Black-Scholes takes one volatility for all strikes. Real option prices do not admit one: the implied volatilities of options on the same underlying and expiry form a smile, and a model with a single sigma cannot produce it. That is not a defect in the arithmetic, it is the lognormal assumption failing. `merton_jump_price` and the Heston model add mechanisms that generate a smile, and `volatility_smile_svi` simply parameterises one without a mechanism. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.statistics.distributions import Gamma +from numeria.statistics.distributions import Poisson +from numeria.monte_carlo import Rng + +class Barrier: + """ +Which barrier a knock-out or knock-in option watches. + +Rust: `finance::options::Barrier` + """ + ... + +class Greeks: + """ +The first-order sensitivities of an option price. + +Rust: `finance::options::Greeks` + """ + def __init__(self, delta: float, gamma: float, vega: float, theta: float, rho: float) -> None: ... + @property + def delta(self) -> float: ... + @property + def gamma(self) -> float: ... + @property + def vega(self) -> float: ... + @property + def theta(self) -> float: ... + @property + def rho(self) -> float: ... + +class Svi: + """ +The raw SVI parameterisation of a volatility smile. + +Rust: `finance::options::Svi` + """ + def __init__(self, a: float, b: float, rho: float, m: float, sigma: float) -> None: ... + @property + def a(self) -> float: ... + @property + def b(self) -> float: ... + @property + def rho(self) -> float: ... + @property + def m(self) -> float: ... + @property + def sigma(self) -> float: ... + +def black_scholes(s: float, k: float, t: float, r: float, sigma: float, q: float, call: bool) -> float: + """ +The Black-Scholes-Merton price of a European option. + +`S e^(-qT) N(d1) - K e^(-rT) N(d2)` for a call, and the mirror for a +put. The two terms are not "probability times payoff": the first is the +value of receiving the share if exercised, computed under a measure in +which the share is the numeraire, and the second is the strike times +the risk-neutral probability of exercise. Reading `N(d2)` as a +real-world probability is the commonest misreading of the formula -- +it is a probability under a measure chosen to make discounted prices +martingales, and has nothing to say about what the share will do. + +Zero volatility or zero time to expiry both collapse the formula to +the discounted intrinsic value, which is handled directly rather than +left to divide by zero. + +Errors: +Returns an error for a non-positive price or strike, a negative time or +volatility, or any input that is not finite. + +Rust: `finance::options::black_scholes` + """ + ... + +def bs_greeks(s: float, k: float, t: float, r: float, sigma: float, q: float, call: bool) -> Greeks: + """ +The Black-Scholes Greeks. + +`vega` is per unit of volatility (so divide by 100 for "per volatility +point"), `theta` is per year (divide by 365 for a daily decay), and +`rho` is per unit of rate. Those conventions differ between desks and +are the commonest source of a factor of a hundred. + +Gamma and vega are the same for a call and a put, because the two +differ by a forward contract, which is linear in the spot and does not +depend on volatility at all. That identity is exact and is what the +tests check rather than the individual numbers. + +Errors: +Returns an error for the same inputs as `black_scholes`, and for a +zero time or volatility, where the derivatives do not exist. + +Rust: `finance::options::bs_greeks` + """ + ... + +def implied_volatility(price: float, s: float, k: float, t: float, r: float, q: float, call: bool) -> Optional[float]: + """ +The volatility that reproduces an observed price, or `None` if no +volatility does. + +Price is strictly increasing in volatility, so the root is unique where +it exists; the search brackets it by doubling and then bisects, taking +Newton steps where vega is large enough to trust and falling back to +bisection where it is not. Deep out-of-the-money options have vega +near zero over a wide range of volatilities, which is exactly where a +pure Newton iteration diverges and where the answer is least +meaningful. + +`None` means no volatility can be recovered, for either of two +reasons. The price may be outside the model's range -- below the +no-arbitrage floor (the discounted intrinsic value), above the +ceiling, or unreachable at any volatility the doubling search reaches. +Or the price may simply not determine one: a deep in-the-money option +with weeks left has a vega around `1e-13`, and prices identically at +5% and at 20% volatility to the last bit of a double. Returning a +number there would be reporting rounding noise as a measurement, so +the answer is withheld when vega falls below `1e-8` relative to the +price. + +Errors: +Returns an error for a non-positive price or strike, a non-positive +time, or a negative observed price. + +Rust: `finance::options::implied_volatility` + """ + ... + +def put_call_parity_check(call: float, put: float, s: float, k: float, t: float, r: float, q: float) -> float: + """ +The put-call parity residual: `C - P - S e^(-qT) + K e^(-rT)`. + +Zero for any pair of European prices that admit no arbitrage, +*whatever* model produced them, because the identity follows from the +payoffs alone: holding a call and selling a put is the same as holding +the forward. A residual is therefore a statement about the prices, not +about the model, and this is the sharpest check available on a pricing +routine that has no closed form to compare with. + +Rust: `finance::options::put_call_parity_check` + """ + ... + +def binomial_crr(s: float, k: float, t: float, r: float, sigma: float, q: float, steps: int, call: bool, american: bool) -> float: + """ +The Cox-Ross-Rubinstein binomial tree. + +Up and down moves of `e^(±sigma sqrt(dt))` with the risk-neutral +probability that makes the discounted price a martingale. Set +`american` to allow exercise at every node. + +Convergence to Black-Scholes is `O(1/steps)` but *oscillatory*: the +error alternates in sign as the strike moves between two adjacent +terminal nodes, so a tree with 101 steps can be further from the answer +than one with 100. Averaging two consecutive step counts removes most +of it, and is why an odd-even pair is the honest way to quote a +lattice price. + +Errors: +Returns an error for bad option parameters, zero steps, more than +twenty thousand steps, or a `dt` so large that the risk-neutral +probability leaves `[0, 1]` -- which happens when the drift outruns +what the volatility can span in one step. + +Rust: `finance::options::binomial_crr` + """ + ... + +def trinomial(s: float, k: float, t: float, r: float, sigma: float, q: float, steps: int, call: bool, american: bool) -> float: + """ +A trinomial tree with an up, down and unchanged move. + +The third branch buys a free parameter, used here to set the space step +to `sigma sqrt(3 dt)`, which is the choice that makes the tree stable +and its convergence smoother than the binomial's. It is the same +explicit finite-difference scheme as the binomial in different +clothing, and the extra branch is what keeps the scheme's coefficients +positive over a wider range of steps. + +The probabilities here match the first two moments of the *log* price. +That is the usual construction and it has a consequence worth knowing: +unlike Cox-Ross-Rubinstein, whose up-probability is chosen to make the +price itself a martingale exactly, this tree is a martingale only to +`O(dt^2)`. So its call and put prices satisfy put-call parity only to +that order -- a residual of about `2e-3` on a two-and-a-half-year +option at seven steps, falling as `1/steps^2` and reaching `4e-8` by +sixteen hundred. The tree is arbitrage-free in the limit and not +before it. Use `binomial_crr` where an exactly consistent call and +put matter more than a smooth convergence. + +Errors: +As `binomial_crr`, with a lower step ceiling since the work is +quadratic in the step count. + +Rust: `finance::options::trinomial` + """ + ... + +def monte_carlo_european(s: float, k: float, t: float, r: float, sigma: float, q: float, call: bool, paths: int, rng: Rng) -> tuple[float, float]: + """ +A European option by Monte Carlo, returning `(price, standard error)`. + +Two variance reductions are applied, and both are exact rather than +heuristic: + +*Antithetic variates* price each draw with `z` and `-z`. The pair has +the same distribution as two independent draws, so the estimator stays +unbiased, and the negative correlation between the two payoffs shrinks +the variance of their mean. + +*A control variate* uses the discounted terminal price, whose expected +value under the risk-neutral measure is exactly `S e^(-qT)` -- known, +not estimated. Subtracting `beta` times its error from each payoff +cannot bias the result whatever `beta` is, and choosing `beta` by +regression on the same sample minimises the variance. + +The reported standard error is the error *of the reduced estimator*, +so it is the honest one to compare against the closed form: a price +two standard errors from Black-Scholes is a failure, and the tests +treat it as one. + +Errors: +Returns an error for bad option parameters or a path count outside +`[2, 2e7]`. + +Rust: `finance::options::monte_carlo_european` + """ + ... + +def monte_carlo_asian(s: float, k: float, t: float, r: float, sigma: float, q: float, call: bool, steps: int, paths: int, rng: Rng) -> tuple[float, float]: + """ +An arithmetic-average Asian option by Monte Carlo, returning +`(price, standard error)`. + +The average is taken over the `steps` monitoring dates, excluding the +start. Averaging is what makes the option cheaper than its European +twin: the average of a lognormal has lower variance than its terminal +value, and lower variance means a lower option price at the same +forward. + +There is no closed form for the arithmetic average -- the sum of +lognormals is not lognormal -- which is why this is a simulation and +not a formula. The *geometric* average does have one, and that is what +makes a geometric control variate the standard variance reduction +here; it is not applied, so expect the error to fall only as the +square root of the path count. + +Errors: +Returns an error for bad option parameters, a path count outside +`[2, 2e7]`, a step count of zero, or more than fifty million total +steps. + +Rust: `finance::options::monte_carlo_asian` + """ + ... + +def monte_carlo_barrier(s: float, k: float, barrier: float, kind: Barrier, t: float, r: float, sigma: float, q: float, call: bool, steps: int, paths: int, rng: Rng) -> tuple[float, float]: + """ +A barrier option by Monte Carlo, returning `(price, standard error)`. + +The barrier is checked only at the `steps` monitoring dates. That is a +*discretely monitored* option and it is worth strictly more than a +continuously monitored one, because a path can cross the barrier and +come back between observations. The gap closes slowly, like +`1/sqrt(steps)`, so a daily-monitored option priced with twelve steps +is materially mispriced -- the discretisation is a modelling choice +here, not a numerical detail. + +The in-out parity holds by construction: a knock-in and its matching +knock-out sum to the vanilla option, since every path pays into exactly +one of them. + +Errors: +As `monte_carlo_asian`, plus a non-positive barrier level. + +Rust: `finance::options::monte_carlo_barrier` + """ + ... + +def monte_carlo_lookback(s: float, k: float, t: float, r: float, sigma: float, q: float, call: bool, steps: int, paths: int, rng: Rng) -> tuple[float, float]: + """ +A fixed-strike lookback option by Monte Carlo, returning +`(price, standard error)`. + +A call pays on the running maximum and a put on the running minimum, so +the holder is credited with the best price the path ever reached. It is +therefore worth at least as much as the European option with the same +strike, always and path by path, and the tests use that as an ordering +rather than a number. + +Discrete monitoring cuts the price for the same reason it raises a +knock-out's: the sampled extremum is closer to the terminal value than +the continuous one. + +Errors: +As `monte_carlo_asian`. + +Rust: `finance::options::monte_carlo_lookback` + """ + ... + +def longstaff_schwartz_american(s: float, k: float, t: float, r: float, sigma: float, q: float, call: bool, steps: int, paths: int, rng: Rng) -> float: + """ +The Longstaff-Schwartz price of an American option by least-squares +Monte Carlo. + +Working backwards from expiry, the continuation value at each exercise +date is regressed on a quadratic in the current price, using only the +paths that are in the money -- and the *fitted* value, not the +realised one, decides whether to exercise. Using the realised future +payoff to make the decision would be looking ahead, and would produce a +price above the true one. + +The estimate is biased low in principle, because the exercise rule +comes from a finite regression and any suboptimal rule undervalues the +option. In practice with a low-order basis it can also come out high +on the same sample the rule was fitted on, which is why the tests +compare it against a binomial tree with a tolerance rather than +asserting a direction. + +Errors: +As `monte_carlo_asian`, and for fewer than two exercise dates. + +Rust: `finance::options::longstaff_schwartz_american` + """ + ... + +def merton_jump_price(s: float, k: float, t: float, r: float, sigma: float, q: float, lambda_: float, jump_mean: float, jump_vol: float, call: bool) -> float: + """ +Merton's jump-diffusion price, as a Poisson-weighted sum of +Black-Scholes prices. + +A jump arriving at rate `lambda` multiplies the price by a lognormal +factor with log-mean `jump_mean` and log-standard-deviation +`jump_vol`. Conditioning on the number of jumps makes each term +lognormal again, so the price is an exact infinite sum of Black-Scholes +prices with adjusted rate and volatility, truncated here once the +Poisson weights are exhausted. + +The drift compensator `-lambda * (e^(jump_mean + jump_vol^2/2) - 1)` +is what keeps the discounted price a martingale: jumps add expected +return, and it must be taken back out of the diffusion or the model +prices an arbitrage. + +Jumps are what generate a smile. A single lognormal cannot make +out-of-the-money options expensive relative to at-the-money ones; a +mixture over jump counts has fatter tails and does exactly that. + +Errors: +Returns an error for bad option parameters, a negative jump intensity +or volatility, or a non-positive maturity. + +Rust: `finance::options::merton_jump_price` + """ + ... + +def heston_price_mc(s: float, k: float, t: float, r: float, q: float, v0: float, kappa: float, theta: float, xi: float, rho: float, call: bool, steps: int, paths: int, rng: Rng) -> tuple[float, float]: + """ +A Heston stochastic-volatility price by Monte Carlo, returning +`(price, standard error)`. + +The variance follows `dv = kappa (theta - v) dt + xi sqrt(v) dW`, with +the variance's Brownian motion correlated with the price's at `rho`. +That correlation is the model's point: a negative `rho` makes the +volatility rise as the price falls, which produces the downward-sloping +implied volatility skew that equity markets actually show, and which no +symmetric model can. + +The variance is simulated with a full-truncation Euler scheme -- the +variance is floored at zero wherever a step takes it negative. Exact +simulation of the variance process is possible but expensive, and +full truncation is the standard compromise; it biases the price +slightly, and the bias falls with the step count rather than the path +count, so refining paths alone will not remove it. + +Errors: +Returns an error for bad option parameters, a negative initial or +long-run variance, a non-positive mean reversion or volatility of +volatility, a correlation outside `[-1, 1]`, or a step or path count +outside its budget. + +Rust: `finance::options::heston_price_mc` + """ + ... + +def volatility_smile_svi(params: Svi | Sequence[float], k: float) -> float: + """ +Total implied variance under raw SVI: +`a + b (rho (k - m) + sqrt((k - m)^2 + sigma^2))`. + +`k` is log-moneyness `ln(K/F)` and the result is *total* variance +`sigma_implied^2 * T`, not annualised variance. SVI is a shape, not a +model: it has no process behind it and makes no prediction, and its +value is that five parameters fit an observed smile closely and the +wings are linear in `k`, which is what Lee's moment formula requires of +any arbitrage-free smile. + +Errors: +Returns an error for a negative `b`, a non-positive `sigma`, a `rho` +outside `[-1, 1]`, or a total variance that comes out negative -- which +is an arbitrage, not a small numerical matter. + +Rust: `finance::options::volatility_smile_svi` + """ + ... + +def svi_fit(log_moneyness: list[float], total_variance: list[float]) -> Svi: + """ +Fits raw SVI to observed total variances by Nelder-Mead on the sum of +squared errors. + +The objective is not convex and the parameters trade off against each +other -- `b` and `sigma` in particular are nearly degenerate for a +shallow smile -- so the search is restarted from the best point found, +which is what rescues it from the flat valley a single pass stalls in. +A good fit here means the shape matches, not that the parameters are +identified. + +Errors: +Returns an error for fewer than five points, mismatched lengths, or a +non-positive total variance among the targets. + +Rust: `finance::options::svi_fit` + """ + ... + +def bs_pde_crank_nicolson(s: float, k: float, t: float, r: float, sigma: float, q: float, call: bool, american: bool, space: int, time_steps: int) -> float: + """ +The Black-Scholes PDE solved by Crank-Nicolson on a log-price grid. + +Solves `dV/dt + (r - q - sigma^2/2) dV/dx + (sigma^2/2) d2V/dx2 = rV` +backwards from the payoff, on `space` points spanning six standard +deviations either side of the log spot, with Dirichlet boundaries set +to the discounted no-arbitrage values. Set `american` to apply the +early-exercise constraint after each step, which makes the scheme a +projected one and costs its second-order accuracy in time near the +exercise boundary. + +Crank-Nicolson is used rather than a fully implicit scheme because it +is second order in time as well as space. The price of that is that it +is only *A*-stable and not *L*-stable: it damps high-frequency error +slowly, so the kink in the payoff at the strike rings for several steps +rather than being smoothed away, and the Greeks near the strike are +visibly noisier than the price. Starting with a few fully implicit +steps -- Rannacher smoothing -- is the standard remedy and is what the +first two steps here do. + +Errors: +Returns an error for bad option parameters, fewer than eleven space +points, no time steps, more than ten million grid cells, or a +tridiagonal system that will not solve. + +Rust: `finance::options::bs_pde_crank_nicolson` + """ + ... + +def delta_hedging_sim(s: float, k: float, t: float, r: float, hedge_vol: float, realised_vol: float, q: float, call: bool, rebalances: int, paths: int, rng: Rng) -> tuple[float, float]: + """ +Simulates delta hedging a short European option, returning +`(mean profit and loss, standard deviation)`. + +The option is sold at its Black-Scholes price and the position is +rehedged `rebalances` times at the model delta; the P&L is what remains +at expiry after the payoff is settled. + +The mean is near zero because the option was sold at its fair price, +but the *standard deviation* is the point: it falls like +`1/sqrt(rebalances)`, so cutting the residual risk in half costs four +times as many trades. That trade-off, not the mean, is what makes +continuous hedging a limit rather than a procedure -- with any +transaction cost at all, the total cost grows as `sqrt(rebalances)` +while the risk falls as `1/sqrt(rebalances)`, and an optimum exists at +a finite frequency. + +A hedge run at a volatility different from the one the path was +generated with does not have a zero mean; the difference is the +volatility arbitrage, and it is what the tests check rather than the +noise. + +Errors: +Returns an error for bad option parameters, no rebalances, a +non-positive maturity, or a path count outside `[2, 2e7]`. + +Rust: `finance::options::delta_hedging_sim` + """ + ... diff --git a/bindings/python/python/numeria/finance/portfolio.pyi b/bindings/python/python/numeria/finance/portfolio.pyi new file mode 100644 index 0000000..ff5a53e --- /dev/null +++ b/bindings/python/python/numeria/finance/portfolio.pyi @@ -0,0 +1,320 @@ +""" +Portfolio construction and performance measurement. # What mean-variance optimisation actually does Markowitz's problem is: given expected returns and a covariance matrix, find the weights minimising variance at each level of expected return. It has a closed form, and that is both its appeal and its trap. The optimiser is an *error maximiser*: it puts weight where the estimated return is highest relative to the estimated risk, which is exactly where the estimates are most likely to be wrong. Expected returns estimated from a decade of monthly data carry standard errors of the same order as the differences between assets, so the "optimal" portfolio is often a leveraged bet on estimation noise. Nothing here shrinks, regularises or constrains, because the roadmap's signatures do not. `min_variance_weights` uses only the covariance matrix, which is estimated far more reliably than the mean, and is for that reason the one output here that survives contact with real data. # Returns compound, and that decides which average to use `returns_from_prices` gives simple returns, whose *arithmetic* mean is the expected one-period return. `log_returns` gives continuously compounded returns, which add across periods, so their *sum* is the total log return. Mixing them up produces the standard error of quoting an arithmetic mean as though it were achievable: a series that gains 50% then loses 50% has an arithmetic mean return of zero and has lost a quarter of its value. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.statistics.distributions import Beta +from numeria.linalg.matrix import Matrix + +def returns_from_prices(prices: list[float]) -> list[float]: + """ +Simple period returns `p[t]/p[t-1] - 1`. + +Errors: +Returns an error for fewer than two prices, or a non-positive or +non-finite price. + +Rust: `finance::portfolio::returns_from_prices` + """ + ... + +def log_returns(prices: list[float]) -> list[float]: + """ +Continuously compounded returns `ln(p[t]/p[t-1])`. + +These add across periods, which is what makes them the right thing to +average when the question is about growth over time rather than about +the next period. They are always smaller than the simple return, by +roughly half the variance, which is the whole content of the +arithmetic-geometric gap. + +Errors: +As `returns_from_prices`. + +Rust: `finance::portfolio::log_returns` + """ + ... + +def portfolio_variance(cov: Matrix | Sequence[Sequence[float]], weights: list[float]) -> float: + """ +The portfolio variance `w' C w`. + +Errors: +Returns an error for a malformed covariance matrix or mismatched +weights. + +Rust: `finance::portfolio::portfolio_variance` + """ + ... + +def min_variance_weights(cov: Matrix | Sequence[Sequence[float]]) -> list[float]: + """ +The global minimum-variance weights, which sum to one. + +`w = C^-1 1 / (1' C^-1 1)`. Expected returns do not appear, which is +why this is the mean-variance output that survives real data: a +covariance matrix estimated from the same sample that produced a +hopeless mean estimate is still usually good enough to rank risk. + +Weights may be negative -- the problem as posed allows short positions, +and with correlated assets the minimum-variance solution frequently +takes them. + +Errors: +Returns an error for a malformed or singular covariance matrix, or one +whose implied weights do not sum to a usable total. + +Rust: `finance::portfolio::min_variance_weights` + """ + ... + +def tangency_portfolio(mu: list[float], cov: Matrix | Sequence[Sequence[float]], risk_free: float) -> list[float]: + """ +The tangency portfolio: the weights maximising the Sharpe ratio at a +given risk-free rate. + +`w = C^-1 (mu - rf) / (1' C^-1 (mu - rf))`. Every portfolio on the +efficient frontier with a risk-free asset available is a mix of this +one and cash, which is the two-fund separation theorem -- and it is +what makes "the market portfolio" a meaningful object in CAPM. + +The normalisation fails when the excess returns are orthogonal to the +inverse-covariance-weighted ones, and flips sign when the excess +returns are net negative, at which point the "tangency portfolio" is a +short position and the geometry has broken down. Both are reported +rather than returned as numbers. + +Errors: +Returns an error for a malformed or singular covariance matrix, +mismatched means, or excess returns that do not determine a tangency. + +Rust: `finance::portfolio::tangency_portfolio` + """ + ... + +def markowitz_frontier(mu: list[float], cov: Matrix | Sequence[Sequence[float]], points: int) -> list[tuple[float, float, list[float]]]: + """ +The efficient frontier as `(standard deviation, expected return, +weights)`, from the minimum-variance point up to the highest mean. + +Each point solves the two-constraint problem exactly through the +standard `a, b, c` scalars, so no numerical optimisation is involved. +The frontier is a hyperbola in mean-standard-deviation space and a +parabola in mean-variance space, and its lower half -- the same +variances at lower returns -- is dominated and not returned. + +Short positions are permitted throughout. A frontier computed with a +no-short constraint is a different and much better behaved object, +and it has no closed form. + +Errors: +Returns an error for a malformed or singular covariance matrix, +mismatched means, fewer than two points, more than ten thousand, or +means that are all equal, where the frontier degenerates to a point. + +Rust: `finance::portfolio::markowitz_frontier` + """ + ... + +def risk_parity_weights(cov: Matrix | Sequence[Sequence[float]]) -> list[float]: + """ +Risk-parity weights: each asset contributes the same share of total +portfolio risk. + +The condition is `w_i (C w)_i` equal across assets, which has no closed +form. It is solved here by the fixed point of +`w_i <- sqrt(w_i / (C w)_i)`, renormalised each pass: at rest that +gives `w_i^2 = k^2 w_i / (C w)_i`, so `w_i (C w)_i` is the same +constant for every asset, which is the condition itself. + +The square root is not decoration. The undamped update +`w_i <- w_i / (C w)_i` converges to `(C w)_i` equal across assets -- +which is the *minimum-variance* condition, not this one, and gives +visibly different weights whenever the assets differ in volatility. + +This is not the same as equal weights, nor as inverse-volatility +weights -- those coincide with it only when correlations are all +equal. The appeal is that it needs no expected returns at all, which +removes the input mean-variance optimisation is most damaged by. + +Weights are constrained positive, which is what makes the problem well +posed: the equal-risk-contribution condition has no positive solution +requirement built in, and shorting breaks the interpretation. + +Errors: +Returns an error for a malformed covariance matrix, or an iteration +that does not converge. + +Rust: `finance::portfolio::risk_parity_weights` + """ + ... + +def risk_contributions(cov: Matrix | Sequence[Sequence[float]], weights: list[float]) -> list[float]: + """ +Each asset's share of total portfolio risk: `w_i (C w)_i / (w' C w)`. + +The shares sum to one by construction, which is what makes "risk +contribution" a decomposition rather than a metaphor -- variance is a +quadratic form and Euler's theorem splits it exactly. + +Errors: +As `portfolio_variance`, plus a portfolio with no variance. + +Rust: `finance::portfolio::risk_contributions` + """ + ... + +def sharpe(returns: list[float], risk_free: float) -> float: + """ +The Sharpe ratio: mean excess return divided by its standard deviation. + +Per period, not annualised -- multiplying by the square root of the +periods per year is the usual annualisation and it assumes returns are +independent, which is exactly what a trending or mean-reverting series +is not. + +The denominator penalises upside and downside alike. A strategy that +occasionally doubles is punished for it, which is what `sortino` +addresses, and a strategy that sells insurance -- small steady gains +and a rare catastrophe -- scores well right up until the catastrophe. +The ratio says nothing about the shape of the distribution beyond its +first two moments. + +Errors: +Returns an error for fewer than two returns, a non-finite value, or a +series with no variation. + +Rust: `finance::portfolio::sharpe` + """ + ... + +def sortino(returns: list[float], risk_free: float, target: float) -> float: + """ +The Sortino ratio: mean excess return over the downside deviation. + +The denominator is the root mean square of the shortfalls below +`target`, counting periods above it as zero rather than dropping them. +That choice matters: dividing by the count of losing periods instead +would make a strategy look better simply for losing less often, and +the two conventions differ by a factor that grows as losses get rarer. + +Errors: +Returns an error for fewer than two returns, a non-finite value, or a +series that never falls below the target. + +Rust: `finance::portfolio::sortino` + """ + ... + +def max_drawdown(prices: list[float]) -> float: + """ +The maximum drawdown: the largest peak-to-trough fall, as a positive +fraction of the peak. + +Computed against the running maximum, so it is a property of the path +and not of the endpoints. Two series with the same start and end can +have wildly different drawdowns, which is the point -- it measures what +an investor would have had to sit through. + +Errors: +Returns an error for fewer than two prices, or a non-positive price. + +Rust: `finance::portfolio::max_drawdown` + """ + ... + +def calmar(prices: list[float], periods_per_year: float) -> float: + """ +The Calmar ratio: annualised return divided by maximum drawdown. + +`periods_per_year` converts the series' own period into a year. The +return used is the *geometric* one -- the constant rate that would have +produced the same total growth -- because that is what an investor +actually earned, unlike the arithmetic mean. + +Errors: +Returns an error for fewer than two prices, a non-positive price or +period count, or a series with no drawdown to divide by. + +Rust: `finance::portfolio::calmar` + """ + ... + +def information_ratio(portfolio: list[float], benchmark: list[float]) -> float: + """ +The information ratio: mean active return over its tracking error. + +Active return is the portfolio's minus the benchmark's, period by +period. It is the Sharpe ratio of a long-short position against the +benchmark, which is why it is the natural measure for a manager judged +relative to an index rather than to cash. + +Errors: +Returns an error for mismatched or too-short series, a non-finite +value, or an active series with no variation. + +Rust: `finance::portfolio::information_ratio` + """ + ... + +def capm_beta(asset: list[float], market: list[float]) -> tuple[float, float]: + """ +The CAPM regression of an asset on the market, returning +`(alpha, beta)`. + +Beta is `cov(asset, market) / var(market)` and alpha is the intercept +that remains. Beta is an estimate of sensitivity and nothing more: it +is a single number summarising a scatter that may not be linear, it is +unstable across sample periods, and a high R-squared is required before +it means very much at all. + +Errors: +Returns an error for mismatched or too-short series, a non-finite +value, or a market series with no variation. + +Rust: `finance::portfolio::capm_beta` + """ + ... + +def kelly_fraction(p: float, b: float) -> float: + """ +The Kelly fraction for a discrete bet won with probability `p` paying +`b` to one: `p - (1 - p)/b`. + +Maximises the expected *logarithm* of wealth, which is the growth rate +achieved almost surely over many repetitions. A negative answer means +the bet has no edge and the optimal stake is nothing. + +The fraction assumes the edge is known exactly. Overestimating it +pushes the stake past the growth-optimal point, where growth falls +faster than it rose: staking twice the Kelly fraction earns no more +than the risk-free rate however large the edge, and beyond that it +loses. That is why practitioners bet a fraction of it. + +Errors: +Returns an error for a probability outside `[0, 1]` or a non-positive +payout. + +Rust: `finance::portfolio::kelly_fraction` + """ + ... + +def kelly_continuous(mu: float, sigma: float, risk_free: float) -> float: + """ +The continuous Kelly fraction `(mu - rf)/sigma^2`. + +The same object for a lognormal asset: the leverage maximising the +long-run growth rate. It is also the tangency portfolio's leverage +under one asset, which is not a coincidence -- both maximise the +Sharpe-like quantity `(mu - rf)/sigma` scaled by the risk taken. + +Errors: +Returns an error for a non-positive volatility or a non-finite input. + +Rust: `finance::portfolio::kelly_continuous` + """ + ... diff --git a/bindings/python/python/numeria/finance/rates.pyi b/bindings/python/python/numeria/finance/rates.pyi new file mode 100644 index 0000000..64e0431 --- /dev/null +++ b/bindings/python/python/numeria/finance/rates.pyi @@ -0,0 +1,410 @@ +""" +Interest rates: discounting, bonds, curves and short-rate models. # Two things a "rate" can mean A quoted rate is meaningless without its compounding convention. 10% compounded annually, semi-annually and continuously produce growth factors of 1.1, 1.1025 and 1.10517 over a year -- differences that are small over one period and decisive over thirty. `Compounding` makes the convention explicit at every call site rather than leaving it to a comment, and `equivalent_rate` converts between them. The second distinction is between a *zero rate*, which discounts a single payment at one maturity, and a *yield*, which is the single rate that reproduces a whole bond's price. They coincide only for a zero-coupon bond. A coupon bond's yield is a weighted average of the zero rates along its life, weighted by the discounted cashflows -- so two bonds of the same maturity and different coupons have different yields off the same curve, which is what makes a yield a property of the instrument rather than of the market. # What is solved and what is assumed `irr`, `ytm_solve` and `bootstrap_zero_curve` invert a price to find a rate, and each has a uniqueness condition that the documentation states and the code checks where it can. A yield always exists and is unique for a bond with positive cashflows; an internal rate of return need not be either, and the sign-change test is the only cheap guarantee available. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +class Compounding: + """ +How often a quoted rate compounds. + +Rust: `finance::rates::Compounding` + """ + def periods_per_year(self) -> Optional[float]: ... + +class CurveBond: + """ +One instrument on the curve to bootstrap: a bond quoted by price. + +Rust: `finance::rates::CurveBond` + """ + def __init__(self, maturity: float, coupon: float, price: float, frequency: float) -> None: ... + @property + def maturity(self) -> float: ... + @property + def coupon(self) -> float: ... + @property + def price(self) -> float: ... + @property + def frequency(self) -> float: ... + +def discount_factor(rate: float, t: float, compounding: Compounding) -> float: + """ +The present value of one unit paid at time `t`. + +`(1 + r/m)^(-m t)` for `m` compounding periods a year, and `e^(-r t)` +continuously. The two agree in the limit `m -> infinity`, which is the +whole reason continuous compounding is used in pricing: it turns a +product over periods into an exponential and makes rates additive +across maturities. + +Errors: +Returns an error for a negative time, a non-finite rate, or a periodic +rate at or below `-100%` per period, where the growth factor is +non-positive and the discount factor does not exist. + +Rust: `finance::rates::discount_factor` + """ + ... + +def equivalent_rate(rate: float, from_: Compounding, to: Compounding) -> float: + """ +Converts a rate between compounding conventions, preserving the growth +factor over a year. + +The number changes but the money does not: 10% semi-annual and 9.7580% +continuous are the same investment written two ways. Quoting the +smaller number is a real practice and this is what makes the two +comparable. + +Errors: +Returns an error for a non-finite rate or a periodic rate at or below +`-100%` per period. + +Rust: `finance::rates::equivalent_rate` + """ + ... + +def npv(rate: float, cashflows: list[float]) -> float: + """ +The net present value of cashflows at times `0, 1, ..., n-1` periods. + +Discounted at the periodic rate `rate`, so `cashflows[0]` is undiscounted. + +Errors: +Returns an error for no cashflows, a non-finite value, or a rate at or +below `-100%`. + +Rust: `finance::rates::npv` + """ + ... + +def irr(cashflows: list[float]) -> Optional[float]: + """ +The internal rate of return: the periodic rate at which the cashflows' +net present value is zero. + +Returns `None` when no rate in `(-99.99%, 1e6)` does, or when the +cashflows change sign more than once and the answer would not be +unique. That second case is the one worth knowing about: Descartes' +rule bounds the number of positive roots by the number of sign changes, +so a single change guarantees at most one rate, and a project that +alternates between spending and earning can genuinely have several +internal rates of return or none at all. Reporting one of them as +*the* return would be a mistake this refuses to make. + +Errors: +Returns an error for fewer than two cashflows, or a non-finite value. + +Rust: `finance::rates::irr` + """ + ... + +def xirr(times: list[float], cashflows: list[float]) -> Optional[float]: + """ +The annualised internal rate of return for cashflows at irregular +dates, given in years from the first. + +The rate is annual with annual compounding, so a payment at 0.5 years +is discounted by `(1 + r)^-0.5`. That fractional exponent is why this +needs its own function rather than being IRR on a padded schedule: real +cashflows do not fall on period boundaries, and forcing them there +misprices by days of interest. + +Errors: +Returns an error for fewer than two flows, mismatched lengths, times +that are not increasing from zero, or a non-finite value. + +Rust: `finance::rates::xirr` + """ + ... + +def bond_price(face: float, coupon: float, ytm: float, periods: int) -> float: + """ +The price of a bond paying `coupon` per period for `periods` periods +and `face` at the end, discounted at the periodic yield `ytm`. + +All three arguments are *per period*, not per year: a 6% annual coupon +on 100 face paid semi-annually for five years is `coupon = 3`, +`periods = 10`, and a yield quoted semi-annually. + +A bond trades above face when its coupon exceeds its yield and below +when it does not, and that is not a market opinion but arithmetic: the +price is the yield's own discounting applied to a coupon stream that +pays more or less than the yield demands. + +Errors: +Returns an error for zero periods, more than ten thousand, a +non-finite input, or a yield at or below `-100%` per period. + +Rust: `finance::rates::bond_price` + """ + ... + +def ytm_solve(price: float, face: float, coupon: float, periods: int) -> float: + """ +The periodic yield that reproduces an observed bond price. + +Unique whenever the coupons and face are non-negative and at least one +is positive: the price is then strictly decreasing in the yield, so +there is exactly one root. That is why a bond has *a* yield where a +project may have several internal rates of return -- the cashflows +after the purchase all point the same way. + +Errors: +Returns an error for a non-positive price, bad bond parameters, or a +price no yield in `(-99.99%, 1e6)` reaches. + +Rust: `finance::rates::ytm_solve` + """ + ... + +def duration_macaulay(face: float, coupon: float, ytm: float, periods: int) -> float: + """ +The Macaulay duration in periods: the discounted-cashflow-weighted +average time to payment. + +It is a *centre of mass*, which is why it has units of time and why a +zero-coupon bond's duration is exactly its maturity: all the weight +sits at one date. Coupons pull the centre earlier, so a higher coupon +always shortens duration at the same maturity. + +Errors: +As `bond_price`, plus a bond whose price comes out non-positive. + +Rust: `finance::rates::duration_macaulay` + """ + ... + +def duration_modified(face: float, coupon: float, ytm: float, periods: int) -> float: + """ +The modified duration: Macaulay duration divided by `1 + ytm`. + +This is the one that answers "how much does the price move": it is +exactly `-(1/P) dP/dy`, so a modified duration of 7 means a price fall +of about 7% for a one-point rise in yield. The word "about" is doing +real work -- duration is the first derivative and the relationship is +convex, so it overstates the loss on a rise and understates the gain +on a fall. `convexity` is the correction. + +Errors: +As `duration_macaulay`. + +Rust: `finance::rates::duration_modified` + """ + ... + +def convexity(face: float, coupon: float, ytm: float, periods: int) -> float: + """ +The convexity in periods squared: `(1/P) d2P/dy2`. + +Always positive for an ordinary bond, which is the reason duration +alone is pessimistic in both directions. Between two bonds of equal +duration the more convex one gains more when yields move either way, +and its price reflects that -- convexity is not a free lunch, it is +paid for in yield. + +Errors: +As `duration_macaulay`. + +Rust: `finance::rates::convexity` + """ + ... + +def bootstrap_zero_curve(bonds: list[CurveBond | Sequence[float]]) -> list[tuple[float, float]]: + """ +Bootstraps a zero-coupon curve from bonds of increasing maturity, +returning `(maturity, continuously compounded zero rate)`. + +Each bond is stripped in turn: its earlier coupons are discounted at +the zero rates already recovered, and whatever discount factor the +final payment needs to make the price work is the new point. The +method is exact and sequential, and that is also its weakness -- an +error in an early quote propagates into every later rate, and there is +no least-squares smoothing to absorb it. + +Coupon dates that fall between known maturities are interpolated +linearly *in the zero rate*, which is a choice: interpolating in the +discount factor or the forward rate gives different curves from the +same bonds, and no market convention makes one correct. + +Errors: +Returns an error for no bonds, maturities that do not increase, a +non-positive price, frequency or maturity, a maturity that is not a +whole number of periods, or a final cashflow whose implied discount +factor is non-positive -- which means the quotes admit an arbitrage. + +Rust: `finance::rates::bootstrap_zero_curve` + """ + ... + +def forward_rate(z1: float, t1: float, z2: float, t2: float) -> float: + """ +The continuously compounded forward rate between two maturities: +`(z2 t2 - z1 t1) / (t2 - t1)`. + +This is the rate the curve implies for borrowing from `t1` to `t2`, and +it follows from no-arbitrage alone: investing to `t2` must pay the same +as investing to `t1` and rolling. It is far more volatile than the zero +rates it comes from, because it is a *difference* of two nearly equal +products -- a small error in a long zero rate becomes a large error in +the forward, which is why bootstrapped curves are usually smoothed +before forwards are read off them. + +Errors: +Returns an error for non-increasing maturities, a non-positive first +maturity, or a non-finite rate. + +Rust: `finance::rates::forward_rate` + """ + ... + +def nelson_siegel(t: float, b0: float, b1: float, b2: float, tau: float) -> float: + """ +The Nelson-Siegel zero rate at maturity `t`. + +`b0 + (b1 + b2) (1 - e^-x)/x - b2 e^-x` with `x = t/tau`. The three +coefficients are usually read as level, slope and curvature: `b0` is +the long rate the curve tends to, `b0 + b1` is the short rate it starts +from, and `b2` is a hump whose position `tau` sets. + +Four parameters is not many for a yield curve, and that is the point: +the shape cannot fit noise, so it smooths, and it extrapolates to a +finite long rate rather than diverging as a polynomial would. What it +cannot do is fit more than one hump, which is where the Svensson +extension with two decay terms is used instead. + +Errors: +Returns an error for a non-positive `tau`, a negative `t`, or a +non-finite coefficient. + +Rust: `finance::rates::nelson_siegel` + """ + ... + +def ns_fit(maturities: list[float], yields: list[float]) -> tuple[float, float, float, float]: + """ +Fits Nelson-Siegel to observed yields, returning `(b0, b1, b2, tau)`. + +For a fixed `tau` the model is *linear* in the three coefficients, so +the fit is a three-parameter least squares that solves exactly. Only +`tau` needs searching, and it is searched over a grid rather than by +gradient because the objective in `tau` is not convex and a local +method lands wherever it started. That split -- exact where the model +is linear, brute force where it is not -- is what makes this reliable +where a five-parameter nonlinear search is not. + +Errors: +Returns an error for fewer than four points, mismatched lengths, a +non-positive maturity, a non-finite value, or maturities that do not +determine the fit. + +Rust: `finance::rates::ns_fit` + """ + ... + +def vasicek_bond_price(r0: float, kappa: float, theta: float, sigma: float, t: float) -> float: + """ +The Vasicek zero-coupon bond price under `dr = kappa (theta - r) dt + +sigma dW`. + +`P(t) = A(t) e^(-B(t) r0)` with `B = (1 - e^(-kappa t))/kappa`. The +model is affine and Gaussian, which is what makes the price a closed +form and also what makes the rate able to go negative -- for decades +that was the standard objection to Vasicek, and since 2014 it has been +the reason to use it. + +The long-run mean of the *rate* is `theta`, but the long-run mean of +the yield is `theta - sigma^2/(2 kappa^2)`, lower by a convexity term +that grows with volatility. Discounting is convex in the rate, so +uncertainty about future rates makes bonds worth more than the average +rate alone would say. + +Errors: +Returns an error for a non-positive `kappa`, a negative `sigma`, a +negative maturity, or a non-finite parameter. + +Rust: `finance::rates::vasicek_bond_price` + """ + ... + +def cir_bond_price(r0: float, kappa: float, theta: float, sigma: float, t: float) -> float: + """ +The Cox-Ingersoll-Ross zero-coupon bond price under +`dr = kappa (theta - r) dt + sigma sqrt(r) dW`. + +The `sqrt(r)` diffusion is what keeps the rate non-negative: volatility +vanishes as the rate approaches zero, so the process cannot cross it. +Whether zero is even reached depends on the Feller condition +`2 kappa theta >= sigma^2` -- satisfied, the rate stays strictly +positive; violated, it touches zero and reflects. The price is still a +closed form either way, and `cir_feller_condition` reports which +regime the parameters are in. + +The formula raises a base tending to one to the power +`2 kappa theta / sigma^2`, so it loses precision as `sigma` shrinks: at +`sigma = 1e-6` the answer is off by about `1e-6` relative, which is a +thousand times larger than the convexity effect it is trying to +capture. `sigma = 0` is handled exactly by the deterministic limit; +between them, below roughly `1e-5`, the price is dominated by rounding +and `vasicek_bond_price` with a zero volatility is the better answer. + +Errors: +Returns an error for a negative initial rate, a non-positive `kappa` or +`theta`, a negative `sigma`, a negative maturity, or a non-finite +parameter. + +Rust: `finance::rates::cir_bond_price` + """ + ... + +def cir_feller_condition(kappa: float, theta: float, sigma: float) -> bool: + """ +Whether the Feller condition `2 kappa theta >= sigma^2` holds, which +decides whether a CIR rate can reach zero. + +Rust: `finance::rates::cir_feller_condition` + """ + ... + +def mortgage_payment(principal: float, rate: float, n: int) -> float: + """ +The level payment that repays `principal` over `n` periods at the +periodic rate `rate`. + +`P r / (1 - (1+r)^-n)`, which is the principal divided by the annuity +factor. At zero rate it degenerates to `P/n`, handled directly. + +Errors: +Returns an error for a non-positive principal, zero periods, more than +a hundred thousand periods, or a rate at or below `-100%`. + +Rust: `finance::rates::mortgage_payment` + """ + ... + +def amortization_schedule(principal: float, rate: float, n: int) -> list[tuple[float, float, float, float]]: + """ +The amortisation schedule as `(payment, interest, principal, balance)` +per period. + +The payment is level; what changes is its split. Early on almost all of +it is interest, because interest is charged on a balance that has +barely fallen, and the crossover to mostly-principal comes surprisingly +late -- past the halfway point of the term for any rate above a few +percent. That is the single most counterintuitive fact about a +mortgage and it falls straight out of the arithmetic. + +The final balance is forced to exactly zero, absorbing the accumulated +rounding into the last principal payment, which is what a lender does. + +Errors: +As `mortgage_payment`. + +Rust: `finance::rates::amortization_schedule` + """ + ... diff --git a/bindings/python/python/numeria/finance/risk.pyi b/bindings/python/python/numeria/finance/risk.pyi new file mode 100644 index 0000000..d6d7097 --- /dev/null +++ b/bindings/python/python/numeria/finance/risk.pyi @@ -0,0 +1,205 @@ +""" +Risk measurement: value at risk, expected shortfall, backtesting. # What value at risk does and does not tell you VaR at confidence `1 - alpha` is a *quantile*: the loss that will be exceeded on a fraction `alpha` of days. It says nothing whatever about how much worse things get beyond it, and that is not a subtlety but the central objection to the measure. Two portfolios with identical VaR can have completely different tails, and the one with the fatter tail is the one that ends the firm. `cvar_historical` -- expected shortfall -- answers the question VaR ducks: the *average* loss given that VaR is exceeded. It is also *coherent* where VaR is not: VaR can penalise diversification, saying a combined portfolio is riskier than the sum of its parts, because a quantile is not subadditive. Expected shortfall cannot do that. Since Basel III, expected shortfall is the regulatory measure and VaR is the one everyone still quotes. # Sign convention Every function here returns a **positive number for a loss**. A VaR of 0.023 means a 2.3% loss. This is the industry convention and it is the opposite of the return series' own sign, which is a standing source of confusion; the tests pin it down explicitly. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.stochastic.timeseries import Garch11 +from numeria.statistics.inference import TestResult + +class BacktestStats: + """ +What a backtest reports. + +Rust: `finance::risk::BacktestStats` + """ + def __init__(self, total_return: float, trades: int, win_rate: float, max_drawdown: float, equity: list[float]) -> None: ... + @property + def total_return(self) -> float: ... + @property + def trades(self) -> int: ... + @property + def win_rate(self) -> float: ... + @property + def max_drawdown(self) -> float: ... + @property + def equity(self) -> list[float]: ... + +def var_historical(returns: list[float], alpha: float) -> float: + """ +Historical value at risk: the empirical `alpha` quantile of the losses. + +No distributional assumption at all -- the sample *is* the +distribution. That is its strength and its limit: it cannot produce a +loss larger than the worst one observed, so a 99% VaR from two hundred +days is estimated from two points and a 99.9% VaR from none. + +Returned positive for a loss. + +Errors: +Returns an error for fewer than two returns, a non-finite value, or an +`alpha` outside `(0, 1)`. + +Rust: `finance::risk::var_historical` + """ + ... + +def var_parametric(returns: list[float], alpha: float) -> float: + """ +Parametric value at risk under a normal distribution: +`-(mean + z_alpha * deviation)`. + +Fits two moments and reads the quantile off a Gaussian. Financial +returns are not Gaussian -- they have fat tails and negative skew -- so +this understates the tail systematically, and by more the further out +you go. At 95% the error is modest; at 99.9% it is a factor. + +Returned positive for a loss. + +Errors: +Returns an error for fewer than two returns, a non-finite value, an +`alpha` outside `(0, 1)`, or a series with no variation. + +Rust: `finance::risk::var_parametric` + """ + ... + +def cvar_historical(returns: list[float], alpha: float) -> float: + """ +Historical expected shortfall: the mean loss among the worst `alpha` +fraction of returns. + +Always at least the VaR at the same level, and strictly greater +whenever the tail has any spread at all. Unlike VaR it is *coherent* -- +in particular subadditive, so combining two portfolios can never make +the measured risk exceed the sum of the parts. VaR has no such +guarantee and can and does penalise diversification. + +Returned positive for a loss. + +Errors: +As `var_historical`, plus an `alpha` so small that no observation +falls in the tail. + +Rust: `finance::risk::cvar_historical` + """ + ... + +def var_cornish_fisher(returns: list[float], alpha: float) -> float: + """ +Cornish-Fisher value at risk: the Gaussian quantile corrected for the +sample's skewness and excess kurtosis. + +The expansion adjusts `z` by terms in the third and fourth moments, +which is enough to capture the direction and rough size of a fat tail +without fitting a distribution. Two limitations are worth stating +plainly, because both bite at ordinary parameters. + +*The kurtosis term changes sign inside the tail.* Its factor is +`z^3 - 3z`, which is zero at `z = -sqrt(3)`, or `alpha` of about 4.2%. +So a fat-tailed sample gets a larger VaR at 1% and a *smaller* one at +5%, from the same correction. The expansion is meant for the far tail +and behaves sensibly there; near the 5% point the fourth-moment term +is doing something close to nothing, and just past it the wrong thing. + +*It is asymptotic, not convergent.* For mild moments it improves on +the Gaussian fit -- with a skew of -0.4 and an excess kurtosis of 0.8 +it moves a 1% VaR from 0.0197 to 0.0234 against a historical 0.0300. +For large ones it overshoots wildly: at a skew of -4.6 and an excess +kurtosis of 33.8 it returns 0.0729 where the sample's own 1% quantile +is 0.0309. There is no cheap test that separates the two, so the +moments must be checked before the answer is trusted. + +What *is* checked is the standard validity condition: the corrected +quantile must be increasing in `z`, since a quantile function that +decreases is not one. That catches the grossest failures and no more. + +Returned positive for a loss. + +Errors: +Returns an error for fewer than four returns, a non-finite value, an +`alpha` outside `(0, 1)`, a series with no variation, or moments large +enough to break the expansion. + +Rust: `finance::risk::var_cornish_fisher` + """ + ... + +def garch_var_forecast(model: Garch11 | Sequence[float], returns: list[float], alpha: float) -> float: + """ +A one-step-ahead parametric VaR from a fitted GARCH(1,1) model. + +Filters the conditional variance through the sample, projects one step +with `omega + alpha r_last^2 + beta sigma_last^2`, and reads a Gaussian +quantile off the result. The point is that VaR from a GARCH forecast +*responds*: after a volatile week it rises, where an unconditional +estimate over the same window barely moves. That responsiveness is +what a risk measure is for, and it is also why GARCH VaR breaches +cluster less than unconditional VaR breaches do. + +The Gaussian quantile still understates the tail; GARCH captures the +clustering of volatility, not the fatness of the conditional +distribution. + +Returned positive for a loss. + +Errors: +Returns an error for fewer than two returns, a non-finite value, an +`alpha` outside `(0, 1)`, or a model whose projected variance is not +positive. + +Rust: `finance::risk::garch_var_forecast` + """ + ... + +def backtest_sma_crossover(prices: list[float], fast: int, slow: int) -> BacktestStats: + """ +Backtests a moving-average crossover: long while the fast average is +above the slow one, flat otherwise. + +Both averages are computed on the closing prices up to and including +the current bar, and the resulting position is applied to the *next* +bar's return. Applying it to the same bar would use the close to decide +a trade executed at that close, which is the commonest way a backtest +invents returns that were never available. + +There are no costs, no slippage and no borrowing charge, so the result +is an upper bound on what the rule could have earned rather than an +estimate of it. A crossover rule trades often enough that realistic +costs frequently reverse its sign. + +Errors: +Returns an error for a non-positive price, fewer prices than the slow +window needs, a zero window, or a fast window at or above the slow one. + +Rust: `finance::risk::backtest_sma_crossover` + """ + ... + +def kupiec_test(violations: int, observations: int, alpha: float) -> TestResult: + """ +Kupiec's unconditional coverage test: does the observed breach count +match the VaR model's claimed `alpha`? + +The likelihood ratio statistic is chi-squared with one degree of +freedom under the null that breaches occur at exactly rate `alpha`. A +small p-value means the model is miscalibrated -- too many breaches +and it understates risk, too few and it overstates it and wastes +capital. + +What it cannot see is *clustering*. A model that breaches on ten +consecutive days and never again can pass Kupiec with the right total, +while being useless: the breaches should be independent, and testing +that needs Christoffersen's conditional coverage test, which this is +only half of. + +Errors: +Returns an error for no observations, more breaches than observations, +or an `alpha` outside `(0, 1)`. + +Rust: `finance::risk::kupiec_test` + """ + ... diff --git a/bindings/python/python/numeria/fluid_instabilities.pyi b/bindings/python/python/numeria/fluid_instabilities.pyi new file mode 100644 index 0000000..b091e5d --- /dev/null +++ b/bindings/python/python/numeria/fluid_instabilities.pyi @@ -0,0 +1,195 @@ +""" +When a fluid configuration stops being stable, and how fast it comes apart. Each entry here is a growth rate or a threshold. Rayleigh-Taylor for a heavy fluid over a light one, with the Atwood number and the most unstable wavelength; Kelvin-Helmholtz for a velocity shear; Rayleigh-Bénard convection through the Rayleigh number and its critical value; Plateau-Rayleigh for the breakup of a liquid column into drops; Richtmyer-Meshkov for a shock crossing an interface; and the Jeans criterion, which is the same instability applied to a self-gravitating gas cloud and so sets the mass at which a cloud collapses into a star. The Richardson number and its stability test cover stratified shear flow. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def rayleigh_taylor_growth_rate(g: float, atwood: float, wavenumber: float) -> float: + """ +Growth rate of the Rayleigh-Taylor instability: γ = √(A g k). + +`atwood` is the Atwood number A = (ρ₂ − ρ₁)/(ρ₂ + ρ₁), `g` the gravitational +acceleration, and `wavenumber` the perturbation wavenumber k. + +Rust: `fluid_instabilities::rayleigh_taylor_growth_rate` + """ + ... + +def atwood_number(density_heavy: float, density_light: float) -> float: + """ +Atwood number: A = (ρ_heavy − ρ_light) / (ρ_heavy + ρ_light). + +Ranges from −1 to 1. Positive when `density_heavy > density_light`. + +Rust: `fluid_instabilities::atwood_number` + """ + ... + +def rt_critical_wavelength(surface_tension: float, density_diff: float, g: float) -> float: + """ +Critical wavelength for capillary stabilization of RT instability: +λ_c = 2π √(σ / (Δρ g)). + +Perturbations shorter than λ_c are stabilized by surface tension σ. + +Rust: `fluid_instabilities::rt_critical_wavelength` + """ + ... + +def rt_most_unstable_wavelength(surface_tension: float, density_diff: float, g: float) -> float: + """ +Most unstable RT wavelength: λ_max = √3 × λ_c. + +Rust: `fluid_instabilities::rt_most_unstable_wavelength` + """ + ... + +def kh_growth_rate(density1: float, density2: float, velocity_diff: float, wavenumber: float) -> float: + """ +Growth rate of the Kelvin-Helmholtz instability: +γ = k |ΔV| √(ρ₁ρ₂) / (ρ₁ + ρ₂). + +Rust: `fluid_instabilities::kh_growth_rate` + """ + ... + +def kh_critical_velocity(density1: float, density2: float, surface_tension: float, wavenumber: float, g: float) -> float: + """ +Critical velocity difference for onset of KH instability: +ΔV_c² = (ρ₁ + ρ₂)/(ρ₁ρ₂) × [g(ρ₂ − ρ₁)/k + σk]. + +Rust: `fluid_instabilities::kh_critical_velocity` + """ + ... + +def rayleigh_number_thermal(g: float, beta: float, delta_t: float, height: float, kinematic_viscosity: float, thermal_diffusivity: float) -> float: + """ +Thermal Rayleigh number: Ra = g β ΔT H³ / (ν α). + +`beta` is the thermal expansion coefficient, `delta_t` the temperature +difference across the layer, `height` the layer thickness, `kinematic_viscosity` +is ν, and `thermal_diffusivity` is α. + +Rust: `fluid_instabilities::rayleigh_number_thermal` + """ + ... + +def critical_rayleigh_number() -> float: + """ +Critical Rayleigh number for rigid-rigid boundaries: Ra_c = 1708. + +Rust: `fluid_instabilities::critical_rayleigh_number` + """ + ... + +def nusselt_from_rayleigh(rayleigh: float) -> float: + """ +Nusselt number from Rayleigh number for turbulent natural convection +(simplified for air, Pr ≈ 0.71): Nu = 0.069 × Ra^(1/3). + +Returns 1.0 (pure conduction) when Ra < Ra_c. + +Rust: `fluid_instabilities::nusselt_from_rayleigh` + """ + ... + +def is_convecting(rayleigh: float) -> bool: + """ +Returns `true` if the Rayleigh number exceeds the critical value (Ra > 1708), +indicating onset of convective motion. + +Rust: `fluid_instabilities::is_convecting` + """ + ... + +def jeans_length(sound_speed: float, density: float) -> float: + """ +Jeans length: λ_J = c_s √(π / (G ρ)). + +Density perturbations larger than λ_J undergo gravitational collapse. + +Rust: `fluid_instabilities::jeans_length` + """ + ... + +def jeans_mass(sound_speed: float, density: float) -> float: + """ +Jeans mass: M_J = (π/6) ρ λ_J³. + +Rust: `fluid_instabilities::jeans_mass` + """ + ... + +def jeans_frequency(sound_speed: float, density: float) -> float: + """ +Jeans angular frequency: ω_J = √(4πGρ). + +This is the frequency at the Jeans wavenumber k_J where ω² = k²c_s² − 4πGρ = 0. + +Rust: `fluid_instabilities::jeans_frequency` + """ + ... + +def plateau_rayleigh_growth_rate(surface_tension: float, density: float, radius: float, wavenumber: float) -> float: + """ +Growth rate of the Plateau-Rayleigh instability (simplified): +γ² = (σ / (ρ r³)) × (kr)(1 − (kr)²). + +Valid for kr < 1 (long-wavelength regime). Returns 0 for kr ≥ 1. + +Rust: `fluid_instabilities::plateau_rayleigh_growth_rate` + """ + ... + +def plateau_rayleigh_critical_wavelength(radius: float) -> float: + """ +Critical wavelength for Plateau-Rayleigh instability: λ_c = 2πr. + +Perturbations with wavelength > λ_c (i.e., longer than the circumference) are unstable. + +Rust: `fluid_instabilities::plateau_rayleigh_critical_wavelength` + """ + ... + +def plateau_rayleigh_most_unstable(radius: float) -> float: + """ +Most unstable wavelength for Plateau-Rayleigh instability: λ_max ≈ 9.02 r. + +Rust: `fluid_instabilities::plateau_rayleigh_most_unstable` + """ + ... + +def richtmyer_meshkov_growth_rate(atwood: float, velocity_jump: float, wavenumber: float) -> float: + """ +Linear growth rate of the Richtmyer-Meshkov instability: +dh/dt = A × ΔV × k × h₀. + +Unlike RT, RM growth is linear (not exponential). Returns the velocity +of the perturbation amplitude growth for unit initial amplitude (h₀ = 1). + +Rust: `fluid_instabilities::richtmyer_meshkov_growth_rate` + """ + ... + +def richardson_number(g: float, density_gradient: float, density: float, velocity_gradient: float) -> float: + """ +Gradient Richardson number: Ri = g (dρ/dz) / (ρ (du/dz)²). + +`density_gradient` is dρ/dz and `velocity_gradient` is du/dz. + +Rust: `fluid_instabilities::richardson_number` + """ + ... + +def is_dynamically_stable(richardson: float) -> bool: + """ +Returns `true` if the gradient Richardson number exceeds the critical value +of 0.25, indicating dynamic stability against shear-driven turbulence. + +Rust: `fluid_instabilities::is_dynamically_stable` + """ + ... diff --git a/bindings/python/python/numeria/fluids.pyi b/bindings/python/python/numeria/fluids.pyi new file mode 100644 index 0000000..5839924 --- /dev/null +++ b/bindings/python/python/numeria/fluids.pyi @@ -0,0 +1,375 @@ +""" +Fluid statics and single-phase flow. Statics: hydrostatic pressure, buoyancy and flotation, Pascal's principle. Inviscid flow: continuity, Bernoulli, Torricelli, the Venturi meter. Viscous flow: Stokes drag, the drag equation and terminal velocity, Poiseuille's law, the Darcy-Weisbach head loss. Compressible flow: Mach number, stagnation and isentropic ratios. Surface tension, capillary rise, vorticity, circulation and the Kutta-Joukowski lift round it out, along with the dimensionless groups that decide which regime you are in -- Reynolds, Froude, Weber, Bond, Peclet, Marangoni, Archimedes. These are the closed-form relations. For flow solved on a grid or with particles see `cfd`. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.audio.envelope import Ar + +def hydrostatic_pressure(density: float, g: float, depth: float) -> float: + """ +Hydrostatic pressure: P = ρ * g * h + +Rust: `fluids::hydrostatic_pressure` + """ + ... + +def total_pressure(atmospheric_pressure: float, density: float, g: float, depth: float) -> float: + """ +Total pressure at depth: P = P_atm + ρ * g * h + +Rust: `fluids::total_pressure` + """ + ... + +def pascal_force(f1: float, a1: float, a2: float) -> float: + """ +Pascal's principle: F2 = F1 * (A2 / A1) + +Rust: `fluids::pascal_force` + """ + ... + +def pressure(force: float, area: float) -> float: + """ +Pressure: P = F / A + +Rust: `fluids::pressure` + """ + ... + +def buoyant_force(fluid_density: float, displaced_volume: float, g: float) -> float: + """ +Buoyant force (Archimedes' principle): F_b = ρ_fluid * V_displaced * g + +Rust: `fluids::buoyant_force` + """ + ... + +def fraction_submerged(object_density: float, fluid_density: float) -> float: + """ +Fraction of object submerged (floating): f = ρ_object / ρ_fluid + +Rust: `fluids::fraction_submerged` + """ + ... + +def apparent_weight(mass: float, object_volume: float, fluid_density: float, g: float) -> float: + """ +Apparent weight in fluid: W_app = W - F_b = mg - ρ_fluid * V * g + +Rust: `fluids::apparent_weight` + """ + ... + +def continuity_velocity(a1: float, v1: float, a2: float) -> float: + """ +Continuity equation: A1 * v1 = A2 * v2 → v2 = A1 * v1 / A2 + +Rust: `fluids::continuity_velocity` + """ + ... + +def flow_rate(area: float, velocity: float) -> float: + """ +Volume flow rate: Q = A * v + +Rust: `fluids::flow_rate` + """ + ... + +def mass_flow_rate(density: float, area: float, velocity: float) -> float: + """ +Mass flow rate: ṁ = ρ * A * v + +Rust: `fluids::mass_flow_rate` + """ + ... + +def bernoulli_pressure(p1: float, density: float, v1: float, h1: float, v2: float, h2: float, g: float) -> float: + """ +Bernoulli's equation: P1 + 0.5*ρ*v1^2 + ρ*g*h1 = P2 + 0.5*ρ*v2^2 + ρ*g*h2 +Returns P2 given all other quantities. + +Rust: `fluids::bernoulli_pressure` + """ + ... + +def torricelli_velocity(g: float, height: float) -> float: + """ +Torricelli's theorem: v = sqrt(2 * g * h) + +Rust: `fluids::torricelli_velocity` + """ + ... + +def venturi_velocity(p1: float, p2: float, density: float, a1: float, a2: float) -> float: + """ +Venturi effect velocity from pressure difference: +v2 = sqrt(2 * (P1 - P2) / (ρ * (1 - (A2/A1)^2))) + +Rust: `fluids::venturi_velocity` + """ + ... + +def drag_force(drag_coefficient: float, density: float, area: float, velocity: float) -> float: + """ +Drag force: F_d = 0.5 * C_d * ρ * A * v^2 + +Rust: `fluids::drag_force` + """ + ... + +def terminal_velocity(mass: float, g: float, density: float, area: float, drag_coefficient: float) -> float: + """ +Terminal velocity: v_t = sqrt(2 * m * g / (ρ * A * C_d)) + +Rust: `fluids::terminal_velocity` + """ + ... + +def stokes_drag(dynamic_viscosity: float, radius: float, velocity: float) -> float: + """ +Stokes' drag (low Reynolds number): F = 6π * μ * r * v + +Rust: `fluids::stokes_drag` + """ + ... + +def reynolds_number(density: float, velocity: float, length: float, dynamic_viscosity: float) -> float: + """ +Reynolds number: Re = ρ * v * L / μ + +Rust: `fluids::reynolds_number` + """ + ... + +def poiseuille_flow_rate(radius: float, pressure_drop: float, dynamic_viscosity: float, length: float) -> float: + """ +Poiseuille's law (volume flow rate through a pipe): +Q = π * r^4 * ΔP / (8 * μ * L) + +Rust: `fluids::poiseuille_flow_rate` + """ + ... + +def surface_tension_force(surface_tension: float, length: float) -> float: + """ +Surface tension force along a line: F = γ * L + +Rust: `fluids::surface_tension_force` + """ + ... + +def capillary_rise(surface_tension: float, contact_angle_rad: float, density: float, g: float, tube_radius: float) -> float: + """ +Capillary rise: h = 2 * γ * cos(θ) / (ρ * g * r) + +Rust: `fluids::capillary_rise` + """ + ... + +def mach_number(velocity: float, speed_of_sound: float) -> float: + """ +Mach number: M = v / a + +Rust: `fluids::mach_number` + """ + ... + +def dynamic_pressure(density: float, velocity: float) -> float: + """ +Dynamic pressure: q = ½ρv² + +Rust: `fluids::dynamic_pressure` + """ + ... + +def stagnation_pressure(static_pressure: float, dynamic_pressure: float) -> float: + """ +Stagnation pressure: P₀ = P + q + +Rust: `fluids::stagnation_pressure` + """ + ... + +def isentropic_pressure_ratio(mach: float, gamma: float) -> float: + """ +Isentropic pressure ratio: P/P₀ = (1 + (γ-1)/2 × M²)^(-γ/(γ-1)) + +Rust: `fluids::isentropic_pressure_ratio` + """ + ... + +def isentropic_temperature_ratio(mach: float, gamma: float) -> float: + """ +Isentropic temperature ratio: T/T₀ = (1 + (γ-1)/2 × M²)^(-1) + +Rust: `fluids::isentropic_temperature_ratio` + """ + ... + +def vorticity_2d(dvx_dy: float, dvy_dx: float) -> float: + """ +Vorticity in 2D: ω = ∂v_y/∂x - ∂v_x/∂y + +Rust: `fluids::vorticity_2d` + """ + ... + +def circulation(vorticity: float, area: float) -> float: + """ +Circulation (uniform vorticity): Γ = ω × A + +Rust: `fluids::circulation` + """ + ... + +def kutta_joukowski_lift(density: float, velocity: float, circulation: float) -> float: + """ +Kutta-Joukowski lift per unit span: L = ρ × V × Γ + +Rust: `fluids::kutta_joukowski_lift` + """ + ... + +def kinematic_viscosity(dynamic_viscosity: float, density: float) -> float: + """ +Kinematic viscosity: ν = μ/ρ + +Rust: `fluids::kinematic_viscosity` + """ + ... + +def pressure_gradient_pipe(flow_rate: float, dynamic_viscosity: float, radius: float, length: float) -> float: + """ +Pressure gradient in a pipe (Poiseuille inverse): dP = 8μLQ/(πr⁴) + +Rust: `fluids::pressure_gradient_pipe` + """ + ... + +def hydraulic_diameter(area: float, wetted_perimeter: float) -> float: + """ +Hydraulic diameter: D_h = 4A/P + +Rust: `fluids::hydraulic_diameter` + """ + ... + +def darcy_friction_factor_laminar(reynolds: float) -> float: + """ +Darcy friction factor for laminar pipe flow: f = 64/Re + +Rust: `fluids::darcy_friction_factor_laminar` + """ + ... + +def darcy_weisbach_head_loss(friction_factor: float, length: float, diameter: float, velocity: float, g: float) -> float: + """ +Darcy-Weisbach head loss: h_L = f × (L/D) × v²/(2g) + +Rust: `fluids::darcy_weisbach_head_loss` + """ + ... + +def buoyancy_velocity(g: float, beta: float, delta_temp: float, length: float) -> float: + """ +Characteristic buoyancy velocity: v = √(gβΔTL) + +Rust: `fluids::buoyancy_velocity` + """ + ... + +def thermal_expansion_coefficient_ideal_gas(temperature: float) -> float: + """ +Thermal expansion coefficient for ideal gas: β = 1/T + +Rust: `fluids::thermal_expansion_coefficient_ideal_gas` + """ + ... + +def viscosity_sutherland(mu0: float, t0: float, t: float, s: float) -> float: + """ +Sutherland's law for viscosity: μ = μ₀ × (T/T₀)^(3/2) × (T₀ + S)/(T + S) + +Rust: `fluids::viscosity_sutherland` + """ + ... + +def thermal_conductivity_gas(k0: float, t0: float, t: float, s: float) -> float: + """ +Sutherland's law for thermal conductivity (same form as viscosity) + +Rust: `fluids::thermal_conductivity_gas` + """ + ... + +def natural_convection_nu_vertical(rayleigh: float) -> float: + """ +Churchill-Chu correlation for natural convection on a vertical plate (air, Pr≈0.71): +Nu = (0.825 + 0.387 × Ra^(1/6) / 1.1936)² + +Rust: `fluids::natural_convection_nu_vertical` + """ + ... + +def natural_convection_nu_horizontal_hot(rayleigh: float) -> float: + """ +Natural convection Nusselt number for hot horizontal plate facing up: +Nu = 0.54 × Ra^(1/4), valid for 10⁴ ≤ Ra ≤ 10⁷ + +Rust: `fluids::natural_convection_nu_horizontal_hot` + """ + ... + +def marangoni_number(surface_tension_gradient: float, length: float, delta_temp: float, dynamic_viscosity: float, thermal_diffusivity: float) -> float: + """ +Marangoni number: Ma = -(dσ/dT) × L × ΔT / (μ × α) + +Rust: `fluids::marangoni_number` + """ + ... + +def bond_number(density_diff: float, g: float, length: float, surface_tension: float) -> float: + """ +Bond number: Bo = Δρ × g × L² / σ (gravity vs surface tension) + +Rust: `fluids::bond_number` + """ + ... + +def weber_number(density: float, velocity: float, length: float, surface_tension: float) -> float: + """ +Weber number: We = ρv²L / σ (inertia vs surface tension) + +Rust: `fluids::weber_number` + """ + ... + +def froude_number(velocity: float, g: float, length: float) -> float: + """ +Froude number: Fr = v / √(gL) (inertia vs gravity in free surface flow) + +Rust: `fluids::froude_number` + """ + ... + +def archimedes_number(density_fluid: float, density_particle: float, diameter: float, dynamic_viscosity: float, g: float) -> float: + """ +Archimedes number: Ar = g × d³ × ρf × (ρp - ρf) / μ² + +Rust: `fluids::archimedes_number` + """ + ... + +def peclet_number(velocity: float, length: float, diffusivity: float) -> float: + """ +Peclet number: Pe = vL/α (advection vs diffusion) + +Rust: `fluids::peclet_number` + """ + ... diff --git a/bindings/python/python/numeria/fractals/__init__.pyi b/bindings/python/python/numeria/fractals/__init__.pyi new file mode 100644 index 0000000..4b8cded --- /dev/null +++ b/bindings/python/python/numeria/fractals/__init__.pyi @@ -0,0 +1,130 @@ +""" +Fractals: escape-time sets, attractors, automata and noise. The module root holds the classic escape-time sets computed directly -- Mandelbrot with smooth (continuous) iteration counts, Julia, burning ship, Newton fractals and the Sierpinski gasket. The submodules generalise each direction: `escape_time` for a generic iteration engine, `attractors` for chaotic flows and maps, `ifs` for iterated function systems and the chaos game, `lsystem` for Lindenmayer rewriting, `automata` for cellular automata and growth, and `noise` for Perlin, OpenSimplex2, Worley and fBm. For the dynamical-systems view -- Lyapunov exponents and bifurcation -- see `nonlinear`. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from . import attractors, automata, escape_time, ifs, lsystem, noise + +def mandelbrot_iterations(c_re: float, c_im: float, max_iter: int) -> int: + """ +Mandelbrot escape-time iteration count: z_{n+1} = z_n² + c, returns iterations to escape |z| > 2. + +Rust: `fractals::mandelbrot_iterations` + """ + ... + +def mandelbrot_smooth(c_re: float, c_im: float, max_iter: int) -> float: + """ +Smooth Mandelbrot iteration count using continuous escape-time coloring. + +Rust: `fractals::mandelbrot_smooth` + """ + ... + +def mandelbrot_grid(x_min: float, x_max: float, y_min: float, y_max: float, width: int, height: int, max_iter: int) -> list[int]: + """ +Compute Mandelbrot iteration counts for an entire grid of pixels. + +Rust: `fractals::mandelbrot_grid` + """ + ... + +def julia_iterations(z_re: float, z_im: float, c_re: float, c_im: float, max_iter: int) -> int: + """ +Julia set escape-time iteration count for a fixed c: z_{n+1} = z_n² + c. + +Rust: `fractals::julia_iterations` + """ + ... + +def julia_grid(c_re: float, c_im: float, x_min: float, x_max: float, y_min: float, y_max: float, width: int, height: int, max_iter: int) -> list[int]: + """ +Compute Julia set iteration counts for an entire grid of pixels. + +Rust: `fractals::julia_grid` + """ + ... + +def burning_ship_iterations(c_re: float, c_im: float, max_iter: int) -> int: + """ +Burning Ship fractal iteration count: z_{n+1} = (|Re(z_n)| + i|Im(z_n)|)² + c. + +Rust: `fractals::burning_ship_iterations` + """ + ... + +def newton_fractal_iterations(z_re: float, z_im: float, max_iter: int, tolerance: float) -> tuple[int, int]: + """ +Newton fractal for f(z) = z³ - 1: returns (iterations, root_index) for convergence. + +Rust: `fractals::newton_fractal_iterations` + """ + ... + +def sierpinski_point(x: float, y: float, iterations: int) -> list[tuple[float, float]]: + """ +Generate Sierpinski triangle points via the chaos game (iterated function system). + +Rust: `fractals::sierpinski_point` + """ + ... + +def barnsley_fern_point(x: float, y: float, iterations: int) -> list[tuple[float, float]]: + """ +Generate Barnsley fern points via the iterated function system with four affine maps. + +Rust: `fractals::barnsley_fern_point` + """ + ... + +def box_count_2d(points: list[tuple[float, float]], grid_size: int, bounds: tuple[float, float, float, float]) -> int: + """ +Count occupied grid cells for box-counting fractal dimension estimation. + +Rust: `fractals::box_count_2d` + """ + ... + +def new(re: float, im: float) -> complex: + """ +Create a complex number from real and imaginary parts. + +Rust: `fractals::Complex::new` + """ + ... + +def norm_sq(z: complex) -> float: + """ +Squared norm (modulus squared): |z|² = re² + im² + +Rust: `fractals::Complex::norm_sq` + """ + ... + +def norm(z: complex) -> float: + """ +Norm (modulus): |z| = √(re² + im²) + +Rust: `fractals::Complex::norm` + """ + ... + +def arg(z: complex) -> float: + """ +Argument (phase angle): arg(z) = atan2(im, re) + +Rust: `fractals::Complex::arg` + """ + ... + +def conjugate(z: complex) -> complex: + """ +Complex conjugate: z* = re - im·i + +Rust: `fractals::Complex::conjugate` + """ + ... diff --git a/bindings/python/python/numeria/fractals/attractors/__init__.pyi b/bindings/python/python/numeria/fractals/attractors/__init__.pyi new file mode 100644 index 0000000..2d0d80d --- /dev/null +++ b/bindings/python/python/numeria/fractals/attractors/__init__.pyi @@ -0,0 +1,128 @@ +""" +Strange attractors: 3-D chaotic flows and 2-D chaotic maps with trajectory integration, Lyapunov spectra (Benettin renormalization), Kaplan-Yorke dimension, Poincaré sections, bifurcation diagrams, and dimension estimators (Grassberger-Procaccia correlation dimension, box counting, Rosenstein's largest-Lyapunov method, Feigenbaum ratios). +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from . import presets +from numeria.spatial.primitives import Rect +from numeria.math import Vec2 +from numeria.math import Vec3 + +class Attractor2Map: + """ +A discrete 2-D map x ← step(x). + +Rust: `fractals::attractors::Attractor2Map` + """ + def trajectory(self, x0: Vec2 | Sequence[float], n: int, burn_in: int) -> list[Vec2]: ... + def density_map(self, x0: Vec2 | Sequence[float], n: int, res: tuple[int, int], bounds: Rect) -> list[int]: ... + def lyapunov(self, x0: Vec2 | Sequence[float], n: int) -> float: ... + +class Attractor3: + """ +A 3-D autonomous flow ẋ = f(x) with a preferred time step. + +Rust: `fractals::attractors::Attractor3` + """ + def trajectory(self, x0: Vec3 | Sequence[float], n: int, method: Integrator) -> list[Vec3]: ... + def lyapunov_spectrum(self, x0: Vec3 | Sequence[float], n: int, dt: float) -> list[float]: ... + def poincare_section(self, x0: Vec3 | Sequence[float], n: int, plane: Plane) -> list[Vec3]: ... + def density_map(self, x0: Vec3 | Sequence[float], n: int, res: tuple[int, int], bounds: Rect) -> list[int]: ... + @property + def dt(self) -> float: ... + +class Integrator: + """ +Fixed-step integration schemes (`Rk45` takes two half steps and +keeps the fifth-order combination, giving adaptive-quality +accuracy at fixed cost). + +Rust: `fractals::attractors::Integrator` + """ + ... + +def kaplan_yorke_dimension(spectrum: list[float]) -> float: + """ +Kaplan-Yorke (Lyapunov) dimension of a spectrum: with exponents +sorted descending and k the largest index with Σ₁ᵏ λᵢ ≥ 0, +D = k + Σ₁ᵏ λᵢ / |λ_{k+1}|. + +Rust: `fractals::attractors::kaplan_yorke_dimension` + """ + ... + +def correlation_dimension(points: list[Vec3 | Sequence[float]], r_min: float, r_max: float, n_r: int) -> float: + """ +Grassberger-Procaccia correlation dimension: the least-squares +slope of ln C(r) against ln r over `n_r` log-spaced radii, where +C(r) is the fraction of point pairs closer than r. + +Panics: +Panics unless there are >= 100 points, `0 < r_min < r_max`, and +`n_r >= 2`. + +Rust: `fractals::attractors::correlation_dimension` + """ + ... + +def box_counting_dimension_3d(points: list[Vec3 | Sequence[float]], scales: list[float]) -> float: + """ +Box-counting dimension of a 3-D point set: slope of ln N(s) +versus ln(1/s) over the given box sizes. + +Panics: +Panics unless points and at least two positive scales are given. + +Rust: `fractals::attractors::box_counting_dimension_3d` + """ + ... + +def delay_embedding(series: list[float], dim: int, delay: int) -> list[list[float]]: + """ +Time-delay embedding: vectors [x(i), x(i+τ), ..., x(i+(m−1)τ)]. + +Panics: +Panics unless the series is long enough for one vector. + +Rust: `fractals::attractors::delay_embedding` + """ + ... + +def recurrence_plot(series: list[float], embed_dim: int, delay: int, eps: float) -> list[bool]: + """ +Recurrence plot: `R[i,j]` is true when embedded states i and j are +within `eps` (row-major over the n embedded points). + +Rust: `fractals::attractors::recurrence_plot` + """ + ... + +def largest_lyapunov_rosenstein(series: list[float], embed_dim: int, delay: int, mean_period: int, max_iter: int) -> float: + """ +Largest Lyapunov exponent from a scalar series by Rosenstein's +method: embed, pair each point with its nearest neighbor at +temporal distance > `mean_period`, and fit the slope of the mean +log divergence over `max_iter` steps. Returned per sample step. + +Panics: +Panics on a series too short for the embedding and tracking. + +Rust: `fractals::attractors::largest_lyapunov_rosenstein` + """ + ... + +def feigenbaum_estimate(bifurcations: list[float]) -> float: + """ +Feigenbaum δ estimate from successive bifurcation parameters: +δₙ = (bₙ₋₁ − bₙ₋₂)/(bₙ − bₙ₋₁) for the last triple. + +Panics: +Panics unless at least 3 bifurcation points are given. + +Rust: `fractals::attractors::feigenbaum_estimate` + """ + ... diff --git a/bindings/python/python/numeria/fractals/attractors/presets.pyi b/bindings/python/python/numeria/fractals/attractors/presets.pyi new file mode 100644 index 0000000..3e7193e --- /dev/null +++ b/bindings/python/python/numeria/fractals/attractors/presets.pyi @@ -0,0 +1,296 @@ +""" +Named systems: 3-D flows with customary parameters and time +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.fractals.attractors import Attractor2Map +from numeria.fractals.attractors import Attractor3 + +def lorenz(sigma: float, rho: float, beta: float) -> Attractor3: + """ +Lorenz 1963: ẋ = σ(y−x), ẏ = x(ρ−z) − y, ż = xy − βz. + +Rust: `fractals::attractors::presets::lorenz` + """ + ... + +def rossler(a: float, b: float, c: float) -> Attractor3: + """ +Rössler 1976: ẋ = −y−z, ẏ = x+ay, ż = b + z(x−c). + +Rust: `fractals::attractors::presets::rossler` + """ + ... + +def aizawa() -> Attractor3: + """ +Aizawa attractor (a sphere-wrapped scroll). + +Rust: `fractals::attractors::presets::aizawa` + """ + ... + +def thomas(b: float) -> Attractor3: + """ +Thomas' cyclically symmetric attractor: ẋ = sin y − bx, ... + +Rust: `fractals::attractors::presets::thomas` + """ + ... + +def chen() -> Attractor3: + """ +Chen 1999 (a = 35, b = 3, c = 28). + +Rust: `fractals::attractors::presets::chen` + """ + ... + +def lu() -> Attractor3: + """ +Lü 2002 (a = 36, b = 3, c = 20), bridging Lorenz and Chen. + +Rust: `fractals::attractors::presets::lu` + """ + ... + +def halvorsen(a: float) -> Attractor3: + """ +Halvorsen's cyclic attractor: ẋ = −ax − 4y − 4z − y². + +Rust: `fractals::attractors::presets::halvorsen` + """ + ... + +def sprott_b() -> Attractor3: + """ +Sprott case B: ẋ = yz, ẏ = x − y, ż = 1 − xy. + +Rust: `fractals::attractors::presets::sprott_b` + """ + ... + +def dadras() -> Attractor3: + """ +Dadras attractor. + +Rust: `fractals::attractors::presets::dadras` + """ + ... + +def rabinovich_fabrikant(a: float, g: float) -> Attractor3: + """ +Rabinovich-Fabrikant: ẋ = y(z − 1 + x²) + γx, ... + +Rust: `fractals::attractors::presets::rabinovich_fabrikant` + """ + ... + +def three_scroll() -> Attractor3: + """ +Three-scroll unified chaotic system (TSUCS-1). + +Rust: `fractals::attractors::presets::three_scroll` + """ + ... + +def arneodo() -> Attractor3: + """ +Arneodo-Coullet: ẋ = y, ẏ = z, ż = ax − by − z − x³ +with (a, b) = (5.5, 3.5). + +Rust: `fractals::attractors::presets::arneodo` + """ + ... + +def nose_hoover() -> Attractor3: + """ +Nosé-Hoover oscillator (Sprott A): ẋ = y, ẏ = −x + yz, +ż = 1 − y². + +Rust: `fractals::attractors::presets::nose_hoover` + """ + ... + +def four_wing() -> Attractor3: + """ +Four-wing attractor. + +Rust: `fractals::attractors::presets::four_wing` + """ + ... + +def chua(alpha: float, beta: float, m0: float, m1: float) -> Attractor3: + """ +Chua's circuit with the piecewise-linear diode +characteristic f(x) = m₁x + (m₀−m₁)(|x+1| − |x−1|)/2. + +Rust: `fractals::attractors::presets::chua` + """ + ... + +def rikitake() -> Attractor3: + """ +Rikitake two-disc dynamo (μ = 1, a = 5). + +Rust: `fractals::attractors::presets::rikitake` + """ + ... + +def duffing_forced(delta: float, alpha: float, beta: float, gamma: float, omega: float) -> Attractor3: + """ +Forced Duffing oscillator as an autonomous 3-D flow with +z = ωt: ẋ = y, ẏ = −δy − αx − βx³ + γ cos z, ż = ω. + +Rust: `fractals::attractors::presets::duffing_forced` + """ + ... + +def clifford(a: float, b: float, c: float, d: float) -> Attractor2Map: + """ +Clifford attractor: x' = sin(ay) + c cos(ax), ... + +Rust: `fractals::attractors::presets::clifford` + """ + ... + +def de_jong(a: float, b: float, c: float, d: float) -> Attractor2Map: + """ +Peter de Jong attractor. + +Rust: `fractals::attractors::presets::de_jong` + """ + ... + +def ikeda(u: float) -> Attractor2Map: + """ +Ikeda map with t = 0.4 − 6/(1 + x² + y²). + +Rust: `fractals::attractors::presets::ikeda` + """ + ... + +def tinkerbell(a: float, b: float, c: float, d: float) -> Attractor2Map: + """ +Tinkerbell map. + +Rust: `fractals::attractors::presets::tinkerbell` + """ + ... + +def gingerbreadman() -> Attractor2Map: + """ +Gingerbreadman map: x' = 1 − y + |x|, y' = x. + +Rust: `fractals::attractors::presets::gingerbreadman` + """ + ... + +def henon(a: float, b: float) -> Attractor2Map: + """ +Hénon map: x' = 1 − ax² + y, y' = bx. + +Rust: `fractals::attractors::presets::henon` + """ + ... + +def duffing_map(a: float, b: float) -> Attractor2Map: + """ +Duffing map: x' = y, y' = −bx + ay − y³. + +Rust: `fractals::attractors::presets::duffing_map` + """ + ... + +def bogdanov(eps: float, k: float, mu: float) -> Attractor2Map: + """ +Bogdanov map. + +Rust: `fractals::attractors::presets::bogdanov` + """ + ... + +def standard_map(k: float) -> Attractor2Map: + """ +Chirikov standard map on the torus [0, 2π)²: +p' = p + k sin θ, θ' = θ + p'. Area-preserving. + +Rust: `fractals::attractors::presets::standard_map` + """ + ... + +def gumowski_mira(a: float, b: float) -> Attractor2Map: + """ +Gumowski-Mira map with g(x) = ax + 2(1−a)x²/(1+x²). + +Rust: `fractals::attractors::presets::gumowski_mira` + """ + ... + +def hopalong(a: float, b: float, c: float) -> Attractor2Map: + """ +Barry Martin's hopalong: x' = y − sign(x)√|bx − c|, y' = a − x. + +Rust: `fractals::attractors::presets::hopalong` + """ + ... + +def bedhead(a: float, b: float) -> Attractor2Map: + """ +Bedhead attractor. + +Rust: `fractals::attractors::presets::bedhead` + """ + ... + +def svensson(a: float, b: float, c: float, d: float) -> Attractor2Map: + """ +Johnny Svensson's attractor. + +Rust: `fractals::attractors::presets::svensson` + """ + ... + +def fractal_dream(a: float, b: float, c: float, d: float) -> Attractor2Map: + """ +"Fractal dream" attractor. + +Rust: `fractals::attractors::presets::fractal_dream` + """ + ... + +def popcorn(h: float) -> Attractor2Map: + """ +Popcorn map: x' = x − h sin(y + tan 3y), ... + +Rust: `fractals::attractors::presets::popcorn` + """ + ... + +def arnold_cat() -> Attractor2Map: + """ +Arnold's cat map on the unit torus. + +Rust: `fractals::attractors::presets::arnold_cat` + """ + ... + +def baker() -> Attractor2Map: + """ +Baker's map on the unit square. + +Rust: `fractals::attractors::presets::baker` + """ + ... + +def zaslavskii(eps: float, nu: float, r: float) -> Attractor2Map: + """ +Zaslavskii map (ε forcing, ν rotation, r damping). + +Rust: `fractals::attractors::presets::zaslavskii` + """ + ... diff --git a/bindings/python/python/numeria/fractals/automata/__init__.pyi b/bindings/python/python/numeria/fractals/automata/__init__.pyi new file mode 100644 index 0000000..7a0d164 --- /dev/null +++ b/bindings/python/python/numeria/fractals/automata/__init__.pyi @@ -0,0 +1,652 @@ +""" +Cellular automata and growth models: elementary 1-D rules, life-like 2-D automata with pattern/RLE placement, cyclic CA, Langton's ant and turmites, Brian's Brain, Wireworld, 3-D life-like rules, SmoothLife and Lenia (direct convolution), abelian sandpiles, stochastic lattice models (forest fire, Greenberg-Hastings, majority/voter dynamics, Schelling segregation), reaction-diffusion systems (Gray-Scott, Gierer-Meinhardt, FitzHugh-Nagumo, Oregonator, Brusselator), and aggregation/percolation (DLA, Eden growth, invasion percolation). +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from . import patterns +from numeria.spatial.primitives import Rect +from numeria.monte_carlo import Rng + +class BelousovZhabotinsky: + """ +Two-variable Oregonator model of the Belousov-Zhabotinsky +reaction: ∂u = ∇²u + (u(1−u) − f·v(u−q)/(u+q))/ε, ∂v = ∇²v·Dᵥ + u − v. + +Rust: `fractals::automata::BelousovZhabotinsky` + """ + def __init__(self, w: int, h: int) -> None: ... + def step(self) -> None: ... + @property + def w(self) -> int: ... + @property + def h(self) -> int: ... + @property + def u(self) -> list[float]: ... + @property + def v(self) -> list[float]: ... + @property + def eps(self) -> float: ... + @property + def f(self) -> float: ... + @property + def q(self) -> float: ... + @property + def dv(self) -> float: ... + @property + def dt(self) -> float: ... + +class BriansBrain: + """ +Brian's Brain: three states (0 dead, 1 dying, 2 firing); a dead +cell fires with exactly two firing neighbors, firing cells decay +to dying, dying cells die. + +Rust: `fractals::automata::BriansBrain` + """ + def __init__(self, w: int, h: int) -> None: ... + def step(self) -> None: ... + @property + def w(self) -> int: ... + @property + def h(self) -> int: ... + @property + def cells(self) -> list[int]: ... + +class Brusselator: + """ +Brusselator: ∂u = Dᵤ∇²u + A − (B+1)u + u²v, ∂v = Dᵥ∇²v + Bu − u²v. + +Rust: `fractals::automata::Brusselator` + """ + def __init__(self, w: int, h: int, a: float, b: float, rng: Rng) -> None: ... + def step(self) -> None: ... + @property + def w(self) -> int: ... + @property + def h(self) -> int: ... + @property + def u(self) -> list[float]: ... + @property + def v(self) -> list[float]: ... + @property + def a(self) -> float: ... + @property + def b(self) -> float: ... + @property + def du(self) -> float: ... + @property + def dv(self) -> float: ... + @property + def dt(self) -> float: ... + +class Ca1D: + """ +Elementary (radius-1, 2-state) 1-D cellular automaton with a +Wolfram rule number. + +Rust: `fractals::automata::Ca1D` + """ + def __init__(self, rule: int, width: int, wrap: bool) -> None: ... + def seed_center(self) -> None: ... + def seed_random(self, rng: Rng, p: float) -> None: ... + def step(self) -> None: ... + def run(self, steps: int) -> list[list[bool]]: ... + def entropy(self) -> float: ... + def is_class4_heuristic(self) -> bool: ... + @property + def rule(self) -> int: ... + @property + def cells(self) -> list[bool]: ... + @property + def wrap(self) -> bool: ... + +class CyclicCa: + """ +Cyclic cellular automaton: state k advances to k+1 (mod states) +when at least `threshold` neighbors within Chebyshev `range` +carry the successor state; produces spiral waves. + +Rust: `fractals::automata::CyclicCa` + """ + def __init__(self, w: int, h: int, states: int, threshold: int, range: int, rng: Rng) -> None: ... + def step(self) -> None: ... + def run(self, n: int) -> None: ... + @property + def w(self) -> int: ... + @property + def h(self) -> int: ... + @property + def states(self) -> int: ... + @property + def cells(self) -> list[int]: ... + @property + def threshold(self) -> int: ... + @property + def range(self) -> int: ... + +class FitzHughNagumo: + """ +FitzHugh-Nagumo excitable medium: ∂v = D∇²v + v − v³/3 − w, +∂w = ε(v + a − b·w), with no-flux boundaries (wrapped copies +annihilate spirals). + +Rust: `fractals::automata::FitzHughNagumo` + """ + def __init__(self, w: int, h: int) -> None: ... + def spiral_wave_seed(self) -> None: ... + def step(self) -> None: ... + def run(self, n: int) -> None: ... + @property + def w(self) -> int: ... + @property + def h(self) -> int: ... + @property + def v(self) -> list[float]: ... + @property + def w_(self) -> list[float]: ... + @property + def a(self) -> float: ... + @property + def b(self) -> float: ... + @property + def eps(self) -> float: ... + @property + def d(self) -> float: ... + @property + def dt(self) -> float: ... + +class GrayScott: + """ +Gray-Scott reaction-diffusion: ∂u = Dᵤ∇²u − uv² + F(1−u), +∂v = Dᵥ∇²v + uv² − (F+k)v (Pearson 1993). + +Rust: `fractals::automata::GrayScott` + """ + def __init__(self, w: int, h: int, feed: float, kill: float) -> None: ... + @staticmethod + def mitosis(w: int, h: int) -> GrayScott: ... + @staticmethod + def coral(w: int, h: int) -> GrayScott: ... + @staticmethod + def spots(w: int, h: int) -> GrayScott: ... + @staticmethod + def worms(w: int, h: int) -> GrayScott: ... + @staticmethod + def maze(w: int, h: int) -> GrayScott: ... + @staticmethod + def holes(w: int, h: int) -> GrayScott: ... + @staticmethod + def waves(w: int, h: int) -> GrayScott: ... + @staticmethod + def solitons(w: int, h: int) -> GrayScott: ... + def seed_square(self, x: int, y: int, size: int) -> None: ... + def step(self) -> None: ... + def run(self, n: int) -> None: ... + @property + def w(self) -> int: ... + @property + def h(self) -> int: ... + @property + def u(self) -> list[float]: ... + @property + def v(self) -> list[float]: ... + @property + def du(self) -> float: ... + @property + def dv(self) -> float: ... + @property + def feed(self) -> float: ... + @property + def kill(self) -> float: ... + @property + def dt(self) -> float: ... + +class LangtonsAnt: + """ +Langton's ant generalized to multi-state turning rules ("RL" is +the classic ant; each letter gives the turn on a cell of that +color). + +Rust: `fractals::automata::LangtonsAnt` + """ + def __init__(self, w: int, h: int, rule: str) -> None: ... + def step(self) -> None: ... + def run(self, n: int) -> None: ... + def highway_detected(self) -> bool: ... + @property + def w(self) -> int: ... + @property + def h(self) -> int: ... + @property + def cells(self) -> list[int]: ... + @property + def pos(self) -> tuple[int, int]: ... + @property + def dir(self) -> int: ... + @property + def rule(self) -> str: ... + +class Lenia: + """ +Lenia (Chan 2019): continuous cellular automaton with a smooth +ring kernel and a Gaussian growth mapping, integrated by direct +convolution. + +Rust: `fractals::automata::Lenia` + """ + def __init__(self, w: int, h: int, radius: int, mu: float, sigma: float) -> None: ... + def step(self, dt: float) -> None: ... + @property + def w(self) -> int: ... + @property + def h(self) -> int: ... + @property + def field(self) -> list[float]: ... + @property + def radius(self) -> int: ... + @property + def kernel(self) -> list[float]: ... + @property + def mu(self) -> float: ... + @property + def sigma(self) -> float: ... + +class LifeLike: + """ +A life-like (outer-totalistic, Moore-neighborhood, 2-state) +automaton on a `w` × `h` grid. + +Rust: `fractals::automata::LifeLike` + """ + def __init__(self, w: int, h: int, cells: list[bool], birth: list[bool], survive: list[bool], wrap: bool) -> None: ... + @staticmethod + def from_rule_string(w: int, h: int, rule: str) -> LifeLike: ... + @staticmethod + def conway(w: int, h: int) -> LifeLike: ... + def step(self) -> None: ... + def run(self, n: int) -> None: ... + def population(self) -> int: ... + def place(self, x: int, y: int, pattern: list[str]) -> None: ... + def place_rle(self, x: int, y: int, rle: str) -> None: ... + def bounding_box(self) -> Optional[Rect]: ... + def detect_period(self, max_steps: int) -> Optional[int]: ... + def is_still_life(self) -> bool: ... + def to_string(self) -> str: ... + @property + def w(self) -> int: ... + @property + def h(self) -> int: ... + @property + def cells(self) -> list[bool]: ... + @property + def birth(self) -> list[bool]: ... + @property + def survive(self) -> list[bool]: ... + @property + def wrap(self) -> bool: ... + +class LifeLike3D: + """ +Life-like automaton on a 3-D grid with the 26-cell Moore +neighborhood and a B/S rule (e.g. "B5/S45" for Clouds-like +rules). + +Rust: `fractals::automata::LifeLike3D` + """ + def __init__(self, w: int, h: int, d: int, cells: list[bool], birth: list[bool], survive: list[bool]) -> None: ... + @staticmethod + def from_rule_string(w: int, h: int, d: int, rule: str) -> LifeLike3D: ... + def step(self) -> None: ... + def population(self) -> int: ... + @property + def w(self) -> int: ... + @property + def h(self) -> int: ... + @property + def d(self) -> int: ... + @property + def cells(self) -> list[bool]: ... + @property + def birth(self) -> list[bool]: ... + @property + def survive(self) -> list[bool]: ... + +class SmoothLife: + """ +SmoothLife: a continuous-state, continuous-neighborhood +generalization of Life, integrated by direct convolution (small +grids; no FFT dependency). + +Rust: `fractals::automata::SmoothLife` + """ + def __init__(self, w: int, h: int, params: SmoothLifeParams) -> None: ... + def step(self, dt: float) -> None: ... + @property + def w(self) -> int: ... + @property + def h(self) -> int: ... + @property + def field(self) -> list[float]: ... + @property + def params(self) -> SmoothLifeParams: ... + +class SmoothLifeParams: + """ +SmoothLife parameters (Rafler 2011): inner/outer disc radii and +the birth/death sigmoid intervals. + +Rust: `fractals::automata::SmoothLifeParams` + """ + def __init__(self, inner_radius: float, outer_radius: float, b1: float, b2: float, d1: float, d2: float, alpha_n: float, alpha_m: float) -> None: ... + @property + def inner_radius(self) -> float: ... + @property + def outer_radius(self) -> float: ... + @property + def b1(self) -> float: ... + @property + def b2(self) -> float: ... + @property + def d1(self) -> float: ... + @property + def d2(self) -> float: ... + @property + def alpha_n(self) -> float: ... + @property + def alpha_m(self) -> float: ... + +class Turing: + """ +Gierer-Meinhardt activator-inhibitor system: +∂a = Dₐ∇²a + a²/h − μa + ρ, ∂h = Dₕ∇²h + a² − νh. + +Rust: `fractals::automata::Turing` + """ + def __init__(self, w: int, h: int, rng: Rng) -> None: ... + def step(self) -> None: ... + @property + def w(self) -> int: ... + @property + def h(self) -> int: ... + @property + def activator(self) -> list[float]: ... + @property + def inhibitor(self) -> list[float]: ... + @property + def da(self) -> float: ... + @property + def dh(self) -> float: ... + @property + def mu(self) -> float: ... + @property + def nu(self) -> float: ... + @property + def rho(self) -> float: ... + @property + def dt(self) -> float: ... + +class Turmite: + """ +A turmite: a two-dimensional Turing machine on cell colors. The +transition table maps (machine state, cell color) to (color to +write, turn in quarter-turns clockwise, next state). + +Rust: `fractals::automata::Turmite` + """ + def __init__(self, w: int, h: int, table: list[list[tuple[int, int, int]]]) -> None: ... + def step(self) -> None: ... + def run(self, n: int) -> None: ... + @property + def w(self) -> int: ... + @property + def h(self) -> int: ... + @property + def cells(self) -> list[int]: ... + @property + def pos(self) -> tuple[int, int]: ... + @property + def dir(self) -> int: ... + @property + def state(self) -> int: ... + @property + def table(self) -> list[list[tuple[int, int, int]]]: ... + +class Wireworld: + """ +Wireworld: 0 empty, 1 electron head, 2 electron tail, +3 conductor. Heads become tails, tails become conductor, and a +conductor becomes a head with one or two neighboring heads. + +Rust: `fractals::automata::Wireworld` + """ + def __init__(self, w: int, h: int, cells: list[int]) -> None: ... + @staticmethod + def from_string(diagram: str) -> Wireworld: ... + def step(self) -> None: ... + def run(self, n: int) -> None: ... + def count_electrons(self) -> int: ... + @property + def w(self) -> int: ... + @property + def h(self) -> int: ... + @property + def cells(self) -> list[int]: ... + +def rule_table(rule: int) -> list[bool]: + """ +The rule as a lookup table indexed by the 3-bit neighborhood +(left·4 + center·2 + right). + +Rust: `fractals::automata::rule_table` + """ + ... + +def rule_is_additive(rule: int) -> bool: + """ +True for additive (XOR-linear) rules like 90, 150, 60: the rule +commutes with XOR of configurations. + +Rust: `fractals::automata::rule_is_additive` + """ + ... + +def rule_classify_wolfram(rule: int) -> int: + """ +Heuristic Wolfram classification of an elementary rule: +1 = dies out, 2 = periodic/fixed, 3 = chaotic (high sustained +block entropy), 4 = complex (intermediate, long transients). +Based on entropy and activity statistics from a random seed; the +boundary between classes 3 and 4 is inherently fuzzy. + +Rust: `fractals::automata::rule_classify_wolfram` + """ + ... + +def sandpile_abelian(grid: MutableSequence[int], w: int, h: int) -> int: + """ +Topples every cell with >= 4 grains (von Neumann neighbors, open +boundary: grains fall off the edge) until stable. Returns the +number of topplings. + +Panics: +Panics unless `grid.len() == w·h`. + +Rust: `fractals::automata::sandpile_abelian` + """ + ... + +def sandpile_identity(w: int, h: int) -> list[int]: + """ +Identity element of the abelian sandpile group on the w×h grid: +stabilize(2·δ − stabilize(2·δ)) with δ the all-6 configuration. +Adding it to any recurrent configuration and stabilizing returns +that configuration. + +Rust: `fractals::automata::sandpile_identity` + """ + ... + +def forest_fire(w: int, h: int, p_grow: float, p_lightning: float, steps: int, rng: Rng) -> list[list[int]]: + """ +Drossel-Schwabl forest fire: 0 empty, 1 tree, 2 burning. Burning +cells become empty; trees with a burning neighbor (or struck by +lightning with probability `p_lightning`) burn; empty cells grow +a tree with probability `p_grow`. Returns every generation. + +Panics: +Panics unless the grid is at least 3×3. + +Rust: `fractals::automata::forest_fire` + """ + ... + +def greenberg_hastings(w: int, h: int, states: int, steps: int, rng: Rng) -> list[list[int]]: + """ +Greenberg-Hastings excitable medium: state 0 rests, 1 fires, +2..states-1 are refractory. A resting cell fires when a von +Neumann neighbor fires; every other state advances and wraps to +rest. Returns every generation from a random start. + +Panics: +Panics unless the grid is at least 3×3 and `states >= 3`. + +Rust: `fractals::automata::greenberg_hastings` + """ + ... + +def majority_rule(cells: MutableSequence[bool], w: int, h: int, steps: int) -> None: + """ +Synchronous majority rule: each cell adopts the majority state of +its Moore neighborhood (including itself; ties keep the state). + +Panics: +Panics unless `cells.len() == w·h`. + +Rust: `fractals::automata::majority_rule` + """ + ... + +def voter_model(cells: MutableSequence[bool], w: int, h: int, steps: int, rng: Rng) -> None: + """ +Voter model: each step a random cell copies a random von Neumann +neighbor (`steps` single-cell updates). + +Panics: +Panics unless `cells.len() == w·h`. + +Rust: `fractals::automata::voter_model` + """ + ... + +def schelling_segregation(grid: MutableSequence[int], w: int, h: int, threshold: float, steps: int, rng: Rng) -> float: + """ +Schelling segregation: two agent types (1, 2) plus vacancies (0). +Unhappy agents (fewer than `threshold` same-type fraction among +occupied Moore neighbors) move to random vacancies. Returns the +final segregation index: the mean same-type fraction over +occupied neighbors of all agents (0.5 = mixed, 1 = segregated). + +Panics: +Panics unless `grid.len() == w·h` and threshold is in [0, 1]. + +Rust: `fractals::automata::schelling_segregation` + """ + ... + +def reaction_diffusion_1d(u: MutableSequence[float], v: MutableSequence[float], f: Callable[[float, float], tuple[float, float]], du: float, dv: float, dt: float, dx: float, steps: int) -> None: + """ +Generic 1-D two-species reaction-diffusion by forward Euler with +zero-flux boundaries: `f(u, v)` returns the two reaction rates. + +Panics: +Panics unless the arrays match and have at least 3 cells, and +`dx > 0`, `dt > 0`. + +Rust: `fractals::automata::reaction_diffusion_1d` + """ + ... + +def diffusion_limited_aggregation(w: int, h: int, particles: int, stickiness: float, rng: Rng) -> list[bool]: + """ +Diffusion-limited aggregation on a lattice: random walkers +launched from a circle stick to the growing cluster with the +given probability. Returns the cluster mask (row-major). + +Panics: +Panics unless the grid is at least 16×16 and stickiness is in +(0, 1]. + +Rust: `fractals::automata::diffusion_limited_aggregation` + """ + ... + +def eden_growth(w: int, h: int, steps: int, rng: Rng) -> list[bool]: + """ +Eden growth: repeatedly turns a random perimeter cell of the +cluster on (compact growth with a rough boundary). + +Panics: +Panics unless the grid is at least 8×8. + +Rust: `fractals::automata::eden_growth` + """ + ... + +def invasion_percolation(w: int, h: int, rng: Rng) -> list[bool]: + """ +Invasion percolation: cells get random strengths; growth always +invades the weakest perimeter cell, until the cluster touches a +boundary. Returns the invaded mask. + +Panics: +Panics unless the grid is at least 8×8. + +Rust: `fractals::automata::invasion_percolation` + """ + ... + +def percolation_cluster(grid: list[bool], w: int, h: int) -> tuple[list[int], bool]: + """ +Labels the 4-connected clusters of `grid` (labels start at 1; +0 = off) and reports whether any cluster spans top to bottom. + +Panics: +Panics unless `grid.len() == w·h`. + +Rust: `fractals::automata::percolation_cluster` + """ + ... + +def percolation_threshold_estimate(w: int, h: int, trials: int, rng: Rng) -> float: + """ +Site-percolation threshold estimate: cells are enabled in a +random order until a cluster spans top to bottom; the spanning +fraction, averaged over `trials`, estimates p_c ≈ 0.5927 on the +square lattice. + +Panics: +Panics unless the grid is at least 8×8 and `trials >= 1`. + +Rust: `fractals::automata::percolation_threshold_estimate` + """ + ... + +def dla_fractal_dimension(cluster: list[bool], w: int, h: int) -> float: + """ +Mass-radius fractal dimension of a cluster mask: the slope of +ln N(r) versus ln r, where N(r) counts cluster cells within +distance r of the cluster centroid (radii doubling from 3 up to +70% of the cluster extent, which avoids finite-size edge bias +that plagues box counting on sparse clusters). + +Panics: +Panics unless `cluster.len() == w·h` and the cluster has at +least 10 cells. + +Rust: `fractals::automata::dla_fractal_dimension` + """ + ... diff --git a/bindings/python/python/numeria/fractals/automata/patterns.pyi b/bindings/python/python/numeria/fractals/automata/patterns.pyi new file mode 100644 index 0000000..fee6b2f --- /dev/null +++ b/bindings/python/python/numeria/fractals/automata/patterns.pyi @@ -0,0 +1,97 @@ +""" +Classic Game of Life patterns in `.O` rows. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def glider() -> list[str]: + """ +Glider (travels (1, 1) every 4 generations). + +Rust: `fractals::automata::patterns::glider` + """ + ... + +def lwss() -> list[str]: + """ +Lightweight spaceship (travels (2, 0) every 4 generations). + +Rust: `fractals::automata::patterns::lwss` + """ + ... + +def gosper_gun() -> list[str]: + """ +Gosper glider gun (period 30, emits one glider per period). + +Rust: `fractals::automata::patterns::gosper_gun` + """ + ... + +def r_pentomino() -> list[str]: + """ +R-pentomino (long-lived methuselah). + +Rust: `fractals::automata::patterns::r_pentomino` + """ + ... + +def acorn() -> list[str]: + """ +Acorn (methuselah, stabilizes after 5206 generations). + +Rust: `fractals::automata::patterns::acorn` + """ + ... + +def diehard() -> list[str]: + """ +Diehard (vanishes after 130 generations). + +Rust: `fractals::automata::patterns::diehard` + """ + ... + +def pulsar() -> list[str]: + """ +Pulsar (period-3 oscillator). + +Rust: `fractals::automata::patterns::pulsar` + """ + ... + +def pentadecathlon() -> list[str]: + """ +Pentadecathlon (period-15 oscillator). + +Rust: `fractals::automata::patterns::pentadecathlon` + """ + ... + +def block() -> list[str]: + """ +Block (still life). + +Rust: `fractals::automata::patterns::block` + """ + ... + +def beehive() -> list[str]: + """ +Beehive (still life). + +Rust: `fractals::automata::patterns::beehive` + """ + ... + +def blinker() -> list[str]: + """ +Blinker (period-2 oscillator). + +Rust: `fractals::automata::patterns::blinker` + """ + ... diff --git a/bindings/python/python/numeria/fractals/escape_time.pyi b/bindings/python/python/numeria/fractals/escape_time.pyi new file mode 100644 index 0000000..340d703 --- /dev/null +++ b/bindings/python/python/numeria/fractals/escape_time.pyi @@ -0,0 +1,290 @@ +""" +Escape-time fractals: a generic iteration engine with smooth coloring, orbit traps, and distance estimation, the classic quadratic families (Mandelbrot, Julia, tricorn, burning ship), Newton/nova and magnet fractals, Lyapunov fractals, Buddhabrot accumulation, and perturbation iteration for deep zooms. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.spatial.primitives import Rect +from numeria.monte_carlo import Rng + +class EscapeParams: + """ +Iteration parameters. + +Rust: `fractals::escape_time::EscapeParams` + """ + def __init__(self, max_iter: int, bailout: float, compute_distance: bool, trap: Optional[OrbitTrap]) -> None: ... + @property + def max_iter(self) -> int: ... + @property + def bailout(self) -> float: ... + @property + def compute_distance(self) -> bool: ... + @property + def trap(self) -> Optional[OrbitTrap]: ... + +class EscapeResult: + """ +Result of iterating one point. + +Rust: `fractals::escape_time::EscapeResult` + """ + def __init__(self, iterations: int, escaped: bool, smooth: float, final_z: complex, distance: Optional[float], orbit_trap: Optional[float]) -> None: ... + @property + def iterations(self) -> int: ... + @property + def escaped(self) -> bool: ... + @property + def smooth(self) -> float: ... + @property + def final_z(self) -> complex: ... + @property + def distance(self) -> Optional[float]: ... + @property + def orbit_trap(self) -> Optional[float]: ... + +class OrbitTrap: + """ +Orbit trap shapes: the result records the minimum distance from +the orbit to the trap. + +Rust: `fractals::escape_time::OrbitTrap` + """ + ... + +def escape_time(f: Callable[[complex, complex], complex], z0: complex, c: complex, params: EscapeParams) -> EscapeResult: + """ +Iterates z ← f(z, c) until |z| exceeds the bailout, recording +smooth iteration counts and orbit-trap distances. `distance` is +`None` here (no derivative is tracked); use +`escape_time_with_derivative` for distance estimates. + +Panics: +Panics unless `max_iter >= 1` and `bailout > 1`. + +Rust: `fractals::escape_time::escape_time` + """ + ... + +def escape_time_with_derivative(f: Callable[[complex, complex], complex], df_dz: Callable[[complex, complex], complex], z0: complex, c: complex, params: EscapeParams) -> EscapeResult: + """ +Escape-time iteration that also tracks the parameter-space +derivative dz ← (∂f/∂z)·dz + 1 (the recurrence for sets like the +Mandelbrot set where c varies per pixel and z₀ is fixed), giving +the exterior distance estimate |z| ln|z| / |dz| on escape. + +Panics: +Panics unless `max_iter >= 1` and `bailout > 1`. + +Rust: `fractals::escape_time::escape_time_with_derivative` + """ + ... + +def mandelbrot(c: complex, params: EscapeParams) -> EscapeResult: + """ +The Mandelbrot set iteration z ← z² + c from z₀ = 0; tracks the +derivative for distance estimates when requested. + +Rust: `fractals::escape_time::mandelbrot` + """ + ... + +def multibrot(c: complex, power: float, params: EscapeParams) -> EscapeResult: + """ +The multibrot iteration z ← z^power + c from z₀ = 0. + +Panics: +Panics unless `power > 1`. + +Rust: `fractals::escape_time::multibrot` + """ + ... + +def julia(z: complex, c: complex, params: EscapeParams) -> EscapeResult: + """ +The Julia set iteration z ← z² + c from the given z; tracks the +dynamic-space derivative for distance estimates when requested. + +Rust: `fractals::escape_time::julia` + """ + ... + +def tricorn(c: complex, params: EscapeParams) -> EscapeResult: + """ +The tricorn (Mandelbar): z ← conj(z)² + c. + +Rust: `fractals::escape_time::tricorn` + """ + ... + +def burning_ship(c: complex, params: EscapeParams) -> EscapeResult: + """ +The burning ship: z ← (|Re z| + i |Im z|)² + c. + +Rust: `fractals::escape_time::burning_ship` + """ + ... + +def phoenix(z: complex, c: complex, p: complex, params: EscapeParams) -> EscapeResult: + """ +The Phoenix fractal: z_{n+1} = z_n² + c + p·z_{n−1}. + +Rust: `fractals::escape_time::phoenix` + """ + ... + +def newton_fractal(z: complex, poly: list[complex], params: EscapeParams) -> tuple[int, int]: + """ +Newton fractal for the polynomial with coefficients `poly` +(constant term first): iterates z ← z − p(z)/p′(z) and returns +the index of the root reached (roots sorted by real then +imaginary part) and the iteration count. Index `degree` (one past +the last root) marks failure to converge within `max_iter`. + +Panics: +Panics unless the polynomial has degree >= 2. + +Rust: `fractals::escape_time::newton_fractal` + """ + ... + +def nova_fractal(z: complex, c: complex, power: float, relax: float, params: EscapeParams) -> EscapeResult: + """ +Nova fractal: relaxed Newton iteration on z^power − 1 with an +added constant, z ← z − relax·(z^p − 1)/(p·z^{p−1}) + c. +`escaped = true` records convergence to a fixed point (|Δz| < +1e-9); `iterations` counts steps to convergence. + +Panics: +Panics unless `power > 1`. + +Rust: `fractals::escape_time::nova_fractal` + """ + ... + +def magnet_type1(c: complex, params: EscapeParams) -> EscapeResult: + """ +Magnet fractal type I: z ← ((z² + c − 1)/(2z + c − 2))². + +Rust: `fractals::escape_time::magnet_type1` + """ + ... + +def magnet_type2(c: complex, params: EscapeParams) -> EscapeResult: + """ +Magnet fractal type II: +z ← ((z³ + 3(c−1)z + (c−1)(c−2)) / (3z² + 3(c−2)z + (c−1)(c−2) + 1))². + +Rust: `fractals::escape_time::magnet_type2` + """ + ... + +def lyapunov_fractal(a: float, b: float, sequence: str, iterations: int, warmup: int) -> float: + """ +Lyapunov exponent of the forced logistic map x ← r·x(1−x) where r +alternates between `a` and `b` according to `sequence` (a string +of 'A's and 'B's, cycled). Negative values mark stability +(colored regions of Markus-Lyapunov fractals), positive chaos. + +Panics: +Panics unless the sequence is non-empty and made of A/B, and +`iterations >= 1`. + +Rust: `fractals::escape_time::lyapunov_fractal` + """ + ... + +def mandelbrot_period(c: complex, max_iter: int) -> Optional[int]: + """ +Period of the attracting cycle at parameter c, by iterating to +the attractor and then measuring the first return within 1e-9. +`None` when the orbit escapes or no cycle of period <= 64 is +found within `max_iter` settling steps. + +Rust: `fractals::escape_time::mandelbrot_period` + """ + ... + +def mandelbrot_in_main_cardioid(c: complex) -> bool: + """ +True inside the main cardioid, where the fixed point is +attracting: q(q + Re c − 1/4) < (Im c)²/4 with q = |c − 1/4|². + +Rust: `fractals::escape_time::mandelbrot_in_main_cardioid` + """ + ... + +def mandelbrot_in_period2_bulb(c: complex) -> bool: + """ +True inside the period-2 bulb |c + 1| < 1/4. + +Rust: `fractals::escape_time::mandelbrot_in_period2_bulb` + """ + ... + +def buddhabrot(samples: int, max_iter: int, res: tuple[int, int], bounds: Rect, rng: Rng) -> list[int]: + """ +Buddhabrot: accumulates the escape orbits of random starting +parameters into a `res.0` × `res.1` grid over `bounds` +(row-major, x fastest). + +Panics: +Panics on an empty grid or degenerate bounds. + +Rust: `fractals::escape_time::buddhabrot` + """ + ... + +def mandelbrot_boundary_points(n: int, rng: Rng) -> list[complex]: + """ +Random points near the Mandelbrot set boundary: rejection +sampling keeping parameters whose escape time falls in +[20, max_iter), i.e. neither deep exterior nor interior. + +Panics: +Panics unless `n >= 1`. + +Rust: `fractals::escape_time::mandelbrot_boundary_points` + """ + ... + +def perturbation_mandelbrot(center_hi: tuple[float, float], delta: complex, reference_orbit: list[complex], params: EscapeParams) -> EscapeResult: + """ +Perturbation iteration for deep zooms: iterates the offset +δ ← 2·Z_n·δ + δ² + δ₀ against a precomputed reference orbit +Z_n (the orbit of `center_hi`), so pixels near the reference +need only f64 offsets. The reference orbit must start at +Z_0 = c_ref (the first iterate of 0). When the reference orbit +is shorter than the escape time, iteration continues directly. + +Panics: +Panics on an empty reference orbit. + +Rust: `fractals::escape_time::perturbation_mandelbrot` + """ + ... + +def color_distance_estimate(r: EscapeResult, pixel_size: float) -> float: + """ +Distance-estimate shading: 0 on the set, saturating to 1 at a +few pixels away; d/pixel_size clamped to [0, 1]. Interior points +(no distance) shade to 0. + +Panics: +Panics unless `pixel_size > 0`. + +Rust: `fractals::escape_time::color_distance_estimate` + """ + ... + +def mandelbrot_reference_orbit(c: complex, max_iter: int) -> list[complex]: + """ +Reference orbit of the Mandelbrot iteration at c (Z_0 = c), +for `perturbation_mandelbrot`. Stops early on escape. + +Rust: `fractals::escape_time::mandelbrot_reference_orbit` + """ + ... diff --git a/bindings/python/python/numeria/fractals/ifs/__init__.pyi b/bindings/python/python/numeria/fractals/ifs/__init__.pyi new file mode 100644 index 0000000..94f99d9 --- /dev/null +++ b/bindings/python/python/numeria/fractals/ifs/__init__.pyi @@ -0,0 +1,82 @@ +""" +Iterated function systems: the chaos game and deterministic attractor construction (Barnsley, "Fractals Everywhere", 1988), Moran similarity dimension, collage error, a library of classic IFS presets in 2-D and 3-D, and Draves-style fractal flames. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from . import presets +from numeria.spatial.transform2d import Affine2 +from numeria.spatial.primitives import Polygon2 +from numeria.spatial.primitives import Rect +from numeria.monte_carlo import Rng +from numeria.math import Vec2 +from numeria.math import Vec3 + +class Ifs: + """ +A 2-D iterated function system: contractive affine maps with +selection probabilities. + +Rust: `fractals::ifs::Ifs` + """ + def __init__(self, maps: list[tuple[Affine2, float]]) -> None: ... + def chaos_game(self, n: int, burn_in: int, rng: Rng) -> list[Vec2]: ... + def chaos_game_colored(self, n: int, burn_in: int, rng: Rng) -> list[tuple[Vec2, int]]: ... + def deterministic(self, depth: int, seed: Polygon2) -> list[Polygon2]: ... + def deterministic_points(self, depth: int) -> list[Vec2]: ... + def similarity_dimension(self) -> Optional[float]: ... + def bounding_rect(self, n: int, rng: Rng) -> Rect: ... + def collage_error(self, target: list[Vec2 | Sequence[float]]) -> float: ... + def render_density(self, n: int, res: tuple[int, int], rng: Rng) -> list[int]: ... + @property + def maps(self) -> list[tuple[Affine2, float]]: ... + +class Ifs3: + """ +A 3-D IFS with affine maps stored as `Mat4`. + +Rust: `fractals::ifs::Ifs3` + """ + def __init__(self, maps: list[tuple[Mat4, float]]) -> None: ... + def chaos_game(self, n: int, burn_in: int, rng: Rng) -> list[Vec3]: ... + def deterministic_points(self, depth: int) -> list[Vec3]: ... + @property + def maps(self) -> list[tuple[Mat4, float]]: ... + +class Variation: + """ +The nonlinear variations of Draves & Reckase, "The Fractal Flame +Algorithm". Variations with free parameters use the fixed values +noted below; Julia uses the Ω = 0 branch so results are +deterministic. + +Rust: `fractals::ifs::Variation` + """ + ... + +def apply_variation(v: Variation, p: Vec2 | Sequence[float]) -> Vec2: + """ +Applies one flame variation to a point. Formulas follow the flame +paper's conventions: r = |p|, θ = atan2(x, y). + +Rust: `fractals::ifs::apply_variation` + """ + ... + +def fractal_flame(maps: list[tuple[Affine2, float, Variation]], n: int, rng: Rng) -> list[tuple[Vec2, float]]: + """ +Fractal flame chaos game: each step applies a randomly chosen +affine map followed by its variation, and blends a per-map color +coordinate c ← (c + cᵢ)/2 with cᵢ = i/(m−1). Returns points with +their color coordinates; non-finite excursions restart from the +origin. + +Panics: +Panics on an empty map list or non-positive total probability. + +Rust: `fractals::ifs::fractal_flame` + """ + ... diff --git a/bindings/python/python/numeria/fractals/ifs/presets.pyi b/bindings/python/python/numeria/fractals/ifs/presets.pyi new file mode 100644 index 0000000..e860ccc --- /dev/null +++ b/bindings/python/python/numeria/fractals/ifs/presets.pyi @@ -0,0 +1,157 @@ +""" +Classic IFS attractors. 2-D maps are written x' = ax + by + e, +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.fractals.ifs import Ifs +from numeria.fractals.ifs import Ifs3 + +def sierpinski() -> Ifs: + """ +Sierpinski triangle: three half-scale maps toward the corners +of an equilateral triangle. Dimension log 3 / log 2. + +Rust: `fractals::ifs::presets::sierpinski` + """ + ... + +def barnsley_fern() -> Ifs: + """ +Barnsley's fern (the classic four maps and probabilities). + +Rust: `fractals::ifs::presets::barnsley_fern` + """ + ... + +def koch() -> Ifs: + """ +Koch curve as four 1/3-scale similitudes. Dimension +log 4 / log 3. + +Rust: `fractals::ifs::presets::koch` + """ + ... + +def dragon() -> Ifs: + """ +Heighway dragon: z → (1+i)z/2 and z → 1 − (1−i)z/2. + +Rust: `fractals::ifs::presets::dragon` + """ + ... + +def levy() -> Ifs: + """ +Lévy C curve: z → wz and z → w̄z + (1 − w̄), w = (1+i)/2. + +Rust: `fractals::ifs::presets::levy` + """ + ... + +def maple_leaf() -> Ifs: + """ +Maple leaf (a well-known four-map collage). + +Rust: `fractals::ifs::presets::maple_leaf` + """ + ... + +def tree() -> Ifs: + """ +Symmetric fractal tree: trunk, two rotated branches, and a +crown copy. + +Rust: `fractals::ifs::presets::tree` + """ + ... + +def spiral() -> Ifs: + """ +Logarithmic spiral of copies: one strong rotation plus a +small displaced copy. + +Rust: `fractals::ifs::presets::spiral` + """ + ... + +def cantor_dust() -> Ifs: + """ +Cantor dust: four 1/3-scale copies at the unit square's +corners. Dimension log 4 / log 3. + +Rust: `fractals::ifs::presets::cantor_dust` + """ + ... + +def pythagoras_tree(angle: float) -> Ifs: + """ +Pythagoras tree with roof angle `angle`: the two square-to- +square similarities of the classic construction (the unit +square is the trunk). + +Panics: +Panics unless 0 < angle < π/2. + +Rust: `fractals::ifs::presets::pythagoras_tree` + """ + ... + +def sierpinski_carpet() -> Ifs: + """ +Sierpinski carpet: eight 1/3-scale copies (all but the +center). Dimension log 8 / log 3. + +Rust: `fractals::ifs::presets::sierpinski_carpet` + """ + ... + +def vicsek() -> Ifs: + """ +Vicsek fractal (plus sign): center and four edge cells at +1/3 scale. Dimension log 5 / log 3. + +Rust: `fractals::ifs::presets::vicsek` + """ + ... + +def menger_sponge_3d() -> Ifs3: + """ +Menger sponge: the twenty 1/3-scale cells of the cube that +survive (drop face centers and the body center). Dimension +log 20 / log 3. + +Rust: `fractals::ifs::presets::menger_sponge_3d` + """ + ... + +def sierpinski_tetrahedron_3d() -> Ifs3: + """ +Sierpinski tetrahedron: four half-scale maps toward the +vertices of a regular tetrahedron. Dimension 2. + +Rust: `fractals::ifs::presets::sierpinski_tetrahedron_3d` + """ + ... + +def pentagon_flake() -> Ifs: + """ +Pentaflake: five copies at the vertices of a regular pentagon +with contraction 1/(1+φ) = (3−√5)/2. Dimension +log 5 / log(1+φ). + +Rust: `fractals::ifs::presets::pentagon_flake` + """ + ... + +def hexaflake() -> Ifs: + """ +Hexaflake: six vertex copies plus the center at 1/3 scale. +Dimension log 7 / log 3. + +Rust: `fractals::ifs::presets::hexaflake` + """ + ... diff --git a/bindings/python/python/numeria/fractals/lsystem/__init__.pyi b/bindings/python/python/numeria/fractals/lsystem/__init__.pyi new file mode 100644 index 0000000..e16703e --- /dev/null +++ b/bindings/python/python/numeria/fractals/lsystem/__init__.pyi @@ -0,0 +1,117 @@ +""" +Lindenmayer systems: parallel string rewriting with simple, stochastic, and context-sensitive rules, 2-D and 3-D turtle interpretation of the ABOP alphabet (Prusinkiewicz & Lindenmayer, "The Algorithmic Beauty of Plants", 1990), and a library of classic presets. Parametric modules are out of scope: the rule set here covers character rewriting only. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from . import presets +from numeria.spatial.frame import Frame +from numeria.spatial.primitives import Rect +from numeria.spatial.primitives import Segment +from numeria.math import Vec2 + +class LSystem: + """ +A Lindenmayer system: axiom, production rules, the turtle turn +angle its drawings use (radians), and characters ignored during +context matching. + +Rust: `fractals::lsystem::LSystem` + """ + def __init__(self, axiom: str, angle_deg: float) -> None: ... + def rule(self, from_: str, to: str) -> LSystem: ... + def stochastic_rule(self, from_: str, options: list[tuple[float, str]]) -> LSystem: ... + @property + def axiom(self) -> str: ... + @property + def rules(self) -> list[Rule]: ... + @property + def angle(self) -> float: ... + @property + def ignore(self) -> list[str]: ... + +class Rule: + """ +A production rule. + +Rust: `fractals::lsystem::Rule` + """ + ... + +class Turtle2: + """ +2-D turtle interpreting the ABOP alphabet: `F`/`G` draw a step, +`f`/`g` move without drawing, `+`/`-` turn left/right by the +turn angle, `|` turns 180°, ``/`` push/pop state, `!` scales +the line width by `width_factor`. Other characters are ignored. + +Rust: `fractals::lsystem::Turtle2` + """ + def __init__(self, step: float, angle_deg: float) -> None: ... + def interpret(self, s: str) -> list[Segment2]: ... + def interpret_with_width(self, s: str) -> list[tuple[Segment2, float]]: ... + def bounds(self) -> Rect: ... + @property + def pos(self) -> Vec2: ... + @property + def heading(self) -> float: ... + @property + def step(self) -> float: ... + @property + def angle(self) -> float: ... + @property + def pen_down(self) -> bool: ... + @property + def line_width(self) -> float: ... + @property + def width_factor(self) -> float: ... + +class Turtle3: + """ +3-D turtle: the frame's local x axis is the heading, y the left +vector, z the up vector. `+`/`-` yaw about up, `&`/`^` pitch +about left, `\\`/`/` roll about the heading, `|` yaws 180°, +`F` draws, `f` moves, ``/`` push/pop, `!` tapers the radius. + +Rust: `fractals::lsystem::Turtle3` + """ + def __init__(self, step: float, angle_deg: float) -> None: ... + def interpret(self, s: str) -> list[Segment]: ... + def interpret_tree(self, s: str) -> list[tuple[Segment, float]]: ... + def to_mesh(self, s: str, base_radius: float, taper: float, segments: int) -> Mesh: ... + @property + def frame(self) -> Frame: ... + @property + def step(self) -> float: ... + @property + def angle(self) -> float: ... + @property + def radius(self) -> float: ... + @property + def taper(self) -> float: ... + +def lsystem_to_polylines(segments: list[Segment2]) -> list[list[Vec2]]: + """ +Chains segments that share endpoints into polylines (in drawing +order): a new polyline starts whenever the pen jumped. + +Rust: `fractals::lsystem::lsystem_to_polylines` + """ + ... + +def fractal_dimension_lsystem(ls: LSystem, iterations: int) -> float: + """ +Box-counting dimension estimate of the drawing produced by +`iterations` rewrites. The two grid resolutions are chosen so the +finest cell is no smaller than a turtle step — below that scale +every curve is one-dimensional and the count slope collapses to 1. + +Panics: +Panics if the drawing is empty or degenerate. + +Rust: `fractals::lsystem::fractal_dimension_lsystem` + """ + ... diff --git a/bindings/python/python/numeria/fractals/lsystem/presets.pyi b/bindings/python/python/numeria/fractals/lsystem/presets.pyi new file mode 100644 index 0000000..082f9b7 --- /dev/null +++ b/bindings/python/python/numeria/fractals/lsystem/presets.pyi @@ -0,0 +1,199 @@ +""" +Classic L-systems, mostly from ABOP. Angles are the turtle turn +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.fractals.lsystem import LSystem +from numeria.fractals.lsystem import Turtle3 + +def koch_curve() -> LSystem: + """ +Koch curve: F → F+F−−F+F at 60°. + +Rust: `fractals::lsystem::presets::koch_curve` + """ + ... + +def koch_snowflake() -> LSystem: + """ +Koch snowflake: the Koch rule on a triangle axiom. + +Rust: `fractals::lsystem::presets::koch_snowflake` + """ + ... + +def koch_island() -> LSystem: + """ +Quadratic Koch island (ABOP fig 1.7a): F → F+F−F−FF+F+F−F +on a square, 90°. + +Rust: `fractals::lsystem::presets::koch_island` + """ + ... + +def dragon() -> LSystem: + """ +Heighway dragon at 90°. + +Rust: `fractals::lsystem::presets::dragon` + """ + ... + +def hilbert() -> LSystem: + """ +Hilbert curve as an L-system (ABOP fig 1.11a), 90°. + +Rust: `fractals::lsystem::presets::hilbert` + """ + ... + +def peano() -> LSystem: + """ +Peano curve variant filling a square, 90°. + +Rust: `fractals::lsystem::presets::peano` + """ + ... + +def gosper() -> LSystem: + """ +Gosper flowsnake at 60° (F and G both draw). + +Rust: `fractals::lsystem::presets::gosper` + """ + ... + +def sierpinski_triangle() -> LSystem: + """ +Sierpinski triangle (F and G both draw), 120°. + +Rust: `fractals::lsystem::presets::sierpinski_triangle` + """ + ... + +def sierpinski_arrowhead() -> LSystem: + """ +Sierpinski arrowhead curve, 60°. + +Rust: `fractals::lsystem::presets::sierpinski_arrowhead` + """ + ... + +def levy_c() -> LSystem: + """ +Lévy C curve, 45°. + +Rust: `fractals::lsystem::presets::levy_c` + """ + ... + +def cantor() -> LSystem: + """ +Cantor set on a line: F draws, f skips the removed middle +third. + +Rust: `fractals::lsystem::presets::cantor` + """ + ... + +def plant_a() -> LSystem: + """ +ABOP fig 1.24a: F → F[+F]F[−F]F at 25.7°. + +Rust: `fractals::lsystem::presets::plant_a` + """ + ... + +def plant_b() -> LSystem: + """ +ABOP fig 1.24b: `F → F[+F]F[−F][F]` at 20°. + +Rust: `fractals::lsystem::presets::plant_b` + """ + ... + +def plant_c() -> LSystem: + """ +ABOP fig 1.24c: F → FF−[−F+F+F]+[+F−F−F] at 22.5°. + +Rust: `fractals::lsystem::presets::plant_c` + """ + ... + +def plant_d() -> LSystem: + """ +ABOP fig 1.24d: X → F[+X]F[−X]+X, F → FF at 20°. + +Rust: `fractals::lsystem::presets::plant_d` + """ + ... + +def plant_e() -> LSystem: + """ +ABOP fig 1.24e: `X → F[+X][−X]FX`, `F → FF` at 25.7°. + +Rust: `fractals::lsystem::presets::plant_e` + """ + ... + +def plant_f() -> LSystem: + """ +ABOP fig 1.24f: `X → F−[[X]+X]+F[+FX]−X`, `F → FF` at 22.5°. + +Rust: `fractals::lsystem::presets::plant_f` + """ + ... + +def tree_3d() -> LSystem: + """ +Simple 3-D tree: trunk then three tapered branches rolled +120° apart (interpret with `Turtle3`). + +Rust: `fractals::lsystem::presets::tree_3d` + """ + ... + +def bush_3d() -> LSystem: + """ +3-D bush after ABOP fig 1.25 (interpret with `Turtle3`). + +Rust: `fractals::lsystem::presets::bush_3d` + """ + ... + +def cesaro() -> LSystem: + """ +Cesàro curve: F → F+F−−F+F at 85°. + +Rust: `fractals::lsystem::presets::cesaro` + """ + ... + +def pentaplexity() -> LSystem: + """ +Pentaplexity (pentagonal flake curve), 36°. + +Rust: `fractals::lsystem::presets::pentaplexity` + """ + ... + +def penrose_lsystem() -> LSystem: + """ +Penrose P3 rhombus tiling as an L-system (the classic +M/N/O/P system, angle 36°; draw F). + +Rust: `fractals::lsystem::presets::penrose_lsystem` + """ + ... + +def hexagonal_gosper() -> LSystem: + """ +Hexagonal Gosper curve (two-symbol XF form), 60°. + +Rust: `fractals::lsystem::presets::hexagonal_gosper` + """ + ... diff --git a/bindings/python/python/numeria/fractals/noise.pyi b/bindings/python/python/numeria/fractals/noise.pyi new file mode 100644 index 0000000..ceceed2 --- /dev/null +++ b/bindings/python/python/numeria/fractals/noise.pyi @@ -0,0 +1,336 @@ +""" +Coherent noise: Perlin gradient noise (Perlin 2002), OpenSimplex2 (ported from K.jpg's reference implementation), value noise, Worley cellular noise, fractal combinators (fBm, turbulence, ridged and hybrid multifractals, domain warping, curl noise), and terrain synthesis (diamond-square, spectral synthesis, thermal and hydraulic erosion, void-and-cluster blue noise). +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.spatial.primitives import Aabb +from numeria.patterns.symmetry import Lattice +from numeria.spatial.primitives import Rect +from numeria.monte_carlo import Rng +from numeria.math import Vec2 +from numeria.math import Vec3 + +class ErosionParams: + """ +Hydraulic erosion droplet parameters (Beyer 2015-style droplet +simulation). + +Rust: `fractals::noise::ErosionParams` + """ + def __init__(self, inertia: float, capacity: float, min_capacity: float, erode_speed: float, deposit_speed: float, evaporate_speed: float, gravity: float, max_lifetime: int) -> None: ... + @property + def inertia(self) -> float: ... + @property + def capacity(self) -> float: ... + @property + def min_capacity(self) -> float: ... + @property + def erode_speed(self) -> float: ... + @property + def deposit_speed(self) -> float: ... + @property + def evaporate_speed(self) -> float: ... + @property + def gravity(self) -> float: ... + @property + def max_lifetime(self) -> int: ... + +class FbmParams: + """ +Fractional Brownian motion parameters. + +Rust: `fractals::noise::FbmParams` + """ + def __init__(self, octaves: int, lacunarity: float, gain: float, frequency: float, amplitude: float) -> None: ... + @property + def octaves(self) -> int: ... + @property + def lacunarity(self) -> float: ... + @property + def gain(self) -> float: ... + @property + def frequency(self) -> float: ... + @property + def amplitude(self) -> float: ... + +class GaborKernel: + """ +One Gabor kernel: a Gaussian-windowed cosine wave. + +Rust: `fractals::noise::GaborKernel` + """ + def __init__(self, center: Vec2 | Sequence[float], frequency: float, orientation: float, bandwidth: float, amplitude: float, phase: float) -> None: ... + @property + def center(self) -> Vec2: ... + @property + def frequency(self) -> float: ... + @property + def orientation(self) -> float: ... + @property + def bandwidth(self) -> float: ... + @property + def amplitude(self) -> float: ... + @property + def phase(self) -> float: ... + +class Metric: + """ +Distance metrics for Worley noise. + +Rust: `fractals::noise::Metric` + """ + ... + +class OpenSimplex2: + """ +OpenSimplex2 noise (the "faster" variant): visually isotropic +gradient noise on simplex-style lattices, in [-1, 1]. The 3-D +evaluator uses the ImproveXY lattice orientation; 4-D noise is +not ported — use `Perlin::noise_4d` when a fourth dimension is +needed. + +Rust: `fractals::noise::OpenSimplex2` + """ + def __init__(self, seed: int) -> None: ... + def noise_2d(self, x: float, y: float) -> float: ... + def noise_3d(self, x: float, y: float, z: float) -> float: ... + +class Perlin: + """ +Classic improved Perlin gradient noise (Perlin, "Improving +Noise", 2002) with a seeded permutation table. Values are in +[-1, 1] and zero at every integer lattice point. + +Rust: `fractals::noise::Perlin` + """ + def __init__(self, seed: int) -> None: ... + def noise_1d(self, x: float) -> float: ... + def noise_2d(self, x: float, y: float) -> float: ... + def noise_3d(self, x: float, y: float, z: float) -> float: ... + def noise_4d(self, x: float, y: float, z: float, w: float) -> float: ... + def gradient_2d(self, x: float, y: float) -> Vec2: ... + def gradient_3d(self, x: float, y: float, z: float) -> Vec3: ... + +class ValueNoise: + """ +Lattice value noise: random values at integer lattice points, +interpolated (quintic-smoothed bilinear, optional bicubic). + +Rust: `fractals::noise::ValueNoise` + """ + def __init__(self, seed: int) -> None: ... + def noise_2d(self, x: float, y: float) -> float: ... + def noise_3d(self, x: float, y: float, z: float) -> float: ... + def noise_2d_cubic(self, x: float, y: float) -> float: ... + +class Worley: + """ +Worley (cellular) noise: one feature point per grid cell of size +`cell`, hashed from the seed; F1/F2 are the distances to the +nearest and second-nearest feature points under `metric`. + +Rust: `fractals::noise::Worley` + """ + def __init__(self, seed: int, cell: float) -> None: ... + def f1_2d(self, x: float, y: float) -> float: ... + def f2_2d(self, x: float, y: float) -> float: ... + def f2_minus_f1_2d(self, x: float, y: float) -> float: ... + def f1_3d(self, x: float, y: float, z: float) -> float: ... + def f2_3d(self, x: float, y: float, z: float) -> float: ... + def cell_id_2d(self, x: float, y: float) -> int: ... + @property + def metric(self) -> Metric: ... + +def fbm_2d(n: Callable[[float, float], float], x: float, y: float, p: FbmParams) -> float: + """ +fBm: Σ amplitude·gainⁱ · n(frequency·lacunarityⁱ · x). + +Rust: `fractals::noise::fbm_2d` + """ + ... + +def fbm_3d(n: Callable[[float, float, float], float], x: float, y: float, z: float, p: FbmParams) -> float: + """ +3-D fBm. + +Rust: `fractals::noise::fbm_3d` + """ + ... + +def turbulence_2d(n: Callable[[float, float], float], x: float, y: float, p: FbmParams) -> float: + """ +Turbulence: fBm of |n| (Perlin 1985's marble basis). + +Rust: `fractals::noise::turbulence_2d` + """ + ... + +def ridged_multifractal_2d(n: Callable[[float, float], float], x: float, y: float, p: FbmParams, offset: float, gain: float) -> float: + """ +Musgrave's ridged multifractal: octaves of (offset − |n|)², +each weighted by the previous octave's signal. + +Rust: `fractals::noise::ridged_multifractal_2d` + """ + ... + +def hybrid_multifractal(n: Callable[[float, float, float], float], x: float, y: float, z: float, p: FbmParams, offset: float) -> float: + """ +Musgrave's hybrid multifractal (3-D): additive multifractal with +octave weights damped by the running product. + +Rust: `fractals::noise::hybrid_multifractal` + """ + ... + +def billow_2d(n: Callable[[float, float], float], x: float, y: float, p: FbmParams) -> float: + """ +Billow: fBm of 2|n| − 1 (puffy cloud look). + +Rust: `fractals::noise::billow_2d` + """ + ... + +def domain_warp_2d(n: Callable[[float, float], float], x: float, y: float, p: FbmParams, warp_strength: float, iterations: int) -> float: + """ +Iterated domain warping (Quilez): the sample point is repeatedly +displaced by an fBm offset field before the final evaluation. + +Rust: `fractals::noise::domain_warp_2d` + """ + ... + +def domain_warp_3d(n: Callable[[float, float, float], float], x: float, y: float, z: float, p: FbmParams, strength: float, iterations: int) -> float: + """ +3-D iterated domain warping. + +Rust: `fractals::noise::domain_warp_3d` + """ + ... + +def curl_noise_2d(n: Callable[[float, float], float], x: float, y: float, eps: float) -> Vec2: + """ +Divergence-free 2-D flow from a scalar noise potential: +v = (∂ψ/∂y, −∂ψ/∂x) by central differences. + +Panics: +Panics unless `eps > 0`. + +Rust: `fractals::noise::curl_noise_2d` + """ + ... + +def curl_noise_3d(n: Callable[[float, float, float], float], x: float, y: float, z: float, eps: float) -> Vec3: + """ +Divergence-free 3-D flow: curl of a vector potential whose three +components are offset copies of `n` (Bridson et al. 2007). + +Panics: +Panics unless `eps > 0`. + +Rust: `fractals::noise::curl_noise_3d` + """ + ... + +def noise_field_2d(n: Callable[[float, float], float], bounds: Rect, w: int, h: int) -> ScalarField2: + """ +Samples a noise function into a scalar field. + +Rust: `fractals::noise::noise_field_2d` + """ + ... + +def noise_field_3d(n: Callable[[float, float, float], float], bounds: Aabb, res: tuple[int, int, int]) -> ScalarField3: + """ +Samples a noise function into a 3-D scalar field. + +Rust: `fractals::noise::noise_field_3d` + """ + ... + +def terrain_heightmap(seed: int, w: int, h: int, p: FbmParams, erosion_iters: int) -> list[float]: + """ +fBm heightmap (row-major, `w` × `h`) with optional thermal +erosion: material moves down slopes exceeding the talus angle, +smoothing scree until the terrain settles. + +Panics: +Panics unless the grid has at least 2×2 samples. + +Rust: `fractals::noise::terrain_heightmap` + """ + ... + +def hydraulic_erosion(height: MutableSequence[float], w: int, h: int, droplets: int, rng: Rng, params: ErosionParams) -> None: + """ +Simulates `droplets` water droplets over the heightmap, eroding +and depositing material along their paths. + +Panics: +Panics unless the grid is at least 3×3 and `height.len() == w·h`. + +Rust: `fractals::noise::hydraulic_erosion` + """ + ... + +def diamond_square(size_pow2: int, roughness: float, seed: int) -> list[float]: + """ +Diamond-square (plasma) fractal heightmap on a +(2^size_pow2 + 1)² grid, row-major, roughness halving the random +amplitude at each subdivision. + +Panics: +Panics unless `1 <= size_pow2 <= 12`. + +Rust: `fractals::noise::diamond_square` + """ + ... + +def spectral_synthesis_2d(w: int, h: int, beta: float, seed: int) -> list[float]: + """ +1/f^β spectral synthesis by direct summation of 64 random plane +waves with amplitudes f^{−β/2} (row-major, values roughly in +[-1, 1] after normalization). + +Panics: +Panics unless the grid has at least 2×2 samples. + +Rust: `fractals::noise::spectral_synthesis_2d` + """ + ... + +def white_noise_2d(seed: int, x: float, y: float) -> float: + """ +Stateless hash white noise in [-1, 1]: the same (seed, x, y) +always yields the same value, with no correlation between +nearby inputs. + +Rust: `fractals::noise::white_noise_2d` + """ + ... + +def blue_noise_texture(w: int, h: int, seed: int) -> list[float]: + """ +Void-and-cluster blue-noise threshold texture (Ulichney 1993): +returns ranks normalized to [0, 1), toroidally tileable; every +rank appears exactly once. + +Panics: +Panics unless `w·h >= 4` (and `w, h >= 2`). + +Rust: `fractals::noise::blue_noise_texture` + """ + ... + +def gabor_noise_2d(x: float, y: float, kernels: list[GaborKernel]) -> float: + """ +Sparse Gabor noise: the sum of the kernels at (x, y) +(Lagae et al. 2009 with an explicit kernel list). + +Rust: `fractals::noise::gabor_noise_2d` + """ + ... diff --git a/bindings/python/python/numeria/general_relativity.pyi b/bindings/python/python/numeria/general_relativity.pyi new file mode 100644 index 0000000..c627055 --- /dev/null +++ b/bindings/python/python/numeria/general_relativity.pyi @@ -0,0 +1,213 @@ +""" +General relativity: black holes and cosmology. The Schwarzschild solution -- the metric components, the horizon at `r_s = 2GM/c²`, proper time and gravitational redshift, the photon sphere at `1.5 r_s` and the innermost stable circular orbit at `3 r_s`, with the effective potential and the orbital energy and angular momentum that produce them. The Kerr solution adds rotation: the horizon, the ergosphere, the shifted ISCO, and the frame-dragging rate. Cosmology covers the Friedmann equation for the Hubble parameter, the critical density, redshift-distance relations, luminosity distance, lookback time, and the scale factor and CMB temperature at a given redshift. For four-vectors and curved-spacetime tensor machinery see `manifold::spacetime` and `manifold::metric`. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.spatial.frame import Frame + +def schwarzschild_radius(mass: float) -> float: + """ +Schwarzschild radius: r_s = 2GM/c² + +Rust: `general_relativity::schwarzschild_radius` + """ + ... + +def event_horizon_radius(mass: float) -> float: + """ +Event horizon radius (alias for schwarzschild_radius). + +Rust: `general_relativity::event_horizon_radius` + """ + ... + +def schwarzschild_metric_tt(mass: float, r: float) -> float: + """ +Time-time component of the Schwarzschild metric: g_tt = -(1 - r_s/r) + +Rust: `general_relativity::schwarzschild_metric_tt` + """ + ... + +def schwarzschild_metric_rr(mass: float, r: float) -> float: + """ +Radial-radial component of the Schwarzschild metric: g_rr = 1/(1 - r_s/r) + +Rust: `general_relativity::schwarzschild_metric_rr` + """ + ... + +def proper_time_factor(mass: float, r: float) -> float: + """ +Proper time factor: dτ/dt = √(1 - r_s/r) + +Rust: `general_relativity::proper_time_factor` + """ + ... + +def gravitational_redshift_factor(mass: float, r_emit: float, r_obs: float) -> float: + """ +Gravitational redshift factor between emitter at r_emit and observer at r_obs: +z_factor = √((1 - r_s/r_obs) / (1 - r_s/r_emit)) + +Rust: `general_relativity::gravitational_redshift_factor` + """ + ... + +def isco_radius(mass: float) -> float: + """ +Innermost stable circular orbit for Schwarzschild: r_isco = 3 r_s = 6GM/c² + +Rust: `general_relativity::isco_radius` + """ + ... + +def photon_sphere_radius(mass: float) -> float: + """ +Photon sphere radius: r_ph = 1.5 r_s = 3GM/c² + +Rust: `general_relativity::photon_sphere_radius` + """ + ... + +def kerr_event_horizon(mass: float, spin: float) -> float: + """ +Kerr outer event horizon: r+ = GM/c² + √((GM/c²)² - a²) +where a is the spin parameter (dimensions of length). + +Rust: `general_relativity::kerr_event_horizon` + """ + ... + +def kerr_ergosphere_radius(mass: float, spin: float, theta: float) -> float: + """ +Kerr ergosphere radius at polar angle theta: +r_ergo = GM/c² + √((GM/c²)² - a²cos²θ) + +Rust: `general_relativity::kerr_ergosphere_radius` + """ + ... + +def kerr_isco(mass: float, spin: float, prograde: bool) -> float: + """ +ISCO radius for a Kerr black hole using the exact Bardeen-Press-Teukolsky formula. +`spin` is the dimensionless spin parameter a/M (in geometric units, a* = Jc/(GM²)). +`prograde` selects co-rotating (true) or counter-rotating (false) orbits. +Returns the ISCO in metres. + +Rust: `general_relativity::kerr_isco` + """ + ... + +def frame_dragging_rate(mass: float, spin: float, r: float) -> float: + """ +Frame-dragging angular velocity (weak-field / Lense-Thirring limit): +Ω = 2GMa / (c²r³) +Here `spin` is the spin parameter a with dimensions of length. + +Rust: `general_relativity::frame_dragging_rate` + """ + ... + +def geodesic_acceleration_schwarzschild(mass: float, r: float, dr_dtau: float, l: float) -> float: + """ +Radial geodesic acceleration in Schwarzschild spacetime (effective potential approach): +d²r/dτ² = -GM/r² + l²(r - 3GM/c²) / r⁴ +where l is the specific angular momentum (per unit mass). + +Rust: `general_relativity::geodesic_acceleration_schwarzschild` + """ + ... + +def effective_potential_schwarzschild(mass: float, r: float, l: float, particle_mass: float) -> float: + """ +Effective potential for a massive particle in Schwarzschild spacetime: +V_eff = -GMm/r + l²/(2mr²) - GMl²/(mc²r³) + +Rust: `general_relativity::effective_potential_schwarzschild` + """ + ... + +def circular_orbit_energy(mass: float, r: float) -> float: + """ +Specific energy of a circular orbit in Schwarzschild: +E/(mc²) = (1 - 2GM/(rc²)) / √(1 - 3GM/(rc²)) + +Rust: `general_relativity::circular_orbit_energy` + """ + ... + +def circular_orbit_angular_momentum(mass: float, r: float) -> float: + """ +Specific angular momentum of a circular orbit in Schwarzschild: +L/(mc) = r √(GM / (r c² - 3GM)) → simplified from the exact expression. +Returns L/(mc) (dimensionless when r and GM/c² share length units, but here in SI +it carries dimensions of length). + +Rust: `general_relativity::circular_orbit_angular_momentum` + """ + ... + +def friedmann_hubble(density: float, curvature: float, cosmological_constant: float) -> float: + """ +Friedmann equation (flat universe, matter-dominated): +H = √(8πGρ/3) +For the full form with curvature and cosmological constant the caller should +construct the effective density; this returns the Hubble parameter for a given +energy density. + +Rust: `general_relativity::friedmann_hubble` + """ + ... + +def critical_density(hubble: float) -> float: + """ +Critical density of the universe: ρ_c = 3H² / (8πG) + +Rust: `general_relativity::critical_density` + """ + ... + +def cosmological_redshift_distance(redshift: float, hubble: float) -> float: + """ +Comoving distance via Hubble's law (valid for z << 1): d ≈ cz / H₀ + +Rust: `general_relativity::cosmological_redshift_distance` + """ + ... + +def luminosity_distance(redshift: float, hubble: float) -> float: + """ +Luminosity distance (first-order expansion): d_L = (c/H₀) z (1 + z/2) + +Rust: `general_relativity::luminosity_distance` + """ + ... + +def lookback_time(redshift: float, hubble: float) -> float: + """ +Lookback time (matter-dominated approximation): t ≈ z / (H₀ (1+z)) + +Rust: `general_relativity::lookback_time` + """ + ... + +def scale_factor_from_redshift(redshift: float) -> float: + """ +Scale factor from cosmological redshift: a = 1/(1+z) + +Rust: `general_relativity::scale_factor_from_redshift` + """ + ... + +def temperature_at_redshift(t0: float, redshift: float) -> float: + """ +CMB temperature at a given redshift: T = T₀ (1+z) + +Rust: `general_relativity::temperature_at_redshift` + """ + ... diff --git a/bindings/python/python/numeria/geometry/__init__.pyi b/bindings/python/python/numeria/geometry/__init__.pyi new file mode 100644 index 0000000..c691a95 --- /dev/null +++ b/bindings/python/python/numeria/geometry/__init__.pyi @@ -0,0 +1,265 @@ +""" +Areas, volumes and surface areas of the standard shapes. Plane figures (circle, ellipse, triangle by base-height and by Heron's formula, regular polygon, sector, annulus) and solids (sphere, cylinder, cone, ellipsoid, torus, frustum, capsule), with perimeters and surface areas alongside. Closed-form mensuration only. For triangle solving see `trigonometry`, for curves and conics see `curves`, for polygon algorithms such as triangulation and offsetting see `patterns::polygon_ops`, and for meshes see `mesh`. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from . import delaunay, geodesy, hull, mesh +from numeria.geometry.geodesy import Ellipsoid as Ellipsoid +from numeria.mesh import Mesh as Mesh +from numeria.geometry.mesh import RayHit as RayHit +from numeria.geometry.delaunay import circumcircle as circumcircle +from numeria.geometry.hull import convex_hull_2d as convex_hull_2d +from numeria.geometry.hull import convex_hull_3d as convex_hull_3d +from numeria.geometry.delaunay import delaunay_2d as delaunay_2d +from numeria.geometry.geodesy import ecef_to_enu as ecef_to_enu +from numeria.geometry.geodesy import ecef_to_geodetic as ecef_to_geodetic +from numeria.geometry.geodesy import geodetic_to_ecef as geodetic_to_ecef +from numeria.geometry.hull import point_in_polygon as point_in_polygon +from numeria.geometry.hull import polygon_area_signed as polygon_area_signed +from numeria.geometry.geodesy import vincenty_direct as vincenty_direct +from numeria.geometry.geodesy import vincenty_inverse as vincenty_inverse +from numeria.geometry.delaunay import voronoi_cells_2d as voronoi_cells_2d + +def area_circle(radius: float) -> float: + """ +Area of a circle: A = πr² + +Rust: `geometry::area_circle` + """ + ... + +def area_ellipse(semi_major: float, semi_minor: float) -> float: + """ +Area of an ellipse: A = πab + +Rust: `geometry::area_ellipse` + """ + ... + +def area_triangle(base: float, height: float) -> float: + """ +Area of a triangle: A = bh/2 + +Rust: `geometry::area_triangle` + """ + ... + +def area_triangle_heron(a: float, b: float, c: float) -> float: + """ +Area of a triangle via Heron's formula: A = √(s(s-a)(s-b)(s-c)) where s = (a+b+c)/2 + +Rust: `geometry::area_triangle_heron` + """ + ... + +def area_regular_polygon(n_sides: int, side_length: float) -> float: + """ +Area of a regular polygon: A = n·s²/(4·tan(π/n)) + +Rust: `geometry::area_regular_polygon` + """ + ... + +def area_sector(radius: float, angle: float) -> float: + """ +Area of a circular sector: A = r²θ/2 + +Rust: `geometry::area_sector` + """ + ... + +def area_annulus(outer_r: float, inner_r: float) -> float: + """ +Area of an annulus: A = π(R² - r²) + +Rust: `geometry::area_annulus` + """ + ... + +def volume_sphere(radius: float) -> float: + """ +Volume of a sphere: V = 4πr³/3 + +Rust: `geometry::volume_sphere` + """ + ... + +def volume_cylinder(radius: float, height: float) -> float: + """ +Volume of a cylinder: V = πr²h + +Rust: `geometry::volume_cylinder` + """ + ... + +def volume_cone(radius: float, height: float) -> float: + """ +Volume of a cone: V = πr²h/3 + +Rust: `geometry::volume_cone` + """ + ... + +def volume_ellipsoid(a: float, b: float, c: float) -> float: + """ +Volume of an ellipsoid: V = 4πabc/3 + +Rust: `geometry::volume_ellipsoid` + """ + ... + +def volume_torus(major_r: float, minor_r: float) -> float: + """ +Volume of a torus: V = 2π²Rr² + +Rust: `geometry::volume_torus` + """ + ... + +def volume_frustum(r1: float, r2: float, height: float) -> float: + """ +Volume of a frustum: V = πh(r₁² + r₁r₂ + r₂²)/3 + +Rust: `geometry::volume_frustum` + """ + ... + +def volume_capsule(radius: float, cylinder_height: float) -> float: + """ +Volume of a capsule (cylinder + sphere): V = πr²h + 4πr³/3 + +Rust: `geometry::volume_capsule` + """ + ... + +def surface_sphere(radius: float) -> float: + """ +Surface area of a sphere: A = 4πr² + +Rust: `geometry::surface_sphere` + """ + ... + +def surface_cylinder_total(radius: float, height: float) -> float: + """ +Total surface area of a cylinder (lateral + both caps): A = 2πr(r + h) + +Rust: `geometry::surface_cylinder_total` + """ + ... + +def surface_cylinder_lateral(radius: float, height: float) -> float: + """ +Lateral surface area of a cylinder: A = 2πrh + +Rust: `geometry::surface_cylinder_lateral` + """ + ... + +def surface_cone_lateral(radius: float, slant_height: float) -> float: + """ +Lateral surface area of a cone: A = πrl + +Rust: `geometry::surface_cone_lateral` + """ + ... + +def surface_torus(major_r: float, minor_r: float) -> float: + """ +Surface area of a torus: A = 4π²Rr + +Rust: `geometry::surface_torus` + """ + ... + +def solid_angle_cone(half_angle: float) -> float: + """ +Solid angle subtended by a cone: Ω = 2π(1 - cos(θ)) + +Rust: `geometry::solid_angle_cone` + """ + ... + +def solid_angle_full_sphere() -> float: + """ +Solid angle of a full sphere: Ω = 4π steradians + +Rust: `geometry::solid_angle_full_sphere` + """ + ... + +def great_circle_distance(r: float, lat1: float, lon1: float, lat2: float, lon2: float) -> float: + """ +Great-circle distance on a sphere: d = r·arccos(sin(φ₁)sin(φ₂) + cos(φ₁)cos(φ₂)cos(Δλ)) + +Rust: `geometry::great_circle_distance` + """ + ... + +def spherical_excess(a: float, b: float, c: float) -> float: + """ +Spherical excess of a spherical triangle: E = A + B + C - π + +Rust: `geometry::spherical_excess` + """ + ... + +def moi_solid_sphere(mass: float, radius: float) -> float: + """ +Moment of inertia of a solid sphere: I = 2mr²/5 + +Rust: `geometry::moi_solid_sphere` + """ + ... + +def moi_hollow_sphere(mass: float, radius: float) -> float: + """ +Moment of inertia of a hollow sphere (thin shell): I = 2mr²/3 + +Rust: `geometry::moi_hollow_sphere` + """ + ... + +def moi_solid_cylinder(mass: float, radius: float) -> float: + """ +Moment of inertia of a solid cylinder about its axis: I = mr²/2 + +Rust: `geometry::moi_solid_cylinder` + """ + ... + +def moi_thin_rod_center(mass: float, length: float) -> float: + """ +Moment of inertia of a thin rod about its center: I = mL²/12 + +Rust: `geometry::moi_thin_rod_center` + """ + ... + +def moi_thin_rod_end(mass: float, length: float) -> float: + """ +Moment of inertia of a thin rod about one end: I = mL²/3 + +Rust: `geometry::moi_thin_rod_end` + """ + ... + +def moi_rectangular_plate(mass: float, width: float, height: float) -> float: + """ +Moment of inertia of a rectangular plate about its center: I = m(w² + h²)/12 + +Rust: `geometry::moi_rectangular_plate` + """ + ... + +def parallel_axis(i_cm: float, mass: float, distance: float) -> float: + """ +Parallel axis theorem: I = I_cm + md² + +Rust: `geometry::parallel_axis` + """ + ... diff --git a/bindings/python/python/numeria/geometry/delaunay.pyi b/bindings/python/python/numeria/geometry/delaunay.pyi new file mode 100644 index 0000000..a56bbe7 --- /dev/null +++ b/bindings/python/python/numeria/geometry/delaunay.pyi @@ -0,0 +1,52 @@ +""" +Delaunay triangulation and Voronoi diagrams in the plane. Triangulation: Bowyer-Watson incremental insertion with a super-triangle. Voronoi cells: half-plane intersection of the perpendicular bisectors (the dual definition), clipped to the bounding box of the sites — robust for boundary cells. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def circumcircle(a: tuple[float, float], b: tuple[float, float], c: tuple[float, float]) -> tuple[tuple[float, float], float]: + """ +Delaunay triangulation and Voronoi diagrams in the plane. + +Triangulation: Bowyer-Watson incremental insertion with a +super-triangle. Voronoi cells: half-plane intersection of the +perpendicular bisectors (the dual definition), clipped to the +bounding box of the sites — robust for boundary cells. +Circumcircle of triangle (a, b, c): (center, radius). + +Panics: +Panics if the points are collinear. + +Rust: `geometry::delaunay::circumcircle` + """ + ... + +def delaunay_2d(points: list[tuple[float, float]]) -> list[list[int]]: + """ +Delaunay triangulation by Bowyer-Watson insertion; returns triangle +index triples into `points`. + +Panics: +Panics with fewer than 3 points or if all points are collinear. + +Rust: `geometry::delaunay::delaunay_2d` + """ + ... + +def voronoi_cells_2d(points: list[tuple[float, float]]) -> list[list[tuple[float, float]]]: + """ +Voronoi cell polygons for each site, clipped to the sites' bounding +box (expanded by 10%). Cell i is the intersection of the half-planes +bounded by the perpendicular bisectors toward every other site — +the exact dual of the Delaunay triangulation. + +Panics: +Panics with fewer than 2 sites. + +Rust: `geometry::delaunay::voronoi_cells_2d` + """ + ... diff --git a/bindings/python/python/numeria/geometry/geodesy.pyi b/bindings/python/python/numeria/geometry/geodesy.pyi new file mode 100644 index 0000000..eeeec75 --- /dev/null +++ b/bindings/python/python/numeria/geometry/geodesy.pyi @@ -0,0 +1,74 @@ +""" +Geodesy on a reference ellipsoid. Vincenty's inverse and direct formulae (Vincenty, "Direct and inverse solutions of geodesics on the ellipsoid", Survey Review 1975) plus geodetic ↔ ECEF ↔ ENU coordinate conversions. Angles are radians; distances and heights are meters. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.math import Vec3 + +class Ellipsoid: + """ +Reference ellipsoid: semi-major axis a (m) and flattening f. + +Rust: `geometry::geodesy::Ellipsoid` + """ + def __init__(self, a: float, f: float) -> None: ... + def b(self) -> float: ... + def e_sq(self) -> float: ... + @property + def a(self) -> float: ... + @property + def f(self) -> float: ... + +def vincenty_inverse(lat1: float, lon1: float, lat2: float, lon2: float, e: Ellipsoid | Sequence[float]) -> tuple[float, float, float]: + """ +Vincenty inverse problem: geodesic distance (m) and forward/reverse +azimuths (rad) between two geodetic points. + +Returns `NoConvergence` for the nearly antipodal cases where +Vincenty's lambda iteration fails, and distance 0 with azimuth 0 +for coincident points. + +Rust: `geometry::geodesy::vincenty_inverse` + """ + ... + +def vincenty_direct(lat1: float, lon1: float, az1: float, dist: float, e: Ellipsoid | Sequence[float]) -> tuple[float, float, float]: + """ +Vincenty direct problem: destination (lat2, lon2) and final azimuth +after traveling `dist` meters from (lat1, lon1) on initial azimuth +`az1`. + +Rust: `geometry::geodesy::vincenty_direct` + """ + ... + +def geodetic_to_ecef(lat: float, lon: float, h: float, e: Ellipsoid | Sequence[float]) -> Vec3: + """ +Geodetic (lat, lon, height) → Earth-centered Earth-fixed Cartesian. + +Rust: `geometry::geodesy::geodetic_to_ecef` + """ + ... + +def ecef_to_geodetic(p: Vec3 | Sequence[float], e: Ellipsoid | Sequence[float]) -> tuple[float, float, float]: + """ +ECEF Cartesian → geodetic (lat, lon, height) by fixed-point +iteration on the latitude (converges to sub-millimeter in a few +steps). + +Rust: `geometry::geodesy::ecef_to_geodetic` + """ + ... + +def ecef_to_enu(p: Vec3 | Sequence[float], ref_lat: float, ref_lon: float, ref_h: float, e: Ellipsoid | Sequence[float]) -> Vec3: + """ +ECEF point → local East-North-Up coordinates relative to the given +geodetic reference. + +Rust: `geometry::geodesy::ecef_to_enu` + """ + ... diff --git a/bindings/python/python/numeria/geometry/hull.pyi b/bindings/python/python/numeria/geometry/hull.pyi new file mode 100644 index 0000000..4014ba2 --- /dev/null +++ b/bindings/python/python/numeria/geometry/hull.pyi @@ -0,0 +1,58 @@ +""" +Convex hulls and polygon predicates. 2-D hull: Andrew's monotone chain (O(n log n)), CCW output. 3-D hull: incremental visible-face (quickhull-style) algorithm returning triangle index triples with outward-facing orientation. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.math import Vec3 + +def convex_hull_2d(points: list[tuple[float, float]]) -> list[tuple[float, float]]: + """ +Convex hull of 2-D points by monotone chain, returned in +counter-clockwise order without repetition of the first vertex. +Collinear boundary points are dropped. Fewer than 3 distinct points +return the distinct points themselves. + +Rust: `geometry::hull::convex_hull_2d` + """ + ... + +def polygon_area_signed(poly: list[tuple[float, float]]) -> float: + """ +Shoelace formula: positive for counter-clockwise vertex order. + +Panics: +Panics if the polygon has fewer than 3 vertices. + +Rust: `geometry::hull::polygon_area_signed` + """ + ... + +def point_in_polygon(p: tuple[float, float], poly: list[tuple[float, float]]) -> bool: + """ +Even-odd (ray casting) point-in-polygon test; boundary points count +as inside up to floating-point tolerance. + +Panics: +Panics if the polygon has fewer than 3 vertices. + +Rust: `geometry::hull::point_in_polygon` + """ + ... + +def convex_hull_3d(points: list[Vec3 | Sequence[float]]) -> list[list[int]]: + """ +Convex hull of 3-D points as outward-oriented triangle index +triples, by the incremental visible-face (quickhull-style) +algorithm. + +Panics: +Panics with fewer than 4 points or fully degenerate (coplanar) +input. + +Rust: `geometry::hull::convex_hull_3d` + """ + ... diff --git a/bindings/python/python/numeria/geometry/mesh.pyi b/bindings/python/python/numeria/geometry/mesh.pyi new file mode 100644 index 0000000..d2d077f --- /dev/null +++ b/bindings/python/python/numeria/geometry/mesh.pyi @@ -0,0 +1,46 @@ +""" +Minimal indexed triangle mesh with ray intersection, backfilling the Part 2 `Mesh` type consumed by acoustics ray tracing and display helpers. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.math import Vec3 + +class Mesh: + """ +Indexed triangle mesh. `materials[i]` is a per-triangle material index +(into caller-owned tables such as absorption coefficients). + +Rust: `geometry::mesh::Mesh` + """ + def __init__(self, ) -> None: ... + @staticmethod + def box_room(size: Vec3 | Sequence[float]) -> Mesh: ... + def triangle_normal(self, i: int) -> Vec3: ... + def intersect_ray(self, origin: Vec3 | Sequence[float], dir: Vec3 | Sequence[float], t_min: float) -> Optional[RayHit]: ... + def segment_clear(self, a: Vec3 | Sequence[float], b: Vec3 | Sequence[float]) -> bool: ... + @property + def vertices(self) -> list[Vec3]: ... + @property + def triangles(self) -> list[list[int]]: ... + @property + def materials(self) -> list[int]: ... + +class RayHit: + """ +A ray/mesh intersection. + +Rust: `geometry::mesh::RayHit` + """ + def __init__(self, t: float, point: Vec3 | Sequence[float], normal: Vec3 | Sequence[float], triangle: int) -> None: ... + @property + def t(self) -> float: ... + @property + def point(self) -> Vec3: ... + @property + def normal(self) -> Vec3: ... + @property + def triangle(self) -> int: ... diff --git a/bindings/python/python/numeria/geophysics.pyi b/bindings/python/python/numeria/geophysics.pyi new file mode 100644 index 0000000..e0f3b60 --- /dev/null +++ b/bindings/python/python/numeria/geophysics.pyi @@ -0,0 +1,202 @@ +""" +The solid Earth: gravity, seismology, and heat. Gravity surveying -- the latitude formula, the free-air and Bouguer corrections, the resulting anomaly, and Airy isostatic compensation. Seismology: P- and S-wave travel times, epicentral distance from the S−P lag, the Richter and moment magnitude scales, and seismic moment and energy. The moment magnitude is the one to use for large events, because Richter saturates. Heat flow: pressure and temperature with depth, the geothermal gradient, and geothermal power. Plate tectonics closes the module with Euler-pole plate velocities and the square-root-of-age law for seafloor depth. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def gravity_at_latitude(latitude_rad: float) -> float: + """ +International gravity formula: g = 9.780327(1 + 0.0053024 sin²φ − 0.0000058 sin²2φ). +`latitude_rad` is the geodetic latitude in radians. + +Rust: `geophysics::gravity_at_latitude` + """ + ... + +def free_air_correction(height: float) -> float: + """ +Free-air gravity correction: Δg = −0.3086 × h (mGal). +`height` is meters above the geoid. + +Rust: `geophysics::free_air_correction` + """ + ... + +def bouguer_correction(height: float, density: float) -> float: + """ +Bouguer slab correction: Δg = 2πGρh. +Accounts for the gravitational attraction of material between the +station and the geoid. Returns the correction in m/s². + +Rust: `geophysics::bouguer_correction` + """ + ... + +def bouguer_anomaly(observed_g: float, latitude_g: float, free_air: float, bouguer: float) -> float: + """ +Bouguer anomaly: Δg_B = g_obs − g_lat + free_air − bouguer. +All values in consistent units (typically mGal or m/s²). + +Rust: `geophysics::bouguer_anomaly` + """ + ... + +def isostatic_compensation_depth(elevation: float, crust_density: float, mantle_density: float) -> float: + """ +Airy isostatic compensation depth: d = elevation × ρ_c / (ρ_m − ρ_c). +Returns the depth of the crustal root below normal Moho. + +Rust: `geophysics::isostatic_compensation_depth` + """ + ... + +def p_wave_travel_time(distance: float, velocity: float) -> float: + """ +P-wave travel time: t = d / v. + +Rust: `geophysics::p_wave_travel_time` + """ + ... + +def s_wave_travel_time(distance: float, velocity: float) -> float: + """ +S-wave travel time: t = d / v. + +Rust: `geophysics::s_wave_travel_time` + """ + ... + +def epicentral_distance_from_lag(t_s_minus_p: float, vp: float, vs: float) -> float: + """ +Epicentral distance from S−P lag time: d = Δt × vp × vs / (vp − vs). + +Rust: `geophysics::epicentral_distance_from_lag` + """ + ... + +def richter_magnitude(amplitude: float, distance_km: float) -> float: + """ +Simplified Richter local magnitude: M_L = log₁₀(A) + 2.76 log₁₀(Δ) − 2.48. +`amplitude` is the maximum trace amplitude in mm, `distance_km` in km. + +Rust: `geophysics::richter_magnitude` + """ + ... + +def moment_magnitude(seismic_moment: float) -> float: + """ +Moment magnitude from seismic moment (N·m): M_w = (2/3) log₁₀(M₀) − 6.07. + +Rust: `geophysics::moment_magnitude` + """ + ... + +def seismic_moment(magnitude: float) -> float: + """ +Seismic moment from moment magnitude (returns N·m): M₀ = 10^(1.5(M_w + 6.07)). + +Rust: `geophysics::seismic_moment` + """ + ... + +def seismic_energy(magnitude: float) -> float: + """ +Seismic energy from magnitude (Gutenberg-Richter): E = 10^(1.5M + 4.8) joules. + +Rust: `geophysics::seismic_energy` + """ + ... + +def pressure_at_depth(depth: float, surface_density: float, g: float) -> float: + """ +Lithostatic pressure at depth (simplified): P ≈ ρgd. +`depth` in meters, `surface_density` in kg/m³, `g` in m/s². + +Rust: `geophysics::pressure_at_depth` + """ + ... + +def temperature_at_depth(surface_temp: float, geothermal_gradient: float, depth: float) -> float: + """ +Temperature at depth assuming a linear geothermal gradient: +T = T₀ + (dT/dz) × z. +`geothermal_gradient` is in K/m (typical ~0.025–0.030 K/m). + +Rust: `geophysics::temperature_at_depth` + """ + ... + +def moho_depth_continental() -> float: + """ +Average continental Moho depth: ~35 km. + +Rust: `geophysics::moho_depth_continental` + """ + ... + +def moho_depth_oceanic() -> float: + """ +Average oceanic Moho depth: ~7 km. + +Rust: `geophysics::moho_depth_oceanic` + """ + ... + +def core_mantle_boundary_depth() -> float: + """ +Core-mantle boundary depth: 2891 km. + +Rust: `geophysics::core_mantle_boundary_depth` + """ + ... + +def heat_flow(conductivity: float, temperature_gradient: float) -> float: + """ +Fourier heat flow: q = k × dT/dz (W/m²). +`conductivity` in W/(m·K), `temperature_gradient` in K/m. + +Rust: `geophysics::heat_flow` + """ + ... + +def geothermal_power(flow_rate: float, specific_heat: float, delta_temp: float) -> float: + """ +Geothermal power extracted from a fluid: P = ṁ c ΔT. +`flow_rate` in kg/s, `specific_heat` in J/(kg·K), `delta_temp` in K. + +Rust: `geophysics::geothermal_power` + """ + ... + +def plate_velocity_euler(omega: float, radius: float, angular_distance: float) -> float: + """ +Plate velocity from an Euler pole: v = ω R sin(θ). +`omega` in rad/s, `radius` in meters, `angular_distance` in radians. + +Rust: `geophysics::plate_velocity_euler` + """ + ... + +def age_of_seafloor(distance: float, spreading_rate: float) -> float: + """ +Age of seafloor from distance to ridge: t = d / (2v). +`distance` in meters, `spreading_rate` is the full rate in m/s. +Division by 2 accounts for the half-spreading rate. + +Rust: `geophysics::age_of_seafloor` + """ + ... + +def ocean_depth_from_age(age_myr: float) -> float: + """ +Ocean depth from seafloor age (Parsons-Sclater model): +d = 2500 + 350√t. +`age_myr` is in millions of years; returns depth in meters. + +Rust: `geophysics::ocean_depth_from_age` + """ + ... diff --git a/bindings/python/python/numeria/graph/__init__.pyi b/bindings/python/python/numeria/graph/__init__.pyi new file mode 100644 index 0000000..1f03bbf --- /dev/null +++ b/bindings/python/python/numeria/graph/__init__.pyi @@ -0,0 +1,13 @@ +""" +Graphs: representation and structure, shortest paths, network flow, matchings, spectral graph theory, colouring, and drawing. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from . import coloring, core, flow, layout, matching, paths, spectral +from numeria.graph.core import Graph as Graph + + diff --git a/bindings/python/python/numeria/graph/coloring.pyi b/bindings/python/python/numeria/graph/coloring.pyi new file mode 100644 index 0000000..79e4c07 --- /dev/null +++ b/bindings/python/python/numeria/graph/coloring.pyi @@ -0,0 +1,320 @@ +""" +Colouring, cliques, independent sets, and covers. Almost everything here is NP-hard in general, so the module is split deliberately between two kinds of routine. The heuristics -- greedy colouring, Welsh-Powell, the two-approximation for vertex cover, the greedy dominating set -- run on any graph and come with a stated guarantee, usually a bound relative to a structural parameter rather than to the optimum. The exact routines carry `_small` or `_exact` in their names and are honest about the size they can take: they enumerate, and the cost is exponential. The exception is Vizing's edge colouring, which is exact-ish for free: the theorem says `Delta` or `Delta + 1` colours always suffice, and the Misra-Gries construction reaches `Delta + 1` in polynomial time. Which of the two a given graph needs is itself NP-hard to decide. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.graph.core import Graph +from numeria.exact.polynomial import PolyQ + +class Order: + """ +The vertex order a greedy colouring walks. + +Greedy colouring gives every vertex the smallest colour none of its +already-coloured neighbours holds. The order is the whole algorithm: some +order always produces an optimal colouring, and finding it is the hard +part, so these are the standard heuristics for choosing one. + +Rust: `graph::coloring::Order` + """ + ... + +def greedy_coloring(g: Graph, order: Order) -> list[int]: + """ +Greedy colouring in the given vertex order. + +Returns one colour per vertex, numbered from zero. Every order yields a +proper colouring; the count of colours is what varies, and +`Order::SmallestLast` and `Order::Dsatur` carry the guarantees worth +having. Self-loops are ignored, since no colouring can respect one. + +Rust: `graph::coloring::greedy_coloring` + """ + ... + +def color_count(coloring: list[int]) -> int: + """ +The number of distinct colours a colouring uses. + +Rust: `graph::coloring::color_count` + """ + ... + +def is_proper_coloring(g: Graph, coloring: list[int]) -> bool: + """ +Whether a colouring gives no edge two ends of the same colour. + +A self-loop always fails, which is the correct answer: a graph with one +has no proper colouring at all. + +Rust: `graph::coloring::is_proper_coloring` + """ + ... + +def welsh_powell(g: Graph) -> list[int]: + """ +Welsh-Powell colouring: sort by descending degree, then fill one colour +class at a time by sweeping the list. + +This is the same colouring `greedy_coloring` produces under +`Order::LargestFirst`, and for the same reason: a vertex takes colour +`c` in the sweep exactly when every earlier class held a neighbour of it, +which is the greedy rule stated the other way round. The procedure is +kept in its own form because the bound it is quoted with -- +`max_i min(d_i + 1, i)` over the sorted degrees -- is a statement about +the sweep. + +Rust: `graph::coloring::welsh_powell` + """ + ... + +def welsh_powell_bound(g: Graph) -> int: + """ +The Welsh-Powell bound on the number of colours: `max_i min(d_i + 1, i)` +over the degrees sorted descending, indexed from one. + +Rust: `graph::coloring::welsh_powell_bound` + """ + ... + +def chromatic_number_exact_small(g: Graph) -> int: + """ +The chromatic number, by exhaustive search. Intended for `n <= 20`. + +Bracketed first: a greedy clique gives a lower bound, since a clique of +size `q` needs `q` colours, and DSATUR gives an upper bound. Then each `k` +in between is decided exactly. On a graph the bracket already pins -- and +it often does -- no search runs at all. + +Panics: +Panics on a self-loop, which admits no proper colouring. + +Rust: `graph::coloring::chromatic_number_exact_small` + """ + ... + +def chromatic_polynomial_small(g: Graph) -> PolyQ: + """ +The chromatic polynomial, exactly, by deletion-contraction. For `n <= 12`. + +`P(G, x)` counts the proper colourings of `G` with `x` colours, and the +recursion is `P(G) = P(G - e) - P(G / e)`: colourings of `G - e` either +give `e`'s ends different colours, which is a colouring of `G`, or the +same colour, which is a colouring of the contraction. Both branches +shrink the graph -- deletion loses an edge, contraction loses a vertex -- +so the recursion terminates on the edgeless graph, whose polynomial is +`x^n`. Memoised on the canonical edge set, which is what makes it +tractable at all: the two branches meet again constantly. + +Panics: +Panics on a self-loop. Contraction can create one only from a parallel +edge, which is collapsed first. + +Rust: `graph::coloring::chromatic_polynomial_small` + """ + ... + +def edge_coloring_vizing(g: Graph) -> list[int]: + """ +Vizing edge colouring by the Misra-Gries construction: one colour per +edge, no two edges sharing a vertex alike, in at most `Delta + 1` colours. + +Vizing's theorem says every simple graph needs `Delta` or `Delta + 1`, and +this reaches the upper end constructively. Each edge is coloured by +building a *fan* around one endpoint -- a run of neighbours where each +one's edge colour is free at the previous, so the whole run can shift +down by one -- then either rotating the fan to slide a free colour into +place, or first inverting a two-colour alternating path to make one free. +The alternating path is the part that makes the bound work: it repairs +the one obstruction rotation alone cannot, and it does so without +disturbing any other vertex, since every interior vertex of the path +simply exchanges its `c` for its `d`. + +Returns one colour per entry of `g.edges()`, in that order. + +Panics: +Panics if the graph is directed, or is not simple. A self-loop cannot be +coloured at all, and a parallel edge takes the bound outside Vizing's +theorem into Shannon's `Delta + mu`. + +Rust: `graph::coloring::edge_coloring_vizing` + """ + ... + +def is_proper_edge_coloring(g: Graph, coloring: list[int]) -> bool: + """ +Whether an edge colouring gives no two edges sharing a vertex the same +colour. + +Rust: `graph::coloring::is_proper_edge_coloring` + """ + ... + +def interval_graph_coloring(intervals: list[tuple[float, float]]) -> list[int]: + """ +Optimal colouring of an interval graph, given the intervals themselves. + +Two intervals conflict when they overlap, and the greedy sweep in order of +left endpoint is optimal here -- unlike on a general graph -- because at +the moment an interval opens, every interval it will ever conflict with +that came earlier is still open. So the colours in use are exactly the +current overlap, and the total is the maximum overlap, which is a lower +bound for any colouring. Half-open intervals: touching at an endpoint is +not an overlap, and an interval whose ends coincide is empty, meets +nothing, and shares the first colour. + +Returns one colour per interval, in the input order. + +Panics: +Panics if an interval has its end before its start, or is not finite. + +Rust: `graph::coloring::interval_graph_coloring` + """ + ... + +def map_coloring_backtrack(adjacency: list[list[int]], k: int) -> Optional[list[int]]: + """ +A proper `k`-colouring of a graph given by adjacency lists, or `None`. + +The classic map-colouring formulation: regions and the regions they +border. Straight chronological backtracking with forward checking, which +is what the four-colour problem was posed as long before it was a theorem. + +Panics: +Panics if an adjacency list names a region outside the range. + +Rust: `graph::coloring::map_coloring_backtrack` + """ + ... + +def all_maximal_cliques(g: Graph) -> list[list[int]]: + """ +Every maximal clique, by Bron-Kerbosch with pivoting. + +A clique is maximal when no vertex can be added; the algorithm grows one +while maintaining the candidates that could still join (`p`) and those +already ruled out (`x`), and reports when both are empty. The pivot is the +speedup: choosing a vertex `q` from `p | x` with the most neighbours in +`p`, and branching only on `p` minus `q`'s neighbourhood, skips the +branches that could only ever rediscover a clique through `q`. + +Panics: +Panics above 64 vertices. The output can be exponential in the input -- +a graph on `3j` vertices can have `3^j` maximal cliques -- so this is for +small graphs by construction. + +Rust: `graph::coloring::all_maximal_cliques` + """ + ... + +def max_clique_bron_kerbosch(g: Graph) -> list[int]: + """ +A maximum clique: the largest set of mutually adjacent vertices. + +Every maximum clique is maximal, so enumerating the maximal ones and +taking the largest is exact. Ties go to the lexicographically first. + +Panics: +Panics above 64 vertices. + +Rust: `graph::coloring::max_clique_bron_kerbosch` + """ + ... + +def independent_set_greedy(g: Graph) -> list[int]: + """ +A maximal independent set, greedily: repeatedly take a vertex of minimum +remaining degree and discard its neighbours. + +Minimum degree first is the right greed here: taking the vertex that +eliminates the fewest others leaves the most room for the rest. The +result is guaranteed maximal -- nothing can be added -- and at least +`sum_v 1/(d_v + 1)` in size by the Caro-Wei bound, but not maximum. + +Rust: `graph::coloring::independent_set_greedy` + """ + ... + +def max_independent_set_small(g: Graph) -> list[int]: + """ +A maximum independent set, exactly, via the complement. + +An independent set in `G` is a clique in the complement of `G` and the +other way round, so this is the clique enumeration with the edges flipped. + +Panics: +Panics above 64 vertices. + +Rust: `graph::coloring::max_independent_set_small` + """ + ... + +def vertex_cover_2approx(g: Graph) -> list[int]: + """ +A vertex cover within a factor of two of the smallest, by taking both ends +of a maximal matching. + +The matching's edges are disjoint, so any cover must contain at least one +end of each, giving `opt >= |M|`; taking both ends gives `2|M| <= 2 opt`. +The bound comes free with the construction and holds on every graph, which +is more than the best known algorithm can say about doing better. + +Rust: `graph::coloring::vertex_cover_2approx` + """ + ... + +def vertex_cover_exact_small(g: Graph) -> list[int]: + """ +A minimum vertex cover, exactly, as the complement of a maximum +independent set. + +Gallai's identity: a set covers every edge exactly when its complement +spans none, so the two problems are the same problem read twice, and +`tau + alpha = n`. + +Panics: +Panics above 64 vertices, or on a self-loop, whose vertex every cover must +contain and which the complement identity does not account for. + +Rust: `graph::coloring::vertex_cover_exact_small` + """ + ... + +def dominating_set_greedy(g: Graph) -> list[int]: + """ +A dominating set, greedily: every vertex is in it or next to it. + +Set cover in disguise, with each vertex offering its closed neighbourhood, +so the greedy choice of whichever vertex newly dominates the most inherits +set cover's `ln(n) + 1` guarantee -- and its hardness, since matching that +factor in polynomial time would collapse the same complexity assumption. + +Rust: `graph::coloring::dominating_set_greedy` + """ + ... + +def feedback_arc_set_greedy(g: Graph) -> list[tuple[int, int]]: + """ +A feedback arc set: arcs whose removal leaves a directed acyclic graph. + +By the Eades-Lin-Smyth ordering. It builds a linear order by repeatedly +taking sinks from the right, sources from the left, and otherwise the +vertex whose out-degree most exceeds its in-degree; the arcs pointing +backwards in that order are the answer. Removing them must leave a DAG, +since a linear order that every remaining arc respects is a topological +order. The count is within `m/2 - n/6` of the total, which is the bound +the heuristic is quoted for. + +Self-loops are always returned: no ordering can place a vertex before +itself. + +Panics: +Panics if the graph is undirected. + +Rust: `graph::coloring::feedback_arc_set_greedy` + """ + ... diff --git a/bindings/python/python/numeria/graph/core.pyi b/bindings/python/python/numeria/graph/core.pyi new file mode 100644 index 0000000..9213b6e --- /dev/null +++ b/bindings/python/python/numeria/graph/core.pyi @@ -0,0 +1,361 @@ +""" +Graphs: representation, structural queries, generators, and products. A `Graph` is an adjacency list of weighted arcs over the vertices `0..n`. An undirected graph stores each edge in both directions, so degree, traversal and neighbour iteration need no special case; `Graph::edges` reports each undirected edge once. Weights are `f64` and default to one. Structural queries here ignore them; the shortest-path and flow modules use them. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.linalg.matrix import Matrix +from numeria.monte_carlo import Rng + +class Graph: + """ +A weighted graph over the vertices `0..n`. + +Rust: `graph::core::Graph` + """ + def __init__(self, n: int, directed: bool) -> None: ... + def add_edge(self, u: int, v: int, w: float) -> None: ... + @staticmethod + def from_edges(n: int, edges: list[tuple[int, int, float]], directed: bool) -> Graph: ... + @staticmethod + def from_adjacency_matrix(m: Matrix | Sequence[Sequence[float]]) -> Graph: ... + def to_adjacency_matrix(self) -> Matrix: ... + def degree(self, v: int) -> int: ... + def out_degree(self, v: int) -> int: ... + def in_degree(self, v: int) -> int: ... + def edges(self) -> list[tuple[int, int, float]]: ... + def edge_count(self) -> int: ... + def reverse(self) -> Graph: ... + def subgraph(self, vs: list[int]) -> Graph: ... + def complement(self) -> Graph: ... + def is_connected(self) -> bool: ... + def connected_components(self) -> list[list[int]]: ... + def strongly_connected_components(self) -> list[list[int]]: ... + def condensation(self) -> tuple[Graph, list[int]]: ... + def is_bipartite(self) -> Optional[list[bool]]: ... + def is_tree(self) -> bool: ... + def is_dag(self) -> bool: ... + def topological_sort(self) -> Optional[list[int]]: ... + def bfs(self, s: int) -> list[Optional[int]]: ... + def dfs(self, s: int) -> list[int]: ... + def bridges(self) -> list[tuple[int, int]]: ... + def articulation_points(self) -> list[int]: ... + def eulerian_circuit(self) -> Optional[list[int]]: ... + def eulerian_path(self) -> Optional[list[int]]: ... + def hamiltonian_path_small(self) -> Optional[list[int]]: ... + def girth(self) -> Optional[int]: ... + def eccentricities(self) -> list[Optional[int]]: ... + def diameter(self) -> Optional[int]: ... + def radius(self) -> Optional[int]: ... + def center(self) -> list[int]: ... + def density(self) -> float: ... + def clustering_coefficient(self, v: int) -> float: ... + def average_clustering(self) -> float: ... + def transitivity(self) -> float: ... + def degree_distribution(self) -> list[int]: ... + def assortativity(self) -> float: ... + def k_core(self, k: int) -> list[int]: ... + def core_numbers(self) -> list[int]: ... + @property + def n(self) -> int: ... + @property + def adj(self) -> list[list[tuple[int, float]]]: ... + @property + def directed(self) -> bool: ... + +def complete_graph(n: int) -> Graph: + """ +`K_n`: every pair joined. + +Rust: `graph::core::complete_graph` + """ + ... + +def cycle_graph(n: int) -> Graph: + """ +`C_n`: a single cycle. Needs at least three vertices to be a simple cycle. + +Panics: +Panics if `n` is below three. + +Rust: `graph::core::cycle_graph` + """ + ... + +def path_graph(n: int) -> Graph: + """ +`P_n`: a single path. + +Rust: `graph::core::path_graph` + """ + ... + +def star_graph(n: int) -> Graph: + """ +A star with `n` vertices: vertex 0 joined to every other. + +Rust: `graph::core::star_graph` + """ + ... + +def wheel_graph(n: int) -> Graph: + """ +A wheel with `n` vertices: a hub at 0 joined to a cycle on the rest. + +Panics: +Panics if `n` is below four. + +Rust: `graph::core::wheel_graph` + """ + ... + +def grid_2d(w: int, h: int) -> Graph: + """ +A `w` by `h` grid, with vertex `(x, y)` at index `y * w + x`. + +Rust: `graph::core::grid_2d` + """ + ... + +def hypercube_graph(d: int) -> Graph: + """ +The `d`-dimensional hypercube: `2^d` vertices, joined when their labels +differ in one bit. + +Panics: +Panics if `d` exceeds 20. + +Rust: `graph::core::hypercube_graph` + """ + ... + +def petersen_graph() -> Graph: + """ +The Petersen graph: the Kneser graph on the 2-subsets of a 5-set, joined +when disjoint. Three-regular, girth five, ten vertices. + +Rust: `graph::core::petersen_graph` + """ + ... + +def complete_bipartite(m: int, n: int) -> Graph: + """ +`K_{m,n}`: the vertices `0..m` each joined to every vertex in `m..m+n`. + +Rust: `graph::core::complete_bipartite` + """ + ... + +def erdos_renyi(n: int, p: float, rng: Rng) -> Graph: + """ +The Erdos-Renyi model `G(n, p)`: each of the `C(n, 2)` pairs is an edge +independently with probability `p`. + +Panics: +Panics unless `p` is in `[0, 1]`. + +Rust: `graph::core::erdos_renyi` + """ + ... + +def barabasi_albert(n: int, m: int, rng: Rng) -> Graph: + """ +The Barabasi-Albert preferential attachment model. + +Starts from a complete graph on `m` vertices and adds the rest one at a +time, each joining `m` distinct existing vertices chosen with probability +proportional to their degree. That is done by sampling from the list of +arc endpoints, in which a vertex appears once per incident edge, which is +exactly the degree distribution. + +Panics: +Panics unless `1 <= m < n`. + +Rust: `graph::core::barabasi_albert` + """ + ... + +def watts_strogatz(n: int, k: int, beta: float, rng: Rng) -> Graph: + """ +The Watts-Strogatz small-world model. + +Starts from a ring in which each vertex joins its `k / 2` nearest +neighbours on each side, then rewires each edge with probability `beta` to +a uniformly chosen vertex, refusing self-loops and duplicates. The result +keeps the ring's clustering while acquiring a short diameter. + +Panics: +Panics unless `k` is even and `2 <= k < n`, or if `beta` is outside +`[0, 1]`. + +Rust: `graph::core::watts_strogatz` + """ + ... + +def random_regular(n: int, d: int, rng: Rng) -> Optional[Graph]: + """ +A random `d`-regular graph by the pairing (configuration) model with +rejection. + +Gives each vertex `d` half-edges, matches them uniformly at random, and +retries the whole draw if the matching produces a self-loop or a repeat. +That rejection is what makes the result uniform over simple `d`-regular +graphs rather than merely `d`-regular on average. + +Returns `None` if `n * d` is odd, when no such graph exists, or if the +rejection loop gives up. + +Panics: +Panics unless `d < n`. + +Rust: `graph::core::random_regular` + """ + ... + +def random_geometric(n: int, radius: float, rng: Rng) -> tuple[Graph, list[tuple[float, float]]]: + """ +A random geometric graph: `n` points uniform in the unit square, joined +when within `radius`. + +Returns the graph and the positions, since the positions are what make the +model meaningful and are otherwise unrecoverable. + +Panics: +Panics if `radius` is negative. + +Rust: `graph::core::random_geometric` + """ + ... + +def stochastic_block_model(sizes: list[int], p_matrix: list[list[float]], rng: Rng) -> Graph: + """ +The stochastic block model: vertices split into blocks of the given sizes, +with an edge between blocks `i` and `j` drawn with probability +`p_matrix[i][j]`. + +Panics: +Panics if `p_matrix` is not square with one row per block, or if any entry +is outside `[0, 1]`. + +Rust: `graph::core::stochastic_block_model` + """ + ... + +def graph_from_mesh(mesh: Mesh) -> Graph: + """ +The edge graph of a triangle mesh: one vertex per mesh vertex, joined when +they share a triangle edge. Weights are the edge lengths. + +Rust: `graph::core::graph_from_mesh` + """ + ... + +def line_graph(g: Graph) -> tuple[Graph, list[tuple[int, int]]]: + """ +The line graph: one vertex per edge of `g`, joined when the edges share an +endpoint. Returns the graph and the edge each vertex came from. + +Panics: +Panics if `g` is directed, for which the construction differs. + +Rust: `graph::core::line_graph` + """ + ... + +def cartesian_product(g: Graph, h: Graph) -> Graph: + """ +The Cartesian product `g x h`: vertex `(u, x)` at index `u * h.n + x`, with +an edge when one coordinate is equal and the other adjacent. + +Rust: `graph::core::cartesian_product` + """ + ... + +def tensor_product(g: Graph, h: Graph) -> Graph: + """ +The tensor (categorical) product: vertex `(u, x)` adjacent to `(v, y)` when +`u ~ v` and `x ~ y`. + +Rust: `graph::core::tensor_product` + """ + ... + +def canonical_form_small(g: Graph) -> list[int]: + """ +A canonical form: the lexicographically least adjacency bitmask sequence +over all vertex relabellings. + +Two graphs are isomorphic exactly when their canonical forms agree, so this +is a complete invariant rather than a heuristic one. It searches all `n!` +relabellings with pruning by the sorted degree sequence, so it is only +affordable for small graphs. + +Panics: +Panics if `g` has more than 10 vertices. + +Rust: `graph::core::canonical_form_small` + """ + ... + +def is_isomorphic_small(g: Graph, h: Graph) -> bool: + """ +True when `g` and `h` are isomorphic. + +Screens on the cheap invariants first -- vertex count, edge count, sorted +degree sequence, sorted triangle counts -- and only then compares canonical +forms. + +Panics: +Panics if either graph has more than 10 vertices. + +Rust: `graph::core::is_isomorphic_small` + """ + ... + +def graph6_encode(g: Graph) -> str: + """ +Encodes an undirected simple graph in the graph6 format. + +The format writes the vertex count, then the strict upper triangle of the +adjacency matrix read column by column, packed six bits per character with +63 added so every byte is printable ASCII. + +Panics: +Panics if the graph is directed, or has more than 62 vertices, which is +where the format's single-character length prefix ends. + +Rust: `graph::core::graph6_encode` + """ + ... + +def graph6_decode(s: str) -> Graph: + """ +Decodes a graph6 string produced by `graph6_encode`. + +Panics: +Panics if the string is empty, contains a byte outside the printable range +the format uses, or is too short for the vertex count it declares. + +Rust: `graph::core::graph6_decode` + """ + ... + +def spanning_tree_count_exact(g: Graph) -> int: + """ +The number of spanning trees, exactly, by the matrix-tree theorem. + +Kirchhoff's theorem says this is any cofactor of the Laplacian; the +determinant is taken over the integers by Bareiss fraction-free +elimination, so the answer is exact rather than a rounded float. + +Parallel edges count as distinct; weights are ignored. + +Panics: +Panics if the graph is directed. + +Rust: `graph::core::spanning_tree_count_exact` + """ + ... diff --git a/bindings/python/python/numeria/graph/flow.pyi b/bindings/python/python/numeria/graph/flow.pyi new file mode 100644 index 0000000..5f7df89 --- /dev/null +++ b/bindings/python/python/numeria/graph/flow.pyi @@ -0,0 +1,254 @@ +""" +Network flow: maximum flow, minimum cut, and the problems that reduce to them. A flow network is a `Graph` whose weights are capacities. An undirected edge is treated as a pair of arcs, each with the full capacity, which is the usual convention: flow may run either way but not both at once. Capacities must be finite and non-negative. The residual graph is built internally as an arc list with paired indices, so the reverse arc of arc `i` is arc `i ^ 1`. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.graph.core import Graph + +def max_flow_dinic(g: Graph, s: int, t: int) -> tuple[float, list[list[float]]]: + """ +The maximum flow from `s` to `t` by Dinic's algorithm, and the flow on each +arc as a matrix. + +Dinic repeatedly builds a level graph by breadth-first search and pushes +blocking flow through it, which bounds the number of phases by the vertex +count rather than by the flow value -- the difference between terminating +and not on a network with large capacities. + +The returned matrix holds the net flow: entry `(u, v)` is what crosses from +`u` to `v`, and is zero where nothing does. + +Panics: +Panics if `s` or `t` is out of range, if they are equal, or if any capacity +is negative or not finite. + +Rust: `graph::flow::max_flow_dinic` + """ + ... + +def max_flow_push_relabel(g: Graph, s: int, t: int) -> float: + """ +The maximum flow value by the push-relabel method with the highest-label +rule. + +A different algorithm from `max_flow_dinic` rather than a variation on +it: push-relabel never maintains a valid flow until it finishes, working +instead with a preflow that it gradually returns to feasibility. The two +agreeing is therefore evidence, not a tautology. + +Panics: +Panics under the same conditions as `max_flow_dinic`. + +Rust: `graph::flow::max_flow_push_relabel` + """ + ... + +def min_cut(g: Graph, s: int, t: int) -> tuple[float, list[bool]]: + """ +The minimum `s`-`t` cut: its capacity and the source side. + +By the max-flow min-cut theorem the capacity equals the maximum flow, and +the source side is exactly what remains reachable from `s` in the residual +network once the flow is maximum. + +Panics: +Panics under the same conditions as `max_flow_dinic`. + +Rust: `graph::flow::min_cut` + """ + ... + +def global_min_cut_stoer_wagner(g: Graph) -> tuple[float, list[int]]: + """ +The global minimum cut, by the Stoer-Wagner algorithm. + +Finds the cheapest way to split the graph in two without naming the two +sides, which no single `s`-`t` computation does. Each phase grows a set by +always adding the most tightly connected vertex, which makes the last two +added a valid `s`-`t` pair for free; merging them and repeating gives the +global optimum in `n - 1` phases. + +Returns the cut capacity and one side of it. + +Panics: +Panics if the graph is directed, or has fewer than two vertices. + +Rust: `graph::flow::global_min_cut_stoer_wagner` + """ + ... + +def min_cost_max_flow(g: Graph, costs: list[float], s: int, t: int) -> tuple[float, float]: + """ +The minimum-cost maximum flow from `s` to `t`. + +`costs` gives the cost per unit on each arc, in the same order as +`g.edges()`. Returns the flow value and its total cost. + +Augments along a shortest path by cost each round, found with Bellman-Ford +so negative costs are allowed. Sending flow along a shortest path keeps the +residual network free of negative cycles, which is what makes the greedy +choice optimal rather than merely feasible. + +Panics: +Panics if `costs` does not have one entry per edge, or under the same +conditions as `max_flow_dinic`. + +Rust: `graph::flow::min_cost_max_flow` + """ + ... + +def circulation_with_demands(g: Graph, demand: list[float], lower: list[float]) -> Optional[list[float]]: + """ +A feasible circulation meeting the given vertex demands, or `None` if none +exists. + +`demand[v]` is positive when `v` must receive that much and negative when +it must send it. `lower` gives the minimum flow on each edge, in the order +of `g.edges()`. Solved by the standard reduction: subtract the lower bounds +into the demands, then look for a saturating flow from a super-source to a +super-sink. + +Returns the flow on each edge in the order of `g.edges()`. + +Panics: +Panics unless `demand` has one entry per vertex, `lower` one per edge, the +demands sum to zero, and every lower bound is within its capacity. + +Rust: `graph::flow::circulation_with_demands` + """ + ... + +def max_bipartite_matching_via_flow(g: Graph, left: list[int]) -> list[Optional[int]]: + """ +A maximum matching of a bipartite graph, found by maximum flow. + +`left` names the vertices on one side; the rest are the other side. Returns +the partner of each vertex, or `None` for the unmatched. + +Slower than `graph::matching::hopcroft_karp` but built from a +different primitive, so the two agreeing is evidence about both. + +Panics: +Panics if `left` names a vertex twice or out of range, or if an edge joins +two vertices on the same side. + +Rust: `graph::flow::max_bipartite_matching_via_flow` + """ + ... + +def edge_disjoint_paths(g: Graph, s: int, t: int) -> int: + """ +The number of pairwise edge-disjoint paths from `s` to `t`. + +Menger's theorem says this equals the minimum number of edges whose removal +separates them, which is the maximum flow with every capacity one. + +Panics: +Panics if `s` or `t` is out of range, or they are equal. + +Rust: `graph::flow::edge_disjoint_paths` + """ + ... + +def vertex_disjoint_paths(g: Graph, s: int, t: int) -> int: + """ +The number of pairwise internally vertex-disjoint paths from `s` to `t`. + +The vertex form of Menger's theorem. Each vertex other than `s` and `t` is +split into an in-copy and an out-copy joined by a unit arc, which caps how +many paths can use it; the answer is then the edge-disjoint count on the +split graph. + +Panics: +Panics if `s` or `t` is out of range, or they are equal. + +Rust: `graph::flow::vertex_disjoint_paths` + """ + ... + +def gomory_hu_tree(g: Graph) -> Graph: + """ +A Gomory-Hu tree: an `n`-vertex tree in which the minimum cut between any +two vertices equals the lightest edge on the tree path between them. + +Built by Gusfield's simplification, which needs only `n - 1` maximum-flow +computations and no vertex contraction. The result encodes all `C(n, 2)` +pairwise minimum cuts in `n - 1` numbers. + +Panics: +Panics if the graph is directed. + +Rust: `graph::flow::gomory_hu_tree` + """ + ... + +def closure_problem(g: Graph, weights: list[float]) -> tuple[float, list[bool]]: + """ +The maximum-weight closed subset of a directed graph. + +A closure is a vertex set containing every successor of every member. The +maximum-weight closure reduces to a minimum cut: positive vertices are +joined to a source with their weight, negative ones to a sink with its +magnitude, and each original arc is given infinite capacity so no cut can +break it, which is exactly the closure condition. + +Returns the weight and the membership flags. + +Panics: +Panics unless `weights` has one entry per vertex. + +Rust: `graph::flow::closure_problem` + """ + ... + +def project_selection(project_revenue: list[float], machine_cost: list[float], requires: list[list[int]]) -> float: + """ +The maximum profit of a project selection problem. + +Projects have revenues and require machines that cost money; a project may +only be taken if every machine it needs is bought. `project_revenue[i]` is +the revenue of project `i`, `machine_cost[j]` the cost of machine `j`, and +`requires[i]` the machines project `i` needs. + +This is `closure_problem` on the bipartite graph of projects and +machines, with revenues positive and costs negative. + +Panics: +Panics if `requires` does not have one entry per project, or names a +machine out of range. + +Rust: `graph::flow::project_selection` + """ + ... + +def max_flow(g: Graph, s: int, t: int) -> float: + """ +The maximum flow value as a plain number, for callers that do not want the +arc-by-arc matrix. + +Panics: +Panics under the same conditions as `max_flow_dinic`. + +Rust: `graph::flow::max_flow` + """ + ... + +def cut_capacity(g: Graph, side: list[bool]) -> float: + """ +The capacity of the cut defined by `side`: the total weight of the edges +leaving the `true` set. + +A directed graph counts only arcs from the `true` side to the `false` one, +which is the `s`-`t` cut convention; an undirected graph counts every edge +crossing. + +Panics: +Panics unless `side` has one flag per vertex. + +Rust: `graph::flow::cut_capacity` + """ + ... diff --git a/bindings/python/python/numeria/graph/layout.pyi b/bindings/python/python/numeria/graph/layout.pyi new file mode 100644 index 0000000..8533044 --- /dev/null +++ b/bindings/python/python/numeria/graph/layout.pyi @@ -0,0 +1,301 @@ +""" +Graph drawing: where to put the vertices. Two families. The *metric* layouts -- Kamada-Kawai, stress majorization, Fruchterman-Reingold, spectral -- treat drawing as optimisation: pick a target distance for every pair, usually the number of edges between them, and place the points so the drawn distances match. What they optimise is stated exactly, so what they achieve can be measured, which is why `layout_stress` is public. The *structural* layouts -- circular, shell, Reingold-Tilford, Sugiyama -- draw a shape the graph already has. They are not approximating anything, and their output satisfies exact statements: a tree drawn by Reingold-Tilford has every parent centred over its children and no two subtrees overlapping, and a layered drawing of an acyclic graph has every arc pointing downward. Planarity sits apart from both: `planarity_test` answers whether a crossing-free drawing exists at all, and `planar_embedding_small` produces the combinatorial structure of one. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.graph.core import Graph +from numeria.monte_carlo import Rng +from numeria.math import Vec2 +from numeria.manifold.vecn import VecN + +def hop_distances(g: Graph) -> list[list[float]]: + """ +Hop distances between every pair, by breadth-first search from each +vertex. + +Edge weights are deliberately ignored: a drawing is laid out by graph +structure, and a weight of a thousand on one edge should not stretch the +picture by a factor of a thousand. Pairs in different components are given +one more than the largest finite distance, which is the usual convention +and keeps the stress function finite. + +Rust: `graph::layout::hop_distances` + """ + ... + +def layout_stress(g: Graph, positions: list[Vec2 | Sequence[float]]) -> float: + """ +The stress of a two-dimensional drawing: the weighted squared mismatch +between drawn and graph distance, `sum_{i float: + """ +The same stress functional for a drawing in any number of dimensions. + +Panics: +Panics unless there is one position per vertex. + +Rust: `graph::layout::stress_nd` + """ + ... + +def circular_layout(n: int) -> list[Vec2]: + """ +`n` points equally spaced around the unit circle, starting at `(1, 0)` and +going anticlockwise. + +The one layout with no free parameters and nothing to converge. Every +vertex is visible and no two coincide, which is why it is the standard +starting point for the iterative layouts here. + +Rust: `graph::layout::circular_layout` + """ + ... + +def shell_layout(g: Graph, shells: list[list[int]]) -> list[Vec2]: + """ +Concentric circles, one per shell, in the order given. + +A shell holding a single vertex is drawn at the centre; every other shell +`k` goes on the circle of radius `k + 1`, its members equally spaced. +Useful when the grouping is already known -- levels of a hierarchy, orbits +of a symmetry, distance classes from a root. + +Panics: +Panics unless the shells partition `0..g.n`. + +Rust: `graph::layout::shell_layout` + """ + ... + +def spectral_layout(g: Graph) -> list[Vec2]: + """ +Spectral layout: the two Laplacian eigenvectors just above the constant +one, used as coordinates. + +The constant vector is the Laplacian's zero eigenvector and carries no +information, so the drawing starts at the next two. Those minimise +`sum_edges |p_u - p_v|^2` subject to being centred and orthonormal, which +is to say they are the drawing that makes edges as short as possible +without collapsing everything to a point. Coordinates come out on the +scale of a unit vector; scale them for display. + +Panics: +Panics if the graph is directed, or has fewer than three vertices. + +Rust: `graph::layout::spectral_layout` + """ + ... + +def kamada_kawai(g: Graph, iters: int) -> list[Vec2]: + """ +Kamada-Kawai layout: move one vertex at a time to the position that best +matches its graph distances to everything else. + +The energy is `layout_stress`. Each round picks the vertex whose +gradient is largest and takes a Newton step on its two coordinates, which +converges quadratically near the solution. The step is accepted only if +the energy actually falls, so the sequence of drawings is monotone: the +result is never worse than the circular layout it starts from. Newton on a +non-convex energy will otherwise happily step uphill. + +Panics: +Panics if the graph is directed. + +Rust: `graph::layout::kamada_kawai` + """ + ... + +def stress_majorization(g: Graph, dim: int, iters: int) -> list[VecN]: + """ +Stress majorization (SMACOF) in `dim` dimensions. + +Each round replaces the stress by a quadratic that touches it at the +current drawing and lies above it everywhere else, then jumps to that +quadratic's minimum. Because the surrogate is an upper bound, the true +stress cannot rise -- which is the whole point, and the reason this is +preferred to gradient descent on the same objective: there is no step size +to tune and no way to overshoot. + +The starting drawing is classical scaling, the closed-form embedding that +best reproduces the *squared* distances. Majorization only ever descends, +so where it starts decides which local minimum it reaches; starting from +the classical solution makes the result deterministic and already close. + +Panics: +Panics if the graph is directed, or `dim` is zero. + +Rust: `graph::layout::stress_majorization` + """ + ... + +def fruchterman_reingold(g: Graph, iters: int, rng: Rng) -> list[Vec2]: + """ +Fruchterman-Reingold: vertices repel like charges, edges pull like +springs, and the whole thing cools. + +Repulsion is `k^2 / r` between every pair and attraction is `r^2 / k` +along every edge, for the ideal separation `k = sqrt(area / n)`. The two +balance at `r = k`, which is what sets the scale of the drawing. The +temperature caps how far any vertex may move in one round and falls +linearly to zero, so the layout freezes rather than oscillating -- the +method is a heuristic with no monotonicity guarantee, and the cooling is +what stands in for one. + +Panics: +Panics if the graph is directed. + +Rust: `graph::layout::fruchterman_reingold` + """ + ... + +def tree_layout_reingold_tilford(g: Graph, root: int) -> list[Vec2]: + """ +Reingold-Tilford tree layout. + +Depth sets the vertical position and the horizontal one is chosen so that +three things hold at once: no two subtrees overlap, every parent sits at +the midpoint of its first and last child, and the drawing is as narrow as +those two allow. The third is what the algorithm is for -- centring a +parent over its children is easy, and doing it while packing sibling +subtrees as tightly as their outlines permit is not. + +Packing works on *contours*: the leftmost and rightmost position each +subtree occupies at every depth. Two siblings are pushed apart by the +largest overlap between the right contour of everything placed so far and +the left contour of the newcomer, so subtrees interlock where their shapes +leave room. + +The root is at `(0, 0)` and depth `k` at `y = -k`, so the tree hangs +downward. Vertices unreachable from the root keep the origin. + +Panics: +Panics if the graph is directed, `root` is out of range, or the graph has +a cycle reachable from the root -- the layout is defined on trees. + +Rust: `graph::layout::tree_layout_reingold_tilford` + """ + ... + +def sugiyama_layered(dag: Graph) -> list[Vec2]: + """ +Sugiyama layered drawing of a directed acyclic graph. + +Layer `k` holds the vertices whose longest incoming path has `k` arcs, so +every arc goes from a strictly lower layer to a higher one and the drawing +reads in one direction. Within a layer the order is fixed by repeated +barycentre sweeps: put each vertex at the average position of its +neighbours in the adjacent layer, sort, and repeat, alternating direction. +That is Sugiyama's crossing-reduction heuristic; minimising crossings +exactly is NP-hard even for two layers. + +Returns `x` as the position within the layer and `y` as minus the layer, +so the arcs point downward. + +Panics: +Panics unless the graph is directed and acyclic. + +Rust: `graph::layout::sugiyama_layered` + """ + ... + +def crossing_number_estimate(g: Graph, layout: list[Vec2 | Sequence[float]]) -> int: + """ +The number of edge crossings in the straight-line drawing given by +`layout`. + +An upper bound on the graph's crossing number, and only that: the crossing +number is the minimum over all drawings, and a graph's best drawing need +not even be straight-line for a general graph. Edges sharing an endpoint +are never counted, and neither is a touching that is not a proper +crossing. + +Panics: +Panics unless there is one position per vertex. + +Rust: `graph::layout::crossing_number_estimate` + """ + ... + +def biconnected_components(g: Graph) -> list[list[tuple[int, int]]]: + """ +The edge sets of the biconnected components, each a maximal subgraph with +no cut vertex. + +A graph is planar exactly when every block is, which is what makes this +the right decomposition to plan a planarity test around: the blocks meet +only at single vertices, and a drawing of each can be rotated and scaled +into place around those without interfering. + +Self-loops are dropped and parallel edges collapsed, so each returned +block lists distinct simple edges. + +Panics: +Panics if the graph is directed. + +Rust: `graph::layout::biconnected_components` + """ + ... + +def planar_embedding_small(g: Graph) -> Optional[list[list[int]]]: + """ +A planar embedding of a biconnected graph, as its faces: each face is the +cyclic sequence of vertices bounding it. `None` if the graph is not +planar. + +By Demoucron's path-addition method. Start with any cycle, which divides +the plane into two faces, and grow: the parts of the graph not yet drawn +-- its *fragments* -- each attach to the drawn part at a set of vertices, +and a fragment can only go inside a face that contains all of them. If +some fragment fits nowhere, the graph is not planar. If a fragment fits in +exactly one face it is forced, so it is drawn first; otherwise any choice +will do, and that is the theorem the method rests on. Drawing a path of a +fragment across a face splits that face in two, and the process repeats +until every edge is drawn. + +The outer face is among those returned; which one it is depends on the +starting cycle, since on the sphere no face is distinguished. + +Panics: +Panics if the graph is directed, has a self-loop, has fewer than three +vertices, or is not biconnected. Use `planarity_test` for a graph that +is any of those: it decomposes into blocks first. + +Rust: `graph::layout::planar_embedding_small` + """ + ... + +def planarity_test(g: Graph) -> bool: + """ +Whether the graph can be drawn in the plane with no edge crossings. + +Exact, not an estimate. Parallel edges and self-loops are ignored, since +neither can make a drawable graph undrawable, and the graph is split into +its blocks: planarity holds for the whole exactly when it holds for each, +and each block is biconnected, which is what +`planar_embedding_small` needs. + +Panics: +Panics if the graph is directed. + +Rust: `graph::layout::planarity_test` + """ + ... diff --git a/bindings/python/python/numeria/graph/matching.pyi b/bindings/python/python/numeria/graph/matching.pyi new file mode 100644 index 0000000..4ed2634 --- /dev/null +++ b/bindings/python/python/numeria/graph/matching.pyi @@ -0,0 +1,193 @@ +""" +Matchings: bipartite, general, weighted, and stable. A matching is a set of edges no two of which share a vertex. It is returned as a partner array: `m[v]` is the vertex matched to `v`, or `None` when `v` is unmatched. That form is symmetric by construction, so `m[m[v]] == v` whenever `m[v]` is `Some`. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.graph.core import Graph +from numeria.linalg.matrix import Matrix + +def hopcroft_karp(left_n: int, right_n: int, edges: list[tuple[int, int]]) -> list[Optional[int]]: + """ +A maximum matching of a bipartite graph, by Hopcroft-Karp. + +The left side is `0..left_n` and the right side `0..right_n`, numbered +separately; `edges` gives `(left, right)` pairs. The returned array is +indexed by left vertex and holds the right vertex matched to it. + +Hopcroft-Karp augments along a maximal set of shortest augmenting paths at +once rather than one at a time, which bounds the number of phases by +`sqrt(V)` instead of `V`. + +Panics: +Panics if an edge names a vertex outside its side. + +Rust: `graph::matching::hopcroft_karp` + """ + ... + +def hungarian(cost: Matrix | Sequence[Sequence[float]]) -> tuple[float, list[int]]: + """ +The minimum-cost perfect assignment, by the Hungarian algorithm in its +`O(n^3)` shortest-augmenting-path form. + +`cost` must be square. Returns the total cost and the column assigned to +each row. + +The algorithm maintains dual potentials that keep every reduced cost +non-negative, so each augmenting search is a Dijkstra rather than a +Bellman-Ford; that is what turns the naive `O(n^4)` into `O(n^3)`. + +Panics: +Panics if `cost` is not square or contains a non-finite entry. + +Rust: `graph::matching::hungarian` + """ + ... + +def auction_assignment(cost: Matrix | Sequence[Sequence[float]], eps: float) -> tuple[float, list[int]]: + """ +The minimum-cost assignment by the auction algorithm. + +Rows bid for columns, raising each column's price by at least `eps` to win +it. The final assignment is within `n * eps` of optimal, so a small `eps` +buys accuracy at the cost of more rounds. Scaling `eps` down geometrically +-- which this does -- reaches the exact optimum for integer costs and a +very good one otherwise. + +Returns the total cost and the column assigned to each row. + +Panics: +Panics if `cost` is not square, contains a non-finite entry, or `eps` is +not positive. + +Rust: `graph::matching::auction_assignment` + """ + ... + +def blossom_max_matching(g: Graph) -> list[Optional[int]]: + """ +A maximum matching of a general graph, by Edmonds' blossom algorithm. + +The bipartite algorithms fail on odd cycles: an augmenting search can enter +one and come back out at the same vertex with the wrong parity. Edmonds' +insight is to contract each such cycle -- a blossom -- to a single vertex, +search the contracted graph, and lift the result back. + +The lifting is the part that is easy to get wrong. Contracting is not +enough: when a blossom forms, the parent pointers of every vertex on the +odd cycle have to be rewired so that a later augmenting path can be traced +back *through* the blossom the long way round. Without that rewiring the +traceback leaves the tree by the wrong edge and produces an asymmetric +pairing. `mark_blossom_path` below is what does it. + +Returns the partner array over all vertices. + +Panics: +Panics if the graph is directed. + +Rust: `graph::matching::blossom_max_matching` + """ + ... + +def stable_marriage(prefs_a: list[list[int]], prefs_b: list[list[int]]) -> list[int]: + """ +A stable marriage by the Gale-Shapley algorithm. + +`prefs_a[i]` ranks every member of the other side in decreasing preference, +and likewise `prefs_b`. Returns, for each member of side A, the member of +side B they are matched to. + +The result is the A-optimal stable matching: every proposer gets the best +partner they could have in any stable matching, and every receiver the +worst. That asymmetry is a property of the algorithm, not an artefact. + +Panics: +Panics unless both preference lists are complete permutations of the other +side, and the two sides are the same size. + +Rust: `graph::matching::stable_marriage` + """ + ... + +def stable_roommates(prefs: list[list[int]]) -> Optional[list[int]]: + """ +A stable roommates matching, or `None` when none exists. + +Unlike stable marriage, this is a single pool with no sides, and a stable +matching need not exist at all -- the smallest counterexample has four +people. Irving's algorithm: a proposal phase, then repeated elimination of +rotations. + +`prefs[i]` ranks the other `n - 1` people in decreasing preference. + +Panics: +Panics unless `n` is even and each list ranks exactly the other people. + +Rust: `graph::matching::stable_roommates` + """ + ... + +def konig_vertex_cover(g: Graph, left: list[int], matching: list[Optional[int]]) -> list[int]: + """ +A minimum vertex cover of a bipartite graph, by Konig's theorem. + +Konig's theorem says the minimum vertex cover of a bipartite graph has +exactly the size of its maximum matching, and names the cover: start an +alternating search from the unmatched left vertices, then take the left +vertices *not* reached together with the right vertices that are. + +`left` names one side; `matching` is a partner array over all vertices. + +Panics: +Panics if `matching` is not symmetric, or `left` names a vertex twice. + +Rust: `graph::matching::konig_vertex_cover` + """ + ... + +def hall_condition_check(g: Graph, left: list[int]) -> None: + """ +Checks Hall's condition on a bipartite graph. + +Hall's theorem says a matching saturating the left side exists exactly when +every subset of the left has at least as many distinct neighbours as it has +members. Returns `Ok(())` when it holds, or the smallest violating subset +found. + +The violating set is not searched for over all `2^|L|` subsets: by Konig's +theorem the deficiency equals `|L|` minus the maximum matching, and the +unreached left vertices of the alternating search form a violating set. + +Errors: +Returns the violating subset of `left` when the condition fails. + +Rust: `graph::matching::hall_condition_check` + """ + ... + +def maximum_weight_bipartite(weights: Matrix | Sequence[Sequence[float]]) -> tuple[float, list[Optional[int]]]: + """ +The maximum-weight bipartite matching, allowing an unbalanced graph and +leaving a vertex unmatched when that pays better. + +`weights` is a left-by-right matrix. Reduces to the Hungarian algorithm by +padding to a square and negating, with the padding entries at zero so an +unprofitable match is never forced. + +Returns the total weight and the partner of each left vertex. + +Rust: `graph::matching::maximum_weight_bipartite` + """ + ... + +def matching_size(m: list[Optional[int]]) -> int: + """ +The number of edges in a partner array. + +Rust: `graph::matching::matching_size` + """ + ... diff --git a/bindings/python/python/numeria/graph/paths.pyi b/bindings/python/python/numeria/graph/paths.pyi new file mode 100644 index 0000000..8adee17 --- /dev/null +++ b/bindings/python/python/numeria/graph/paths.pyi @@ -0,0 +1,381 @@ +""" +Shortest paths, spanning trees, and tours. Distances are `f64` and an unreachable vertex is `f64::INFINITY`, so the results compose without an `Option` at every step. Predecessor arrays use `None` for the source and for unreachable vertices alike; the distance distinguishes the two. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.graph.core import Graph +from numeria.linalg.matrix import Matrix +from numeria.cfd.riemann import Prim + +def dijkstra(g: Graph, s: int) -> tuple[list[float], list[Optional[int]]]: + """ +Single-source shortest paths with non-negative weights, by Dijkstra. + +Returns the distances and the predecessor array. Unreached vertices have +distance `f64::INFINITY` and no predecessor. + +Panics: +Panics if any weight is negative, where the algorithm is simply wrong +rather than merely slow -- use `bellman_ford` instead. + +Rust: `graph::paths::dijkstra` + """ + ... + +def dijkstra_target(g: Graph, s: int, t: int) -> Optional[tuple[float, list[int]]]: + """ +The shortest path from `s` to `t` and its length, or `None` if `t` is +unreachable. + +Rust: `graph::paths::dijkstra_target` + """ + ... + +def bellman_ford(g: Graph, s: int) -> tuple[list[float], list[Optional[int]]]: + """ +Single-source shortest paths allowing negative weights, by Bellman-Ford. + +Errors: +Returns `NegativeCycle` when a cycle of negative total weight is +reachable from `s`, which is detected by one relaxation pass beyond the +`n - 1` that suffice when none exists. + +Rust: `graph::paths::bellman_ford` + """ + ... + +def floyd_warshall(g: Graph) -> Matrix: + """ +All-pairs shortest paths by Floyd-Warshall, `O(n^3)`. + +Entry `(i, j)` is the distance, `f64::INFINITY` when unreachable. Negative +cycles are not detected here; a negative diagonal entry in the result is +the sign of one. + +Rust: `graph::paths::floyd_warshall` + """ + ... + +def johnson(g: Graph) -> Matrix: + """ +All-pairs shortest paths by Johnson's algorithm: a Bellman-Ford pass from a +virtual source supplies potentials that make every weight non-negative, +then one Dijkstra per vertex. + +Faster than Floyd-Warshall on sparse graphs, and unlike plain Dijkstra it +tolerates negative weights. + +Errors: +Returns `NegativeCycle` if the graph contains one. + +Rust: `graph::paths::johnson` + """ + ... + +def a_star(g: Graph, s: int, t: int, h: Callable[[int], float]) -> Optional[tuple[float, list[int]]]: + """ +A* search with the heuristic `h`. + +Returns the path and its true length, or `None` if `t` is unreachable. The +result is optimal exactly when `h` is admissible -- never overestimating +the remaining distance -- and the search is efficient when `h` is also +consistent. An inadmissible heuristic still terminates but may return a +suboptimal path, which is the caller's trade to make. + +Panics: +Panics if any weight is negative. + +Rust: `graph::paths::a_star` + """ + ... + +def bidirectional_dijkstra(g: Graph, s: int, t: int) -> Optional[tuple[float, list[int]]]: + """ +Dijkstra from both ends at once, alternating between them. + +Both searches settle vertices; the answer is the best path through any +vertex either has reached, and the search stops once the two settled +radii sum to at least the best path found. On a graph where the reachable +set grows with the radius, this settles roughly the square root of the +vertices a one-sided search would. + +Panics: +Panics if any weight is negative. + +Rust: `graph::paths::bidirectional_dijkstra` + """ + ... + +def k_shortest_paths_yen(g: Graph, s: int, t: int, k: int) -> list[tuple[float, list[int]]]: + """ +The `k` shortest loopless paths from `s` to `t`, by Yen's algorithm. + +Returns them in increasing length, and may return fewer than `k` when +fewer exist. Each candidate is found by forcing a shared prefix with an +already-accepted path and forbidding the arc it took next, which is what +keeps the results distinct and loopless. + +Rust: `graph::paths::k_shortest_paths_yen` + """ + ... + +def widest_path(g: Graph, s: int, t: int) -> Optional[tuple[float, list[int]]]: + """ +The widest path: the one whose narrowest edge is as wide as possible. + +Also called the bottleneck shortest path or the maximum capacity path. +Dijkstra with `min` in place of `+` and `max` in place of `min`, which is +valid because `min` is monotone in the same way `+` is. + +Rust: `graph::paths::widest_path` + """ + ... + +def minimax_path(g: Graph, s: int, t: int) -> Optional[tuple[float, list[int]]]: + """ +The minimax path: the one whose widest edge is as narrow as possible. + +The dual of `widest_path`, and the path a minimum spanning tree gives +between any two vertices. + +Rust: `graph::paths::minimax_path` + """ + ... + +def dag_shortest(g: Graph, s: int) -> list[float]: + """ +Shortest distances from `s` in a DAG, by relaxing in topological order. + +Linear time and correct with negative weights, neither of which Dijkstra +manages. + +Panics: +Panics if the graph is not a DAG. + +Rust: `graph::paths::dag_shortest` + """ + ... + +def dag_longest(g: Graph, s: int) -> list[float]: + """ +Longest distances from `s` in a DAG. + +Longest path is NP-hard in general but linear on a DAG, since the +topological order removes any need to revisit. + +Panics: +Panics if the graph is not a DAG. + +Rust: `graph::paths::dag_longest` + """ + ... + +def count_paths_dag(g: Graph, s: int, t: int) -> int: + """ +The number of distinct directed paths from `s` to `t` in a DAG. + +Exact, because the count grows exponentially: a grid DAG of side `n` has +`C(2n, n)` paths, past `u64` before `n = 34`. + +Panics: +Panics if the graph is not a DAG. + +Rust: `graph::paths::count_paths_dag` + """ + ... + +def transitive_closure(g: Graph) -> list[list[bool]]: + """ +The reachability matrix: `[i][j]` is true when `j` is reachable from `i`. + +Every vertex reaches itself. + +Rust: `graph::paths::transitive_closure` + """ + ... + +def minimum_spanning_tree_kruskal(g: Graph) -> tuple[float, list[tuple[int, int]]]: + """ +A minimum spanning forest by Kruskal's algorithm: sort the edges, accept +each one that joins two different components. + +Returns the total weight and the edges, each with `u < v`. On a +disconnected graph this is a spanning forest, and the edge count is +`n - components` rather than `n - 1`. + +Rust: `graph::paths::minimum_spanning_tree_kruskal` + """ + ... + +def minimum_spanning_tree_prim(g: Graph) -> tuple[float, list[tuple[int, int]]]: + """ +A minimum spanning forest by Prim's algorithm: grow a tree from each +unvisited vertex, always taking the cheapest edge leaving it. + +Returns the same weight as Kruskal on any graph, though possibly a +different tree when weights tie. + +Rust: `graph::paths::minimum_spanning_tree_prim` + """ + ... + +def minimum_spanning_tree_boruvka(g: Graph) -> tuple[float, list[tuple[int, int]]]: + """ +A minimum spanning forest by Boruvka's algorithm: every component picks its +own cheapest outgoing edge, and all of them are added at once. + +Halves the component count per round, so `O(log n)` rounds suffice. Ties +are broken by edge index, which is what stops two components from each +picking the other's edge and forming a cycle. + +Rust: `graph::paths::minimum_spanning_tree_boruvka` + """ + ... + +def second_best_mst(g: Graph) -> Optional[tuple[float, list[tuple[int, int]]]]: + """ +The second-best spanning tree: the cheapest spanning tree that differs from +the minimum one in at least one edge. + +Found by swapping: for each non-tree edge, adding it creates one cycle, and +removing the heaviest tree edge on that cycle gives the cheapest tree +containing it. The best such swap is the answer. + +Returns `None` when the graph is disconnected or has no non-tree edge, so +no second tree exists. + +Rust: `graph::paths::second_best_mst` + """ + ... + +def steiner_tree_small(g: Graph, terminals: list[int]) -> tuple[float, list[tuple[int, int]]]: + """ +A minimum Steiner tree spanning the given terminals, by Dreyfus-Wagner. + +Returns the weight and the edges. The tree may use non-terminal vertices, +which is what separates the problem from a spanning tree. Costs +`O(3^t n + 2^t n^2)` for `t` terminals, so the terminal count is what has +to stay small, not the graph. + +Panics: +Panics if there are more than 12 terminals, or a terminal is out of range. + +Rust: `graph::paths::steiner_tree_small` + """ + ... + +def traveling_salesman_exact(dist: Matrix | Sequence[Sequence[float]]) -> tuple[float, list[int]]: + """ +The exact optimal travelling salesman tour, by Held-Karp. + +Returns the tour length and the tour as a vertex sequence starting and +ending at 0, with the final return implied rather than repeated. Costs +`O(2^n n^2)` time and `O(2^n n)` memory. + +Panics: +Panics if `dist` is not square, or has more than 20 rows. + +Rust: `graph::paths::traveling_salesman_exact` + """ + ... + +def tsp_nearest_neighbor(dist: Matrix | Sequence[Sequence[float]]) -> tuple[float, list[int]]: + """ +A nearest-neighbour tour: repeatedly walk to the closest unvisited city. + +Fast and usually poor: on a metric instance it can be a logarithmic factor +worse than optimal, so it is a starting point for `tsp_2opt` rather than +an answer. + +Panics: +Panics if `dist` is not square. + +Rust: `graph::paths::tsp_nearest_neighbor` + """ + ... + +def tour_length(dist: Matrix | Sequence[Sequence[float]], tour: list[int]) -> float: + """ +The length of a closed tour under `dist`. + +Rust: `graph::paths::tour_length` + """ + ... + +def tsp_2opt(dist: Matrix | Sequence[Sequence[float]], tour: list[int]) -> tuple[float, list[int]]: + """ +2-opt local search: repeatedly reverse a tour segment when that shortens +the tour, until no single reversal helps. + +The result is 2-optimal, not optimal. On a symmetric instance a reversal +changes only the two edges at its ends, which is what makes each move an +`O(1)` decision. + +Panics: +Panics if `dist` is not square, or `tour` is not a permutation of its rows. + +Rust: `graph::paths::tsp_2opt` + """ + ... + +def tsp_or_opt(dist: Matrix | Sequence[Sequence[float]], tour: list[int]) -> tuple[float, list[int]]: + """ +Or-opt local search: relocate a run of one, two or three consecutive cities +elsewhere in the tour, in either orientation, while that shortens it. + +Complements 2-opt, which can only reverse: a run that belongs elsewhere +entirely is a move 2-opt cannot make in one step. + +Panics: +Panics if `dist` is not square, or `tour` is not a permutation of its rows. + +Rust: `graph::paths::tsp_or_opt` + """ + ... + +def tsp_christofides(dist: Matrix | Sequence[Sequence[float]]) -> Optional[tuple[float, list[int]]]: + """ +Christofides' tour, which is within a factor of 1.5 of optimal on a metric +instance. + +Takes a minimum spanning tree, adds a minimum-weight perfect matching on +the odd-degree vertices to make every degree even, walks the resulting +Eulerian circuit, and shortcuts repeats. The matching here is exact by +brute force over pairings, which is affordable because a tree has few +odd-degree vertices on the instances this is used for, and is refused +beyond sixteen of them rather than silently degrading to a greedy one. + +Returns `None` when the odd set is too large for the exact matching. + +Panics: +Panics if `dist` is not square or is not symmetric, since the guarantee +needs a metric. + +Rust: `graph::paths::tsp_christofides` + """ + ... + +def chinese_postman(g: Graph) -> Optional[tuple[float, list[int]]]: + """ +A shortest closed walk crossing every edge at least once: the Chinese +postman problem. + +Returns the walk's total weight and the vertex sequence. When every degree +is already even the answer is an Eulerian circuit and costs exactly the +total edge weight; otherwise the odd-degree vertices are paired up by a +minimum-weight perfect matching over shortest paths, and those paths are +duplicated. Returns `None` when the edges span more than one component, so +that no single closed walk can cross them all, or when the odd set is too +large for the exact matching. An edgeless graph has nothing to cross, so it +returns the empty route rather than failing on being disconnected. + +Panics: +Panics if the graph is directed, where the construction differs. + +Rust: `graph::paths::chinese_postman` + """ + ... diff --git a/bindings/python/python/numeria/graph/spectral.pyi b/bindings/python/python/numeria/graph/spectral.pyi new file mode 100644 index 0000000..faf7a24 --- /dev/null +++ b/bindings/python/python/numeria/graph/spectral.pyi @@ -0,0 +1,475 @@ +""" +Spectral graph theory: Laplacians, centralities, resistances, and community detection. The Laplacian `L = D - A` is the object almost everything here rests on. It is symmetric positive semi-definite for an undirected graph, its smallest eigenvalue is always zero with the all-ones eigenvector, and the multiplicity of that zero is the number of connected components. The second-smallest eigenvalue -- the algebraic connectivity -- measures how hard the graph is to cut, and its eigenvector orders the vertices in a way that separates the graph well. Weights are treated as edge multiplicities where that makes sense (Laplacian, resistance, random walks) and ignored where it does not (the combinatorial centralities, which count edges). +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.graph.core import Graph +from numeria.linalg.matrix import Matrix +from numeria.monte_carlo import Rng + +def laplacian_matrix(g: Graph) -> Matrix: + """ +The combinatorial Laplacian `L = D - A`. + +The degree is the weighted degree, so `L` has row sums of exactly zero and +the all-ones vector is always in its kernel. Self-loops contribute to +neither the degree nor the adjacency, since they cancel. + +Panics: +Panics if the graph is directed. + +Rust: `graph::spectral::laplacian_matrix` + """ + ... + +def weighted_degrees(g: Graph) -> list[float]: + """ +The weighted degree of each vertex, ignoring self-loops. + +Rust: `graph::spectral::weighted_degrees` + """ + ... + +def normalized_laplacian(g: Graph) -> Matrix: + """ +The symmetric normalized Laplacian `I - D^(-1/2) A D^(-1/2)`. + +Its spectrum lies in `[0, 2]` whatever the graph, which is what makes it +the right object for comparing graphs of different sizes and densities. +The upper end is reached exactly on a bipartite component. An isolated +vertex has no degree to normalize by and is given a diagonal of zero. + +Panics: +Panics if the graph is directed. + +Rust: `graph::spectral::normalized_laplacian` + """ + ... + +def adjacency_spectrum(g: Graph) -> list[float]: + """ +The adjacency eigenvalues, ascending. + +Panics: +Panics if the graph is directed, or the solver fails to converge. + +Rust: `graph::spectral::adjacency_spectrum` + """ + ... + +def laplacian_spectrum(g: Graph) -> list[float]: + """ +The Laplacian eigenvalues, ascending. The first is always zero. + +Panics: +Panics if the graph is directed, or the solver fails to converge. + +Rust: `graph::spectral::laplacian_spectrum` + """ + ... + +def normalized_laplacian_spectrum(g: Graph) -> list[float]: + """ +The normalized Laplacian eigenvalues, ascending. All lie in `[0, 2]`. + +Panics: +Panics if the graph is directed, or the solver fails to converge. + +Rust: `graph::spectral::normalized_laplacian_spectrum` + """ + ... + +def algebraic_connectivity(g: Graph) -> float: + """ +The algebraic connectivity: the second-smallest Laplacian eigenvalue. + +Zero exactly when the graph is disconnected, and larger the harder the +graph is to cut. Returns zero for fewer than two vertices. + +Panics: +Panics if the graph is directed. + +Rust: `graph::spectral::algebraic_connectivity` + """ + ... + +def fiedler_vector(g: Graph) -> list[float]: + """ +The Fiedler vector: the Laplacian eigenvector for the second-smallest +eigenvalue. + +Its sign pattern is the classic spectral bisection, and its ordering is a +good one-dimensional embedding of the graph. Normalized to unit length, +with the sign fixed so the first non-zero entry is positive -- an +eigenvector is only defined up to sign, and leaving that free would make +the output unreproducible. + +Panics: +Panics if the graph is directed, or has fewer than two vertices. + +Rust: `graph::spectral::fiedler_vector` + """ + ... + +def spectral_bisection(g: Graph) -> list[bool]: + """ +Spectral bisection: split the vertices by the sign of the Fiedler vector. + +Panics: +Panics if the graph is directed, or has fewer than two vertices. + +Rust: `graph::spectral::spectral_bisection` + """ + ... + +def spectral_clustering(g: Graph, k: int, rng: Rng) -> list[int]: + """ +Spectral clustering into `k` groups. + +Embeds each vertex in the `k` lowest Laplacian eigenvectors and runs +k-means there. The embedding is what does the work: in it, vertices that +are hard to separate by cutting edges sit close together, so a distance +clustering in that space corresponds to a good cut in the graph. + +Panics: +Panics if the graph is directed, `k` is zero, or `k` exceeds the vertex +count. + +Rust: `graph::spectral::spectral_clustering` + """ + ... + +def number_spanning_trees(g: Graph) -> float: + """ +The number of spanning trees, by Kirchhoff's matrix-tree theorem. + +Any cofactor of the Laplacian gives the count; this uses the product of +the non-zero Laplacian eigenvalues divided by `n`, which is the same +number and needs no pivoting. Returns zero for a disconnected graph. + +The result is a float and is only exact while the count stays inside 53 +bits; `graph::core::spanning_tree_count_exact` does it over the +integers. + +Panics: +Panics if the graph is directed. + +Rust: `graph::spectral::number_spanning_trees` + """ + ... + +def number_spanning_trees_exact(g: Graph) -> int: + """ +The number of spanning trees, exactly. + +Re-exported from `graph::core::spanning_tree_count_exact` so the +spectral module offers both the float and the exact form side by side. + +Panics: +Panics if the graph is directed. + +Rust: `graph::spectral::number_spanning_trees_exact` + """ + ... + +def pagerank(g: Graph, damping: float, tol: float) -> list[float]: + """ +PageRank with the given damping factor. + +The rank vector is the stationary distribution of a random surfer who +follows an out-link with probability `damping` and teleports uniformly +otherwise. A vertex with no out-links would leak probability, so its mass +is redistributed uniformly -- without that the result would not sum to one. + +Returns a distribution summing to one. + +Panics: +Panics unless `damping` is in `[0, 1)` and `tol` is positive. + +Rust: `graph::spectral::pagerank` + """ + ... + +def hits(g: Graph, tol: float) -> tuple[list[float], list[float]]: + """ +HITS: the hub and authority scores. + +A good authority is pointed to by good hubs and a good hub points to good +authorities, which is a mutual recurrence solved by alternating updates. +Both vectors are normalized to unit length. + +Panics: +Panics unless `tol` is positive. + +Rust: `graph::spectral::hits` + """ + ... + +def eigenvector_centrality(g: Graph, tol: float) -> list[float]: + """ +Eigenvector centrality: the principal eigenvector of the adjacency matrix. + +A vertex is important when its neighbours are, which is exactly the +eigenvector equation. Found by power iteration; the result is +non-negative by Perron-Frobenius and is normalized to unit length. + +Panics: +Panics unless `tol` is positive. + +Rust: `graph::spectral::eigenvector_centrality` + """ + ... + +def katz_centrality(g: Graph, alpha: float) -> list[float]: + """ +Katz centrality: the attenuated count of walks reaching each vertex. + +`x = (I - alpha A)^-1 * 1 - 1`, summed over walk lengths with each step +weighted by `alpha`. Converges only when `alpha` is below the reciprocal +of the largest adjacency eigenvalue, which is the caller's responsibility; +beyond that the walk count diverges and so does the series. + +Panics: +Panics unless `alpha` is positive. + +Rust: `graph::spectral::katz_centrality` + """ + ... + +def betweenness_centrality(g: Graph) -> list[float]: + """ +Betweenness centrality, by Brandes' algorithm. + +The number of shortest paths through each vertex, summed over all source +and target pairs and normalized by how many shortest paths there are. +Brandes computes it in `O(VE)` by accumulating dependencies backwards +along one shortest-path DAG per source, rather than enumerating the +quadratically many pairs. + +Counts hops rather than weights. An undirected graph counts each unordered +pair once, so the values are halved. + +Rust: `graph::spectral::betweenness_centrality` + """ + ... + +def closeness_centrality(g: Graph) -> list[float]: + """ +Closeness centrality: the reciprocal of the mean hop distance to every +reachable vertex, scaled by the fraction reachable. + +The scaling is what makes the value comparable across components: without +it, a vertex in a small tight component would outrank one in a large +well-connected component. + +Rust: `graph::spectral::closeness_centrality` + """ + ... + +def harmonic_centrality(g: Graph) -> list[float]: + """ +Harmonic centrality: the sum of reciprocal distances. + +Unlike closeness this needs no special case for a disconnected graph -- an +unreachable vertex contributes `1/infinity = 0` -- which is why it is +preferred when the graph may not be connected. + +Rust: `graph::spectral::harmonic_centrality` + """ + ... + +def effective_resistance(g: Graph, u: int, v: int) -> float: + """ +The effective resistance between two vertices, treating each edge as a +conductance equal to its weight. + +`R(u,v) = L+(u,u) + L+(v,v) - 2 L+(u,v)` for the Laplacian pseudoinverse. +Infinite when the two lie in different components. + +Panics: +Panics if the graph is directed, or an endpoint is out of range. + +Rust: `graph::spectral::effective_resistance` + """ + ... + +def resistance_matrix(g: Graph) -> Matrix: + """ +The effective resistance between every pair. + +Panics: +Panics if the graph is directed. + +Rust: `graph::spectral::resistance_matrix` + """ + ... + +def commute_time(g: Graph, u: int, v: int) -> float: + """ +The commute time between two vertices: the expected number of steps for a +random walk to go from `u` to `v` and back. + +Equal to `2m * R(u,v)` for total edge weight `m`, which is the theorem +that makes effective resistance a graph distance rather than merely an +analogy. + +Panics: +Panics if the graph is directed. + +Rust: `graph::spectral::commute_time` + """ + ... + +def random_walk_stationary(g: Graph) -> list[float]: + """ +The stationary distribution of a simple random walk. + +On a connected undirected graph this is the degree distribution: the walk +spends time at a vertex in proportion to its weighted degree. + +Panics: +Panics if the graph is directed. + +Rust: `graph::spectral::random_walk_stationary` + """ + ... + +def mixing_time_estimate(g: Graph, eps: float) -> float: + """ +An estimate of the mixing time: how many steps until the walk is within +`eps` of stationary in total variation. + +Bounded by `log(1/(eps * pi_min)) / (1 - lambda2)` for the second-largest +transition eigenvalue in magnitude, which relates mixing to the spectral +gap. Infinite when the graph is disconnected or bipartite, where the walk +does not converge at all. + +Panics: +Panics if the graph is directed, or `eps` is not in `(0, 1)`. + +Rust: `graph::spectral::mixing_time_estimate` + """ + ... + +def cheeger_bound(g: Graph) -> tuple[float, float]: + """ +The Cheeger bounds on the graph's conductance. + +Cheeger's inequality brackets the conductance `h` between `mu/2` and +`sqrt(2 mu)` for the second-smallest normalized Laplacian eigenvalue `mu`. +Returns `(lower, upper)`. + +Panics: +Panics if the graph is directed, or has fewer than two vertices. + +Rust: `graph::spectral::cheeger_bound` + """ + ... + +def expander_check(g: Graph, target_gap: float) -> bool: + """ +True when the graph's spectral gap is at least `target_gap`. + +The gap is what makes an expander an expander: a large gap forces every +cut to be expensive, by Cheeger's inequality. + +Panics: +Panics if the graph is directed. + +Rust: `graph::spectral::expander_check` + """ + ... + +def graph_energy(g: Graph) -> float: + """ +The graph energy: the sum of the absolute adjacency eigenvalues. + +Panics: +Panics if the graph is directed. + +Rust: `graph::spectral::graph_energy` + """ + ... + +def estrada_index(g: Graph) -> float: + """ +The Estrada index: the sum of `exp(lambda)` over the adjacency spectrum. + +Equal to the trace of `exp(A)`, which counts closed walks with each length +weighted by the reciprocal of its factorial. + +Panics: +Panics if the graph is directed. + +Rust: `graph::spectral::estrada_index` + """ + ... + +def isospectral_check(g: Graph, h: Graph, tol: float) -> bool: + """ +True when two graphs have the same adjacency spectrum to within `tol`. + +Isomorphic graphs are always isospectral; the converse is false, which is +what makes the spectrum a cheap but incomplete invariant. + +Panics: +Panics if either graph is directed. + +Rust: `graph::spectral::isospectral_check` + """ + ... + +def modularity(g: Graph, communities: list[int]) -> float: + """ +Newman's modularity of a vertex partition. + +The fraction of edge weight inside communities, minus what that fraction +would be if the same degrees were wired at random. Positive means the +partition captures more structure than chance; the maximum over all +partitions is what community detection tries to find. + +Panics: +Panics if the graph is directed, or `communities` does not have one label +per vertex. + +Rust: `graph::spectral::modularity` + """ + ... + +def community_louvain(g: Graph, rng: Rng) -> list[int]: + """ +Community detection by the Louvain method. + +Two phases repeated: move each vertex to whichever neighbouring community +most improves modularity, then contract each community to a single vertex +and repeat on the smaller graph. The contraction is what lets it find +structure at several scales rather than only among immediate neighbours. + +Labels are renumbered from zero in order of first appearance. + +Panics: +Panics if the graph is directed. + +Rust: `graph::spectral::community_louvain` + """ + ... + +def label_propagation(g: Graph, rng: Rng) -> list[int]: + """ +Community detection by label propagation. + +Each vertex repeatedly adopts the label carried by the greatest weight +among its neighbours, ties broken at random. Near-linear and parameter- +free, but the outcome depends on the visiting order, which is why the +generator is a parameter rather than fixed. + +Panics: +Panics if the graph is directed. + +Rust: `graph::spectral::label_propagation` + """ + ... diff --git a/bindings/python/python/numeria/gravitation.pyi b/bindings/python/python/numeria/gravitation.pyi new file mode 100644 index 0000000..73b93a8 --- /dev/null +++ b/bindings/python/python/numeria/gravitation.pyi @@ -0,0 +1,126 @@ +""" +Newtonian gravity and two-body orbits. The inverse-square force and its potential energy, the field of a point mass, escape and circular orbital velocity, and Kepler's third law in both directions. The vis-viva equation `v² = μ(2/r − 1/a)` ties speed to position on any conic orbit, and the specific orbital energy fixes which conic it is. Also the Roche limit, the Hill sphere, the Schwarzschild radius and gravitational time dilation -- the last two are the points at which Newtonian gravity stops being enough; see `general_relativity`. For orbits propagated rather than characterised, and for transfers between them, see `astrophysics`. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.math import Vec3 + +def gravitational_force(m1: float, m2: float, distance: float) -> float: + """ +Gravitational force magnitude between two masses: F = G * m1 * m2 / r^2 + +Rust: `gravitation::gravitational_force` + """ + ... + +def gravitational_force_vec(m1: float, pos1: Vec3 | Sequence[float], m2: float, pos2: Vec3 | Sequence[float]) -> Vec3: + """ +Gravitational force vector from body1 toward body2. + +Rust: `gravitation::gravitational_force_vec` + """ + ... + +def gravitational_potential_energy(m1: float, m2: float, distance: float) -> float: + """ +Gravitational potential energy: U = -G * m1 * m2 / r + +Rust: `gravitation::gravitational_potential_energy` + """ + ... + +def gravitational_field(mass: float, distance: float) -> float: + """ +Gravitational field strength at distance r from mass M: g = G * M / r^2 + +Rust: `gravitation::gravitational_field` + """ + ... + +def escape_velocity(mass: float, radius: float) -> float: + """ +Escape velocity from a body of mass M and radius r: v = sqrt(2GM/r) + +Rust: `gravitation::escape_velocity` + """ + ... + +def orbital_velocity(central_mass: float, orbital_radius: float) -> float: + """ +Orbital velocity for a circular orbit: v = sqrt(GM/r) + +Rust: `gravitation::orbital_velocity` + """ + ... + +def orbital_period(central_mass: float, orbital_radius: float) -> float: + """ +Orbital period (Kepler's third law): T = 2π * sqrt(r^3 / (G*M)) + +Rust: `gravitation::orbital_period` + """ + ... + +def semi_major_axis_from_period(central_mass: float, period: float) -> float: + """ +Semi-major axis from orbital period (inverse Kepler's third law): +a = (G*M*T^2 / (4π^2))^(1/3) + +Rust: `gravitation::semi_major_axis_from_period` + """ + ... + +def schwarzschild_radius(mass: float) -> float: + """ +Schwarzschild radius of a black hole: r_s = 2GM / c^2 + +Rust: `gravitation::schwarzschild_radius` + """ + ... + +def gravitational_time_dilation(mass: float, distance: float) -> float: + """ +Gravitational time dilation factor at distance r from mass M: +sqrt(1 - 2GM/(rc^2)) + +Rust: `gravitation::gravitational_time_dilation` + """ + ... + +def roche_limit(primary_radius: float, primary_density: float, satellite_density: float) -> float: + """ +Roche limit (fluid body): d = R * (2 * ρ_M / ρ_m)^(1/3) +R = radius of primary, ρ_M = density of primary, ρ_m = density of satellite + +Rust: `gravitation::roche_limit` + """ + ... + +def vis_viva(central_mass: float, distance: float, semi_major_axis: float) -> float: + """ +Vis-viva equation: v^2 = GM * (2/r - 1/a) +Returns the orbital speed at distance r for an orbit with semi-major axis a. + +Rust: `gravitation::vis_viva` + """ + ... + +def specific_orbital_energy(central_mass: float, semi_major_axis: float) -> float: + """ +Specific orbital energy: ε = -GM / (2a) + +Rust: `gravitation::specific_orbital_energy` + """ + ... + +def hill_sphere_radius(semi_major_axis: float, orbiting_mass: float, central_mass: float) -> float: + """ +Hill sphere radius: r_H ≈ a * (m / (3M))^(1/3) + +Rust: `gravitation::hill_sphere_radius` + """ + ... diff --git a/bindings/python/python/numeria/information_theory.pyi b/bindings/python/python/numeria/information_theory.pyi new file mode 100644 index 0000000..3f968c6 --- /dev/null +++ b/bindings/python/python/numeria/information_theory.pyi @@ -0,0 +1,156 @@ +""" +Shannon information: entropy, divergence, and channel capacity. Entropy in bits and in nats, the maximum-entropy bound for a given alphabet, and the entropy rate. Then the relations between two distributions: cross entropy, Kullback-Leibler divergence, and the Jensen-Shannon divergence -- which unlike KL is symmetric and bounded, which is why it is the one that behaves like a distance. Mutual information and conditional entropy connect the two, and the binary entropy function gives the capacity of a binary symmetric channel as `C = 1 − H₂(p)`. Fisher information and the Cramér-Rao bound cover the estimation side. For codes that approach these limits see `codes`. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def shannon_entropy(probabilities: list[float]) -> float: + """ +Shannon information: entropy, divergence, and channel capacity. + +Entropy in bits and in nats, the maximum-entropy bound for a given +alphabet, and the entropy rate. Then the relations between two +distributions: cross entropy, Kullback-Leibler divergence, and the +Jensen-Shannon divergence -- which unlike KL is symmetric and bounded, +which is why it is the one that behaves like a distance. + +Mutual information and conditional entropy connect the two, and the +binary entropy function gives the capacity of a binary symmetric +channel as `C = 1 − H₂(p)`. Fisher information and the Cramér-Rao +bound cover the estimation side. + +For codes that approach these limits see `codes`. +H = -Σ pi × log₂(pi), skipping pi = 0. + +Rust: `information_theory::shannon_entropy` + """ + ... + +def shannon_entropy_nats(probabilities: list[float]) -> float: + """ +H = -Σ pi × ln(pi), in nats. + +Rust: `information_theory::shannon_entropy_nats` + """ + ... + +def max_entropy(n_symbols: int) -> float: + """ +H_max = log₂(N) for N equally likely symbols. + +Rust: `information_theory::max_entropy` + """ + ... + +def entropy_rate(conditional_entropy: float) -> float: + """ +Identity function returning the conditional entropy value. Provided for API +completeness so callers can be explicit about what the quantity represents. + +Rust: `information_theory::entropy_rate` + """ + ... + +def kl_divergence(p: list[float], q: list[float]) -> float: + """ +D_KL(P||Q) = Σ pi × ln(pi/qi), skipping pi = 0. + +Rust: `information_theory::kl_divergence` + """ + ... + +def js_divergence(p: list[float], q: list[float]) -> float: + """ +Jensen-Shannon divergence: JSD(P||Q) = (D_KL(P||M) + D_KL(Q||M)) / 2 +where M = (P + Q) / 2. + +Rust: `information_theory::js_divergence` + """ + ... + +def cross_entropy(p: list[float], q: list[float]) -> float: + """ +H(P, Q) = -Σ pi × log₂(qi). + +Rust: `information_theory::cross_entropy` + """ + ... + +def mutual_information(joint: list[float], marginal_x: list[float], marginal_y: list[float], nx: int, ny: int) -> float: + """ +I(X;Y) = Σ p(x,y) × ln(p(x,y) / (p(x) × p(y))). +`joint` is an nx×ny row-major probability table. + +Rust: `information_theory::mutual_information` + """ + ... + +def conditional_entropy(joint: list[float], marginal_condition: list[float], nx: int, ny: int) -> float: + """ +H(Y|X) = -Σ p(x,y) × ln(p(y|x)). +`joint` is an nx×ny row-major probability table, `marginal_condition` are +the marginal probabilities p(x). + +Rust: `information_theory::conditional_entropy` + """ + ... + +def binary_entropy(p: float) -> float: + """ +H(p) = -p × log₂(p) - (1-p) × log₂(1-p). + +Rust: `information_theory::binary_entropy` + """ + ... + +def binary_symmetric_channel_capacity(error_prob: float) -> float: + """ +C = 1 - H(p) for a binary symmetric channel with crossover probability p. + +Rust: `information_theory::binary_symmetric_channel_capacity` + """ + ... + +def compression_ratio(original_bits: float, compressed_bits: float) -> float: + """ +R = original_bits / compressed_bits. + +Rust: `information_theory::compression_ratio` + """ + ... + +def redundancy(entropy: float, max_entropy: float) -> float: + """ +D = 1 - H / H_max. + +Rust: `information_theory::redundancy` + """ + ... + +def efficiency(entropy: float, avg_code_length: float) -> float: + """ +η = H / L where L is average code length. + +Rust: `information_theory::efficiency` + """ + ... + +def fisher_information_gaussian(sigma: float) -> float: + """ +I(μ) = 1/σ² for a Gaussian when estimating the mean. + +Rust: `information_theory::fisher_information_gaussian` + """ + ... + +def cramer_rao_bound(fisher_info: float) -> float: + """ +Cramér-Rao lower bound: var(θ̂) ≥ 1 / I(θ). + +Rust: `information_theory::cramer_rao_bound` + """ + ... diff --git a/bindings/python/python/numeria/learn/__init__.pyi b/bindings/python/python/numeria/learn/__init__.pyi new file mode 100644 index 0000000..5a7d515 --- /dev/null +++ b/bindings/python/python/numeria/learn/__init__.pyi @@ -0,0 +1,12 @@ +""" +Learning algorithms, written to be read rather than to be fast. Every method here has a closed-form or exactly-checkable property attached to it, because that is what makes a learning algorithm testable at all. A network that trains to a plausible loss is not evidence of anything -- gradient descent will happily reduce the loss of a model whose gradients are wrong, just more slowly. What settles it is comparing the analytic gradient against a finite difference, comparing a linear model fitted by descent against the normal equations, or checking that a clustering agrees with itself under a relabelling. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from . import cluster, gp, nn, tree + + diff --git a/bindings/python/python/numeria/learn/cluster.pyi b/bindings/python/python/numeria/learn/cluster.pyi new file mode 100644 index 0000000..5772c7d --- /dev/null +++ b/bindings/python/python/numeria/learn/cluster.pyi @@ -0,0 +1,304 @@ +""" +Clustering, mixture models and nearest neighbours. # Clustering has no ground truth, so the tests need invariants Nothing here has a right answer to compare against. What it has instead is a supply of exact statements, and those are what the tests use: *Lloyd's algorithm cannot go uphill.* Each half of a k-means iteration -- reassigning points to their nearest centre, then moving each centre to its cluster's mean -- minimises the same objective over one of its two arguments, so the inertia is non-increasing and the algorithm terminates in finitely many steps. There are finitely many assignments and none repeats. *Expectation-maximisation cannot go downhill.* The same argument in the other direction: each step maximises a lower bound that touches the log-likelihood at the current parameters, so the likelihood climbs monotonically. Both are asserted step by step rather than end to end, because a monotone sequence is a much sharper claim than an improved endpoint. *A label is not a name.* Cluster indices are arbitrary, so every comparison between two clusterings has to be invariant under relabelling either of them. `adjusted_rand_index` is, exactly, and it is corrected for chance so that two independent random partitions score about zero rather than about a half. # Where the guarantees stop, and why that is worth saying Single and complete linkage produce merge heights that never decrease, so their dendrograms can be drawn without crossings. *Centroid linkage does not.* Merging two clusters moves their centre to somewhere between them, which can be closer to a third cluster than either original was, and the dendrogram then contains an inversion. That is a property of the method, not a bug in it, and `Linkage::Centroid` is documented and tested as inverting rather than quietly producing dendrograms nobody should draw. DBSCAN's core points are determined by the data alone and do not depend on the order it arrives in. Its *border* points can: a point within reach of two clusters joins whichever claimed it first. That asymmetry is in the algorithm as Ester and colleagues defined it, and pretending otherwise would mean inventing a tie-break and calling it DBSCAN. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.linalg.matrix import Matrix +from numeria.monte_carlo import Rng + +class Gmm: + """ +A fitted Gaussian mixture. + +Rust: `learn::cluster::Gmm` + """ + def __init__(self, weights: list[float], means: list[list[float]], covariances: list[Matrix | Sequence[Sequence[float]]], log_likelihood_history: list[float]) -> None: ... + def log_likelihood(self) -> float: ... + @property + def weights(self) -> list[float]: ... + @property + def means(self) -> list[list[float]]: ... + @property + def covariances(self) -> list[Matrix]: ... + @property + def log_likelihood_history(self) -> list[float]: ... + +class KMeans: + """ +The outcome of a k-means run. + +Rust: `learn::cluster::KMeans` + """ + def __init__(self, centroids: list[list[float]], labels: list[int], inertia_history: list[float], iterations: int) -> None: ... + def inertia(self) -> float: ... + @property + def centroids(self) -> list[list[float]]: ... + @property + def labels(self) -> list[int]: ... + @property + def inertia_history(self) -> list[float]: ... + @property + def iterations(self) -> int: ... + +class Linkage: + """ +How the distance between two merged clusters is defined. + +Rust: `learn::cluster::Linkage` + """ + ... + +def kmeans_pp_init(data: list[list[float]], k: int, rng: Rng) -> list[list[float]]: + """ +Chooses `k` starting centres by the k-means++ rule: the first +uniformly at random, each subsequent one with probability +proportional to its squared distance from the nearest centre already +chosen. + +The rule matters. Uniform initialisation regularly puts two centres +in the same dense region and leaves another region unclaimed, and +Lloyd's algorithm cannot repair that -- it is a local method and the +bad split is a local optimum. The `D^2` weighting makes the expected +final inertia within a logarithmic factor of the best possible, +which is the only approximation guarantee k-means has. + +Errors: + +`SolveError::InvalidArgument` for an invalid dataset, `k == 0`, or +more centres than points. + +Rust: `learn::cluster::kmeans_pp_init` + """ + ... + +def kmeans(data: list[list[float]], k: int, iters: int, rng: Rng) -> KMeans: + """ +Lloyd's algorithm, restarted `RESTARTS` (10) times from independent +k-means++ starts, keeping the run with the lowest inertia. + +The result carries the winning run's inertia history, which is +non-increasing within that run -- see the module note. Use +`kmeans_once` to observe a single trajectory. + +Errors: + +`SolveError::InvalidArgument` for an invalid dataset, `k == 0`, +more clusters than points, or zero iterations. + +Rust: `learn::cluster::kmeans` + """ + ... + +def kmeans_once(data: list[list[float]], k: int, iters: int, rng: Rng) -> KMeans: + """ +A single run of Lloyd's algorithm from one k-means++ start. + +Runs until the assignment stops changing or `iters` iterations have +passed. The inertia after each iteration is recorded, and it is +non-increasing by construction. + +An empty cluster is refilled with the point currently furthest from +its own centre. Leaving it empty would silently return fewer clusters +than were asked for, and the mean of no points is not a number. + +Errors: + +As `kmeans`. + +Rust: `learn::cluster::kmeans_once` + """ + ... + +def elbow_data(data: list[list[float]], k_range: list[int], iters: int, rng: Rng) -> list[tuple[int, float]]: + """ +The final inertia for each cluster count in `k_range`, for plotting +an elbow. + +Inertia falls monotonically with `k` in expectation and reaches zero +when every point is its own cluster, so the number alone says +nothing -- the elbow is where the fall stops being worth the extra +cluster, and that is a judgement rather than a computation. The +function returns the curve and declines to pick a point on it. + +Errors: + +As `kmeans`, or `SolveError::InvalidArgument` for an empty range. + +Rust: `learn::cluster::elbow_data` + """ + ... + +def dbscan(data: list[list[float]], eps: float, min_pts: int) -> list[int]: + """ +Density-based clustering. Returns a label per point, with `-1` for +noise. + +A point is a *core* point if at least `min_pts` points (itself +included) lie within `eps`. Clusters are the connected components of +the core points, plus the non-core points within `eps` of one. + +Core points are determined by the data alone. Border points are not: +one within reach of two clusters joins whichever reaches it first, +which depends on the order the points arrive in. That is in the +algorithm as defined, not an artefact here -- see the module note. + +Errors: + +`SolveError::InvalidArgument` for an invalid dataset, a +non-positive `eps`, or `min_pts == 0`. + +Rust: `learn::cluster::dbscan` + """ + ... + +def hierarchical_agglomerative(data: list[list[float]], linkage: Linkage) -> list[tuple[int, int, float]]: + """ +Agglomerative clustering, returning the merges in order as +`(left, right, height)`. + +Cluster indices below `n` are the original points; the merge at step +`t` creates cluster `n + t`. Heights are Euclidean distances under +the chosen linkage. + +Errors: + +`SolveError::InvalidArgument` for an invalid dataset or fewer than +two points. + +Rust: `learn::cluster::hierarchical_agglomerative` + """ + ... + +def dendrogram_cut(merges: list[tuple[int, int, float]], n: int, k: int) -> list[int]: + """ +Cuts a dendrogram into `k` clusters, returning a label per original +point. + +Errors: + +`SolveError::InvalidArgument` if `k` is zero or exceeds the point +count, or if the merge list is not `n - 1` long. + +Rust: `learn::cluster::dendrogram_cut` + """ + ... + +def gaussian_mixture_em(data: list[list[float]], k: int, iters: int, rng: Rng) -> Gmm: + """ +Fits a Gaussian mixture by expectation-maximisation. + +Each step increases the log-likelihood, which is recorded so that the +monotonicity can be checked rather than assumed. + +Errors: + +`SolveError::InvalidArgument` for an invalid dataset, `k == 0`, +more components than points, or zero iterations; +`SolveError::NotPositiveDefinite` if a covariance cannot be +factored even with the floor applied. + +Rust: `learn::cluster::gaussian_mixture_em` + """ + ... + +def silhouette_score(data: list[list[float]], labels: list[int]) -> float: + """ +The mean silhouette over all points, in `[-1, 1]`. + +A point's silhouette compares the mean distance to its own cluster +against the mean distance to the nearest other cluster. One means +the clusters are tight and far apart; zero means the point sits on a +boundary; negative means it is closer to another cluster than its +own. A point alone in its cluster scores zero by convention -- there +is no within-cluster distance to compute, and calling it a perfect +one would reward splitting every point off. + +Errors: + +`SolveError::InvalidArgument` for an invalid dataset or fewer than +two distinct labels; `SolveError::DimensionMismatch` on a length +mismatch. + +Rust: `learn::cluster::silhouette_score` + """ + ... + +def adjusted_rand_index(a: list[int], b: list[int]) -> float: + """ +The adjusted Rand index between two partitions. + +Counts the pairs of points the two partitions agree about, then +subtracts what agreement would be expected by chance from partitions +with the same cluster sizes. Identical partitions score exactly one; +independent random ones score about zero, and may score below it. + +The correction is what makes the number usable. The unadjusted Rand +index of two random partitions of many points into a few clusters is +close to one, because most pairs are in different clusters under both +and that counts as agreement. + +Invariant under relabelling either partition, which is the minimum a +comparison between clusterings has to satisfy: a cluster index is not +a name. + +Errors: + +`SolveError::DimensionMismatch` if the two have different lengths; +`SolveError::InvalidArgument` if they are empty. + +Rust: `learn::cluster::adjusted_rand_index` + """ + ... + +def davies_bouldin(data: list[list[float]], labels: list[int]) -> float: + """ +The Davies-Bouldin index: the mean over clusters of the worst ratio +of within-cluster spread to between-cluster separation. + +Lower is better, and zero is unattainable. Unlike the silhouette it +is unbounded above, and unlike the silhouette it uses only the +centroids, so it is cheap and it is blind to cluster shape. + +Errors: + +As `silhouette_score`. + +Rust: `learn::cluster::davies_bouldin` + """ + ... + +def knn_classify(train: list[list[float]], labels: list[int], x: list[float], k: int) -> int: + """ +Classifies `x` by a majority vote of its `k` nearest neighbours. + +Ties are broken towards the smaller label, which is arbitrary but +deterministic; an even `k` on a two-class problem can produce them, +which is the usual reason to prefer an odd one. + +Errors: + +`SolveError::InvalidArgument` for an invalid or empty training set, +`k == 0`, or more neighbours than points; +`SolveError::DimensionMismatch` on a label count or query +dimension mismatch. + +Rust: `learn::cluster::knn_classify` + """ + ... + +def knn_regress(train: list[list[float]], targets: list[float], x: list[float], k: int) -> float: + """ +Predicts a value for `x` as the mean of its `k` nearest neighbours' +targets. + +Errors: + +As `knn_classify`. + +Rust: `learn::cluster::knn_regress` + """ + ... diff --git a/bindings/python/python/numeria/learn/gp.pyi b/bindings/python/python/numeria/learn/gp.pyi new file mode 100644 index 0000000..f3e6e0a --- /dev/null +++ b/bindings/python/python/numeria/learn/gp.pyi @@ -0,0 +1,55 @@ +""" +Gaussian process regression. # A distribution over functions, conditioned A Gaussian process says that any finite set of function values is jointly normal, with a covariance given by the kernel. Regression is then not fitting but conditioning: the posterior over an unobserved point is the conditional of a multivariate normal, and that has a closed form. There is no optimisation anywhere in `Gp::fit` -- it is one Cholesky factorisation, and the answer is exact given the kernel. Two consequences are worth stating because they surprise people and because they are exactly testable. *The posterior variance does not depend on what was observed.* It is `k(x,x) - k_*^T K^-1 k_*`, and `y` does not appear. Uncertainty in a Gaussian process is a statement about where the data *is*, not about what it said. Doubling every observation doubles the mean and leaves every error bar alone. *With no noise the mean interpolates exactly and the variance vanishes at the data.* The conditional of a normal on one of its own coordinates is a point mass. Adding noise is what turns interpolation into smoothing, and the residual at the data grows from zero in proportion to it. # Which kernel is a modelling choice, not a detail The kernel *is* the prior. A squared exponential asserts that the function is infinitely differentiable, which is a very strong claim and the reason its posterior can look implausibly smooth between widely spaced points. The Matern family asserts a finite number of derivatives -- `3/2` gives one, `5/2` gives two -- and is usually the better default for anything physical. A periodic kernel asserts exact periodicity, and `KernelFn::Periodic` satisfies `k(x, x + p) = k(x, x)` to rounding rather than approximately. Kernels are closed under addition and multiplication, which is what `KernelFn::Sum` and `KernelFn::Product` are for: a sum models additive structure (a trend plus a wiggle), a product models interaction (a periodicity whose amplitude decays). # The marginal likelihood balances fit against complexity on its own `log p(y | X)` splits into a data-fit term `-y^T K^-1 y / 2` and a complexity penalty `-log|K| / 2`. Making the kernel more flexible improves the first and costs the second, and the trade is not a hyperparameter anyone chose -- it falls out of the normalisation of a probability distribution. That is why hyperparameters can be tuned by maximising it without a validation set. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.monte_carlo import Rng + +class Gp: + """ +A fitted Gaussian process. + +Rust: `learn::gp::Gp` + """ + @staticmethod + def fit(kernel: KernelFn, noise: float, x: list[list[float]], y: list[float]) -> Gp: ... + def len(self) -> int: ... + def is_empty(self) -> bool: ... + def condition_estimate(self) -> float: ... + def predict(self, x_star: list[list[float]]) -> tuple[list[float], list[float]]: ... + def log_marginal_likelihood(self) -> float: ... + def optimize_hyperparams(self, restarts: int, rng: Rng) -> Gp: ... + def sample_posterior(self, x_star: list[list[float]], count: int, rng: Rng) -> list[list[float]]: ... + @property + def kernel(self) -> KernelFn: ... + @property + def noise(self) -> float: ... + +class KernelFn: + """ +A covariance function. + +Rust: `learn::gp::KernelFn` + """ + def eval(self, a: list[float], b: list[float]) -> float: ... + def is_valid(self) -> bool: ... + def parameters(self) -> list[float]: ... + def with_parameters(self, values: list[float]) -> KernelFn: ... + +def sample_prior(kernel: KernelFn, x: list[list[float]], count: int, rng: Rng) -> list[list[float]]: + """ +Draws sample functions from a prior with the given kernel. + +Errors: + +`SolveError::InvalidArgument` for an invalid kernel or an empty or +ragged point set; `SolveError::NotPositiveDefinite` if the +covariance matrix cannot be factored. + +Rust: `learn::gp::sample_prior` + """ + ... diff --git a/bindings/python/python/numeria/learn/nn.pyi b/bindings/python/python/numeria/learn/nn.pyi new file mode 100644 index 0000000..afc5fc3 --- /dev/null +++ b/bindings/python/python/numeria/learn/nn.pyi @@ -0,0 +1,113 @@ +""" +Feed-forward networks, trained by backpropagation. # Backpropagation is the chain rule with the products reassociated The derivative of the loss with respect to an early weight is a product of Jacobians, one per layer. Multiplying them left to right costs a matrix-matrix product per layer; multiplying right to left, starting from the scalar loss, costs a matrix-*vector* product per layer. Backpropagation is the second association, and that is the whole of it. It is not an approximation and it is not specific to neural networks -- it is reverse-mode differentiation, and the cost of one gradient is a small multiple of the cost of one forward pass however many parameters there are. Which is why `Mlp::numerical_grad_check` is the test that matters. Descent will reduce a loss using wrong gradients, just more slowly and towards somewhere else, so a falling training curve is no evidence at all. A central difference agreeing with the analytic gradient to eight digits is. # Softmax and cross-entropy belong together Taken separately, softmax has a Jacobian and cross-entropy has a gradient, and composing them involves a matrix. Taken together the product collapses: the gradient of cross-entropy with respect to the *logits* is exactly `p - y`, the predicted distribution minus the target. That cancellation is worth having for accuracy as well as speed -- computing the two separately loses precision exactly where the network is confident and the softmax output is near zero or one. The two are therefore fused here, and `Loss::CrossEntropy` requires `Act::Softmax` on the output layer. # Initialisation is not cosmetic Weights start from a scaled normal draw -- the He scaling `sqrt(2/fan_in)` for rectifiers, the Xavier scaling `sqrt(1/fan_in)` otherwise. Initialising everything to zero makes every hidden unit in a layer compute the same thing and receive the same gradient forever, so the layer has one effective unit no matter how wide it is; initialising too large saturates the sigmoid and tanh, whose derivative is then near zero and whose gradient therefore vanishes. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.linalg.matrix import Matrix +from numeria.monte_carlo import Rng + +class Act: + """ +The activation applied after a layer's affine map. + +Rust: `learn::nn::Act` + """ + ... + +class Gradients: + """ +The gradient of the loss with respect to every parameter, shaped +like the network itself. + +Rust: `learn::nn::Gradients` + """ + def __init__(self, layers: list[tuple[Matrix | Sequence[Sequence[float]], list[float]]]) -> None: ... + def norm(self) -> float: ... + @property + def layers(self) -> list[tuple[Matrix, list[float]]]: ... + +class Loss: + """ +What the network is asked to minimise. + +Rust: `learn::nn::Loss` + """ + ... + +class Mlp: + """ +A fully connected feed-forward network. + +Rust: `learn::nn::Mlp` + """ + def __init__(self, sizes: list[int], activation: Act, output_activation: Act, rng: Rng) -> None: ... + def input_size(self) -> int: ... + def output_size(self) -> int: ... + def parameter_count(self) -> int: ... + def preactivations(self, x: list[float]) -> list[list[float]]: ... + def forward(self, x: list[float]) -> list[float]: ... + def predict(self, x: list[float]) -> int: ... + def example_loss(self, x: list[float], y: list[float], loss: Loss) -> float: ... + def loss(self, data: list[tuple[list[float], list[float]]], loss: Loss) -> float: ... + def backward(self, x: list[float], y: list[float], loss: Loss) -> Gradients: ... + def numerical_grad_check(self, x: list[float], y: list[float], loss: Loss) -> float: ... + def train_sgd(self, data: list[tuple[list[float], list[float]]], loss: Loss, epochs: int, lr: float, batch: int, rng: Rng) -> list[float]: ... + def train_adam(self, data: list[tuple[list[float], list[float]]], loss: Loss, epochs: int, lr: float, batch: int, rng: Rng) -> list[float]: ... + @property + def layers(self) -> list[tuple[Matrix, list[float]]]: ... + @property + def activation(self) -> Act: ... + @property + def output_activation(self) -> Act: ... + +def conv2d_forward(input: list[float], w: int, h: int, kernels: list[tuple[list[float], int, int]], stride: int, pad: int) -> tuple[list[list[float]], int, int]: + """ +One convolution layer's forward pass: `kernels` applied to a single +channel image, with the given stride and zero padding. + +Returns one output plane per kernel, each row-major, along with the +output width and height. The convolution here is the cross-correlation +that every machine learning library calls a convolution -- the kernel +is *not* flipped. Against a symmetric kernel the two agree and the +distinction never shows; against an asymmetric one they differ by a +reflection, so a signal-processing convolution needs the kernel +reversed on the way in. + +Errors: + +`SolveError::InvalidArgument` for a zero stride, an empty kernel +set, a kernel larger than the padded image, or mismatched sizes; +`SolveError::DimensionMismatch` if the image is not `w * h`. + +Rust: `learn::nn::conv2d_forward` + """ + ... + +def linear_regression_gd_check(x: Matrix | Sequence[Sequence[float]], y: list[float], iterations: int) -> float: + """ +Fits `y = X b` by gradient descent and reports how far the answer is +from the closed-form least-squares solution, relative to its size. + +The point is the comparison. Least squares has an exact answer +through the normal equations, so an iterative method solving the same +problem has somewhere to be checked against -- and that check is +worth more than any amount of watching a loss go down, because a +descent with the wrong gradient also produces a loss that goes down. + +The step size is taken as `1 / L` with `L` the largest eigenvalue of +`X^T X`, estimated by a few power iterations. That is the largest +step for which gradient descent on a quadratic is guaranteed to +converge, and going past it diverges rather than converging slowly. + +Errors: + +`SolveError::InvalidArgument` for an empty or ill-shaped problem; +whatever the least-squares solver reports otherwise. + +Rust: `learn::nn::linear_regression_gd_check` + """ + ... diff --git a/bindings/python/python/numeria/learn/tree.pyi b/bindings/python/python/numeria/learn/tree.pyi new file mode 100644 index 0000000..dcfa0f9 --- /dev/null +++ b/bindings/python/python/numeria/learn/tree.pyi @@ -0,0 +1,225 @@ +""" +Decision trees, random forests and gradient boosting. # What a tree does that a linear model cannot A decision tree asks a sequence of threshold questions about single features. Three consequences follow, and they are what the method is for rather than incidental to it. *Scale does not matter.* A threshold on a feature is decided by the order of its values, not their magnitudes, so multiplying a column by a thousand changes the thresholds and nothing else -- the tree computes the same function and the predictions are identical. Nothing that measures a distance can say that: k-nearest-neighbours, k-means and a Gaussian process all change their answers entirely under the same rescaling. This is asserted directly. *Interactions come free.* A split below a split conditions on the first, so a tree represents `x > a AND y > b` without anyone writing the product term. *Nothing is extrapolated.* Every prediction is a leaf's summary of the training points that reached it, so a tree's output outside the training range is flat. That is honest and it is also useless for trend extrapolation, which is the usual reason to reach for something else. # The impurity decrease is never negative A split is chosen to minimise the weighted impurity of its two children, and refusing to split is always available, so the decrease recorded at every node is at least zero. Feature importances are sums of those decreases, weighted by how many samples passed through, so they are nonnegative and they sum to exactly the total impurity the tree removed. Both are checked rather than assumed. # A single tree overfits by construction Grown without limit, a tree separates every training point that can be separated, and its training error reaches zero. That number is therefore worthless as evidence of anything, in the same way a 1-nearest-neighbour training error is. What the ensembles do about it differs: - a **random forest** grows many deep trees on bootstrap samples with a random subset of features considered at each split, and averages them. The trees are individually overfitted and their errors are decorrelated, so averaging cancels the variance without adding bias. - **gradient boosting** grows shallow trees in sequence, each fitted to what the previous ones got wrong. The trees are individually underfitted and the bias comes down step by step, which is why the learning rate matters and why the round count is what has to be stopped early. The two are opposite strategies and neither is a variant of the other. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.monte_carlo import Rng + +class Forest: + """ +An ensemble of trees grown on bootstrap samples. + +Rust: `learn::tree::Forest` + """ + def __init__(self, trees: list[Tree], out_of_bag: list[list[int]]) -> None: ... + @property + def trees(self) -> list[Tree]: ... + @property + def out_of_bag(self) -> list[list[int]]: ... + +class Gbm: + """ +A gradient boosted regressor: a constant plus a sequence of shallow +trees. + +Rust: `learn::tree::Gbm` + """ + def __init__(self, base: float, trees: list[Tree], learning_rate: float, loss_history: list[float]) -> None: ... + @property + def base(self) -> float: ... + @property + def trees(self) -> list[Tree]: ... + @property + def learning_rate(self) -> float: ... + @property + def loss_history(self) -> list[float]: ... + +class Tree: + """ +A fitted decision tree. Node zero is the root. + +Rust: `learn::tree::Tree` + """ + def __init__(self, nodes: list[TreeNode], n_features: int) -> None: ... + @property + def nodes(self) -> list[TreeNode]: ... + @property + def n_features(self) -> int: ... + +class TreeNode: + """ +A node of a fitted tree. + +Rust: `learn::tree::TreeNode` + """ + ... + +def gini(counts: list[int]) -> float: + """ +The Gini impurity of a set of class counts, `1 - sum p^2`. + +Exactly zero for a pure node and exactly `1 - 1/k` for `k` classes in +equal proportion, which is its maximum. Both are identities rather +than limits. + +Compared with `entropy` it is cheaper -- no logarithm -- and the +two rank splits almost identically, which is why the choice between +them is very nearly arbitrary. + +Rust: `learn::tree::gini` + """ + ... + +def entropy(counts: list[int]) -> float: + """ +The Shannon entropy of a set of class counts, in nats. + +Zero for a pure node and `ln k` for `k` classes in equal proportion. +A count of zero contributes nothing, which is the continuous +extension of `p ln p` at the origin rather than a special case. + +Rust: `learn::tree::entropy` + """ + ... + +def decision_tree_fit(x: list[list[float]], y: list[int], max_depth: int, min_leaf: int) -> Tree: + """ +Fits a classification tree by greedy Gini reduction. + +Errors: + +`SolveError::InvalidArgument` for an invalid dataset or a zero +`min_leaf`; `SolveError::DimensionMismatch` on a label count +mismatch. + +Rust: `learn::tree::decision_tree_fit` + """ + ... + +def regression_tree_fit(x: list[list[float]], y: list[float], max_depth: int, min_leaf: int) -> Tree: + """ +Fits a regression tree by greedy variance reduction. + +Errors: + +As `decision_tree_fit`, and additionally for non-finite targets. + +Rust: `learn::tree::regression_tree_fit` + """ + ... + +def tree_predict(tree: Tree, x: list[float]) -> int: + """ +The class a tree predicts for a point. + +Errors: + +`SolveError::DimensionMismatch` if the point has the wrong width. + +Rust: `learn::tree::tree_predict` + """ + ... + +def tree_predict_value(tree: Tree, x: list[float]) -> float: + """ +The value a regression tree predicts for a point. + +Errors: + +As `tree_predict`. + +Rust: `learn::tree::tree_predict_value` + """ + ... + +def feature_importance(tree: Tree) -> list[float]: + """ +How much impurity each feature removed, summed over the splits that +used it and weighted by the samples that reached them. + +Nonnegative, because no split with a negative decrease is ever taken, +and summing to exactly the tree's total weighted impurity decrease. +Unnormalised on purpose: the total is a meaningful quantity, and +dividing by it throws away how much the tree explained in favour of +how it divided the credit. + +Rust: `learn::tree::feature_importance` + """ + ... + +def random_forest_fit(x: list[list[float]], y: list[int], n_trees: int, max_depth: int, min_leaf: int, features_per_split: int, rng: Rng) -> Forest: + """ +Grows a random forest: `n_trees` classification trees, each on a +bootstrap resample, each split choosing among a random subset of +features. + +Both sources of randomness are needed. Bagging alone leaves the trees +too much alike, because whichever feature is most informative is +chosen at the root of nearly all of them; restricting the features +considered at each split is what decorrelates the errors, and +averaging only cancels errors that are not shared. + +`features_per_split` defaults to the square root of the feature +count when given as zero, which is the usual choice for +classification. + +Errors: + +As `decision_tree_fit`, plus `SolveError::InvalidArgument` for +zero trees. + +Rust: `learn::tree::random_forest_fit` + """ + ... + +def forest_predict(forest: Forest, x: list[float]) -> int: + """ +The forest's majority vote. + +Errors: + +As `tree_predict`. + +Rust: `learn::tree::forest_predict` + """ + ... + +def gradient_boosting_lite(x: list[list[float]], y: list[float], n_rounds: int, learning_rate: float, depth: int) -> Gbm: + """ +Fits a gradient boosted regressor under squared loss. + +Starts at the mean and adds `learning_rate` times a shallow tree +fitted to the current residual, `n_rounds` times. Under squared loss +the negative gradient *is* the residual, which is why this simplest +case looks like nothing more than fitting the errors -- for other +losses the tree is fitted to the gradient and the leaf values are +then corrected, which is where the name comes from. + +The loss falls monotonically for a learning rate at or below one, +because each tree reduces the squared residual it was fitted to and +shrinking a descent step cannot turn it into an ascent. + +Errors: + +As `regression_tree_fit`, plus `SolveError::InvalidArgument` for +a learning rate outside `(0, 1]`. + +Rust: `learn::tree::gradient_boosting_lite` + """ + ... + +def gbm_predict(model: Gbm, x: list[float]) -> float: + """ +The boosted model's prediction. + +Errors: + +As `tree_predict_value`. + +Rust: `learn::tree::gbm_predict` + """ + ... diff --git a/bindings/python/python/numeria/linalg/__init__.pyi b/bindings/python/python/numeria/linalg/__init__.pyi new file mode 100644 index 0000000..246706d --- /dev/null +++ b/bindings/python/python/numeria/linalg/__init__.pyi @@ -0,0 +1,161 @@ +""" +Dense and sparse linear algebra. `Matrix` is the dense row-major `f64` type everything here operates on. The factorizations are chosen by what the matrix is: `lu` with partial pivoting for a general square solve, `cholesky` for symmetric positive-definite (half the work, and it fails cleanly if the matrix is not), `qr` by Householder reflections for least squares, `svd` by one-sided Jacobi for rank and pseudo-inverse, and `tridiagonal` for the Thomas algorithm in O(n). `eigen` provides the symmetric eigenproblem and general eigenvalues. `sparse` provides CSR storage with conjugate gradient and a Jacobi-preconditioned variant, for the large systems that the PDE solvers in `fem` produce. Note that `pcg_jacobi`'s tolerance is relative to the norm of the right-hand side, not absolute. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from . import cholesky, eigen, lu, matrix, qr, sparse, svd, tridiagonal +from numeria.math import Vec3 +from numeria.linalg.sparse import CsrMatrix as CsrMatrix +from numeria.linalg.lu import Lu as Lu +from numeria.linalg.matrix import Matrix as Matrix +from numeria.linalg.qr import Qr as Qr +from numeria.linalg.svd import Svd as Svd +from numeria.linalg.eigen import SymEigen as SymEigen +from numeria.linalg.cholesky import cholesky_solve as cholesky_solve +from numeria.linalg.sparse import conjugate_gradient as conjugate_gradient +from numeria.linalg.eigen import eigen_symmetric as eigen_symmetric +from numeria.linalg.tridiagonal import eigen_symmetric_tridiagonal as eigen_symmetric_tridiagonal +from numeria.linalg.eigen import eigenvalues_general as eigenvalues_general +from numeria.linalg.svd import kabsch as kabsch +from numeria.linalg.qr import least_squares as least_squares +from numeria.linalg.lu import lu_decompose as lu_decompose +from numeria.linalg.sparse import pcg_jacobi as pcg_jacobi +from numeria.linalg.svd import pseudoinverse as pseudoinverse +from numeria.linalg.qr import qr_householder as qr_householder +from numeria.linalg.svd import rank as rank +from numeria.linalg.lu import solve as solve +from numeria.linalg.tridiagonal import thomas_solve as thomas_solve + +class Mat3: + """ + +Rust: `linalg::Mat3` + """ + @staticmethod + def zero() -> Mat3: ... + @staticmethod + def identity() -> Mat3: ... + @staticmethod + def from_rows(r0: list[float], r1: list[float], r2: list[float]) -> Mat3: ... + def determinant(self) -> float: ... + def transpose(self) -> Mat3: ... + def inverse(self) -> Optional[Mat3]: ... + def trace(self) -> float: ... + def mul_vec(self, v: Vec3 | Sequence[float]) -> Vec3: ... + def mul_mat(self, other: Mat3) -> Mat3: ... + @staticmethod + def scale(s: float) -> Mat3: ... + def principal_axes(self) -> tuple[list[float], Mat3]: ... + def principal_axes_3x3(self) -> tuple[list[float], Mat3]: ... + def mul_scalar(self, s: float) -> Mat3: ... + @property + def data(self) -> list[list[float]]: ... + +class Mat4: + """ +A dense 4x4 matrix with fixed-size storage. + +Rust: `linalg::Mat4` + """ + @staticmethod + def zero() -> Mat4: ... + @staticmethod + def identity() -> Mat4: ... + @staticmethod + def from_rows(r0: list[float], r1: list[float], r2: list[float], r3: list[float]) -> Mat4: ... + def mul_mat(self, other: Mat4) -> Mat4: ... + def mul_vec4(self, v: list[float]) -> list[float]: ... + def transpose(self) -> Mat4: ... + def trace(self) -> float: ... + def determinant(self) -> float: ... + def inverse(self) -> Optional[Mat4]: ... + def to_matrix(self) -> Matrix: ... + @staticmethod + def from_matrix(m: Matrix | Sequence[Sequence[float]]) -> Mat4: ... + @property + def data(self) -> list[list[float]]: ... + +def rotation_x(angle: float) -> Mat3: + """ +Rotation matrix about the x-axis by the given angle in radians. + +Rust: `linalg::rotation_x` + """ + ... + +def rotation_y(angle: float) -> Mat3: + """ +Rotation matrix about the y-axis by the given angle in radians. + +Rust: `linalg::rotation_y` + """ + ... + +def rotation_z(angle: float) -> Mat3: + """ +Rotation matrix about the z-axis by the given angle in radians. + +Rust: `linalg::rotation_z` + """ + ... + +def rotation_axis_angle(axis: Vec3 | Sequence[float], angle: float) -> Mat3: + """ +Rodrigues' rotation formula: rotate by `angle` radians about `axis`. +The axis is normalized internally. + +Rust: `linalg::rotation_axis_angle` + """ + ... + +def cartesian_to_spherical(x: float, y: float, z: float) -> tuple[float, float, float]: + """ +Returns (r, theta, phi) where theta is the polar angle from +z and phi is the azimuthal angle from +x. + +Rust: `linalg::cartesian_to_spherical` + """ + ... + +def spherical_to_cartesian(r: float, theta: float, phi: float) -> tuple[float, float, float]: + """ +Converts spherical coordinates (r, theta, phi) to Cartesian (x, y, z). + +Rust: `linalg::spherical_to_cartesian` + """ + ... + +def cartesian_to_cylindrical(x: float, y: float, z: float) -> tuple[float, float, float]: + """ +Returns (rho, phi, z) where rho is the radial distance in the xy-plane and phi is the azimuthal angle from +x. + +Rust: `linalg::cartesian_to_cylindrical` + """ + ... + +def cylindrical_to_cartesian(rho: float, phi: float, z: float) -> tuple[float, float, float]: + """ +Converts cylindrical coordinates (rho, phi, z) to Cartesian (x, y, z). + +Rust: `linalg::cylindrical_to_cartesian` + """ + ... + +def polar_to_cartesian(r: float, theta: float) -> tuple[float, float]: + """ +Converts 2D polar coordinates (r, theta) to Cartesian (x, y). + +Rust: `linalg::polar_to_cartesian` + """ + ... + +def cartesian_to_polar(x: float, y: float) -> tuple[float, float]: + """ +Returns (r, theta) where theta is the angle from +x. + +Rust: `linalg::cartesian_to_polar` + """ + ... diff --git a/bindings/python/python/numeria/linalg/cholesky.pyi b/bindings/python/python/numeria/linalg/cholesky.pyi new file mode 100644 index 0000000..23b94cf --- /dev/null +++ b/bindings/python/python/numeria/linalg/cholesky.pyi @@ -0,0 +1,31 @@ +""" +Cholesky factorization of symmetric positive-definite matrices. Reference: Golub & Van Loan, *Matrix Computations*, §4.2: A = L·Lᵀ with L lower triangular and positive diagonal. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.linalg.matrix import Matrix + +def cholesky(a: Matrix | Sequence[Sequence[float]]) -> Matrix: + """ +Factors a symmetric positive-definite matrix as A = L·Lᵀ, returning +the lower-triangular factor L. + +Returns `InvalidArgument` for non-square or asymmetric input and +`NotPositiveDefinite` when a diagonal pivot is not strictly positive. + +Rust: `linalg::cholesky::cholesky` + """ + ... + +def cholesky_solve(l: Matrix | Sequence[Sequence[float]], b: list[float]) -> list[float]: + """ +Solves A·x = b given the Cholesky factor L of A (A = L·Lᵀ), by one +forward and one back substitution. + +Rust: `linalg::cholesky::cholesky_solve` + """ + ... diff --git a/bindings/python/python/numeria/linalg/eigen.pyi b/bindings/python/python/numeria/linalg/eigen.pyi new file mode 100644 index 0000000..0922219 --- /dev/null +++ b/bindings/python/python/numeria/linalg/eigen.pyi @@ -0,0 +1,49 @@ +""" +Eigenvalue solvers. Symmetric matrices use the cyclic Jacobi rotation method (Golub & Van Loan §8.5), which is unconditionally convergent. General real matrices are reduced to upper Hessenberg form by Gaussian similarity transformations and their eigenvalues extracted with the Francis-shift QR iteration (Wilkinson, *The Algebraic Eigenvalue Problem*; the classic `hqr` algorithm). +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.linalg.matrix import Matrix + +class SymEigen: + """ +Eigen-decomposition of a symmetric matrix: A·vᵢ = λᵢ·vᵢ. + +`values[i]` pairs with column i of `vectors`; entries are sorted in +descending eigenvalue order and the vectors are orthonormal. + +Rust: `linalg::eigen::SymEigen` + """ + def __init__(self, values: list[float], vectors: Matrix | Sequence[Sequence[float]]) -> None: ... + @property + def values(self) -> list[float]: ... + @property + def vectors(self) -> Matrix: ... + +def eigen_symmetric(a: Matrix | Sequence[Sequence[float]], tol: float, max_sweeps: int) -> SymEigen: + """ +Cyclic Jacobi eigen-decomposition of a symmetric matrix. + +Sweeps Givens rotations over all off-diagonal pairs until the +off-diagonal Frobenius norm falls below `tol` (relative to ‖A‖) or +`max_sweeps` is exhausted, in which case `NoConvergence` is returned. + +Rust: `linalg::eigen::eigen_symmetric` + """ + ... + +def eigenvalues_general(a: Matrix | Sequence[Sequence[float]], max_iter: int) -> list[complex]: + """ +All eigenvalues (possibly complex) of a general real square matrix, +by Hessenberg reduction followed by Francis-shift QR iteration. + +`max_iter` bounds the QR iterations spent per eigenvalue (30 is the +classical choice). + +Rust: `linalg::eigen::eigenvalues_general` + """ + ... diff --git a/bindings/python/python/numeria/linalg/lu.pyi b/bindings/python/python/numeria/linalg/lu.pyi new file mode 100644 index 0000000..872a53e --- /dev/null +++ b/bindings/python/python/numeria/linalg/lu.pyi @@ -0,0 +1,51 @@ +""" +LU decomposition with partial pivoting (Doolittle form). Reference: Golub & Van Loan, *Matrix Computations*, §3.4. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.linalg.matrix import Matrix + +class Lu: + """ +Packed LU factorization P·A = L·U. + +`lu` stores U on and above the diagonal and the unit-lower-triangular +L (implicit ones on the diagonal) below it. `perm[i]` is the row of A +that ended up in position i; `sign` is the permutation's parity (±1). + +Rust: `linalg::lu::Lu` + """ + def __init__(self, lu: Matrix | Sequence[Sequence[float]], perm: list[int], sign: float) -> None: ... + def solve(self, b: list[float]) -> list[float]: ... + def solve_matrix(self, b: Matrix | Sequence[Sequence[float]]) -> Matrix: ... + def determinant(self) -> float: ... + def inverse(self) -> Matrix: ... + @property + def lu(self) -> Matrix: ... + @property + def perm(self) -> list[int]: ... + @property + def sign(self) -> float: ... + +def lu_decompose(a: Matrix | Sequence[Sequence[float]]) -> Lu: + """ +Factors a square matrix as P·A = L·U with partial (row) pivoting. + +Returns `SolveError::InvalidArgument` for non-square input and +`SolveError::Singular` when a pivot falls below the threshold. + +Rust: `linalg::lu::lu_decompose` + """ + ... + +def solve(a: Matrix | Sequence[Sequence[float]], b: list[float]) -> list[float]: + """ +Convenience wrapper: factor `a` and solve A·x = b in one call. + +Rust: `linalg::lu::solve` + """ + ... diff --git a/bindings/python/python/numeria/linalg/matrix.pyi b/bindings/python/python/numeria/linalg/matrix.pyi new file mode 100644 index 0000000..99df4ec --- /dev/null +++ b/bindings/python/python/numeria/linalg/matrix.pyi @@ -0,0 +1,46 @@ +""" +Dense row-major matrix of `f64`. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.linalg import Mat3 + +class Matrix: + """ +Dense matrix with row-major storage: element (r, c) lives at +`data[r * cols + c]`. + +Rust: `linalg::matrix::Matrix` + """ + def __init__(self, rows: int, cols: int, data: list[float]) -> None: ... + @staticmethod + def zeros(rows: int, cols: int) -> Matrix: ... + @staticmethod + def identity(n: int) -> Matrix: ... + @staticmethod + def from_rows(rows: list[list[float]]) -> Matrix: ... + @staticmethod + def from_fn(rows: int, cols: int, f: Callable[[int, int], float]) -> Matrix: ... + def get(self, r: int, c: int) -> float: ... + def set(self, r: int, c: int, v: float) -> None: ... + def row(self, r: int) -> list[float]: ... + def transpose(self) -> Matrix: ... + def mul(self, other: Matrix | Sequence[Sequence[float]]) -> Matrix: ... + def mul_vec(self, v: list[float]) -> list[float]: ... + def add(self, other: Matrix | Sequence[Sequence[float]]) -> Matrix: ... + def scale(self, k: float) -> Matrix: ... + def frobenius_norm(self) -> float: ... + def is_square(self) -> bool: ... + def is_symmetric(self, tol: float) -> bool: ... + @staticmethod + def from_mat3(m: Mat3) -> Matrix: ... + @property + def rows(self) -> int: ... + @property + def cols(self) -> int: ... + @property + def data(self) -> list[float]: ... diff --git a/bindings/python/python/numeria/linalg/qr.pyi b/bindings/python/python/numeria/linalg/qr.pyi new file mode 100644 index 0000000..ec62558 --- /dev/null +++ b/bindings/python/python/numeria/linalg/qr.pyi @@ -0,0 +1,44 @@ +""" +QR decomposition by Householder reflections and least-squares solve. Reference: Golub & Van Loan, *Matrix Computations*, §5.2: A = Q·R with Q orthogonal (m×m) and R upper trapezoidal (m×n). +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.linalg.matrix import Matrix + +class Qr: + """ +QR factorization A = Q·R. + +Rust: `linalg::qr::Qr` + """ + def __init__(self, q: Matrix | Sequence[Sequence[float]], r: Matrix | Sequence[Sequence[float]]) -> None: ... + @property + def q(self) -> Matrix: ... + @property + def r(self) -> Matrix: ... + +def qr_householder(a: Matrix | Sequence[Sequence[float]]) -> Qr: + """ +Factors A (m×n) as Q·R using Householder reflections. + +Q is m×m orthogonal; R is m×n with zeros below the diagonal. + +Rust: `linalg::qr::qr_householder` + """ + ... + +def least_squares(a: Matrix | Sequence[Sequence[float]], b: list[float]) -> list[float]: + """ +Solves the least-squares problem min ‖A·x − b‖₂ via QR. + +Requires m ≥ n and full column rank; returns `Singular` when R has a +negligible diagonal entry, `DimensionMismatch` when `b.len() != m`, +and `InvalidArgument` when the system is underdetermined (m < n). + +Rust: `linalg::qr::least_squares` + """ + ... diff --git a/bindings/python/python/numeria/linalg/sparse.pyi b/bindings/python/python/numeria/linalg/sparse.pyi new file mode 100644 index 0000000..ff206e1 --- /dev/null +++ b/bindings/python/python/numeria/linalg/sparse.pyi @@ -0,0 +1,59 @@ +""" +Compressed sparse row (CSR) matrices and conjugate-gradient solvers. Reference: Golub & Van Loan §11.5 (CG), Saad, *Iterative Methods for Sparse Linear Systems* §9.2 (Jacobi-preconditioned CG). +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.linalg.matrix import Matrix + +class CsrMatrix: + """ +Sparse matrix in CSR form: row r's entries live at indices +`row_ptr[r]..row_ptr[r+1]` of `col_idx`/`vals`. + +Rust: `linalg::sparse::CsrMatrix` + """ + def __init__(self, rows: int, cols: int, row_ptr: list[int], col_idx: list[int], vals: list[float]) -> None: ... + @staticmethod + def from_triplets(rows: int, cols: int, entries: list[tuple[int, int, float]]) -> CsrMatrix: ... + @staticmethod + def from_dense(m: Matrix | Sequence[Sequence[float]], tol: float) -> CsrMatrix: ... + def mul_vec(self, v: list[float]) -> list[float]: ... + @staticmethod + def laplacian_2d(nx: int, ny: int, h: float) -> CsrMatrix: ... + @property + def rows(self) -> int: ... + @property + def cols(self) -> int: ... + @property + def row_ptr(self) -> list[int]: ... + @property + def col_idx(self) -> list[int]: ... + @property + def vals(self) -> list[float]: ... + +def conjugate_gradient(a: CsrMatrix, b: list[float], x0: list[float], tol: float, max_iter: int) -> list[float]: + """ +Conjugate gradient for SPD systems A·x = b starting from `x0`. + +Converges when ‖r‖₂ ≤ tol·max(‖b‖₂, 1); returns `NoConvergence` +with the final residual otherwise. + +Rust: `linalg::sparse::conjugate_gradient` + """ + ... + +def pcg_jacobi(a: CsrMatrix, b: list[float], tol: float, max_iter: int) -> list[float]: + """ +Jacobi (diagonal) preconditioned conjugate gradient with x₀ = 0. + +Requires strictly positive diagonal entries (fails with +`NotPositiveDefinite` otherwise). Convergence criterion matches +`conjugate_gradient`. + +Rust: `linalg::sparse::pcg_jacobi` + """ + ... diff --git a/bindings/python/python/numeria/linalg/svd.pyi b/bindings/python/python/numeria/linalg/svd.pyi new file mode 100644 index 0000000..1a912ae --- /dev/null +++ b/bindings/python/python/numeria/linalg/svd.pyi @@ -0,0 +1,65 @@ +""" +Singular value decomposition by one-sided Jacobi rotations. Reference: Golub & Van Loan §8.6 / Demmel & Veselić, "Jacobi's method is more accurate than QR". Produces the thin decomposition A = U·Σ·Vᵀ with U m×n (orthonormal columns where σ > 0), Σ the non-negative singular values in descending order, and Vᵀ n×n. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.linalg import Mat3 +from numeria.linalg.matrix import Matrix +from numeria.math import Vec3 + +class Svd: + """ +Thin SVD: A = U·Σ·Vᵀ. + +Rust: `linalg::svd::Svd` + """ + def __init__(self, u: Matrix | Sequence[Sequence[float]], sigma: list[float], vt: Matrix | Sequence[Sequence[float]]) -> None: ... + @property + def u(self) -> Matrix: ... + @property + def sigma(self) -> list[float]: ... + @property + def vt(self) -> Matrix: ... + +def svd(a: Matrix | Sequence[Sequence[float]]) -> Svd: + """ +One-sided Jacobi SVD of an m×n matrix with m ≥ n; for m < n the +transpose is factored and the roles of U and V are swapped. + +Rust: `linalg::svd::svd` + """ + ... + +def pseudoinverse(a: Matrix | Sequence[Sequence[float]], rcond: float) -> Matrix: + """ +Moore-Penrose pseudoinverse A⁺ = V·Σ⁺·Uᵀ; singular values below +`rcond · σ_max` are treated as zero. + +Rust: `linalg::svd::pseudoinverse` + """ + ... + +def rank(a: Matrix | Sequence[Sequence[float]], tol: float) -> int: + """ +Numerical rank: the number of singular values greater than `tol`. + +Rust: `linalg::svd::rank` + """ + ... + +def kabsch(p: list[Vec3 | Sequence[float]], q: list[Vec3 | Sequence[float]]) -> Mat3: + """ +Kabsch algorithm: the rotation R minimizing Σ‖R·pᵢ − qᵢ‖². + +Both point sets are used as given (no centroid subtraction); center +them first for the usual superposition problem. Fails with +`DimensionMismatch` when the sets differ in length and +`InvalidArgument` when fewer than 3 points are supplied. + +Rust: `linalg::svd::kabsch` + """ + ... diff --git a/bindings/python/python/numeria/linalg/tridiagonal.pyi b/bindings/python/python/numeria/linalg/tridiagonal.pyi new file mode 100644 index 0000000..3a9df00 --- /dev/null +++ b/bindings/python/python/numeria/linalg/tridiagonal.pyi @@ -0,0 +1,35 @@ +""" +Tridiagonal linear solve (Thomas algorithm). Reference: Press et al., *Numerical Recipes*, §2.4. Solves `sub[i-1]·x[i-1] + diag[i]·x[i] + sup[i]·x[i+1] = rhs[i]` in O(n). +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def thomas_solve(sub: list[float], diag: list[float], sup: list[float], rhs: list[float]) -> list[float]: + """ +Solves a tridiagonal system with the Thomas algorithm. + +`diag` and `rhs` have length n; `sub` (below-diagonal) and `sup` +(above-diagonal) have length n−1. Numerically stable for diagonally +dominant or symmetric positive-definite systems. + +Rust: `linalg::tridiagonal::thomas_solve` + """ + ... + +def eigen_symmetric_tridiagonal(diag: list[float], off: list[float]) -> tuple[list[float], list[list[float]]]: + """ +Eigen-decomposition of a symmetric tridiagonal matrix by the QL +algorithm with implicit shifts (EISPACK `tql2`): returns eigenvalues +(ascending) and matching orthonormal eigenvectors. + +Errors: +Returns `DimensionMismatch` for inconsistent inputs and +`NoConvergence` if an eigenvalue fails to settle in 50 iterations. + +Rust: `linalg::tridiagonal::eigen_symmetric_tridiagonal` + """ + ... diff --git a/bindings/python/python/numeria/magnetohydrodynamics.pyi b/bindings/python/python/numeria/magnetohydrodynamics.pyi new file mode 100644 index 0000000..13303e5 --- /dev/null +++ b/bindings/python/python/numeria/magnetohydrodynamics.pyi @@ -0,0 +1,167 @@ +""" +Magnetohydrodynamics: a conducting fluid and the field frozen into it. The dimensionless numbers first, because they decide the regime: magnetic Reynolds (advection against diffusion, and so whether the field is frozen in), Lundquist, Hartmann, and the plasma beta -- the ratio of thermal to magnetic pressure, which says whether the field or the gas is in charge. Wave speeds: Alfvén, and the slow and fast magnetosonic branches. Equilibria: pinch pressure balance, the Bennett condition, and the Grad-Shafranov beta limit. Reconnection is covered by the Sweet-Parker rate and the associated electric field. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def magnetic_reynolds_number(velocity: float, length: float, conductivity: float) -> float: + """ +Magnetic Reynolds number: Rm = μ₀ σ v L. +Quantifies the ratio of magnetic advection to diffusion. + +Rust: `magnetohydrodynamics::magnetic_reynolds_number` + """ + ... + +def magnetic_diffusivity(conductivity: float) -> float: + """ +Magnetic diffusivity: η = 1/(μ₀ σ). + +Rust: `magnetohydrodynamics::magnetic_diffusivity` + """ + ... + +def lundquist_number(alfven_speed: float, length: float, diffusivity: float) -> float: + """ +Lundquist number: S = vₐ L / η. +Ratio of resistive diffusion time to Alfvén transit time. + +Rust: `magnetohydrodynamics::lundquist_number` + """ + ... + +def hartmann_number(b_field: float, length: float, conductivity: float, dynamic_viscosity: float) -> float: + """ +Hartmann number: Ha = B L √(σ / μ_visc). +Ratio of electromagnetic to viscous forces in a conducting fluid. + +Rust: `magnetohydrodynamics::hartmann_number` + """ + ... + +def magnetic_pressure(b_field: float) -> float: + """ +Magnetic pressure: P_B = B² / (2 μ₀). + +Rust: `magnetohydrodynamics::magnetic_pressure` + """ + ... + +def total_pressure(gas_pressure: float, b_field: float) -> float: + """ +Total pressure (gas + magnetic): P_total = P + B² / (2 μ₀). + +Rust: `magnetohydrodynamics::total_pressure` + """ + ... + +def plasma_beta(gas_pressure: float, b_field: float) -> float: + """ +Plasma beta: β = 2 μ₀ P / B². +Ratio of gas pressure to magnetic pressure. + +Rust: `magnetohydrodynamics::plasma_beta` + """ + ... + +def alfven_speed(b_field: float, density: float) -> float: + """ +Alfvén speed: vₐ = B / √(μ₀ ρ). + +Rust: `magnetohydrodynamics::alfven_speed` + """ + ... + +def slow_magnetosonic_speed(alfven: float, sound: float) -> float: + """ +Slow magnetosonic speed (perpendicular propagation): v_slow = min(vₐ, cₛ). + +Rust: `magnetohydrodynamics::slow_magnetosonic_speed` + """ + ... + +def fast_magnetosonic_speed(alfven: float, sound: float) -> float: + """ +Fast magnetosonic speed (perpendicular propagation): v_fast = √(vₐ² + cₛ²). + +Rust: `magnetohydrodynamics::fast_magnetosonic_speed` + """ + ... + +def magnetosonic_mach(velocity: float, alfven: float, sound: float) -> float: + """ +Magnetosonic Mach number: M_ms = v / v_fast. + +Rust: `magnetohydrodynamics::magnetosonic_mach` + """ + ... + +def pinch_pressure_balance(current: float, radius: float) -> float: + """ +Z-pinch pressure balance. +Computes the magnetic pressure from the azimuthal field B_θ = μ₀ I / (2π r). + +Rust: `magnetohydrodynamics::pinch_pressure_balance` + """ + ... + +def bennett_pinch_condition(current: float, line_density: float, temperature: float) -> bool: + """ +Bennett pinch condition: checks whether I² ≈ 8π N k_B T / μ₀. +Returns `true` when the plasma is in pressure balance. + +Rust: `magnetohydrodynamics::bennett_pinch_condition` + """ + ... + +def grad_shafranov_beta_limit(aspect_ratio: float) -> float: + """ +Rough Troyon-like beta limit: β_max ≈ 1 / aspect_ratio. + +Rust: `magnetohydrodynamics::grad_shafranov_beta_limit` + """ + ... + +def sweet_parker_rate(alfven_speed: float, lundquist: float) -> float: + """ +Sweet-Parker reconnection rate: v_in / vₐ = 1 / √S. + +Rust: `magnetohydrodynamics::sweet_parker_rate` + """ + ... + +def reconnection_electric_field(b_field: float, inflow_velocity: float) -> float: + """ +Reconnection electric field: E = v_in × B (magnitude). + +Rust: `magnetohydrodynamics::reconnection_electric_field` + """ + ... + +def magnetic_diffusion_time(length: float, diffusivity: float) -> float: + """ +Magnetic diffusion time: τ_d = L² / η. + +Rust: `magnetohydrodynamics::magnetic_diffusion_time` + """ + ... + +def advection_time(length: float, velocity: float) -> float: + """ +Advection time: τ_a = L / v. + +Rust: `magnetohydrodynamics::advection_time` + """ + ... + +def is_frozen_in(reynolds_mag: float) -> bool: + """ +Returns `true` when Rm > 100, indicating the magnetic field is frozen into the plasma. + +Rust: `magnetohydrodynamics::is_frozen_in` + """ + ... diff --git a/bindings/python/python/numeria/manifold/__init__.pyi b/bindings/python/python/numeria/manifold/__init__.pyi new file mode 100644 index 0000000..01ef787 --- /dev/null +++ b/bindings/python/python/numeria/manifold/__init__.pyi @@ -0,0 +1,272 @@ +""" +Manifolds and higher-dimensional geometry: generic n-dimensional vectors and tensors, metric-driven curvature, and (in later modules) geodesics, Lie groups, constant-curvature spaces, polytopes, Clifford algebras, embeddings, discrete exterior calculus, and spacetimes. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from . import clifford, dec, embedding, geodesic, hyperbolic, lie, metric, polytope4, spacetime, spherical, vecn +from numeria.manifold.spacetime import Causal as Causal +from numeria.manifold.dec import DecMesh as DecMesh +from numeria.manifold.spacetime import FourVector as FourVector +from numeria.manifold.geodesic import GeodesicState as GeodesicState +from numeria.manifold.lie import Heisenberg3 as Heisenberg3 +from numeria.manifold.hyperbolic import HypModel as HypModel +from numeria.manifold.hyperbolic import HypPoint as HypPoint +from numeria.manifold.geodesic import Integrator as Integrator +from numeria.manifold.spacetime import KerrConstants as KerrConstants +from numeria.manifold.spacetime import LorentzTransform as LorentzTransform +from numeria.manifold.metric import Metric as Metric +from numeria.manifold.clifford import Multivector as Multivector +from numeria.manifold.spacetime import Plane as Plane +from numeria.manifold.polytope4 import Polytope4 as Polytope4 +from numeria.manifold.lie import Se2 as Se2 +from numeria.manifold.lie import Se3 as Se3 +from numeria.manifold.metric import Sig as Sig +from numeria.manifold.lie import Sim3 as Sim3 +from numeria.manifold.lie import Sl2C as Sl2C +from numeria.manifold.lie import Sl2Class as Sl2Class +from numeria.manifold.lie import Sl2R as Sl2R +from numeria.manifold.lie import So2 as So2 +from numeria.manifold.lie import So3 as So3 +from numeria.manifold.lie import So4 as So4 +from numeria.manifold.lie import Su2 as Su2 +from numeria.manifold.vecn import TensorN as TensorN +from numeria.manifold.lie import Unitary as Unitary +from numeria.manifold.polytope4 import Vec4 as Vec4 +from numeria.manifold.vecn import VecN as VecN +from numeria.manifold.clifford import algebra_dimension as algebra_dimension +from numeria.manifold.hyperbolic import apollonian_from_mobius as apollonian_from_mobius +from numeria.manifold.spherical import azimuthal_equidistant as azimuthal_equidistant +from numeria.manifold.spacetime import bekenstein_entropy as bekenstein_entropy +from numeria.manifold.dec import betti_curve as betti_curve +from numeria.manifold.spacetime import black_hole_shadow_radius as black_hole_shadow_radius +from numeria.manifold.clifford import blade_name as blade_name +from numeria.manifold.embedding import blobs as blobs +from numeria.manifold.lie import casimir_so3 as casimir_so3 +from numeria.manifold.clifford import cayley_table as cayley_table +from numeria.manifold.embedding import classical_mds as classical_mds +from numeria.manifold.lie import clebsch_gordan as clebsch_gordan +from numeria.manifold.polytope4 import clifford_torus as clifford_torus +from numeria.manifold.polytope4 import clifford_torus_mesh as clifford_torus_mesh +from numeria.manifold.embedding import continuity as continuity +from numeria.manifold.spacetime import cosmological_distances as cosmological_distances +from numeria.manifold.polytope4 import coxeter_plane_projection as coxeter_plane_projection +from numeria.manifold.polytope4 import cross_polytope_n as cross_polytope_n +from numeria.manifold.polytope4 import d4_lattice_points as d4_lattice_points +from numeria.manifold.vecn import determinant_n as determinant_n +from numeria.manifold.embedding import diffusion_maps as diffusion_maps +from numeria.manifold.hyperbolic import disk_to_hyperboloid as disk_to_hyperboloid +from numeria.manifold.hyperbolic import disk_to_klein as disk_to_klein +from numeria.manifold.hyperbolic import disk_to_uhp as disk_to_uhp +from numeria.manifold.embedding import dist_matrix as dist_matrix +from numeria.manifold.polytope4 import e8_lattice_nearest as e8_lattice_nearest +from numeria.manifold.polytope4 import e8_roots as e8_roots +from numeria.manifold.spacetime import eddington_finkelstein as eddington_finkelstein +from numeria.manifold.hyperbolic import equidistant_curve_disk as equidistant_curve_disk +from numeria.manifold.spherical import equirectangular as equirectangular +from numeria.manifold.spacetime import evaporation_time as evaporation_time +from numeria.manifold.spacetime import extra_dimension_gravity_law as extra_dimension_gravity_law +from numeria.manifold.polytope4 import f4_roots as f4_roots +from numeria.manifold.hyperbolic import fundamental_polygon_genus as fundamental_polygon_genus +from numeria.manifold.spherical import gauss_legendre_sphere as gauss_legendre_sphere +from numeria.manifold.polytope4 import gaussian_concentration_radius as gaussian_concentration_radius +from numeria.manifold.embedding import geodesic_distance_matrix as geodesic_distance_matrix +from numeria.manifold.embedding import geodesic_kmeans as geodesic_kmeans +from numeria.manifold.geodesic import geodesics_on_mesh_exact as geodesics_on_mesh_exact +from numeria.manifold.spherical import gnomonic as gnomonic +from numeria.manifold.embedding import grassmann_distance as grassmann_distance +from numeria.manifold.spacetime import gravitational_lens_einstein_radius as gravitational_lens_einstein_radius +from numeria.manifold.geodesic import great_circle_check as great_circle_check +from numeria.manifold.spacetime import gw_chirp_mass as gw_chirp_mass +from numeria.manifold.spacetime import gw_waveform_inspiral as gw_waveform_inspiral +from numeria.manifold.polytope4 import h4_roots as h4_roots +from numeria.manifold.lie import hand_eye_calibration as hand_eye_calibration +from numeria.manifold.spherical import haversine as haversine +from numeria.manifold.spacetime import hawking_temperature as hawking_temperature +from numeria.manifold.spherical import healpix_ang2pix as healpix_ang2pix +from numeria.manifold.spherical import healpix_npix as healpix_npix +from numeria.manifold.spherical import healpix_pix2ang as healpix_pix2ang +from numeria.manifold.geodesic import heat_method_geodesic as heat_method_geodesic +from numeria.manifold.embedding import helix_sample as helix_sample +from numeria.manifold.spherical import hopf_fiber as hopf_fiber +from numeria.manifold.spherical import hopf_fiber_stereographic as hopf_fiber_stereographic +from numeria.manifold.spherical import hopf_fibration as hopf_fibration +from numeria.manifold.hyperbolic import horocycle_disk as horocycle_disk +from numeria.manifold.hyperbolic import hyp_angle_of_parallelism as hyp_angle_of_parallelism +from numeria.manifold.hyperbolic import hyp_area_circle as hyp_area_circle +from numeria.manifold.hyperbolic import hyp_area_triangle as hyp_area_triangle +from numeria.manifold.hyperbolic import hyp_centroid_disk as hyp_centroid_disk +from numeria.manifold.hyperbolic import hyp_circle_disk as hyp_circle_disk +from numeria.manifold.hyperbolic import hyp_circumference as hyp_circumference +from numeria.manifold.hyperbolic import hyp_convex_hull_disk as hyp_convex_hull_disk +from numeria.manifold.hyperbolic import hyp_delaunay_disk as hyp_delaunay_disk +from numeria.manifold.hyperbolic import hyp_distance_disk as hyp_distance_disk +from numeria.manifold.hyperbolic import hyp_distance_hyperboloid as hyp_distance_hyperboloid +from numeria.manifold.hyperbolic import hyp_distance_uhp as hyp_distance_uhp +from numeria.manifold.hyperbolic import hyp_embed_graph_mds as hyp_embed_graph_mds +from numeria.manifold.hyperbolic import hyp_embed_tree as hyp_embed_tree +from numeria.manifold.hyperbolic import hyp_geodesic_circle_disk as hyp_geodesic_circle_disk +from numeria.manifold.hyperbolic import hyp_geodesic_disk as hyp_geodesic_disk +from numeria.manifold.hyperbolic import hyp_law_of_cosines as hyp_law_of_cosines +from numeria.manifold.hyperbolic import hyp_law_of_sines as hyp_law_of_sines +from numeria.manifold.hyperbolic import hyp_mean_curvature_flow as hyp_mean_curvature_flow +from numeria.manifold.hyperbolic import hyp_tiling as hyp_tiling +from numeria.manifold.hyperbolic import hyp_tiling_exists as hyp_tiling_exists +from numeria.manifold.hyperbolic import hyp_triangle_from_angles as hyp_triangle_from_angles +from numeria.manifold.hyperbolic import hyp_volume_ball as hyp_volume_ball +from numeria.manifold.hyperbolic import hyp_voronoi_disk as hyp_voronoi_disk +from numeria.manifold.hyperbolic import hyperbolic_rotation as hyperbolic_rotation +from numeria.manifold.hyperbolic import hyperbolic_translation as hyperbolic_translation +from numeria.manifold.hyperbolic import hyperboloid_to_disk as hyperboloid_to_disk +from numeria.manifold.polytope4 import hypercube_graph_n as hypercube_graph_n +from numeria.manifold.polytope4 import hypercube_n as hypercube_n +from numeria.manifold.polytope4 import hypercube_slicing_volume as hypercube_slicing_volume +from numeria.manifold.polytope4 import hypersphere_cap_fraction as hypersphere_cap_fraction +from numeria.manifold.polytope4 import hypersphere_s3_points as hypersphere_s3_points +from numeria.manifold.polytope4 import hypersphere_volume as hypersphere_volume +from numeria.manifold.embedding import intrinsic_dimension_correlation as intrinsic_dimension_correlation +from numeria.manifold.embedding import intrinsic_dimension_mle as intrinsic_dimension_mle +from numeria.manifold.embedding import intrinsic_dimension_two_nn as intrinsic_dimension_two_nn +from numeria.manifold.spherical import inverse_stereographic as inverse_stereographic +from numeria.manifold.clifford import is_isomorphic_to_known as is_isomorphic_to_known +from numeria.manifold.embedding import isomap as isomap +from numeria.manifold.hyperbolic import isometry_disk_from_two_points as isometry_disk_from_two_points +from numeria.manifold.spherical import kent_distribution_pdf as kent_distribution_pdf +from numeria.manifold.embedding import kernel_pca as kernel_pca +from numeria.manifold.metric import kerr_boyer_lindquist as kerr_boyer_lindquist +from numeria.manifold.spacetime import kerr_geodesic_constants as kerr_geodesic_constants +from numeria.manifold.lie import killing_form as killing_form +from numeria.manifold.polytope4 import kissing_number_known as kissing_number_known +from numeria.manifold.spacetime import kk_compactification_mass_spectrum as kk_compactification_mass_spectrum +from numeria.manifold.spacetime import kk_reduce_geodesic_to_charged as kk_reduce_geodesic_to_charged +from numeria.manifold.embedding import klein_sample as klein_sample +from numeria.manifold.hyperbolic import klein_to_disk as klein_to_disk +from numeria.manifold.embedding import knn_graph as knn_graph +from numeria.manifold.spacetime import kruskal_from_schwarzschild as kruskal_from_schwarzschild +from numeria.manifold.spherical import lambert_azimuthal_equal_area as lambert_azimuthal_equal_area +from numeria.manifold.embedding import laplacian_eigenmaps as laplacian_eigenmaps +from numeria.manifold.spherical import lebedev_quadrature as lebedev_quadrature +from numeria.manifold.polytope4 import leech_lattice_min_vectors_count as leech_lattice_min_vectors_count +from numeria.manifold.spacetime import lens_equation_solve as lens_equation_solve +from numeria.manifold.lie import lie_bracket_matrix as lie_bracket_matrix +from numeria.manifold.spacetime import light_cone_check as light_cone_check +from numeria.manifold.geodesic import light_deflection as light_deflection +from numeria.manifold.hyperbolic import limit_set_schottky as limit_set_schottky +from numeria.manifold.embedding import lle as lle +from numeria.manifold.hyperbolic import lorentz_boost_hyperboloid as lorentz_boost_hyperboloid +from numeria.manifold.embedding import manifold_curvature_estimate as manifold_curvature_estimate +from numeria.manifold.embedding import manifold_interpolation_rbf as manifold_interpolation_rbf +from numeria.manifold.lie import matrix_exp as matrix_exp +from numeria.manifold.lie import matrix_log as matrix_log +from numeria.manifold.lie import matrix_sqrt as matrix_sqrt +from numeria.manifold.spherical import mercator as mercator +from numeria.manifold.embedding import metric_mds_smacof as metric_mds_smacof +from numeria.manifold.hyperbolic import mobius_disk as mobius_disk +from numeria.manifold.embedding import mobius_sample as mobius_sample +from numeria.manifold.hyperbolic import mobius_uhp as mobius_uhp +from numeria.manifold.spherical import mollweide as mollweide +from numeria.manifold.embedding import neighborhood_preservation as neighborhood_preservation +from numeria.manifold.embedding import nonmetric_mds as nonmetric_mds +from numeria.manifold.spacetime import orbit_schwarzschild_full as orbit_schwarzschild_full +from numeria.manifold.spherical import orthographic as orthographic +from numeria.manifold.hyperbolic import parabolic as parabolic +from numeria.manifold.embedding import pca as pca +from numeria.manifold.spacetime import penrose_diagram_coords as penrose_diagram_coords +from numeria.manifold.geodesic import perihelion_precession as perihelion_precession +from numeria.manifold.dec import persistence_diagram_bottleneck as persistence_diagram_bottleneck +from numeria.manifold.dec import persistent_homology_vietoris_rips as persistent_homology_vietoris_rips +from numeria.manifold.polytope4 import petrie_polygon_projection as petrie_polygon_projection +from numeria.manifold.geodesic import photon_orbit_stability as photon_orbit_stability +from numeria.manifold.spacetime import photon_ray_trace_schwarzschild as photon_ray_trace_schwarzschild +from numeria.manifold.hyperbolic import poincare_embedding_train as poincare_embedding_train +from numeria.manifold.spacetime import point_lens_magnification as point_lens_magnification +from numeria.manifold.embedding import procrustes_align as procrustes_align +from numeria.manifold.polytope4 import project_n_to_2 as project_n_to_2 +from numeria.manifold.polytope4 import project_n_to_3 as project_n_to_3 +from numeria.manifold.polytope4 import random_walk_n_return_prob as random_walk_n_return_prob +from numeria.manifold.spacetime import relativistic_rocket as relativistic_rocket +from numeria.manifold.embedding import riemannian_gradient_descent_sphere as riemannian_gradient_descent_sphere +from numeria.manifold.spacetime import rindler_coords as rindler_coords +from numeria.manifold.spacetime import rindler_horizon as rindler_horizon +from numeria.manifold.spherical import robinson as robinson +from numeria.manifold.polytope4 import rotate_4d as rotate_4d +from numeria.manifold.polytope4 import rotate_4d_double as rotate_4d_double +from numeria.manifold.spherical import rotate_sphere_points as rotate_sphere_points +from numeria.manifold.lie import rotate_spherical_harmonics as rotate_spherical_harmonics +from numeria.manifold.polytope4 import rotation_4d_planes as rotation_4d_planes +from numeria.manifold.lie import rotation_averaging as rotation_averaging +from numeria.manifold.spherical import s3_geodesic as s3_geodesic +from numeria.manifold.spherical import s3_uniform_points as s3_uniform_points +from numeria.manifold.embedding import s_curve as s_curve +from numeria.manifold.spacetime import schwarzschild_geodesic_metric as schwarzschild_geodesic_metric +from numeria.manifold.geodesic import schwarzschild_orbit as schwarzschild_orbit +from numeria.manifold.lie import se3 as se3 +from numeria.manifold.geodesic import shapiro_delay as shapiro_delay +from numeria.manifold.polytope4 import simplex_n as simplex_n +from numeria.manifold.spacetime import simultaneity_plane as simultaneity_plane +from numeria.manifold.lie import so3 as so3 +from numeria.manifold.lie import so3_haar_measure_density as so3_haar_measure_density +from numeria.manifold.lie import so3_uniform_grid as so3_uniform_grid +from numeria.manifold.embedding import spectral_embedding as spectral_embedding +from numeria.manifold.spherical import sphere_cap_area as sphere_cap_area +from numeria.manifold.spherical import sphere_cap_volume as sphere_cap_volume +from numeria.manifold.spherical import sphere_distance_n as sphere_distance_n +from numeria.manifold.spherical import sphere_exp_n as sphere_exp_n +from numeria.manifold.spherical import sphere_geodesic_n as sphere_geodesic_n +from numeria.manifold.spherical import sphere_log_n as sphere_log_n +from numeria.manifold.spherical import sphere_parallel_transport_n as sphere_parallel_transport_n +from numeria.manifold.embedding import sphere_sample as sphere_sample +from numeria.manifold.spherical import sphere_surface_n as sphere_surface_n +from numeria.manifold.spherical import sphere_uniform_points_n as sphere_uniform_points_n +from numeria.manifold.spherical import sphere_volume_n as sphere_volume_n +from numeria.manifold.spherical import spherical_cap_packing as spherical_cap_packing +from numeria.manifold.spherical import spherical_centroid as spherical_centroid +from numeria.manifold.spherical import spherical_code_min_angle as spherical_code_min_angle +from numeria.manifold.spherical import spherical_convex_hull as spherical_convex_hull +from numeria.manifold.spherical import spherical_convolution as spherical_convolution +from numeria.manifold.spherical import spherical_delaunay as spherical_delaunay +from numeria.manifold.spherical import spherical_harmonic_inverse as spherical_harmonic_inverse +from numeria.manifold.spherical import spherical_harmonic_transform as spherical_harmonic_transform +from numeria.manifold.spherical import spherical_harmonics_complex as spherical_harmonics_complex +from numeria.manifold.spherical import spherical_heat_flow as spherical_heat_flow +from numeria.manifold.spherical import spherical_kmeans as spherical_kmeans +from numeria.manifold.spherical import spherical_laplacian_spectral as spherical_laplacian_spectral +from numeria.manifold.spherical import spherical_law_of_cosines as spherical_law_of_cosines +from numeria.manifold.spherical import spherical_law_of_sines as spherical_law_of_sines +from numeria.manifold.spherical import spherical_mean_weighted as spherical_mean_weighted +from numeria.manifold.spherical import spherical_polygon_area as spherical_polygon_area +from numeria.manifold.spherical import spherical_t_design as spherical_t_design +from numeria.manifold.spherical import spherical_triangle_angles as spherical_triangle_angles +from numeria.manifold.spherical import spherical_triangle_area as spherical_triangle_area +from numeria.manifold.spherical import spherical_voronoi as spherical_voronoi +from numeria.manifold.spherical import spherical_wavelets as spherical_wavelets +from numeria.manifold.spacetime import sta_vs_matrix_lorentz_check as sta_vs_matrix_lorentz_check +from numeria.manifold.spherical import stereographic as stereographic +from numeria.manifold.spherical import stereographic_n as stereographic_n +from numeria.manifold.embedding import stiefel_project as stiefel_project +from numeria.manifold.embedding import stress as stress +from numeria.manifold.lie import structure_constants as structure_constants +from numeria.manifold.embedding import swiss_roll as swiss_roll +from numeria.manifold.embedding import tangent_space_estimate as tangent_space_estimate +from numeria.manifold.spherical import thomson_problem as thomson_problem +from numeria.manifold.embedding import torus_sample as torus_sample +from numeria.manifold.embedding import trustworthiness as trustworthiness +from numeria.manifold.embedding import tsne as tsne +from numeria.manifold.spacetime import twin_paradox_ages as twin_paradox_ages +from numeria.manifold.embedding import two_moons as two_moons +from numeria.manifold.hyperbolic import uhp_to_disk as uhp_to_disk +from numeria.manifold.embedding import umap_lite as umap_lite +from numeria.manifold.lie import umeyama_alignment as umeyama_alignment +from numeria.manifold.spacetime import unruh_temperature as unruh_temperature +from numeria.manifold.spherical import vmf_fit as vmf_fit +from numeria.manifold.spherical import vmf_sample as vmf_sample +from numeria.manifold.polytope4 import volume_ball_vs_cube_ratio as volume_ball_vs_cube_ratio +from numeria.manifold.spherical import von_mises_fisher_pdf as von_mises_fisher_pdf +from numeria.manifold.vecn import wedge as wedge +from numeria.manifold.lie import wigner_d as wigner_d +from numeria.manifold.lie import wigner_d_small as wigner_d_small + + diff --git a/bindings/python/python/numeria/manifold/clifford/__init__.pyi b/bindings/python/python/numeria/manifold/clifford/__init__.pyi new file mode 100644 index 0000000..237324b --- /dev/null +++ b/bindings/python/python/numeria/manifold/clifford/__init__.pyi @@ -0,0 +1,116 @@ +""" +Clifford (geometric) algebras Cl(p, q, r): a dense multivector type over any signature, with the geometric/outer/inner products, versors and rotors, and specialized models — Euclidean `cl3`, projective `pga3`, conformal `cga3`, and spacetime `sta` geometric algebra. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from . import cga3, cl3, pga3, sta +from numeria.quantum.schrodinger import Basis +from numeria.linalg.matrix import Matrix +from numeria.quaternion import Quaternion + +class Multivector: + """ +A dense multivector in Cl(p, q, r): 2^(p+q+r) coefficients indexed by +basis-blade bitmask (bit i set means basis vector i is a factor; bits +0..p square to +1, the next q to -1, the last r to 0). + +Rust: `manifold::clifford::Multivector` + """ + def __init__(self, p: int, q: int, r: int, coeffs: list[float]) -> None: ... + @staticmethod + def zero(p: int, q: int, r: int) -> Multivector: ... + @staticmethod + def scalar(s: float, p: int, q: int, r: int) -> Multivector: ... + @staticmethod + def vector(v: list[float], p: int, q: int, r: int) -> Multivector: ... + @staticmethod + def basis_blade(mask: int, p: int, q: int, r: int) -> Multivector: ... + @staticmethod + def pseudoscalar(p: int, q: int, r: int) -> Multivector: ... + def geometric(self, o: Multivector) -> Multivector: ... + def wedge(self, o: Multivector) -> Multivector: ... + def inner(self, o: Multivector) -> Multivector: ... + def scalar_product(self, o: Multivector) -> float: ... + def commutator(self, o: Multivector) -> Multivector: ... + def regressive(self, o: Multivector) -> Multivector: ... + def reverse(self) -> Multivector: ... + def grade_involution(self) -> Multivector: ... + def clifford_conjugate(self) -> Multivector: ... + def dual(self) -> Multivector: ... + def undual(self) -> Multivector: ... + def grade(self, k: int) -> Multivector: ... + def grades(self) -> list[int]: ... + def is_blade(self) -> bool: ... + def is_versor(self) -> bool: ... + def norm_squared(self) -> float: ... + def norm(self) -> float: ... + def normalized(self) -> Multivector: ... + def inverse(self) -> Optional[Multivector]: ... + def exp(self) -> Multivector: ... + def log(self) -> Optional[Multivector]: ... + def sandwich(self, x: Multivector) -> Multivector: ... + @staticmethod + def rotor_from_vectors(a: Multivector, b: Multivector) -> Multivector: ... + @staticmethod + def rotor_from_plane_angle(b: Multivector, angle: float) -> Multivector: ... + def rotor_interpolate(self, o: Multivector, t: float) -> Multivector: ... + def to_quaternion(self) -> Optional[Quaternion]: ... + @staticmethod + def from_quaternion(q: Quaternion | Sequence[float]) -> Multivector: ... + def to_matrix_rep(self) -> Matrix: ... + def meet(self, o: Multivector) -> Multivector: ... + def join(self, o: Multivector) -> Multivector: ... + def blade_factor(self) -> list[Multivector]: ... + def project_onto_blade(self, b: Multivector) -> Multivector: ... + def reject_from_blade(self, b: Multivector) -> Multivector: ... + def reflect_in_vector(self, n: Multivector) -> Multivector: ... + def reflect_in_hyperplane(self, n: Multivector) -> Multivector: ... + def add(self, o: Multivector) -> Multivector: ... + def sub(self, o: Multivector) -> Multivector: ... + def scale(self, k: float) -> Multivector: ... + def mul_scalar(self, k: float) -> Multivector: ... + def to_string_blades(self) -> str: ... + @property + def p(self) -> int: ... + @property + def q(self) -> int: ... + @property + def r(self) -> int: ... + @property + def coeffs(self) -> list[float]: ... + +def cayley_table(p: int, q: int, r: int) -> list[list[tuple[float, int]]]: + """ +Basis-blade multiplication table: `table[a][b] = (sign, result mask)`. + +Rust: `manifold::clifford::cayley_table` + """ + ... + +def blade_name(mask: int, p: int, q: int, r: int) -> str: + """ +Name of a basis blade, e.g. "e12" (1-indexed factors). + +Rust: `manifold::clifford::blade_name` + """ + ... + +def algebra_dimension(p: int, q: int, r: int) -> int: + """ +Dimension of the algebra: 2^(p+q+r). + +Rust: `manifold::clifford::algebra_dimension` + """ + ... + +def is_isomorphic_to_known(p: int, q: int, r: int) -> str: + """ +Classification of small Clifford algebras by isomorphism type. + +Rust: `manifold::clifford::is_isomorphic_to_known` + """ + ... diff --git a/bindings/python/python/numeria/manifold/clifford/cga3.pyi b/bindings/python/python/numeria/manifold/clifford/cga3.pyi new file mode 100644 index 0000000..e4a670e --- /dev/null +++ b/bindings/python/python/numeria/manifold/clifford/cga3.pyi @@ -0,0 +1,322 @@ +""" +Conformal geometric algebra Cl(4, 1): points, spheres, circles, lines +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.manifold.clifford import Multivector +from numeria.statistics.distributions import Normal +from numeria.manifold.lie import Sim3 +from numeria.spatial.primitives import Sphere +from numeria.math import Vec3 +from numeria.manifold.polytope4 import Vec4 + +class CgaObject: + """ +Kinds of CGA object. + +Rust: `manifold::clifford::cga3::CgaObject` + """ + ... + +def e_inf() -> Multivector: + """ +The null vector at infinity. + +Rust: `manifold::clifford::cga3::e_inf` + """ + ... + +def e_0() -> Multivector: + """ +The null origin vector. + +Rust: `manifold::clifford::cga3::e_0` + """ + ... + +def e_plus() -> Multivector: + """ +The positive-signature extra basis vector. + +Rust: `manifold::clifford::cga3::e_plus` + """ + ... + +def e_minus() -> Multivector: + """ +The negative-signature extra basis vector. + +Rust: `manifold::clifford::cga3::e_minus` + """ + ... + +def point(p: Vec3 | Sequence[float]) -> Multivector: + """ +Conformal up-projection of a Euclidean point: +P = p + (1/2) p^2 e_inf + e_0. + +Rust: `manifold::clifford::cga3::point` + """ + ... + +def down(x: Multivector) -> Optional[Vec3]: + """ +Euclidean coordinates of a conformal point (None for ideal points). + +Rust: `manifold::clifford::cga3::down` + """ + ... + +def sphere(center: Vec3 | Sequence[float], r: float) -> Multivector: + """ +IPNS sphere with the given center and radius. + +Rust: `manifold::clifford::cga3::sphere` + """ + ... + +def plane(n: Vec3 | Sequence[float], d: float) -> Multivector: + """ +IPNS plane n . x = d (unit normal recommended). + +Rust: `manifold::clifford::cga3::plane` + """ + ... + +def circle_from_points(a: Vec3 | Sequence[float], b: Vec3 | Sequence[float], c: Vec3 | Sequence[float]) -> Multivector: + """ +IPNS circle through three points. + +Rust: `manifold::clifford::cga3::circle_from_points` + """ + ... + +def line_from_points(a: Vec3 | Sequence[float], b: Vec3 | Sequence[float]) -> Multivector: + """ +IPNS line through two points. + +Rust: `manifold::clifford::cga3::line_from_points` + """ + ... + +def point_pair(a: Vec3 | Sequence[float], b: Vec3 | Sequence[float]) -> Multivector: + """ +IPNS point pair. + +Rust: `manifold::clifford::cga3::point_pair` + """ + ... + +def sphere_from_points(a: Vec3 | Sequence[float], b: Vec3 | Sequence[float], c: Vec3 | Sequence[float], d: Vec3 | Sequence[float]) -> Multivector: + """ +IPNS sphere through four points. + +Rust: `manifold::clifford::cga3::sphere_from_points` + """ + ... + +def meet(a: Multivector, b: Multivector) -> Multivector: + """ +Meet of IPNS objects (their intersection): the outer product. + +Rust: `manifold::clifford::cga3::meet` + """ + ... + +def circle_center_radius_normal(c: Multivector) -> tuple[Vec3, float, Vec3]: + """ +Center, radius, and plane normal of an IPNS circle. + +Rust: `manifold::clifford::cga3::circle_center_radius_normal` + """ + ... + +def sphere_center_radius(s: Multivector) -> tuple[Vec3, float]: + """ +Center and radius of an IPNS sphere. + +Rust: `manifold::clifford::cga3::sphere_center_radius` + """ + ... + +def line_point_direction(l: Multivector) -> tuple[Vec3, Vec3]: + """ +A point on an IPNS line and its direction. + +Rust: `manifold::clifford::cga3::line_point_direction` + """ + ... + +def plane_normal_distance(pl: Multivector) -> tuple[Vec3, float]: + """ +Normal and offset of an IPNS plane (n . x = d). + +Rust: `manifold::clifford::cga3::plane_normal_distance` + """ + ... + +def classify(x: Multivector) -> CgaObject: + """ +Classify an IPNS object by grade and flatness. + +Rust: `manifold::clifford::cga3::classify` + """ + ... + +def distance(a: Multivector, b: Multivector) -> float: + """ +Euclidean distance between two conformal points: d^2 = -2 A . B. + +Rust: `manifold::clifford::cga3::distance` + """ + ... + +def is_inside_sphere(p: Multivector, s: Multivector) -> bool: + """ +True when the point lies strictly inside the sphere. + +Rust: `manifold::clifford::cga3::is_inside_sphere` + """ + ... + +def translator(t: Vec3 | Sequence[float]) -> Multivector: + """ +Translator versor: T = 1 - (1/2) t e_inf. + +Rust: `manifold::clifford::cga3::translator` + """ + ... + +def rotor(axis: Vec3 | Sequence[float], angle: float) -> Multivector: + """ +Euclidean rotor about an axis through the origin. + +Rust: `manifold::clifford::cga3::rotor` + """ + ... + +def dilator(scale: float) -> Multivector: + """ +Dilator scaling by `scale` about the origin. + +Rust: `manifold::clifford::cga3::dilator` + """ + ... + +def transversor(v: Vec3 | Sequence[float]) -> Multivector: + """ +Transversor (special conformal) versor. + +Rust: `manifold::clifford::cga3::transversor` + """ + ... + +def inversion_in_sphere(s: Multivector) -> Multivector: + """ +Inversion versor in a sphere (the sphere itself acts by sandwich). + +Rust: `manifold::clifford::cga3::inversion_in_sphere` + """ + ... + +def motor(t: Vec3 | Sequence[float], axis: Vec3 | Sequence[float], angle: float) -> Multivector: + """ +Rigid motor: translation then rotation. + +Rust: `manifold::clifford::cga3::motor` + """ + ... + +def conformal_from_similarity(s: Sim3) -> Multivector: + """ +Conformal versor for a similarity transform. + +Rust: `manifold::clifford::cga3::conformal_from_similarity` + """ + ... + +def apply(versor: Multivector, x: Multivector) -> Multivector: + """ +Apply a versor by the sandwich product (with the grade involution +for odd versors such as spheres and planes). + +Rust: `manifold::clifford::cga3::apply` + """ + ... + +def reflect_in_sphere(x: Multivector, s: Multivector) -> Multivector: + """ +Sphere inversion as a reflection: S X S normalized. + +Rust: `manifold::clifford::cga3::reflect_in_sphere` + """ + ... + +def interpolate_versor(a: Multivector, b: Multivector, t: float) -> Multivector: + """ +Linear versor interpolation with renormalization. + +Rust: `manifold::clifford::cga3::interpolate_versor` + """ + ... + +def apollonius_problem(c1: Multivector, c2: Multivector, c3: Multivector) -> list[Multivector]: + """ +Apollonius problem: spheres tangent to three given spheres (solved +in Euclidean form, returned as IPNS spheres). + +Rust: `manifold::clifford::cga3::apollonius_problem` + """ + ... + +def circle_through_intersection(s1: Multivector, s2: Multivector) -> Multivector: + """ +The circle in which two spheres intersect. + +Rust: `manifold::clifford::cga3::circle_through_intersection` + """ + ... + +def tangent_at(surface: Multivector, pt: Multivector) -> Multivector: + """ +Tangent plane to a sphere at a point on it. + +Rust: `manifold::clifford::cga3::tangent_at` + """ + ... + +def carrier(x: Multivector) -> Multivector: + """ +The flat carrier of a round: the plane containing a circle. + +Rust: `manifold::clifford::cga3::carrier` + """ + ... + +def dual_cga(x: Multivector) -> Multivector: + """ +IPNS <-> OPNS dualization. + +Rust: `manifold::clifford::cga3::dual_cga` + """ + ... + +def point_to_sphere_tangent_distance(p: Multivector, s: Multivector) -> float: + """ +Length of the tangent from a point to a sphere. + +Rust: `manifold::clifford::cga3::point_to_sphere_tangent_distance` + """ + ... + +def stereographic_via_cga(p: Vec3 | Sequence[float]) -> Vec4: + """ +Inverse stereographic projection R3 -> S3 via the conformal model. + +Rust: `manifold::clifford::cga3::stereographic_via_cga` + """ + ... diff --git a/bindings/python/python/numeria/manifold/clifford/cl3.pyi b/bindings/python/python/numeria/manifold/clifford/cl3.pyi new file mode 100644 index 0000000..8f4c268 --- /dev/null +++ b/bindings/python/python/numeria/manifold/clifford/cl3.pyi @@ -0,0 +1,102 @@ +""" +Euclidean 3D geometric algebra Cl(3, 0). +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.manifold.clifford import Multivector +from numeria.quaternion import Quaternion +from numeria.math import Vec3 + +def vec(v: Vec3 | Sequence[float]) -> Multivector: + """ +Grade-1 vector. + +Rust: `manifold::clifford::cl3::vec` + """ + ... + +def bivec(b: Vec3 | Sequence[float]) -> Multivector: + """ +Bivector dual to the vector b (the plane with normal b). + +Rust: `manifold::clifford::cl3::bivec` + """ + ... + +def pseudoscalar() -> Multivector: + """ +The pseudoscalar e123. + +Rust: `manifold::clifford::cl3::pseudoscalar` + """ + ... + +def rotor(axis: Vec3 | Sequence[float], angle: float) -> Multivector: + """ +Rotor for a rotation about `axis` by `angle` (matches quaternion +rotation). + +Rust: `manifold::clifford::cl3::rotor` + """ + ... + +def rotate(v: Vec3 | Sequence[float], r: Multivector) -> Vec3: + """ +Rotate a vector with a rotor: R v R~. + +Rust: `manifold::clifford::cl3::rotate` + """ + ... + +def cross_via_wedge(a: Vec3 | Sequence[float], b: Vec3 | Sequence[float]) -> Vec3: + """ +The cross product via the wedge: a x b = -I (a ∧ b). + +Rust: `manifold::clifford::cl3::cross_via_wedge` + """ + ... + +def reflect(v: Vec3 | Sequence[float], n: Vec3 | Sequence[float]) -> Vec3: + """ +Reflect v in the plane with unit normal n. + +Rust: `manifold::clifford::cl3::reflect` + """ + ... + +def to_vec3(m: Multivector) -> Optional[Vec3]: + """ +Extract the grade-1 part as a Vec3 (None if other grades dominate). + +Rust: `manifold::clifford::cl3::to_vec3` + """ + ... + +def plane_from_points(a: Vec3 | Sequence[float], b: Vec3 | Sequence[float], c: Vec3 | Sequence[float]) -> Multivector: + """ +The plane (bivector) through three points, with weight twice the +triangle area. + +Rust: `manifold::clifford::cl3::plane_from_points` + """ + ... + +def line_from_points(a: Vec3 | Sequence[float], b: Vec3 | Sequence[float]) -> Multivector: + """ +The line direction blade through two points (their difference). + +Rust: `manifold::clifford::cl3::line_from_points` + """ + ... + +def rotor_to_quaternion(r: Multivector) -> Optional[Quaternion]: + """ +Rotor as quaternion. + +Rust: `manifold::clifford::cl3::rotor_to_quaternion` + """ + ... diff --git a/bindings/python/python/numeria/manifold/clifford/pga3.pyi b/bindings/python/python/numeria/manifold/clifford/pga3.pyi new file mode 100644 index 0000000..ad4c80c --- /dev/null +++ b/bindings/python/python/numeria/manifold/clifford/pga3.pyi @@ -0,0 +1,240 @@ +""" +Plane-based projective geometric algebra Cl(3, 0, 1): planes are +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.manifold.clifford import Multivector +from numeria.manifold.lie import Se3 +from numeria.math import Vec3 + +def plane(n: Vec3 | Sequence[float], d: float) -> Multivector: + """ +The plane n . x + d = 0 as a grade-1 element. + +Rust: `manifold::clifford::pga3::plane` + """ + ... + +def point(p: Vec3 | Sequence[float]) -> Multivector: + """ +A Euclidean point as the meet of three axis-aligned planes. + +Rust: `manifold::clifford::pga3::point` + """ + ... + +def point_at_infinity(d: Vec3 | Sequence[float]) -> Multivector: + """ +Ideal (infinite) point in direction d. + +Rust: `manifold::clifford::pga3::point_at_infinity` + """ + ... + +def line_from_points(a: Vec3 | Sequence[float], b: Vec3 | Sequence[float]) -> Multivector: + """ +Line through two points (their join). + +Rust: `manifold::clifford::pga3::line_from_points` + """ + ... + +def line_from_planes(p: Multivector, q: Multivector) -> Multivector: + """ +Line as the meet of two planes. + +Rust: `manifold::clifford::pga3::line_from_planes` + """ + ... + +def plane_from_points(a: Vec3 | Sequence[float], b: Vec3 | Sequence[float], c: Vec3 | Sequence[float]) -> Multivector: + """ +Plane through three points (their join). + +Rust: `manifold::clifford::pga3::plane_from_points` + """ + ... + +def meet(a: Multivector, b: Multivector) -> Multivector: + """ +Meet (intersection): the outer product in the plane-based algebra. + +Rust: `manifold::clifford::pga3::meet` + """ + ... + +def join(a: Multivector, b: Multivector) -> Multivector: + """ +Join (span): the regressive product. + +Rust: `manifold::clifford::pga3::join` + """ + ... + +def to_vec3(p: Multivector) -> Optional[Vec3]: + """ +Euclidean coordinates of a (normalized or unnormalized) point. + +Rust: `manifold::clifford::pga3::to_vec3` + """ + ... + +def is_ideal(x: Multivector) -> bool: + """ +True for ideal (infinite) elements: zero weight. + +Rust: `manifold::clifford::pga3::is_ideal` + """ + ... + +def distance_point_plane(p: Multivector, pl: Multivector) -> float: + """ +Signed distance from a point to a plane (both normalized inside). + +Rust: `manifold::clifford::pga3::distance_point_plane` + """ + ... + +def distance_point_line(p: Multivector, l: Multivector) -> float: + """ +Distance from a point to a line. + +Rust: `manifold::clifford::pga3::distance_point_line` + """ + ... + +def distance_lines(l1: Multivector, l2: Multivector) -> float: + """ +Distance between two lines. + +Rust: `manifold::clifford::pga3::distance_lines` + """ + ... + +def angle_planes(p: Multivector, q: Multivector) -> float: + """ +Angle between two planes. + +Rust: `manifold::clifford::pga3::angle_planes` + """ + ... + +def angle_lines(l1: Multivector, l2: Multivector) -> float: + """ +Angle between two lines. + +Rust: `manifold::clifford::pga3::angle_lines` + """ + ... + +def motor_translation(t: Vec3 | Sequence[float]) -> Multivector: + """ +Motor translating by t. + +Rust: `manifold::clifford::pga3::motor_translation` + """ + ... + +def motor_rotation(axis_line: Multivector, angle: float) -> Multivector: + """ +Motor rotating by `angle` about an axis line. + +Rust: `manifold::clifford::pga3::motor_rotation` + """ + ... + +def motor_screw(line: Multivector, angle: float, dist: float) -> Multivector: + """ +Screw motor: rotate by `angle` about the line while translating +`dist` along it. + +Rust: `manifold::clifford::pga3::motor_screw` + """ + ... + +def motor_from_se3(m: Se3) -> Multivector: + """ +Motor from a rigid transform. + +Rust: `manifold::clifford::pga3::motor_from_se3` + """ + ... + +def motor_to_se3(m: Multivector) -> Se3: + """ +Rigid transform from a motor. + +Rust: `manifold::clifford::pga3::motor_to_se3` + """ + ... + +def motor_interpolate(a: Multivector, b: Multivector, t: float) -> Multivector: + """ +Screw interpolation between motors (through Se3's exact screw). + +Rust: `manifold::clifford::pga3::motor_interpolate` + """ + ... + +def motor_apply(m: Multivector, x: Multivector) -> Multivector: + """ +Apply a motor by the sandwich product. + +Rust: `manifold::clifford::pga3::motor_apply` + """ + ... + +def project_point_on_line(p: Multivector, l: Multivector) -> Multivector: + """ +Orthogonal projection of a point onto a line. + +Rust: `manifold::clifford::pga3::project_point_on_line` + """ + ... + +def project_point_on_plane(p: Multivector, pl: Multivector) -> Multivector: + """ +Orthogonal projection of a point onto a plane. + +Rust: `manifold::clifford::pga3::project_point_on_plane` + """ + ... + +def project_line_on_plane(l: Multivector, pl: Multivector) -> Multivector: + """ +Orthogonal projection of a line onto a plane. + +Rust: `manifold::clifford::pga3::project_line_on_plane` + """ + ... + +def rigid_body_step(motor: Multivector, rate: Multivector, dt: float) -> None: + """ +One explicit step of PGA rigid-body dynamics (Gunn): the motor +advances by its body-frame rate bivector. + +Rust: `manifold::clifford::pga3::rigid_body_step` + """ + ... + +def inertia_dual_map(rate: Multivector, inertia: list[float], mass: float) -> Multivector: + """ +Diagonal inertia map on body-rate bivectors: scales the rotational +components by (ixx, iyy, izz) and the translational by the mass. + +Rust: `manifold::clifford::pga3::inertia_dual_map` + """ + ... + +def forque(force: Vec3 | Sequence[float], application_point: Vec3 | Sequence[float]) -> Multivector: + """ +Forque (force + torque) bivector of a force applied at a point: the +weighted line through the point in the force direction. + +Rust: `manifold::clifford::pga3::forque` + """ + ... diff --git a/bindings/python/python/numeria/manifold/clifford/sta.pyi b/bindings/python/python/numeria/manifold/clifford/sta.pyi new file mode 100644 index 0000000..5aa1fee --- /dev/null +++ b/bindings/python/python/numeria/manifold/clifford/sta.pyi @@ -0,0 +1,111 @@ +""" +Spacetime algebra Cl(1, 3): gamma_0 squares to +1 (bit 0), the spatial +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.manifold.clifford import Multivector +from numeria.quaternion import Quaternion +from numeria.math import Vec3 + +def event(t: float, x: Vec3 | Sequence[float]) -> Multivector: + """ +Spacetime event t gamma_0 + x . gamma. + +Rust: `manifold::clifford::sta::event` + """ + ... + +def boost(v: Vec3 | Sequence[float]) -> Multivector: + """ +Boost rotor for velocity v (|v| < 1, c = 1). + +Rust: `manifold::clifford::sta::boost` + """ + ... + +def rotation(axis: Vec3 | Sequence[float], angle: float) -> Multivector: + """ +Spatial rotation rotor. + +Rust: `manifold::clifford::sta::rotation` + """ + ... + +def lorentz_apply(r: Multivector, e: Multivector) -> Multivector: + """ +Apply a Lorentz rotor to an event: R e R~. + +Rust: `manifold::clifford::sta::lorentz_apply` + """ + ... + +def bivector_em(e_field: Vec3 | Sequence[float], b_field: Vec3 | Sequence[float]) -> Multivector: + """ +Faraday bivector F = E . sigma + I B . sigma. + +Rust: `manifold::clifford::sta::bivector_em` + """ + ... + +def em_invariants(f: Multivector) -> tuple[float, float]: + """ +Electromagnetic invariants from F^2 = (E^2 - B^2) + 2 (E . B) I: +returns (E^2 - B^2, E . B). + +Rust: `manifold::clifford::sta::em_invariants` + """ + ... + +def lorentz_force_sta(f: Multivector, velocity: Multivector, q: float, m: float) -> Multivector: + """ +Lorentz force: dp/dtau = q F . v (grade-1 contraction), for a +particle of charge q and mass m returns the 4-acceleration. + +Rust: `manifold::clifford::sta::lorentz_force_sta` + """ + ... + +def proper_time(path: list[Multivector]) -> float: + """ +Proper time along a piecewise-linear worldline of events. + +Rust: `manifold::clifford::sta::proper_time` + """ + ... + +def rapidity(v: float) -> float: + """ +Rapidity of a speed: atanh(v). + +Rust: `manifold::clifford::sta::rapidity` + """ + ... + +def spacetime_split(x: Multivector, observer: Multivector) -> tuple[float, Vec3]: + """ +Split an event into (time, space) relative to an observer 4-velocity +(default observer: gamma_0). + +Rust: `manifold::clifford::sta::spacetime_split` + """ + ... + +def dirac_gamma_matrices() -> list[list[list[complex]]]: + """ +The Dirac gamma matrices (Dirac basis) as 4x4 complex matrices. + +Rust: `manifold::clifford::sta::dirac_gamma_matrices` + """ + ... + +def pauli_to_sta(q: Quaternion | Sequence[float]) -> Multivector: + """ +Map a Pauli/quaternion rotation to the STA spatial rotor. + +Rust: `manifold::clifford::sta::pauli_to_sta` + """ + ... diff --git a/bindings/python/python/numeria/manifold/dec.pyi b/bindings/python/python/numeria/manifold/dec.pyi new file mode 100644 index 0000000..c8fb57a --- /dev/null +++ b/bindings/python/python/numeria/manifold/dec.pyi @@ -0,0 +1,83 @@ +""" +Discrete exterior calculus on triangle meshes: exterior derivatives, diagonal Hodge stars, Laplacians, Hodge decomposition, harmonic forms and Betti numbers, heat and Poisson solves, spectral shape analysis, curvature flows, and persistent homology. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.linalg.sparse import CsrMatrix +from numeria.math import Vec3 +from numeria.manifold.vecn import VecN + +class DecMesh: + """ +A triangle mesh with its DEC operators: primal edges, exterior +derivatives d0 and d1, and diagonal Hodge stars. + +Rust: `manifold::dec::DecMesh` + """ + def __init__(self, mesh: Mesh) -> None: ... + def d0(self) -> CsrMatrix: ... + def d1(self) -> CsrMatrix: ... + def hodge0(self) -> list[float]: ... + def hodge1(self) -> list[float]: ... + def hodge2(self) -> list[float]: ... + def dual_areas(self) -> list[float]: ... + def laplace_beltrami(self) -> CsrMatrix: ... + def laplace_1form(self) -> CsrMatrix: ... + def gradient(self, f: list[float]) -> list[float]: ... + def curl(self, w: list[float]) -> list[float]: ... + def divergence(self, w: list[float]) -> list[float]: ... + def hodge_decomposition(self, w: list[float]) -> tuple[list[float], list[float], list[float]]: ... + def harmonic_forms(self) -> list[list[float]]: ... + def betti_numbers(self) -> list[int]: ... + def simplicial_cohomology_rank(self, k: int) -> int: ... + def interpolate_1form_to_vectors(self, w: list[float]) -> list[Vec3]: ... + def vector_field_to_1form(self, v: list[Vec3 | Sequence[float]]) -> list[float]: ... + def heat_flow(self, f0: list[float], t: float, steps: int) -> list[float]: ... + def poisson_solve(self, rho: list[float], fixed: list[tuple[int, float]]) -> list[float]: ... + def eigenmodes(self, n: int) -> tuple[list[float], list[list[float]]]: ... + def geodesic_heat_method(self, source: int, t: float) -> list[float]: ... + def vector_heat_method(self, source: int, v0: Vec3 | Sequence[float], t: float) -> list[Vec3]: ... + def trivial_connection(self, singularities: list[tuple[int, float]]) -> list[float]: ... + def smoothest_direction_field(self, n_rosy: int) -> list[Vec3]: ... + def stream_function(self, v: list[float]) -> list[float]: ... + def fluid_step_dec(self, w: MutableSequence[float], dt: float, nu: float) -> None: ... + def mean_curvature_flow_step(self, dt: float) -> Mesh: ... + def willmore_energy(self) -> float: ... + def discrete_gauss_bonnet_check(self) -> float: ... + @property + def mesh(self) -> Mesh: ... + @property + def edges(self) -> list[tuple[int, int]]: ... + +def persistent_homology_vietoris_rips(points: list[VecN | Sequence[float]], max_eps: float, max_dim: int) -> list[tuple[int, float, float]]: + """ +Vietoris-Rips persistent homology in dimensions 0 and 1: returns +(dimension, birth, death) pairs (essential classes get death = +`max_eps`). + +Rust: `manifold::dec::persistent_homology_vietoris_rips` + """ + ... + +def persistence_diagram_bottleneck(a: list[tuple[float, float]], b: list[tuple[float, float]]) -> float: + """ +Bottleneck distance between two persistence diagrams (same dimension), +by binary search over candidate distances with greedy augmenting-path +matching (diagonal projections allowed). + +Rust: `manifold::dec::persistence_diagram_bottleneck` + """ + ... + +def betti_curve(pairs: list[tuple[int, float, float]], eps_range: list[float]) -> list[tuple[float, list[int]]]: + """ +Betti curve: Betti numbers (dims 0..2) as a function of the filtration +parameter, from the persistence pairs. + +Rust: `manifold::dec::betti_curve` + """ + ... diff --git a/bindings/python/python/numeria/manifold/embedding.pyi b/bindings/python/python/numeria/manifold/embedding.pyi new file mode 100644 index 0000000..2f31546 --- /dev/null +++ b/bindings/python/python/numeria/manifold/embedding.pyi @@ -0,0 +1,335 @@ +""" +Manifold learning and dimensionality reduction: spectral embeddings (MDS, Isomap, LLE, Laplacian eigenmaps, diffusion maps), PCA and kernel PCA, stochastic neighbor embeddings, intrinsic-dimension estimators, embedding quality metrics, benchmark datasets, and optimization on matrix manifolds. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.cfd.sph import Kernel +from numeria.linalg.matrix import Matrix +from numeria.monte_carlo import Rng +from numeria.manifold.vecn import VecN + +def dist_matrix(points: list[VecN | Sequence[float]]) -> Matrix: + """ +Pairwise Euclidean distance matrix. + +Rust: `manifold::embedding::dist_matrix` + """ + ... + +def classical_mds(dist: Matrix | Sequence[Sequence[float]], dim: int) -> list[VecN]: + """ +Classical (Torgerson) multidimensional scaling from a distance matrix. + +Rust: `manifold::embedding::classical_mds` + """ + ... + +def metric_mds_smacof(dist: Matrix | Sequence[Sequence[float]], dim: int, iters: int, rng: Rng) -> tuple[list[VecN], float]: + """ +Metric MDS by SMACOF stress majorization. Returns (embedding, stress). + +Rust: `manifold::embedding::metric_mds_smacof` + """ + ... + +def nonmetric_mds(dist: Matrix | Sequence[Sequence[float]], dim: int, iters: int, rng: Rng) -> list[VecN]: + """ +Nonmetric MDS: SMACOF against monotone-regressed disparities. + +Rust: `manifold::embedding::nonmetric_mds` + """ + ... + +def knn_graph(points: list[VecN | Sequence[float]], k: int) -> list[list[tuple[int, float]]]: + """ +k-nearest-neighbor graph: for each point, its k neighbors and distances. + +Rust: `manifold::embedding::knn_graph` + """ + ... + +def geodesic_distance_matrix(knn: list[list[tuple[int, float]]]) -> Matrix: + """ +All-pairs shortest paths over a kNN graph (Floyd-Warshall; symmetrized). + +Rust: `manifold::embedding::geodesic_distance_matrix` + """ + ... + +def isomap(points: list[VecN | Sequence[float]], k_neighbors: int, dim: int) -> list[VecN]: + """ +Isomap: geodesic distances over the kNN graph fed to classical MDS. + +Rust: `manifold::embedding::isomap` + """ + ... + +def lle(points: list[VecN | Sequence[float]], k: int, dim: int, reg: float) -> list[VecN]: + """ +Locally linear embedding. + +Rust: `manifold::embedding::lle` + """ + ... + +def laplacian_eigenmaps(points: list[VecN | Sequence[float]], k: int, dim: int, sigma: float) -> list[VecN]: + """ +Laplacian eigenmaps with heat-kernel weights. + +Rust: `manifold::embedding::laplacian_eigenmaps` + """ + ... + +def diffusion_maps(points: list[VecN | Sequence[float]], eps: float, dim: int, t: float) -> list[VecN]: + """ +Diffusion maps: eigenfunctions of the diffusion operator, scaled by +lambda^t. + +Rust: `manifold::embedding::diffusion_maps` + """ + ... + +def spectral_embedding(adjacency: Matrix | Sequence[Sequence[float]], dim: int) -> list[VecN]: + """ +Spectral embedding of a graph adjacency matrix. + +Rust: `manifold::embedding::spectral_embedding` + """ + ... + +def pca(points: list[VecN | Sequence[float]], dim: int) -> tuple[list[VecN], list[float], Matrix]: + """ +Principal component analysis: returns (projected points, explained +variance per component, components as rows). + +Rust: `manifold::embedding::pca` + """ + ... + +def kernel_pca(points: list[VecN | Sequence[float]], kernel: Callable[[VecN | Sequence[float], VecN | Sequence[float]], float], dim: int) -> list[VecN]: + """ +Kernel PCA with a user-supplied kernel. + +Rust: `manifold::embedding::kernel_pca` + """ + ... + +def tsne(points: list[VecN | Sequence[float]], dim: int, perplexity: float, iters: int, lr: float, rng: Rng) -> list[VecN]: + """ +t-SNE (exact gradients; suitable for small point sets). + +Rust: `manifold::embedding::tsne` + """ + ... + +def umap_lite(points: list[VecN | Sequence[float]], k: int, dim: int, min_dist: float, epochs: int, rng: Rng) -> list[VecN]: + """ +Lightweight UMAP: fuzzy kNN weights optimized by SGD attraction and +random-negative repulsion. + +Rust: `manifold::embedding::umap_lite` + """ + ... + +def intrinsic_dimension_mle(points: list[VecN | Sequence[float]], k: int) -> float: + """ +Levina-Bickel maximum-likelihood intrinsic dimension using k neighbors. + +Rust: `manifold::embedding::intrinsic_dimension_mle` + """ + ... + +def intrinsic_dimension_correlation(points: list[VecN | Sequence[float]], r_range: tuple[float, float]) -> float: + """ +Correlation-dimension estimate: log-log slope of the correlation +integral over the radius range. + +Rust: `manifold::embedding::intrinsic_dimension_correlation` + """ + ... + +def intrinsic_dimension_two_nn(points: list[VecN | Sequence[float]]) -> float: + """ +TwoNN intrinsic dimension (Facco et al.): d = n / sum ln(r2/r1). + +Rust: `manifold::embedding::intrinsic_dimension_two_nn` + """ + ... + +def trustworthiness(high: list[VecN | Sequence[float]], low: list[VecN | Sequence[float]], k: int) -> float: + """ +Trustworthiness of a low-dimensional embedding (1 = perfect). + +Rust: `manifold::embedding::trustworthiness` + """ + ... + +def continuity(high: list[VecN | Sequence[float]], low: list[VecN | Sequence[float]], k: int) -> float: + """ +Continuity of an embedding (trustworthiness with roles swapped). + +Rust: `manifold::embedding::continuity` + """ + ... + +def stress(dist_high: Matrix | Sequence[Sequence[float]], dist_low: Matrix | Sequence[Sequence[float]]) -> float: + """ +Kruskal stress between two distance matrices. + +Rust: `manifold::embedding::stress` + """ + ... + +def neighborhood_preservation(high: list[VecN | Sequence[float]], low: list[VecN | Sequence[float]], k: int) -> float: + """ +Fraction of k-nearest neighbors preserved by the embedding. + +Rust: `manifold::embedding::neighborhood_preservation` + """ + ... + +def procrustes_align(a: list[VecN | Sequence[float]], b: list[VecN | Sequence[float]]) -> tuple[list[VecN], float]: + """ +Procrustes alignment of b onto a (rotation + scale + translation); +returns (aligned b, residual). + +Rust: `manifold::embedding::procrustes_align` + """ + ... + +def swiss_roll(n: int, noise: float, rng: Rng) -> tuple[list[VecN], list[float]]: + """ +Swiss roll in R3 with the unrolled arc-length parameter as ground truth. + +Rust: `manifold::embedding::swiss_roll` + """ + ... + +def s_curve(n: int, noise: float, rng: Rng) -> tuple[list[VecN], list[float]]: + """ +S-curve dataset with the curve parameter as ground truth. + +Rust: `manifold::embedding::s_curve` + """ + ... + +def torus_sample(n: int, big_r: float, small_r: float, rng: Rng) -> tuple[list[VecN], list[tuple[float, float]]]: + """ +Points on a torus with (u, v) angles as ground truth. + +Rust: `manifold::embedding::torus_sample` + """ + ... + +def sphere_sample(n: int, rng: Rng) -> list[VecN]: + """ +Uniform points on the unit 2-sphere in R3. + +Rust: `manifold::embedding::sphere_sample` + """ + ... + +def helix_sample(n: int, rng: Rng) -> tuple[list[VecN], list[float]]: + """ +Helix in R3 with the parameter as ground truth. + +Rust: `manifold::embedding::helix_sample` + """ + ... + +def mobius_sample(n: int, rng: Rng) -> list[VecN]: + """ +Points on a Mobius band. + +Rust: `manifold::embedding::mobius_sample` + """ + ... + +def klein_sample(n: int, rng: Rng) -> list[VecN]: + """ +Points on the figure-8 immersion of the Klein bottle in R3. + +Rust: `manifold::embedding::klein_sample` + """ + ... + +def two_moons(n: int, noise: float, rng: Rng) -> tuple[list[VecN], list[int]]: + """ +The two-moons dataset with labels. + +Rust: `manifold::embedding::two_moons` + """ + ... + +def blobs(n: int, centers: list[VecN | Sequence[float]], spread: float, rng: Rng) -> tuple[list[VecN], list[int]]: + """ +Isotropic Gaussian blobs with labels. + +Rust: `manifold::embedding::blobs` + """ + ... + +def tangent_space_estimate(points: list[VecN | Sequence[float]], idx: int, k: int, dim: int) -> Matrix: + """ +Local tangent space at a point by PCA of its k neighbors: rows are an +orthonormal basis. + +Rust: `manifold::embedding::tangent_space_estimate` + """ + ... + +def manifold_curvature_estimate(points: list[VecN | Sequence[float]], idx: int, k: int) -> float: + """ +Curvature proxy at a point: residual variance fraction outside the local +tangent plane. + +Rust: `manifold::embedding::manifold_curvature_estimate` + """ + ... + +def grassmann_distance(a: Matrix | Sequence[Sequence[float]], b: Matrix | Sequence[Sequence[float]]) -> float: + """ +Grassmann distance between subspaces spanned by the rows of a and b +(square root of the sum of squared principal angles). + +Rust: `manifold::embedding::grassmann_distance` + """ + ... + +def stiefel_project(m: Matrix | Sequence[Sequence[float]]) -> Matrix: + """ +Project a matrix onto the Stiefel manifold (nearest orthonormal-column +matrix, via the polar factor). + +Rust: `manifold::embedding::stiefel_project` + """ + ... + +def riemannian_gradient_descent_sphere(f: Callable[[VecN | Sequence[float]], float], grad: Callable[[VecN | Sequence[float]], VecN | Sequence[float]], x0: VecN | Sequence[float], iters: int, lr: float) -> VecN: + """ +Riemannian gradient descent on the unit sphere. + +Rust: `manifold::embedding::riemannian_gradient_descent_sphere` + """ + ... + +def geodesic_kmeans(metric: Metric, points: list[VecN | Sequence[float]], k: int, iters: int, rng: Rng) -> tuple[list[VecN], list[int]]: + """ +k-means with distances and means taken in a Riemannian metric (uses the +metric's exp/log maps). Returns (centroids, labels). + +Rust: `manifold::embedding::geodesic_kmeans` + """ + ... + +def manifold_interpolation_rbf(points: list[VecN | Sequence[float]], values: list[float], query: VecN | Sequence[float], kernel: Callable[[float], float]) -> float: + """ +Radial-basis interpolation of scattered manifold data. + +Rust: `manifold::embedding::manifold_interpolation_rbf` + """ + ... diff --git a/bindings/python/python/numeria/manifold/geodesic.pyi b/bindings/python/python/numeria/manifold/geodesic.pyi new file mode 100644 index 0000000..5786dad --- /dev/null +++ b/bindings/python/python/numeria/manifold/geodesic.pyi @@ -0,0 +1,109 @@ +""" +Geodesics, parallel transport, Jacobi fields, and relativistic orbits, all driven by the finite-difference `Metric` machinery. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.statistics.distributions import Poisson +from numeria.math import Vec3 +from numeria.manifold.vecn import VecN + +class GeodesicState: + """ +A point on a geodesic: position, velocity, affine parameter. + +Rust: `manifold::geodesic::GeodesicState` + """ + def __init__(self, x: VecN | Sequence[float], v: VecN | Sequence[float], tau: float) -> None: ... + @property + def x(self) -> VecN: ... + @property + def v(self) -> VecN: ... + @property + def tau(self) -> float: ... + +class Integrator: + """ +Time integrator selection for geodesic integration. + +Rust: `manifold::geodesic::Integrator` + """ + ... + +def great_circle_check(r: float) -> bool: + """ +Verify that sphere geodesics are great circles: shoot a unit-speed +geodesic along the equator and check it closes after 2 pi r. + +Rust: `manifold::geodesic::great_circle_check` + """ + ... + +def schwarzschild_orbit(m: float, r0: float, l: float, e: float, phi_end: float, dt: float) -> list[tuple[float, float]]: + """ +Equatorial Schwarzschild orbit r(phi) starting at r0 with dr/dphi = 0, +angular momentum `l` per unit mass (the energy parameter `_e` is +determined by the turning-point condition and kept for signature +compatibility). Integrates u'' + u = M/L^2 + 3 M u^2 with RK4. Returns +(phi, r) samples. + +Rust: `manifold::geodesic::schwarzschild_orbit` + """ + ... + +def perihelion_precession(m: float, a: float, e: float) -> float: + """ +Leading-order perihelion precession per orbit: 6 pi M / (a (1 - e^2)). + +Rust: `manifold::geodesic::perihelion_precession` + """ + ... + +def light_deflection(m: float, b: float) -> float: + """ +Leading-order light deflection by a mass: 4 M / b. + +Rust: `manifold::geodesic::light_deflection` + """ + ... + +def shapiro_delay(m: float, r1: float, r2: float, b: float) -> float: + """ +Shapiro time delay for a signal grazing at impact parameter `b` between +radii r1 and r2: 2M ln(4 r1 r2 / b^2). + +Rust: `manifold::geodesic::shapiro_delay` + """ + ... + +def photon_orbit_stability(m: float) -> float: + """ +Lyapunov instability exponent of the circular photon orbit at r = 3M: +lambda = 1 / (3 sqrt(3) M) per unit affine time. + +Rust: `manifold::geodesic::photon_orbit_stability` + """ + ... + +def geodesics_on_mesh_exact(mesh: Mesh, a: int, b: int) -> list[Vec3]: + """ +Shortest path between two mesh vertices along mesh edges (Dijkstra — +an upper bound on the exact geodesic). Returns the vertex positions. + +Rust: `manifold::geodesic::geodesics_on_mesh_exact` + """ + ... + +def heat_method_geodesic(mesh: Mesh, source: int, t: float) -> list[float]: + """ +Heat-method geodesic distance from a source vertex (Crane et al.): +diffuse heat for time `t`, normalize the gradient per face, solve a +Poisson equation for the distance. Dense solves; suitable for small +meshes. + +Rust: `manifold::geodesic::heat_method_geodesic` + """ + ... diff --git a/bindings/python/python/numeria/manifold/hyperbolic.pyi b/bindings/python/python/numeria/manifold/hyperbolic.pyi new file mode 100644 index 0000000..349a405 --- /dev/null +++ b/bindings/python/python/numeria/manifold/hyperbolic.pyi @@ -0,0 +1,413 @@ +""" +Hyperbolic geometry across the standard models: Poincare disk/ball, upper half-plane/space, Klein disk, and the hyperboloid, with isometries, trigonometry, tilings, and low-distortion embeddings. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.linalg.matrix import Matrix +from numeria.monte_carlo import Rng +from numeria.manifold.lie import Sl2C +from numeria.manifold.lie import Sl2R +from numeria.math import Vec3 +from numeria.manifold.vecn import VecN + +class HypModel: + """ +The classical models of hyperbolic space. + +Rust: `manifold::hyperbolic::HypModel` + """ + ... + +class HypPoint: + """ +A point of hyperbolic space tagged with the model its coordinates use. + +Rust: `manifold::hyperbolic::HypPoint` + """ + def __init__(self, coords: VecN | Sequence[float], model: HypModel) -> None: ... + @staticmethod + def origin(model: HypModel, dim: int) -> HypPoint: ... + @staticmethod + def from_polar(r: float, theta: float) -> HypPoint: ... + def to(self, model: HypModel) -> HypPoint: ... + def distance(self, other: HypPoint) -> float: ... + def geodesic_to(self, other: HypPoint, n: int) -> list[HypPoint]: ... + def midpoint(self, other: HypPoint) -> HypPoint: ... + def reflect_across(self, geodesic: tuple[HypPoint, HypPoint]) -> HypPoint: ... + def angle_at(self, a: HypPoint, b: HypPoint) -> float: ... + def to_euclidean_display(self) -> VecN: ... + @property + def coords(self) -> VecN: ... + @property + def model(self) -> HypModel: ... + +def hyp_distance_disk(z: complex, w: complex) -> float: + """ +Poincare disk distance. + +Rust: `manifold::hyperbolic::hyp_distance_disk` + """ + ... + +def hyp_distance_uhp(z: complex, w: complex) -> float: + """ +Upper half-plane distance. + +Rust: `manifold::hyperbolic::hyp_distance_uhp` + """ + ... + +def hyp_distance_hyperboloid(x: VecN | Sequence[float], y: VecN | Sequence[float]) -> float: + """ +Hyperboloid-model distance acosh(-) with the Minkowski form + = -x0 y0 + sum xi yi. + +Rust: `manifold::hyperbolic::hyp_distance_hyperboloid` + """ + ... + +def hyp_geodesic_circle_disk(z: complex, w: complex) -> Optional[tuple[complex, float]]: + """ +Center and radius of the circular arc through z and w orthogonal to the +unit circle; None when the geodesic is a diameter. + +Rust: `manifold::hyperbolic::hyp_geodesic_circle_disk` + """ + ... + +def hyp_geodesic_disk(z: complex, w: complex, n: int) -> list[complex]: + """ +Sample the disk geodesic between z and w at n+1 points. + +Rust: `manifold::hyperbolic::hyp_geodesic_disk` + """ + ... + +def hyp_circle_disk(center: complex, radius: float, n: int) -> list[complex]: + """ +Hyperbolic circle in the disk: a Euclidean circle with offset center. +Returns n boundary samples. + +Rust: `manifold::hyperbolic::hyp_circle_disk` + """ + ... + +def hyp_area_triangle(alpha: float, beta: float, gamma: float) -> float: + """ +Area of a hyperbolic triangle from its angles: pi - (alpha + beta + gamma). + +Rust: `manifold::hyperbolic::hyp_area_triangle` + """ + ... + +def hyp_triangle_from_angles(alpha: float, beta: float, gamma: float) -> list[complex]: + """ +A triangle with prescribed angles (alpha at the origin, beta and gamma at +the other vertices), realized in the Poincare disk. + +Rust: `manifold::hyperbolic::hyp_triangle_from_angles` + """ + ... + +def hyp_law_of_cosines(a: float, b: float, gamma: float) -> float: + """ +Hyperbolic law of cosines: cosh c = cosh a cosh b - sinh a sinh b cos gamma. + +Rust: `manifold::hyperbolic::hyp_law_of_cosines` + """ + ... + +def hyp_law_of_sines(a: float, b: float, beta: float) -> float: + """ +Hyperbolic law of sines: returns sin(alpha) for side a opposite alpha, +given (a, b, beta) via sin(alpha)/sinh(a) = sin(beta)/sinh(b). + +Rust: `manifold::hyperbolic::hyp_law_of_sines` + """ + ... + +def hyp_angle_of_parallelism(d: float) -> float: + """ +Angle of parallelism Pi(d) = 2 atan(e^{-d}). + +Rust: `manifold::hyperbolic::hyp_angle_of_parallelism` + """ + ... + +def hyp_circumference(r: float) -> float: + """ +Circumference of a hyperbolic circle: 2 pi sinh r. + +Rust: `manifold::hyperbolic::hyp_circumference` + """ + ... + +def hyp_area_circle(r: float) -> float: + """ +Area of a hyperbolic disk: 2 pi (cosh r - 1). + +Rust: `manifold::hyperbolic::hyp_area_circle` + """ + ... + +def hyp_volume_ball(r: float, dim: int) -> float: + """ +Volume of a hyperbolic ball in `dim` dimensions: +vol(S^{n-1}) * integral of sinh^{n-1}. + +Rust: `manifold::hyperbolic::hyp_volume_ball` + """ + ... + +def mobius_disk(z: complex, a: complex, theta: float) -> complex: + """ +Mobius isometry of the disk: z -> e^{i theta} (z - a)/(1 - conj(a) z). + +Rust: `manifold::hyperbolic::mobius_disk` + """ + ... + +def mobius_uhp(z: complex, m: Sl2R) -> complex: + """ +Mobius action of an SL(2, R) element on the upper half-plane. + +Rust: `manifold::hyperbolic::mobius_uhp` + """ + ... + +def isometry_disk_from_two_points(z1: complex, z2: complex, w1: complex, w2: complex) -> Optional[tuple[complex, float]]: + """ +The disk isometry (a, theta) sending z1 -> w1 and z2 -> w2 when the +distances agree (None otherwise). + +Rust: `manifold::hyperbolic::isometry_disk_from_two_points` + """ + ... + +def hyperbolic_translation(dist: float, direction: float) -> Sl2R: + """ +SL(2, R) hyperbolic translation by `dist` along the geodesic in the +direction `direction` (an angle in the UHP tangent at i). + +Rust: `manifold::hyperbolic::hyperbolic_translation` + """ + ... + +def hyperbolic_rotation(theta: float) -> Sl2R: + """ +Elliptic rotation about i in the upper half-plane by angle theta. + +Rust: `manifold::hyperbolic::hyperbolic_rotation` + """ + ... + +def parabolic(t: float) -> Sl2R: + """ +Parabolic translation z -> z + t. + +Rust: `manifold::hyperbolic::parabolic` + """ + ... + +def disk_to_uhp(z: complex) -> complex: + """ +Cayley transform disk -> upper half-plane. + +Rust: `manifold::hyperbolic::disk_to_uhp` + """ + ... + +def uhp_to_disk(w: complex) -> complex: + """ +Inverse Cayley transform. + +Rust: `manifold::hyperbolic::uhp_to_disk` + """ + ... + +def disk_to_klein(z: complex) -> complex: + """ +Poincare disk -> Klein disk. + +Rust: `manifold::hyperbolic::disk_to_klein` + """ + ... + +def klein_to_disk(k: complex) -> complex: + """ +Klein disk -> Poincare disk. + +Rust: `manifold::hyperbolic::klein_to_disk` + """ + ... + +def disk_to_hyperboloid(z: complex) -> VecN: + """ +Poincare disk -> hyperboloid (x0, x1, x2). + +Rust: `manifold::hyperbolic::disk_to_hyperboloid` + """ + ... + +def hyperboloid_to_disk(x: VecN | Sequence[float]) -> complex: + """ +Hyperboloid -> Poincare disk. + +Rust: `manifold::hyperbolic::hyperboloid_to_disk` + """ + ... + +def ball_to_half_space(p: Vec3 | Sequence[float]) -> Vec3: + """ +Poincare ball -> upper half-space (3D). + +Rust: `manifold::hyperbolic::ball_to_half_space` + """ + ... + +def lorentz_boost_hyperboloid(v: VecN | Sequence[float]) -> Matrix: + """ +Lorentz boost that carries the hyperboloid basepoint (1, 0, ..) to the +given hyperboloid point (an isometry of the model). + +Rust: `manifold::hyperbolic::lorentz_boost_hyperboloid` + """ + ... + +def hyp_tiling_exists(p: int, q: int) -> bool: + """ +True when a regular {p, q} tiling is hyperbolic: 1/p + 1/q < 1/2. + +Rust: `manifold::hyperbolic::hyp_tiling_exists` + """ + ... + +def hyp_tiling(p: int, q: int, depth: int, model: HypModel) -> list[list[complex]]: + """ +Regular {p, q} tiling of the disk generated by reflections, to the given +recursion depth. Returns the polygons as vertex lists. + +Rust: `manifold::hyperbolic::hyp_tiling` + """ + ... + +def fundamental_polygon_genus(g: int) -> list[complex]: + """ +Vertices of the regular 4g-gon fundamental polygon for a genus-g surface +(all angles sum to 2 pi). + +Rust: `manifold::hyperbolic::fundamental_polygon_genus` + """ + ... + +def hyp_voronoi_disk(sites: list[complex], n_res: int) -> list[list[complex]]: + """ +Approximate hyperbolic Voronoi cells: sample directions around each site +and march to the bisector. Returns one polygon per site. + +Rust: `manifold::hyperbolic::hyp_voronoi_disk` + """ + ... + +def hyp_delaunay_disk(sites: list[complex]) -> list[list[int]]: + """ +Hyperbolic Delaunay triangulation by the empty-circumdisk test in the +Klein model (hyperbolic Delaunay = Euclidean Delaunay of Klein points). + +Rust: `manifold::hyperbolic::hyp_delaunay_disk` + """ + ... + +def hyp_convex_hull_disk(points: list[complex]) -> list[complex]: + """ +Hyperbolic convex hull via the Klein model (geodesics are straight +there). Returns hull vertices in order. + +Rust: `manifold::hyperbolic::hyp_convex_hull_disk` + """ + ... + +def hyp_centroid_disk(points: list[complex], iters: int) -> complex: + """ +Hyperbolic centroid (Karcher mean) of disk points. + +Rust: `manifold::hyperbolic::hyp_centroid_disk` + """ + ... + +def hyp_embed_tree(adjacency: list[list[int]], root: int) -> list[complex]: + """ +Sarkar's low-distortion embedding of a tree into the Poincare disk. + +Rust: `manifold::hyperbolic::hyp_embed_tree` + """ + ... + +def hyp_embed_graph_mds(dist: Matrix | Sequence[Sequence[float]], dim: int, iters: int) -> list[VecN]: + """ +Stress-majorization MDS into hyperbolic space (Poincare ball of the +given dimension) matching the target distance matrix. + +Rust: `manifold::hyperbolic::hyp_embed_graph_mds` + """ + ... + +def poincare_embedding_train(graph_edges: list[tuple[int, int]], dim: int, epochs: int, lr: float, rng: Rng) -> list[VecN]: + """ +Nickel-Kiela Poincare embedding of a graph by Riemannian SGD on +edge-distance loss (connected pairs pulled together, random negatives +pushed apart). + +Rust: `manifold::hyperbolic::poincare_embedding_train` + """ + ... + +def hyp_mean_curvature_flow(curve: list[complex], dt: float, steps: int) -> list[complex]: + """ +Curve-shortening flow of a closed disk polygon under the hyperbolic +metric (explicit steps toward the hyperbolic midpoint of neighbors). + +Rust: `manifold::hyperbolic::hyp_mean_curvature_flow` + """ + ... + +def horocycle_disk(ideal_point: complex, through: complex, n: int) -> list[complex]: + """ +Horocycle at the ideal point through a given interior point: a Euclidean +circle tangent to the boundary at the ideal point. + +Rust: `manifold::hyperbolic::horocycle_disk` + """ + ... + +def equidistant_curve_disk(geodesic: tuple[complex, complex], d: float, n: int) -> list[complex]: + """ +Equidistant curve at hyperbolic distance `d` from the geodesic through +two boundary-anchored points (sampled along one side). + +Rust: `manifold::hyperbolic::equidistant_curve_disk` + """ + ... + +def limit_set_schottky(generators: list[Sl2C], depth: int) -> list[complex]: + """ +Limit set of a Schottky-like group: orbit of a basepoint under words in +the generators up to the given length, keeping the deepest images. + +Rust: `manifold::hyperbolic::limit_set_schottky` + """ + ... + +def apollonian_from_mobius(depth: int) -> list[tuple[complex, float]]: + """ +Circles of an Apollonian gasket generated from the classic +(-1, 2, 2, 3) Descartes configuration by Vieta reflection; returns +(center, radius) pairs (the bounding circle first). + +Rust: `manifold::hyperbolic::apollonian_from_mobius` + """ + ... diff --git a/bindings/python/python/numeria/manifold/lie.pyi b/bindings/python/python/numeria/manifold/lie.pyi new file mode 100644 index 0000000..3cf1c78 --- /dev/null +++ b/bindings/python/python/numeria/manifold/lie.pyi @@ -0,0 +1,439 @@ +""" +Lie groups and algebras: rotation and rigid-motion groups in 2/3/4 dimensions, SU(2) and SL(2) groups, matrix exponentials and logarithms, representation-theory helpers (Wigner d, Clebsch-Gordan), and estimation algorithms on these manifolds (pose graphs, hand-eye, Umeyama). +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.linalg import Mat3 +from numeria.linalg.matrix import Matrix +from numeria.quaternion import Quaternion +from numeria.monte_carlo import Rng +from numeria.manifold.vecn import TensorN +from numeria.math import Vec2 +from numeria.math import Vec3 + +class Heisenberg3: + """ +The 3D Heisenberg group with coordinates (x, y, z) and product +(x, y, z)(x', y', z') = (x + x', y + y', z + z' + x y'). + +Rust: `manifold::lie::Heisenberg3` + """ + def __init__(self, x: float, y: float, z: float) -> None: ... + @staticmethod + def identity() -> Heisenberg3: ... + def compose(self, o: Heisenberg3 | Sequence[float]) -> Heisenberg3: ... + def inverse(self) -> Heisenberg3: ... + def commutator(self, o: Heisenberg3 | Sequence[float]) -> Heisenberg3: ... + @property + def x(self) -> float: ... + @property + def y(self) -> float: ... + @property + def z(self) -> float: ... + +class Se2: + """ +Planar rigid transform. + +Rust: `manifold::lie::Se2` + """ + def __init__(self, theta: float, t: Vec2 | Sequence[float]) -> None: ... + @staticmethod + def identity() -> Se2: ... + @staticmethod + def exp(v: list[float]) -> Se2: ... + def log(self) -> list[float]: ... + def compose(self, other: Se2) -> Se2: ... + def inverse(self) -> Se2: ... + def adjoint(self) -> list[list[float]]: ... + def apply(self, p: Vec2 | Sequence[float]) -> Vec2: ... + def interpolate(self, other: Se2, t: float) -> Se2: ... + def to_affine2(self) -> list[list[float]]: ... + @property + def theta(self) -> float: ... + @property + def t(self) -> Vec2: ... + +class Se3: + """ +Rigid transform: rotation then translation. + +Rust: `manifold::lie::Se3` + """ + def __init__(self, r: So3, t: Vec3 | Sequence[float]) -> None: ... + @staticmethod + def identity() -> Se3: ... + @staticmethod + def exp(xi: se3) -> Se3: ... + def log(self) -> se3: ... + def compose(self, other: Se3) -> Se3: ... + def inverse(self) -> Se3: ... + def adjoint(self) -> list[list[float]]: ... + def apply_point(self, p: Vec3 | Sequence[float]) -> Vec3: ... + def apply_vector(self, v: Vec3 | Sequence[float]) -> Vec3: ... + def to_mat4(self) -> Mat4: ... + @staticmethod + def from_mat4(m: Mat4) -> Se3: ... + @staticmethod + def from_frame(r: Mat3, origin: Vec3 | Sequence[float]) -> Se3: ... + def to_frame(self) -> tuple[Mat3, Vec3]: ... + def interpolate(self, other: Se3, t: float) -> Se3: ... + def screw_axis(self) -> tuple[Vec3, Vec3, float, float]: ... + @staticmethod + def twist_to_velocity(xi: se3, p: Vec3 | Sequence[float]) -> Vec3: ... + @staticmethod + def jacobian_left(xi: se3) -> list[list[float]]: ... + @staticmethod + def jacobian_right(xi: se3) -> list[list[float]]: ... + def distance(self, other: Se3, weight: float) -> float: ... + @staticmethod + def random(rng: Rng) -> Se3: ... + @staticmethod + def mean(poses: list[Se3], iters: int) -> Se3: ... + @staticmethod + def relative(a: Se3, b: Se3) -> Se3: ... + @property + def r(self) -> So3: ... + @property + def t(self) -> Vec3: ... + +class Sim3: + """ +Similarity transform: scale, rotation, translation. + +Rust: `manifold::lie::Sim3` + """ + def __init__(self, s: float, r: So3, t: Vec3 | Sequence[float]) -> None: ... + @staticmethod + def identity() -> Sim3: ... + def apply(self, p: Vec3 | Sequence[float]) -> Vec3: ... + def compose(self, other: Sim3) -> Sim3: ... + def inverse(self) -> Sim3: ... + @property + def s(self) -> float: ... + @property + def r(self) -> So3: ... + @property + def t(self) -> Vec3: ... + +class Sl2C: + """ +SL(2, C) matrix. + +Rust: `manifold::lie::Sl2C` + """ + @staticmethod + def identity() -> Sl2C: ... + def mobius(self, z: complex) -> complex: ... + def compose(self, o: Sl2C) -> Sl2C: ... + def inverse(self) -> Sl2C: ... + def to_lorentz(self) -> Mat4: ... + @staticmethod + def from_lorentz_boost(n: Vec3 | Sequence[float], phi: float) -> Sl2C: ... + def classify(self) -> Sl2Class: ... + @property + def m(self) -> list[list[complex]]: ... + +class Sl2Class: + """ +Classification of Mobius/SL(2) elements. + +Rust: `manifold::lie::Sl2Class` + """ + ... + +class Sl2R: + """ +SL(2, R) matrix. + +Rust: `manifold::lie::Sl2R` + """ + @staticmethod + def identity() -> Sl2R: ... + def log(self) -> list[list[float]]: ... + def compose(self, o: Sl2R) -> Sl2R: ... + def inverse(self) -> Sl2R: ... + def act_on_upper_half_plane(self, z: complex) -> complex: ... + def classify(self) -> Sl2Class: ... + def fixed_points(self) -> list[complex]: ... + def translation_length(self) -> float: ... + @property + def m(self) -> list[list[float]]: ... + +class So2: + """ +Planar rotation by an angle. + +Rust: `manifold::lie::So2` + """ + def compose(self, other: So2) -> So2: ... + def inverse(self) -> So2: ... + def apply(self, v: Vec2 | Sequence[float]) -> Vec2: ... + +class So3: + """ +A 3D rotation stored as a matrix. + +Rust: `manifold::lie::So3` + """ + @staticmethod + def identity() -> So3: ... + @staticmethod + def hat(w: Vec3 | Sequence[float]) -> Mat3: ... + @staticmethod + def vee(m: Mat3) -> Vec3: ... + @staticmethod + def exp(w: Vec3 | Sequence[float]) -> So3: ... + def log(self) -> Vec3: ... + @staticmethod + def from_axis_angle(axis: Vec3 | Sequence[float], angle: float) -> So3: ... + @staticmethod + def from_quat(q: Quaternion | Sequence[float]) -> So3: ... + def to_quat(self) -> Quaternion: ... + def compose(self, other: So3) -> So3: ... + def inverse(self) -> So3: ... + def adjoint(self) -> Mat3: ... + def apply(self, v: Vec3 | Sequence[float]) -> Vec3: ... + def angle(self) -> float: ... + def distance(self, other: So3) -> float: ... + def interpolate(self, other: So3, t: float) -> So3: ... + @staticmethod + def random(rng: Rng) -> So3: ... + @staticmethod + def project(m: Mat3) -> So3: ... + @staticmethod + def left_jacobian(w: Vec3 | Sequence[float]) -> Mat3: ... + @staticmethod + def right_jacobian(w: Vec3 | Sequence[float]) -> Mat3: ... + @staticmethod + def left_jacobian_inv(w: Vec3 | Sequence[float]) -> Mat3: ... + @staticmethod + def right_jacobian_inv(w: Vec3 | Sequence[float]) -> Mat3: ... + @staticmethod + def bch(a: Vec3 | Sequence[float], b: Vec3 | Sequence[float], order: int) -> Vec3: ... + @staticmethod + def geodesic_mean(rots: list[So3], iters: int) -> So3: ... + +class So4: + """ +A 4D rotation matrix. + +Rust: `manifold::lie::So4` + """ + @staticmethod + def identity() -> So4: ... + @staticmethod + def exp(bivector: list[float]) -> So4: ... + def log(self) -> list[float]: ... + @staticmethod + def from_double_quaternion(l: Quaternion | Sequence[float], r: Quaternion | Sequence[float]) -> So4: ... + def to_double_quaternion(self) -> tuple[Quaternion, Quaternion]: ... + @staticmethod + def isoclinic_left(q: Quaternion | Sequence[float]) -> So4: ... + @staticmethod + def isoclinic_right(q: Quaternion | Sequence[float]) -> So4: ... + def compose(self, other: So4) -> So4: ... + def inverse(self) -> So4: ... + def apply(self, p: list[float]) -> list[float]: ... + @staticmethod + def random(rng: Rng) -> So4: ... + @staticmethod + def simple_rotation(plane: tuple[int, int], angle: float) -> So4: ... + @staticmethod + def double_rotation(angle1: float, angle2: float) -> So4: ... + +class Su2: + """ +SU(2) element stored as a unit quaternion. + +Rust: `manifold::lie::Su2` + """ + @staticmethod + def exp(a: Vec3 | Sequence[float]) -> Su2: ... + def log(self) -> Vec3: ... + def to_so3(self) -> So3: ... + def pauli_decompose(self) -> list[complex]: ... + def to_matrix_2x2(self) -> list[list[complex]]: ... + def compose(self, other: Su2) -> Su2: ... + def inverse(self) -> Su2: ... + def trace(self) -> float: ... + def character(self, j: float) -> float: ... + +class Unitary: + """ +A unitary matrix U(n) with complex entries. + +Rust: `manifold::lie::Unitary` + """ + def __init__(self, m: list[list[complex]]) -> None: ... + @staticmethod + def from_hermitian_exp(h: list[list[complex]], t: float) -> Unitary: ... + def is_unitary(self, tol: float) -> bool: ... + def compose(self, o: Unitary) -> Unitary: ... + def dagger(self) -> Unitary: ... + @staticmethod + def random_haar(n: int, rng: Rng) -> Unitary: ... + @property + def m(self) -> list[list[complex]]: ... + +class se3: + """ +se(3) algebra element: linear part rho, angular part phi. + +Rust: `manifold::lie::se3` + """ + def __init__(self, rho: Vec3 | Sequence[float], phi: Vec3 | Sequence[float]) -> None: ... + @property + def rho(self) -> Vec3: ... + @property + def phi(self) -> Vec3: ... + +class so3: + """ +so(3) algebra element (axis-angle vector). + +Rust: `manifold::lie::so3` + """ + ... + +def lie_bracket_matrix(a: Matrix | Sequence[Sequence[float]], b: Matrix | Sequence[Sequence[float]]) -> Matrix: + """ +Commutator [A, B] = AB - BA. + +Rust: `manifold::lie::lie_bracket_matrix` + """ + ... + +def matrix_exp(m: Matrix | Sequence[Sequence[float]]) -> Matrix: + """ +Matrix exponential by scaling-and-squaring with a Taylor/Pade core. + +Rust: `manifold::lie::matrix_exp` + """ + ... + +def matrix_sqrt(m: Matrix | Sequence[Sequence[float]]) -> Matrix: + """ +Principal matrix square root by the Denman-Beavers iteration. + +Rust: `manifold::lie::matrix_sqrt` + """ + ... + +def matrix_log(m: Matrix | Sequence[Sequence[float]]) -> Matrix: + """ +Principal matrix logarithm by inverse scaling-and-squaring with a +Gregory series core. Requires eigenvalues off the negative real axis. + +Rust: `manifold::lie::matrix_log` + """ + ... + +def killing_form(basis: list[Matrix | Sequence[Sequence[float]]]) -> Matrix: + """ +Killing form B_ij = tr(ad_i ad_j) for a Lie algebra basis of matrices. + +Rust: `manifold::lie::killing_form` + """ + ... + +def structure_constants(basis: list[Matrix | Sequence[Sequence[float]]]) -> TensorN: + """ +Structure constants c^k_{ij} with [b_i, b_j] = c^k_{ij} b_k, obtained by +least squares in the vectorized basis. + +Rust: `manifold::lie::structure_constants` + """ + ... + +def casimir_so3(j: float) -> float: + """ +Casimir eigenvalue of the spin-j representation of so(3): j(j+1). + +Rust: `manifold::lie::casimir_so3` + """ + ... + +def wigner_d_small(j: float, m1: float, m2: float, beta: float) -> float: + """ +Wigner small-d matrix element d^j_{m1 m2}(beta) (Wigner's sum formula). + +Rust: `manifold::lie::wigner_d_small` + """ + ... + +def wigner_d(j: float, m1: float, m2: float, alpha: float, beta: float, gamma: float) -> complex: + """ +Full Wigner D-matrix element +D^j_{m1 m2}(alpha, beta, gamma) = e^{-i m1 alpha} d^j_{m1 m2}(beta) +e^{-i m2 gamma}. + +Rust: `manifold::lie::wigner_d` + """ + ... + +def clebsch_gordan(j1: float, m1: float, j2: float, m2: float, j: float, m: float) -> float: + """ +Clebsch-Gordan coefficient (Racah's formula). + +Rust: `manifold::lie::clebsch_gordan` + """ + ... + +def rotate_spherical_harmonics(coeffs: list[complex], l: int, r: So3) -> list[complex]: + """ +Rotate the degree-l band of complex spherical-harmonic coefficients +(ordered m = -l..l) by the rotation `r` using the Wigner D-matrix with +zyz Euler angles. + +Rust: `manifold::lie::rotate_spherical_harmonics` + """ + ... + +def so3_uniform_grid(n: int) -> list[So3]: + """ +Near-uniform deterministic grid on SO(3) built from a Fibonacci sphere +of axes and a golden-ratio sweep of angles. + +Rust: `manifold::lie::so3_uniform_grid` + """ + ... + +def so3_haar_measure_density(angle: float) -> float: + """ +Haar measure density over the rotation angle in [0, pi]: +rho(theta) = (1 - cos theta)/pi, normalized to integrate to 1. + +Rust: `manifold::lie::so3_haar_measure_density` + """ + ... + +def hand_eye_calibration(a: list[Se3], b: list[Se3]) -> Se3: + """ +Park-Martin hand-eye calibration: solve AX = XB from motion pairs. + +Rust: `manifold::lie::hand_eye_calibration` + """ + ... + +def umeyama_alignment(src: list[Vec3 | Sequence[float]], dst: list[Vec3 | Sequence[float]], with_scale: bool) -> Sim3: + """ +Umeyama similarity alignment: the Sim3 (or Se3 when `with_scale` is +false) minimizing sum |dst_i - (s R src_i + t)|^2. + +Rust: `manifold::lie::umeyama_alignment` + """ + ... + +def rotation_averaging(rots: list[So3], weights: list[float]) -> So3: + """ +Rotation averaging: chordal L2 mean (projected arithmetic mean of the +matrices) refined by a few IRLS iterations in the tangent space. + +Rust: `manifold::lie::rotation_averaging` + """ + ... diff --git a/bindings/python/python/numeria/manifold/metric.pyi b/bindings/python/python/numeria/manifold/metric.pyi new file mode 100644 index 0000000..3dc8add --- /dev/null +++ b/bindings/python/python/numeria/manifold/metric.pyi @@ -0,0 +1,94 @@ +""" +Metric geometry on n-dimensional manifolds: a metric is a function from coordinates to a matrix g_ij, and everything else — Christoffel symbols, Riemann/Ricci/Weyl curvature, covariant derivatives, geodesic machinery inputs — is derived from it by finite differences. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.linalg.matrix import Matrix +from numeria.manifold.vecn import TensorN +from numeria.manifold.vecn import VecN + +class Metric: + """ +A (pseudo-)Riemannian metric given by a coordinate chart function. + +Rust: `manifold::metric::Metric` + """ + @staticmethod + def euclidean(n: int) -> Metric: ... + @staticmethod + def minkowski(n: int, signature: Sig) -> Metric: ... + @staticmethod + def sphere(n: int, r: float) -> Metric: ... + @staticmethod + def hyperbolic_ball(n: int) -> Metric: ... + @staticmethod + def poincare_half_space(n: int) -> Metric: ... + @staticmethod + def torus_flat(n: int, radii: list[float]) -> Metric: ... + @staticmethod + def schwarzschild(m: float) -> Metric: ... + @staticmethod + def kerr(m: float, a: float) -> Metric: ... + @staticmethod + def de_sitter(l: float) -> Metric: ... + @staticmethod + def anti_de_sitter(l: float) -> Metric: ... + @staticmethod + def induced_from_embedding(dim: int, embed: Callable[[VecN | Sequence[float]], VecN | Sequence[float]]) -> Metric: ... + def at(self, p: VecN | Sequence[float]) -> Matrix: ... + def inverse_at(self, p: VecN | Sequence[float]) -> Matrix: ... + def det_at(self, p: VecN | Sequence[float]) -> float: ... + def signature(self, p: VecN | Sequence[float]) -> tuple[int, int]: ... + def dg(self, p: VecN | Sequence[float], k: int) -> Matrix: ... + def christoffel(self, p: VecN | Sequence[float]) -> TensorN: ... + def christoffel_first_kind(self, p: VecN | Sequence[float]) -> TensorN: ... + def riemann(self, p: VecN | Sequence[float]) -> TensorN: ... + def riemann_lowered(self, p: VecN | Sequence[float]) -> TensorN: ... + def ricci(self, p: VecN | Sequence[float]) -> Matrix: ... + def ricci_scalar(self, p: VecN | Sequence[float]) -> float: ... + def einstein_tensor(self, p: VecN | Sequence[float]) -> Matrix: ... + def weyl(self, p: VecN | Sequence[float]) -> TensorN: ... + def kretschmann(self, p: VecN | Sequence[float]) -> float: ... + def sectional_curvature(self, p: VecN | Sequence[float], u: VecN | Sequence[float], v: VecN | Sequence[float]) -> float: ... + def gaussian_curvature(self, p: VecN | Sequence[float]) -> Optional[float]: ... + def is_flat(self, p: VecN | Sequence[float], tol: float) -> bool: ... + def is_einstein(self, p: VecN | Sequence[float], tol: float) -> bool: ... + def covariant_derivative_vector(self, v: Callable[[VecN | Sequence[float]], VecN | Sequence[float]], p: VecN | Sequence[float], direction: VecN | Sequence[float]) -> VecN: ... + def divergence(self, v: Callable[[VecN | Sequence[float]], VecN | Sequence[float]], p: VecN | Sequence[float]) -> float: ... + def laplace_beltrami(self, f: Callable[[VecN | Sequence[float]], float], p: VecN | Sequence[float]) -> float: ... + def gradient(self, f: Callable[[VecN | Sequence[float]], float], p: VecN | Sequence[float]) -> VecN: ... + def volume_element(self, p: VecN | Sequence[float]) -> float: ... + def volume_integrate(self, f: Callable[[VecN | Sequence[float]], float], bounds: list[tuple[float, float]], n_per_dim: int) -> float: ... + def length_of_curve(self, c: Callable[[float], VecN | Sequence[float]], t0: float, t1: float, n: int) -> float: ... + def inner(self, p: VecN | Sequence[float], u: VecN | Sequence[float], v: VecN | Sequence[float]) -> float: ... + def norm(self, p: VecN | Sequence[float], v: VecN | Sequence[float]) -> float: ... + def angle(self, p: VecN | Sequence[float], u: VecN | Sequence[float], v: VecN | Sequence[float]) -> float: ... + def orthonormal_frame(self, p: VecN | Sequence[float]) -> Matrix: ... + def lie_derivative_metric(self, xi: Callable[[VecN | Sequence[float]], VecN | Sequence[float]], p: VecN | Sequence[float]) -> Matrix: ... + def killing_check(self, xi: Callable[[VecN | Sequence[float]], VecN | Sequence[float]], p: VecN | Sequence[float], tol: float) -> bool: ... + def conformal_factor_to(self, other: Metric, p: VecN | Sequence[float]) -> Optional[float]: ... + def bianchi_identity_residual(self, p: VecN | Sequence[float]) -> float: ... + @property + def dim(self) -> int: ... + @property + def h(self) -> float: ... + +class Sig: + """ +Signature convention for Minkowski-type metrics. + +Rust: `manifold::metric::Sig` + """ + ... + +def kerr_boyer_lindquist(m: float, a: float) -> Metric: + """ +Kerr metric in Boyer-Lindquist coordinates (t, r, theta, phi). + +Rust: `manifold::metric::kerr_boyer_lindquist` + """ + ... diff --git a/bindings/python/python/numeria/manifold/polytope4.pyi b/bindings/python/python/numeria/manifold/polytope4.pyi new file mode 100644 index 0000000..e7fa6bd --- /dev/null +++ b/bindings/python/python/numeria/manifold/polytope4.pyi @@ -0,0 +1,324 @@ +""" +Four-dimensional polytopes: the six regular 4-polytopes with their full combinatorics, prisms and products, projections and cross-sections, duals, Coxeter-plane pictures, exceptional root systems and lattices, and curse-of-dimensionality demonstrations. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.manifold.lie import So4 +from numeria.math import Vec2 +from numeria.math import Vec3 +from numeria.manifold.vecn import VecN + +class Polytope4: + """ +A 4-polytope: vertices with edge, face (2D), and cell (3D facet) +combinatorics. Faces and cells list vertex indices. + +Rust: `manifold::polytope4::Polytope4` + """ + def __init__(self, vertices: list[Vec4 | Sequence[float]], edges: list[tuple[int, int]], faces: list[list[int]], cells: list[list[int]]) -> None: ... + @staticmethod + def tesseract() -> Polytope4: ... + @staticmethod + def cell16() -> Polytope4: ... + @staticmethod + def cell24() -> Polytope4: ... + @staticmethod + def simplex5() -> Polytope4: ... + @staticmethod + def cell600() -> Polytope4: ... + @staticmethod + def cell120() -> Polytope4: ... + @staticmethod + def duoprism(p: int, q: int) -> Polytope4: ... + @staticmethod + def duocylinder(n: int) -> Polytope4: ... + @staticmethod + def grand_antiprism() -> Polytope4: ... + @staticmethod + def cubinder(n: int) -> Polytope4: ... + @staticmethod + def spherinder(n: int) -> Polytope4: ... + def rectified(self) -> Polytope4: ... + def truncated(self) -> Polytope4: ... + def dual(self) -> Polytope4: ... + def euler_characteristic(self) -> int: ... + def f_vector(self) -> list[int]: ... + def edge_length(self) -> float: ... + def rotate(self, r: So4) -> Polytope4: ... + def project_perspective(self, distance: float) -> Mesh: ... + def project_orthographic(self, drop_axis: int) -> list[Vec3]: ... + def project_stereographic(self) -> list[Vec3]: ... + def cross_section(self, w: float) -> Mesh: ... + def cross_section_oriented(self, normal: Vec4 | Sequence[float], offset: float) -> Mesh: ... + def unfold_net(self) -> list[Mesh]: ... + def schlegel_diagram(self, cell: int) -> list[Vec3]: ... + def vertex_figure(self, v: int) -> Mesh: ... + def symmetry_order(self) -> int: ... + def coxeter_group_generators(self) -> list[So4]: ... + @staticmethod + def from_wythoff(symbol: str) -> Optional[Polytope4]: ... + def dihedral_angle(self) -> float: ... + def circumradius(self) -> float: ... + def inradius(self) -> float: ... + def hypervolume(self) -> float: ... + def surface_volume(self) -> float: ... + @property + def vertices(self) -> list[Vec4]: ... + @property + def edges(self) -> list[tuple[int, int]]: ... + @property + def faces(self) -> list[list[int]]: ... + @property + def cells(self) -> list[list[int]]: ... + +class Vec4: + """ +A 4D vector. + +Rust: `manifold::polytope4::Vec4` + """ + def __init__(self, x: float, y: float, z: float, w: float) -> None: ... + def dot(self, o: Vec4 | Sequence[float]) -> float: ... + def norm(self) -> float: ... + def normalized(self) -> Vec4: ... + def add(self, o: Vec4 | Sequence[float]) -> Vec4: ... + def sub(self, o: Vec4 | Sequence[float]) -> Vec4: ... + def scale(self, k: float) -> Vec4: ... + @property + def x(self) -> float: ... + @property + def y(self) -> float: ... + @property + def z(self) -> float: ... + @property + def w(self) -> float: ... + +def rotation_4d_planes() -> list[tuple[int, int]]: + """ +The six coordinate rotation planes of R4. + +Rust: `manifold::polytope4::rotation_4d_planes` + """ + ... + +def rotate_4d(p: Vec4 | Sequence[float], plane: tuple[int, int], angle: float) -> Vec4: + """ +Rotate a point in a single coordinate plane. + +Rust: `manifold::polytope4::rotate_4d` + """ + ... + +def rotate_4d_double(p: Vec4 | Sequence[float], angle_xy: float, angle_zw: float) -> Vec4: + """ +Double rotation: xy-plane by `angle_xy`, zw-plane by `angle_zw`. + +Rust: `manifold::polytope4::rotate_4d_double` + """ + ... + +def clifford_torus(u: float, v: float, r: float) -> Vec4: + """ +Point on the Clifford torus in S3: parameter angles (u, v), aspect r. + +Rust: `manifold::polytope4::clifford_torus` + """ + ... + +def clifford_torus_mesh(nu: int, nv: int, r: float) -> list[Vec4]: + """ +Sampled Clifford torus grid. + +Rust: `manifold::polytope4::clifford_torus_mesh` + """ + ... + +def hypersphere_s3_points(n: int) -> list[Vec4]: + """ +Near-uniform points on S3 (from the super-Fibonacci quaternions). + +Rust: `manifold::polytope4::hypersphere_s3_points` + """ + ... + +def hypersphere_volume(r: float, n: int) -> float: + """ +Volume of the n-ball of radius r (alias into the spherical module). + +Rust: `manifold::polytope4::hypersphere_volume` + """ + ... + +def simplex_n(n: int) -> tuple[list[VecN], list[tuple[int, int]]]: + """ +Regular n-simplex: n+1 vertices in R^n, unit circumradius, with edges. + +Rust: `manifold::polytope4::simplex_n` + """ + ... + +def hypercube_n(n: int) -> tuple[list[VecN], list[tuple[int, int]]]: + """ +n-cube vertices (coordinates +-1/2) with edges. + +Rust: `manifold::polytope4::hypercube_n` + """ + ... + +def cross_polytope_n(n: int) -> tuple[list[VecN], list[tuple[int, int]]]: + """ +n-dimensional cross-polytope (unit vertices +-e_i) with edges. + +Rust: `manifold::polytope4::cross_polytope_n` + """ + ... + +def hypercube_graph_n(n: int) -> list[tuple[int, int]]: + """ +Edges of the n-cube graph (bitmask vertices, Hamming distance 1). + +Rust: `manifold::polytope4::hypercube_graph_n` + """ + ... + +def project_n_to_3(points: list[VecN | Sequence[float]], basis: list[VecN | Sequence[float]]) -> list[Vec3]: + """ +Project n-dimensional points into 3D with the given (orthonormal) basis. + +Rust: `manifold::polytope4::project_n_to_3` + """ + ... + +def project_n_to_2(points: list[VecN | Sequence[float]], basis: list[VecN | Sequence[float]]) -> list[Vec2]: + """ +Project n-dimensional points into 2D. + +Rust: `manifold::polytope4::project_n_to_2` + """ + ... + +def petrie_polygon_projection(p: Polytope4) -> list[Vec2]: + """ +Petrie polygon projection of a regular 4-polytope: its vertices +projected into the Coxeter plane of the matching symmetry group, using +root systems realized in the polytope's own coordinates. + +Rust: `manifold::polytope4::petrie_polygon_projection` + """ + ... + +def coxeter_plane_projection(vertices: list[VecN | Sequence[float]], group: str) -> list[Vec2]: + """ +Project points into the Coxeter plane of the given group ("B4", "D4", +"F4", "H4", "E8"), with root systems in standard coordinates. The plane +is the invariant plane of a Coxeter element, found as the 2D eigenspace +of (w + w^T)/2 with eigenvalue cos(2 pi/h). + +Rust: `manifold::polytope4::coxeter_plane_projection` + """ + ... + +def e8_roots() -> list[VecN]: + """ +The 240 roots of E8 (norm sqrt 2). + +Rust: `manifold::polytope4::e8_roots` + """ + ... + +def e8_lattice_nearest(p: VecN | Sequence[float]) -> VecN: + """ +Nearest E8 lattice point (D8 plus glue-vector decoding). + +Rust: `manifold::polytope4::e8_lattice_nearest` + """ + ... + +def leech_lattice_min_vectors_count() -> int: + """ +The Leech lattice minimal-vector count (kissing number in 24D). + +Rust: `manifold::polytope4::leech_lattice_min_vectors_count` + """ + ... + +def d4_lattice_points(r: float) -> list[Vec4]: + """ +D4 lattice points (integer coordinates, even sum) within radius r. + +Rust: `manifold::polytope4::d4_lattice_points` + """ + ... + +def f4_roots() -> list[Vec4]: + """ +The 48 roots of F4. + +Rust: `manifold::polytope4::f4_roots` + """ + ... + +def h4_roots() -> list[Vec4]: + """ +The 120 roots of H4 (the unit icosians). + +Rust: `manifold::polytope4::h4_roots` + """ + ... + +def kissing_number_known(n: int) -> Optional[int]: + """ +Known kissing numbers by dimension. + +Rust: `manifold::polytope4::kissing_number_known` + """ + ... + +def hypercube_slicing_volume(n: int, s: float) -> float: + """ +(n-1)-volume of the slice of the unit n-cube `[0,1]^n` by the hyperplane +sum(x) = s, times sqrt(n) (the Irwin-Hall density scaled to a volume). + +Rust: `manifold::polytope4::hypercube_slicing_volume` + """ + ... + +def hypersphere_cap_fraction(n: int, theta: float) -> float: + """ +Fraction of the (n-1)-sphere's surface within angle theta of a pole: +regularized incomplete beta I_{sin^2 theta}((n-1)/2, 1/2) / 2 for +theta <= pi/2. + +Rust: `manifold::polytope4::hypersphere_cap_fraction` + """ + ... + +def gaussian_concentration_radius(n: int) -> float: + """ +Gaussian mass concentrates at radius sqrt(n). + +Rust: `manifold::polytope4::gaussian_concentration_radius` + """ + ... + +def random_walk_n_return_prob(n: int, steps: int) -> float: + """ +Monte Carlo probability that a simple random walk on Z^n returns to the +origin within `steps` steps (deterministic internal seed). + +Rust: `manifold::polytope4::random_walk_n_return_prob` + """ + ... + +def volume_ball_vs_cube_ratio(n: int) -> float: + """ +Ratio of the volume of the inscribed ball to the unit cube in n +dimensions (goes to zero fast). + +Rust: `manifold::polytope4::volume_ball_vs_cube_ratio` + """ + ... diff --git a/bindings/python/python/numeria/manifold/spacetime.pyi b/bindings/python/python/numeria/manifold/spacetime.pyi new file mode 100644 index 0000000..7fbfc64 --- /dev/null +++ b/bindings/python/python/numeria/manifold/spacetime.pyi @@ -0,0 +1,376 @@ +""" +Special and general relativity: four-vectors and Lorentz transforms, Rindler and Kruskal coordinates, Schwarzschild and Kerr geodesics, gravitational lensing, black hole thermodynamics, cosmological distances, inspiral waveforms, and Kaluza-Klein reduction. Kinematics and geometry use geometric units (G = c = 1) with the mostly-minus signature (+, -, -, -) unless stated otherwise; the thermodynamic and cosmological helpers use SI units. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.manifold.geodesic import GeodesicState +from numeria.manifold.metric import Sig +from numeria.manifold.lie import Sl2C +from numeria.manifold.lie import So3 +from numeria.math import Vec3 +from numeria.manifold.vecn import VecN + +class Causal: + """ +Causal relation of event `b` relative to event `a`. + +Rust: `manifold::spacetime::Causal` + """ + ... + +class FourVector: + """ +A spacetime four-vector (t, x) in units with c = 1. + +Rust: `manifold::spacetime::FourVector` + """ + def __init__(self, t: float, x: Vec3 | Sequence[float]) -> None: ... + def minkowski_dot(self, o: FourVector) -> float: ... + def minkowski_dot_sig(self, o: FourVector, sig: Sig) -> float: ... + def norm_squared(self) -> float: ... + def is_timelike(self) -> bool: ... + def is_spacelike(self) -> bool: ... + def is_null(self, tol: float) -> bool: ... + def boost(self, v: Vec3 | Sequence[float]) -> FourVector: ... + def rotate(self, r: So3) -> FourVector: ... + @staticmethod + def from_velocity(v: Vec3 | Sequence[float]) -> FourVector: ... + @staticmethod + def from_momentum(m: float, v: Vec3 | Sequence[float]) -> FourVector: ... + def energy(self) -> float: ... + def spatial_momentum(self) -> Vec3: ... + def rapidity(self) -> float: ... + def scale(self, k: float) -> FourVector: ... + def proper_time_to(self, o: FourVector) -> float: ... + def __add__(self, o: FourVector) -> FourVector: ... + def __sub__(self, o: FourVector) -> FourVector: ... + @property + def t(self) -> float: ... + @property + def x(self) -> Vec3: ... + +class KerrConstants: + """ +Carter constants of motion for a timelike Kerr geodesic: energy `e`, +axial angular momentum `l`, and Carter constant `q` per unit mass. + +Rust: `manifold::spacetime::KerrConstants` + """ + def __init__(self, m: float, a: float, e: float, l: float, q: float) -> None: ... + def radial_potential(self, r: float) -> float: ... + def theta_potential(self, theta: float) -> float: ... + @property + def m(self) -> float: ... + @property + def a(self) -> float: ... + @property + def e(self) -> float: ... + @property + def l(self) -> float: ... + @property + def q(self) -> float: ... + +class LorentzTransform: + """ +A Lorentz transformation as a 4x4 matrix acting on (t, x, y, z). + +Rust: `manifold::spacetime::LorentzTransform` + """ + @staticmethod + def identity() -> LorentzTransform: ... + @staticmethod + def boost(v: Vec3 | Sequence[float]) -> LorentzTransform: ... + @staticmethod + def boost_x(beta: float) -> LorentzTransform: ... + @staticmethod + def rotation(r: So3) -> LorentzTransform: ... + def compose(self, o: LorentzTransform) -> LorentzTransform: ... + def inverse(self) -> LorentzTransform: ... + def apply(self, v: FourVector) -> FourVector: ... + def is_lorentz(self, tol: float) -> bool: ... + @staticmethod + def thomas_wigner_rotation(v1: Vec3 | Sequence[float], v2: Vec3 | Sequence[float]) -> So3: ... + @staticmethod + def velocity_addition(u: Vec3 | Sequence[float], v: Vec3 | Sequence[float]) -> Vec3: ... + @staticmethod + def from_sl2c(m: Sl2C) -> LorentzTransform: ... + def to_sl2c(self) -> Sl2C: ... + +class Plane: + """ +A hyperplane of events: all x with normal . x = offset (Minkowski dot). + +Rust: `manifold::spacetime::Plane` + """ + def __init__(self, normal: FourVector, offset: float) -> None: ... + @property + def normal(self) -> FourVector: ... + @property + def offset(self) -> float: ... + +def light_cone_check(a: FourVector, b: FourVector) -> Causal: + """ +Classify the separation b - a on the light cone of `a`. + +Rust: `manifold::spacetime::light_cone_check` + """ + ... + +def simultaneity_plane(observer_vel: Vec3 | Sequence[float], event: FourVector) -> Plane: + """ +The simultaneity hyperplane through `event` for an observer moving at +`observer_vel`: events x with u . (x - event) = 0 for the observer +four-velocity u. + +Rust: `manifold::spacetime::simultaneity_plane` + """ + ... + +def twin_paradox_ages(v: float, t_coordinate: float) -> tuple[float, float]: + """ +Ages (stay-at-home, traveler) after coordinate time `t_coordinate` with +the traveler cruising at speed `v`. + +Rust: `manifold::spacetime::twin_paradox_ages` + """ + ... + +def relativistic_rocket(accel_proper: float, tau: float) -> tuple[float, float, float]: + """ +Relativistic rocket with constant proper acceleration: returns +(coordinate time, distance, speed) after proper time `tau`. + +Rust: `manifold::spacetime::relativistic_rocket` + """ + ... + +def rindler_coords(t: float, x: float, a: float) -> tuple[float, float]: + """ +Rindler coordinates (eta, xi) of the Minkowski event (t, x) in the +right wedge x > |t|, normalized so the observer at proper acceleration +`a` sits at xi = 1/a: t = xi sinh(a eta), x = xi cosh(a eta). + +Rust: `manifold::spacetime::rindler_coords` + """ + ... + +def rindler_horizon(a: float) -> float: + """ +Distance from a uniformly accelerated observer to their Rindler +horizon: c^2 / a (geometric units: 1/a). + +Rust: `manifold::spacetime::rindler_horizon` + """ + ... + +def unruh_temperature(a: float) -> float: + """ +Unruh temperature of a uniformly accelerated observer (SI units): +T = hbar a / (2 pi c k_B). + +Rust: `manifold::spacetime::unruh_temperature` + """ + ... + +def kruskal_from_schwarzschild(t: float, r: float, m: float) -> tuple[float, float]: + """ +Kruskal-Szekeres coordinates (T, X) of the Schwarzschild event (t, r), +smooth across the horizon r = 2M (exterior region I and interior +region II). + +Rust: `manifold::spacetime::kruskal_from_schwarzschild` + """ + ... + +def penrose_diagram_coords(t: float, r: float, m: float) -> tuple[float, float]: + """ +Penrose diagram coordinates: Kruskal null coordinates compactified with +arctangent; returns (T, X) of the conformal diagram. + +Rust: `manifold::spacetime::penrose_diagram_coords` + """ + ... + +def eddington_finkelstein(t: float, r: float, m: float) -> float: + """ +Ingoing Eddington-Finkelstein null coordinate v = t + r* with the +tortoise coordinate r* = r + 2M ln|r/2M - 1|. + +Rust: `manifold::spacetime::eddington_finkelstein` + """ + ... + +def schwarzschild_geodesic_metric(m: float) -> Metric: + """ +The Schwarzschild metric wired into the finite-difference `Metric` +machinery (coordinates t, r, theta, phi; signature -+++). + +Rust: `manifold::spacetime::schwarzschild_geodesic_metric` + """ + ... + +def orbit_schwarzschild_full(m: float, e: float, l: float, r0: float, tau_end: float, dt: float) -> list[tuple[float, float, float, float]]: + """ +Full timelike Schwarzschild orbit in the equatorial plane from the +first integrals: energy `e` and angular momentum `l` per unit mass, +starting at r0 (infalling if r0 is not a turning point). Returns +(t, r, phi, tau) samples every proper-time step `dt`. + +Rust: `manifold::spacetime::orbit_schwarzschild_full` + """ + ... + +def photon_ray_trace_schwarzschild(m: float, b: float, phi_max: float) -> list[tuple[float, float]]: + """ +Photon trajectory around a Schwarzschild black hole with impact +parameter `b`, from the orbit equation u'' + u = 3 M u^2 starting at +infinity. Returns (phi, r) samples; stops at `phi_max`, escape, or +capture inside the photon sphere. + +Rust: `manifold::spacetime::photon_ray_trace_schwarzschild` + """ + ... + +def black_hole_shadow_radius(m: float, a: float, inclination: float) -> float: + """ +Apparent black hole shadow radius for a Kerr hole of spin `a` seen at +`inclination` (radians from the spin axis), averaged over the shadow +boundary via Bardeen's celestial coordinates; sqrt(27) M for a = 0. + +Rust: `manifold::spacetime::black_hole_shadow_radius` + """ + ... + +def kerr_geodesic_constants(m: float, a: float, e: float, l: float, q: float) -> KerrConstants: + """ +Bundle the Kerr geodesic constants (energy, axial angular momentum, and +Carter constant per unit rest mass) with their potentials. + +Rust: `manifold::spacetime::kerr_geodesic_constants` + """ + ... + +def gravitational_lens_einstein_radius(m: float, d_l: float, d_s: float, d_ls: float) -> float: + """ +Einstein ring angular radius (geometric units, angles in radians): +theta_E = sqrt(4 M d_ls / (d_l d_s)). + +Rust: `manifold::spacetime::gravitational_lens_einstein_radius` + """ + ... + +def point_lens_magnification(u: float) -> float: + """ +Total point-lens magnification at impact parameter u (in Einstein +radii): (u^2 + 2) / (u sqrt(u^2 + 4)). + +Rust: `manifold::spacetime::point_lens_magnification` + """ + ... + +def lens_equation_solve(beta: float, mass_model: Callable[[float], float]) -> list[float]: + """ +Solve the lens equation beta = theta - alpha(theta) for image positions +given the deflection profile `mass_model` (alpha as a function of +theta, odd in theta). Returns all real images found on both sides. + +Rust: `manifold::spacetime::lens_equation_solve` + """ + ... + +def hawking_temperature(m: float) -> float: + """ +Hawking temperature of a Schwarzschild black hole (SI): +T = hbar c^3 / (8 pi G M k_B). + +Rust: `manifold::spacetime::hawking_temperature` + """ + ... + +def bekenstein_entropy(m: float) -> float: + """ +Bekenstein-Hawking entropy (SI): S = 4 pi G M^2 k_B / (hbar c). + +Rust: `manifold::spacetime::bekenstein_entropy` + """ + ... + +def evaporation_time(m: float) -> float: + """ +Black hole evaporation time (SI): t = 5120 pi G^2 M^3 / (hbar c^4). + +Rust: `manifold::spacetime::evaporation_time` + """ + ... + +def cosmological_distances(z: float, h0: float, omega_m: float, omega_l: float) -> tuple[float, float, float, float]: + """ +Cosmological distances in a flat-ish FRW universe (SI: `h0` in 1/s, +distances in meters, lookback time in seconds): returns (comoving, +angular-diameter, luminosity, lookback). + +Rust: `manifold::spacetime::cosmological_distances` + """ + ... + +def gw_chirp_mass(m1: float, m2: float) -> float: + """ +Chirp mass (m1 m2)^(3/5) / (m1 + m2)^(1/5). + +Rust: `manifold::spacetime::gw_chirp_mass` + """ + ... + +def gw_waveform_inspiral(m1: float, m2: float, d: float, t: list[float]) -> tuple[list[float], list[float]]: + """ +Leading-order (Newtonian chirp) inspiral waveform at luminosity +distance `d` (SI units, face-on): returns (h_plus, h_cross) sampled at +the times `t`, with coalescence at the last sample. + +Rust: `manifold::spacetime::gw_waveform_inspiral` + """ + ... + +def kk_reduce_geodesic_to_charged(geo5: list[GeodesicState], radius: float) -> tuple[float, list[VecN]]: + """ +Reduce a 5D Kaluza-Klein geodesic to 4D charged-particle data: the +conserved fifth momentum gives the charge-to-mass ratio (valid when +the gauge potential vanishes at the initial point and phi = 1), and +the positions project to the 4D worldline. + +Rust: `manifold::spacetime::kk_reduce_geodesic_to_charged` + """ + ... + +def kk_compactification_mass_spectrum(radius: float, n_max: int) -> list[float]: + """ +Kaluza-Klein tower masses n / R for mode numbers 0..=n_max. + +Rust: `manifold::spacetime::kk_compactification_mass_spectrum` + """ + ... + +def extra_dimension_gravity_law(r: float, n_extra: int, size: float) -> float: + """ +Gravitational force law with `n_extra` compact extra dimensions of size +`size` (normalized to 1/r^2 at large r): 1/r^2 outside, continuously +matched to size^n / r^(2+n) inside. + +Rust: `manifold::spacetime::extra_dimension_gravity_law` + """ + ... + +def sta_vs_matrix_lorentz_check(v: Vec3 | Sequence[float]) -> float: + """ +Cross-validate the spacetime algebra boost rotor against the matrix +Lorentz boost: the maximum component difference over a set of basis +events (the STA rotor R e R~ realizes the inverse boost, so it is +compared against B(-v)). + +Rust: `manifold::spacetime::sta_vs_matrix_lorentz_check` + """ + ... diff --git a/bindings/python/python/numeria/manifold/spherical.pyi b/bindings/python/python/numeria/manifold/spherical.pyi new file mode 100644 index 0000000..d77e953 --- /dev/null +++ b/bindings/python/python/numeria/manifold/spherical.pyi @@ -0,0 +1,577 @@ +""" +Spherical geometry: n-sphere maps, spherical trigonometry, map projections, the Hopf fibration, spherical harmonics and their transforms, sky pixelizations, point distributions, and directional statistics. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.statistics.distributions import Exponential +from numeria.quaternion import Quaternion +from numeria.monte_carlo import Rng +from numeria.manifold.lie import So3 +from numeria.math import Vec2 +from numeria.math import Vec3 +from numeria.manifold.vecn import VecN + +def sphere_distance_n(a: VecN | Sequence[float], b: VecN | Sequence[float]) -> float: + """ +Geodesic (angular) distance on the unit n-sphere. + +Rust: `manifold::spherical::sphere_distance_n` + """ + ... + +def sphere_geodesic_n(a: VecN | Sequence[float], b: VecN | Sequence[float], t: float) -> VecN: + """ +Slerp along the great circle from a to b. + +Rust: `manifold::spherical::sphere_geodesic_n` + """ + ... + +def sphere_exp_n(p: VecN | Sequence[float], v: VecN | Sequence[float]) -> VecN: + """ +Exponential map at p: follow the great circle in direction v (tangent, +|v| = arc length). + +Rust: `manifold::spherical::sphere_exp_n` + """ + ... + +def sphere_log_n(p: VecN | Sequence[float], q: VecN | Sequence[float]) -> VecN: + """ +Logarithm map: tangent vector at p pointing toward q with |v| equal to +the geodesic distance. + +Rust: `manifold::spherical::sphere_log_n` + """ + ... + +def sphere_parallel_transport_n(v: VecN | Sequence[float], p: VecN | Sequence[float], q: VecN | Sequence[float]) -> VecN: + """ +Parallel transport of tangent vector v from p to q along the connecting +geodesic. + +Rust: `manifold::spherical::sphere_parallel_transport_n` + """ + ... + +def spherical_triangle_area(a: Vec3 | Sequence[float], b: Vec3 | Sequence[float], c: Vec3 | Sequence[float]) -> float: + """ +Area of a spherical triangle on the unit sphere via l'Huilier's theorem. + +Rust: `manifold::spherical::spherical_triangle_area` + """ + ... + +def spherical_triangle_angles(a: Vec3 | Sequence[float], b: Vec3 | Sequence[float], c: Vec3 | Sequence[float]) -> tuple[float, float, float]: + """ +Interior angles of a spherical triangle at vertices (a, b, c). + +Rust: `manifold::spherical::spherical_triangle_angles` + """ + ... + +def spherical_law_of_cosines(a: float, b: float, gamma: float) -> float: + """ +Spherical law of cosines: cos c = cos a cos b + sin a sin b cos gamma. + +Rust: `manifold::spherical::spherical_law_of_cosines` + """ + ... + +def spherical_law_of_sines(a: float, b: float, beta: float) -> float: + """ +Spherical law of sines: alpha from (a, b, beta) via +sin alpha / sin a = sin beta / sin b. + +Rust: `manifold::spherical::spherical_law_of_sines` + """ + ... + +def haversine(lat1: float, lon1: float, lat2: float, lon2: float, r: float) -> float: + """ +Haversine great-circle distance on a sphere of radius r. + +Rust: `manifold::spherical::haversine` + """ + ... + +def spherical_polygon_area(vertices: list[Vec3 | Sequence[float]]) -> float: + """ +Area of a spherical polygon (unit sphere) by summing triangle fan areas +with orientation from the spherical excess formula. + +Rust: `manifold::spherical::spherical_polygon_area` + """ + ... + +def spherical_centroid(points: list[Vec3 | Sequence[float]]) -> Vec3: + """ +Spherical centroid: normalized arithmetic mean. + +Rust: `manifold::spherical::spherical_centroid` + """ + ... + +def spherical_mean_weighted(points: list[Vec3 | Sequence[float]], weights: list[float]) -> Vec3: + """ +Weighted spherical mean. + +Rust: `manifold::spherical::spherical_mean_weighted` + """ + ... + +def spherical_delaunay(sites: list[Vec3 | Sequence[float]]) -> list[list[int]]: + """ +Spherical Delaunay triangulation by the empty-circumcap test (brute +force; suitable for modest site counts). + +Rust: `manifold::spherical::spherical_delaunay` + """ + ... + +def spherical_voronoi(sites: list[Vec3 | Sequence[float]]) -> list[list[Vec3]]: + """ +Spherical Voronoi cells as the dual of the Delaunay triangulation: each +cell is the list of circumcenters of triangles incident to the site, +ordered by angle. + +Rust: `manifold::spherical::spherical_voronoi` + """ + ... + +def spherical_convex_hull(points: list[Vec3 | Sequence[float]]) -> list[int]: + """ +Indices of points on the 3D convex hull (brute-force facet search). + +Rust: `manifold::spherical::spherical_convex_hull` + """ + ... + +def stereographic(p: Vec3 | Sequence[float]) -> Vec2: + """ +Stereographic projection from the north pole onto the equatorial plane. + +Rust: `manifold::spherical::stereographic` + """ + ... + +def inverse_stereographic(q: Vec2 | Sequence[float]) -> Vec3: + """ +Inverse stereographic projection. + +Rust: `manifold::spherical::inverse_stereographic` + """ + ... + +def stereographic_n(p: VecN | Sequence[float]) -> VecN: + """ +Stereographic projection of the unit n-sphere from the last-coordinate +pole. + +Rust: `manifold::spherical::stereographic_n` + """ + ... + +def gnomonic(p: Vec3 | Sequence[float], center: Vec3 | Sequence[float]) -> Vec2: + """ +Gnomonic projection about `center` (great circles map to lines). + +Rust: `manifold::spherical::gnomonic` + """ + ... + +def gnomonic_inverse(q: Vec2 | Sequence[float], center: Vec3 | Sequence[float]) -> Vec3: + """ +Inverse gnomonic projection. + +Rust: `manifold::spherical::gnomonic_inverse` + """ + ... + +def orthographic(p: Vec3 | Sequence[float], center: Vec3 | Sequence[float]) -> Vec2: + """ +Orthographic projection about `center`. + +Rust: `manifold::spherical::orthographic` + """ + ... + +def orthographic_inverse(q: Vec2 | Sequence[float], center: Vec3 | Sequence[float]) -> Vec3: + """ +Inverse orthographic (near-side solution). + +Rust: `manifold::spherical::orthographic_inverse` + """ + ... + +def mercator(p: Vec3 | Sequence[float]) -> Vec2: + """ +Mercator projection (x = lon, y = ln tan(pi/4 + lat/2)). + +Rust: `manifold::spherical::mercator` + """ + ... + +def mercator_inverse(q: Vec2 | Sequence[float]) -> Vec3: + """ +Inverse Mercator. + +Rust: `manifold::spherical::mercator_inverse` + """ + ... + +def lambert_azimuthal_equal_area(p: Vec3 | Sequence[float]) -> Vec2: + """ +Lambert azimuthal equal-area projection about the north pole. + +Rust: `manifold::spherical::lambert_azimuthal_equal_area` + """ + ... + +def lambert_azimuthal_equal_area_inverse(q: Vec2 | Sequence[float]) -> Vec3: + """ +Inverse Lambert azimuthal equal-area. + +Rust: `manifold::spherical::lambert_azimuthal_equal_area_inverse` + """ + ... + +def mollweide(p: Vec3 | Sequence[float]) -> Vec2: + """ +Mollweide projection (equal-area pseudocylindrical). + +Rust: `manifold::spherical::mollweide` + """ + ... + +def mollweide_inverse(q: Vec2 | Sequence[float]) -> Vec3: + """ +Inverse Mollweide. + +Rust: `manifold::spherical::mollweide_inverse` + """ + ... + +def equirectangular(p: Vec3 | Sequence[float]) -> Vec2: + """ +Equirectangular projection (x = lon, y = lat). + +Rust: `manifold::spherical::equirectangular` + """ + ... + +def equirectangular_inverse(q: Vec2 | Sequence[float]) -> Vec3: + """ +Inverse equirectangular. + +Rust: `manifold::spherical::equirectangular_inverse` + """ + ... + +def azimuthal_equidistant(p: Vec3 | Sequence[float]) -> Vec2: + """ +Azimuthal equidistant projection about the north pole. + +Rust: `manifold::spherical::azimuthal_equidistant` + """ + ... + +def azimuthal_equidistant_inverse(q: Vec2 | Sequence[float]) -> Vec3: + """ +Inverse azimuthal equidistant. + +Rust: `manifold::spherical::azimuthal_equidistant_inverse` + """ + ... + +def robinson(p: Vec3 | Sequence[float]) -> Vec2: + """ +Robinson projection (table-interpolated pseudocylindrical). + +Rust: `manifold::spherical::robinson` + """ + ... + +def robinson_inverse(q: Vec2 | Sequence[float]) -> Vec3: + """ +Inverse Robinson (bisection on the latitude table). + +Rust: `manifold::spherical::robinson_inverse` + """ + ... + +def hopf_fibration(q: Quaternion | Sequence[float]) -> Vec3: + """ +Hopf map S3 -> S2: q -> q k q^-1 image of the base point, giving +(2(xz + wy), 2(yz - wx), w^2 + z^2 - x^2 - y^2). + +Rust: `manifold::spherical::hopf_fibration` + """ + ... + +def hopf_fiber(p: Vec3 | Sequence[float], n: int) -> list[Quaternion]: + """ +The circle fiber in S3 above a point of S2, sampled at n quaternions. + +Rust: `manifold::spherical::hopf_fiber` + """ + ... + +def hopf_fiber_stereographic(p: Vec3 | Sequence[float], n: int) -> list[Vec3]: + """ +Hopf fiber stereographically projected to R3 (a Villarceau circle). + +Rust: `manifold::spherical::hopf_fiber_stereographic` + """ + ... + +def s3_geodesic(a: Quaternion | Sequence[float], b: Quaternion | Sequence[float], t: float) -> Quaternion: + """ +Geodesic on S3 between unit quaternions (slerp). + +Rust: `manifold::spherical::s3_geodesic` + """ + ... + +def s3_uniform_points(n: int) -> list[Quaternion]: + """ +Near-uniform deterministic points on S3 (super-Fibonacci spiral). + +Rust: `manifold::spherical::s3_uniform_points` + """ + ... + +def sphere_uniform_points_n(n: int, dim: int, rng: Rng) -> list[VecN]: + """ +Uniform random points on the unit (dim-1)-sphere in R^dim. + +Rust: `manifold::spherical::sphere_uniform_points_n` + """ + ... + +def sphere_volume_n(r: float, n: int) -> float: + """ +Volume of the n-ball of radius r. + +Rust: `manifold::spherical::sphere_volume_n` + """ + ... + +def sphere_surface_n(r: float, n: int) -> float: + """ +Surface area of the (n-1)-sphere of radius r in R^n. + +Rust: `manifold::spherical::sphere_surface_n` + """ + ... + +def sphere_cap_area(r: float, theta: float) -> float: + """ +Area of a spherical cap of opening angle theta on a sphere of radius r. + +Rust: `manifold::spherical::sphere_cap_area` + """ + ... + +def sphere_cap_volume(r: float, theta: float) -> float: + """ +Volume of the corresponding solid cap. + +Rust: `manifold::spherical::sphere_cap_volume` + """ + ... + +def spherical_harmonics_complex(l: int, m: int, theta: float, phi: float) -> complex: + """ +Complex spherical harmonic Y_l^m(theta, phi) with the Condon-Shortley +phase. + +Rust: `manifold::spherical::spherical_harmonics_complex` + """ + ... + +def spherical_harmonic_transform(f: Callable[[float, float], float], l_max: int, n_theta: int, n_phi: int) -> list[complex]: + """ +Forward spherical harmonic transform up to `l_max` by quadrature on an +(n_theta x n_phi) grid. Coefficients are ordered (l, m) with +index l^2 + l + m. + +Rust: `manifold::spherical::spherical_harmonic_transform` + """ + ... + +def spherical_harmonic_inverse(coeffs: list[complex], l_max: int, theta: float, phi: float) -> float: + """ +Evaluate a coefficient vector at (theta, phi). + +Rust: `manifold::spherical::spherical_harmonic_inverse` + """ + ... + +def spherical_convolution(f_coeffs: list[complex], g_coeffs: list[complex], l_max: int) -> list[complex]: + """ +Spectral convolution with a zonal kernel: multiplies each (l, m) +coefficient by sqrt(4 pi/(2l+1)) g_l0. + +Rust: `manifold::spherical::spherical_convolution` + """ + ... + +def spherical_laplacian_spectral(coeffs: list[complex], l_max: int) -> list[complex]: + """ +Spectral Laplace-Beltrami: multiplies each degree-l coefficient by +-l(l+1). + +Rust: `manifold::spherical::spherical_laplacian_spectral` + """ + ... + +def spherical_heat_flow(coeffs: list[complex], l_max: int, t: float) -> list[complex]: + """ +Heat flow on the sphere: coefficients decay as exp(-l(l+1) t). + +Rust: `manifold::spherical::spherical_heat_flow` + """ + ... + +def spherical_wavelets(coeffs: list[complex], l_max: int, t: float) -> list[complex]: + """ +Simple spherical wavelet band-pass: difference of two heat kernels at +scales t and 2t applied spectrally. + +Rust: `manifold::spherical::spherical_wavelets` + """ + ... + +def healpix_npix(nside: int) -> int: + """ +Number of HEALPix pixels: 12 nside^2. + +Rust: `manifold::spherical::healpix_npix` + """ + ... + +def healpix_ang2pix(nside: int, theta: float, phi: float) -> int: + """ +HEALPix ring-scheme pixel index for direction (theta, phi). + +Rust: `manifold::spherical::healpix_ang2pix` + """ + ... + +def healpix_pix2ang(nside: int, pix: int) -> tuple[float, float]: + """ +Center direction (theta, phi) of a HEALPix ring-scheme pixel. + +Rust: `manifold::spherical::healpix_pix2ang` + """ + ... + +def spherical_cap_packing(n: int) -> float: + """ +Estimate of the Tammes-problem packing angle for n caps (empirical +asymptotic bound). + +Rust: `manifold::spherical::spherical_cap_packing` + """ + ... + +def thomson_problem(n: int, iters: int, rng: Rng) -> list[Vec3]: + """ +Thomson problem: minimize Coulomb energy of n charges by projected +gradient descent. Returns the final configuration. + +Rust: `manifold::spherical::thomson_problem` + """ + ... + +def spherical_code_min_angle(points: list[Vec3 | Sequence[float]]) -> float: + """ +Minimum pairwise angular distance of a spherical code. + +Rust: `manifold::spherical::spherical_code_min_angle` + """ + ... + +def rotate_sphere_points(points: list[Vec3 | Sequence[float]], r: So3) -> list[Vec3]: + """ +Rotate a point set by a rotation. + +Rust: `manifold::spherical::rotate_sphere_points` + """ + ... + +def spherical_kmeans(points: list[Vec3 | Sequence[float]], k: int, iters: int, rng: Rng) -> tuple[list[Vec3], list[int]]: + """ +Spherical k-means with cosine distance. Returns (centroids, labels). + +Rust: `manifold::spherical::spherical_kmeans` + """ + ... + +def von_mises_fisher_pdf(x: Vec3 | Sequence[float], mu: Vec3 | Sequence[float], kappa: float) -> float: + """ +Von Mises-Fisher density on S2. + +Rust: `manifold::spherical::von_mises_fisher_pdf` + """ + ... + +def vmf_sample(mu: Vec3 | Sequence[float], kappa: float, rng: Rng) -> Vec3: + """ +Sample from the von Mises-Fisher distribution on S2 (Ulrich/Wood). + +Rust: `manifold::spherical::vmf_sample` + """ + ... + +def vmf_fit(points: list[Vec3 | Sequence[float]]) -> tuple[Vec3, float]: + """ +Fit (mu, kappa) of a von Mises-Fisher distribution from samples. + +Rust: `manifold::spherical::vmf_fit` + """ + ... + +def kent_distribution_pdf(x: Vec3 | Sequence[float], g1: Vec3 | Sequence[float], g2: Vec3 | Sequence[float], g3: Vec3 | Sequence[float], kappa: float, beta: float) -> float: + """ +Kent (Fisher-Bingham 5-parameter) density up to normalization refinement: +f = C exp(kappa g1.x + beta ((g2.x)^2 - (g3.x)^2)). + +Rust: `manifold::spherical::kent_distribution_pdf` + """ + ... + +def spherical_t_design(t: int) -> Optional[list[Vec3]]: + """ +Small spherical t-designs from tables: t = 1 (antipodes), 2 +(tetrahedron), 3 (octahedron), 5 (icosahedron). None otherwise. + +Rust: `manifold::spherical::spherical_t_design` + """ + ... + +def lebedev_quadrature(order: int) -> list[tuple[Vec3, float]]: + """ +Lebedev quadrature nodes and weights for orders 6, 14, and 26 (weights +sum to 1; integrates times 4 pi). + +Panics: +Panics for unsupported orders. + +Rust: `manifold::spherical::lebedev_quadrature` + """ + ... + +def gauss_legendre_sphere(n_theta: int, n_phi: int) -> list[tuple[float, float, float]]: + """ +Product Gauss-Legendre x uniform-phi quadrature on the sphere: returns +(theta, phi, weight) with weights summing to 4 pi. + +Rust: `manifold::spherical::gauss_legendre_sphere` + """ + ... diff --git a/bindings/python/python/numeria/manifold/vecn.pyi b/bindings/python/python/numeria/manifold/vecn.pyi new file mode 100644 index 0000000..2509985 --- /dev/null +++ b/bindings/python/python/numeria/manifold/vecn.pyi @@ -0,0 +1,122 @@ +""" +n-dimensional vectors and arbitrary-rank tensors: the generic machinery behind the metric-driven differential geometry in this module tree. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.linalg.matrix import Matrix +from numeria.monte_carlo import Rng +from numeria.math import Vec2 +from numeria.math import Vec3 + +class TensorN: + """ +A dense tensor of arbitrary rank, stored row-major (last index fastest). + +Rust: `manifold::vecn::TensorN` + """ + def __init__(self, shape: list[int], data: list[float]) -> None: ... + @staticmethod + def zeros(shape: list[int]) -> TensorN: ... + @staticmethod + def ones(shape: list[int]) -> TensorN: ... + @staticmethod + def identity_2(n: int) -> TensorN: ... + @staticmethod + def from_fn(shape: list[int], f: Callable[[list[int]], float]) -> TensorN: ... + @staticmethod + def from_matrix(m: Matrix | Sequence[Sequence[float]]) -> TensorN: ... + def to_matrix(self) -> Optional[Matrix]: ... + def get(self, idx: list[int]) -> float: ... + def set(self, idx: list[int], v: float) -> None: ... + def rank(self) -> int: ... + def size(self) -> int: ... + def contract(self, i: int, j: int) -> TensorN: ... + def tensor_product(self, other: TensorN) -> TensorN: ... + def contract_with(self, other: TensorN, i_self: int, j_other: int) -> TensorN: ... + def transpose(self, perm: list[int]) -> TensorN: ... + def symmetrize(self, i: int, j: int) -> TensorN: ... + def antisymmetrize(self, i: int, j: int) -> TensorN: ... + def is_symmetric(self, i: int, j: int, tol: float) -> bool: ... + def raise_index(self, i: int, metric_inv: Matrix | Sequence[Sequence[float]]) -> TensorN: ... + def lower_index(self, i: int, metric: Matrix | Sequence[Sequence[float]]) -> TensorN: ... + def slice(self, axis: int, idx: int) -> TensorN: ... + def norm_frobenius(self) -> float: ... + def map(self, f: Callable[[float], float]) -> TensorN: ... + def add(self, other: TensorN) -> TensorN: ... + def sub(self, other: TensorN) -> TensorN: ... + def scale(self, k: float) -> TensorN: ... + @staticmethod + def einsum(spec: str, tensors: list[TensorN]) -> TensorN: ... + @staticmethod + def levi_civita(n: int) -> TensorN: ... + @staticmethod + def kronecker(n: int) -> TensorN: ... + def hodge_dual_vector(self, metric: Matrix | Sequence[Sequence[float]]) -> TensorN: ... + @property + def shape(self) -> list[int]: ... + @property + def data(self) -> list[float]: ... + +class VecN: + """ +A dense n-dimensional vector. + +Rust: `manifold::vecn::VecN` + """ + def __init__(self, data: list[float]) -> None: ... + @staticmethod + def zeros(n: int) -> VecN: ... + @staticmethod + def ones(n: int) -> VecN: ... + @staticmethod + def unit(n: int, i: int) -> VecN: ... + @staticmethod + def from_(slice: list[float]) -> VecN: ... + def dim(self) -> int: ... + def dot(self, other: VecN | Sequence[float]) -> float: ... + def norm(self) -> float: ... + def normalized(self) -> VecN: ... + def add(self, other: VecN | Sequence[float]) -> VecN: ... + def sub(self, other: VecN | Sequence[float]) -> VecN: ... + def scale(self, k: float) -> VecN: ... + def outer(self, other: VecN | Sequence[float]) -> Matrix: ... + def project_onto(self, other: VecN | Sequence[float]) -> VecN: ... + def angle_between(self, other: VecN | Sequence[float]) -> float: ... + def lerp(self, other: VecN | Sequence[float], t: float) -> VecN: ... + def cross_3d(self, other: VecN | Sequence[float]) -> Optional[Vec3]: ... + def to_vec3(self) -> Optional[Vec3]: ... + def to_vec2(self) -> Optional[Vec2]: ... + @staticmethod + def gram_schmidt(vectors: list[VecN | Sequence[float]]) -> list[VecN]: ... + @staticmethod + def random_unit(n: int, rng: Rng) -> VecN: ... + @staticmethod + def random_gaussian(n: int, rng: Rng) -> VecN: ... + def __add__(self, rhs: VecN | Sequence[float]) -> VecN: ... + def __sub__(self, rhs: VecN | Sequence[float]) -> VecN: ... + def __mul__(self, k: float) -> VecN: ... + def __neg__(self) -> VecN: ... + @property + def data(self) -> list[float]: ... + +def wedge(a: TensorN, b: TensorN) -> TensorN: + """ +Wedge product of antisymmetric forms: antisymmetrization of the tensor +product with the standard combinatorial normalization +(a ^ b)_{i...j...} = (p+q)!/(p! q!) Alt(a (x) b). + +Rust: `manifold::vecn::wedge` + """ + ... + +def determinant_n(m: Matrix | Sequence[Sequence[float]]) -> float: + """ +Determinant of a square matrix via LU decomposition. + +Rust: `manifold::vecn::determinant_n` + """ + ... diff --git a/bindings/python/python/numeria/materials/__init__.pyi b/bindings/python/python/numeria/materials/__init__.pyi new file mode 100644 index 0000000..4dbbcc9 --- /dev/null +++ b/bindings/python/python/numeria/materials/__init__.pyi @@ -0,0 +1,12 @@ +""" +Reference property tables. Lookup data rather than computation: `elements` carries all 118 elements with atomic mass, density, melting and boiling points and thermal and electrical conductivity; `common` carries engineering solids; `fluids` carries liquids with density, viscosity, surface tension and speed of sound; and `gases` carries molar mass, specific heat ratio and thermal conductivity. Values are room-temperature and one-atmosphere unless stated. They are reference figures for calculation, not a substitute for a datasheet on a specific alloy or grade. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from . import common, elements, fluids, gases + + diff --git a/bindings/python/python/numeria/materials/common.pyi b/bindings/python/python/numeria/materials/common.pyi new file mode 100644 index 0000000..3d6c15d --- /dev/null +++ b/bindings/python/python/numeria/materials/common.pyi @@ -0,0 +1,69 @@ +""" +Engineering solids: metals, alloys, polymers and ceramics. Density, Young's modulus, yield and tensile strength, Poisson's ratio, thermal conductivity and expansion, and specific heat. Room-temperature values; a specific alloy, temper or grade will differ, sometimes substantially. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.statistics.distributions import Poisson + +class Material: + """ +Engineering solids: metals, alloys, polymers and ceramics. + +Density, Young's modulus, yield and tensile strength, Poisson's ratio, +thermal conductivity and expansion, and specific heat. Room-temperature +values; a specific alloy, temper or grade will differ, sometimes +substantially. +An engineering material with mechanical and thermal properties. + +All values use SI units: +- `density`: kg/m^3 +- `youngs_modulus`, `yield_strength`, `tensile_strength`: Pa +- `thermal_conductivity`: W/(m*K) +- `specific_heat`: J/(kg*K) +- `thermal_expansion`: 1/K +- `melting_point`: K + +Rust: `materials::common::Material` + """ + @property + def name(self) -> str: ... + @property + def density(self) -> float: ... + @property + def youngs_modulus(self) -> float: ... + @property + def poisson_ratio(self) -> float: ... + @property + def yield_strength(self) -> float: ... + @property + def tensile_strength(self) -> float: ... + @property + def thermal_conductivity(self) -> float: ... + @property + def specific_heat(self) -> float: ... + @property + def thermal_expansion(self) -> float: ... + @property + def melting_point(self) -> float: ... + +def by_name(name: str) -> Optional[Material]: + """ +Looks up a material by name using case-insensitive ASCII comparison. + +Zero-allocation: uses `eq_ignore_ascii_case` instead of `to_lowercase`. + +Rust: `materials::common::by_name` + """ + ... + +def all() -> list[Material]: + """ +Returns a slice of all common engineering materials. + +Rust: `materials::common::all` + """ + ... diff --git a/bindings/python/python/numeria/materials/elements.pyi b/bindings/python/python/numeria/materials/elements.pyi new file mode 100644 index 0000000..b4f29f0 --- /dev/null +++ b/bindings/python/python/numeria/materials/elements.pyi @@ -0,0 +1,137 @@ +""" +The 118 chemical elements. Atomic number, symbol, name, atomic mass, density, melting and boiling points, and thermal and electrical conductivity, with lookup by atomic number, symbol or name. Densities are for the standard state at room temperature, so gases are quoted at STP. Where an element has no stable isotope the atomic mass is that of the longest-lived one, and properties that have never been measured are absent rather than guessed. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.audio.envelope import Ar + +class Element: + """ +A chemical element with its physical and chemical properties. + +All values use SI units unless otherwise noted: +- `density`: kg/m^3 +- `melting_point`, `boiling_point`: Kelvin +- `specific_heat`: J/(kg*K) +- `thermal_conductivity`: W/(m*K) +- `ionization_energy`, `electron_affinity`: eV +- `atomic_radius`: pm (picometers) + +Rust: `materials::elements::Element` + """ + @property + def atomic_number(self) -> int: ... + @property + def symbol(self) -> str: ... + @property + def name(self) -> str: ... + @property + def atomic_mass(self) -> float: ... + @property + def density(self) -> float: ... + @property + def melting_point(self) -> float: ... + @property + def boiling_point(self) -> float: ... + @property + def specific_heat(self) -> float: ... + @property + def thermal_conductivity(self) -> float: ... + @property + def electronegativity(self) -> float: ... + @property + def ionization_energy(self) -> float: ... + @property + def electron_affinity(self) -> float: ... + @property + def atomic_radius(self) -> float: ... + @property + def category(self) -> ElementCategory: ... + @property + def standard_state(self) -> StandardState: ... + @property + def electron_configuration(self) -> str: ... + @property + def oxidation_states(self) -> str: ... + +class ElementCategory: + """ +The 118 chemical elements. + +Atomic number, symbol, name, atomic mass, density, melting and boiling +points, and thermal and electrical conductivity, with lookup by atomic +number, symbol or name. + +Densities are for the standard state at room temperature, so gases are +quoted at STP. Where an element has no stable isotope the atomic mass +is that of the longest-lived one, and properties that have never been +measured are absent rather than guessed. +Classification of an element within the periodic table. + +Rust: `materials::elements::ElementCategory` + """ + ... + +class StandardState: + """ +The standard state of an element at room temperature and pressure (STP). + +Rust: `materials::elements::StandardState` + """ + ... + +def by_atomic_number(z: int) -> Optional[Element]: + """ +Looks up an element by its atomic number (1..=118). + +Returns `None` if `z` is outside the valid range. +This is an O(1) direct index lookup. + +Rust: `materials::elements::by_atomic_number` + """ + ... + +def by_symbol(symbol: str) -> Optional[Element]: + """ +Looks up an element by its chemical symbol (case-sensitive, e.g. "He", "Fe"). + +Performs a linear scan over the 118 elements with direct string comparison. + +Rust: `materials::elements::by_symbol` + """ + ... + +def by_symbol_static(symbol: str) -> Optional[Element]: + """ +O(1) lookup for commonly used element symbols, falling back to linear scan. + +Covers the 27 most frequently looked-up elements (H, He, Li, Be, B, C, N, +O, F, Ne, Na, Mg, Al, Si, P, S, Cl, Ar, K, Ca, Fe, Cu, Zn, Ag, Au, Pb, U) +via a match statement that returns a direct index into the static array. +All other symbols fall back to `by_symbol`. + +Rust: `materials::elements::by_symbol_static` + """ + ... + +def by_name(name: str) -> Optional[Element]: + """ +Looks up an element by name using case-insensitive ASCII comparison. + +Zero-allocation: uses `eq_ignore_ascii_case` instead of `to_lowercase`. + +Rust: `materials::elements::by_name` + """ + ... + +def all() -> list[Element]: + """ +Returns a slice of all 118 elements, ordered by atomic number. + +Rust: `materials::elements::all` + """ + ... diff --git a/bindings/python/python/numeria/materials/fluids.pyi b/bindings/python/python/numeria/materials/fluids.pyi new file mode 100644 index 0000000..5d0f07b --- /dev/null +++ b/bindings/python/python/numeria/materials/fluids.pyi @@ -0,0 +1,71 @@ +""" +Common liquids. Density, dynamic and kinematic viscosity, surface tension, speed of sound, specific heat, and boiling and freezing points, at room temperature and one atmosphere. Viscosity is the strongly temperature-dependent one: it can change by a factor of several over a few tens of degrees, so a single figure is only a starting point. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +class Fluid: + """ +Common liquids. + +Density, dynamic and kinematic viscosity, surface tension, speed of +sound, specific heat, and boiling and freezing points, at room +temperature and one atmosphere. + +Viscosity is the strongly temperature-dependent one: it can change by a +factor of several over a few tens of degrees, so a single figure is +only a starting point. +A fluid (liquid) with its mechanical and thermal properties at 20 degrees C +unless otherwise noted in the entry. + +All values use SI units: +- `density`: kg/m^3 +- `dynamic_viscosity`: Pa*s +- `kinematic_viscosity`: m^2/s +- `surface_tension`: N/m +- `specific_heat`: J/(kg*K) +- `thermal_conductivity`: W/(m*K) +- `boiling_point`, `freezing_point`: K + +Rust: `materials::fluids::Fluid` + """ + @property + def name(self) -> str: ... + @property + def density(self) -> float: ... + @property + def dynamic_viscosity(self) -> float: ... + @property + def kinematic_viscosity(self) -> float: ... + @property + def surface_tension(self) -> float: ... + @property + def specific_heat(self) -> float: ... + @property + def thermal_conductivity(self) -> float: ... + @property + def boiling_point(self) -> float: ... + @property + def freezing_point(self) -> float: ... + +def by_name(name: str) -> Optional[Fluid]: + """ +Looks up a fluid by name using case-insensitive ASCII comparison. + +Zero-allocation: uses `eq_ignore_ascii_case` instead of `to_lowercase`. + +Rust: `materials::fluids::by_name` + """ + ... + +def all() -> list[Fluid]: + """ +Returns a slice of all fluids in the database. + +Rust: `materials::fluids::all` + """ + ... diff --git a/bindings/python/python/numeria/materials/gases.pyi b/bindings/python/python/numeria/materials/gases.pyi new file mode 100644 index 0000000..2f6478f --- /dev/null +++ b/bindings/python/python/numeria/materials/gases.pyi @@ -0,0 +1,68 @@ +""" +Common gases. Molar mass, density at STP, specific heat at constant pressure and the specific heat ratio `γ`, thermal conductivity, viscosity and the speed of sound. `γ` is the entry most often needed: it fixes the adiabatic relations and the speed of sound `c = √(γRT/M)`, and it follows the molecular structure -- about 5/3 for a monatomic gas, 7/5 for a diatomic one. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +class Gas: + """ +Common gases. + +Molar mass, density at STP, specific heat at constant pressure and the +specific heat ratio `γ`, thermal conductivity, viscosity and the speed +of sound. + +`γ` is the entry most often needed: it fixes the adiabatic relations +and the speed of sound `c = √(γRT/M)`, and it follows the molecular +structure -- about 5/3 for a monatomic gas, 7/5 for a diatomic one. +A gas with its thermodynamic and transport properties at STP. + +All values use SI units: +- `molar_mass`: kg/mol +- `cp`, `cv`: J/(kg*K) +- `density_stp`: kg/m^3 +- `dynamic_viscosity`: Pa*s +- `thermal_conductivity`: W/(m*K) + +Rust: `materials::gases::Gas` + """ + @property + def name(self) -> str: ... + @property + def formula(self) -> str: ... + @property + def molar_mass(self) -> float: ... + @property + def specific_heat_ratio(self) -> float: ... + @property + def cp(self) -> float: ... + @property + def cv(self) -> float: ... + @property + def density_stp(self) -> float: ... + @property + def dynamic_viscosity(self) -> float: ... + @property + def thermal_conductivity(self) -> float: ... + +def by_name(name: str) -> Optional[Gas]: + """ +Looks up a gas by name using case-insensitive ASCII comparison. + +Zero-allocation: uses `eq_ignore_ascii_case` instead of `to_lowercase`. + +Rust: `materials::gases::by_name` + """ + ... + +def all() -> list[Gas]: + """ +Returns a slice of all gases in the database. + +Rust: `materials::gases::all` + """ + ... diff --git a/bindings/python/python/numeria/math/__init__.pyi b/bindings/python/python/numeria/math/__init__.pyi new file mode 100644 index 0000000..0cc00ad --- /dev/null +++ b/bindings/python/python/numeria/math/__init__.pyi @@ -0,0 +1,65 @@ +""" +Vectors and the crate's table of physical constants. `Vec2` and `Vec3` with the usual algebra -- addition, scaling, dot and cross products, norms, normalization, projection, reflection, rotation and interpolation. `constants` is the single table the rest of the crate refers back to, and it is deliberately one table: duplicate definitions elsewhere are re-exports of it, and a test enforces that they agree. The values fixed by the 2019 SI redefinition -- `C`, `H`, `HBAR`, `E_CHARGE`, `K_B`, `N_A` -- are exact by definition rather than measured. Constants that are products of others, such as `FARADAY = N_A · E_CHARGE`, are computed from their factors rather than transcribed, so they cannot disagree with them. For the 2022 CODATA set with units attached see `units::quantity::constants_codata`. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from . import constants + +class Vec2: + """ +2D vector (fluid grids, planar geometry). + +Rust: `math::Vec2` + """ + def __init__(self, x: float, y: float) -> None: ... + def magnitude(self) -> float: ... + def magnitude_squared(self) -> float: ... + def normalized(self) -> Vec2: ... + def dot(self, other: Vec2 | Sequence[float]) -> float: ... + def cross(self, other: Vec2 | Sequence[float]) -> float: ... + def perp(self) -> Vec2: ... + def lerp(self, other: Vec2 | Sequence[float], t: float) -> Vec2: ... + def distance_to(self, other: Vec2 | Sequence[float]) -> float: ... + def angle_between(self, other: Vec2 | Sequence[float]) -> float: ... + def rotate(self, angle: float) -> Vec2: ... + def to_vec3(self) -> Vec3: ... + def __add__(self, rhs: Vec2 | Sequence[float]) -> Vec2: ... + def __sub__(self, rhs: Vec2 | Sequence[float]) -> Vec2: ... + def __mul__(self, rhs: float) -> Vec2: ... + def __neg__(self) -> Vec2: ... + @property + def x(self) -> float: ... + @property + def y(self) -> float: ... + +class Vec3: + """ +3D vector used throughout the physics engine. + +Rust: `math::Vec3` + """ + def __init__(self, x: float, y: float, z: float) -> None: ... + def magnitude(self) -> float: ... + def magnitude_squared(self) -> float: ... + def normalized(self) -> Vec3: ... + def dot(self, other: Vec3 | Sequence[float]) -> float: ... + def cross(self, other: Vec3 | Sequence[float]) -> Vec3: ... + def distance_to(self, other: Vec3 | Sequence[float]) -> float: ... + def angle_between(self, other: Vec3 | Sequence[float]) -> float: ... + def lerp(self, other: Vec3 | Sequence[float], t: float) -> Vec3: ... + def project_onto(self, other: Vec3 | Sequence[float]) -> Vec3: ... + def reflect(self, normal: Vec3 | Sequence[float]) -> Vec3: ... + def __add__(self, rhs: Vec3 | Sequence[float]) -> Vec3: ... + def __sub__(self, rhs: Vec3 | Sequence[float]) -> Vec3: ... + def __mul__(self, rhs: float) -> Vec3: ... + def __neg__(self) -> Vec3: ... + @property + def x(self) -> float: ... + @property + def y(self) -> float: ... + @property + def z(self) -> float: ... diff --git a/bindings/python/python/numeria/math/constants.pyi b/bindings/python/python/numeria/math/constants.pyi new file mode 100644 index 0000000..856f6d0 --- /dev/null +++ b/bindings/python/python/numeria/math/constants.pyi @@ -0,0 +1,129 @@ +""" +Physical and mathematical constants (NIST CODATA 2018 / 2019 SI redefinition). +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +PI: float + +TAU: float + +E: float + +SQRT_2: float + +LN_2: float + +LN_10: float + +C: float + +G: float + +H: float + +HBAR: float + +K_B: float + +E_CHARGE: float + +N_A: float + +R: float + +FARADAY: float + +G_ACCEL: float + +M_ELECTRON: float + +M_PROTON: float + +M_NEUTRON: float + +AMU: float + +EPSILON_0: float + +MU_0: float + +K_E: float + +VACUUM_IMPEDANCE: float + +MAGNETIC_FLUX_QUANTUM: float + +CONDUCTANCE_QUANTUM: float + +VON_KLITZING: float + +JOSEPHSON: float + +SIGMA: float + +WIEN_DISPLACEMENT: float + +FIRST_RADIATION: float + +SECOND_RADIATION: float + +RYDBERG: float + +RYDBERG_ENERGY: float + +BOHR_RADIUS: float + +BOHR_MAGNETON: float + +NUCLEAR_MAGNETON: float + +ALPHA: float + +ALPHA_INV: float + +PLANCK_MASS: float + +PLANCK_LENGTH: float + +PLANCK_TIME: float + +PLANCK_TEMPERATURE: float + +PLANCK_CHARGE: float + +SOLAR_MASS: float + +SOLAR_RADIUS: float + +SOLAR_LUMINOSITY: float + +SOLAR_TEMPERATURE: float + +EARTH_MASS: float + +EARTH_RADIUS: float + +EARTH_MOON_DISTANCE: float + +AU: float + +LIGHT_YEAR: float + +PARSEC: float + +HUBBLE: float + +CMB_TEMPERATURE: float + +EV_TO_JOULES: float + +CALORIE: float + +ATM: float + +TORR: float diff --git a/bindings/python/python/numeria/mesh/__init__.pyi b/bindings/python/python/numeria/mesh/__init__.pyi new file mode 100644 index 0000000..a15fa62 --- /dev/null +++ b/bindings/python/python/numeria/mesh/__init__.pyi @@ -0,0 +1,69 @@ +""" +Indexed triangle meshes: construction, mass properties, cleanup, spatial queries, and OBJ/STL interchange. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from . import analyze, generate, isosurface, parameterize, subdivide, surfaces +from numeria.spatial.primitives import Aabb +from numeria.spatial.bvh import Bvh +from numeria.linalg import Mat3 +from numeria.quaternion import Quaternion +from numeria.spatial.primitives import Ray +from numeria.monte_carlo import Rng +from numeria.spatial.primitives import Sphere +from numeria.spatial.primitives import Triangle +from numeria.math import Vec2 +from numeria.math import Vec3 + +class Mesh: + """ +Indexed triangle mesh with optional per-vertex normals and UVs. + +`normals` and `uvs`, when present, are parallel to `vertices`. + +Rust: `mesh::Mesh` + """ + def __init__(self, vertices: list[Vec3 | Sequence[float]], indices: list[list[int]]) -> None: ... + def triangle(self, i: int) -> Triangle: ... + def triangles(self) -> list[Triangle]: ... + def to_triangles(self) -> list[Triangle]: ... + def face_normals(self) -> list[Vec3]: ... + def compute_vertex_normals(self) -> None: ... + def surface_area(self) -> float: ... + def volume(self) -> float: ... + def centroid(self) -> Vec3: ... + def center_of_mass_surface(self) -> Vec3: ... + def inertia_tensor(self, density: float) -> Mat3: ... + def principal_inertia(self, density: float) -> tuple[list[float], Mat3]: ... + def bounding_box(self) -> Aabb: ... + def bounding_sphere(self) -> Sphere: ... + def translate(self, offset: Vec3 | Sequence[float]) -> None: ... + def scale(self, factor: float) -> None: ... + def rotate(self, q: Quaternion | Sequence[float]) -> None: ... + def merge(self, other: Mesh) -> None: ... + def flip_normals(self) -> None: ... + def weld_vertices(self, tol: float) -> int: ... + def remove_unused_vertices(self) -> None: ... + def remove_degenerate_triangles(self, area_tol: float) -> int: ... + def edges(self) -> list[tuple[int, int]]: ... + def adjacency(self) -> list[list[int]]: ... + def face_adjacency(self) -> list[list[Optional[int]]]: ... + def build_bvh(self) -> Bvh: ... + def raycast(self, r: Ray, bvh: Optional[Bvh]) -> Optional[tuple[int, RayHit]]: ... + def sample_surface(self, n: int, rng: Rng) -> list[Vec3]: ... + def to_obj(self) -> str: ... + @staticmethod + def from_obj(s: str) -> Mesh: ... + def to_stl_ascii(self) -> str: ... + @property + def vertices(self) -> list[Vec3]: ... + @property + def indices(self) -> list[list[int]]: ... + @property + def normals(self) -> Optional[list[Vec3]]: ... + @property + def uvs(self) -> Optional[list[Vec2]]: ... diff --git a/bindings/python/python/numeria/mesh/analyze.pyi b/bindings/python/python/numeria/mesh/analyze.pyi new file mode 100644 index 0000000..ac1140c --- /dev/null +++ b/bindings/python/python/numeria/mesh/analyze.pyi @@ -0,0 +1,242 @@ +""" +Mesh analysis: topology (manifoldness, orientation, boundary, components, genus), quality statistics, QEM decimation, discrete curvatures, and geodesic distances. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.spatial.bvh import Bvh +from numeria.graph.core import Graph + +class MeshStats: + """ +Summary statistics of a mesh. + +Rust: `mesh::analyze::MeshStats` + """ + def __init__(self, vertices: int, edges: int, faces: int, euler: int, genus: Optional[int], boundary_loops: int, is_manifold: bool, is_closed: bool, is_oriented: bool, min_angle_deg: float, max_angle_deg: float, min_edge: float, max_edge: float) -> None: ... + @property + def vertices(self) -> int: ... + @property + def edges(self) -> int: ... + @property + def faces(self) -> int: ... + @property + def euler(self) -> int: ... + @property + def genus(self) -> Optional[int]: ... + @property + def boundary_loops(self) -> int: ... + @property + def is_manifold(self) -> bool: ... + @property + def is_closed(self) -> bool: ... + @property + def is_oriented(self) -> bool: ... + @property + def min_angle_deg(self) -> float: ... + @property + def max_angle_deg(self) -> float: ... + @property + def min_edge(self) -> float: ... + @property + def max_edge(self) -> float: ... + +def stats(m: Mesh) -> MeshStats: + """ +Computes all `MeshStats` in one pass over the mesh. + +Rust: `mesh::analyze::stats` + """ + ... + +def euler_characteristic(m: Mesh) -> int: + """ +Euler characteristic χ = V − E + F. + +Rust: `mesh::analyze::euler_characteristic` + """ + ... + +def is_manifold(m: Mesh) -> bool: + """ +True when every edge belongs to one or two faces and the faces +around every vertex form a single fan (connected through shared +edges at that vertex). + +Rust: `mesh::analyze::is_manifold` + """ + ... + +def is_closed(m: Mesh) -> bool: + """ +True when every edge belongs to exactly two faces. + +Rust: `mesh::analyze::is_closed` + """ + ... + +def is_consistently_oriented(m: Mesh) -> bool: + """ +True when every shared edge is traversed once in each direction +(faces agree on winding). + +Rust: `mesh::analyze::is_consistently_oriented` + """ + ... + +def fix_orientation(m: Mesh) -> bool: + """ +Makes the orientation consistent per connected component by BFS +flipping. Returns false (leaving a best-effort result) when the +mesh is non-orientable (e.g. a Möbius band) or an edge has more +than two faces. A closed, consistently oriented component with +negative volume is flipped outward. + +Rust: `mesh::analyze::fix_orientation` + """ + ... + +def boundary_loops(m: Mesh) -> list[list[int]]: + """ +Boundary loops as ordered vertex index cycles (each loop closed +implicitly; first vertex not repeated). + +Rust: `mesh::analyze::boundary_loops` + """ + ... + +def connected_components(m: Mesh) -> list[Mesh]: + """ +Splits into connected components (each with its own compacted +vertex list), ordered by smallest original vertex index. + +Rust: `mesh::analyze::connected_components` + """ + ... + +def non_manifold_edges(m: Mesh) -> list[tuple[int, int]]: + """ +Edges belonging to more than two faces. + +Rust: `mesh::analyze::non_manifold_edges` + """ + ... + +def duplicate_faces(m: Mesh) -> list[tuple[int, int]]: + """ +Pairs of faces with identical vertex sets (regardless of winding), +each pair reported once as `(earlier, later)`. + +Rust: `mesh::analyze::duplicate_faces` + """ + ... + +def self_intersections(m: Mesh, bvh: Optional[Bvh] = None) -> list[tuple[int, int]]: + """ +Face pairs that intersect without sharing a vertex index. Pass the +mesh's BVH (`Mesh::build_bvh`) to prune candidate pairs. + +Rust: `mesh::analyze::self_intersections` + """ + ... + +def decimate_edge_collapse(m: Mesh, target_faces: int) -> Mesh: + """ +Garland-Heckbert quadric error metric decimation ("Surface +Simplification Using Quadric Error Metrics", SIGGRAPH 1997): +greedily collapses the cheapest edge whose collapse keeps the mesh +manifold (link condition) and does not flip surviving faces, until +at most `target_faces` faces remain or no valid collapse exists. + +Rust: `mesh::analyze::decimate_edge_collapse` + """ + ... + +def vertex_valence(m: Mesh) -> list[int]: + """ +Number of distinct neighbors of each vertex. + +Rust: `mesh::analyze::vertex_valence` + """ + ... + +def dihedral_angles(m: Mesh) -> list[float]: + """ +Angle between the normals of the two faces at each interior edge +(0 for coplanar faces), aligned with `Mesh::edges` order; +boundary and non-manifold edges get 0. + +Rust: `mesh::analyze::dihedral_angles` + """ + ... + +def sharp_edges(m: Mesh, angle_threshold_rad: float) -> list[tuple[int, int]]: + """ +Interior edges whose faces' normals differ by more than the +threshold angle (radians). + +Rust: `mesh::analyze::sharp_edges` + """ + ... + +def discrete_gaussian_curvature(m: Mesh) -> list[float]: + """ +Integrated Gaussian curvature per vertex as the angle deficit: +2π − Σ incident angles (π − Σ on the boundary). Summing over a +closed mesh gives exactly 2πχ (discrete Gauss-Bonnet). + +Rust: `mesh::analyze::discrete_gaussian_curvature` + """ + ... + +def discrete_mean_curvature(m: Mesh) -> list[float]: + """ +Discrete (unsigned) mean curvature per vertex via the cotangent +Laplacian: H = |Σ (cot α + cot β)(vᵢ − vⱼ)| / (4 A) with A the +mixed Voronoi vertex area (Meyer et al. 2003). Zero on boundary +vertices. + +Rust: `mesh::analyze::discrete_mean_curvature` + """ + ... + +def geodesic_distance_dijkstra(m: Mesh, source: int) -> list[float]: + """ +Graph-shortest-path distance along mesh edges from `source` to +every vertex (an upper bound on true geodesic distance). + +Panics: +Panics when `source` is out of range. + +Rust: `mesh::analyze::geodesic_distance_dijkstra` + """ + ... + +def geodesic_distance_fast_marching(m: Mesh, source: int) -> list[float]: + """ +First-order fast marching on the triangle mesh (Kimmel & Sethian +1998): distances propagate as planar wavefronts across triangles, +converging to the true geodesic distance under refinement (unlike +edge-graph Dijkstra). + +Panics: +Panics when `source` is out of range. + +Rust: `mesh::analyze::geodesic_distance_fast_marching` + """ + ... + +def geodesic_path(m: Mesh, from_: int, to: int) -> list[int]: + """ +Shortest edge path between two vertices (inclusive); empty when +unreachable. + +Panics: +Panics when either endpoint is out of range. + +Rust: `mesh::analyze::geodesic_path` + """ + ... diff --git a/bindings/python/python/numeria/mesh/generate.pyi b/bindings/python/python/numeria/mesh/generate.pyi new file mode 100644 index 0000000..7e19fc1 --- /dev/null +++ b/bindings/python/python/numeria/mesh/generate.pyi @@ -0,0 +1,204 @@ +""" +Procedural mesh generators. Closed shapes are watertight (shared seam vertices, no duplicates) with outward-facing counterclockwise winding. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.spatial.primitives import Capsule +from numeria.spatial.primitives import Cylinder +from numeria.spatial.primitives import Polygon2 +from numeria.spatial.primitives import Polyline +from numeria.math import Vec2 +from numeria.math import Vec3 + +def uv_sphere(radius: float, segments: int, rings: int) -> Mesh: + """ +Latitude/longitude sphere: `rings` latitude bands, `segments` +meridians, poles as single vertices. Closed manifold (Euler +characteristic 2). + +Panics: +Panics unless `radius > 0`, `segments >= 3`, and `rings >= 2`. + +Rust: `mesh::generate::uv_sphere` + """ + ... + +def icosphere(radius: float, subdivisions: int) -> Mesh: + """ +Geodesic sphere: an icosahedron subdivided `subdivisions` times, +vertices projected to the sphere. Closed manifold. + +Panics: +Panics unless `radius > 0`. + +Rust: `mesh::generate::icosphere` + """ + ... + +def box_mesh(half: Vec3 | Sequence[float]) -> Mesh: + """ +Axis-aligned box with the given half extents: 8 shared vertices, 12 +triangles. Closed manifold. + +Panics: +Panics unless every half extent is positive. + +Rust: `mesh::generate::box_mesh` + """ + ... + +def cylinder(radius: float, height: float, segments: int, capped: bool) -> Mesh: + """ +Cylinder along the y axis, centered at the origin, spanning +`[-height/2, height/2]`. With `capped`, both ends are closed by +fans sharing the rim vertices (closed manifold). + +Panics: +Panics unless `radius > 0`, `height > 0`, `segments >= 3`. + +Rust: `mesh::generate::cylinder` + """ + ... + +def cone(radius: float, height: float, segments: int) -> Mesh: + """ +Cone with its base disk in the y = 0 plane (centered at the origin) +and apex at `(0, height, 0)`. Closed manifold. + +Panics: +Panics unless `radius > 0`, `height > 0`, `segments >= 3`. + +Rust: `mesh::generate::cone` + """ + ... + +def torus(major: float, minor: float, major_segs: int, minor_segs: int) -> Mesh: + """ +Torus around the y axis: tube of radius `minor` swept along a +circle of radius `major` in the xz plane. Closed manifold (Euler +characteristic 0). + +Panics: +Panics unless `0 < minor < major` and both segment counts are >= 3. + +Rust: `mesh::generate::torus` + """ + ... + +def plane_grid(width: float, depth: float, nx: int, nz: int) -> Mesh: + """ +Flat grid in the xz plane centered at the origin, normals facing ++y, `nx` by `nz` cells. Open (has a boundary). + +Panics: +Panics unless `width > 0`, `depth > 0`, `nx >= 1`, `nz >= 1`. + +Rust: `mesh::generate::plane_grid` + """ + ... + +def capsule(radius: float, height: float, segments: int, rings: int) -> Mesh: + """ +Capsule along the y axis: a cylinder of length `height` between two +hemispherical caps of the given radius (`rings` latitude bands per +hemisphere). Closed manifold. + +Panics: +Panics unless `radius > 0`, `height >= 0`, `segments >= 3`, +`rings >= 1`. + +Rust: `mesh::generate::capsule` + """ + ... + +def disk(radius: float, segments: int) -> Mesh: + """ +Flat disk in the y = 0 plane centered at the origin, normal +y. +Open (has a boundary). + +Panics: +Panics unless `radius > 0` and `segments >= 3`. + +Rust: `mesh::generate::disk` + """ + ... + +def tube_along_polyline(path: Polyline, radius: float, segments: int) -> Mesh: + """ +Tube of the given radius swept along a polyline using parallel +transport frames (rotation-minimizing, so the tube does not twist). +A closed path joins the last ring back to the first; an open path +leaves the ends uncapped. + +Panics: +Panics unless `radius > 0`, `segments >= 3`, and the path has at +least two points with nonzero consecutive tangents. + +Rust: `mesh::generate::tube_along_polyline` + """ + ... + +def extrude_polygon(poly: Polygon2, height: float) -> Mesh: + """ +Extrudes a simple polygon (in the xy plane) along +z from z = 0 to +z = `height`, with ear-clipped caps. Closed manifold for a simple +polygon. Clockwise input is treated as its counterclockwise +reversal. + +Panics: +Panics unless the polygon has at least 3 vertices and +`height > 0`. + +Rust: `mesh::generate::extrude_polygon` + """ + ... + +def revolve_profile(profile: list[Vec2 | Sequence[float]], segments: int) -> Mesh: + """ +Revolves a profile polyline around the y axis. Each profile point +`(x, y)` gives radius `x` at height `y`; points with `x == 0` +become poles. The profile should ascend in y for outward normals; +a profile that starts and ends on the axis yields a closed +manifold. + +Panics: +Panics unless the profile has >= 2 points, `segments >= 3`, and no +profile radius is negative. + +Rust: `mesh::generate::revolve_profile` + """ + ... + +def heightfield(heights: list[float], nx: int, nz: int, dx: float, dz: float) -> Mesh: + """ +Height field surface: vertex `(i, j)` sits at +`(i * dx, heights[j * nx + i], j * dz)`, triangles facing +y for +positive `dx`, `dz`. Open. + +Panics: +Panics unless `nx >= 2`, `nz >= 2`, +`heights.len() == nx * nz`, and `dx, dz > 0`. + +Rust: `mesh::generate::heightfield` + """ + ... + +def from_parametric(f: Callable[[float, float], Vec3 | Sequence[float]], u_range: tuple[float, float], v_range: tuple[float, float], nu: int, nv: int, close_u: bool, close_v: bool) -> Mesh: + """ +Samples a parametric surface on an `nu` by `nv` cell grid. +`close_u`/`close_v` wrap the respective direction (the last row of +samples is omitted and faces reference the first, so periodic +surfaces come out watertight). Triangles are wound so normals +follow `∂f/∂u × ∂f/∂v`. + +Panics: +Panics unless `nu >= 1`, `nv >= 1` (>= 3 for a closed direction) +and each range is nonempty. + +Rust: `mesh::generate::from_parametric` + """ + ... diff --git a/bindings/python/python/numeria/mesh/isosurface.pyi b/bindings/python/python/numeria/mesh/isosurface.pyi new file mode 100644 index 0000000..03b459b --- /dev/null +++ b/bindings/python/python/numeria/mesh/isosurface.pyi @@ -0,0 +1,153 @@ +""" +Isosurface and isocontour extraction from sampled scalar fields: marching squares/cubes/tetrahedra, surface nets, dual contouring, and metaballs. Convention: a sample is "inside" when its value is below the iso level (matching signed distance fields, negative inside). Output triangles are wound counterclockwise seen from the outside (normals point toward values above the iso level); 2-D contours keep the inside region on their left, so they run counterclockwise around regions below the iso level. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.spatial.primitives import Aabb +from numeria.core.dual import Dual +from numeria.spatial.primitives import Rect +from numeria.math import Vec2 +from numeria.math import Vec3 + +class ScalarField2: + """ +Scalar samples on a regular 2-D grid (`width` x `height` samples, +x-fastest layout: `data[j * width + i]`). + +Rust: `mesh::isosurface::ScalarField2` + """ + def __init__(self, width: int, height: int, data: list[float], bounds: Rect) -> None: ... + @staticmethod + def from_fn(bounds: Rect, width: int, height: int, f: Callable[[Vec2 | Sequence[float]], float]) -> ScalarField2: ... + def get(self, i: int, j: int) -> float: ... + def position(self, i: int, j: int) -> Vec2: ... + @property + def width(self) -> int: ... + @property + def height(self) -> int: ... + @property + def data(self) -> list[float]: ... + @property + def bounds(self) -> Rect: ... + +class ScalarField3: + """ +Scalar samples on a regular 3-D grid (`nx` x `ny` x `nz` samples, +x-fastest layout: `data[(k * ny + j) * nx + i]`). + +Rust: `mesh::isosurface::ScalarField3` + """ + def __init__(self, nx: int, ny: int, nz: int, data: list[float], bounds: Aabb) -> None: ... + @staticmethod + def from_fn(bounds: Aabb, nx: int, ny: int, nz: int, f: Callable[[Vec3 | Sequence[float]], float]) -> ScalarField3: ... + @staticmethod + def from_sdf(bounds: Aabb, nx: int, ny: int, nz: int, sdf: Callable[[Vec3 | Sequence[float]], float]) -> ScalarField3: ... + def get(self, i: int, j: int, k: int) -> float: ... + def position(self, i: int, j: int, k: int) -> Vec3: ... + def sample_trilinear(self, p: Vec3 | Sequence[float]) -> float: ... + def gradient(self, i: int, j: int, k: int) -> Vec3: ... + @property + def nx(self) -> int: ... + @property + def ny(self) -> int: ... + @property + def nz(self) -> int: ... + @property + def data(self) -> list[float]: ... + @property + def bounds(self) -> Aabb: ... + +def marching_squares(field: ScalarField2, iso: float) -> list[Segment2]: + """ +All isocontour crossings as directed segments (inside on the left). + +Rust: `mesh::isosurface::marching_squares` + """ + ... + +def marching_squares_polylines(field: ScalarField2, iso: float) -> list[list[Vec2]]: + """ +Isocontours joined into polylines. Closed loops repeat their first +point at the end; open chains (which begin and end on the grid +boundary) do not. Inside (< iso) lies on the left of the direction +of travel. + +Rust: `mesh::isosurface::marching_squares_polylines` + """ + ... + +def contour_levels(field: ScalarField2, levels: list[float]) -> list[tuple[float, list[list[Vec2]]]]: + """ +Joined contours for each requested level. + +Rust: `mesh::isosurface::contour_levels` + """ + ... + +def marching_cubes(field: ScalarField3, iso: float) -> Mesh: + """ +Extracts the isosurface by marching cubes (Lorensen & Cline 1987; +the case table is generated by face-consistent cycle construction, +see `mc_table`). Output vertices are shared across cells (keyed +by grid edge), so the mesh is watertight wherever the surface does +not leave the grid. + +Rust: `mesh::isosurface::marching_cubes` + """ + ... + +def marching_tetrahedra(field: ScalarField3, iso: float) -> Mesh: + """ +Extracts the isosurface by marching tetrahedra over the Freudenthal +(Kuhn) 6-tetrahedra decomposition of each cell. The decomposition +cuts every cell face along its min-to-max diagonal, which depends +only on global grid coordinates, so adjacent cells agree and the +output is watertight. No ambiguous cases exist; the surface is +finer (more triangles) than marching cubes for the same grid. + +Rust: `mesh::isosurface::marching_tetrahedra` + """ + ... + +def surface_nets(field: ScalarField3, iso: float) -> Mesh: + """ +Naive surface nets (Gibson 1998): each crossed cell gets the +centroid of its edge crossings; quads connect cells around crossed +interior grid edges. Smoother than marching cubes at the same +resolution but not guaranteed to stay inside each cell. + +Rust: `mesh::isosurface::surface_nets` + """ + ... + +def dual_contouring(field: ScalarField3, iso: float, normals: Callable[[Vec3 | Sequence[float]], Vec3 | Sequence[float]]) -> Mesh: + """ +Dual contouring (Ju et al. 2002): like surface nets, but each cell +vertex minimizes the quadratic error function +Σ (nᵢ · (x − pᵢ))² over the cell's edge crossings, with normals +supplied by `normals` (e.g. an SDF gradient). Reproduces sharp +features. The QEF is solved by regularized normal equations +(Gaussian elimination with partial pivoting), and the vertex is +clamped into its cell. + +Rust: `mesh::isosurface::dual_contouring` + """ + ... + +def metaballs(centers: list[tuple[Vec3 | Sequence[float], float]], bounds: Aabb, res: int, threshold: float) -> Mesh: + """ +Polygonizes a metaball (blobby) surface: field +Σ rᵢ² / |p − cᵢ|² compared against `threshold`, marched on a +`res`³ grid over `bounds`. Larger `threshold` shrinks the blobs. + +Panics: +Panics unless `threshold > 0`, `res >= 2`, and `centers` is +nonempty. + +Rust: `mesh::isosurface::metaballs` + """ + ... diff --git a/bindings/python/python/numeria/mesh/parameterize.pyi b/bindings/python/python/numeria/mesh/parameterize.pyi new file mode 100644 index 0000000..551840f --- /dev/null +++ b/bindings/python/python/numeria/mesh/parameterize.pyi @@ -0,0 +1,115 @@ +""" +Mesh parameterization: closed-form spherical/planar/cylindrical projections, harmonic (cotangent-Laplace) disk parameterization with fixed boundaries, least-squares conformal maps (Lévy, Petitjean, Ray & Maillot 2002), and per-triangle conformal and area distortion measures. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.math import Vec2 +from numeria.math import Vec3 + +class BoundaryShape: + """ +Target shape for the fixed boundary of a harmonic +parameterization. + +Rust: `mesh::parameterize::BoundaryShape` + """ + ... + +def spherical_uv(m: Mesh) -> None: + """ +Spherical texture coordinates for a (genus-0) mesh: each vertex +direction from the centroid maps to +u = ½ + atan2(d_y, d_x)/2π, v = acos(d_z)/π. Writes `m.uvs`. + +Panics: +Panics on an empty mesh. + +Rust: `mesh::parameterize::spherical_uv` + """ + ... + +def planar_uv(m: Mesh, normal: Vec3 | Sequence[float]) -> None: + """ +Planar projection along `normal`, normalized so the projected +bounding box spans [0, 1]². Writes `m.uvs`. + +Panics: +Panics on an empty mesh or a zero normal. + +Rust: `mesh::parameterize::planar_uv` + """ + ... + +def cylindrical_uv(m: Mesh, axis: Vec3 | Sequence[float]) -> None: + """ +Cylindrical projection about `axis` through the centroid: +u = angle/2π around the axis, v = normalized height along it. +Writes `m.uvs`. + +Panics: +Panics on an empty mesh or a zero axis. + +Rust: `mesh::parameterize::cylindrical_uv` + """ + ... + +def harmonic_parameterization(m: Mesh, boundary_shape: BoundaryShape) -> list[Vec2]: + """ +Harmonic (cotangent-weight) parameterization of a disk-topology +mesh: the boundary loop is pinned to the target shape and the +interior solves the discrete Laplace equation, giving the unique +harmonic extension (identity up to similarity on flat meshes; +convexity of the target keeps the map injective for meshes with +non-negative weights). Returns one UV per vertex. + +Panics: +Panics unless the mesh has disk topology and the linear solve +converges. + +Rust: `mesh::parameterize::harmonic_parameterization` + """ + ... + +def lscm(m: Mesh, pinned: list[tuple[int, Vec2 | Sequence[float]]]) -> list[Vec2]: + """ +Least-squares conformal map: minimizes the conformal energy +Σ_T A_T |∇u rotated 90° − ∇v|² with two pinned vertices removing +the similarity ambiguity. Solved through the normal equations by +conjugate gradients. Returns one UV per vertex. + +Panics: +Panics unless the two pins are distinct valid vertices and the +solve converges. + +Rust: `mesh::parameterize::lscm` + """ + ... + +def conformal_distortion(m: Mesh, uv: list[Vec2 | Sequence[float]]) -> list[float]: + """ +Per-triangle conformal distortion σ₁/σ₂ of the parameterization +(1 = angle-preserving; degenerate triangles report 1). + +Panics: +Panics unless `uv` has one entry per vertex. + +Rust: `mesh::parameterize::conformal_distortion` + """ + ... + +def area_distortion(m: Mesh, uv: list[Vec2 | Sequence[float]]) -> list[float]: + """ +Per-triangle area distortion: the UV/3-D area ratio normalized by +the global ratio, so a globally scaled isometry reports 1 +everywhere. + +Panics: +Panics unless `uv` has one entry per vertex. + +Rust: `mesh::parameterize::area_distortion` + """ + ... diff --git a/bindings/python/python/numeria/mesh/subdivide.pyi b/bindings/python/python/numeria/mesh/subdivide.pyi new file mode 100644 index 0000000..f843dca --- /dev/null +++ b/bindings/python/python/numeria/mesh/subdivide.pyi @@ -0,0 +1,115 @@ +""" +Subdivision surfaces (Loop, Catmull-Clark, sqrt(3), midpoint) and Laplacian-family smoothing. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.math import Vec3 + +class QuadMesh: + """ +Quadrilateral mesh (faces as counterclockwise vertex quadruples). + +Rust: `mesh::subdivide::QuadMesh` + """ + def __init__(self, vertices: list[Vec3 | Sequence[float]], quads: list[list[int]]) -> None: ... + def to_triangles(self) -> Mesh: ... + @staticmethod + def from_box(half: Vec3 | Sequence[float]) -> QuadMesh: ... + @staticmethod + def from_grid(width: float, depth: float, nx: int, nz: int) -> QuadMesh: ... + @property + def vertices(self) -> list[Vec3]: ... + @property + def quads(self) -> list[list[int]]: ... + +def loop_subdivide(m: Mesh) -> Mesh: + """ +One level of Loop subdivision (Loop 1987): each triangle splits +into four; new edge vertices use the 3/8-1/8 stencil, old vertices +the valence-dependent β stencil, with the standard boundary rules +(midpoint and 3/4-1/8-1/8). + +Rust: `mesh::subdivide::loop_subdivide` + """ + ... + +def loop_subdivide_n(m: Mesh, n: int) -> Mesh: + """ +`n` levels of Loop subdivision. + +Rust: `mesh::subdivide::loop_subdivide_n` + """ + ... + +def catmull_clark(q: QuadMesh) -> QuadMesh: + """ +One level of Catmull-Clark subdivision (Catmull & Clark 1978): +face points at centroids, edge points averaging endpoints and face +points, old vertices moved by (F + 2R + (n-3)P)/n, standard +boundary rules. Each quad becomes four. + +Rust: `mesh::subdivide::catmull_clark` + """ + ... + +def catmull_clark_n(q: QuadMesh, n: int) -> QuadMesh: + """ +`n` levels of Catmull-Clark. + +Rust: `mesh::subdivide::catmull_clark_n` + """ + ... + +def sqrt3_subdivide(m: Mesh) -> Mesh: + """ +One level of sqrt(3) subdivision (Kobbelt 2000): a centroid vertex +per face, original interior edges flipped, old vertices smoothed by +the α_n stencil (boundary vertices stay). + +Rust: `mesh::subdivide::sqrt3_subdivide` + """ + ... + +def midpoint_subdivide(m: Mesh) -> Mesh: + """ +One 1-to-4 split at edge midpoints with no repositioning: geometry +is unchanged (flat faces stay flat). + +Rust: `mesh::subdivide::midpoint_subdivide` + """ + ... + +def laplacian_smooth(m: Mesh, iterations: int, lambda_: float) -> None: + """ +Uniform-weight Laplacian smoothing: each iteration moves every +vertex by `lambda` toward the average of its neighbors. Shrinks +closed meshes. + +Rust: `mesh::subdivide::laplacian_smooth` + """ + ... + +def taubin_smooth(m: Mesh, iterations: int, lambda_: float, mu: float) -> None: + """ +Taubin's λ|μ smoothing (Taubin 1995): alternating positive +(`lambda`) and negative (`mu`, with `mu < -lambda` typically) +steps smooth without significant shrinkage. + +Rust: `mesh::subdivide::taubin_smooth` + """ + ... + +def hc_laplacian_smooth(m: Mesh, iterations: int, alpha: float, beta: float) -> None: + """ +HC-Laplacian smoothing (Vollmer, Mencl & Müller 1999): a Laplacian +step followed by a correction that pushes points back toward a +blend of their original (`alpha`) and previous positions, the +correction itself averaged over neighbors (`beta`). + +Rust: `mesh::subdivide::hc_laplacian_smooth` + """ + ... diff --git a/bindings/python/python/numeria/mesh/surfaces.pyi b/bindings/python/python/numeria/mesh/surfaces.pyi new file mode 100644 index 0000000..f31733d --- /dev/null +++ b/bindings/python/python/numeria/mesh/surfaces.pyi @@ -0,0 +1,278 @@ +""" +Parametric surfaces: Bézier/B-spline/NURBS patches, classic surface constructions, differential geometry via fundamental forms, and a catalogue of named surfaces. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.math import Vec2 +from numeria.math import Vec3 + +class BSplineSurface: + """ +Tensor-product B-spline surface. Parameters range over +`[knots[degree], knots[len - degree - 1]]` in each direction. + +Rust: `mesh::surfaces::BSplineSurface` + """ + def __init__(self, degree_u: int, degree_v: int, knots_u: list[float], knots_v: list[float], control: list[list[Vec3 | Sequence[float]]]) -> None: ... + @staticmethod + def uniform(degree_u: int, degree_v: int, control: list[list[Vec3 | Sequence[float]]]) -> BSplineSurface: ... + def eval(self, u: float, v: float) -> Vec3: ... + def normal(self, u: float, v: float) -> Vec3: ... + def to_mesh(self, nu: int, nv: int) -> Mesh: ... + @property + def degree_u(self) -> int: ... + @property + def degree_v(self) -> int: ... + @property + def knots_u(self) -> list[float]: ... + @property + def knots_v(self) -> list[float]: ... + @property + def control(self) -> list[list[Vec3]]: ... + +class BezierPatch: + """ +Bicubic Bézier patch; `control[i][j]` weights the Bernstein product +B_i(u) B_j(v). + +Rust: `mesh::surfaces::BezierPatch` + """ + def eval(self, u: float, v: float) -> Vec3: ... + def du(self, u: float, v: float) -> Vec3: ... + def dv(self, u: float, v: float) -> Vec3: ... + def normal(self, u: float, v: float) -> Vec3: ... + def to_mesh(self, nu: int, nv: int) -> Mesh: ... + def subdivide(self) -> list[BezierPatch]: ... + @property + def control(self) -> list[list[Vec3]]: ... + +class FundamentalForms: + """ +First (E, F, G) and second (L, M, N) fundamental form coefficients +of a parametric surface. + +Rust: `mesh::surfaces::FundamentalForms` + """ + def __init__(self, e: float, f: float, g: float, l: float, m: float, n: float) -> None: ... + @property + def e(self) -> float: ... + @property + def f(self) -> float: ... + @property + def g(self) -> float: ... + @property + def l(self) -> float: ... + @property + def m(self) -> float: ... + @property + def n(self) -> float: ... + +class NurbsSurface: + """ +Tensor-product NURBS surface (rational B-spline): projective +weights allow exact conics. + +Rust: `mesh::surfaces::NurbsSurface` + """ + def __init__(self, degree_u: int, degree_v: int, knots_u: list[float], knots_v: list[float], control: list[list[Vec3 | Sequence[float]]], weights: list[list[float]]) -> None: ... + def eval(self, u: float, v: float) -> Vec3: ... + def to_mesh(self, nu: int, nv: int) -> Mesh: ... + @staticmethod + def sphere(r: float) -> NurbsSurface: ... + @staticmethod + def torus(big_r: float, r: float) -> NurbsSurface: ... + @staticmethod + def cylinder(r: float, h: float) -> NurbsSurface: ... + @property + def degree_u(self) -> int: ... + @property + def degree_v(self) -> int: ... + @property + def knots_u(self) -> list[float]: ... + @property + def knots_v(self) -> list[float]: ... + @property + def control(self) -> list[list[Vec3]]: ... + @property + def weights(self) -> list[list[float]]: ... + +def surface_of_revolution(profile: Callable[[float], Vec2 | Sequence[float]], t: float, theta: float) -> Vec3: + """ +Point of the surface of revolution of a planar profile +`t ↦ (radius, height)` rotated by `theta` around the y axis. + +Rust: `mesh::surfaces::surface_of_revolution` + """ + ... + +def ruled_surface(c1: Callable[[float], Vec3 | Sequence[float]], c2: Callable[[float], Vec3 | Sequence[float]], u: float, v: float) -> Vec3: + """ +Ruled surface: linear blend between two curves, +`(1 − v) c1(u) + v c2(u)`. + +Rust: `mesh::surfaces::ruled_surface` + """ + ... + +def coons_patch(c0: Callable[[float], Vec3 | Sequence[float]], c1: Callable[[float], Vec3 | Sequence[float]], d0: Callable[[float], Vec3 | Sequence[float]], d1: Callable[[float], Vec3 | Sequence[float]], u: float, v: float) -> Vec3: + """ +Bilinearly blended Coons patch interpolating four boundary curves: +`c0` (v = 0), `c1` (v = 1) over u, and `d0` (u = 0), `d1` (u = 1) +over v. The curves must agree at the corners. + +Rust: `mesh::surfaces::coons_patch` + """ + ... + +def fundamental_forms(f: Callable[[float, float], Vec3 | Sequence[float]], u: float, v: float, h: float) -> FundamentalForms: + """ +Fundamental forms by central finite differences with step `h`. + +Panics: +Panics unless `h > 0` and the surface is regular (nonzero +`f_u × f_v`) at `(u, v)`. + +Rust: `mesh::surfaces::fundamental_forms` + """ + ... + +def gaussian_curvature(forms: FundamentalForms | Sequence[float]) -> float: + """ +Gaussian curvature K = (LN − M²) / (EG − F²). + +Rust: `mesh::surfaces::gaussian_curvature` + """ + ... + +def mean_curvature(forms: FundamentalForms | Sequence[float]) -> float: + """ +Mean curvature H = (EN − 2FM + GL) / (2 (EG − F²)). + +Rust: `mesh::surfaces::mean_curvature` + """ + ... + +def principal_curvatures(forms: FundamentalForms | Sequence[float]) -> tuple[float, float]: + """ +Principal curvatures `(κ₁, κ₂)` with κ₁ ≥ κ₂: +H ± sqrt(H² − K). + +Rust: `mesh::surfaces::principal_curvatures` + """ + ... + +def surface_area_parametric(f: Callable[[float, float], Vec3 | Sequence[float]], u_range: tuple[float, float], v_range: tuple[float, float], nu: int, nv: int) -> float: + """ +Surface area of a parametric patch by the midpoint rule on +`nu` x `nv` cells: Σ |f_u × f_v| du dv, partials by central +differences at each cell midpoint. + +Panics: +Panics unless `nu >= 1` and `nv >= 1`. + +Rust: `mesh::surfaces::surface_area_parametric` + """ + ... + +def mobius_strip(u: float, v: float, r: float, w: float) -> Vec3: + """ +Möbius strip of center radius `r` and half-width `w`: +u ∈ [0, 2π) around, v ∈ [−1, 1] across. + +Rust: `mesh::surfaces::mobius_strip` + """ + ... + +def klein_bottle(u: float, v: float) -> Vec3: + """ +Figure-8 immersion of the Klein bottle, u, v ∈ [0, 2π). + +Rust: `mesh::surfaces::klein_bottle` + """ + ... + +def enneper(u: float, v: float) -> Vec3: + """ +Enneper's minimal surface. + +Rust: `mesh::surfaces::enneper` + """ + ... + +def catenoid(u: float, v: float, c: float) -> Vec3: + """ +Catenoid (minimal): u around, v along the axis, waist radius `c`. + +Rust: `mesh::surfaces::catenoid` + """ + ... + +def helicoid(u: float, v: float, c: float) -> Vec3: + """ +Helicoid (minimal): pitch parameter `c`. + +Rust: `mesh::surfaces::helicoid` + """ + ... + +def monkey_saddle(u: float, v: float) -> Vec3: + """ +Monkey saddle z = u³ − 3uv². + +Rust: `mesh::surfaces::monkey_saddle` + """ + ... + +def dini(u: float, v: float, a: float, b: float) -> Vec3: + """ +Dini's surface (constant negative curvature −1/(a² + b²)): +a twisted pseudosphere. v ∈ (0, π). + +Rust: `mesh::surfaces::dini` + """ + ... + +def boy_surface(u: float, v: float) -> Vec3: + """ +Boy's surface (Apéry parametrization of the real projective +plane), u ∈ [−π/2, π/2], v ∈ [0, π/2]. + +Rust: `mesh::surfaces::boy_surface` + """ + ... + +def superellipsoid(u: float, v: float, a: Vec3 | Sequence[float], e1: float, e2: float) -> Vec3: + """ +Superellipsoid with semi-axes `a` and exponents `e1` (latitude), +`e2` (longitude): u = longitude ∈ [−π, π], v = latitude ∈ +[−π/2, π/2]. + +Rust: `mesh::surfaces::superellipsoid` + """ + ... + +def supershape_2d(theta: float, m: float, n1: float, n2: float, n3: float, a: float, b: float) -> Vec2: + """ +Gielis superformula in the plane: +r(θ) = (|cos(mθ/4)/a|^n2 + |sin(mθ/4)/b|^n3)^(−1/n1). + +Panics: +Panics unless `a, b > 0` and `n1 != 0`. + +Rust: `mesh::surfaces::supershape_2d` + """ + ... + +def supershape_3d(theta: float, phi: float, params: list[float]) -> Vec3: + """ +3-D supershape: the spherical product of two superformulas, +`params = [m1, n11, n12, n13, a1, b1, m2, n21, n22, n23, a2, b2]` +with θ = longitude ∈ [−π, π], φ = latitude ∈ [−π/2, π/2]. + +Rust: `mesh::surfaces::supershape_3d` + """ + ... diff --git a/bindings/python/python/numeria/monte_carlo/__init__.pyi b/bindings/python/python/numeria/monte_carlo/__init__.pyi new file mode 100644 index 0000000..1776ae9 --- /dev/null +++ b/bindings/python/python/numeria/monte_carlo/__init__.pyi @@ -0,0 +1,129 @@ +""" +Monte Carlo methods and the random number generator behind them. Integration (plain, 2-D, and importance-sampled), random walks in one to three dimensions, the Wiener and Ornstein-Uhlenbeck processes, Langevin dynamics, and Metropolis-Hastings sampling with a worked Ising example. # A warning about `Rng` It is a linear congruential generator that returns its raw state, so the low bits have a short period: `next_u64() % m` for a power-of-two `m` cycles through a handful of values -- `% 2` gives 0,1,0,1 and `% 4` gives 0,3,2,1 forever. Use `Rng::below`, which takes the high bits, for any small-integer draw. It is adequate for simulation and testing and is not cryptographically secure. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from . import quasi +from numeria.monte_carlo.quasi import Halton as Halton +from numeria.monte_carlo.quasi import Sobol as Sobol +from numeria.monte_carlo.quasi import mc_integrate_sobol as mc_integrate_sobol + +class Rng: + """ +Linear congruential pseudo-random number generator. + +Rust: `monte_carlo::Rng` + """ + def __init__(self, seed: int) -> None: ... + def next_u64(self) -> int: ... + def next_f64(self) -> float: ... + def below(self, n: int) -> int: ... + def next_gaussian(self) -> float: ... + +def mc_integrate_1d(f: Callable[[float], float], a: float, b: float, n: int, rng: Rng) -> float: + """ +Monte Carlo integration of a 1D function over [a, b] using uniform random sampling. + +Rust: `monte_carlo::mc_integrate_1d` + """ + ... + +def mc_integrate_2d(f: Callable[[float, float], float], x_range: tuple[float, float], y_range: tuple[float, float], n: int, rng: Rng) -> float: + """ +Monte Carlo integration of a 2D function over a rectangular domain using uniform random sampling. + +Rust: `monte_carlo::mc_integrate_2d` + """ + ... + +def mc_estimate_pi(n: int, rng: Rng) -> float: + """ +Estimates pi by uniform random sampling inside the unit square: π ≈ 4 × (points inside unit circle) / N. + +Rust: `monte_carlo::mc_estimate_pi` + """ + ... + +def random_walk_1d(steps: int, step_size: float, rng: Rng) -> list[float]: + """ +Simulates a 1D symmetric random walk with equal probability of stepping left or right. + +Rust: `monte_carlo::random_walk_1d` + """ + ... + +def random_walk_2d(steps: int, step_size: float, rng: Rng) -> list[tuple[float, float]]: + """ +Simulates a 2D random walk with uniformly distributed step direction in [0, 2π). + +Rust: `monte_carlo::random_walk_2d` + """ + ... + +def random_walk_3d(steps: int, step_size: float, rng: Rng) -> list[tuple[float, float, float]]: + """ +Simulates a 3D random walk with uniformly distributed direction via Marsaglia's Gaussian method. + +Rust: `monte_carlo::random_walk_3d` + """ + ... + +def wiener_process(n_steps: int, dt: float, rng: Rng) -> list[float]: + """ +Generates a discretized Wiener process (Brownian motion): W(t+dt) = W(t) + √dt × N(0,1). + +Rust: `monte_carlo::wiener_process` + """ + ... + +def ornstein_uhlenbeck(n_steps: int, dt: float, theta: float, mu: float, sigma: float, x0: float, rng: Rng) -> list[float]: + """ +Simulates an Ornstein-Uhlenbeck process: dx = θ(μ - x)dt + σdW. + +Rust: `monte_carlo::ornstein_uhlenbeck` + """ + ... + +def langevin_step(x: float, v: float, force: float, mass: float, gamma: float, temperature: float, dt: float, rng: Rng) -> tuple[float, float]: + """ +Performs one Langevin dynamics step with thermal noise: dv = (F/m - γv)dt + √(2γk_BT/m) dW. + +Rust: `monte_carlo::langevin_step` + """ + ... + +def metropolis_step(energy_current: float, energy_proposed: float, temperature: float, rng: Rng) -> bool: + """ +Metropolis acceptance criterion: accepts if ΔE < 0, otherwise accepts with probability exp(-ΔE / k_BT). + +Rust: `monte_carlo::metropolis_step` + """ + ... + +def ising_energy_1d(spins: list[int], j_coupling: float, h_field: float) -> float: + """ +Computes the 1D Ising model energy: E = -J Σ s_i s_{i+1} - H Σ s_i. + +Rust: `monte_carlo::ising_energy_1d` + """ + ... + +def ising_magnetization(spins: list[int]) -> float: + """ +Computes the mean magnetization of a spin configuration: M = (Σ s_i) / N. + +Rust: `monte_carlo::ising_magnetization` + """ + ... + +def ising_step_1d(spins: MutableSequence[int], j_coupling: float, h_field: float, temperature: float, rng: Rng) -> None: + """ +Performs one Metropolis single-spin-flip update on a 1D Ising model. + +Rust: `monte_carlo::ising_step_1d` + """ + ... diff --git a/bindings/python/python/numeria/monte_carlo/quasi.pyi b/bindings/python/python/numeria/monte_carlo/quasi.pyi new file mode 100644 index 0000000..293bc6e --- /dev/null +++ b/bindings/python/python/numeria/monte_carlo/quasi.pyi @@ -0,0 +1,45 @@ +""" +Quasi-random (low-discrepancy) sequences: Sobol and Halton. Sobol points use the Gray-code construction of Bratley & Fox with the Joe-Kuo (new-joe-kuo-6) primitive polynomials and initial direction numbers, embedded here for dimensions up to 21. Halton points use the radical inverse in the first `dim` primes. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +class Halton: + """ +Halton low-discrepancy sequence: dimension d uses the radical +inverse in the d-th prime. + +Rust: `monte_carlo::quasi::Halton` + """ + def __init__(self, dim: int) -> None: ... + def next(self) -> list[float]: ... + +class Sobol: + """ +Sobol low-discrepancy sequence in `dim` dimensions (dim ≤ 21). + +Successive calls to `Sobol::next` return points x₁, x₂, … in +[0, 1)^dim (the origin point x₀ = 0 is skipped). + +Rust: `monte_carlo::quasi::Sobol` + """ + def __init__(self, dim: int) -> None: ... + def next(self) -> list[float]: ... + def skip(self, n: int) -> None: ... + def dim(self) -> int: ... + +def mc_integrate_sobol(f: Callable[[list[float]], float], dim: int, n: int) -> float: + """ +Quasi-Monte Carlo integration of f over [0, 1]^dim with the first n +Sobol points. + +Panics: +Panics unless n > 0 and dim is Sobol-supported. + +Rust: `monte_carlo::quasi::mc_integrate_sobol` + """ + ... diff --git a/bindings/python/python/numeria/neutronics.pyi b/bindings/python/python/numeria/neutronics.pyi new file mode 100644 index 0000000..428c019 --- /dev/null +++ b/bindings/python/python/numeria/neutronics.pyi @@ -0,0 +1,189 @@ +""" +Reactor physics: criticality, neutron diffusion, and shielding. Criticality through the six-factor formula and `k_eff`, with reactivity and the reactor period. Neutron transport in diffusion theory: the diffusion coefficient and length, migration length, thermal utilization, and the flux in a slab. Cross sections and reaction rates convert between microscopic and macroscopic pictures, including the 1/v absorption law. Operations covers reactor power, burnup and decay heat. Shielding closes with attenuation, half- and tenth-value layers, and the buildup factor that corrects the exponential law for scattered photons -- the correction that matters, since ignoring it underestimates the dose behind a thick shield. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def k_effective(production_rate: float, loss_rate: float) -> float: + """ +Effective multiplication factor: k_eff = production_rate / loss_rate + +Rust: `neutronics::k_effective` + """ + ... + +def reactivity(k_eff: float) -> float: + """ +Reactivity: ρ = (k - 1) / k + +Rust: `neutronics::reactivity` + """ + ... + +def doubling_time(k_eff: float, neutron_lifetime: float) -> float: + """ +Doubling time for supercritical reactor: T = l × ln(2) / (k - 1) +Returns f64::INFINITY if k_eff <= 1.0 (not supercritical). + +Rust: `neutronics::doubling_time` + """ + ... + +def six_factor_formula(eta: float, f: float, p: float, epsilon: float, p_fnl: float, p_tnl: float) -> float: + """ +Six-factor formula: k_eff = η × f × p × ε × P_FNL × P_TNL + +Rust: `neutronics::six_factor_formula` + """ + ... + +def reproduction_factor(nu: float, sigma_f: float, sigma_a: float) -> float: + """ +Reproduction factor: η = ν × σ_f / σ_a + +Rust: `neutronics::reproduction_factor` + """ + ... + +def diffusion_coefficient(transport_mfp: float) -> float: + """ +Diffusion coefficient: D = λ_tr / 3 + +Rust: `neutronics::diffusion_coefficient` + """ + ... + +def diffusion_length(diffusion_coeff: float, absorption_xs: float) -> float: + """ +Diffusion length: L = √(D / Σ_a) + +Rust: `neutronics::diffusion_length` + """ + ... + +def migration_length(diffusion_length: float, slowing_down_length: float) -> float: + """ +Migration length: M = √(L² + τ) where τ is the Fermi age (slowing-down area) + +Rust: `neutronics::migration_length` + """ + ... + +def thermal_utilization(sigma_a_fuel: float, sigma_a_total: float) -> float: + """ +Thermal utilization factor: f = Σ_a_fuel / Σ_a_total + +Rust: `neutronics::thermal_utilization` + """ + ... + +def neutron_flux_slab(source: float, diffusion_coeff: float, sigma_a: float, x: float, half_thickness: float) -> float: + """ +Neutron flux in a slab reactor with uniform source: +φ(x) = (S / Σ_a) × (1 - cosh(x/L) / cosh(a/L)) +where L = √(D/Σ_a) and a = half-thickness (extrapolated). + +Rust: `neutronics::neutron_flux_slab` + """ + ... + +def microscopic_to_macroscopic(micro_xs: float, number_density: float) -> float: + """ +Macroscopic cross section from microscopic: Σ = N × σ + +Rust: `neutronics::microscopic_to_macroscopic` + """ + ... + +def number_density(density: float, molar_mass: float) -> float: + """ +Number density from bulk density and molar mass: N = ρ × N_A / M + +Rust: `neutronics::number_density` + """ + ... + +def mean_free_path_neutron(macro_xs: float) -> float: + """ +Mean free path for neutrons: λ = 1 / Σ + +Rust: `neutronics::mean_free_path_neutron` + """ + ... + +def reaction_rate_neutron(macro_xs: float, flux: float) -> float: + """ +Neutron reaction rate: R = Σ × φ + +Rust: `neutronics::reaction_rate_neutron` + """ + ... + +def one_over_v_xs(sigma_0: float, e_0: float, energy: float) -> float: + """ +1/v cross section law for thermal neutrons: σ(E) = σ₀ × √(E₀ / E) + +Rust: `neutronics::one_over_v_xs` + """ + ... + +def reactor_power(fission_rate: float, energy_per_fission: float) -> float: + """ +Reactor thermal power: P = R_f × E_f + +Rust: `neutronics::reactor_power` + """ + ... + +def burnup(power: float, time: float, mass_heavy_metal: float) -> float: + """ +Burnup: BU = P × t / M (MWd/kg when units are consistent) + +Rust: `neutronics::burnup` + """ + ... + +def decay_heat_fraction(time_after_shutdown: float) -> float: + """ +Decay heat fraction (Way-Wigner approximation for long prior operation): +P/P₀ ≈ 0.066 × t^(-0.2) + +Rust: `neutronics::decay_heat_fraction` + """ + ... + +def transmission_factor(macro_xs: float, thickness: float) -> float: + """ +Transmission factor (uncollided): I/I₀ = e^(-Σx) + +Rust: `neutronics::transmission_factor` + """ + ... + +def half_value_layer(macro_xs: float) -> float: + """ +Half-value layer: HVL = ln(2) / Σ + +Rust: `neutronics::half_value_layer` + """ + ... + +def tenth_value_layer(macro_xs: float) -> float: + """ +Tenth-value layer: TVL = ln(10) / Σ + +Rust: `neutronics::tenth_value_layer` + """ + ... + +def buildup_factor_approx(macro_xs: float, thickness: float) -> float: + """ +Linear buildup factor approximation for thin shields: B ≈ 1 + Σx + +Rust: `neutronics::buildup_factor_approx` + """ + ... diff --git a/bindings/python/python/numeria/nonlinear.pyi b/bindings/python/python/numeria/nonlinear.pyi new file mode 100644 index 0000000..dbd4337 --- /dev/null +++ b/bindings/python/python/numeria/nonlinear.pyi @@ -0,0 +1,127 @@ +""" +Chaos in low-dimensional systems. The logistic map and its period-doubling route to chaos, the Hénon map, and the Lorenz and Rössler flows given as derivative functions to hand to an integrator from `numerical`. Lyapunov exponents are the quantitative test: a positive exponent means nearby trajectories separate exponentially, which is what makes a system chaotic rather than merely complicated. Dimension estimators -- box counting and the correlation dimension -- measure the attractor that results. For strange attractors as drawable objects, escape-time fractals and cellular automata see `fractals`. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def logistic_map(r: float, x: float) -> float: + """ +Chaos in low-dimensional systems. + +The logistic map and its period-doubling route to chaos, the Hénon +map, and the Lorenz and Rössler flows given as derivative functions to +hand to an integrator from `numerical`. + +Lyapunov exponents are the quantitative test: a positive exponent means +nearby trajectories separate exponentially, which is what makes a +system chaotic rather than merely complicated. Dimension estimators -- +box counting and the correlation dimension -- measure the attractor +that results. + +For strange attractors as drawable objects, escape-time fractals and +cellular automata see `fractals`. +Computes one iteration of the logistic map: x_{n+1} = r × x × (1 - x). + +Rust: `nonlinear::logistic_map` + """ + ... + +def logistic_map_iterate(r: float, x0: float, n: int) -> list[float]: + """ +Iterates the logistic map n times from x0, returning the full trajectory. + +Rust: `nonlinear::logistic_map_iterate` + """ + ... + +def logistic_map_converge(r: float, x0: float, transient: int, samples: int) -> list[float]: + """ +Discards an initial transient, then collects post-transient samples from the logistic map. + +Rust: `nonlinear::logistic_map_converge` + """ + ... + +def lyapunov_exponent_logistic(r: float, x0: float, n: int) -> float: + """ +Computes the Lyapunov exponent of the logistic map: λ = (1/N) Σ ln|r(1 - 2x_n)|. + +Rust: `nonlinear::lyapunov_exponent_logistic` + """ + ... + +def lyapunov_exponent_1d(f: Callable[[float], float], df: Callable[[float], float], x0: float, n: int) -> float: + """ +Computes the Lyapunov exponent of a general 1D map: λ = (1/N) Σ ln|f'(x_n)|. + +Rust: `nonlinear::lyapunov_exponent_1d` + """ + ... + +def lorenz_derivatives(state: list[float], sigma: float, rho: float, beta: float) -> list[float]: + """ +Computes the Lorenz system derivatives: dx/dt = σ(y-x), dy/dt = x(ρ-z)-y, dz/dt = xy-βz. + +Rust: `nonlinear::lorenz_derivatives` + """ + ... + +def rossler_derivatives(state: list[float], a: float, b: float, c: float) -> list[float]: + """ +Computes the Roessler system derivatives: dx/dt = -y-z, dy/dt = x+ay, dz/dt = b+z(x-c). + +Rust: `nonlinear::rossler_derivatives` + """ + ... + +def henon_map(x: float, y: float, a: float, b: float) -> tuple[float, float]: + """ +Computes one iteration of the Henon map: x_{n+1} = 1 - ax² + y, y_{n+1} = bx. + +Rust: `nonlinear::henon_map` + """ + ... + +def henon_iterate(x0: float, y0: float, a: float, b: float, n: int) -> list[tuple[float, float]]: + """ +Iterates the Henon map n times from (x0, y0), returning the full trajectory of (x, y) pairs. + +Rust: `nonlinear::henon_iterate` + """ + ... + +def correlation_dimension_estimate(distances: list[float], r: float) -> float: + """ +Estimates the correlation integral C(r) as the fraction of pairwise distances below threshold r. + +Rust: `nonlinear::correlation_dimension_estimate` + """ + ... + +def box_counting_dimension(occupied_boxes: list[tuple[int, int]], grid_sizes: list[int]) -> float: + """ +Estimates the box-counting (Minkowski) dimension via least-squares fit of log(N) vs log(1/ε). + +Rust: `nonlinear::box_counting_dimension` + """ + ... + +def fixed_point_iterate(f: Callable[[float], float], x0: float, tol: float, max_iter: int) -> Optional[float]: + """ +Finds a fixed point of f by iterating x_{n+1} = f(x_n) until convergence within tolerance. + +Rust: `nonlinear::fixed_point_iterate` + """ + ... + +def is_stable_fixed_point(df_at_fixed: float) -> bool: + """ +Returns true if the fixed point is stable, i.e., |f'(x*)| < 1. + +Rust: `nonlinear::is_stable_fixed_point` + """ + ... diff --git a/bindings/python/python/numeria/nuclear.pyi b/bindings/python/python/numeria/nuclear.pyi new file mode 100644 index 0000000..0a9ca4d --- /dev/null +++ b/bindings/python/python/numeria/nuclear.pyi @@ -0,0 +1,173 @@ +""" +Radioactive decay, nuclear binding, and dosimetry. Exponential decay in its several parameterisations -- decay constant, half-life, mean lifetime -- and activity. Binding energy from the mass defect, binding energy per nucleon (the curve whose peak at iron-56 is why both fission and fusion release energy), and reaction Q-values. Nuclear size follows `R = R₀A^(1/3)`, giving a roughly constant nuclear density. Dosimetry covers absorbed and equivalent dose and the inverse-square falloff of intensity with distance. For reactor-scale neutron transport see `neutronics`. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.statistical_mechanics.kinetics import Reaction + +def remaining_nuclei(initial_count: float, decay_constant: float, time: float) -> float: + """ +Number of remaining nuclei: N(t) = N_0 * e^(-λt) + +Rust: `nuclear::remaining_nuclei` + """ + ... + +def activity(initial_count: float, decay_constant: float, time: float) -> float: + """ +Activity (decay rate): A = λ * N = λ * N_0 * e^(-λt) + +Rust: `nuclear::activity` + """ + ... + +def half_life(decay_constant: float) -> float: + """ +Half-life from decay constant: t_1/2 = ln(2) / λ + +Rust: `nuclear::half_life` + """ + ... + +def decay_constant(half_life: float) -> float: + """ +Decay constant from half-life: λ = ln(2) / t_1/2 + +Rust: `nuclear::decay_constant` + """ + ... + +def mean_lifetime(decay_constant: float) -> float: + """ +Mean lifetime: τ = 1 / λ + +Rust: `nuclear::mean_lifetime` + """ + ... + +def remaining_after_half_lives(initial_count: float, num_half_lives: float) -> float: + """ +Number of nuclei remaining after n half-lives: N = N_0 / 2^n + +Rust: `nuclear::remaining_after_half_lives` + """ + ... + +def num_half_lives(time: float, half_life: float) -> float: + """ +Number of half-lives elapsed: n = t / t_1/2 + +Rust: `nuclear::num_half_lives` + """ + ... + +def mass_defect(num_protons: int, num_neutrons: int, nucleus_mass: float) -> float: + """ +Mass defect: Δm = Z*m_p + N*m_n - M_nucleus + +Rust: `nuclear::mass_defect` + """ + ... + +def binding_energy(mass_defect: float) -> float: + """ +Binding energy from mass defect: E_b = Δm * c^2 + +Rust: `nuclear::binding_energy` + """ + ... + +def binding_energy_per_nucleon(total_binding_energy: float, mass_number: int) -> float: + """ +Binding energy per nucleon: E_b / A + +Rust: `nuclear::binding_energy_per_nucleon` + """ + ... + +def q_value(reactant_mass: float, product_mass: float) -> float: + """ +Q-value of a nuclear reaction: Q = (m_reactants - m_products) * c^2 + +Rust: `nuclear::q_value` + """ + ... + +def mass_energy(mass: float) -> float: + """ +Energy released from mass conversion: E = Δm * c^2 + +Rust: `nuclear::mass_energy` + """ + ... + +def energy_from_amu(mass_difference_amu: float) -> float: + """ +Energy from fission/fusion (given mass difference in amu): +E = Δm(amu) * 931.5 MeV + +Rust: `nuclear::energy_from_amu` + """ + ... + +def reaction_rate(number_density: float, cross_section: float, flux: float) -> float: + """ +Reaction rate: R = n * σ * Φ +n = target number density, σ = cross section, Φ = flux + +Rust: `nuclear::reaction_rate` + """ + ... + +def nuclear_mean_free_path(number_density: float, cross_section: float) -> float: + """ +Mean free path in nuclear context: λ = 1 / (n * σ) + +Rust: `nuclear::nuclear_mean_free_path` + """ + ... + +def nuclear_radius(mass_number: int) -> float: + """ +Nuclear radius (empirical): R = R_0 * A^(1/3) where R_0 ≈ 1.2 fm + +Rust: `nuclear::nuclear_radius` + """ + ... + +def nuclear_density() -> float: + """ +Nuclear density (approximately constant): ρ ≈ 3m_p / (4π * R_0^3) + +Rust: `nuclear::nuclear_density` + """ + ... + +def absorbed_dose(energy: float, mass: float) -> float: + """ +Absorbed dose: D = E / m (Gray, Gy = J/kg) + +Rust: `nuclear::absorbed_dose` + """ + ... + +def equivalent_dose(absorbed_dose: float, weighting_factor: float) -> float: + """ +Equivalent dose: H = D * w_R (Sievert, Sv) +w_R is the radiation weighting factor + +Rust: `nuclear::equivalent_dose` + """ + ... + +def radiation_intensity_distance(initial_intensity: float, initial_distance: float, new_distance: float) -> float: + """ +Inverse square law for radiation intensity: I = I_0 * (r_0 / r)^2 + +Rust: `nuclear::radiation_intensity_distance` + """ + ... diff --git a/bindings/python/python/numeria/numerical/__init__.pyi b/bindings/python/python/numeria/numerical/__init__.pyi new file mode 100644 index 0000000..fbeeb31 --- /dev/null +++ b/bindings/python/python/numeria/numerical/__init__.pyi @@ -0,0 +1,38 @@ +""" +Numerical methods: quadrature, root finding, ODE solvers, and interpolation. Submodules are re-exported so historical paths such as `numerical::trapezoid` keep working. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from . import bvp, integrate, interpolate, ode, roots +from numeria.numerical.interpolate import BSpline as BSpline +from numeria.numerical.interpolate import CubicSpline as CubicSpline +from numeria.numerical.integrate import QuadResult as QuadResult +from numeria.numerical.integrate import adaptive_quad as adaptive_quad +from numeria.numerical.roots import bisection as bisection +from numeria.numerical.roots import brent_root as brent_root +from numeria.numerical.interpolate import catmull_rom as catmull_rom +from numeria.numerical.interpolate import catmull_rom_2d as catmull_rom_2d +from numeria.numerical.interpolate import cubic_interp as cubic_interp +from numeria.numerical.interpolate import de_casteljau as de_casteljau +from numeria.numerical.bvp import finite_difference_linear_bvp as finite_difference_linear_bvp +from numeria.numerical.integrate import gauss_kronrod_15 as gauss_kronrod_15 +from numeria.numerical.integrate import gaussian_quadrature_5 as gaussian_quadrature_5 +from numeria.numerical.integrate import integrate_infinite as integrate_infinite +from numeria.numerical.interpolate import lerp as lerp +from numeria.numerical.interpolate import linear_interp as linear_interp +from numeria.numerical.roots import newton_raphson as newton_raphson +from numeria.numerical.roots import polynomial_eval as polynomial_eval +from numeria.numerical.roots import polynomial_eval_complex as polynomial_eval_complex +from numeria.numerical.roots import polynomial_roots as polynomial_roots +from numeria.numerical.integrate import richardson_extrapolate as richardson_extrapolate +from numeria.numerical.integrate import romberg as romberg +from numeria.numerical.roots import secant as secant +from numeria.numerical.bvp import shooting as shooting +from numeria.numerical.integrate import simpson as simpson +from numeria.numerical.integrate import trapezoid as trapezoid + + diff --git a/bindings/python/python/numeria/numerical/bvp.pyi b/bindings/python/python/numeria/numerical/bvp.pyi new file mode 100644 index 0000000..1722cd2 --- /dev/null +++ b/bindings/python/python/numeria/numerical/bvp.pyi @@ -0,0 +1,34 @@ +""" +Two-point boundary value problems. `shooting` reduces y'' = f(t, y, y') with y(t0) = y0, y(t1) = y1 to root finding on the initial slope (integrated with Dormand-Prince, slope found with Brent's method). `finite_difference_linear_bvp` discretizes the linear problem y'' + p·y' + q·y = r on a uniform grid and solves the tridiagonal system with the Thomas algorithm (Burden & Faires, *Numerical Analysis*, §11.3). +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def shooting(f: Callable[[float, float, float], float], t0: float, t1: float, y0: float, y1_target: float, guess_lo: float, guess_hi: float, tol: float) -> list[tuple[float, float]]: + """ +Solves y'' = f(t, y, y') with y(t0) = y0 and y(t1) = y1_target by +the shooting method: the unknown initial slope is bracketed by +[guess_lo, guess_hi] and found with Brent's method. + +Returns the (t, y) trajectory of the converged solution. Fails with +`InvalidArgument` if the bracket does not straddle the target. + +Rust: `numerical::bvp::shooting` + """ + ... + +def finite_difference_linear_bvp(p: Callable[[float], float], q: Callable[[float], float], r: Callable[[float], float], a: float, b: float, ya: float, yb: float, n: int) -> list[float]: + """ +Solves the linear BVP y'' + p(x)·y' + q(x)·y = r(x) on [a, b] with +y(a) = ya, y(b) = yb using n interior points and second-order +central differences; the tridiagonal system goes to `thomas_solve`. + +Returns the full grid of n + 2 values including both boundaries. + +Rust: `numerical::bvp::finite_difference_linear_bvp` + """ + ... diff --git a/bindings/python/python/numeria/numerical/integrate.pyi b/bindings/python/python/numeria/numerical/integrate.pyi new file mode 100644 index 0000000..5182fe5 --- /dev/null +++ b/bindings/python/python/numeria/numerical/integrate.pyi @@ -0,0 +1,104 @@ +""" +Numerical integration (quadrature) rules. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +class QuadResult: + """ +Result of an error-estimating quadrature: the integral estimate, an +upper bound on its error, and the number of function evaluations. + +Rust: `numerical::integrate::QuadResult` + """ + def __init__(self, value: float, error: float, evals: int) -> None: ... + @property + def value(self) -> float: ... + @property + def error(self) -> float: ... + @property + def evals(self) -> int: ... + +def trapezoid(f: Callable[[float], float], a: float, b: float, n: int) -> float: + """ +Trapezoidal rule for numerical integration of f over [a, b] with n subintervals. +Sample values are accumulated with Neumaier compensated summation. + +Rust: `numerical::integrate::trapezoid` + """ + ... + +def simpson(f: Callable[[float], float], a: float, b: float, n: int) -> float: + """ +Simpson's 1/3 rule for numerical integration of f over [a, b] with n subintervals. +If n is odd it is rounded up to the next even number. + +Rust: `numerical::integrate::simpson` + """ + ... + +def gaussian_quadrature_5(f: Callable[[float], float], a: float, b: float) -> float: + """ +5-point Gauss-Legendre quadrature of f over [a, b]. + +Rust: `numerical::integrate::gaussian_quadrature_5` + """ + ... + +def gauss_kronrod_15(f: Callable[[float], float], a: float, b: float) -> QuadResult: + """ +15-point Gauss-Kronrod quadrature of f over [a, b]. + +`value` is the K15 estimate; `error` is |K15 − G7|, the classical +(conservative) error bound from the embedded 7-point Gauss rule. + +Rust: `numerical::integrate::gauss_kronrod_15` + """ + ... + +def adaptive_quad(f: Callable[[float], float], a: float, b: float, tol: float, max_depth: int) -> QuadResult: + """ +Adaptive quadrature: recursive bisection with the GK15 rule until +each panel's error estimate is below its share of `tol`. + +Rust: `numerical::integrate::adaptive_quad` + """ + ... + +def romberg(f: Callable[[float], float], a: float, b: float, max_levels: int, tol: float) -> QuadResult: + """ +Romberg integration: trapezoid estimates at h, h/2, h/4, … with +Richardson extrapolation across the levels (NR §4.3). Converges when +two successive diagonal entries agree within `tol`. + +Rust: `numerical::integrate::romberg` + """ + ... + +def richardson_extrapolate(estimates: list[float], ratio: float, order: int) -> float: + """ +One generalized Richardson extrapolation pass over estimates whose +step sizes shrink by `ratio` between entries and whose error expands +in powers of h^order (E = c₁·h^p + c₂·h^{2p} + …). Returns the +highest-order extrapolant. + +Panics: +Panics if `estimates` is empty, `ratio <= 1`, or `order == 0`. + +Rust: `numerical::integrate::richardson_extrapolate` + """ + ... + +def integrate_infinite(f: Callable[[float], float], tol: float) -> QuadResult: + """ +Integral of f over (−∞, ∞) via the substitution x = t/(1−t²), +dx = (1+t²)/(1−t²)² dt, mapped onto t ∈ (−1, 1) and evaluated with +`adaptive_quad`. Requires f to decay at infinity. + +Rust: `numerical::integrate::integrate_infinite` + """ + ... diff --git a/bindings/python/python/numeria/numerical/interpolate.pyi b/bindings/python/python/numeria/numerical/interpolate.pyi new file mode 100644 index 0000000..9065a46 --- /dev/null +++ b/bindings/python/python/numeria/numerical/interpolate.pyi @@ -0,0 +1,111 @@ +""" +Interpolation routines. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.math import Vec3 + +class BSpline: + """ +Clamped uniform B-spline curve evaluated by de Boor's algorithm +(de Boor, *A Practical Guide to Splines*). + +Rust: `numerical::interpolate::BSpline` + """ + def __init__(self, degree: int, knots: list[float], control: list[Vec3 | Sequence[float]]) -> None: ... + @staticmethod + def uniform(degree: int, control: list[Vec3 | Sequence[float]]) -> BSpline: ... + def domain(self) -> tuple[float, float]: ... + def eval(self, u: float) -> Vec3: ... + def derivative(self, u: float) -> Vec3: ... + @property + def degree(self) -> int: ... + @property + def knots(self) -> list[float]: ... + @property + def control(self) -> list[Vec3]: ... + +class CubicSpline: + """ +Piecewise cubic spline S_i(t) = a_i + b_i·Δ + c_i·Δ² + d_i·Δ³ with +Δ = t − x_i on segment i (Burden & Faires, *Numerical Analysis*, +§3.5). Built with the Thomas tridiagonal solve; C² across knots. + +Rust: `numerical::interpolate::CubicSpline` + """ + @staticmethod + def natural(x: list[float], y: list[float]) -> CubicSpline: ... + @staticmethod + def clamped(x: list[float], y: list[float], dy0: float, dyn_: float) -> CubicSpline: ... + def eval(self, t: float) -> float: ... + def derivative(self, t: float) -> float: ... + def integrate(self, a: float, b: float) -> float: ... + +def lerp(a: float, b: float, t: float) -> float: + """ +Interpolation routines. +Linear interpolation between a and b: a + t*(b - a). + +Rust: `numerical::interpolate::lerp` + """ + ... + +def linear_interp(x_data: list[float], y_data: list[float], x: float) -> float: + """ +Piecewise linear interpolation in sorted (x_data, y_data). +Clamps to the endpoint values if x is outside the data range. +Panics if data slices are empty or mismatched in length. + +Rust: `numerical::interpolate::linear_interp` + """ + ... + +def cubic_interp(x_data: list[float], y_data: list[float], x: float) -> float: + """ +Natural cubic spline interpolation for a single query point. +Falls back to linear interpolation if fewer than 4 data points. +Panics if data slices are empty or mismatched in length. + +Rust: `numerical::interpolate::cubic_interp` + """ + ... + +def catmull_rom(points: list[Vec3 | Sequence[float]], t: float) -> Vec3: + """ +Uniform Catmull-Rom spline through `points`, parameterized so that +`t = i` lands exactly on `points[i]` (`t ∈ [0, n−1]`; endpoints use +duplicated boundary points). + +Panics: +Panics unless there are at least 2 points and t is within range. + +Rust: `numerical::interpolate::catmull_rom` + """ + ... + +def catmull_rom_2d(points: list[tuple[float, float]], t: float) -> tuple[float, float]: + """ +2-D uniform Catmull-Rom spline (same parameterization as +`catmull_rom`). + +Panics: +Panics unless there are at least 2 points and t is within range. + +Rust: `numerical::interpolate::catmull_rom_2d` + """ + ... + +def de_casteljau(control: list[Vec3 | Sequence[float]], t: float) -> Vec3: + """ +Arbitrary-degree Bezier curve point by the de Casteljau algorithm. + +Panics: +Panics if `control` is empty or t is outside [0, 1]. + +Rust: `numerical::interpolate::de_casteljau` + """ + ... diff --git a/bindings/python/python/numeria/numerical/ode/__init__.pyi b/bindings/python/python/numeria/numerical/ode/__init__.pyi new file mode 100644 index 0000000..36163bf --- /dev/null +++ b/bindings/python/python/numeria/numerical/ode/__init__.pyi @@ -0,0 +1,22 @@ +""" +Ordinary differential equation solvers. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from . import adaptive, explicit, implicit, symplectic +from numeria.numerical.ode.adaptive import AdaptiveResult as AdaptiveResult +from numeria.numerical.ode.adaptive import dormand_prince as dormand_prince +from numeria.numerical.ode.adaptive import dormand_prince_dense as dormand_prince_dense +from numeria.numerical.ode.explicit import euler_step as euler_step +from numeria.numerical.ode.symplectic import leapfrog_kick_drift_kick as leapfrog_kick_drift_kick +from numeria.numerical.ode.explicit import rk4_solve as rk4_solve +from numeria.numerical.ode.explicit import rk4_step as rk4_step +from numeria.numerical.ode.explicit import rk4_step_vec as rk4_step_vec +from numeria.numerical.ode.symplectic import velocity_verlet as velocity_verlet +from numeria.numerical.ode.symplectic import yoshida4 as yoshida4 + + diff --git a/bindings/python/python/numeria/numerical/ode/adaptive.pyi b/bindings/python/python/numeria/numerical/ode/adaptive.pyi new file mode 100644 index 0000000..5b7e9a5 --- /dev/null +++ b/bindings/python/python/numeria/numerical/ode/adaptive.pyi @@ -0,0 +1,49 @@ +""" +Adaptive Runge-Kutta integration: Dormand-Prince 5(4). Reference: Dormand & Prince, "A family of embedded Runge-Kutta formulae" (1980); Hairer, Nørsett & Wanner, *Solving ODEs I*, §II.4. The embedded 4th-order solution provides the error estimate; steps use the FSAL (first-same-as-last) property. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +class AdaptiveResult: + """ +Result of an adaptive integration: accepted step times, states, and +the number of rejected trial steps. + +Rust: `numerical::ode::adaptive::AdaptiveResult` + """ + def __init__(self, t: list[float], y: list[list[float]], steps_rejected: int) -> None: ... + @property + def t(self) -> list[float]: ... + @property + def y(self) -> list[list[float]]: ... + @property + def steps_rejected(self) -> int: ... + +def dormand_prince(f: Callable[[float, list[float]], list[float]], t0: float, t1: float, y0: list[float], rtol: float, atol: float, h0: float) -> AdaptiveResult: + """ +Integrates dy/dt = f(t, y) from t0 to t1 with adaptive step size, +recording every accepted step. + +The local error per step is kept near `atol + rtol·|y|` (RMS over +components). Fails with `NoConvergence` if the step count budget is +exhausted or the step size underflows. + +Rust: `numerical::ode::adaptive::dormand_prince` + """ + ... + +def dormand_prince_dense(f: Callable[[float, list[float]], list[float]], t0: float, t1: float, y0: list[float], rtol: float, atol: float, h0: float, sample_times: list[float]) -> AdaptiveResult: + """ +Like `dormand_prince` but returns the solution interpolated at +`sample_times` (cubic Hermite between accepted steps, using the +stored derivatives at the step endpoints). + +`sample_times` must be non-decreasing and lie within [t0, t1]. + +Rust: `numerical::ode::adaptive::dormand_prince_dense` + """ + ... diff --git a/bindings/python/python/numeria/numerical/ode/explicit.pyi b/bindings/python/python/numeria/numerical/ode/explicit.pyi new file mode 100644 index 0000000..72ecbc2 --- /dev/null +++ b/bindings/python/python/numeria/numerical/ode/explicit.pyi @@ -0,0 +1,43 @@ +""" +Explicit fixed-step ODE integrators. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def euler_step(f: Callable[[float, float], float], t: float, y: float, dt: float) -> float: + """ +Explicit fixed-step ODE integrators. +Single forward Euler step: y_next = y + dt * f(t, y). + +Rust: `numerical::ode::explicit::euler_step` + """ + ... + +def rk4_step(f: Callable[[float, float], float], t: float, y: float, dt: float) -> float: + """ +Single step of the classic 4th-order Runge-Kutta method. + +Rust: `numerical::ode::explicit::rk4_step` + """ + ... + +def rk4_solve(f: Callable[[float, float], float], t0: float, y0: float, t_end: float, dt: float) -> list[tuple[float, float]]: + """ +Full RK4 integration of dy/dt = f(t, y) from t0 to t_end, returning (t, y) pairs. + +Rust: `numerical::ode::explicit::rk4_solve` + """ + ... + +def rk4_step_vec(f: Callable[[float, list[float]], list[float]], t: float, y: list[float], dt: float) -> list[float]: + """ +Single RK4 step for a system of ODEs (vector state). +`f(t, y)` returns a `Vec` of derivatives matching the length of `y`. + +Rust: `numerical::ode::explicit::rk4_step_vec` + """ + ... diff --git a/bindings/python/python/numeria/numerical/ode/implicit.pyi b/bindings/python/python/numeria/numerical/ode/implicit.pyi new file mode 100644 index 0000000..a629b19 --- /dev/null +++ b/bindings/python/python/numeria/numerical/ode/implicit.pyi @@ -0,0 +1,11 @@ +""" +Implicit (stiff-stable) ODE steps: backward Euler and BDF2. Both solve their implicit update equation with damped-free Newton iteration; the Jacobian of f is user-supplied or, when `None`, approximated by forward finite differences (an opaque `f64` closure cannot be differentiated with `core::dual`, so finite differences stand in for it here). +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + + diff --git a/bindings/python/python/numeria/numerical/ode/symplectic.pyi b/bindings/python/python/numeria/numerical/ode/symplectic.pyi new file mode 100644 index 0000000..87eaf34 --- /dev/null +++ b/bindings/python/python/numeria/numerical/ode/symplectic.pyi @@ -0,0 +1,46 @@ +""" +Symplectic integrators for second-order systems x'' = a(x). These preserve phase-space volume, so energy errors stay bounded instead of drifting. References: Verlet (1967); Yoshida, "Construction of higher order symplectic integrators", Phys. Lett. A 150 (1990). +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def velocity_verlet(acc: Callable[[list[float]], list[float]], x: MutableSequence[float], v: MutableSequence[float], dt: float) -> None: + """ +Symplectic integrators for second-order systems x'' = a(x). + +These preserve phase-space volume, so energy errors stay bounded +instead of drifting. References: Verlet (1967); Yoshida, "Construction +of higher order symplectic integrators", Phys. Lett. A 150 (1990). +One velocity-Verlet step (kick-drift-kick): +v½ = v + a(x)·dt/2; x₁ = x + v½·dt; v₁ = v½ + a(x₁)·dt/2. + +Panics: +Panics if `x` and `v` differ in length or `acc` returns the wrong +length. + +Rust: `numerical::ode::symplectic::velocity_verlet` + """ + ... + +def leapfrog_kick_drift_kick(acc: Callable[[list[float]], list[float]], x: MutableSequence[float], v: MutableSequence[float], dt: float) -> None: + """ +Alias of the kick-drift-kick leapfrog scheme (identical to velocity +Verlet in this synchronized form). + +Rust: `numerical::ode::symplectic::leapfrog_kick_drift_kick` + """ + ... + +def yoshida4(acc: Callable[[list[float]], list[float]], x: MutableSequence[float], v: MutableSequence[float], dt: float) -> None: + """ +One 4th-order Yoshida step: composition of three velocity-Verlet +sub-steps with weights w1, w0, w1 where +w1 = 1/(2 − 2^(1/3)), w0 = −2^(1/3)·w1. + +Rust: `numerical::ode::symplectic::yoshida4` + """ + ... diff --git a/bindings/python/python/numeria/numerical/roots.pyi b/bindings/python/python/numeria/numerical/roots.pyi new file mode 100644 index 0000000..ce90360 --- /dev/null +++ b/bindings/python/python/numeria/numerical/roots.pyi @@ -0,0 +1,85 @@ +""" +Scalar and polynomial root finding. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def bisection(f: Callable[[float], float], a: float, b: float, tol: float, max_iter: int) -> Optional[float]: + """ +Bisection method for finding a root of f in [a, b]. +Returns `None` if f(a) and f(b) have the same sign. + +Rust: `numerical::roots::bisection` + """ + ... + +def newton_raphson(f: Callable[[float], float], df: Callable[[float], float], x0: float, tol: float, max_iter: int) -> Optional[float]: + """ +Newton-Raphson method starting from x0. +Returns `None` if the derivative is zero or the method does not converge within max_iter. + +Rust: `numerical::roots::newton_raphson` + """ + ... + +def secant(f: Callable[[float], float], x0: float, x1: float, tol: float, max_iter: int) -> Optional[float]: + """ +Secant method starting from two initial guesses x0 and x1. +Returns `None` if the method does not converge within max_iter. + +Rust: `numerical::roots::secant` + """ + ... + +def polynomial_eval(coeffs: list[float], x: float) -> float: + """ +Evaluates a real polynomial by Horner's rule; `coeffs` are ordered +highest degree first. + +Panics: +Panics if `coeffs` is empty. + +Rust: `numerical::roots::polynomial_eval` + """ + ... + +def polynomial_eval_complex(coeffs: list[float], z: complex) -> complex: + """ +Horner evaluation of a real-coefficient polynomial at a complex +point; `coeffs` are ordered highest degree first. + +Panics: +Panics if `coeffs` is empty. + +Rust: `numerical::roots::polynomial_eval_complex` + """ + ... + +def polynomial_roots(coeffs: list[float]) -> list[complex]: + """ +All complex roots of a real polynomial (coefficients highest degree +first) via the Durand-Kerner (Weierstrass) simultaneous iteration. + +Leading zeros are stripped; the root count equals the degree. +Returns `InvalidArgument` for constant (degree-0) or all-zero input +and `NoConvergence` if the iteration stalls. + +Rust: `numerical::roots::polynomial_roots` + """ + ... + +def brent_root(f: Callable[[float], float], a: float, b: float, tol: float, max_iter: int) -> float: + """ +Brent's method: bracketing root finder combining bisection, secant, +and inverse quadratic interpolation (Brent 1973; NR §9.3). +Superlinear convergence with guaranteed bracket retention. + +Returns `InvalidArgument` unless f(a) and f(b) have opposite signs. + +Rust: `numerical::roots::brent_root` + """ + ... diff --git a/bindings/python/python/numeria/optics.pyi b/bindings/python/python/numeria/optics.pyi new file mode 100644 index 0000000..bc3a734 --- /dev/null +++ b/bindings/python/python/numeria/optics.pyi @@ -0,0 +1,173 @@ +""" +Geometric and wave optics. Refraction by Snell's law, the critical angle for total internal reflection, and the Brewster angle at which reflected light is fully polarized. Imaging through the thin-lens and mirror equations, magnification, lens power, combined focal lengths and the lensmaker's radius of curvature. Wave optics covers single-slit minima, double-slit maxima, the grating equation, thin-film interference, and the Rayleigh resolution criterion `θ = 1.22 λ/D`. Malus's law closes it. For Gaussian beams, fibre optics and ray transfer matrices see `photonics`. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def snells_law(n1: float, angle1_rad: float, n2: float) -> Optional[float]: + """ +Snell's law: n1 * sin(θ1) = n2 * sin(θ2) → θ2 = asin(n1 * sin(θ1) / n2) +Returns the refraction angle in radians, or None for total internal reflection. + +Rust: `optics::snells_law` + """ + ... + +def critical_angle(n1: float, n2: float) -> Optional[float]: + """ +Critical angle for total internal reflection: θ_c = asin(n2 / n1) +Only valid when n1 > n2. Returns None otherwise. + +Rust: `optics::critical_angle` + """ + ... + +def brewster_angle(n1: float, n2: float) -> float: + """ +Brewster's angle: θ_B = atan(n2 / n1) + +Rust: `optics::brewster_angle` + """ + ... + +def refractive_index(speed_in_medium: float) -> float: + """ +Index of refraction: n = c / v + +Rust: `optics::refractive_index` + """ + ... + +def speed_in_medium(refractive_index: float) -> float: + """ +Speed of light in a medium: v = c / n + +Rust: `optics::speed_in_medium` + """ + ... + +def image_distance(focal_length: float, object_distance: float) -> float: + """ +Mirror/thin lens equation: 1/f = 1/d_o + 1/d_i → d_i = f*d_o / (d_o - f) + +Rust: `optics::image_distance` + """ + ... + +def magnification(image_distance: float, object_distance: float) -> float: + """ +Magnification: m = -d_i / d_o + +Rust: `optics::magnification` + """ + ... + +def magnification_from_heights(image_height: float, object_height: float) -> float: + """ +Magnification from image and object heights: m = h_i / h_o + +Rust: `optics::magnification_from_heights` + """ + ... + +def lens_focal_length(n: float, r1: float, r2: float) -> float: + """ +Lens maker's equation: 1/f = (n-1) * (1/R1 - 1/R2) + +Rust: `optics::lens_focal_length` + """ + ... + +def lens_power(focal_length: float) -> float: + """ +Power of a lens: P = 1/f (in diopters when f is in meters) + +Rust: `optics::lens_power` + """ + ... + +def combined_focal_length(f1: float, f2: float) -> float: + """ +Combined focal length of two thin lenses in contact: 1/f = 1/f1 + 1/f2 + +Rust: `optics::combined_focal_length` + """ + ... + +def radius_of_curvature(focal_length: float) -> float: + """ +Mirror radius of curvature: R = 2f + +Rust: `optics::radius_of_curvature` + """ + ... + +def single_slit_minimum(order: int, wavelength: float, slit_width: float) -> Optional[float]: + """ +Single slit diffraction minima: a * sin(θ) = m * λ → θ = asin(m * λ / a) +Returns angle in radians for the m-th minimum. + +Rust: `optics::single_slit_minimum` + """ + ... + +def double_slit_maximum(order: int, wavelength: float, slit_separation: float) -> Optional[float]: + """ +Double slit maxima: d * sin(θ) = m * λ → θ = asin(m * λ / d) + +Rust: `optics::double_slit_maximum` + """ + ... + +def diffraction_grating_angle(order: int, wavelength: float, grating_spacing: float) -> Optional[float]: + """ +Diffraction grating: d * sin(θ) = m * λ + +Rust: `optics::diffraction_grating_angle` + """ + ... + +def rayleigh_resolution(wavelength: float, aperture_diameter: float) -> float: + """ +Rayleigh criterion (angular resolution): θ = 1.22 * λ / D + +Rust: `optics::rayleigh_resolution` + """ + ... + +def thin_film_constructive_thickness(order: int, wavelength: float, film_index: float) -> float: + """ +Thin film interference (constructive, normal incidence): +2 * n * t = (m + 0.5) * λ for reflection with one phase change + +Rust: `optics::thin_film_constructive_thickness` + """ + ... + +def constructive_path_diff(order: int, wavelength: float) -> float: + """ +Path difference for constructive interference: Δ = m * λ + +Rust: `optics::constructive_path_diff` + """ + ... + +def destructive_path_diff(order: int, wavelength: float) -> float: + """ +Path difference for destructive interference: Δ = (m + 0.5) * λ + +Rust: `optics::destructive_path_diff` + """ + ... + +def malus_law(initial_intensity: float, angle_rad: float) -> float: + """ +Malus's law: I = I_0 * cos^2(θ) + +Rust: `optics::malus_law` + """ + ... diff --git a/bindings/python/python/numeria/optimization/__init__.pyi b/bindings/python/python/numeria/optimization/__init__.pyi new file mode 100644 index 0000000..50645de --- /dev/null +++ b/bindings/python/python/numeria/optimization/__init__.pyi @@ -0,0 +1,115 @@ +""" +Optimization: continuous, combinatorial, and strategic. The module root holds the scalar and unconstrained-gradient methods -- golden section and Brent for a bracketed minimum of one variable, then gradient descent with and without momentum, Adam, numerical gradients, and the regression and curve fitting built on them. The submodules take it further: `lp` for linear programming and duality, `integer` for branch-and-bound and dynamic programming, `network` for flows and scheduling, `convex` for L-BFGS, proximal methods and ADMM, `metaheuristics` for the derivative-free and population-based methods, `game_theory` for equilibria and cooperative solutions, and `least_squares` for Levenberg-Marquardt. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from . import convex, game_theory, integer, least_squares, lp, metaheuristics, network +from numeria.optimization.least_squares import LmResult as LmResult +from numeria.optimization.least_squares import fit_exponential_decay as fit_exponential_decay +from numeria.optimization.least_squares import fit_gaussian_peak as fit_gaussian_peak + +def golden_section_min(f: Callable[[float], float], a: float, b: float, tol: float, max_iter: int) -> float: + """ +Golden-section search for the minimum of `f` on `[a, b]`. + +Returns the x value that minimizes f within tolerance `tol`. + +Rust: `optimization::golden_section_min` + """ + ... + +def brent_min(f: Callable[[float], float], a: float, b: float, tol: float, max_iter: int) -> float: + """ +Brent's method for 1-D minimization, combining golden-section search with +parabolic interpolation. + +Rust: `optimization::brent_min` + """ + ... + +def numerical_gradient_vec(f: Callable[[list[float]], float], x: list[float], h: float) -> list[float]: + """ +Central-difference numerical gradient of a scalar function of n variables. + +Rust: `optimization::numerical_gradient_vec` + """ + ... + +def gradient_descent(f: Callable[[list[float]], float], grad: Callable[[list[float]], list[float]], x0: list[float], learning_rate: float, tol: float, max_iter: int) -> list[float]: + """ +Vanilla gradient descent: x ← x − α∇f. + +Rust: `optimization::gradient_descent` + """ + ... + +def gradient_descent_momentum(f: Callable[[list[float]], float], grad: Callable[[list[float]], list[float]], x0: list[float], learning_rate: float, momentum: float, tol: float, max_iter: int) -> list[float]: + """ +Gradient descent with momentum: v ← μv − α∇f, x ← x + v. + +Rust: `optimization::gradient_descent_momentum` + """ + ... + +def adam(f: Callable[[list[float]], float], grad: Callable[[list[float]], list[float]], x0: list[float], learning_rate: float, tol: float, max_iter: int) -> list[float]: + """ +Adam optimizer (β1=0.9, β2=0.999, ε=1e-8). + +Rust: `optimization::adam` + """ + ... + +def nelder_mead(f: Callable[[list[float]], float], x0: list[float], step: float, tol: float, max_iter: int) -> list[float]: + """ +Nelder-Mead simplex algorithm for unconstrained minimization. + +Rust: `optimization::nelder_mead` + """ + ... + +def simulated_annealing(f: Callable[[list[float]], float], x0: list[float], temp_initial: float, cooling_rate: float, step_size: float, max_iter: int) -> list[float]: + """ +Simulated annealing for unconstrained minimization. + +T = temp_initial × cooling_rate^iter. Accepts uphill moves with probability +exp(−ΔE / T). LCG seeded deterministically from `x0`. + +Rust: `optimization::simulated_annealing` + """ + ... + +def linear_regression(x: list[float], y: list[float]) -> tuple[float, float]: + """ +Ordinary linear regression: minimizes ‖a0 + a1·x − y‖₂ via +Householder-QR least squares (`linalg::qr::least_squares`). + +Returns `(slope, intercept)`. + +Rust: `optimization::linear_regression` + """ + ... + +def polynomial_fit(x: list[float], y: list[float], degree: int) -> list[float]: + """ +Fit a polynomial of the given degree to (x, y) data by QR least +squares on the Vandermonde matrix, falling back to normal equations +with Gaussian elimination when the system is rank deficient. + +Returns coefficients `[a0, a1, …, a_degree]` such that +ŷ = a0 + a1·x + a2·x² + … + +Rust: `optimization::polynomial_fit` + """ + ... + +def r_squared(y_actual: list[float], y_predicted: list[float]) -> float: + """ +Coefficient of determination R² = 1 − SS_res / SS_tot. + +Rust: `optimization::r_squared` + """ + ... diff --git a/bindings/python/python/numeria/optimization/convex.pyi b/bindings/python/python/numeria/optimization/convex.pyi new file mode 100644 index 0000000..6f6db7d --- /dev/null +++ b/bindings/python/python/numeria/optimization/convex.pyi @@ -0,0 +1,553 @@ +""" +Convex optimisation: gradient methods, quasi-Newton methods, proximal splitting, and constrained solvers. Convexity buys one thing, and it is decisive: every local minimum is global. That removes the question the methods in `optimization::metaheuristics` spend all their effort on -- where else to look -- and replaces it with a purely local question, how fast to get downhill. Everything here is an answer to that. The answers differ in what they know about curvature. Gradient descent knows nothing and pays for it: on a quadratic its error contracts by `(k-1)/(k+1)` per step, so a condition number of a thousand costs a thousand-fold more iterations than a condition number of one. Conjugate gradients build a set of mutually conjugate directions and finish an `n`-dimensional quadratic in at most `n` steps exactly. Newton's method uses the Hessian outright and lands on a quadratic's minimum in a single step. Quasi-Newton methods sit in between, accumulating an approximation to the Hessian from the gradients they have already paid for. Those are not asymptotic claims but exact ones, and the tests check them as such: Newton in one step, conjugate gradients in `n`, and every method against the closed-form minimiser `-Q^-1 c` of the quadratic it was given. The proximal half of the module handles objectives that are convex but not differentiable -- an L1 penalty, a constraint set -- by splitting them into a smooth part, handled by a gradient step, and a simple part, handled by its proximal operator. The reason that works is that the awkward part is usually simple in isolation: the proximal operator of an L1 penalty is soft thresholding, of a box is clamping, and of a simplex is a sorted shift. Each is a projection or near-projection with a closed form, so the non-smoothness costs almost nothing. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.core.dual import Dual +from numeria.linalg.matrix import Matrix +from numeria.monte_carlo import Rng + +def backtracking(f: Callable[[list[float]], float], x: list[float], direction: list[float], gradient: list[float], c: float, max_halvings: int) -> float: + """ +Backtracking line search satisfying the Armijo sufficient-decrease +condition. + +Halves the step until `f(x + t d) <= f(x) + c t g . d`. The condition is +what stops a long step that reduces the objective by less than the +gradient promised, which is how a descent method diverges on a curved +function despite every step going downhill. + +Panics: +Panics unless the direction is a descent direction and `c` lies in +`(0, 1)`. + +Rust: `optimization::convex::backtracking` + """ + ... + +def line_search_wolfe(f: Callable[[list[float]], float], grad: Callable[[list[float]], list[float]], x: list[float], direction: list[float], c1: float, c2: float) -> float: + """ +A line search satisfying the strong Wolfe conditions. + +Armijo alone allows arbitrarily *short* steps, which stalls a quasi-Newton +method: the curvature information it accumulates comes from the difference +between successive gradients, and a step too short to change the gradient +carries none. The second Wolfe condition, +`|g(x + t d) . d| <= c2 |g(x) . d|`, rules that out by demanding the slope +actually flatten. Together they are what makes the BFGS update +well defined. + +Panics: +Panics unless `0 < c1 < c2 < 1` and the direction descends. + +Rust: `optimization::convex::line_search_wolfe` + """ + ... + +def exact_line_search(grad: Callable[[list[float]], list[float]], x: list[float], direction: list[float]) -> Optional[float]: + """ +An exact line search, by root-finding on the directional derivative. + +The minimiser of `phi(t) = f(x + t d)` is where `phi'(t) = g(x + t d) . d` +vanishes. Since `phi'(0) < 0` for a descent direction, all that is needed +is a `t` where the slope has turned non-negative; bisection then locates +the root to machine precision. On a quadratic `phi'` is affine, so the +answer is exact to rounding. + +Returns `None` when no such bracket exists within a doubling cap, which +means the function is unbounded below along the direction -- there is no +minimiser to find, and the caller should use an inexact search instead. + +Panics: +Panics unless the direction descends. + +Rust: `optimization::convex::exact_line_search` + """ + ... + +def nesterov(grad: Callable[[list[float]], list[float]], x0: list[float], learning_rate: float, momentum: float, iterations: int) -> list[float]: + """ +Nesterov's accelerated gradient method. + +Evaluates the gradient at an extrapolated point rather than the current +one, which is the whole difference from heavy-ball momentum: the method +gets to see where the momentum is taking it before committing. That +changes the convergence rate on a smooth convex function from `O(1/k)` to +`O(1/k^2)`, which is optimal for a method that only ever sees gradients. + +Panics: +Panics if the learning rate is not positive. + +Rust: `optimization::convex::nesterov` + """ + ... + +def adagrad(grad: Callable[[list[float]], list[float]], x0: list[float], learning_rate: float, iterations: int) -> list[float]: + """ +Adagrad: scale each coordinate's step by the inverse root of its +accumulated squared gradient. + +Coordinates with consistently large gradients get short steps and rare +coordinates get long ones, which is what makes it suit sparse problems. +The accumulator only grows, so the effective learning rate decays +monotonically to zero -- helpful for convergence, fatal if the problem +needs to keep moving, which is what `rmsprop` fixes. + +Panics: +Panics if the learning rate is not positive. + +Rust: `optimization::convex::adagrad` + """ + ... + +def rmsprop(grad: Callable[[list[float]], list[float]], x0: list[float], learning_rate: float, decay: float, iterations: int) -> list[float]: + """ +RMSProp: Adagrad with an exponentially weighted accumulator. + +Forgetting old gradients keeps the effective learning rate from decaying +to zero, so the method can keep making progress indefinitely. + +Panics: +Panics unless the learning rate is positive and `decay` lies in `[0, 1)`. + +Rust: `optimization::convex::rmsprop` + """ + ... + +def adamw(grad: Callable[[list[float]], list[float]], x0: list[float], learning_rate: float, weight_decay: float, iterations: int) -> list[float]: + """ +AdamW: Adam with the weight decay applied to the parameters directly +rather than folded into the gradient. + +The distinction matters because Adam divides the gradient by its own +running scale. A decay term added to the gradient gets divided too, so its +strength ends up depending on how large the other gradients happen to be; +applied to the parameters it does not. That is the entire content of the +change, and it is why the two behave differently at the same nominal decay. + +Panics: +Panics unless the learning rate is positive and both moment decays lie in +`[0, 1)`. + +Rust: `optimization::convex::adamw` + """ + ... + +def subgradient_method(f: Callable[[list[float]], float], subgradient: Callable[[list[float]], list[float]], x0: list[float], initial_step: float, iterations: int) -> tuple[list[float], float]: + """ +Subgradient descent for a convex objective that is not differentiable. + +A subgradient is not a descent direction -- moving along it can increase +the objective, which is why the best value seen has to be tracked +separately rather than read off the last iterate. With a step size going +to zero but summing to infinity the method converges, at `O(1/sqrt(k))`: +far worse than the smooth case, and the price of giving up +differentiability. + +Returns the best point found. + +Panics: +Panics if the initial step is not positive. + +Rust: `optimization::convex::subgradient_method` + """ + ... + +def bfgs(f: Callable[[list[float]], float], grad: Callable[[list[float]], list[float]], x0: list[float], tol: float, max_iter: int) -> tuple[list[float], float]: + """ +BFGS with a Wolfe line search. + +Maintains an approximation to the *inverse* Hessian, updated from the +change in gradient across each step, so a Newton-like direction costs one +matrix-vector product and no solve. The update preserves positive +definiteness whenever the curvature condition `y . s > 0` holds, which the +Wolfe line search guarantees -- the two are designed together, and pairing +BFGS with a plain Armijo search is a classic way to make it fail. + +Errors: +Returns an error if the starting point is empty. + +Rust: `optimization::convex::bfgs` + """ + ... + +def lbfgs(f: Callable[[list[float]], float], grad: Callable[[list[float]], list[float]], x0: list[float], m: int, tol: float, max_iter: int) -> tuple[list[float], float]: + """ +Limited-memory BFGS. + +Stores the last `m` pairs of step and gradient change instead of a full +matrix, and reconstructs the search direction by a two-loop recursion. +Memory drops from `n^2` to `mn`, which is what makes the method usable +where `n` runs to millions and a dense inverse Hessian could not be stored +at all, let alone factored. + +Errors: +Returns an error if the starting point is empty or `m` is zero. + +Rust: `optimization::convex::lbfgs` + """ + ... + +def conjugate_gradient_nonlinear(f: Callable[[list[float]], float], grad: Callable[[list[float]], list[float]], x0: list[float], tol: float, max_iter: int) -> tuple[list[float], float]: + """ +Nonlinear conjugate gradients with the Polak-Ribiere update. + +On a quadratic the directions produced are mutually conjugate, so the +method reaches the exact minimum in at most `n` steps -- an exact finite +termination, not a rate. Away from a quadratic that guarantee lapses, +and the restart when `beta` goes negative is what keeps the directions +descending regardless. + +Errors: +Returns an error if the starting point is empty. + +Rust: `optimization::convex::conjugate_gradient_nonlinear` + """ + ... + +def prox_l1(v: list[float], t: float) -> list[float]: + """ +The proximal operator of `t ||x||_1`: soft thresholding. + +`prox(v) = sign(v) max(|v| - t, 0)`, which is the exact minimiser of +`||x - v||^2 / 2 + t ||x||_1`. It is what makes L1 penalties produce +genuinely zero coefficients rather than merely small ones -- the operator +maps a whole interval to exactly zero, which no smooth penalty does. + +Rust: `optimization::convex::prox_l1` + """ + ... + +def prox_l2(v: list[float], t: float) -> list[float]: + """ +The proximal operator of `t ||x||_2` (the norm, not its square): block +soft thresholding. + +Shrinks the whole vector toward zero and sets it to exactly zero once its +norm falls below `t`. Unlike `prox_l1` it acts on the vector as a unit, +which is what group-sparse penalties need. + +Rust: `optimization::convex::prox_l2` + """ + ... + +def prox_box(v: list[float], lo: float, hi: float) -> list[float]: + """ +The proximal operator of a box constraint: clamping. + +The proximal operator of an indicator function is the projection onto the +set, and for a box that is coordinatewise clamping. + +Rust: `optimization::convex::prox_box` + """ + ... + +def prox_simplex(v: list[float]) -> list[float]: + """ +Euclidean projection onto the probability simplex. + +Sort, find the threshold at which the shifted positive parts sum to one, +and subtract it. The result is the closest point of the simplex, which is +not simply the clamped-and-renormalised vector -- that is a common +substitute and it is a different point. + +Panics: +Panics if the vector is empty. + +Rust: `optimization::convex::prox_simplex` + """ + ... + +def proximal_gradient(smooth_grad: Callable[[list[float]], list[float]], prox: Callable[[list[float], float], list[float]], x0: list[float], step: float, iterations: int) -> list[float]: + """ +Proximal gradient descent, also called ISTA: a gradient step on the smooth +part followed by the proximal operator of the rest. + +The whole point is that the non-smooth part never needs a gradient. It +only has to have a proximal operator that can be evaluated, and for the +penalties that matter -- L1, group norms, indicator functions -- that +operator is a closed form. + +Converges at `O(1/k)`. + +Panics: +Panics if the step size is not positive. + +Rust: `optimization::convex::proximal_gradient` + """ + ... + +def fista(smooth_grad: Callable[[list[float]], list[float]], prox: Callable[[list[float], float], list[float]], x0: list[float], step: float, iterations: int) -> list[float]: + """ +FISTA: proximal gradient descent with Nesterov's extrapolation. + +The same two operations per iteration as `proximal_gradient`, applied at +an extrapolated point, which improves the rate from `O(1/k)` to `O(1/k^2)` +for no extra cost per step. The momentum sequence +`t_{k+1} = (1 + sqrt(1 + 4 t_k^2)) / 2` is what makes the accelerated +bound come out; an arbitrary momentum does not. + +Panics: +Panics if the step size is not positive. + +Rust: `optimization::convex::fista` + """ + ... + +def projected_gradient(grad: Callable[[list[float]], list[float]], project: Callable[[list[float]], list[float]], x0: list[float], step: float, iterations: int) -> list[float]: + """ +Projected gradient descent for a constrained smooth problem. + +Take a gradient step, then project back onto the feasible set. Correct +whenever the set is convex and the projection is available; the projection +is what makes or breaks it, since for most sets it is itself an +optimisation problem. + +Panics: +Panics if the step size is not positive. + +Rust: `optimization::convex::projected_gradient` + """ + ... + +def frank_wolfe(grad: Callable[[list[float]], list[float]], linear_oracle: Callable[[list[float]], list[float]], x0: list[float], iterations: int) -> list[float]: + """ +The Frank-Wolfe method, also called conditional gradient. + +Instead of projecting, it minimises a linear approximation over the +feasible set and moves toward that vertex. The iterate stays feasible +automatically as a convex combination of feasible points, so no projection +is ever needed -- which is the reason to use it when a linear minimisation +over the set is cheap and a projection is not. + +`linear_oracle` returns the minimiser of a linear function over the set. + +Panics: +Panics if the starting point is empty. + +Rust: `optimization::convex::frank_wolfe` + """ + ... + +def mirror_descent_simplex(grad: Callable[[list[float]], list[float]], x0: list[float], step: float, iterations: int) -> list[float]: + """ +Mirror descent on the probability simplex, with the entropy mirror map. + +The multiplicative update `x_i <- x_i exp(-t g_i)` followed by +renormalisation. Because the geometry matches the constraint set, the +dependence on dimension is `sqrt(log n)` rather than the `sqrt(n)` a +Euclidean projected gradient pays -- a large difference when the simplex +is over thousands of outcomes. + +Panics: +Panics if the starting point is empty or the step is not positive. + +Rust: `optimization::convex::mirror_descent_simplex` + """ + ... + +def ridge_closed_form(a: Matrix | Sequence[Sequence[float]], b: list[float], lambda_: float) -> list[float]: + """ +Ridge regression in closed form: solve `(A'A + lambda I) x = A'b`. + +The one regularised regression with an exact answer, because the penalty +is smooth and quadratic like the loss. The added `lambda I` is also what +makes the system solvable when `A'A` is singular -- ridge regression +regularises the numerics as much as the statistics. + +Errors: +Returns an error on a shape mismatch, a negative penalty, or a system that +is singular even after regularisation. + +Rust: `optimization::convex::ridge_closed_form` + """ + ... + +def lasso_coordinate_descent(a: Matrix | Sequence[Sequence[float]], b: list[float], lambda_: float, iterations: int) -> list[float]: + """ +Lasso by cyclic coordinate descent. + +Each coordinate is minimised exactly with the others held fixed, and that +one-dimensional problem has the soft-threshold closed form. Coordinate +descent works here precisely because the non-smooth part is *separable*: +the L1 penalty splits across coordinates, so a coordinatewise minimum is a +genuine minimum. On a non-separable penalty the same loop can stall at a +point that is optimal in every single direction and not optimal at all. + +Minimises `||A x - b||^2 / (2 n) + lambda ||x||_1`. + +Errors: +Returns an error on a shape mismatch or a negative penalty. + +Rust: `optimization::convex::lasso_coordinate_descent` + """ + ... + +def admm_lasso(a: Matrix | Sequence[Sequence[float]], b: list[float], lambda_: float, rho: float, iterations: int) -> list[float]: + """ +The lasso by the alternating direction method of multipliers. + +Splits the objective into the smooth least-squares part and the L1 part +with a copy of the variable, then alternates: a ridge solve, a soft +threshold, and a dual update. The factorisation of the ridge system does +not change between iterations, so it can be computed once -- which is what +makes ADMM cheap here despite doing a linear solve every step. + +Solves the same problem as `lasso_coordinate_descent` and must agree +with it. + +Errors: +Returns an error on a shape mismatch, a non-positive penalty parameter, or +a singular system. + +Rust: `optimization::convex::admm_lasso` + """ + ... + +def admm_generic(prox_f: Callable[[list[float], float], list[float]], prox_g: Callable[[list[float], float], list[float]], x0: list[float], rho: float, iterations: int) -> list[float]: + """ +A generic two-block ADMM. + +Minimises `f(x) + g(z)` subject to `x = z`, given only the proximal +operator of each part. The two halves never need to be handled together, +which is the point: a problem that is hard as a whole is often two easy +problems joined by a constraint. + +Panics: +Panics if `rho` is not positive or the starting point is empty. + +Rust: `optimization::convex::admm_generic` + """ + ... + +def elastic_net(a: Matrix | Sequence[Sequence[float]], b: list[float], l1: float, l2: float, iterations: int) -> list[float]: + """ +Elastic net regression: an L1 and an L2 penalty together. + +The L1 part selects variables and the L2 part keeps correlated ones +together. Pure lasso picks arbitrarily among a group of correlated +predictors and zeroes the rest, which is unstable under resampling; the +ridge term removes that arbitrariness. At `l1 = 0` it is ridge and at +`l2 = 0` it is the lasso, and the tests check both limits. + +Errors: +Returns an error on a shape mismatch or a negative penalty. + +Rust: `optimization::convex::elastic_net` + """ + ... + +def logistic_regression_fit(x: Matrix | Sequence[Sequence[float]], y: list[float], lambda_: float, iterations: int) -> list[float]: + """ +L2-penalised logistic regression, fitted by Newton's method. + +The penalised log-likelihood is strictly concave for any positive penalty, +so the maximum is unique and Newton's method converges quadratically to +it. Without the penalty, perfectly separable data has no finite maximiser +at all -- the coefficients run to infinity as the fitted probabilities +approach zero and one -- which is a property of the data rather than a +failure of the solver, and the penalty is what makes the problem +well posed. + +`y` holds zeros and ones. Returns the coefficients. + +Errors: +Returns an error on a shape mismatch, a label outside `{0, 1}`, or a +non-positive penalty. + +Rust: `optimization::convex::logistic_regression_fit` + """ + ... + +def quadratic_program_active_set(q: Matrix | Sequence[Sequence[float]], c: list[float], a: Matrix | Sequence[Sequence[float]], b: list[float]) -> tuple[list[float], list[float]]: + """ +A convex quadratic program with equality constraints, by the active-set +idea applied to the equalities alone. + +Minimises `x'Qx/2 + c'x` subject to `Ax = b`. With only equalities the +active set is fixed, so the whole problem is one KKT linear system: +stationarity and feasibility stacked together. The solution satisfies +`Qx + c + A'y = 0` exactly, which is what the tests check rather than +merely that the objective looks small. + +Errors: +Returns an error on a shape mismatch or a singular KKT system. + +Rust: `optimization::convex::quadratic_program_active_set` + """ + ... + +def kkt_residual(objective_gradient: list[float], constraint_values: list[float], constraint_gradients: list[list[float]], multipliers: list[float]) -> float: + """ +The norm of the Karush-Kuhn-Tucker residual at a candidate point. + +Stacks the stationarity condition `grad f + sum y_i grad c_i` and the +feasibility conditions `c_i(x) = 0`. Zero exactly at a constrained +stationary point, which makes it the natural way to check a constrained +solver: it tests the conditions the answer must satisfy rather than +comparing against another solver that could share the same mistake. + +Rust: `optimization::convex::kkt_residual` + """ + ... + +def dual_ascent(minimise_lagrangian: Callable[[list[float]], list[float]], constraints: Callable[[list[float]], list[float]], multipliers0: list[float], step: float, iterations: int) -> tuple[list[float], list[float]]: + """ +Dual ascent for an equality-constrained problem. + +Alternates minimising the Lagrangian over `x` with a gradient step on the +multipliers, whose gradient is the constraint violation itself. It +converges only under strong assumptions -- strict convexity of the +objective, chiefly -- which is precisely the gap that the augmented +Lagrangian and ADMM close by adding a penalty term. + +`minimise_lagrangian` returns the minimiser of `f(x) + y . c(x)` for the +given multipliers. + +Panics: +Panics if the step is not positive. + +Rust: `optimization::convex::dual_ascent` + """ + ... + +def convexity_check_numeric(f: Callable[[list[float]], float], bounds: list[tuple[float, float]], trials: int, rng: Rng) -> bool: + """ +Tests convexity numerically by sampling the midpoint inequality. + +A convex function satisfies `f((a+b)/2) <= (f(a) + f(b)) / 2` for every +pair. Sampling can only ever *refute* convexity, never establish it: a +single violating pair is a proof of non-convexity, while a million +satisfying pairs prove nothing about the pairs not tried. The return value +should be read accordingly -- `false` is a fact and `true` is an absence +of evidence. + +Panics: +Panics if the bounds are empty or `trials` is zero. + +Rust: `optimization::convex::convexity_check_numeric` + """ + ... + +def condition_number_effect_demo(kappa: float) -> tuple[int, int]: + """ +Iterations that gradient descent and conjugate gradients need on a +two-dimensional quadratic of the given condition number. + +Returns `(gradient descent, conjugate gradients)`. The contrast is the +whole point: gradient descent's error contracts by `(k-1)/(k+1)` per +step, so its count grows linearly in the condition number, while +conjugate gradients terminate in at most `n` steps whatever the +conditioning. At a condition number of a thousand that is hundreds of +iterations against two. + +Panics: +Panics if the condition number is below one. + +Rust: `optimization::convex::condition_number_effect_demo` + """ + ... diff --git a/bindings/python/python/numeria/optimization/game_theory.pyi b/bindings/python/python/numeria/optimization/game_theory.pyi new file mode 100644 index 0000000..0232d72 --- /dev/null +++ b/bindings/python/python/numeria/optimization/game_theory.pyi @@ -0,0 +1,734 @@ +""" +Game theory: equilibria, dynamics, cooperative solution concepts, auctions, and two-player search. The organising fact of the non-cooperative half is that equilibrium is a *fixed-point* condition and not an optimisation: no player is optimising against a fixed environment, because the environment is the other players doing the same thing. That is why the zero-sum case is easy and the general case is not. In a zero-sum game the two players' problems are linear programs dual to each other, so von Neumann's minimax theorem is a corollary of LP duality and the equilibrium is computable in polynomial time. In a bimatrix game there is no such dual, the equilibrium set can be disconnected, and the best general algorithms are pivoting schemes with exponential worst cases. The cooperative half asks a different question -- not what players will do but how a surplus they have already agreed to create should be split -- and its solution concepts are axiomatic. The Shapley value is the unique allocation satisfying efficiency, symmetry, the null-player property and additivity; the core is the set of allocations no coalition can improve on; and the two can be disjoint, since a game can have an empty core while the Shapley value always exists. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.statistics.distributions import Exponential +from numeria.linalg.matrix import Matrix +from numeria.monte_carlo import Rng + +class AlwaysCooperate: + """ +Always cooperate. + +Rust: `optimization::game_theory::AlwaysCooperate` + """ + ... + +class AlwaysDefect: + """ +Always defect: the unique equilibrium of the one-shot game and of any +finitely repeated game with a commonly known end. + +Rust: `optimization::game_theory::AlwaysDefect` + """ + ... + +class GameTree: + """ +A node of an extensive-form game tree. + +A leaf carries a payoff for each player; an internal node names the player +to move and its children. + +Rust: `optimization::game_theory::GameTree` + """ + ... + +class GenerousTitForTat: + """ +Tit for tat that forgives an occasional defection, which is what keeps two +copies of it from locking into mutual retaliation under noise. + +Rust: `optimization::game_theory::GenerousTitForTat` + """ + def __init__(self, forgiveness: float) -> None: ... + @property + def forgiveness(self) -> float: ... + +class Grim: + """ +Cooperate until defected on once, then defect forever. + +Rust: `optimization::game_theory::Grim` + """ + ... + +class Move: + """ +A move in the iterated prisoner's dilemma. + +Rust: `optimization::game_theory::Move` + """ + ... + +class Pavlov: + """ +Win-stay lose-shift: repeat the last move if it earned a good payoff, +switch if it did not. + +Rust: `optimization::game_theory::Pavlov` + """ + ... + +class RandomPlayer: + """ +Cooperate with fixed probability, ignoring the opponent. + +Rust: `optimization::game_theory::RandomPlayer` + """ + def __init__(self, cooperate_probability: float) -> None: ... + @property + def cooperate_probability(self) -> float: ... + +class TitForTat: + """ +Cooperate first, then copy the opponent's last move. + +Rust: `optimization::game_theory::TitForTat` + """ + ... + +def minimax_value(payoff: Matrix | Sequence[Sequence[float]]) -> tuple[float, list[float], list[float]]: + """ +The value of a two-player zero-sum game and the optimal mixed strategies, +as `(value, row strategy, column strategy)`. + +The row player's guaranteed floor and the column player's guaranteed +ceiling coincide. That coincidence is the minimax theorem, and it is not +assumed here: the two players' programs are LP duals, so strong duality +delivers it. What makes the result surprising is that it fails without +mixing -- in matching pennies the pure maximin is -1 and the pure minimax +is +1 -- so the theorem is really a statement about the power of +randomisation. + +Errors: +Returns an error if the underlying program has no optimum, which for a +finite game means a numerical failure rather than a modelling one. + +Rust: `optimization::game_theory::minimax_value` + """ + ... + +def dominated_strategies(payoff: Matrix | Sequence[Sequence[float]]) -> list[int]: + """ +The row indices strictly dominated by some other pure row. + +Strict domination is the one elimination that is always safe: a strictly +dominated strategy is played with probability zero in every equilibrium, +so removing it removes no equilibria. *Weak* domination does not have that +property, which is why only the strict version is offered. + +Rust: `optimization::game_theory::dominated_strategies` + """ + ... + +def iterated_elimination(a: Matrix | Sequence[Sequence[float]], b: Matrix | Sequence[Sequence[float]]) -> tuple[list[int], list[int]]: + """ +Iterated elimination of strictly dominated strategies, returning the row +and column indices that survive. + +The order of elimination does not matter for strict domination: the +surviving set is the same however the eliminations are sequenced. That is +a genuine theorem and it is what makes the procedure well defined -- the +weak-domination analogue is order dependent and so is not a solution +concept at all. + +`a` is the row player's payoff and `b` the column player's. + +Errors: +Returns an error if the two payoff matrices have different shapes. + +Rust: `optimization::game_theory::iterated_elimination` + """ + ... + +def best_response(payoff: Matrix | Sequence[Sequence[float]], opponent_mixed: list[float]) -> list[int]: + """ +The pure best responses to an opponent's mixed strategy. + +Returns every index attaining the maximum, not just one. The set matters: +a mixed equilibrium exists precisely because a player is indifferent among +several best responses, so an implementation that returned a single index +would be unable to express one. + +`payoff` is the responding player's own payoff matrix, with the responder +indexing rows. + +Panics: +Panics if the opponent's strategy has the wrong length. + +Rust: `optimization::game_theory::best_response` + """ + ... + +def nash_deviation_gain(a: Matrix | Sequence[Sequence[float]], b: Matrix | Sequence[Sequence[float]], p: list[float], q: list[float]) -> float: + """ +The largest gain any player could get by deviating unilaterally from the +given strategy profile. + +Zero -- to tolerance -- is exactly the definition of a Nash equilibrium, +so this is the certificate that any equilibrium-finding routine should be +held to. A deviation only ever needs to be checked against *pure* +strategies, since the payoff is linear in one's own mixture and a linear +function on a simplex attains its maximum at a vertex. + +Errors: +Returns an error on a shape mismatch between the payoffs and the profile. + +Rust: `optimization::game_theory::nash_deviation_gain` + """ + ... + +def nash_2x2(a: Matrix | Sequence[Sequence[float]], b: Matrix | Sequence[Sequence[float]]) -> list[tuple[list[float], list[float]]]: + """ +Every Nash equilibrium of a 2x2 bimatrix game, pure and mixed. + +Small enough to enumerate completely, which makes it the reference the +general algorithms are checked against. The mixed equilibrium, when it +exists, has the property that trips people up: each player's mixture is +chosen to make the *opponent* indifferent, not themselves. One's own +payoff plays no part in one's own probabilities. + +Errors: +Returns an error unless both matrices are 2x2. + +Rust: `optimization::game_theory::nash_2x2` + """ + ... + +def nash_support_enumeration(a: Matrix | Sequence[Sequence[float]], b: Matrix | Sequence[Sequence[float]], max_support: int) -> list[tuple[list[float], list[float]]]: + """ +Nash equilibria by support enumeration. + +For each pair of candidate supports, the indifference conditions are a +linear system: every strategy in a player's support must earn the same +expected payoff, and the probabilities must sum to one. Solving it and +then *checking* the result -- non-negative probabilities, and no +unsupported strategy earning more -- is what makes the method sound. The +checking is not optional bookkeeping: most supports produce a solution to +the linear system that is not an equilibrium at all. + +Exponential in the number of strategies, so `max_support` bounds the +support size considered. + +Errors: +Returns an error on a shape mismatch. + +Rust: `optimization::game_theory::nash_support_enumeration` + """ + ... + +def nash_bimatrix_lemke_howson(a: Matrix | Sequence[Sequence[float]], b: Matrix | Sequence[Sequence[float]], initial_label: int) -> tuple[list[float], list[float]]: + """ +One Nash equilibrium of a bimatrix game by the Lemke-Howson algorithm. + +Complementary pivoting on the two players' best-response polytopes. Every +vertex pair is labelled by the strategies that are either unplayed or +unprofitable; a pair carrying all labels is an equilibrium, and the +algorithm walks an edge path from the artificial equilibrium at the origin +to one that does. The path cannot revisit a vertex and the polytopes are +finite, so it terminates -- which is a constructive proof that a Nash +equilibrium exists, independent of Kakutani's fixed-point theorem. + +`initial_label` selects which strategy's label is dropped to start the +path; different choices generally reach different equilibria. + +Errors: +Returns an error on a shape mismatch, an out-of-range label, or a +degenerate game where the pivot becomes ambiguous. + +Rust: `optimization::game_theory::nash_bimatrix_lemke_howson` + """ + ... + +def correlated_equilibrium_lp(a: Matrix | Sequence[Sequence[float]], b: Matrix | Sequence[Sequence[float]]) -> Matrix: + """ +A correlated equilibrium of maximum expected total payoff, as a joint +distribution over strategy profiles. + +The reason this is an LP and Nash equilibrium is not: the unknown is the +joint distribution itself rather than each player's marginal, so the +incentive constraints -- obeying the recommendation beats any deviation, +*conditional* on having received it -- are linear. Every Nash equilibrium +is a correlated equilibrium (take the product of the marginals), so the +set is never empty, and it is generally larger: correlation can achieve +payoffs outside the convex hull of the Nash outcomes. + +Errors: +Returns an error on a shape mismatch or if the program has no optimum. + +Rust: `optimization::game_theory::correlated_equilibrium_lp` + """ + ... + +def fictitious_play(a: Matrix | Sequence[Sequence[float]], b: Matrix | Sequence[Sequence[float]], iterations: int) -> tuple[list[float], list[float]]: + """ +Fictitious play: each player best-responds to the empirical frequency of +the other's past moves. + +Returns the two empirical frequency vectors. It converges to equilibrium +in zero-sum games, in 2xN games, and in games solvable by iterated strict +dominance -- and famously does *not* converge in general, Shapley's 3x3 +example cycling forever. So this is a model of learning that sometimes +finds equilibrium, not an algorithm for computing one. + +Errors: +Returns an error on a shape mismatch. + +Rust: `optimization::game_theory::fictitious_play` + """ + ... + +def replicator_dynamics(payoff: Matrix | Sequence[Sequence[float]], x0: list[float], t_end: float, dt: float) -> list[list[float]]: + """ +The replicator dynamic for a symmetric game, returning the trajectory. + +`dx_i/dt = x_i (e_i . A x - x . A x)`: a strategy grows when it does +better than the population average. The equation arises from asexual +reproduction proportional to payoff, and its fixed points include every +symmetric Nash equilibrium -- but not only those, since every vertex of +the simplex is a fixed point whether or not it is an equilibrium. The +simplex is invariant, which is what makes the dynamic well posed. + +Errors: +Returns an error unless the payoff is square, the initial population is a +distribution over its strategies, and the step is positive. + +Rust: `optimization::game_theory::replicator_dynamics` + """ + ... + +def evolutionarily_stable_check(payoff: Matrix | Sequence[Sequence[float]], strategy: list[float], tol: float) -> bool: + """ +Whether a strategy is evolutionarily stable in a symmetric game. + +Maynard Smith's two conditions: the strategy is a symmetric Nash +equilibrium, and against any alternative best response it does strictly +better than that alternative does against itself. The second condition is +what "stable" adds to "equilibrium" -- it says a small invading mutant +earns less than the resident and so dies out, which a mere Nash +equilibrium does not guarantee. + +Checked against pure alternatives, which suffices: the payoff is linear in +the mutant's mixture, so if no pure mutant invades then none does. + +Errors: +Returns an error unless the payoff is square and the strategy is a +distribution over its rows. + +Rust: `optimization::game_theory::evolutionarily_stable_check` + """ + ... + +def hawk_dove(v: float, c: float) -> Matrix: + """ +The hawk-dove game: contesting a resource worth `v` at an injury cost `c`. + +Returns the symmetric payoff matrix with hawk first. When `c > v` the +game has a mixed ESS playing hawk with probability `v / c`, which is the +canonical demonstration that a population can be stable while every +individual in it is randomising. + +Panics: +Panics unless the cost is positive. + +Rust: `optimization::game_theory::hawk_dove` + """ + ... + +def prisoners_dilemma(t: float, r: float, p: float, s: float) -> Matrix: + """ +The prisoner's dilemma with the conventional temptation, reward, +punishment and sucker payoffs. Cooperate is strategy zero. + +Panics: +Panics unless `t > r > p > s`, which is what makes it a dilemma at all -- +defection strictly dominates while mutual cooperation beats mutual +defection. + +Rust: `optimization::game_theory::prisoners_dilemma` + """ + ... + +def stag_hunt() -> Matrix: + """ +The stag hunt: two pure equilibria, one payoff dominant and one risk +dominant. Hunting stag is strategy zero. + +Rust: `optimization::game_theory::stag_hunt` + """ + ... + +def chicken() -> Matrix: + """ +Chicken, also called hawk-dove in its ordinal form: two asymmetric pure +equilibria and one mixed. Swerving is strategy zero. + +Rust: `optimization::game_theory::chicken` + """ + ... + +def matching_pennies() -> Matrix: + """ +Matching pennies: the smallest zero-sum game with no pure equilibrium. + +Rust: `optimization::game_theory::matching_pennies` + """ + ... + +def rock_paper_scissors() -> Matrix: + """ +Rock-paper-scissors as a zero-sum payoff matrix, in that order. + +Rust: `optimization::game_theory::rock_paper_scissors` + """ + ... + +def shapley_value(v: Callable[[int], float], n: int) -> list[float]: + """ +The Shapley value of a cooperative game given by its characteristic +function on coalitions encoded as bitmasks. + +Player `i`'s value is the average over all orderings of the players of +what `i` adds to the coalition already formed. The averaging is what makes +it fair in a precise sense: it is the *unique* allocation satisfying +efficiency, symmetry, the null-player property and additivity, so any +objection to the Shapley value has to be an objection to one of those. + +Exact, and so exponential: `2^n` coalitions. + +Errors: +Returns an error unless `1 <= n <= 20`, beyond which the enumeration is +not worth attempting. + +Rust: `optimization::game_theory::shapley_value` + """ + ... + +def shapley_monte_carlo(v: Callable[[int], float], n: int, samples: int, rng: Rng) -> list[float]: + """ +The Shapley value estimated by sampling random orderings. + +The same average as `shapley_value`, taken over sampled permutations +instead of all of them. Unbiased, with error falling as the reciprocal +square root of the sample count, which is what makes it the only option +once the player count passes about twenty. + +Errors: +Returns an error if there are no players or no samples. + +Rust: `optimization::game_theory::shapley_monte_carlo` + """ + ... + +def banzhaf_index(v: Callable[[int], float], n: int) -> list[float]: + """ +The normalised Banzhaf index: each player's share of the swings they can +make. + +Differs from the Shapley value in what it averages over -- coalitions +rather than orderings -- and so weights the sizes differently. The two +disagree, and the disagreement is the point: there is no single correct +measure of power, only different axiomatisations of it. + +A game in which no player ever swings anything has no power to apportion, +and the shares come back as zeros rather than as a division by zero. + +Errors: +Returns an error unless `1 <= n <= 20`. + +Rust: `optimization::game_theory::banzhaf_index` + """ + ... + +def core_check_small(v: Callable[[int], float], n: int, allocation: list[float]) -> bool: + """ +Whether an allocation lies in the core: efficient, and unimprovable by any +coalition. + +The core can be empty -- three players splitting a pound where any two can +take it all has no core allocation at all -- which is exactly why the +Shapley value, which always exists, is worth having as well. + +Errors: +Returns an error on a bad player count or allocation length. + +Rust: `optimization::game_theory::core_check_small` + """ + ... + +def nucleolus_small(v: Callable[[int], float], n: int) -> list[float]: + """ +The nucleolus of a small cooperative game. + +Lexicographically minimises the vector of coalition excesses -- how much +each coalition is short of what it could get on its own -- worst first. +Solved as a sequence of linear programs: maximise the smallest slack, fix +whichever coalitions are then tight, repeat on the rest. Unlike the core +it is never empty, and unlike the Shapley value it always lies in the core +when the core is non-empty, which is the property that motivates it. + +Errors: +Returns an error for more than about a dozen players, or if a program +fails. + +Rust: `optimization::game_theory::nucleolus_small` + """ + ... + +def voting_power_weighted(weights: list[float], quota: float) -> list[float]: + """ +The Banzhaf power of each voter in a weighted voting game. + +The point of the exercise is that power is not proportional to weight. A +voter with a large weight can have the same power as a small one -- and a +voter with positive weight can be a dummy with no power at all, if no +coalition ever needs them. + +Errors: +Returns an error for an empty or oversized electorate. + +Rust: `optimization::game_theory::voting_power_weighted` + """ + ... + +def first_price_auction_equilibrium_uniform(n: int) -> float: + """ +The symmetric equilibrium bid shading factor in a first-price sealed-bid +auction with `n` bidders whose values are uniform on `[0, 1]`. + +The equilibrium bid is `(n - 1) / n` times one's value. Shading is not a +mistake: bidding one's value in a first-price auction guarantees zero +surplus whether one wins or not. As the field grows the shading vanishes, +which is the mechanism behind revenue equivalence. + +Panics: +Panics unless there are at least two bidders. + +Rust: `optimization::game_theory::first_price_auction_equilibrium_uniform` + """ + ... + +def second_price_dominant_check() -> bool: + """ +Confirms by exhaustive case analysis that truthful bidding weakly +dominates in a second-price auction. + +Returns true when no misreport ever beats the truth, over a grid of +values, bids and highest-rival bids. The argument is a two-case one -- +bidding above one's value can only win auctions one regrets, bidding below +can only lose auctions one wanted -- and neither case depends on beliefs +about the rivals, which is what makes the dominance so strong. + +Rust: `optimization::game_theory::second_price_dominant_check` + """ + ... + +def revenue_equivalence_sim(n: int, trials: int, rng: Rng) -> tuple[float, float]: + """ +Simulates first- and second-price auctions with uniform values, returning +the two average revenues. + +The revenue equivalence theorem says they coincide: any two mechanisms +that allocate to the highest value and give a zero-value bidder zero +surplus raise the same expected revenue. The first-price auction collects +a shaded bid from the winner, the second-price auction collects the +runner-up's full value, and in expectation those are the same number. + +Errors: +Returns an error for fewer than two bidders or no trials. + +Rust: `optimization::game_theory::revenue_equivalence_sim` + """ + ... + +def vcg_auction(bids: list[list[float]], items: int) -> tuple[list[Optional[int]], list[float]]: + """ +A VCG auction for distinct items, one per winner. + +`bids[i][k]` is bidder `i`'s value for item `k`. Returns the item assigned +to each bidder, if any, and each bidder's payment. The payment is the +externality imposed: the welfare others would have had in one's absence, +less the welfare they actually get. That is what makes truthful bidding +dominant -- one's own report shifts only the allocation, never the price +one pays for it. + +The welfare-maximising assignment is found by exhaustive search, so this is +for small instances. + +Errors: +Returns an error for ragged bids or more than eight bidders or items. + +Rust: `optimization::game_theory::vcg_auction` + """ + ... + +def cake_cutting_divide_choose(density_a: Callable[[float], float], density_b: Callable[[float], float], resolution: int) -> tuple[float, float]: + """ +Divide and choose over a cake whose value density differs between the two +players, returning each one's share of their own total value. + +`density_a` and `density_b` give the two valuations over `[0, 1]`, sampled +on `resolution` intervals. The cutter divides at their own halfway point +and the chooser takes the piece they prefer, so the cutter gets exactly a +half by their own measure and the chooser at least a half by theirs. That +is envy-freeness for two players -- and it does not extend: no analogous +finite protocol was known for three until 1960, and for four until 2016. + +Errors: +Returns an error if the resolution is zero or a valuation is not positive. + +Rust: `optimization::game_theory::cake_cutting_divide_choose` + """ + ... + +def gale_shapley_optimality_check(prefs_a: list[list[int]], prefs_b: list[list[int]]) -> bool: + """ +Confirms that the deferred-acceptance matching is stable and optimal for +the proposing side. + +Gale-Shapley's guarantee is sharper than stability: among *all* stable +matchings, every proposer gets their best possible partner and every +receiver their worst. So the same algorithm run from the other side gives +a different matching, and which side proposes is a distributional +decision, not an implementation detail. Both halves are checked here by +enumerating the stable matchings directly. + +Errors: +Returns an error unless the preference lists are square, complete, and no +larger than seven a side. + +Rust: `optimization::game_theory::gale_shapley_optimality_check` + """ + ... + +def stackelberg_2x2(a: Matrix | Sequence[Sequence[float]], b: Matrix | Sequence[Sequence[float]]) -> tuple[int, int, float, float]: + """ +The Stackelberg equilibrium of a 2x2 game where the row player commits +first to a *pure* strategy, as +`(leader move, follower move, leader payoff, follower payoff)`. + +Committing to a pure strategy is at least as good as any pure equilibrium +-- the leader can commit to what they would have played anyway, and the +follower's reply is unchanged -- and it is often strictly better, which is +what first-mover advantage means. + +It is not, however, at least as good as every *mixed* equilibrium. The +general theorem is about commitment to mixed strategies; restricted to +pure ones a leader can end up below their mixed Nash payoff, since the +mixture they would have randomised over is no longer available to them. + +Errors: +Returns an error unless both matrices are 2x2. + +Rust: `optimization::game_theory::stackelberg_2x2` + """ + ... + +def cournot_equilibrium(demand_intercept: float, demand_slope: float, costs: list[float]) -> list[float]: + """ +The Cournot equilibrium quantities for `n` firms with constant marginal +costs facing a linear inverse demand `p = intercept - slope * Q`. + +Each firm's best response is linear in the others' total, and the system +solves in closed form. The comparison with Bertrand is the standard +lesson: competing in quantities leaves price above marginal cost however +many firms there are, while competing in prices drives it to marginal cost +with only two. + +Errors: +Returns an error for no firms, a non-positive slope, or a cost above the +choke price. + +Rust: `optimization::game_theory::cournot_equilibrium` + """ + ... + +def bertrand_equilibrium(costs: list[float]) -> float: + """ +The Bertrand equilibrium price with identical firms: marginal cost. + +Two firms suffice. Any price above cost is undercut by a rival who then +takes the whole market, so the only equilibrium is the competitive one -- +the "Bertrand paradox", since it predicts that a duopoly behaves like +perfect competition. With asymmetric costs the low-cost firm prices just +under the rival's cost, which is what this returns. + +Errors: +Returns an error for fewer than two firms. + +Rust: `optimization::game_theory::bertrand_equilibrium` + """ + ... + +def public_goods_game_sim(n: int, multiplier: float, rounds: int, rng: Rng) -> list[float]: + """ +A public goods game with a linear return, returning the average +contribution per round. + +Each of `n` players contributes some fraction of an endowment to a pot +that is multiplied by `multiplier` and split evenly. A unit contributed +costs its contributor one and returns `multiplier / n` to them, so the +threshold is at `multiplier = n`: below it contributing is individually +irrational and collectively optimal, which is the free-rider problem in +its simplest form, and above it the two coincide. + +Players here are conditional cooperators, matching what the others gave +and adjusting by their own marginal return -- the rule the laboratory +evidence supports. Note that imitating the highest *earner* instead would +drive contributions to zero at any multiplier whatever, because within a +round every player receives the same share and so the smallest contributor +always earns most. That comparison is between players, and the incentive +that matters is the effect of a player's own contribution on their own +earnings; conflating the two is an easy way to build a model that cannot +represent the threshold at all. + +Errors: +Returns an error for bad parameters. + +Rust: `optimization::game_theory::public_goods_game_sim` + """ + ... + +def colonel_blotto_sim(fields: int, troops: int, strategies: int, rng: Rng) -> Matrix: + """ +A Colonel Blotto tournament between random allocations, returning the +win-rate matrix between the sampled strategies. + +Troops are split across fields and each field goes to whoever committed +more. The game has no pure equilibrium and no dominant allocation: every +deterministic plan is beaten by some other, so the equilibrium is +necessarily in mixed strategies. The matrix is the empirical payoff of the +sampled strategies against one another. + +Errors: +Returns an error for fewer than two fields, no troops, or fewer than two +sampled strategies. + +Rust: `optimization::game_theory::colonel_blotto_sim` + """ + ... + +def backward_induction(tree: GameTree) -> tuple[list[int], list[float]]: + """ +Backward induction on a game tree, returning the equilibrium path of moves +and the payoffs it reaches. + +Solving from the leaves upward gives a subgame perfect equilibrium, which +rules out the equilibria of the normal form that rest on threats the +threatener would not want to carry out. That is the whole content of the +refinement: a Nash equilibrium can be sustained by a promise to behave +irrationally off the path, and backward induction cannot represent one. + +Errors: +Returns an error if a decision node has no children or the payoff vectors +disagree in length. + +Rust: `optimization::game_theory::backward_induction` + """ + ... diff --git a/bindings/python/python/numeria/optimization/integer.pyi b/bindings/python/python/numeria/optimization/integer.pyi new file mode 100644 index 0000000..a89925f --- /dev/null +++ b/bindings/python/python/numeria/optimization/integer.pyi @@ -0,0 +1,518 @@ +""" +Integer programming, dynamic programming, and combinatorial search. Adding "and the answer must be a whole number" to a linear program changes its character completely. The feasible region stops being a convex polyhedron and becomes a scatter of lattice points inside one, so the guarantee that made linear programming easy -- that an optimum sits at a vertex, reachable by local moves -- is gone. What remains is the relaxation: drop the integrality, solve the linear program, and use its value as a bound on what any integer solution could achieve. Branch and bound is that observation applied recursively, and the bound is the only reason it terminates before enumerating everything. Most problems here have that flavour. A few do not, and those are the dynamic programming classics: when a problem decomposes into overlapping subproblems whose optimal solutions compose, the exponential search collapses to a table and the answer is exact in polynomial time. Knapsack, edit distance and the rest are here because the boundary between the two situations is worth being able to see -- the 0/1 knapsack is NP-hard and yet has a pseudo-polynomial table, which is not a contradiction but a statement about what "polynomial" is measured against. Where an exact method is impractical the module gives a greedy one with its proven ratio: first-fit-decreasing bin packing within `11/9` of optimal, greedy set cover within `H_n`, longest-processing-time scheduling within `4/3 - 1/(3m)`. Those ratios are worst-case guarantees rather than typical behaviour, and the tests check the guarantee holds against an exact answer on small instances rather than checking the greedy answer is merely plausible. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.statistics.distributions import Exponential +from numeria.optimization.lp import LpProblem +from numeria.linalg.matrix import Matrix + +class EditOp: + """ +One edit in a transformation from one sequence to another. + +Rust: `optimization::integer::EditOp` + """ + ... + +def branch_and_bound(p: LpProblem, integer_vars: list[int], node_limit: int) -> Optional[tuple[list[float], float]]: + """ +Solves a mixed-integer linear program by branch and bound. + +Solves the linear relaxation; if the named variables all came out integral +the answer is optimal, and otherwise one fractional variable is chosen and +the problem split into the branch where it is rounded down and the branch +where it is rounded up. The relaxation's value bounds every integer +solution below it, so a branch whose relaxation is already worse than the +best integer solution found can be discarded whole -- which is the entire +content of the method, and the reason it beats enumeration. + +`node_limit` caps the search. Returns `None` if the problem is infeasible +over the integers, or if the limit is reached before any integer solution +is found. + +Errors: +Returns an error if a named variable is out of range, or the underlying +linear program is malformed. + +Rust: `optimization::integer::branch_and_bound` + """ + ... + +def gomory_cuts(p: LpProblem, integer_vars: list[int], max_cuts: int) -> LpProblem: + """ +Adds Chvatal-Gomory rounding cuts to a linear program. + +A cut is only worth the name if it is valid: satisfied by every integer +point of the feasible region, while removing part of the fractional +relaxation. The rounding cut earns that as follows. Scale a `<=` row by +some `lambda > 0`, so `lambda a . x <= lambda b` still holds. Rounding each +coefficient down can only lower the left-hand side when `x >= 0`, so +`floor(lambda a) . x <= lambda b`. But the left-hand side is now an integer +combination of integers, hence an integer, so it is bounded by the floor of +the right: + + +Every non-negative integer point survives that, and a fractional one need +not. Multipliers are tried at the reciprocals of the row's own +coefficients and at a few small fractions, and a cut is kept only when the +current relaxation optimum actually violates it. + +Errors: +Returns an error unless every variable is integer and non-negative, which +is what the rounding argument requires, or if the relaxation has no +optimum. + +Rust: `optimization::integer::gomory_cuts` + """ + ... + +def knapsack_01(values: list[int], weights: list[int], capacity: int) -> tuple[int, list[bool]]: + """ +The 0/1 knapsack by dynamic programming: each item taken at most once. + +Returns the best value and which items to take. The table is +`O(n * capacity)`, which is polynomial in the *value* of the capacity but +exponential in the number of digits it takes to write it down -- the +problem is NP-hard, and the table is pseudo-polynomial rather than a +contradiction of that. + +Panics: +Panics unless the value and weight lists have the same length. + +Rust: `optimization::integer::knapsack_01` + """ + ... + +def knapsack_unbounded(values: list[int], weights: list[int], capacity: int) -> tuple[int, list[int]]: + """ +The unbounded knapsack: each item available without limit. + +Returns the best value and how many of each item to take. A one-dimensional +table suffices, because an item may be reused within the same pass. + +Panics: +Panics unless the lists match in length and every weight is positive. + +Rust: `optimization::integer::knapsack_unbounded` + """ + ... + +def knapsack_bounded(values: list[int], weights: list[int], limits: list[int], capacity: int) -> tuple[int, list[int]]: + """ +The bounded knapsack: each item available up to its own limit. + +Expanded by binary splitting -- an item with a limit of `k` becomes items +of multiplicity `1, 2, 4, ...` summing to `k` -- so any count up to the +limit is expressible and the 0/1 solver applies. That costs +`O(log k)` copies rather than the `k` a naive expansion would need. + +Panics: +Panics unless all three lists match in length. + +Rust: `optimization::integer::knapsack_bounded` + """ + ... + +def knapsack_multiple(values: list[int], weights: list[int], capacities: list[int]) -> tuple[int, list[Optional[int]]]: + """ +The multiple knapsack: several bins, each item into at most one. + +Solved greedily by value density with a first-fit placement, which is not +exact -- the problem is NP-hard even with two bins -- so the result is a +lower bound on the optimum. Returns the total value and the bin each item +went into, `None` for an item left out. + +Panics: +Panics unless the lists match in length. + +Rust: `optimization::integer::knapsack_multiple` + """ + ... + +def knapsack_branch_bound(values: list[int], weights: list[int], capacity: int) -> tuple[int, list[bool]]: + """ +The 0/1 knapsack by branch and bound over the fractional relaxation. + +The relaxation of a knapsack is solved by taking items in density order +and splitting the last one, which gives a bound in linear time once the +items are sorted. Nodes whose bound cannot beat the incumbent are pruned. + +Exact, and must agree with `knapsack_01` on every instance -- one walks a +table and the other a search tree, so their agreement is a real check on +both. + +Panics: +Panics unless the lists match in length. + +Rust: `optimization::integer::knapsack_branch_bound` + """ + ... + +def subset_sum(xs: list[int], target: int) -> Optional[list[int]]: + """ +Indices of a subset summing exactly to `target`, if one exists. + +Panics: +Panics if the values are large enough that the table would not fit. + +Rust: `optimization::integer::subset_sum` + """ + ... + +def subset_sum_count(xs: list[int], target: int) -> int: + """ +How many subsets sum exactly to `target`. + +Counted as a `BigInt`, since the number of subsets of an `n`-element set +is `2^n` and the count routinely overflows a machine word well before the +table does. + +Rust: `optimization::integer::subset_sum_count` + """ + ... + +def partition_min_diff(xs: list[int]) -> tuple[int, list[bool]]: + """ +Splits the values into two groups whose totals are as close as possible. + +Returns the difference and the membership flags. The problem is +NP-hard in general and solved here by the subset-sum table over half the +total, which is exact and pseudo-polynomial. + +Rust: `optimization::integer::partition_min_diff` + """ + ... + +def bin_packing_ffd(sizes: list[float], capacity: float) -> list[list[int]]: + """ +Bin packing by first-fit-decreasing: sort the items large to small and put +each into the first bin it fits. + +Returns the item indices in each bin. The rule uses at most +`11/9 OPT + 6/9` bins, a bound that is tight -- so the tests check the +guarantee against an exact answer rather than checking the result merely +looks reasonable. + +Panics: +Panics if any item exceeds the bin capacity, which makes packing +impossible rather than merely hard. + +Rust: `optimization::integer::bin_packing_ffd` + """ + ... + +def bin_packing_lower_bound(sizes: list[float], capacity: float) -> int: + """ +The fewest bins any packing could use: the total size divided by the +capacity, rounded up. + +A valid lower bound because a bin holds at most `capacity`, so no packing +can use fewer. It is not always attainable -- three items of size 0.4 need +two bins though their total is 1.2 -- which is exactly why it is a bound +and not an answer. + +Rust: `optimization::integer::bin_packing_lower_bound` + """ + ... + +def bin_packing_exact_small(sizes: list[float], capacity: float) -> list[list[int]]: + """ +The exact minimum number of bins, by trying each count in turn. + +Exponential, and meant for the small instances the tests use to check the +first-fit-decreasing guarantee. Returns the packing. + +Panics: +Panics under the same conditions as `bin_packing_ffd`. + +Rust: `optimization::integer::bin_packing_exact_small` + """ + ... + +def set_cover_greedy(universe_n: int, sets: list[list[int]]) -> Optional[list[int]]: + """ +Greedy set cover: repeatedly take the set covering the most of what is +still uncovered. + +Returns the indices of the chosen sets, or `None` if the sets do not cover +the universe at all. Greedy uses at most `H_n` times the optimal number of +sets, where `H_n` is the `n`-th harmonic number, and no polynomial +algorithm does asymptotically better unless P equals NP -- so this is not +a placeholder for something better. + +Rust: `optimization::integer::set_cover_greedy` + """ + ... + +def set_cover_exact_small(universe_n: int, sets: list[list[int]]) -> Optional[list[int]]: + """ +The exact minimum set cover, by trying every subset of the sets in order of +size. + +For the small instances that make the greedy ratio checkable. + +Panics: +Panics if there are more than 20 sets, where the enumeration stops being +reasonable. + +Rust: `optimization::integer::set_cover_exact_small` + """ + ... + +def facility_location_greedy(open_costs: list[float], serve_costs: Matrix | Sequence[Sequence[float]]) -> tuple[float, list[bool]]: + """ +Uncapacitated facility location, solved greedily. + +`open_costs[i]` is the fixed cost of opening facility `i` and +`serve_costs[(i, j)]` the cost of serving client `j` from it. Facilities +are opened one at a time, each time the one whose opening cost plus +improved service most reduces the total. + +Returns the total cost and which facilities to open. + +Panics: +Panics unless the shapes agree and there is at least one facility. + +Rust: `optimization::integer::facility_location_greedy` + """ + ... + +def cutting_stock_column_generation(demand: list[int], lengths: list[int], stock_length: int, max_rounds: int) -> float: + """ +The cutting stock problem by column generation, relaxed. + +Each cutting pattern is a column of the linear program, and there are far +too many to write down, so patterns are generated on demand: solve the +relaxation over the patterns in hand, read the dual prices off it, and ask +which single new pattern would be most profitable at those prices. That +subproblem is an unbounded knapsack, and when its best pattern is not +profitable the relaxation is optimal over *all* patterns without ever +having enumerated them. + +Returns the relaxed number of stock lengths needed, which lower-bounds the +integer answer. + +Errors: +Returns an error if the inputs disagree in length, or a piece is longer +than the stock. + +Rust: `optimization::integer::cutting_stock_column_generation` + """ + ... + +def coin_change_min(coins: list[int], amount: int) -> Optional[list[int]]: + """ +The fewest coins summing to `amount`, or `None` if no combination does. + +Returns how many of each denomination. Greedy is wrong for general +denominations -- with coins 1, 3, 4 and an amount of 6, greedy takes +4 + 1 + 1 while two threes do it -- so this is a table, not a loop. + +Rust: `optimization::integer::coin_change_min` + """ + ... + +def coin_change_count(coins: list[int], amount: int) -> int: + """ +How many combinations of coins sum to `amount`, order disregarded. + +Iterating coins in the outer loop is what makes this count combinations +rather than permutations: each coin is considered once for the whole table, +so `1 + 2` and `2 + 1` are never both counted. + +Rust: `optimization::integer::coin_change_count` + """ + ... + +def longest_increasing_subsequence(x: list[float]) -> list[int]: + """ +Indices of a longest strictly increasing subsequence, in `O(n log n)`. + +The trick is to keep, for each length, the smallest value that can end a +subsequence of that length. That list is sorted by construction, so the +position each new element belongs at is a binary search rather than a scan +-- which is what turns the quadratic table into an `n log n` sweep. + +Rust: `optimization::integer::longest_increasing_subsequence` + """ + ... + +def edit_distance(a: list[int], b: list[int]) -> int: + """ +The Levenshtein distance: the fewest single-symbol insertions, deletions +and substitutions turning `a` into `b`. + +It is a metric on sequences -- symmetric, zero only between equal +sequences, and obeying the triangle inequality -- which is what makes it +usable for clustering and nearest-neighbour search rather than merely a +similarity score. + +Rust: `optimization::integer::edit_distance` + """ + ... + +def edit_distance_ops(a: list[int], b: list[int]) -> list[EditOp]: + """ +The edits themselves, in order, from a full table. + +Applying them to `a` reproduces `b`, and their count of non-`Keep` +operations is exactly `edit_distance`. + +Rust: `optimization::integer::edit_distance_ops` + """ + ... + +def longest_common_subsequence(a: list[int], b: list[int]) -> list[int]: + """ +A longest common subsequence of two sequences. + +Rust: `optimization::integer::longest_common_subsequence` + """ + ... + +def matrix_chain_order(dims: list[int]) -> tuple[int, str]: + """ +The cheapest way to parenthesise a chain of matrix multiplications. + +`dims` holds the shared dimensions: matrix `k` is `dims[k]` by +`dims[k + 1]`. Returns the scalar multiplication count and the +parenthesisation as a string. + +The order matters enormously -- multiplying a `1x100`, `100x1` and `1x100` +chain costs 200 one way and 20,000 the other -- and the number of +parenthesisations is Catalan, so the table is what makes it tractable. + +Panics: +Panics unless there are at least two dimensions. + +Rust: `optimization::integer::matrix_chain_order` + """ + ... + +def rod_cutting(prices: list[int], n: int) -> tuple[int, list[int]]: + """ +The most valuable way to cut a rod of length `n` into pieces. + +`prices[k]` is what a piece of length `k + 1` sells for. Returns the value +and the piece lengths. + +Rust: `optimization::integer::rod_cutting` + """ + ... + +def egg_drop(eggs: int, floors: int) -> int: + """ +The fewest drops that always determine the critical floor, with `eggs` +eggs and `floors` floors. + +The classic answer for two eggs and a hundred floors is fourteen: drop +from 14, then 27, then 39, and so on, each interval one shorter than the +last so the worst case stays flat. + +Rust: `optimization::integer::egg_drop` + """ + ... + +def optimal_bst(frequencies: list[float]) -> float: + """ +The expected search cost of the optimal binary search tree over keys with +the given access frequencies. + +Frequencies are taken in key order. The optimum is not the balanced tree: +a key accessed far more often than the rest belongs near the root even if +that unbalances everything else. + +Rust: `optimization::integer::optimal_bst` + """ + ... + +def viterbi_generic(transition: Matrix | Sequence[Sequence[float]], emission: Matrix | Sequence[Sequence[float]]) -> list[int]: + """ +The least-cost state path through a trellis. + +`transition[(a, b)]` is the cost of moving from state `a` to state `b`, and +`emission[(s, t)]` the cost of state `s` at time `t`. Returns the best path. + +The same recursion as the probabilistic Viterbi algorithm in +`stochastic::hmm`, stated in costs rather than log-probabilities -- which +is the more general form, since any additive path cost works. + +Errors: +Returns an error if the matrices disagree in shape or there are no steps. + +Rust: `optimization::integer::viterbi_generic` + """ + ... + +def exact_cover_dlx(matrix: list[list[bool]]) -> Optional[list[int]]: + """ +Solves an exact cover problem: choose rows so that every column is covered +exactly once. + +Implemented as Knuth's Algorithm X with the column-selection heuristic that +makes dancing links effective -- always branch on the column with the +fewest remaining options, which fails fast and keeps the search tree +narrow. The doubly linked list of the classic implementation is replaced +here by bitmask bookkeeping, which is the same algorithm with the same +search order for the column counts this module needs. + +Returns the chosen row indices, or `None` if no exact cover exists. + +Errors: +Returns an error for a ragged matrix or more than 64 columns. + +Rust: `optimization::integer::exact_cover_dlx` + """ + ... + +def n_queens(n: int) -> list[list[int]]: + """ +Every placement of `n` non-attacking queens, each as the column of the +queen in each row. + +Panics: +Panics if `n` exceeds 12, where the count runs into the hundreds of +thousands and the list stops being a sensible return value. + +Rust: `optimization::integer::n_queens` + """ + ... + +def n_queens_count(n: int) -> int: + """ +How many placements of `n` non-attacking queens exist. + +The sequence begins 1, 0, 0, 2, 10, 4, 40, 92 for boards one to eight wide +-- there is no solution on a three-square board, and a six-square board has +fewer than a five-square one, which is the usual surprise. + +Panics: +Panics if `n` exceeds 14. + +Rust: `optimization::integer::n_queens_count` + """ + ... + +def constraint_propagation_ac3(domains: list[int], constraints: list[tuple[int, int]]) -> Optional[list[int]]: + """ +Arc consistency by AC-3: prunes values that cannot participate in any +solution. + +`domains[i]` is a bitmask of the values variable `i` may take, and +`constraints` lists pairs `(i, j)` that must differ. Repeatedly removes any +value in one domain with no support in a neighbour's, until nothing +changes. + +Returns the reduced domains, or `None` if some domain empties -- which +proves the constraints unsatisfiable without any search. AC-3 never removes +a value that appears in a solution, so the reduced domains are a sound +simplification rather than a heuristic. + +Rust: `optimization::integer::constraint_propagation_ac3` + """ + ... diff --git a/bindings/python/python/numeria/optimization/least_squares.pyi b/bindings/python/python/numeria/optimization/least_squares.pyi new file mode 100644 index 0000000..209c84d --- /dev/null +++ b/bindings/python/python/numeria/optimization/least_squares.pyi @@ -0,0 +1,51 @@ +""" +Nonlinear least squares: Levenberg-Marquardt. Reference: Marquardt (1963); Nocedal & Wright, *Numerical Optimization*, §10.3. Minimizes ½‖r(p)‖² by solving (JᵀJ + λ·diag(JᵀJ))·δ = −Jᵀr and adapting λ on accept/reject. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.linalg.matrix import Matrix + +class LmResult: + """ +Result of a Levenberg-Marquardt fit. + +`residual` is the final sum of squared residuals ‖r(p)‖²; +`covariance` is s²·(JᵀJ)⁻¹ with s² = SSR/(m−n) when m > n and JᵀJ is +invertible, `None` otherwise. + +Rust: `optimization::least_squares::LmResult` + """ + def __init__(self, params: list[float], residual: float, iters: int, covariance: Optional[Matrix | Sequence[Sequence[float]]]) -> None: ... + @property + def params(self) -> list[float]: ... + @property + def residual(self) -> float: ... + @property + def iters(self) -> int: ... + @property + def covariance(self) -> Optional[Matrix]: ... + +def fit_exponential_decay(t: list[float], y: list[float]) -> tuple[float, float]: + """ +Fits y ≈ A·e^(−k·t), returning (A, k). Initial guess from a +log-linear regression over the positive samples, refined by LM. + +Fails with `InvalidArgument` unless there are ≥ 2 samples with +matching lengths and at least two positive y values. + +Rust: `optimization::least_squares::fit_exponential_decay` + """ + ... + +def fit_gaussian_peak(x: list[float], y: list[float]) -> tuple[float, float, float]: + """ +Fits y ≈ A·exp(−(x−μ)²/(2σ²)), returning (A, μ, σ). Initial guess +from the sample peak and moment-based width, refined by LM. + +Rust: `optimization::least_squares::fit_gaussian_peak` + """ + ... diff --git a/bindings/python/python/numeria/optimization/lp.pyi b/bindings/python/python/numeria/optimization/lp.pyi new file mode 100644 index 0000000..0cb541b --- /dev/null +++ b/bindings/python/python/numeria/optimization/lp.pyi @@ -0,0 +1,333 @@ +""" +Linear programming: the simplex method, interior point methods, duality, and the classical models that reduce to a linear program. This module sits alongside the continuous optimisers in the parent module rather than replacing them. Those search a smooth objective by following gradients or shrinking a simplex, and stop at a local optimum. A linear program has no local optima to stop at: the objective is linear and the feasible region is a convex polyhedron, so any local optimum is global and at least one optimum sits at a vertex. That is the whole reason the subject exists as a separate discipline, and why an exact answer is available where a nonlinear problem admits only an approximation. Two solvers are provided because they fail in different ways. The simplex method walks vertex to vertex along the boundary, and terminates in an exactly optimal basis, but its worst case is exponential and it can cycle in the presence of degeneracy -- handled here by Bland's rule, which guarantees termination at the cost of speed. The interior point method approaches the optimum through the middle of the region, takes a number of iterations that barely grows with problem size, and never lands exactly on a vertex. Running both on the same problem and comparing is the cheapest real check available on either. Duality is the organising idea. Every linear program has a dual whose optimal value equals its own, and whose optimal solution is the vector of rates at which the primal objective responds to relaxing each constraint. Those rates -- shadow prices -- are usually worth more than the solution itself, since they say which constraint to attack. The convention used here is stated once and adhered to throughout: > `duals[i]` is the derivative of the reported objective with respect to > `b[i]`. That definition is what makes the sensitivity ranges mean something, and it is what the tests check: perturbing a right-hand side within its range changes the objective by exactly `duals[i]` times the perturbation. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.linalg.matrix import Matrix + +class Cmp: + """ +The sense of a constraint row. + +Rust: `optimization::lp::Cmp` + """ + ... + +class LpProblem: + """ +A linear program. + +Minimises (or maximises) `c . x` subject to the rows of `a` compared +against `b` by `constraint_types`, with each variable confined to its +entry of `bounds`. A bound of `(0.0, f64::INFINITY)` is the default +non-negative variable; `(f64::NEG_INFINITY, f64::INFINITY)` makes a +variable free. + +Rust: `optimization::lp::LpProblem` + """ + def __init__(self, c: list[float], a: Matrix | Sequence[Sequence[float]], b: list[float], maximize: bool) -> None: ... + def n(self) -> int: ... + def m(self) -> int: ... + def validate(self) -> None: ... + def objective_at(self, x: list[float]) -> float: ... + def is_feasible(self, x: list[float], tol: float) -> bool: ... + @property + def c(self) -> list[float]: ... + @property + def a(self) -> Matrix: ... + @property + def b(self) -> list[float]: ... + @property + def constraint_types(self) -> list[Cmp]: ... + @property + def bounds(self) -> list[tuple[float, float]]: ... + @property + def maximize(self) -> bool: ... + +class LpResult: + """ +What a solver concluded. + +Rust: `optimization::lp::LpResult` + """ + def objective(self) -> Optional[float]: ... + def solution(self) -> Optional[list[float]]: ... + +def simplex(p: LpProblem) -> LpResult: + """ +Solves a linear program by the two-phase simplex method. + +Phase one minimises the total artificial infeasibility from an +artificial-variable basis; a positive optimum there proves the problem +infeasible, since that value is the least total violation achievable. +Phase two then optimises the real objective from the feasible basis phase +one produced. + +Bland's rule is used throughout, so the method terminates on any problem, +including degenerate ones where a faster pivoting rule would cycle. + +Errors: +Returns an error if the problem's parts disagree in shape. + +Rust: `optimization::lp::simplex` + """ + ... + +def lp_dual(p: LpProblem) -> LpProblem: + """ +The dual linear program. + +For a minimisation `min c'x` subject to rows compared against `b` with +`x >= 0`, the dual is `max b'y` subject to `A'y <= c`, with each `y_i` +signed by the sense of its row: non-positive for a `<=` row, non-negative +for a `>=` row, free for an equality. Maximisation mirrors it. + +Solving the dual gives the same optimal value as the primal and its +solution is the primal's vector of shadow prices, which is the practical +content of duality: the answer to "what is this constraint costing me" is +a solution to a different linear program of the same size. + +Errors: +Returns an error unless every primal variable carries the default bounds +`(0, inf)`. A bounded variable contributes an extra dual row, which would +change the problem's shape rather than transpose it. + +Rust: `optimization::lp::lp_dual` + """ + ... + +def sensitivity_ranges(p: LpProblem) -> tuple[list[tuple[float, float]], list[tuple[float, float]]]: + """ +Ranges over which the optimal basis survives, as +`(objective coefficient ranges, right-hand side ranges)`. + +Inside a right-hand side's range the shadow price is constant, so the +objective moves by exactly `duals[i]` per unit of `b[i]`. That linearity +is the point of the exercise and is what the tests check; outside the +range the basis changes and the rate does too. + +Inside an objective coefficient's range the optimal *point* does not move +at all, only the value. + +Errors: +Returns an error if the problem is not solved to an optimum, or if any +variable carries non-default bounds -- a finite upper bound becomes an +extra row during standardisation, and the ranges would then be reported +against rows the caller never wrote. + +Rust: `optimization::lp::sensitivity_ranges` + """ + ... + +def dual_simplex(p: LpProblem, basis: list[int]) -> LpResult: + """ +The dual simplex method, started from a given basis. + +Where the primal simplex keeps every basic variable non-negative and works +toward optimality, the dual simplex keeps the reduced costs optimal and +works toward feasibility. That is the right way round after a right-hand +side changes -- the old basis stays dual-feasible while becoming primal +infeasible, so re-solving costs a few pivots instead of a fresh start. + +`basis` names one standard-form column per constraint row. Column indices +run over the structural variables first, then the slack and surplus +columns in row order. + +Errors: +Returns an error if the basis has the wrong length, names a column out of +range, or is singular. A basis that is not dual-feasible is reported as +`GeomError::Degenerate` rather than silently repaired. + +Rust: `optimization::lp::dual_simplex` + """ + ... + +def interior_point(p: LpProblem, tol: float) -> LpResult: + """ +Solves a linear program by a primal-dual path-following interior point +method. + +The method keeps `x > 0` and `s > 0` strictly, and drives the duality +measure `x's/n` toward zero along the central path. Each iteration solves +one Newton system, reduced to the normal equations `A D A' dy = r` with +`D = diag(x_i / s_i)` and factored by Cholesky. Unlike the simplex method +it never lands exactly on a vertex, and unlike the simplex method its +iteration count barely grows with the size of the problem. + +The starting point is deliberately infeasible -- all ones -- and the primal +and dual residuals are driven to zero alongside the duality gap. That +avoids needing a phase one, but means infeasibility shows up as a failure +to converge rather than as a proof, so an unconverged run is reported as +`LpResult::Infeasible` only when the residuals are still large while the +gap has closed. + +Errors: +Returns an error if the problem's parts disagree in shape or `tol` is not +positive. + +Rust: `optimization::lp::interior_point` + """ + ... + +def lp_from_str(text: str) -> LpProblem: + """ +Parses a linear program from text. + +The grammar is deliberately tiny: + + +The first line gives the sense and the objective. Everything after +`subject to` (or `st`, or `s.t.`) is a constraint row until an optional +`bounds` section, where single-variable lines set bounds rather than adding +rows and `free x` removes a variable's lower bound. Blank lines and `#` +comments are ignored, coefficients may be omitted, and variables are +numbered in order of first appearance. + +Errors: +Returns `GeomError::InvalidArgument` naming the first thing that could +not be read. + +Rust: `optimization::lp::lp_from_str` + """ + ... + +def diet_problem(costs: list[float], nutrients: Matrix | Sequence[Sequence[float]], requirements: list[float]) -> LpProblem: + """ +Stigler's diet problem: the cheapest combination of foods meeting every +nutritional minimum. + +`costs` gives the price per unit of each food, `nutrients` holds the amount +of nutrient `k` in one unit of food `j` at `(k, j)`, and `requirements` +the minimum of each nutrient. + +Errors: +Returns an error if the shapes disagree. + +Rust: `optimization::lp::diet_problem` + """ + ... + +def production_planning(profits: list[float], usage: Matrix | Sequence[Sequence[float]], available: list[float]) -> LpProblem: + """ +A production plan: how much of each product to make to maximise profit +under resource limits. + +`usage` holds the amount of resource `k` consumed per unit of product `j` +at `(k, j)`, and `available` the stock of each resource. + +Errors: +Returns an error if the shapes disagree. + +Rust: `optimization::lp::production_planning` + """ + ... + +def transportation_problem(supply: list[float], demand: list[float], costs: Matrix | Sequence[Sequence[float]]) -> LpResult: + """ +The transportation problem: ship from sources to sinks at least cost. + +`costs` holds the unit cost from source `i` to sink `j` at `(i, j)`. +Supply is an upper limit and demand a lower one, so unbalanced instances +are handled without inventing a dummy row. + +The constraint matrix is totally unimodular, so with integer supplies and +demands the simplex optimum is automatically integral -- no branch and +bound is needed, which is why the problem is solved as a linear program at +all. + +Errors: +Returns an error if the shapes disagree or total demand exceeds total +supply, which is infeasible by inspection. + +Rust: `optimization::lp::transportation_problem` + """ + ... + +def two_player_zero_sum_lp(payoff: Matrix | Sequence[Sequence[float]]) -> tuple[list[float], list[float], float]: + """ +Solves a two-player zero-sum game, returning +`(row strategy, column strategy, value)`. + +`payoff` holds the row player's gain at `(i, j)`. The row player maximises +the worst case and the column player minimises the best case, and von +Neumann's minimax theorem says the two coincide -- which here is not an +extra assumption but a consequence of LP duality, since the two players' +programs are duals of each other. The column strategy is read directly off +the row program's shadow prices. + +The payoff is shifted to be strictly positive before solving, since the +standard formulation divides by the value; the shift is undone on the way +out. + +Errors: +Returns an error if the resulting program has no optimum, which cannot +happen for a finite game and would indicate a numerical failure. + +Rust: `optimization::lp::two_player_zero_sum_lp` + """ + ... + +def chebyshev_center(a: Matrix | Sequence[Sequence[float]], b: list[float]) -> tuple[list[float], float]: + """ +The Chebyshev centre of the polyhedron `{x : a_i . x <= b_i}`: the point +furthest from every face, and that distance. + +Maximises `r` subject to `a_i . x + r ||a_i|| <= b_i`. The norm term is +what turns "satisfy the constraint" into "stay `r` away from it", and it is +why the problem is linear at all -- the distance from a point to a +hyperplane is linear in the point. + +Returns `(centre, radius)`. The radius is always unique, but the centre +need not be: in a box four wide and six tall the largest inscribed circle +has radius two and can sit anywhere along a vertical segment. Only the +coordinates that the touching faces pin down are determined, and the +returned point is one vertex of that optimal face. + +An unbounded polyhedron gives an infinite radius; an empty one is an error. + +Errors: +Returns an error for a shape mismatch, a zero row, or an infeasible system. + +Rust: `optimization::lp::chebyshev_center` + """ + ... + +def l1_regression_lp(x: Matrix | Sequence[Sequence[float]], y: list[float]) -> list[float]: + """ +Least-absolute-deviations regression, solved as a linear program. + +Minimises `sum |y_i - x_i . beta|` by splitting each residual into a +positive and a negative part. The result is far less sensitive to an +outlier than a least-squares fit, because the cost of a large residual +grows linearly rather than quadratically -- an outlier at ten standard +deviations pulls a hundred times harder on a least-squares fit than on +this one. + +`x` holds one row per observation. Add a column of ones for an intercept. + +Errors: +Returns an error on a shape mismatch or if the program has no optimum. + +Rust: `optimization::lp::l1_regression_lp` + """ + ... + +def linf_regression_lp(x: Matrix | Sequence[Sequence[float]], y: list[float]) -> list[float]: + """ +Chebyshev (minimax) regression, solved as a linear program. + +Minimises the largest absolute residual rather than their sum. Where the +L1 fit ignores an outlier, this one is dominated by it -- the fit is +pinned by the extreme points and by nothing else, which is exactly what is +wanted when the residuals are bounded errors rather than noise. + +Errors: +Returns an error on a shape mismatch or if the program has no optimum. + +Rust: `optimization::lp::linf_regression_lp` + """ + ... diff --git a/bindings/python/python/numeria/optimization/metaheuristics.pyi b/bindings/python/python/numeria/optimization/metaheuristics.pyi new file mode 100644 index 0000000..919415d --- /dev/null +++ b/bindings/python/python/numeria/optimization/metaheuristics.pyi @@ -0,0 +1,267 @@ +""" +Derivative-free and population-based optimisation, and the benchmark landscapes used to tell one method from another. Every method here treats the objective as a black box: it may be discontinuous, noisy, or defined only by a simulation, and no gradient is available even in principle. That rules out every gradient-based method and leaves search. What distinguishes the methods here is what they do with the evaluations they have spent. Pattern search and Nelder-Mead keep a small geometric structure and move it downhill; they are cheap and get stuck in the first basin they find. Differential evolution and particle swarms keep a population, and their mutation steps are built from *differences between members*, so the search scale adapts to the spread of the population without anyone tuning it. CMA-ES goes furthest: it estimates the covariance of the successful steps and samples from that, which amounts to learning the local metric of the landscape, and is why it handles badly scaled and rotated problems that defeat the others. None of them is guaranteed to find a global optimum in finite time, and any claim otherwise is a claim about the objective rather than the method. What the tests here check is therefore not "finds the optimum" in general, but the properties that must hold regardless: bounds are respected, the best-so-far never worsens, a Pareto front contains nothing dominated, and on landscapes whose optima are known analytically the methods get there. The benchmark table exists so those claims can be made against something. Its stated optima are checked by dense sampling in the tests rather than taken on trust -- a benchmark whose recorded optimum is wrong silently invalidates every comparison made with it. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.graph.coloring import Order +from numeria.sim.cloth_sim import Particle +from numeria.monte_carlo import Rng +from numeria.spatial.primitives import Sphere + +class Benchmark: + """ +A benchmark landscape: name, function, per-coordinate bounds, and the +known global minimum value. + +Rust: `optimization::metaheuristics::Benchmark` + """ + @property + def name(self) -> str: ... + @property + def bounds(self) -> list[tuple[float, float]]: ... + @property + def optimum(self) -> float: ... + +class GaConfig: + """ +Settings for the real-valued genetic algorithm. + +Rust: `optimization::metaheuristics::GaConfig` + """ + def __init__(self, population: int, generations: int, mutation_rate: float, mutation_scale: float, elite: int) -> None: ... + @property + def population(self) -> int: ... + @property + def generations(self) -> int: ... + @property + def mutation_rate(self) -> float: ... + @property + def mutation_scale(self) -> float: ... + @property + def elite(self) -> int: ... + +def pattern_search(f: Callable[[list[float]], float], x0: list[float], step: float, tol: float, max_iter: int) -> tuple[list[float], float]: + """ +Compass pattern search: probe one coordinate step in each direction, move +to any improvement, and halve the step when none is found. + +The simplest direct search that still has a convergence proof: on a +smooth function the step only shrinks when the current point beats all +`2n` neighbours, which forces the gradient toward zero as the step does. +Slower than Nelder-Mead in practice and far more robust, since it never +deforms its search pattern and so cannot collapse into a degenerate +simplex. + +Panics: +Panics unless the starting point is non-empty and the step and tolerance +are positive. + +Rust: `optimization::metaheuristics::pattern_search` + """ + ... + +def basin_hopping(f: Callable[[list[float]], float], x0: list[float], step: float, temperature: float, hops: int, rng: Rng) -> tuple[list[float], float]: + """ +Basin hopping: repeated local descent from perturbed starting points, +keeping the perturbation only when it leads somewhere better. + +The Metropolis acceptance is applied to the *local minima*, not to the raw +function, which is what makes it a search over basins rather than over +points. On a landscape of many narrow wells separated by high barriers -- +the case that defeats plain annealing -- collapsing each well to its floor +first turns the problem into a much smoother one. + +Panics: +Panics unless the temperature and step are positive. + +Rust: `optimization::metaheuristics::basin_hopping` + """ + ... + +def multistart_local(f: Callable[[list[float]], float], bounds: list[tuple[float, float]], starts: int, rng: Rng) -> tuple[list[float], float]: + """ +Repeated local search from random starting points inside a box. + +The cheapest defence against a multimodal landscape, and a fair baseline: +any population method that cannot beat enough random restarts to match its +evaluation budget is not earning its complexity. + +Panics: +Panics if `bounds` is empty or `starts` is zero. + +Rust: `optimization::metaheuristics::multistart_local` + """ + ... + +def differential_evolution(f: Callable[[list[float]], float], bounds: list[tuple[float, float]], population: int, cr: float, weight: float, generations: int, rng: Rng) -> tuple[list[float], float]: + """ +Differential evolution: mutate by adding a scaled difference of two +population members to a third, then cross over with the target. + +The difference vector is the whole idea. Early on the population is spread +out and the differences are large, so the search is global; as it +converges the differences shrink with it and the search becomes local. +Nobody has to schedule that -- the step size is read off the population's +own spread, which is why the method has so few parameters and why they +transfer between problems. + +`cr` is the crossover rate in `[0, 1]` and `weight` the differential +scaling, conventionally near `0.8`. + +Panics: +Panics unless the population is at least four, `cr` lies in `[0, 1]`, and +the bounds are non-empty. + +Rust: `optimization::metaheuristics::differential_evolution` + """ + ... + +def particle_swarm(f: Callable[[list[float]], float], bounds: list[tuple[float, float]], particles: int, inertia: float, cognitive: float, social: float, iterations: int, rng: Rng) -> tuple[list[float], float]: + """ +Particle swarm optimisation: each particle carries a velocity pulled +toward its own best and the swarm's best. + +`inertia` retains the previous velocity, `cognitive` weights the pull +toward the particle's own history and `social` the pull toward the +swarm's. The classic failure is setting inertia too high, where the swarm +never settles, or too low, where it collapses onto the first decent point +found and stops exploring. + +Panics: +Panics unless the swarm is non-empty and the bounds are non-empty. + +Rust: `optimization::metaheuristics::particle_swarm` + """ + ... + +def cma_es(f: Callable[[list[float]], float], x0: list[float], sigma0: float, generations: int, rng: Rng) -> tuple[list[float], float]: + """ +The covariance matrix adaptation evolution strategy. + +Samples a population from a multivariate normal, keeps the better half, +and updates the mean, the step size and the full covariance from them. The +covariance is what sets it apart: after enough generations it approximates +the inverse Hessian up to scale, so the sampling distribution stretches +along the valley floor of a badly conditioned problem instead of +stumbling across it. That is the same information Newton's method uses, +obtained without a single derivative. + +The step size is adapted separately, by comparing the length of the path +the mean has actually travelled against the length a random walk would +have covered; a mean that keeps moving in one direction is taking steps +that are too small. + +Panics: +Panics unless the starting point is non-empty and `sigma0` is positive. + +Rust: `optimization::metaheuristics::cma_es` + """ + ... + +def genetic_algorithm(f: Callable[[list[float]], float], bounds: list[tuple[float, float]], config: GaConfig, rng: Rng) -> tuple[list[float], float]: + """ +A real-valued genetic algorithm with tournament selection, blend +crossover and Gaussian mutation. + +Elitism is what makes the best-so-far monotone: without carrying the best +members over untouched, a generation can be strictly worse than the last, +and the algorithm has no memory to recover it from. + +Minimises `f`. + +Panics: +Panics unless the population exceeds the elite count and the bounds are +non-empty. + +Rust: `optimization::metaheuristics::genetic_algorithm` + """ + ... + +def genetic_algorithm_permutation(cost: Callable[[list[int]], float], n: int, config: GaConfig, rng: Rng) -> tuple[list[int], float]: + """ +A genetic algorithm over permutations, with order crossover and swap +mutation. + +Blend crossover is meaningless on a permutation -- averaging two orderings +does not give an ordering. Order crossover instead copies a slice from one +parent and fills the rest in the order the other parent visits them, which +preserves relative order from both and always produces a valid +permutation. That closure property is the whole difficulty of the +permutation case. + +Minimises `cost`. + +Panics: +Panics unless `n >= 2` and the population exceeds the elite count. + +Rust: `optimization::metaheuristics::genetic_algorithm_permutation` + """ + ... + +def pareto_front(points: list[list[float]]) -> list[int]: + """ +Indices of the non-dominated points: those no other point beats on every +objective while beating it on at least one. + +Minimisation in every coordinate. The result is the Pareto front, and the +point of computing it is that without further information there is no +reason to prefer any member of it to any other -- a single "best" answer +only exists once the objectives are weighted, which is a decision the +optimiser cannot make. + +Rust: `optimization::metaheuristics::pareto_front` + """ + ... + +def hypervolume_2d(front: list[list[float]], reference: tuple[float, float]) -> float: + """ +The area dominated by a two-objective front, bounded by a reference point. + +The standard scalar summary of a front's quality, and the only common one +that is strictly monotone: adding a point that is not already dominated +can only increase it, so it cannot reward a front for losing coverage. +Points not dominating the reference contribute nothing. + +Panics: +Panics if a front point is not two-dimensional. + +Rust: `optimization::metaheuristics::hypervolume_2d` + """ + ... + +def benchmark_functions() -> list[Benchmark]: + """ +The standard test landscapes, in two dimensions. + +They are chosen to fail different methods. Sphere is convex and separable +and everything solves it. Rosenbrock's optimum sits at the end of a curved +valley whose floor is nearly flat, which punishes anything that treats the +coordinates independently. Rastrigin and Ackley add a regular lattice of +local minima on top of a global structure, so a purely local method stops +at the first one. Griewank's local minima vanish as the dimension grows, +which makes it *easier* in higher dimensions and is a standing warning +about extrapolating benchmark results. Schwefel puts its optimum near a +corner, far from the centre where most methods are initialised. + +The recorded optima are verified by dense sampling in this module's tests +rather than taken on trust. + +Rust: `optimization::metaheuristics::benchmark_functions` + """ + ... + +def convergence_curve(history: list[float]) -> list[float]: + """ +The running best of a sequence of objective values. + +Monotone non-increasing by construction, which is what makes two runs +comparable: the raw values of a stochastic search jump around and say +nothing about progress. + +Rust: `optimization::metaheuristics::convergence_curve` + """ + ... diff --git a/bindings/python/python/numeria/optimization/network.pyi b/bindings/python/python/numeria/optimization/network.pyi new file mode 100644 index 0000000..50cffd8 --- /dev/null +++ b/bindings/python/python/numeria/optimization/network.pyi @@ -0,0 +1,328 @@ +""" +Network models and scheduling: project planning, flows on networks, and the sequencing rules that provably optimise a stated objective. Two threads run through this module. The first is that several graph problems are linear programs in disguise, and their constraint matrices are totally unimodular, so the linear relaxation is automatically integral. Shortest path and maximum flow both have this property, which is why they can be solved by combinatorial algorithms *and* by a general linear programming solver with the same answer. Having both is worth the duplication: the graph module's algorithms are far faster, and the linear programs are an independent check on them. The second is that scheduling is a subject of exact greedy rules rather than heuristics. Sorting by processing time minimises mean flow time; sorting by due date minimises maximum lateness; Moore and Hodgson's rule minimises the *number* of late jobs; Johnson's rule minimises makespan on two machines. Each is provably optimal for its own objective and provably not for the others -- shortest-processing-time can make a job catastrophically late while minimising the average -- so the objective must be chosen before the rule. The tests check each rule against exhaustive enumeration of every permutation, on the objective it claims and on nothing else. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.graph.core import Graph +from numeria.optimization.lp import LpResult +from numeria.linalg.matrix import Matrix + +class TaskTimes: + """ +The four schedule times of one task: earliest start, earliest finish, +latest start, latest finish. + +Rust: `optimization::network::TaskTimes` + """ + def __init__(self, early_start: float, early_finish: float, late_start: float, late_finish: float) -> None: ... + def slack(self) -> float: ... + @property + def early_start(self) -> float: ... + @property + def early_finish(self) -> float: ... + @property + def late_start(self) -> float: ... + @property + def late_finish(self) -> float: ... + +def transshipment(supply: list[float], arcs: list[tuple[int, int, float, float]]) -> LpResult: + """ +The transshipment problem: ship from sources to sinks through intermediate +nodes at least cost. + +`supply[i]` is positive at a source, negative at a sink, and zero at a pure +transshipment node; the entries must sum to zero. `arcs` lists +`(from, to, unit cost, capacity)`. + +Generalises the transportation problem by allowing goods to pass through a +node rather than only from a source directly to a sink, which is what makes +it a network rather than a bipartite matching. + +Errors: +Returns an error if an arc names a node out of range or the supplies do not +balance. + +Rust: `optimization::network::transshipment` + """ + ... + +def shortest_path_lp_check(g: Graph, s: int, t: int) -> Optional[float]: + """ +The length of a shortest path, computed as a linear program. + +The dual of the shortest path problem asks for node potentials that +maximise the gap between source and target while no arc rises by more than +its length -- so the answer comes out of a linear program whose constraint +matrix is a node-arc incidence matrix, which is totally unimodular. + +Its purpose is to check the graph module's Dijkstra against a completely +different method. Slower by a wide margin, and worth it only as +verification. + +Errors: +Returns an error if the endpoints are out of range, or the graph has a +negative-length arc, where the linear program is unbounded rather than +merely wrong. + +Rust: `optimization::network::shortest_path_lp_check` + """ + ... + +def max_flow_lp_check(g: Graph, s: int, t: int) -> Optional[float]: + """ +The value of a maximum flow, computed as a linear program. + +Maximises the net outflow from the source subject to conservation at every +other node and each arc's capacity. Like the shortest path formulation this +exists to check the graph module's combinatorial algorithms rather than to +replace them. + +Errors: +Returns an error if the endpoints are out of range or coincide. + +Rust: `optimization::network::max_flow_lp_check` + """ + ... + +def network_simplex_lite(balance: list[float], arcs: list[tuple[int, int, float, float]]) -> LpResult: + """ +A minimum-cost flow by the network simplex, expressed through the general +simplex method. + +`arcs` are `(from, to, unit cost, capacity)` and `balance[i]` the net +supply at node `i`, summing to zero. The genuine network simplex maintains +a spanning tree basis and pivots in `O(m)` per step rather than solving a +linear system; this routes the same problem through the general solver, +which is correct and slower, and is named "lite" for that reason. + +Errors: +Returns an error under the same conditions as `transshipment`. + +Rust: `optimization::network::network_simplex_lite` + """ + ... + +def critical_path_method(tasks: list[tuple[float, list[int]]]) -> tuple[float, list[int], list[TaskTimes]]: + """ +The critical path method: the shortest possible project duration, which +tasks cannot slip, and every task's four schedule times. + +`tasks[i]` is `(duration, predecessors)`. Returns +`(duration, critical task indices, times)`. + +The critical path is the longest path through the precedence graph, and the +project cannot finish sooner than that however many resources are thrown at +it -- which is the point of computing it. A task is critical exactly when +its slack is zero, so shortening a non-critical task buys nothing at all. + +Errors: +Returns an error if a predecessor is out of range or the precedences +contain a cycle, which makes the project unschedulable. + +Rust: `optimization::network::critical_path_method` + """ + ... + +def pert(tasks: list[tuple[float, float, float, list[int]]]) -> tuple[float, float]: + """ +PERT: the mean and variance of the project duration under three-point +estimates. + +`tasks[i]` is `(optimistic, most likely, pessimistic, predecessors)`. Each +task's duration is taken as a beta distribution with mean +`(a + 4m + b) / 6` and standard deviation `(b - a) / 6`, and the project +duration as the sum along the critical path. + +The variance is the sum of the *critical path's* variances only, which is +the method's known weakness: a near-critical path with high variance can +overtake the critical one and PERT will not see it, so the figure +understates the true spread. It is reported because it is what PERT means, +not because it is the whole answer. + +Errors: +Returns an error if an estimate is out of order or the precedences are +unschedulable. + +Rust: `optimization::network::pert` + """ + ... + +def vehicle_routing_savings(distance: Matrix | Sequence[Sequence[float]], demand: list[float], capacity: float) -> list[list[int]]: + """ +Clarke-Wright savings for the capacitated vehicle routing problem. + +Every customer starts on its own out-and-back route. Merging the routes +ending at `i` and beginning at `j` saves `d(0,i) + d(0,j) - d(i,j)` -- the +two depot legs replaced by one direct leg -- so merges are tried in +decreasing order of that saving, subject to capacity. + +Returns the routes as customer sequences, excluding the depot at each end. + +Errors: +Returns an error if the distance matrix is the wrong shape, or a customer's +demand exceeds a vehicle's capacity, which makes routing impossible. + +Rust: `optimization::network::vehicle_routing_savings` + """ + ... + +def job_shop_shifting_bottleneck_lite(jobs: list[list[tuple[int, float]]], machines: int) -> float: + """ +A lower bound on a job shop makespan by the shifting bottleneck idea, +simplified. + +`jobs[j]` lists `(machine, duration)` in the order job `j` must visit them. +Returns the larger of the busiest machine's total load and the longest +job's total work -- both of which any schedule must exceed, since a machine +cannot process two operations at once and a job cannot be in two places. + +The full shifting bottleneck procedure solves a one-machine sequencing +problem per machine and iterates; this reports the elementary bound those +iterations start from. + +Errors: +Returns an error if a machine index exceeds the machine count. + +Rust: `optimization::network::job_shop_shifting_bottleneck_lite` + """ + ... + +def scheduling_spt(jobs: list[float]) -> list[int]: + """ +Shortest processing time first: the order minimising mean flow time on one +machine. + +Optimal by an exchange argument -- swapping an adjacent out-of-order pair +always improves the total -- and optimal for nothing else. It can make one +long job arbitrarily late while the average looks excellent, which is why +the objective has to be chosen before the rule. + +`jobs[i]` is a processing time. Returns the job order. + +Rust: `optimization::network::scheduling_spt` + """ + ... + +def scheduling_edd(jobs: list[tuple[float, float]]) -> list[int]: + """ +Earliest due date first: the order minimising maximum lateness on one +machine. + +Jackson's rule. Also by an exchange argument, and again optimal only for +its own objective: it makes no attempt to reduce the *number* of late jobs, +which is what `moore_hodgson` is for. + +`jobs[i]` is `(processing time, due date)`. Returns the job order. + +Rust: `optimization::network::scheduling_edd` + """ + ... + +def moore_hodgson(jobs: list[tuple[float, float]]) -> list[int]: + """ +The Moore-Hodgson rule: the order minimising the *number* of late jobs on +one machine. + +Work through the jobs by due date; whenever the schedule falls behind, +throw out the longest job accepted so far. That one removal buys the most +time back, and the jobs thrown out are exactly the late ones, which is what +makes the rule optimal rather than merely sensible. + +Returns the order: the on-time jobs first in due-date order, then the late +ones. + +`jobs[i]` is `(processing time, due date)`. + +Rust: `optimization::network::moore_hodgson` + """ + ... + +def johnson_two_machine(jobs: list[tuple[float, float]]) -> list[int]: + """ +Johnson's rule: the order minimising makespan through two machines in +series. + +Every job visits machine one then machine two. Jobs whose first operation +is the shorter go first, in increasing order of that operation; the rest go +last, in decreasing order of their second. The first group fills machine +two's queue quickly and the second keeps it busy at the end, which is what +the exchange argument formalises. + +`jobs[i]` is `(time on machine one, time on machine two)`. + +Rust: `optimization::network::johnson_two_machine` + """ + ... + +def two_machine_makespan(jobs: list[tuple[float, float]], order: list[int]) -> float: + """ +The makespan of a two-machine flow shop under a given order. + +Machine two cannot start a job before machine one finishes it, nor before +it finishes the previous job, which is the whole recursion. + +Rust: `optimization::network::two_machine_makespan` + """ + ... + +def lpt_makespan(jobs: list[float], machines: int) -> tuple[float, list[int]]: + """ +Longest processing time first onto identical parallel machines. + +Returns the makespan and which machine each job went to. The rule finishes +within `4/3 - 1/(3m)` of the optimum, and that bound is tight -- so it is a +guarantee rather than an observation, and the tests check it against an +exact answer. + +Panics: +Panics if `machines` is zero. + +Rust: `optimization::network::lpt_makespan` + """ + ... + +def interval_scheduling_max(intervals: list[tuple[float, float]]) -> list[int]: + """ +The largest set of pairwise disjoint intervals, by earliest finish time. + +The greedy choice is optimal, and the proof is the reason: whatever the +optimal set, replacing its first interval by the one that finishes earliest +leaves it still valid and no smaller, so an optimal solution containing the +greedy choice always exists. + +`intervals[i]` is `(start, end)`. Returns the chosen indices. + +Rust: `optimization::network::interval_scheduling_max` + """ + ... + +def weighted_interval_scheduling(intervals: list[tuple[float, float, float]]) -> tuple[float, list[int]]: + """ +The most valuable set of pairwise disjoint intervals. + +Weights break the greedy argument completely -- one long valuable interval +can be worth more than any number of short ones -- so this is a table: +sort by finish time and, for each interval, either take it and jump to the +last compatible one or skip it. + +`intervals[i]` is `(start, end, weight)`. Returns the total and the chosen +indices. + +Rust: `optimization::network::weighted_interval_scheduling` + """ + ... + +def gantt_data(processing: list[float], order: list[int]) -> list[tuple[int, float, float]]: + """ +Turns a single-machine job order into `(job, start, finish)` bars. + +Jobs run back to back in the given order from time zero, which is what a +single-machine sequencing rule assumes. + +Rust: `optimization::network::gantt_data` + """ + ... diff --git a/bindings/python/python/numeria/particle_physics.pyi b/bindings/python/python/numeria/particle_physics.pyi new file mode 100644 index 0000000..d44295c --- /dev/null +++ b/bindings/python/python/numeria/particle_physics.pyi @@ -0,0 +1,204 @@ +""" +Relativistic kinematics and scattering for particle collisions. Invariant mass -- the quantity every collider analysis is built on, because it is the same in every frame -- along with centre-of-mass energy for colliding and fixed-target geometries, and the Lorentz boost of energy and longitudinal momentum. The collider coordinates: rapidity, pseudorapidity and transverse momentum, chosen because rapidity differences are boost invariant along the beam. Scattering by the Rutherford cross section and the Breit-Wigner resonance shape, with the width-lifetime relation `Γτ = ħ` and branching ratios. Also the conservation-law checks -- charge, lepton number, baryon number -- that say whether a proposed reaction can happen at all. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def invariant_mass(energy: float, momentum: float) -> float: + """ +Invariant mass from total energy and scalar momentum magnitude. +m = √(E² - p²c²) / c² + +Rust: `particle_physics::invariant_mass` + """ + ... + +def invariant_mass_two_body(e1: float, px1: float, py1: float, pz1: float, e2: float, px2: float, py2: float, pz2: float) -> float: + """ +Invariant mass of a two-body system from individual four-momenta. +m² = (E1+E2)² - |p1+p2|²c², return m/c². + +Rust: `particle_physics::invariant_mass_two_body` + """ + ... + +def center_of_mass_energy(e_beam: float, e_target: float, p_beam: float, p_target: float) -> float: + """ +Center-of-mass energy √s for two particles with given energies and momenta. +√s = √((E1+E2)² - (p1+p2)²c²) + +Rust: `particle_physics::center_of_mass_energy` + """ + ... + +def fixed_target_com_energy(beam_energy: float, target_mass: float) -> float: + """ +Fixed-target center-of-mass energy (high-energy approximation). +√s ≈ √(2 × E_beam × m_target × c²) + +Rust: `particle_physics::fixed_target_com_energy` + """ + ... + +def lorentz_boost_energy(energy: float, momentum_z: float, beta: float) -> float: + """ +Lorentz boost of energy along z. E' = γ(E - β pz c) + +Rust: `particle_physics::lorentz_boost_energy` + """ + ... + +def lorentz_boost_pz(energy: float, momentum_z: float, beta: float) -> float: + """ +Lorentz boost of z-momentum. pz' = γ(pz - β E/c) + +Rust: `particle_physics::lorentz_boost_pz` + """ + ... + +def rapidity(energy: float, pz: float) -> float: + """ +Rapidity y = 0.5 × ln((E + pz c) / (E - pz c)) + +Rust: `particle_physics::rapidity` + """ + ... + +def pseudorapidity(theta: float) -> float: + """ +Pseudorapidity η = -ln(tan(θ/2)) + +Rust: `particle_physics::pseudorapidity` + """ + ... + +def transverse_momentum(px: float, py: float) -> float: + """ +Transverse momentum pT = √(px² + py²) + +Rust: `particle_physics::transverse_momentum` + """ + ... + +def rutherford_cross_section(z1: float, z2: float, energy: float, angle: float) -> float: + """ +Rutherford scattering differential cross section. +dσ/dΩ = (Z1 Z2 k_e e² / (4E))² / sin⁴(θ/2) + +Rust: `particle_physics::rutherford_cross_section` + """ + ... + +def breit_wigner(energy: float, mass: float, width: float) -> float: + """ +Non-relativistic Breit-Wigner resonance (normalized to peak = 1). +BW(E) = (Γ/2)² / ((E - M)² + (Γ/2)²) + +Rust: `particle_physics::breit_wigner` + """ + ... + +def decay_rate_from_lifetime(lifetime: float) -> float: + """ +Decay rate from lifetime. Γ = ℏ / τ + +Rust: `particle_physics::decay_rate_from_lifetime` + """ + ... + +def lifetime_from_width(width_joules: float) -> float: + """ +Lifetime from decay width. τ = ℏ / Γ + +Rust: `particle_physics::lifetime_from_width` + """ + ... + +def branching_ratio(partial_width: float, total_width: float) -> float: + """ +Branching ratio BR = Γ_i / Γ_total + +Rust: `particle_physics::branching_ratio` + """ + ... + +def mean_free_path_particle(cross_section: float, number_density: float) -> float: + """ +Mean free path λ = 1 / (n σ) + +Rust: `particle_physics::mean_free_path_particle` + """ + ... + +def luminosity_to_event_rate(luminosity: float, cross_section: float) -> float: + """ +Event rate R = L × σ + +Rust: `particle_physics::luminosity_to_event_rate` + """ + ... + +def is_charge_conserved(charges_in: list[float], charges_out: list[float]) -> bool: + """ +Check charge conservation: sum of input charges ≈ sum of output charges. + +Rust: `particle_physics::is_charge_conserved` + """ + ... + +def is_lepton_number_conserved(leptons_in: list[int], leptons_out: list[int]) -> bool: + """ +Check lepton number conservation. + +Rust: `particle_physics::is_lepton_number_conserved` + """ + ... + +def is_baryon_number_conserved(baryons_in: list[int], baryons_out: list[int]) -> bool: + """ +Check baryon number conservation. + +Rust: `particle_physics::is_baryon_number_conserved` + """ + ... + +def four_momentum_magnitude(energy: float, px: float, py: float, pz: float) -> float: + """ +Four-momentum magnitude (invariant mass × c). +√(E² - p²c²) / c = mc + +Rust: `particle_physics::four_momentum_magnitude` + """ + ... + +M_MUON: float + +M_TAU: float + +M_PION_CHARGED: float + +M_PION_NEUTRAL: float + +M_KAON: float + +M_W_BOSON: float + +M_Z_BOSON: float + +M_HIGGS: float + +M_TOP_QUARK: float + +CHARGE_UP: float + +CHARGE_DOWN: float + +FINE_STRUCTURE: float + +WEAK_MIXING_ANGLE_SIN2: float + +STRONG_COUPLING: float diff --git a/bindings/python/python/numeria/patterns/__init__.pyi b/bindings/python/python/numeria/patterns/__init__.pyi new file mode 100644 index 0000000..7bc9cc0 --- /dev/null +++ b/bindings/python/python/numeria/patterns/__init__.pyi @@ -0,0 +1,12 @@ +""" +Geometric patterns: polygon algorithms, sampling distributions, phyllotaxis, tilings, symmetry groups, packings, space-filling curves, polyhedra, aperiodic tilings, and knots. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from . import aperiodic, knots, packing, phyllotaxis, polygon_ops, polyhedra, sampling, space_filling, symmetry, tilings + + diff --git a/bindings/python/python/numeria/patterns/aperiodic.pyi b/bindings/python/python/numeria/patterns/aperiodic.pyi new file mode 100644 index 0000000..a6ba16c --- /dev/null +++ b/bindings/python/python/numeria/patterns/aperiodic.pyi @@ -0,0 +1,176 @@ +""" +Aperiodic tilings: Penrose P2 (kite/dart) and P3 (rhombs) by Robinson-triangle deflation, de Bruijn multigrid projection, Ammann-Beenker, the hat and spectre monotiles (ported from the reference implementations accompanying Smith, Myers, Kaplan & Goodman-Strauss 2023), the pinwheel tiling, and 1-D quasiperiodic sequences. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.spatial.primitives import Polygon2 +from numeria.spatial.primitives import Rect +from numeria.math import Vec2 + +class PenroseTile: + """ +Penrose tile kinds. + +Rust: `patterns::aperiodic::PenroseTile` + """ + ... + +class PlacedTile: + """ +A placed Penrose tile. Vertices are ordered so positions 1 and 3 +are the tile's internal axis/diagonal (the symmetry axis for +kites and darts, the splitting diagonal for rhombs). + +Rust: `patterns::aperiodic::PlacedTile` + """ + def __init__(self, kind: PenroseTile, vertices: list[Vec2 | Sequence[float]]) -> None: ... + @property + def kind(self) -> PenroseTile: ... + @property + def vertices(self) -> list[Vec2]: ... + +def penrose_p2_deflate(tiles: list[PlacedTile], iterations: int) -> list[PlacedTile]: + """ +Deflates P2 (kite/dart) tiles `iterations` times; each round +shrinks edges by 1/φ. + +Rust: `patterns::aperiodic::penrose_p2_deflate` + """ + ... + +def penrose_p3_deflate(tiles: list[PlacedTile], iterations: int) -> list[PlacedTile]: + """ +Deflates P3 (rhomb) tiles `iterations` times. + +Rust: `patterns::aperiodic::penrose_p3_deflate` + """ + ... + +def penrose_p3_sun(radius: float) -> list[PlacedTile]: + """ +P3 "sun" seed: ten half-thin triangles around the origin (rhomb +edges of length `radius`). + +Panics: +Panics unless `radius > 0`. + +Rust: `patterns::aperiodic::penrose_p3_sun` + """ + ... + +def penrose_p3_star(radius: float) -> list[PlacedTile]: + """ +P3 "star" seed: the mirrored wheel, deflated once so full rhombs +exist. + +Panics: +Panics unless `radius > 0`. + +Rust: `patterns::aperiodic::penrose_p3_star` + """ + ... + +def penrose_p2_seed(radius: float) -> list[PlacedTile]: + """ +P2 "sun" seed: five kites around the origin (kite long edges of +length `radius`). + +Panics: +Panics unless `radius > 0`. + +Rust: `patterns::aperiodic::penrose_p2_seed` + """ + ... + +def ratio_thick_to_thin(tiles: list[PlacedTile]) -> float: + """ +Number ratio of thick rhombs (plus kites) to thin rhombs (plus +darts); converges to φ under deflation. + +Panics: +Panics when the denominator count is zero. + +Rust: `patterns::aperiodic::ratio_thick_to_thin` + """ + ... + +def penrose_by_projection(extent: Rect, offsets: list[float]) -> list[PlacedTile]: + """ +Penrose P3 rhombs by de Bruijn's pentagrid projection: five line +grids with the given offsets (their sum should be an integer for a +true Penrose tiling; generic values give a generalized tiling). + +Rust: `patterns::aperiodic::penrose_by_projection` + """ + ... + +def ammann_beenker(extent: Rect, iterations: int) -> list[Polygon2]: + """ +Ammann-Beenker (octagonal) tiling of squares and 45° rhombs by the +four-grid de Bruijn dual. The `iterations` argument scales the +generated patch density (offsets stay fixed), kept for signature +compatibility with substitution-style generators. + +Rust: `patterns::aperiodic::ammann_beenker` + """ + ... + +def hat_monotile(extent: Rect, iterations: int) -> list[Polygon2]: + """ +A patch of hat monotiles (Smith, Myers, Kaplan & Goodman-Strauss +2023) built by `iterations` rounds of the H/T/P/F metatile +substitution; hats whose centroid lies in `extent` are returned +(hat edge lengths 1 and √3, fixed scale — grow the extent or the +iteration count for more tiles). + +Rust: `patterns::aperiodic::hat_monotile` + """ + ... + +def spectre_monotile(extent: Rect, iterations: int) -> list[Polygon2]: + """ +A patch of spectre monotiles ("A chiral aperiodic monotile", +Smith, Myers, Kaplan & Goodman-Strauss 2023) built by `iterations` +substitution rounds; spectres with centroid inside `extent` are +returned (unit edge length, fixed scale). + +Rust: `patterns::aperiodic::spectre_monotile` + """ + ... + +def pinwheel(extent: Rect, iterations: int) -> list[Polygon2]: + """ +Pinwheel tiling (Radin 1994): 1:2:√5 right triangles subdivided +`iterations` times, seeded by two triangles covering the extent. + +Rust: `patterns::aperiodic::pinwheel` + """ + ... + +def fibonacci_word(n: int) -> list[bool]: + """ +The Fibonacci word: fixed point of a→ab, b→a. Returns the first +`n` letters (`true` = a). + +Rust: `patterns::aperiodic::fibonacci_word` + """ + ... + +def cut_and_project_1d(slope: float, extent: float) -> list[float]: + """ +1-D quasicrystal by the canonical cut-and-project scheme: lattice +points of ℤ² whose perpendicular coordinate falls in the canonical +window are projected onto the line of the given slope. Returns +sorted positions with |x| <= extent. Irrational slopes give +aperiodic point sets (slope 1/φ gives the Fibonacci chain). + +Panics: +Panics unless `extent > 0`. + +Rust: `patterns::aperiodic::cut_and_project_1d` + """ + ... diff --git a/bindings/python/python/numeria/patterns/knots.pyi b/bindings/python/python/numeria/patterns/knots.pyi new file mode 100644 index 0000000..e50f52e --- /dev/null +++ b/bindings/python/python/numeria/patterns/knots.pyi @@ -0,0 +1,259 @@ +""" +Knots and space curves: parametric knot families, Frenet and rotation-minimizing frames, curvature/torsion estimates, and the classical knot invariants computable from a curve in space — writhe and linking number by the Gauss integral, crossing numbers of projections, and the Alexander polynomial from a knot diagram. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.spatial.frame import Frame +from numeria.spatial.primitives import Polyline +from numeria.math import Vec3 + +def torus_knot(p: int, q: int, r_major: float, r_minor: float, t: float) -> Vec3: + """ +Point on the (p, q) torus knot at parameter `t` ∈ [0, 2π): +winds `p` times around the torus axis and `q` times through the +hole of the torus with radii `r_major` > `r_minor`. + +x = (R + r cos qt) cos pt, y = (R + r cos qt) sin pt, z = r sin qt. + +Panics: +Panics unless `p, q >= 1` and `r_major > r_minor > 0`. + +Rust: `patterns::knots::torus_knot` + """ + ... + +def torus_knot_curve(p: int, q: int, r_major: float, r_minor: float, n: int) -> Polyline: + """ +Closed polyline sampling of the (p, q) torus knot with `n` +vertices. + +Panics: +Panics unless `n >= 3` (and the `torus_knot` preconditions hold). + +Rust: `patterns::knots::torus_knot_curve` + """ + ... + +def lissajous_knot(nx: int, ny: int, nz: int, phase_x: float, phase_y: float, phase_z: float, t: float) -> Vec3: + """ +Point on a Lissajous knot: x = cos(nx t + φx), y = cos(ny t + φy), +z = cos(nz t + φz). Coprime frequencies with generic phases give +knotted closed curves (e.g. (3, 2, 7) with φ = (0.7, 0.2, 0)). + +Rust: `patterns::knots::lissajous_knot` + """ + ... + +def trefoil(t: float) -> Vec3: + """ +The trefoil knot 3₁ in its symmetric parametrization: +(sin t + 2 sin 2t, cos t − 2 cos 2t, −sin 3t), t ∈ [0, 2π). + +Rust: `patterns::knots::trefoil` + """ + ... + +def figure_eight_knot(t: float) -> Vec3: + """ +The figure-eight knot 4₁: +((2 + cos 2t) cos 3t, (2 + cos 2t) sin 3t, sin 4t), t ∈ [0, 2π). + +Rust: `patterns::knots::figure_eight_knot` + """ + ... + +def cinquefoil(t: float) -> Vec3: + """ +The cinquefoil (Solomon's seal) knot 5₁ = (2, 5) torus knot on +the torus R = 2, r = 1. + +Rust: `patterns::knots::cinquefoil` + """ + ... + +def frenet_frame(curve: Callable[[float], Vec3 | Sequence[float]], t: float, h: float) -> Optional[Frame]: + """ +Frenet frame of a curve at `t` by central differences with step +`h`: x axis = unit tangent T, y = principal normal N, z = +binormal B = T × N. `None` where the frame is undefined (zero +speed or zero curvature). + +Panics: +Panics unless `h > 0`. + +Rust: `patterns::knots::frenet_frame` + """ + ... + +def frenet_frames_polyline(pl: Polyline) -> list[Frame]: + """ +Discrete Frenet frames at every vertex of a polyline (tangent by +central difference, normal from the discrete curvature vector). +Straight stretches inherit the previous normal so the field stays +continuous. + +Panics: +Panics unless the polyline has at least 2 points. + +Rust: `patterns::knots::frenet_frames_polyline` + """ + ... + +def parallel_transport_frames(pl: Polyline) -> list[Frame]: + """ +Rotation-minimizing frames along a polyline by the double +reflection method (Wang, Jüttler, Zheng & Liu 2008): each step +reflects the previous frame in the chord bisector plane and then +in the tangent bisector plane, which transports the normal with +no spurious twist (fourth-order accurate for smooth curves). + +Panics: +Panics unless the polyline has at least 2 points. + +Rust: `patterns::knots::parallel_transport_frames` + """ + ... + +def curvature_torsion(curve: Callable[[float], Vec3 | Sequence[float]], t: float, h: float) -> tuple[float, float]: + """ +Curvature and torsion of a curve at `t` by finite differences: +κ = |c′ × c″| / |c′|³ and τ = (c′ × c″)·c‴ / |c′ × c″|². + +Panics: +Panics unless `h > 0`. + +Rust: `patterns::knots::curvature_torsion` + """ + ... + +def total_curvature(pl: Polyline) -> float: + """ +Total curvature of a polyline: the sum of exterior turning angles +between consecutive segments. For closed knotted curves this is +at least 4π (Fáry-Milnor). + +Rust: `patterns::knots::total_curvature` + """ + ... + +def writhe(pl: Polyline) -> float: + """ +Writhe of a closed polyline: the Gauss double integral +Wr = (1/4π) ∮∮ (dr₁ × dr₂)·(r₁ − r₂)/|r₁ − r₂|³, evaluated +exactly over segment pairs by the solid-angle formula. Planar +curves have writhe 0. + +Panics: +Panics unless the polyline is closed with at least 3 points. + +Rust: `patterns::knots::writhe` + """ + ... + +def linking_number(a: Polyline, b: Polyline) -> int: + """ +Linking number of two closed polylines by the Gauss double sum; +the result is an integer for disjoint closed curves. + +Panics: +Panics unless both polylines are closed with at least 3 points. + +Rust: `patterns::knots::linking_number` + """ + ... + +def crossing_number_projection(pl: Polyline, direction: Vec3 | Sequence[float]) -> int: + """ +Number of crossings in the projection of the polyline along +`direction` (transverse double points of the diagram). + +Panics: +Panics unless the polyline is closed and `direction` is non-zero. + +Rust: `patterns::knots::crossing_number_projection` + """ + ... + +def alexander_polynomial_coeffs(pl: Polyline) -> list[int]: + """ +Alexander polynomial coefficients (lowest degree first) computed +from the diagram of the closed polyline projected along +z. Arcs +run between undercrossings; each crossing contributes the +abelianized Fox-derivative row of its Wirtinger relation +(over-arc 1 − t, incoming under-arc t, outgoing under-arc −1 for +a positive crossing), one row and one column are deleted, and the +determinant is recovered by evaluation at integer points and +Lagrange interpolation. Normalized so the constant term is +non-zero and the leading coefficient positive; the unknot (no +crossings) gives `[1]`. + +The projection must be regular: only transverse double points. +Sample the curve finely enough that no segment participates in +two crossings with nearly equal positions. + +Panics: +Panics unless the polyline is closed with at least 3 points. + +Rust: `patterns::knots::alexander_polynomial_coeffs` + """ + ... + +def knot_tube(pl: Polyline, radius: float, segments: int) -> Mesh: + """ +Sweeps a circle of `radius` along the polyline (delegates to +`mesh::generate::tube_along_polyline`). + +Rust: `patterns::knots::knot_tube` + """ + ... + +def helix(radius: float, pitch: float, turns: float, n: int) -> Polyline: + """ +Circular helix of given radius, pitch (rise per turn), and number +of turns, sampled at `n` points. + +Panics: +Panics unless `radius > 0`, `turns > 0`, and `n >= 2`. + +Rust: `patterns::knots::helix` + """ + ... + +def double_helix(radius: float, pitch: float, turns: float, n: int, phase: float) -> tuple[Polyline, Polyline]: + """ +Two helices on the same axis separated by `phase` radians (DNA +uses phase ≈ 2.1 rad for the minor/major groove asymmetry). + +Rust: `patterns::knots::double_helix` + """ + ... + +def viviani_curve(a: float, t: float) -> Vec3: + """ +Viviani's curve: the intersection of the sphere of radius 2a with +the cylinder of radius a tangent to its vertical axis: +(a(1 + cos t), a sin t, 2a sin(t/2)), t ∈ [0, 4π) for the full +figure-eight. + +Panics: +Panics unless `a > 0`. + +Rust: `patterns::knots::viviani_curve` + """ + ... + +def tennis_ball_curve(a: float, b: float, t: float) -> Vec3: + """ +Tennis-ball seam curve: (a cos t + b cos 3t, a sin t − b sin 3t, +2 √(ab) sin 2t) lies on the sphere of radius a + b. + +Panics: +Panics unless `a, b > 0`. + +Rust: `patterns::knots::tennis_ball_curve` + """ + ... diff --git a/bindings/python/python/numeria/patterns/packing.pyi b/bindings/python/python/numeria/patterns/packing.pyi new file mode 100644 index 0000000..28e7e6c --- /dev/null +++ b/bindings/python/python/numeria/patterns/packing.pyi @@ -0,0 +1,216 @@ +""" +Circle and sphere packings: Descartes/Apollonian circles, lattice packings, random sequential adsorption, Doyle spirals, Ford circles, Steiner chains, and the problem of Apollonius. Lattice generators include every circle/sphere whose *center* lies in the half-open region, so exact-multiple regions give the exact lattice density. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.spatial.primitives import Aabb +from numeria.astrophysics.nbody import Body +from numeria.spatial.primitives import Circle +from numeria.spatial.primitives import Polygon2 +from numeria.spatial.primitives import Rect +from numeria.monte_carlo import Rng +from numeria.spatial.primitives import Sphere + +def descartes_fourth_circle(k1: float, k2: float, k3: float) -> tuple[float, float]: + """ +Descartes circle theorem: curvatures of the two circles tangent to +three mutually tangent circles with curvatures k1, k2, k3: +k4 = k1 + k2 + k3 ± 2√(k1k2 + k2k3 + k3k1). + +Panics: +Panics when the discriminant is negative (not a tangent triple). + +Rust: `patterns::packing::descartes_fourth_circle` + """ + ... + +def apollonian_gasket(outer: Circle, k2: float, k3: float, depth: int) -> list[Circle]: + """ +Apollonian gasket inside `outer`: the two seed circles have +curvatures `k2`, `k3` (both tangent to the outer circle and each +other, placed on the horizontal axis), recursively filled to +`depth`. Returns all circles including the outer and seeds. + +Panics: +Panics unless the curvatures are compatible: `k2, k3 > 1/R` and +`1/k2 + 1/k3 = 2R - ...` — concretely both seed radii must fit: +`1/k2 + 1/k3 == R` is required for a tangent chain on the axis. + +Rust: `patterns::packing::apollonian_gasket` + """ + ... + +def apollonian_gasket_integral(depth: int) -> list[Circle]: + """ +The classic integral Apollonian gasket with curvatures +(−1, 2, 2, 3, 3): outer unit circle, two half circles. + +Rust: `patterns::packing::apollonian_gasket_integral` + """ + ... + +def circle_pack_greedy(region: Polygon2, radii: list[float], rng: Rng, attempts: int) -> list[Circle]: + """ +Greedy random packing: for each radius in order, up to `attempts` +random placements inside the polygon (respecting the boundary and +previously placed circles); radii that do not fit are skipped. + +Rust: `patterns::packing::circle_pack_greedy` + """ + ... + +def circle_pack_hex(region: Rect, r: float) -> list[Circle]: + """ +Hexagonal (densest) circle packing: circles of radius `r` whose +centers lie in the half-open region. + +Panics: +Panics unless `r > 0`. + +Rust: `patterns::packing::circle_pack_hex` + """ + ... + +def circle_pack_square(region: Rect, r: float) -> list[Circle]: + """ +Square-lattice circle packing (centers in the half-open region). + +Panics: +Panics unless `r > 0`. + +Rust: `patterns::packing::circle_pack_square` + """ + ... + +def circle_pack_relax(circles: MutableSequence[Circle], region: Rect, iterations: int) -> None: + """ +Relaxes overlapping circles by symmetric push-apart steps, keeping +centers at least their radius away from the rectangle boundary. + +Rust: `patterns::packing::circle_pack_relax` + """ + ... + +def sphere_pack_fcc(region: Aabb, r: float) -> list[Sphere]: + """ +Face-centered-cubic sphere packing (density π/(3√2) ≈ 0.7405), +centers in the half-open box. + +Panics: +Panics unless `r > 0`. + +Rust: `patterns::packing::sphere_pack_fcc` + """ + ... + +def sphere_pack_hcp(region: Aabb, r: float) -> list[Sphere]: + """ +Hexagonal-close-packed spheres (same density as FCC), ABAB layer +stacking along z; centers in the half-open box. + +Panics: +Panics unless `r > 0`. + +Rust: `patterns::packing::sphere_pack_hcp` + """ + ... + +def sphere_pack_bcc(region: Aabb, r: float) -> list[Sphere]: + """ +Body-centered-cubic spheres (density π√3/8 ≈ 0.6802), centers in +the half-open box. + +Panics: +Panics unless `r > 0`. + +Rust: `patterns::packing::sphere_pack_bcc` + """ + ... + +def sphere_pack_random_sequential(region: Aabb, r: float, rng: Rng, max_attempts: int) -> list[Sphere]: + """ +Random sequential adsorption: spheres placed uniformly at random, +rejected on overlap, until `max_attempts` placements fail +(saturation density ≈ 0.38). + +Panics: +Panics unless `r > 0`. + +Rust: `patterns::packing::sphere_pack_random_sequential` + """ + ... + +def packing_density_2d(circles: list[Circle], region: Rect) -> float: + """ +Fraction of the region area covered, counting each circle's full +area (consistent with the centers-in-region conventions above). + +Rust: `patterns::packing::packing_density_2d` + """ + ... + +def packing_density_3d(spheres: list[Sphere], region: Aabb) -> float: + """ +Fraction of the box volume covered, counting each sphere's full +volume. + +Rust: `patterns::packing::packing_density_3d` + """ + ... + +def doyle_spiral(p: int, q: int, count: int) -> list[Circle]: + """ +Doyle spiral circle packing with `p` and `q` arms: each circle is +tangent to its neighbors along both spiral directions. The moduli +of the two spiral generators are solved numerically (Newton with +numeric Jacobian) so all three tangency ratios agree. + +Panics: +Panics unless `1 <= p < q` and the solver converges. + +Rust: `patterns::packing::doyle_spiral` + """ + ... + +def ford_circles(max_denominator: int) -> list[Circle]: + """ +Ford circles: for every reduced fraction p/q with +`q <= max_denominator` in [0, 1], the circle tangent to the x axis +at p/q with radius 1/(2q²). + +Panics: +Panics unless `max_denominator >= 1`. + +Rust: `patterns::packing::ford_circles` + """ + ... + +def steiner_chain(outer: Circle, inner: Circle, n: int) -> Optional[list[Circle]]: + """ +Steiner chain of `n` circles in the annular region between `inner` +and `outer` (inner strictly inside outer). Returns `None` when the +pair does not admit a closed chain of exactly `n` circles +(Steiner's porism: feasibility depends only on the inversive +distance). + +Panics: +Panics unless `n >= 3` and `inner` is strictly inside `outer`. + +Rust: `patterns::packing::steiner_chain` + """ + ... + +def tangent_circles_to_three(c1: Circle, c2: Circle, c3: Circle) -> list[Circle]: + """ +The problem of Apollonius: circles tangent to three given circles +(up to 8 solutions, one per internal/external tangency sign +choice). Solved by reducing the tangency equations to a linear +system plus a quadratic in the radius. + +Rust: `patterns::packing::tangent_circles_to_three` + """ + ... diff --git a/bindings/python/python/numeria/patterns/phyllotaxis.pyi b/bindings/python/python/numeria/patterns/phyllotaxis.pyi new file mode 100644 index 0000000..5884369 --- /dev/null +++ b/bindings/python/python/numeria/patterns/phyllotaxis.pyi @@ -0,0 +1,220 @@ +""" +Phyllotactic patterns and spirals: Vogel sunflowers, Fibonacci point sets, the classical spiral family, and parastichy analysis. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.patterns.tilings import Archimedean +from numeria.math import Vec2 +from numeria.math import Vec3 + +def vogel_sunflower(n: int, scale: float) -> list[Vec2]: + """ +Vogel's sunflower model (Vogel 1979): floret i at radius +`scale · √i` and angle `i · GOLDEN_ANGLE`. + +Rust: `patterns::phyllotaxis::vogel_sunflower` + """ + ... + +def vogel_sunflower_angle(n: int, scale: float, angle: float) -> list[Vec2]: + """ +Vogel model with an arbitrary divergence angle. + +Rust: `patterns::phyllotaxis::vogel_sunflower_angle` + """ + ... + +def fibonacci_sphere(n: int) -> list[Vec3]: + """ +Near-uniform points on the unit sphere: latitude strips of equal +area, longitude advanced by the golden angle. + +Panics: +Panics unless `n >= 1`. + +Rust: `patterns::phyllotaxis::fibonacci_sphere` + """ + ... + +def fibonacci_disk(n: int) -> list[Vec2]: + """ +Near-uniform points on the unit disk (Vogel pattern scaled so the +n-th floret reaches radius 1). + +Panics: +Panics unless `n >= 1`. + +Rust: `patterns::phyllotaxis::fibonacci_disk` + """ + ... + +def fibonacci_hemisphere(n: int) -> list[Vec3]: + """ +Near-uniform points on the upper (y > 0) unit hemisphere. + +Panics: +Panics unless `n >= 1`. + +Rust: `patterns::phyllotaxis::fibonacci_hemisphere` + """ + ... + +def golden_spiral(turns: float, points_per_turn: int, a: float) -> list[Vec2]: + """ +Golden spiral: logarithmic spiral growing by φ every quarter turn, +starting radius `a`. + +Panics: +Panics unless `turns > 0`, `points_per_turn >= 1`, `a > 0`. + +Rust: `patterns::phyllotaxis::golden_spiral` + """ + ... + +def archimedean_spiral(a: float, b: float, theta_max: float, n: int) -> list[Vec2]: + """ +Archimedean spiral r = a + bθ sampled on `n` points over +θ ∈ [0, theta_max]. + +Panics: +Panics unless `n >= 2`. + +Rust: `patterns::phyllotaxis::archimedean_spiral` + """ + ... + +def logarithmic_spiral(a: float, b: float, theta_max: float, n: int) -> list[Vec2]: + """ +Logarithmic spiral r = a e^{bθ}. + +Panics: +Panics unless `n >= 2`. + +Rust: `patterns::phyllotaxis::logarithmic_spiral` + """ + ... + +def fermat_spiral(a: float, theta_max: float, n: int) -> list[Vec2]: + """ +Fermat (parabolic) spiral r = a √θ. + +Panics: +Panics unless `n >= 2` and `theta_max >= 0`. + +Rust: `patterns::phyllotaxis::fermat_spiral` + """ + ... + +def hyperbolic_spiral(a: float, theta_range: tuple[float, float], n: int) -> list[Vec2]: + """ +Hyperbolic spiral r = a/θ over `theta_range` (which must exclude +0). + +Panics: +Panics unless `n >= 2` and the range excludes zero. + +Rust: `patterns::phyllotaxis::hyperbolic_spiral` + """ + ... + +def lituus(a: float, theta_range: tuple[float, float], n: int) -> list[Vec2]: + """ +Lituus r = a/√θ over `theta_range` (positive). + +Panics: +Panics unless `n >= 2` and the range is positive. + +Rust: `patterns::phyllotaxis::lituus` + """ + ... + +def euler_spiral(length: float, n: int) -> list[Vec2]: + """ +Euler spiral (clothoid): curvature grows linearly with arclength, +κ(s) = s. Points via composite-Simpson evaluation of the Fresnel +integrals x = ∫cos(t²/2)dt, y = ∫sin(t²/2)dt. + +Panics: +Panics unless `n >= 2` and `length > 0`. + +Rust: `patterns::phyllotaxis::euler_spiral` + """ + ... + +def spiral_of_theodorus(n: int) -> list[Vec2]: + """ +Spiral of Theodorus (square-root spiral): `n` right triangles with +unit legs; vertex k lies at radius √(k+1). + +Panics: +Panics unless `n >= 1`. + +Rust: `patterns::phyllotaxis::spiral_of_theodorus` + """ + ... + +def conical_spiral(a: float, b: float, h: float, turns: float, n: int) -> list[Vec3]: + """ +Conical spiral: radius `a + b t`, height `h t`, `turns` full turns +over t ∈ [0, 1], axis y. + +Panics: +Panics unless `n >= 2`. + +Rust: `patterns::phyllotaxis::conical_spiral` + """ + ... + +def spherical_spiral(turns: float, n: int) -> list[Vec3]: + """ +Spherical spiral on the unit sphere: polar angle sweeps 0..π while +the azimuth makes `turns` turns (axis y). + +Panics: +Panics unless `n >= 2`. + +Rust: `patterns::phyllotaxis::spherical_spiral` + """ + ... + +def parastichy_counts(points: list[Vec2 | Sequence[float]]) -> tuple[int, int]: + """ +Detects the two dominant parastichy (visible spiral) families: +the two most common index differences between each floret and its +nearest neighbors, returned ascending. For golden-angle patterns +these are consecutive Fibonacci numbers. + +Panics: +Panics unless at least 8 points are given. + +Rust: `patterns::phyllotaxis::parastichy_counts` + """ + ... + +def cylinder_phyllotaxis(n: int, rise: float, angle: float, radius: float) -> list[Vec3]: + """ +Helical (cylindrical) phyllotaxis: point i at height `i · rise` +and azimuth `i · angle` on a cylinder of the given radius. + +Rust: `patterns::phyllotaxis::cylinder_phyllotaxis` + """ + ... + +def spiral_interpolate_sequence(a: Vec2 | Sequence[float], b: Vec2 | Sequence[float], n: int) -> list[Vec2]: + """ +Logarithmic-spiral arc from `a` to `b` about the origin: `n` +points (inclusive) interpolating radius geometrically and angle +linearly (shortest way around). + +Panics: +Panics unless `n >= 2` and both points are away from the origin. + +Rust: `patterns::phyllotaxis::spiral_interpolate_sequence` + """ + ... + +GOLDEN_ANGLE: float diff --git a/bindings/python/python/numeria/patterns/polygon_ops.pyi b/bindings/python/python/numeria/patterns/polygon_ops.pyi new file mode 100644 index 0000000..b9cb63a --- /dev/null +++ b/bindings/python/python/numeria/patterns/polygon_ops.pyi @@ -0,0 +1,368 @@ +""" +2-D polygon algorithms: triangulation, simplification, offsetting, Minkowski sums, boolean operations, clipping, decomposition, hulls, skeletons, enclosing/inscribed shapes, and fill patterns. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.spatial.primitives import Circle +from numeria.spatial.primitives import Polygon2 +from numeria.spatial.primitives import Rect +from numeria.math import Vec2 +from numeria.math import Vec3 + +class JoinStyle: + """ +How offset corners are joined. + +Rust: `patterns::polygon_ops::JoinStyle` + """ + ... + +def triangulate_ear_clipping(poly: Polygon2) -> list[list[int]]: + """ +Ear-clipping triangulation of a simple polygon. Indices refer to +the polygon's own vertex order (clockwise input is handled). + +Errors: +`GeomError::InvalidArgument` for fewer than 3 vertices; +`GeomError::Degenerate` for zero area or self-intersecting input. + +Rust: `patterns::polygon_ops::triangulate_ear_clipping` + """ + ... + +def triangulate_with_holes(outer: Polygon2, holes: list[Polygon2]) -> tuple[list[Vec2], list[list[int]]]: + """ +Triangulates a polygon with holes by bridging each hole to the +outer boundary (rightmost-vertex visibility bridge) and ear +clipping the result. Returns the combined vertex list (outer, then +holes in bridging order, with two duplicated bridge vertices per +hole) and triangles into it. + +Errors: +Propagates the failure modes of `triangulate_ear_clipping`; +holes must be strictly inside the outer polygon and disjoint. + +Rust: `patterns::polygon_ops::triangulate_with_holes` + """ + ... + +def simplify_douglas_peucker(pts: list[Vec2 | Sequence[float]], epsilon: float) -> list[Vec2]: + """ +Ramer-Douglas-Peucker polyline simplification: keeps points whose +deviation exceeds `epsilon`. Endpoints are always kept. + +Panics: +Panics unless `epsilon >= 0`. + +Rust: `patterns::polygon_ops::simplify_douglas_peucker` + """ + ... + +def simplify_visvalingam(pts: list[Vec2 | Sequence[float]], min_area: float) -> list[Vec2]: + """ +Visvalingam-Whyatt simplification: repeatedly removes the interior +point spanning the smallest triangle until every remaining point +spans at least `min_area`. + +Panics: +Panics unless `min_area >= 0`. + +Rust: `patterns::polygon_ops::simplify_visvalingam` + """ + ... + +def offset_polygon(poly: Polygon2, distance: float, join: JoinStyle) -> list[Polygon2]: + """ +Offsets a simple polygon outward (`distance > 0`) or inward +(`distance < 0`), joining corners by `join`. Self-intersections of +the raw offset ring (spikes collapsing under inset, etc.) are +resolved by splitting into simple loops and keeping +counterclockwise ones; an inset larger than the inradius returns +an empty vector. Input orientation does not matter; outputs are +counterclockwise. + +Panics: +Panics unless the polygon has >= 3 vertices and `distance != 0`. + +Rust: `patterns::polygon_ops::offset_polygon` + """ + ... + +def minkowski_sum_convex(a: Polygon2, b: Polygon2) -> Polygon2: + """ +Minkowski sum of two convex polygons by the edge-merge +(convolution) construction; output is convex and counterclockwise. + +Panics: +Panics unless both polygons are convex with >= 3 vertices. + +Rust: `patterns::polygon_ops::minkowski_sum_convex` + """ + ... + +def minkowski_sum(a: Polygon2, b: Polygon2) -> list[Polygon2]: + """ +Minkowski sum of two simple polygons via convex decomposition: +pairwise convex sums, unioned together. + +Rust: `patterns::polygon_ops::minkowski_sum` + """ + ... + +def boolean_union(a: Polygon2, b: Polygon2) -> list[Polygon2]: + """ +Union of two simple polygons. Outer loops come out +counterclockwise; holes (e.g. two C shapes closing a ring) +clockwise. + +Rust: `patterns::polygon_ops::boolean_union` + """ + ... + +def boolean_intersection(a: Polygon2, b: Polygon2) -> list[Polygon2]: + """ +Intersection of two simple polygons (possibly several pieces). + +Rust: `patterns::polygon_ops::boolean_intersection` + """ + ... + +def boolean_difference(a: Polygon2, b: Polygon2) -> list[Polygon2]: + """ +Difference a − b; a hole fully inside `a` is returned as a +clockwise loop. + +Rust: `patterns::polygon_ops::boolean_difference` + """ + ... + +def boolean_xor(a: Polygon2, b: Polygon2) -> list[Polygon2]: + """ +Symmetric difference: (a − b) ∪ (b − a), returned as the two +difference loop sets concatenated. + +Rust: `patterns::polygon_ops::boolean_xor` + """ + ... + +def clip_polygon_convex(subject: Polygon2, clip: Polygon2) -> Polygon2: + """ +Sutherland-Hodgman clipping of an arbitrary subject polygon +against a convex clip polygon. + +Panics: +Panics unless `clip` is convex with >= 3 vertices. + +Rust: `patterns::polygon_ops::clip_polygon_convex` + """ + ... + +def clip_polygon_rect(subject: Polygon2, rect: Rect) -> Polygon2: + """ +Clips a polygon to an axis-aligned rectangle +(Sutherland-Hodgman). + +Rust: `patterns::polygon_ops::clip_polygon_rect` + """ + ... + +def clip_line_rect(a: Vec2 | Sequence[float], b: Vec2 | Sequence[float], rect: Rect) -> Optional[tuple[Vec2, Vec2]]: + """ +Liang-Barsky segment clipping against a rectangle; `None` when the +segment misses it entirely. + +Rust: `patterns::polygon_ops::clip_line_rect` + """ + ... + +def convex_decomposition(poly: Polygon2) -> list[Polygon2]: + """ +Hertel-Mehlhorn convex decomposition: triangulate, then greedily +remove inessential diagonals. At most 4x the optimal piece count. + +Panics: +Panics when the polygon cannot be triangulated (see +`triangulate_ear_clipping` for the failure modes). + +Rust: `patterns::polygon_ops::convex_decomposition` + """ + ... + +def convex_hull_2d(points: list[Vec2 | Sequence[float]]) -> Polygon2: + """ +Convex hull by Andrew's monotone chain, counterclockwise, minimal +vertex set (collinear points dropped). + +Panics: +Panics with fewer than 3 input points. + +Rust: `patterns::polygon_ops::convex_hull_2d` + """ + ... + +def convex_hull_3d(points: list[Vec3 | Sequence[float]]) -> Mesh: + """ +Convex hull of 3-D points as a triangle mesh (incremental hull, +outward-facing counterclockwise faces). + +Panics: +Panics with fewer than 4 points or fully coplanar input. + +Rust: `patterns::polygon_ops::convex_hull_3d` + """ + ... + +def straight_skeleton(poly: Polygon2) -> list[Segment2]: + """ +Straight skeleton arcs of a simple polygon by the +shrinking-wavefront (roof) construction in the style of Felkel & +Obdržálek 1998, processing edge events (wavefront edges collapsing +as vertices meet). Split events of reflex vertices are not +resolved, so results are exact for convex polygons and approximate +for mildly non-convex ones. Each arc runs from a wavefront vertex +(original or intermediate) to the event point that consumed it. + +Panics: +Panics unless the polygon is simple with >= 3 vertices. + +Rust: `patterns::polygon_ops::straight_skeleton` + """ + ... + +def largest_inscribed_circle(poly: Polygon2) -> Circle: + """ +Largest inscribed circle (pole of inaccessibility) by Mapbox's +polylabel quadtree refinement. + +Panics: +Panics unless the polygon has >= 3 vertices and `precision > 0` +would hold for the derived tolerance (bbox-scaled 1e-6). + +Rust: `patterns::polygon_ops::largest_inscribed_circle` + """ + ... + +def smallest_enclosing_circle(points: list[Vec2 | Sequence[float]]) -> Circle: + """ +Smallest enclosing circle by Welzl's expected-linear incremental +algorithm (deterministically shuffled). + +Panics: +Panics on empty input. + +Rust: `patterns::polygon_ops::smallest_enclosing_circle` + """ + ... + +def minimum_bounding_rect(points: list[Vec2 | Sequence[float]]) -> tuple[Vec2, Vec2, float]: + """ +Minimum-area oriented bounding rectangle by rotating calipers over +the convex hull: returns `(center, half_extents, angle)`, the +rectangle's local x axis rotated by `angle` from world x. + +Panics: +Panics with fewer than 3 points. + +Rust: `patterns::polygon_ops::minimum_bounding_rect` + """ + ... + +def polygon_diameter(poly: Polygon2) -> tuple[int, int, float]: + """ +Farthest vertex pair (diameter) of a polygon: indices and +distance. + +Panics: +Panics with fewer than 2 vertices. + +Rust: `patterns::polygon_ops::polygon_diameter` + """ + ... + +def polygon_width(poly: Polygon2) -> float: + """ +Minimum width of the polygon: the smallest distance between +parallel supporting lines (over hull edge directions). + +Panics: +Panics with fewer than 3 vertices. + +Rust: `patterns::polygon_ops::polygon_width` + """ + ... + +def resample_polygon(poly: Polygon2, n: int) -> Polygon2: + """ +Resamples the polygon boundary into `n` equally spaced points +(by arclength) starting at vertex 0. + +Panics: +Panics unless `n >= 3` and the polygon has positive perimeter. + +Rust: `patterns::polygon_ops::resample_polygon` + """ + ... + +def smooth_chaikin(poly: Polygon2, iterations: int) -> Polygon2: + """ +Chaikin corner cutting (closed polygon): each iteration replaces +every edge with its 1/4 and 3/4 points, converging to a smooth +quadratic B-spline. + +Rust: `patterns::polygon_ops::smooth_chaikin` + """ + ... + +def round_corners(poly: Polygon2, radius: float, segments: int) -> Polygon2: + """ +Replaces each corner by a circular arc of the given radius +(clamped to half of the shorter adjacent edge), sampled with +`segments` points. + +Panics: +Panics unless `radius > 0` and `segments >= 1`. + +Rust: `patterns::polygon_ops::round_corners` + """ + ... + +def hatch_fill(poly: Polygon2, spacing: float, angle: float) -> list[Segment2]: + """ +Parallel hatch lines filling the polygon: scanlines spaced by +`spacing`, rotated by `angle` radians from the x axis (even-odd +filled). + +Panics: +Panics unless `spacing > 0`. + +Rust: `patterns::polygon_ops::hatch_fill` + """ + ... + +def contour_fill(poly: Polygon2, spacing: float) -> list[Polygon2]: + """ +Concentric fill: repeated inward offsets by `spacing` until the +polygon vanishes. + +Panics: +Panics unless `spacing > 0`. + +Rust: `patterns::polygon_ops::contour_fill` + """ + ... + +def polygon_to_mesh_2d(poly: Polygon2, holes: list[Polygon2]) -> Mesh: + """ +Triangulates a polygon (optionally with holes) into a flat mesh at +z = 0, facing +z. + +Panics: +Panics when triangulation fails (non-simple input). + +Rust: `patterns::polygon_ops::polygon_to_mesh_2d` + """ + ... diff --git a/bindings/python/python/numeria/patterns/polyhedra.pyi b/bindings/python/python/numeria/patterns/polyhedra.pyi new file mode 100644 index 0000000..20dd09a --- /dev/null +++ b/bindings/python/python/numeria/patterns/polyhedra.pyi @@ -0,0 +1,349 @@ +""" +Polyhedra: Platonic/Archimedean/Catalan/Johnson solids, Goldberg and geodesic polyhedra, and Conway polyhedron operators. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.patterns.tilings import Archimedean +from numeria.patterns.symmetry import PointGroup3 +from numeria.math import Vec3 + +class ArchimedeanSolid: + """ +The 13 Archimedean solids. + +Rust: `patterns::polyhedra::ArchimedeanSolid` + """ + ... + +class Polyhedron: + """ +A polyhedron with polygonal faces (counterclockwise seen from +outside). + +Rust: `patterns::polyhedra::Polyhedron` + """ + def __init__(self, vertices: list[Vec3 | Sequence[float]], faces: list[list[int]]) -> None: ... + def to_mesh(self) -> Mesh: ... + def edges(self) -> list[tuple[int, int]]: ... + def euler(self) -> int: ... + def face_centroids(self) -> list[Vec3]: ... + def face_normals(self) -> list[Vec3]: ... + def volume(self) -> float: ... + def surface_area(self) -> float: ... + def is_convex(self) -> bool: ... + def faces_around_vertex(self, v: int) -> list[int]: ... + def vertex_figure(self, v: int) -> list[int]: ... + def normalize(self, radius: float) -> Polyhedron: ... + def canonicalize(self, iterations: int) -> Polyhedron: ... + def dual(self) -> Polyhedron: ... + def symmetry_group(self) -> Optional[PointGroup3]: ... + @property + def vertices(self) -> list[Vec3]: ... + @property + def faces(self) -> list[list[int]]: ... + +def conway_dual(p: Polyhedron) -> Polyhedron: + """ +Conway dual (alias of `Polyhedron::dual`). + +Rust: `patterns::polyhedra::conway_dual` + """ + ... + +def kis(p: Polyhedron, apex_height: float) -> Polyhedron: + """ +Conway kis: a pyramid of the given apex height over every face. + +Rust: `patterns::polyhedra::kis` + """ + ... + +def ambo(p: Polyhedron) -> Polyhedron: + """ +Conway ambo (rectification): vertices at edge midpoints. + +Rust: `patterns::polyhedra::ambo` + """ + ... + +def truncate(p: Polyhedron, ratio: float) -> Polyhedron: + """ +Conway truncate: cuts each corner, moving `ratio` along every +edge (1/3 turns regular triangles into regular hexagons). + +Panics: +Panics unless `0 < ratio < 1/2`. + +Rust: `patterns::polyhedra::truncate` + """ + ... + +def chamfer(p: Polyhedron, ratio: float) -> Polyhedron: + """ +Conway chamfer: shrinks faces in-plane by `ratio` and replaces +each edge with a hexagon (original vertices kept). + +Panics: +Panics unless `0 < ratio < 1`. + +Rust: `patterns::polyhedra::chamfer` + """ + ... + +def gyro(p: Polyhedron) -> Polyhedron: + """ +Conway gyro: pentagonal faces, one per (face, edge) incidence. + +Rust: `patterns::polyhedra::gyro` + """ + ... + +def propellor(p: Polyhedron) -> Polyhedron: + """ +Conway propellor: each face spins off a smaller rotated copy +surrounded by quads. + +Rust: `patterns::polyhedra::propellor` + """ + ... + +def whirl(p: Polyhedron) -> Polyhedron: + """ +Conway whirl: hexagons spiral around shrunken rotated faces. + +Rust: `patterns::polyhedra::whirl` + """ + ... + +def join(p: Polyhedron) -> Polyhedron: + """ +Conway join = dual(ambo): rhombic faces over each original edge. + +Rust: `patterns::polyhedra::join` + """ + ... + +def needle(p: Polyhedron) -> Polyhedron: + """ +Conway needle = kis(dual). + +Rust: `patterns::polyhedra::needle` + """ + ... + +def zip(p: Polyhedron) -> Polyhedron: + """ +Conway zip = dual(kis). + +Rust: `patterns::polyhedra::zip` + """ + ... + +def ortho(p: Polyhedron) -> Polyhedron: + """ +Conway ortho = join(join). + +Rust: `patterns::polyhedra::ortho` + """ + ... + +def expand(p: Polyhedron) -> Polyhedron: + """ +Conway expand = ambo(ambo). + +Rust: `patterns::polyhedra::expand` + """ + ... + +def bevel(p: Polyhedron) -> Polyhedron: + """ +Conway bevel = truncate(ambo). + +Rust: `patterns::polyhedra::bevel` + """ + ... + +def meta(p: Polyhedron) -> Polyhedron: + """ +Conway meta = kis(join). + +Rust: `patterns::polyhedra::meta` + """ + ... + +def snub(p: Polyhedron) -> Polyhedron: + """ +Conway snub = dual(gyro). + +Rust: `patterns::polyhedra::snub` + """ + ... + +def conway_apply(p: Polyhedron, notation: str) -> Polyhedron: + """ +Applies a Conway notation string, e.g. `"tkT"` or `"dsI"`: the +rightmost character may be a seed (T, C, O, D, I); otherwise the +operators apply to `p`. Operators: d a k t j n z o e b m s g p c w. + +Errors: +Returns `GeomError::InvalidArgument` for an unknown character. + +Rust: `patterns::polyhedra::conway_apply` + """ + ... + +def tetrahedron() -> Polyhedron: + """ +Regular tetrahedron (edge 2√2). + +Rust: `patterns::polyhedra::tetrahedron` + """ + ... + +def cube() -> Polyhedron: + """ +Cube (edge 2). + +Rust: `patterns::polyhedra::cube` + """ + ... + +def octahedron() -> Polyhedron: + """ +Regular octahedron (edge √2). + +Rust: `patterns::polyhedra::octahedron` + """ + ... + +def icosahedron() -> Polyhedron: + """ +Regular icosahedron. + +Rust: `patterns::polyhedra::icosahedron` + """ + ... + +def dodecahedron() -> Polyhedron: + """ +Regular dodecahedron (dual of the icosahedron). + +Rust: `patterns::polyhedra::dodecahedron` + """ + ... + +def prism(n: int, h: float) -> Polyhedron: + """ +Right prism over a regular n-gon (unit edge circumcircle scaled so +the polygon edge is 1), height `h`. + +Panics: +Panics unless `n >= 3` and `h > 0`. + +Rust: `patterns::polyhedra::prism` + """ + ... + +def antiprism(n: int, h: float) -> Polyhedron: + """ +Antiprism over a regular n-gon (unit polygon edge), height `h`. + +Panics: +Panics unless `n >= 3` and `h > 0`. + +Rust: `patterns::polyhedra::antiprism` + """ + ... + +def pyramid(n: int, h: float) -> Polyhedron: + """ +Pyramid over a regular n-gon (unit edge base), apex height `h`. + +Panics: +Panics unless `n >= 3` and `h > 0`. + +Rust: `patterns::polyhedra::pyramid` + """ + ... + +def bipyramid(n: int, h: float) -> Polyhedron: + """ +Bipyramid over a regular n-gon (unit edge equator), apexes at ±h. + +Panics: +Panics unless `n >= 3` and `h > 0`. + +Rust: `patterns::polyhedra::bipyramid` + """ + ... + +def from_convex_points(points: list[Vec3 | Sequence[float]]) -> Polyhedron: + """ +Builds a polyhedron as the convex hull of a point set, merging +coplanar triangles into polygon faces. + +Panics: +Panics with fewer than 4 points or degenerate input. + +Rust: `patterns::polyhedra::from_convex_points` + """ + ... + +def archimedean(kind: ArchimedeanSolid) -> Polyhedron: + """ +Constructs an Archimedean solid: exact coordinates or exact Conway +constructions everywhere except the snub dodecahedron, which is +built combinatorially by the snub operator and canonicalized (its +coordinates are then approximate). + +Rust: `patterns::polyhedra::archimedean` + """ + ... + +def catalan(kind: ArchimedeanSolid) -> Polyhedron: + """ +Catalan solid: the dual of the corresponding Archimedean solid +(canonicalized so faces are planar and congruent). + +Rust: `patterns::polyhedra::catalan` + """ + ... + +def johnson(n: int) -> Optional[Polyhedron]: + """ +The first 20 Johnson solids J1..J20 with unit edges; `None` for +n = 0 or n > 20. + +Rust: `patterns::polyhedra::johnson` + """ + ... + +def geodesic_sphere(frequency: int) -> Polyhedron: + """ +Class I geodesic sphere: each icosahedron face subdivided into +`frequency`² triangles, projected to the unit sphere. + +Panics: +Panics unless `frequency >= 1`. + +Rust: `patterns::polyhedra::geodesic_sphere` + """ + ... + +def goldberg(m: int, n: int) -> Polyhedron: + """ +Goldberg polyhedron GP(m, n): hexagons plus 12 pentagons. Class I +(n = 0) and class II (m = n) are supported (class II via a √3 +refinement of the class I triangulation); general class III is +not. + +Panics: +Panics unless `m >= 1` and (`n == 0` or `n == m`). + +Rust: `patterns::polyhedra::goldberg` + """ + ... diff --git a/bindings/python/python/numeria/patterns/sampling.pyi b/bindings/python/python/numeria/patterns/sampling.pyi new file mode 100644 index 0000000..0b66a03 --- /dev/null +++ b/bindings/python/python/numeria/patterns/sampling.pyi @@ -0,0 +1,310 @@ +""" +Random and low-discrepancy sampling: Poisson disk (Bridson), blue-noise ranking, stratified jitter, uniform samplers over shapes, random polygons (Valtr), random rotations (Shoemake), and Lloyd relaxation. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.spatial.primitives import Aabb +from numeria.spatial.primitives import Circle +from numeria.spatial.primitives import Obb +from numeria.statistics.distributions import Poisson +from numeria.spatial.primitives import Polygon2 +from numeria.quaternion import Quaternion +from numeria.spatial.primitives import Rect +from numeria.monte_carlo import Rng +from numeria.spatial.primitives import Sphere +from numeria.spatial.primitives import Triangle +from numeria.spatial.primitives import Triangle2 +from numeria.math import Vec2 +from numeria.math import Vec3 + +def poisson_disk_2d(region: Rect, min_dist: float, k: int, rng: Rng) -> list[Vec2]: + """ +Bridson's Poisson disk sampling in a rectangle ("Fast Poisson Disk +Sampling in Arbitrary Dimensions", SIGGRAPH 2007): no two samples +closer than `min_dist`, maximal up to `k` candidate attempts per +active sample. + +Panics: +Panics unless `min_dist > 0` and `k >= 1`. + +Rust: `patterns::sampling::poisson_disk_2d` + """ + ... + +def poisson_disk_3d(region: Aabb, min_dist: float, k: int, rng: Rng) -> list[Vec3]: + """ +Bridson Poisson disk sampling in a box (3-D). + +Panics: +Panics unless `min_dist > 0` and `k >= 1`. + +Rust: `patterns::sampling::poisson_disk_3d` + """ + ... + +def poisson_disk_polygon(poly: Polygon2, min_dist: float, k: int, rng: Rng) -> list[Vec2]: + """ +Poisson disk sampling restricted to a polygon: Bridson over the +bounding rectangle, samples outside the polygon rejected. + +Panics: +Panics unless `min_dist > 0`, `k >= 1`, and the polygon has >= 3 +vertices. + +Rust: `patterns::sampling::poisson_disk_polygon` + """ + ... + +def poisson_disk_variable(region: Rect, density: Callable[[Vec2 | Sequence[float]], float], k: int, rng: Rng) -> list[Vec2]: + """ +Variable-density Poisson disk sampling: `density` maps a point to +its local minimum distance (larger density value = larger +spacing). Dart throwing against a conflict grid keyed by the +smallest local radius. + +Panics: +Panics unless `k >= 1` and `density` returns positive values over +the region (sampled at the corners and center). + +Rust: `patterns::sampling::poisson_disk_variable` + """ + ... + +def poisson_disk_surface(mesh: Mesh, min_dist: float, rng: Rng) -> list[Vec3]: + """ +Poisson disk sampling on a mesh surface by dart throwing over +area-weighted surface samples. + +Panics: +Panics unless `min_dist > 0` and the mesh has positive area. + +Rust: `patterns::sampling::poisson_disk_surface` + """ + ... + +def blue_noise_void_cluster(w: int, h: int, n: int) -> list[Vec2]: + """ +Blue-noise point ranking on a `w` x `h` grid by the void-and-cluster +method (Ulichney 1993, toroidal Gaussian energy): returns the `n` +best-spread grid cell centers. + +Panics: +Panics unless `n <= w * h / 2` and the grid is nonempty. + +Rust: `patterns::sampling::blue_noise_void_cluster` + """ + ... + +def stratified_2d(nx: int, ny: int, jitter: float, rng: Rng) -> list[Vec2]: + """ +Stratified jittered samples on the unit square: one sample per +cell of an `nx` x `ny` grid, jittered by `jitter` in [0, 1]. + +Panics: +Panics unless `nx, ny >= 1` and `jitter` is in [0, 1]. + +Rust: `patterns::sampling::stratified_2d` + """ + ... + +def uniform_in_triangle(t: Triangle2, rng: Rng) -> Vec2: + """ +Uniform point in a 2-D triangle by the square-root warp. + +Rust: `patterns::sampling::uniform_in_triangle` + """ + ... + +def uniform_in_triangle_3d(t: Triangle, rng: Rng) -> Vec3: + """ +Uniform point in a 3-D triangle. + +Rust: `patterns::sampling::uniform_in_triangle_3d` + """ + ... + +def uniform_in_polygon(poly: Polygon2, rng: Rng) -> Vec2: + """ +Uniform point in a simple polygon: triangulate, pick a triangle by +area, sample it. + +Panics: +Panics when the polygon cannot be triangulated. + +Rust: `patterns::sampling::uniform_in_polygon` + """ + ... + +def uniform_in_circle(c: Circle, rng: Rng) -> Vec2: + """ +Uniform point inside a circle. + +Rust: `patterns::sampling::uniform_in_circle` + """ + ... + +def uniform_on_circle(c: Circle, rng: Rng) -> Vec2: + """ +Uniform point on a circle's boundary. + +Rust: `patterns::sampling::uniform_on_circle` + """ + ... + +def uniform_in_sphere(s: Sphere, rng: Rng) -> Vec3: + """ +Uniform point inside a sphere (cube-root radial warp). + +Rust: `patterns::sampling::uniform_in_sphere` + """ + ... + +def uniform_on_sphere(s: Sphere, rng: Rng) -> Vec3: + """ +Uniform point on a sphere's surface. + +Rust: `patterns::sampling::uniform_on_sphere` + """ + ... + +def uniform_on_hemisphere(n: Vec3 | Sequence[float], rng: Rng) -> Vec3: + """ +Uniform direction on the unit hemisphere around `n`. + +Panics: +Panics when `n` is zero. + +Rust: `patterns::sampling::uniform_on_hemisphere` + """ + ... + +def cosine_weighted_hemisphere(n: Vec3 | Sequence[float], rng: Rng) -> Vec3: + """ +Cosine-weighted direction on the hemisphere around `n` (Malley's +method: uniform disk lifted to the sphere). + +Panics: +Panics when `n` is zero. + +Rust: `patterns::sampling::cosine_weighted_hemisphere` + """ + ... + +def uniform_in_aabb(b: Aabb, rng: Rng) -> Vec3: + """ +Uniform point inside an axis-aligned box. + +Rust: `patterns::sampling::uniform_in_aabb` + """ + ... + +def uniform_in_obb(b: Obb, rng: Rng) -> Vec3: + """ +Uniform point inside an oriented box. + +Rust: `patterns::sampling::uniform_in_obb` + """ + ... + +def uniform_on_aabb_surface(b: Aabb, rng: Rng) -> Vec3: + """ +Uniform point on the surface of an axis-aligned box +(area-weighted face choice). + +Rust: `patterns::sampling::uniform_on_aabb_surface` + """ + ... + +def uniform_in_annulus(c: Vec2 | Sequence[float], r_in: float, r_out: float, rng: Rng) -> Vec2: + """ +Uniform point in the annulus between `r_in` and `r_out`. + +Panics: +Panics unless `0 <= r_in < r_out`. + +Rust: `patterns::sampling::uniform_in_annulus` + """ + ... + +def uniform_in_cone(axis: Vec3 | Sequence[float], angle: float, rng: Rng) -> Vec3: + """ +Uniform direction within the cone of half-angle `angle` around +`axis` (solid-angle uniform). + +Panics: +Panics unless `axis` is nonzero and `angle` is in (0, π]. + +Rust: `patterns::sampling::uniform_in_cone` + """ + ... + +def random_convex_polygon(n: int, rng: Rng) -> Polygon2: + """ +Random convex polygon with `n` vertices by Valtr's algorithm +(uniform over convex polygons in the unit square), counterclockwise. + +Panics: +Panics unless `n >= 3`. + +Rust: `patterns::sampling::random_convex_polygon` + """ + ... + +def random_simple_polygon(n: int, rng: Rng) -> Polygon2: + """ +Random simple polygon: random points untangled by repeatedly +swapping crossing edges (2-opt), which strictly shortens the +perimeter and therefore terminates. + +Panics: +Panics unless `n >= 3`. + +Rust: `patterns::sampling::random_simple_polygon` + """ + ... + +def random_rotation(rng: Rng) -> Quaternion: + """ +Uniform random rotation by Shoemake's subgroup algorithm (uniform +over SO(3)). + +Rust: `patterns::sampling::random_rotation` + """ + ... + +def random_unit_vector(rng: Rng) -> Vec3: + """ +Uniform random unit vector (normalized Gaussian triple). + +Rust: `patterns::sampling::random_unit_vector` + """ + ... + +def lloyd_relaxation(points: MutableSequence[Vec2 | Sequence[float]], region: Rect, iterations: int) -> None: + """ +Lloyd relaxation toward a centroidal Voronoi arrangement: each +iteration moves every point to the centroid of its (grid-sampled) +Voronoi cell within `region`. + +Panics: +Panics when `points` is empty. + +Rust: `patterns::sampling::lloyd_relaxation` + """ + ... + +def stipple(density: Callable[[Vec2 | Sequence[float]], float], region: Rect, n: int, iterations: int, rng: Rng) -> list[Vec2]: + """ +Weighted stippling: `n` seed points relaxed by density-weighted +Lloyd iterations, so point density tracks `density`. + +Panics: +Panics unless `n >= 1` and `density` is nonnegative where sampled. + +Rust: `patterns::sampling::stipple` + """ + ... diff --git a/bindings/python/python/numeria/patterns/space_filling.pyi b/bindings/python/python/numeria/patterns/space_filling.pyi new file mode 100644 index 0000000..797a29c --- /dev/null +++ b/bindings/python/python/numeria/patterns/space_filling.pyi @@ -0,0 +1,227 @@ +""" +Space-filling curves and locality-preserving orders: Hilbert (2-D and 3-D), Peano, Morton/Z-order, Gray codes, and L-system curves (Sierpiński arrowhead, Moore, Gosper). +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.math import Vec2 +from numeria.math import Vec3 + +def hilbert_d2xy(order: int, d: int) -> tuple[int, int]: + """ +Hilbert curve index to grid coordinates on a `2^order` square grid +(Wikipedia's iterative rotate-and-flip formulation). + +Panics: +Panics unless `1 <= order <= 31` and `d < 4^order`. + +Rust: `patterns::space_filling::hilbert_d2xy` + """ + ... + +def hilbert_xy2d(order: int, x: int, y: int) -> int: + """ +Grid coordinates to Hilbert index (inverse of `hilbert_d2xy`). + +Panics: +Panics unless `1 <= order <= 31` and both coordinates are below +`2^order`. + +Rust: `patterns::space_filling::hilbert_xy2d` + """ + ... + +def hilbert_curve_2d(order: int) -> list[Vec2]: + """ +The full Hilbert curve as points in the unit square (cell +centers), in curve order. + +Panics: +Panics unless `1 <= order <= 10` (2^20 points at most). + +Rust: `patterns::space_filling::hilbert_curve_2d` + """ + ... + +def hilbert_3d_d2xyz(order: int, d: int) -> tuple[int, int, int]: + """ +3-D Hilbert index to grid coordinates on a `2^order` cube. + +Panics: +Panics unless `1 <= order <= 21` and `d < 8^order`. + +Rust: `patterns::space_filling::hilbert_3d_d2xyz` + """ + ... + +def hilbert_3d_xyz2d(order: int, x: int, y: int, z: int) -> int: + """ +3-D grid coordinates to Hilbert index (inverse of +`hilbert_3d_d2xyz`). + +Panics: +Panics unless `1 <= order <= 21` and all coordinates are below +`2^order`. + +Rust: `patterns::space_filling::hilbert_3d_xyz2d` + """ + ... + +def hilbert_curve_3d(order: int) -> list[Vec3]: + """ +The 3-D Hilbert curve as points in the unit cube, in curve order. + +Panics: +Panics unless `1 <= order <= 6` (2^18 points at most). + +Rust: `patterns::space_filling::hilbert_curve_3d` + """ + ... + +def peano_curve(order: int) -> list[Vec2]: + """ +Peano curve on a `3^order` grid via the ternary digit formula +(Peano 1890): points in the unit square in curve order. + +Panics: +Panics unless `1 <= order <= 6`. + +Rust: `patterns::space_filling::peano_curve` + """ + ... + +def morton_encode_2d(x: int, y: int) -> int: + """ +Interleaves the bits of x (even positions) and y (odd positions). + +Rust: `patterns::space_filling::morton_encode_2d` + """ + ... + +def morton_decode_2d(m: int) -> tuple[int, int]: + """ +Inverse of `morton_encode_2d`. + +Rust: `patterns::space_filling::morton_decode_2d` + """ + ... + +def morton_encode_3d(x: int, y: int, z: int) -> int: + """ +Interleaves 21 bits each of x, y, z. + +Panics: +Panics when any coordinate exceeds 21 bits. + +Rust: `patterns::space_filling::morton_encode_3d` + """ + ... + +def morton_decode_3d(m: int) -> tuple[int, int, int]: + """ +Inverse of `morton_encode_3d`. + +Rust: `patterns::space_filling::morton_decode_3d` + """ + ... + +def z_order_curve(order: int) -> list[Vec2]: + """ +The Z-order (Morton) traversal of a `2^order` grid as unit-square +points. + +Panics: +Panics unless `1 <= order <= 10`. + +Rust: `patterns::space_filling::z_order_curve` + """ + ... + +def gray_code(n: int) -> int: + """ +Binary reflected Gray code. + +Rust: `patterns::space_filling::gray_code` + """ + ... + +def gray_decode(g: int) -> int: + """ +Inverse Gray code (prefix xor by doubling). + +Rust: `patterns::space_filling::gray_decode` + """ + ... + +def sierpinski_curve(order: int) -> list[Vec2]: + """ +Sierpiński arrowhead curve (traverses the Sierpiński triangle), +unit steps from the origin. + +Panics: +Panics unless `1 <= order <= 10`. + +Rust: `patterns::space_filling::sierpinski_curve` + """ + ... + +def moore_curve(order: int) -> list[Vec2]: + """ +Moore curve: the closed variant of the Hilbert curve (last point +adjacent to the first), unit grid steps. + +Panics: +Panics unless `1 <= order <= 8`. + +Rust: `patterns::space_filling::moore_curve` + """ + ... + +def gosper_curve(order: int) -> list[Vec2]: + """ +Gosper (flowsnake) curve, unit steps. + +Panics: +Panics unless `1 <= order <= 6`. + +Rust: `patterns::space_filling::gosper_curve` + """ + ... + +def sort_by_hilbert(points: MutableSequence[Vec2 | Sequence[float]], order: int) -> None: + """ +Sorts points by their Hilbert index on a `2^order` grid over the +bounding box. + +Panics: +Panics unless `1 <= order <= 31`. + +Rust: `patterns::space_filling::sort_by_hilbert` + """ + ... + +def sort_by_morton(points: MutableSequence[Vec3 | Sequence[float]]) -> None: + """ +Sorts 3-D points by Morton code (21 bits per axis over the +bounding box). + +Rust: `patterns::space_filling::sort_by_morton` + """ + ... + +def hilbert_locality_ratio(points: list[Vec2 | Sequence[float]], order: int) -> float: + """ +Locality measure of the Hilbert order: mean |index difference| +(normalized by the index range) divided by mean spatial distance +(normalized by the bounding-box diagonal) over all point pairs. +Lower means indices track spatial proximity better. + +Panics: +Panics unless `1 <= order <= 31` and at least 2 points are given. + +Rust: `patterns::space_filling::hilbert_locality_ratio` + """ + ... diff --git a/bindings/python/python/numeria/patterns/symmetry.pyi b/bindings/python/python/numeria/patterns/symmetry.pyi new file mode 100644 index 0000000..23c7325 --- /dev/null +++ b/bindings/python/python/numeria/patterns/symmetry.pyi @@ -0,0 +1,207 @@ +""" +Plane symmetry groups (the 17 wallpaper groups and 7 frieze groups), lattices, 3-D point groups, symmetry detection, and Hankin-style Islamic star patterns. Wallpaper operations are expressed in unit-cell (lattice) coordinates: an element maps the unit cell to itself modulo unit translations, so the returned sets are the coset representatives of the point group (plus the centering translation for the centered groups cm and cmm). +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.spatial.transform2d import Affine2 +from numeria.spatial.primitives import Polygon2 +from numeria.quaternion import Quaternion +from numeria.spatial.primitives import Rect +from numeria.patterns.tilings import Tiling +from numeria.math import Vec2 +from numeria.math import Vec3 + +class FriezeGroup: + """ +The 7 frieze groups (IUCr-style names). + +Rust: `patterns::symmetry::FriezeGroup` + """ + ... + +class Lattice: + """ +A 2-D lattice spanned by two basis vectors. + +Rust: `patterns::symmetry::Lattice` + """ + def __init__(self, a: Vec2 | Sequence[float], b: Vec2 | Sequence[float]) -> None: ... + @staticmethod + def square(s: float) -> Lattice: ... + @staticmethod + def hexagonal(s: float) -> Lattice: ... + @staticmethod + def rectangular(w: float, h: float) -> Lattice: ... + @staticmethod + def rhombic(s: float, angle: float) -> Lattice: ... + @staticmethod + def oblique(a: Vec2 | Sequence[float], b: Vec2 | Sequence[float]) -> Lattice: ... + def reduce(self) -> Lattice: ... + def to_world(self, p: Vec2 | Sequence[float]) -> Vec2: ... + @property + def a(self) -> Vec2: ... + @property + def b(self) -> Vec2: ... + +class PointGroup3: + """ +3-D point groups (rotation parts). + +Rust: `patterns::symmetry::PointGroup3` + """ + ... + +class WallpaperGroup: + """ +The 17 wallpaper groups. + +Rust: `patterns::symmetry::WallpaperGroup` + """ + ... + +def wallpaper_generators(g: WallpaperGroup) -> list[Affine2]: + """ +The coset representatives of the wallpaper group's operations in +unit-cell coordinates (closed under composition modulo unit +translations; the centered groups include their centering +translation). + +Rust: `patterns::symmetry::wallpaper_generators` + """ + ... + +def wallpaper_group_order(g: WallpaperGroup) -> int: + """ +The order of the returned operation set. + +Rust: `patterns::symmetry::wallpaper_group_order` + """ + ... + +def wallpaper_lattice(g: WallpaperGroup, scale: float) -> Lattice: + """ +A natural lattice for the group at the given scale: square for the +tetragonal groups, hexagonal (120°) for the tri/hexagonal groups, +rectangular otherwise. + +Rust: `patterns::symmetry::wallpaper_lattice` + """ + ... + +def wallpaper_fundamental_domain(g: WallpaperGroup) -> Polygon2: + """ +A fundamental domain in unit-cell coordinates with area 1/order of +the cell. For the rectangular-cell groups it is a genuine +fundamental domain; for the centered and hexagonal groups it is an +area-correct representative slab (one valid choice among many +shapes). + +Rust: `patterns::symmetry::wallpaper_fundamental_domain` + """ + ... + +def tile_motif(g: WallpaperGroup, motif: list[Polygon2], lattice: Lattice, extent: Rect) -> list[Polygon2]: + """ +Tiles a motif (given in unit-cell coordinates) by the group and +lattice over the extent: every group operation applied to every +motif polygon, replicated over the lattice translations whose cell +origin falls in the extent. + +Rust: `patterns::symmetry::tile_motif` + """ + ... + +def tile_points(g: WallpaperGroup, points: list[Vec2 | Sequence[float]], lattice: Lattice, extent: Rect) -> list[Vec2]: + """ +Tiles a point set (unit-cell coordinates) by the group and lattice +over the extent. + +Rust: `patterns::symmetry::tile_points` + """ + ... + +def frieze_generators(g: FriezeGroup, period: float) -> list[Affine2]: + """ +Frieze group operations modulo the period translation, in world +coordinates with the frieze axis along x and period `period`. + +Rust: `patterns::symmetry::frieze_generators` + """ + ... + +def frieze_motif(g: FriezeGroup, motif: list[Polygon2], period: float, count: int) -> list[Polygon2]: + """ +Replicates a motif under the frieze group for `count` periods +(translations 0..count). + +Rust: `patterns::symmetry::frieze_motif` + """ + ... + +def rosette(motif: list[Polygon2], n: int, mirror: bool) -> list[Polygon2]: + """ +Rosette symmetry: the motif under the cyclic group C_n (rotations) +or dihedral D_n (`mirror` adds reflections), about the origin. + +Panics: +Panics unless `n >= 1`. + +Rust: `patterns::symmetry::rosette` + """ + ... + +def detect_symmetries_2d(points: list[Vec2 | Sequence[float]], tol: float) -> list[Affine2]: + """ +Detects rotations and reflections about the centroid that map the +point set to itself within `tol`. Checks rotation orders up to the +point count and reflection axes through point/midpoint directions. + +Rust: `patterns::symmetry::detect_symmetries_2d` + """ + ... + +def point_group_rotations(g: PointGroup3) -> list[Quaternion]: + """ +All proper rotations of the point group as quaternions (generated +by closure from the group's standard generators). + +Rust: `patterns::symmetry::point_group_rotations` + """ + ... + +def point_group_order(g: PointGroup3) -> int: + """ +The full group order (including improper operations for the +mirror-bearing groups). + +Rust: `patterns::symmetry::point_group_order` + """ + ... + +def point_group_orbit(g: PointGroup3, p: Vec3 | Sequence[float]) -> list[Vec3]: + """ +Orbit of a point under the group's rotations (deduplicated within +1e-9 of the point scale). + +Rust: `patterns::symmetry::point_group_orbit` + """ + ... + +def hankin_star_pattern(tiling: Tiling, contact_angle: float, delta: float) -> list[Segment2]: + """ +Hankin's method for Islamic star patterns (after Kaplan): from two +points straddling each edge midpoint (offset `delta` along the +edge), rays leave into the polygon at `contact_angle` from the +edge; consecutive rays around the polygon are intersected to form +the strap segments. + +Panics: +Panics unless `0 < contact_angle < π/2` and `delta >= 0`. + +Rust: `patterns::symmetry::hankin_star_pattern` + """ + ... diff --git a/bindings/python/python/numeria/patterns/tilings.pyi b/bindings/python/python/numeria/patterns/tilings.pyi new file mode 100644 index 0000000..3db6d2d --- /dev/null +++ b/bindings/python/python/numeria/patterns/tilings.pyi @@ -0,0 +1,161 @@ +""" +Plane tilings: regular and Archimedean (uniform) tilings, their Laves duals, hex-grid coordinate algebra, and a few classic non-edge-to-edge patterns (brick, herringbone). +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.spatial.primitives import Polygon2 +from numeria.spatial.primitives import Rect +from numeria.math import Vec2 + +class Archimedean: + """ +The 11 Archimedean (uniform) tilings by vertex configuration. + +Rust: `patterns::tilings::Archimedean` + """ + ... + +class Hex: + """ +Axial hex-grid coordinate (Red Blob Games convention); the third +cube coordinate is `s = -q - r`. + +Rust: `patterns::tilings::Hex` + """ + def __init__(self, q: int, r: int) -> None: ... + def s(self) -> int: ... + def add(self, other: Hex) -> Hex: ... + def sub(self, other: Hex) -> Hex: ... + def scale(self, k: int) -> Hex: ... + def neighbors(self) -> list[Hex]: ... + def distance(self, other: Hex) -> int: ... + def to_pixel(self, size: float, pointy: bool) -> Vec2: ... + @staticmethod + def from_pixel(p: Vec2 | Sequence[float], size: float, pointy: bool) -> Hex: ... + def ring(self, radius: int) -> list[Hex]: ... + def spiral(self, radius: int) -> list[Hex]: ... + def line_to(self, other: Hex) -> list[Hex]: ... + def rotate60(self) -> Hex: ... + def reflect_q(self) -> Hex: ... + @property + def q(self) -> int: ... + @property + def r(self) -> int: ... + +class Tiling: + """ +A tiling as an indexed face set: `faces` are counterclockwise +vertex loops, `edges` the unique undirected edges. + +Rust: `patterns::tilings::Tiling` + """ + def __init__(self, vertices: list[Vec2 | Sequence[float]], faces: list[list[int]], edges: list[tuple[int, int]]) -> None: ... + def clip_to_rect(self, rect: Rect) -> Tiling: ... + def polygons(self) -> list[Polygon2]: ... + def face_centroids(self) -> list[Vec2]: ... + def dual(self) -> Tiling: ... + @property + def vertices(self) -> list[Vec2]: ... + @property + def faces(self) -> list[list[int]]: ... + @property + def edges(self) -> list[tuple[int, int]]: ... + +def square_grid(nx: int, ny: int, size: float) -> Tiling: + """ +Square grid of `nx` x `ny` cells with the given cell size. + +Rust: `patterns::tilings::square_grid` + """ + ... + +def triangular_grid(nx: int, ny: int, size: float) -> Tiling: + """ +Triangular grid: `nx` x `ny` rhombi split into unit triangles of +the given edge length. + +Rust: `patterns::tilings::triangular_grid` + """ + ... + +def hexagonal_grid(nx: int, ny: int, size: float, pointy_top: bool) -> Tiling: + """ +Hexagonal grid: `nx` x `ny` hexagons of circumradius `size`. +`pointy_top` orients a vertex upward; otherwise an edge is up. + +Rust: `patterns::tilings::hexagonal_grid` + """ + ... + +def archimedean(kind: Archimedean, extent: Rect, size: float) -> Tiling: + """ +Archimedean (uniform) tiling of the given kind with edge length +`size`, covering `extent` (faces with centroid inside). + +Rust: `patterns::tilings::archimedean` + """ + ... + +def laves(kind: Archimedean, extent: Rect, size: float) -> Tiling: + """ +Laves tiling: the dual of the corresponding Archimedean tiling. + +Rust: `patterns::tilings::laves` + """ + ... + +def hex_range(center: Hex, radius: int) -> list[Hex]: + """ +All hexes within `radius` of `center` (hex-distance ball). + +Panics: +Panics for negative radius. + +Rust: `patterns::tilings::hex_range` + """ + ... + +def cairo_pentagonal(extent: Rect, size: float) -> Tiling: + """ +Cairo pentagonal tiling: the dual of the snub square tiling. + +Rust: `patterns::tilings::cairo_pentagonal` + """ + ... + +def rhombille(extent: Rect, size: float) -> Tiling: + """ +Rhombille (tumbling blocks) tiling: the dual of the trihexagonal +tiling. + +Rust: `patterns::tilings::rhombille` + """ + ... + +def brick(extent: Rect, w: float, h: float, offset: float) -> Tiling: + """ +Running-bond brick pattern: rows of `w` x `h` bricks, each row +shifted by `offset` (in units of `w`). + +Panics: +Panics unless `w, h > 0`. + +Rust: `patterns::tilings::brick` + """ + ... + +def herringbone(extent: Rect, w: float, h: float) -> Tiling: + """ +Herringbone pattern of `w` x `h` bricks (alternating horizontal +and vertical along the diagonals). + +Panics: +Panics unless `0 < h < w`. + +Rust: `patterns::tilings::herringbone` + """ + ... diff --git a/bindings/python/python/numeria/photonics.pyi b/bindings/python/python/numeria/photonics.pyi new file mode 100644 index 0000000..a8035ff --- /dev/null +++ b/bindings/python/python/numeria/photonics.pyi @@ -0,0 +1,210 @@ +""" +Laser beams, optical fibre, and interferometry. Gaussian beam propagation: waist, Rayleigh range, radius and curvature against distance, divergence, the Gouy phase, and on-axis intensity. Fibre through the numerical aperture, acceptance angle, and the V-number that decides single- versus multi-mode operation, plus attenuation and dispersion broadening. Ray transfer (ABCD) matrices compose optical elements by matrix multiplication. Coherence length and time, fringe visibility, and the Fabry-Pérot transmission with its free spectral range close the module. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def beam_waist_from_divergence(wavelength: float, divergence_half_angle: float) -> float: + """ +Beam waist from far-field divergence: w₀ = λ/(π×θ) + +Rust: `photonics::beam_waist_from_divergence` + """ + ... + +def rayleigh_range(waist: float, wavelength: float) -> float: + """ +Rayleigh range: z_R = πw₀²/λ + +Rust: `photonics::rayleigh_range` + """ + ... + +def beam_radius(waist: float, z: float, rayleigh: float) -> float: + """ +Beam radius at axial position z: w(z) = w₀√(1+(z/z_R)²) + +Rust: `photonics::beam_radius` + """ + ... + +def beam_divergence(waist: float, wavelength: float) -> float: + """ +Far-field half-angle divergence: θ = λ/(πw₀) + +Rust: `photonics::beam_divergence` + """ + ... + +def beam_curvature(z: float, rayleigh: float) -> float: + """ +Radius of curvature of the wavefront: R(z) = z(1+(z_R/z)²) + +Rust: `photonics::beam_curvature` + """ + ... + +def gouy_phase(z: float, rayleigh: float) -> float: + """ +Gouy phase shift: ψ(z) = atan(z/z_R) + +Rust: `photonics::gouy_phase` + """ + ... + +def beam_intensity(power: float, waist: float, r: float, z: float, rayleigh: float) -> float: + """ +Peak-normalized Gaussian beam intensity at radial offset r and axial position z: +I = (2P/(πw²)) exp(-2r²/w²), where w = w(z). + +Rust: `photonics::beam_intensity` + """ + ... + +def beam_parameter(waist: float, z: float, rayleigh: float) -> tuple[float, float]: + """ +Combined beam parameter: returns (w(z), R(z)) at axial position z. + +Rust: `photonics::beam_parameter` + """ + ... + +def numerical_aperture(n_core: float, n_clad: float) -> float: + """ +Numerical aperture of a step-index fiber: NA = √(n_core² - n_clad²) + +Rust: `photonics::numerical_aperture` + """ + ... + +def acceptance_angle(na: float) -> float: + """ +Maximum acceptance half-angle: θ_max = arcsin(NA) + +Rust: `photonics::acceptance_angle` + """ + ... + +def v_number(radius: float, na: float, wavelength: float) -> float: + """ +Normalized frequency (V-number): V = 2πr×NA/λ + +Rust: `photonics::v_number` + """ + ... + +def is_single_mode(v_number: float) -> bool: + """ +True when the fiber supports only the fundamental mode (V < 2.405). + +Rust: `photonics::is_single_mode` + """ + ... + +def number_of_modes(v_number: float) -> float: + """ +Approximate mode count for a step-index multimode fiber: M ≈ V²/2 + +Rust: `photonics::number_of_modes` + """ + ... + +def fiber_attenuation(input_power: float, attenuation_db_per_km: float, length_km: float) -> float: + """ +Output power after propagation through a lossy fiber: +P_out = P_in × 10^(-αL/10), where α is in dB/km and L in km. + +Rust: `photonics::fiber_attenuation` + """ + ... + +def dispersion_broadening(dispersion: float, length: float, spectral_width: float) -> float: + """ +Chromatic dispersion pulse broadening: Δt = D × L × Δλ +D in ps/(nm·km), L in km, Δλ in nm → Δt in ps. + +Rust: `photonics::dispersion_broadening` + """ + ... + +def critical_angle_fiber(n_core: float, n_clad: float) -> float: + """ +Critical angle for total internal reflection inside the fiber core: +θc = arcsin(n_clad / n_core) + +Rust: `photonics::critical_angle_fiber` + """ + ... + +def thin_lens_matrix(focal_length: float) -> list[list[float]]: + """ +Thin lens matrix: [[1, 0], [-1/f, 1]] + +Rust: `photonics::thin_lens_matrix` + """ + ... + +def free_space_matrix(distance: float) -> list[list[float]]: + """ +Free-space propagation matrix: [[1, d], [0, 1]] + +Rust: `photonics::free_space_matrix` + """ + ... + +def image_distance_thick_lens(n: float, r1: float, r2: float, thickness: float, object_dist: float) -> float: + """ +Image distance for a thick lens via ABCD matrix composition. + +Constructs the system matrix from: refraction at R1, propagation through +the lens of thickness `t` and index `n`, refraction at R2, then solves +for the image distance using the thin-lens-equivalent focal length. + +Rust: `photonics::image_distance_thick_lens` + """ + ... + +def coherence_length(wavelength: float, bandwidth: float) -> float: + """ +Temporal coherence length: L_c = λ²/Δλ + +Rust: `photonics::coherence_length` + """ + ... + +def coherence_time(bandwidth_hz: float) -> float: + """ +Coherence time from frequency bandwidth: τ_c = 1/Δf + +Rust: `photonics::coherence_time` + """ + ... + +def fringe_visibility(i_max: float, i_min: float) -> float: + """ +Fringe visibility (contrast): V = (I_max - I_min) / (I_max + I_min) + +Rust: `photonics::fringe_visibility` + """ + ... + +def fabry_perot_transmission(reflectance: float, phase: float) -> float: + """ +Fabry-Perot etalon transmission (Airy function): +T = (1-R)² / ((1-R)² + 4R sin²(δ/2)) + +Rust: `photonics::fabry_perot_transmission` + """ + ... + +def free_spectral_range(cavity_length: float, n: float) -> float: + """ +Free spectral range of a Fabry-Perot cavity: FSR = c/(2nL) in Hz. + +Rust: `photonics::free_spectral_range` + """ + ... diff --git a/bindings/python/python/numeria/plasma.pyi b/bindings/python/python/numeria/plasma.pyi new file mode 100644 index 0000000..d49a301 --- /dev/null +++ b/bindings/python/python/numeria/plasma.pyi @@ -0,0 +1,137 @@ +""" +Plasma parameters: the characteristic lengths, frequencies and speeds. The Debye length is where it starts -- the distance over which a plasma screens a charge, and therefore the scale below which "plasma" stops being the right description. The Debye number counts particles in that sphere, and a plasma is only collective if that number is large. Frequencies: electron and ion plasma frequencies, and the cyclotron frequencies in a magnetic field, with the associated Larmor radius. Speeds: thermal, ion-acoustic, Alfvén and magnetosonic. Plus magnetic pressure, plasma beta, the skin depth, the Coulomb logarithm and the collision frequency. For a conducting fluid treated as a continuum see `magnetohydrodynamics`. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def debye_length(temperature: float, density: float, charge: float) -> float: + """ +λD = √(ε₀kT / (nq²)) + +Rust: `plasma::debye_length` + """ + ... + +def plasma_frequency_electron(density: float) -> float: + """ +ωp = √(ne² / (mₑε₀)) + +Rust: `plasma::plasma_frequency_electron` + """ + ... + +def plasma_frequency_ion(density: float, ion_mass: float) -> float: + """ +ωp = √(ne² / (m_ion × ε₀)) + +Rust: `plasma::plasma_frequency_ion` + """ + ... + +def cyclotron_frequency_electron(b_field: float) -> float: + """ +ωc = eB / mₑ + +Rust: `plasma::cyclotron_frequency_electron` + """ + ... + +def cyclotron_frequency_ion(b_field: float, ion_mass: float, charge: float) -> float: + """ +ωc = qB / m + +Rust: `plasma::cyclotron_frequency_ion` + """ + ... + +def larmor_radius(velocity_perp: float, mass: float, charge: float, b_field: float) -> float: + """ +rL = mv⊥ / (|q|B) + +Rust: `plasma::larmor_radius` + """ + ... + +def plasma_beta(pressure: float, b_field: float) -> float: + """ +β = 2μ₀p / B² + +Rust: `plasma::plasma_beta` + """ + ... + +def alfven_speed(b_field: float, density: float) -> float: + """ +vA = B / √(μ₀ρ) + +Rust: `plasma::alfven_speed` + """ + ... + +def sound_speed_plasma(gamma: float, temperature: float, ion_mass: float) -> float: + """ +cs = √(γkT / m) + +Rust: `plasma::sound_speed_plasma` + """ + ... + +def thermal_velocity(temperature: float, mass: float) -> float: + """ +vth = √(2kT / m) + +Rust: `plasma::thermal_velocity` + """ + ... + +def debye_number(density: float, debye_len: float) -> float: + """ +ND = n × (4π/3)λD³ + +Rust: `plasma::debye_number` + """ + ... + +def coulomb_logarithm(temperature: float, density: float) -> float: + """ +lnΛ = ln(12π × ND) + +Rust: `plasma::coulomb_logarithm` + """ + ... + +def magnetic_pressure(b_field: float) -> float: + """ +Pm = B² / (2μ₀) + +Rust: `plasma::magnetic_pressure` + """ + ... + +def magnetosonic_speed(alfven: float, sound: float) -> float: + """ +vms = √(vA² + cs²) + +Rust: `plasma::magnetosonic_speed` + """ + ... + +def skin_depth_plasma(plasma_freq: float) -> float: + """ +δ = c / ωp + +Rust: `plasma::skin_depth_plasma` + """ + ... + +def collision_frequency(density: float, temperature: float, coulomb_log: float, mass: float, charge: float) -> float: + """ +ν = nq⁴lnΛ / (4πε₀²m²vth³) + +Rust: `plasma::collision_frequency` + """ + ... diff --git a/bindings/python/python/numeria/propulsion.pyi b/bindings/python/python/numeria/propulsion.pyi new file mode 100644 index 0000000..5b9cd83 --- /dev/null +++ b/bindings/python/python/numeria/propulsion.pyi @@ -0,0 +1,132 @@ +""" +Rocket propulsion and impulsive orbital transfers. The Tsiolkovsky equation `Δv = v_e ln(m₀/m_f)` and the specific impulse and mass ratio around it, thrust with and without the pressure-thrust term, staged Δv, and the gravity-turn loss that makes the ideal Δv an underestimate for a launch. Transfers: Hohmann Δv and time, the bi-elliptic alternative (which wins beyond a radius ratio of about 11.94), and plane changes. Nozzle design covers exit velocity, throat area and the area ratio for a given exit Mach number. For Lambert targeting, J2 effects and orbit propagation see `astrophysics`. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def tsiolkovsky_delta_v(exhaust_velocity: float, mass_initial: float, mass_final: float) -> float: + """ +Tsiolkovsky rocket equation: Δv = ve × ln(m0/mf) + +Rust: `propulsion::tsiolkovsky_delta_v` + """ + ... + +def mass_ratio(delta_v: float, exhaust_velocity: float) -> float: + """ +Mass ratio from the rocket equation inverted: m0/mf = exp(Δv/ve) + +Rust: `propulsion::mass_ratio` + """ + ... + +def specific_impulse(thrust: float, mass_flow_rate: float, g: float) -> float: + """ +Specific impulse: Isp = F / (ṁ × g) + +Rust: `propulsion::specific_impulse` + """ + ... + +def exhaust_velocity_from_isp(isp: float, g: float) -> float: + """ +Effective exhaust velocity from specific impulse: ve = Isp × g + +Rust: `propulsion::exhaust_velocity_from_isp` + """ + ... + +def thrust(mass_flow_rate: float, exhaust_velocity: float) -> float: + """ +Thrust from momentum: F = ṁ × ve + +Rust: `propulsion::thrust` + """ + ... + +def thrust_with_pressure(mass_flow_rate: float, exhaust_velocity: float, exit_pressure: float, ambient_pressure: float, exit_area: float) -> float: + """ +Thrust including pressure term: F = ṁve + (Pe - Pa)Ae + +Rust: `propulsion::thrust_with_pressure` + """ + ... + +def delta_v_staged(stages: list[tuple[float, float, float]]) -> float: + """ +Total Δv for a multi-stage rocket. Each element is (exhaust_velocity, mass_full, mass_empty). + +Rust: `propulsion::delta_v_staged` + """ + ... + +def hohmann_delta_v(mu: float, r1: float, r2: float) -> tuple[float, float]: + """ +Hohmann transfer Δv values. Returns (Δv1, Δv2) for departure and arrival burns. + +Rust: `propulsion::hohmann_delta_v` + """ + ... + +def hohmann_transfer_time(mu: float, r1: float, r2: float) -> float: + """ +Transfer time for a Hohmann orbit: t = π√((r1+r2)³ / (8μ)) + +Rust: `propulsion::hohmann_transfer_time` + """ + ... + +def gravity_turn_loss(g: float, burn_time: float) -> float: + """ +Gravity drag loss approximation: Δv_loss ≈ g × t + +Rust: `propulsion::gravity_turn_loss` + """ + ... + +def delta_v_plane_change(velocity: float, angle: float) -> float: + """ +Plane change Δv: Δv = 2v × sin(θ/2) + +Rust: `propulsion::delta_v_plane_change` + """ + ... + +def bi_elliptic_delta_v(mu: float, r1: float, r2: float, r_intermediate: float) -> float: + """ +Bi-elliptic transfer total Δv via three burns through an intermediate radius. + +Rust: `propulsion::bi_elliptic_delta_v` + """ + ... + +def nozzle_exit_velocity(chamber_temp: float, molar_mass: float, gamma: float, pressure_ratio: float) -> float: + """ +Nozzle exit velocity from thermodynamic properties: +ve = √( 2γRT / (M(γ-1)) × (1 - (Pe/Pc)^((γ-1)/γ)) ) + +Rust: `propulsion::nozzle_exit_velocity` + """ + ... + +def throat_area(mass_flow: float, chamber_pressure: float, chamber_temp: float, gamma: float, molar_mass: float) -> float: + """ +Throat area required for a given mass flow: +A* = (ṁ / Pc) × √(R T / (γ M)) / (2/(γ+1))^((γ+1)/(2(γ-1))) + +Rust: `propulsion::throat_area` + """ + ... + +def area_ratio_from_mach(mach: float, gamma: float) -> float: + """ +Area ratio Ae/A* as a function of Mach number and heat capacity ratio: +Ae/A* = (1/M) × ((2/(γ+1)) × (1 + (γ-1)/2 × M²))^((γ+1)/(2(γ-1))) + +Rust: `propulsion::area_ratio_from_mach` + """ + ... diff --git a/bindings/python/python/numeria/py.typed b/bindings/python/python/numeria/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/bindings/python/python/numeria/quantum/__init__.pyi b/bindings/python/python/numeria/quantum/__init__.pyi new file mode 100644 index 0000000..084c869 --- /dev/null +++ b/bindings/python/python/numeria/quantum/__init__.pyi @@ -0,0 +1,234 @@ +""" +Quantum mechanics: the elementary relations here, with the wavefunction machinery and the Schrodinger solvers in submodules. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from . import algorithms, circuit, schrodinger, solid_state, spin, wavefunction +from numeria.transforms.wavelet import Threshold + +def de_broglie_wavelength(mass: float, velocity: float) -> float: + """ +de Broglie wavelength: λ = h / p = h / (m * v) + +Rust: `quantum::de_broglie_wavelength` + """ + ... + +def de_broglie_wavelength_from_energy(mass: float, kinetic_energy: float) -> float: + """ +de Broglie wavelength from kinetic energy: λ = h / sqrt(2mE) + +Rust: `quantum::de_broglie_wavelength_from_energy` + """ + ... + +def photon_momentum(wavelength: float) -> float: + """ +Photon momentum: p = h / λ = h*f / c + +Rust: `quantum::photon_momentum` + """ + ... + +def photon_energy(frequency: float) -> float: + """ +Photon energy: E = h * f + +Rust: `quantum::photon_energy` + """ + ... + +def photon_energy_from_wavelength(wavelength: float) -> float: + """ +Photon energy from wavelength: E = h * c / λ + +Rust: `quantum::photon_energy_from_wavelength` + """ + ... + +def photoelectric_ke(frequency: float, work_function: float) -> float: + """ +Photoelectric effect: KE_max = h*f - φ (work function) +Returns max kinetic energy of emitted electron. + +Rust: `quantum::photoelectric_ke` + """ + ... + +def threshold_frequency(work_function: float) -> float: + """ +Threshold frequency: f_0 = φ / h + +Rust: `quantum::threshold_frequency` + """ + ... + +def threshold_wavelength(work_function: float) -> float: + """ +Threshold wavelength: λ_0 = h * c / φ + +Rust: `quantum::threshold_wavelength` + """ + ... + +def stopping_potential(max_kinetic_energy: float) -> float: + """ +Stopping potential: V_s = KE_max / e + +Rust: `quantum::stopping_potential` + """ + ... + +def min_momentum_uncertainty(position_uncertainty: float) -> float: + """ +Heisenberg uncertainty principle (position-momentum): Δx * Δp ≥ ℏ/2 +Returns minimum uncertainty in momentum given position uncertainty. + +Rust: `quantum::min_momentum_uncertainty` + """ + ... + +def min_position_uncertainty(momentum_uncertainty: float) -> float: + """ +Minimum position uncertainty given momentum uncertainty. + +Rust: `quantum::min_position_uncertainty` + """ + ... + +def min_energy_uncertainty(time_uncertainty: float) -> float: + """ +Energy-time uncertainty: ΔE * Δt ≥ ℏ/2 + +Rust: `quantum::min_energy_uncertainty` + """ + ... + +def min_time_uncertainty(energy_uncertainty: float) -> float: + """ +Minimum time uncertainty given energy uncertainty: Δt ≥ ℏ / (2ΔE) + +Rust: `quantum::min_time_uncertainty` + """ + ... + +def bohr_radius() -> float: + """ +Bohr radius: a_0 = ℏ^2 / (m_e * k_e * e^2) + +Rust: `quantum::bohr_radius` + """ + ... + +def hydrogen_energy_level(n: int) -> float: + """ +Energy levels of hydrogen atom: E_n = -13.6 eV / n^2 +Returns energy in Joules. + +Rust: `quantum::hydrogen_energy_level` + """ + ... + +def hydrogen_transition_energy(n_initial: int, n_final: int) -> float: + """ +Energy of photon emitted in hydrogen transition: E = 13.6 eV * (1/n_f^2 - 1/n_i^2) +Returns energy in Joules (positive for emission when n_i > n_f). + +Rust: `quantum::hydrogen_transition_energy` + """ + ... + +def hydrogen_transition_wavelength(n_initial: int, n_final: int) -> float: + """ +Wavelength of photon from hydrogen transition (Rydberg formula): +1/λ = R_H * (1/n_f^2 - 1/n_i^2) + +Rust: `quantum::hydrogen_transition_wavelength` + """ + ... + +def hydrogen_orbital_radius(n: int) -> float: + """ +Orbital radius of nth level in hydrogen: r_n = n^2 * a_0 + +Rust: `quantum::hydrogen_orbital_radius` + """ + ... + +def hydrogen_orbital_velocity(n: int) -> float: + """ +Orbital velocity in nth Bohr orbit: v_n = e^2 / (4πε_0 * n * ℏ) + +Rust: `quantum::hydrogen_orbital_velocity` + """ + ... + +def tunneling_transmission(mass: float, barrier_height: float, particle_energy: float, barrier_width: float) -> float: + """ +Transmission coefficient for a rectangular barrier (approximate, E < V): +T ≈ e^(-2κL) where κ = sqrt(2m(V-E)) / ℏ + +Rust: `quantum::tunneling_transmission` + """ + ... + +def particle_in_box_energy(n: int, mass: float, box_length: float) -> float: + """ +Energy levels of a particle in a 1D infinite potential well: +E_n = n^2 * π^2 * ℏ^2 / (2 * m * L^2) + +Rust: `quantum::particle_in_box_energy` + """ + ... + +def zero_point_energy(mass: float, box_length: float) -> float: + """ +Zero-point energy (ground state, n=1): + +Rust: `quantum::zero_point_energy` + """ + ... + +def compton_wavelength_shift(scattering_angle_rad: float) -> float: + """ +Compton wavelength shift: Δλ = (h / (m_e * c)) * (1 - cos(θ)) + +Rust: `quantum::compton_wavelength_shift` + """ + ... + +def compton_wavelength_electron() -> float: + """ +Compton wavelength of the electron: λ_C = h / (m_e * c) + +Rust: `quantum::compton_wavelength_electron` + """ + ... + +def wien_peak_wavelength(temperature: float) -> float: + """ +Wien's displacement law: λ_max = b / T where b ≈ 2.898e-3 m·K + +Rust: `quantum::wien_peak_wavelength` + """ + ... + +def blackbody_power(area: float, temperature: float) -> float: + """ +Stefan-Boltzmann law (total power): P = σ * A * T^4 + +Rust: `quantum::blackbody_power` + """ + ... + +def planck_spectral_radiance(wavelength: float, temperature: float) -> float: + """ +Planck's law (spectral radiance): B(λ,T) = (2hc^2/λ^5) / (e^(hc/(λkT)) - 1) + +Rust: `quantum::planck_spectral_radiance` + """ + ... diff --git a/bindings/python/python/numeria/quantum/algorithms.pyi b/bindings/python/python/numeria/quantum/algorithms.pyi new file mode 100644 index 0000000..4d3b495 --- /dev/null +++ b/bindings/python/python/numeria/quantum/algorithms.pyi @@ -0,0 +1,374 @@ +""" +Quantum algorithms on the state-vector simulator. What the speedups have in common is not "trying every answer at once". A superposition over `2^n` inputs is easy; the difficulty is that measurement returns one of them at random, so the exponential is useless by itself. Every algorithm here earns its advantage by arranging *interference* -- amplitudes for wrong answers cancelling while the right one adds -- and the structure being exploited differs each time: a global property of a function for Deutsch-Jozsa, a hidden period for Shor, and nothing at all for Grover, which is why Grover's speedup is only quadratic and provably cannot be more. Oracles are given as ordinary Rust closures and applied directly to the amplitudes. That is exactly what a black box means: the algorithm is charged for each query and never sees inside. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.quantum.circuit import Circuit +from numeria.quantum.circuit import Gate +from numeria.quantum.circuit import QState +from numeria.monte_carlo import Rng + +def qft_circuit(n: int) -> Circuit: + """ +The quantum Fourier transform on `n` qubits. + +`O(n^2)` gates against the `O(n 2^n)` of the classical fast transform on +the same many amplitudes -- an exponential saving that is nonetheless not +directly useful, because the output is a superposition whose amplitudes +cannot be read out. What it is good for is exposing a *period*, which is +how Shor's algorithm uses it and why the QFT never appears alone. + +The controlled rotations shrink as `pi / 2^k`, so the far ones are almost +the identity; dropping them is the standard approximate QFT and costs +remarkably little. + +Errors: +Returns an error for a bad qubit count. + +Rust: `quantum::algorithms::qft_circuit` + """ + ... + +def iqft(n: int) -> Circuit: + """ +The inverse quantum Fourier transform. + +Errors: +Returns an error for a bad qubit count. + +Rust: `quantum::algorithms::iqft` + """ + ... + +def qft_check_vs_fft(n: int) -> float: + """ +The largest discrepancy between the QFT circuit and the discrete Fourier +transform it is supposed to implement. + +Errors: +Returns an error for a bad qubit count or if the circuit cannot run. + +Rust: `quantum::algorithms::qft_check_vs_fft` + """ + ... + +def deutsch_jozsa(f: Callable[[int], bool], n: int) -> bool: + """ +Deutsch-Jozsa: decides whether a promised function is constant or +balanced in a single query. + +Returns true for constant. The classical worst case needs `2^(n-1) + 1` +queries, and the quantum algorithm needs exactly one -- the largest +separation there is, though it depends entirely on the promise. Without +it the problem is no easier quantumly. + +Errors: +Returns an error for a bad qubit count. + +Rust: `quantum::algorithms::deutsch_jozsa` + """ + ... + +def bernstein_vazirani(secret: int, n: int) -> int: + """ +Bernstein-Vazirani: recovers a hidden bit string from one query to +`f(x) = s . x mod 2`. + +Classically it takes `n` queries, one per bit. The quantum algorithm gets +the whole string at once because the Hadamard transform maps the phase +pattern `(-1)^(s . x)` onto the single basis state `|s>` -- interference +doing in one step what `n` separate questions do classically. + +Errors: +Returns an error for a bad qubit count. + +Rust: `quantum::algorithms::bernstein_vazirani` + """ + ... + +def simon_lite(f: Callable[[int], int], n: int, rng: Rng) -> int: + """ +Simon's problem: finds the hidden period of a two-to-one function +satisfying `f(x) = f(x ^ s)`. + +The quantum step returns a random string orthogonal to `s` under the +bitwise dot product; collecting `n - 1` independent ones and solving the +linear system classically gives `s`. This is the first problem with an +exponential separation for a decision task, and its structure -- a hidden +subgroup -- is exactly the structure Shor's algorithm exploits. + +Errors: +Returns an error for a bad qubit count or if the samples never become +independent. + +Rust: `quantum::algorithms::simon_lite` + """ + ... + +def grover_optimal_iterations(items: int, marked: int) -> int: + """ +The number of Grover iterations that maximises the success probability. + +`floor(pi / 4 sqrt(N / M))`. Overshooting *reduces* the success +probability -- the amplitude rotates past the target and back down -- so +more iterations are not better, which is the least intuitive feature of +the algorithm and the reason the marked count has to be known or +estimated. + +Errors: +Returns an error unless there is at least one item and at least one +marked, with no more marked than items. + +Rust: `quantum::algorithms::grover_optimal_iterations` + """ + ... + +def grover(marked: list[int], n: int, iterations: Optional[int], rng: Rng) -> tuple[int, float]: + """ +Grover's search, returning the measured index and the success probability +it was drawn from. + +The oracle phase-flips the marked states and the diffusion operator +reflects about the uniform superposition; the pair is a rotation by a +fixed angle in the two-dimensional plane spanned by the marked and +unmarked subspaces, which is why the analysis is exactly trigonometry. + +Errors: +Returns an error for a bad qubit count or an empty marked set. + +Rust: `quantum::algorithms::grover` + """ + ... + +def quantum_counting(marked: list[int], n: int, precision: int) -> float: + """ +Estimates how many items an oracle marks, without finding them. + +Amplitude estimation: the Grover operator rotates by an angle whose sine +squared is the marked fraction, so estimating that angle by phase +estimation counts the solutions. It is the same primitive that gives the +quadratic speedup for Monte Carlo estimation generally. + +Errors: +Returns an error for a bad qubit count. + +Rust: `quantum::algorithms::quantum_counting` + """ + ... + +def phase_estimation(unitary: Gate, eigenstate: QState, ancilla: int) -> float: + """ +Phase estimation for a one-qubit unitary and one of its eigenstates. + +Returns the estimated phase in `[0, 1)`, where the eigenvalue is +`exp(2 pi i phase)`. With `ancilla` counting qubits the answer is exact +whenever the phase is a multiple of `2^-ancilla`, and otherwise correct to +that resolution with high probability. Every algorithm with an exponential +speedup runs through this routine. + +Errors: +Returns an error for a bad ancilla count or a non-eigenstate. + +Rust: `quantum::algorithms::phase_estimation` + """ + ... + +def shor_period_finding_sim(a: int, modulus: int, counting: int, rng: Rng) -> Optional[int]: + """ +The period of `a^x mod modulus`, by simulating the quantum subroutine. + +The modular exponentiation is a permutation of basis states, so it is +applied as one rather than compiled into gates -- the algorithm's +behaviour is identical and the simulation is `O(2^n)` instead of hopeless. +The counting register is transformed and measured, and the period is read +off by continued fractions, which is where the classical part of Shor's +algorithm begins. + +Errors: +Returns an error for a bad modulus, a base sharing a factor with it, or +too small a counting register. + +Rust: `quantum::algorithms::shor_period_finding_sim` + """ + ... + +def shor_classical_post(a: int, r: int, modulus: int) -> Optional[tuple[int, int]]: + """ +The classical half of Shor's algorithm: turns a period into factors. + +Works only when the period is even and `a^(r/2)` is not congruent to +`-1`; those conditions fail for a constant fraction of bases, which is +why the algorithm is randomised and retried rather than deterministic. + +Errors: +Returns an error for a bad modulus or period. + +Rust: `quantum::algorithms::shor_classical_post` + """ + ... + +def pauli_sum_expectation(terms: list[tuple[str, float]], state: QState) -> float: + """ +The expectation of a Pauli-sum Hamiltonian in a state. + +Errors: +Returns an error if a term has the wrong width or an unknown symbol. + +Rust: `quantum::algorithms::pauli_sum_expectation` + """ + ... + +def h2_model_hamiltonian(bond_length: float) -> list[tuple[str, float]]: + """ +A two-qubit model Hamiltonian for molecular hydrogen. + +This is *not* a table of ab initio coefficients. It is a two-qubit +operator constructed so that its ground eigenvalue follows the known H2 +potential curve -- a Morse form with a well depth of 0.1745 hartree at a +separation of 0.7414 angstrom, giving -1.1373 hartree at equilibrium and +dissociating to -1.0 -- while its excited states sit plausibly above. +The distinction matters: a real STO-3G calculation produces the +coefficients from integrals over basis functions, and inventing numbers +that merely look like published ones would be worse than useless. + +What it *is* good for is exercising a variational eigensolver against a +Hamiltonian whose exact ground energy is known in closed form, which is +what the tests below need. + +The construction: the `|00>` and `|11>` states form the bonding block, +coupled by the `XX` term, and their splitting is set to the desired gap; +the other two states are placed above both. + +Errors: +Returns an error for a non-positive bond length. + +Rust: `quantum::algorithms::h2_model_hamiltonian` + """ + ... + +def h2_ground_energy_model(bond_length: float) -> float: + """ +The model H2 ground-state energy in hartree, as a Morse curve. + +The parameters are the measured ones: a dissociation energy of 0.1744 +hartree (4.75 electronvolts), an equilibrium separation of 0.7414 +angstrom, and the Morse width 1.9426 per angstrom. They are mutually +consistent by construction -- the curve dissociates to exactly -1.0 +hartree, two hydrogen atoms at -0.5 each -- which a minimum taken from a +small-basis calculation and a well depth taken from experiment would not +be. + +Rust: `quantum::algorithms::h2_ground_energy_model` + """ + ... + +def pauli_sum_ground_energy(terms: list[tuple[str, float]], n: int) -> float: + """ +The exact lowest eigenvalue of a Pauli-sum Hamiltonian on a few qubits, +by building the matrix and diagonalising. + +The reference a variational result should be measured against. + +Errors: +Returns an error for a bad width or an eigensolver failure. + +Rust: `quantum::algorithms::pauli_sum_ground_energy` + """ + ... + +def qaoa_maxcut(vertices: int, edges: list[tuple[int, int]], layers: int) -> tuple[float, list[float], int]: + """ +QAOA for maximum cut on a small graph given by its edge list. + +Returns the best cut value found, the parameters, and the bit string. The +ansatz alternates a cost phase and a mixing rotation; at one layer it is +weak, and the interest is that the quality rises with the layer count -- +at infinitely many layers it becomes exact, since it approximates +adiabatic evolution. + +Errors: +Returns an error for a bad vertex count or an out-of-range edge. + +Rust: `quantum::algorithms::qaoa_maxcut` + """ + ... + +def trotter_evolution(terms: list[tuple[str, float]], t: float, steps: int, n: int) -> Circuit: + """ +A Trotterised circuit for `exp(-i H t)` with `H` a sum of Pauli terms. + +First order: each term is exponentiated in turn, which is exact only if +they commute. The error per step is the commutator, so it falls as +`t^2 / steps` -- and the whole point of Trotterisation is that a +Hamiltonian nobody can exponentiate is a sum of terms everybody can. + +Errors: +Returns an error for a bad width, zero steps, or an unknown symbol. + +Rust: `quantum::algorithms::trotter_evolution` + """ + ... + +def quantum_walk_line(steps: int, coin: Gate) -> list[float]: + """ +A discrete quantum walk on a line, returning the position distribution +after the given number of steps. + +The distribution spreads *linearly* in time rather than as its square +root, and it is bimodal with peaks at the edges rather than a bell curve +in the middle -- the opposite of a classical random walk in both respects, +and the reason quantum walks give speedups at all. + +Errors: +Returns an error for zero steps or a non-unitary coin. + +Rust: `quantum::algorithms::quantum_walk_line` + """ + ... + +def error_correction_3bit_flip_demo(p: float, trials: int, rng: Rng) -> tuple[float, float]: + """ +The three-qubit bit-flip code, returning the logical and physical error +rates measured over the given number of trials. + +The code corrects any single bit flip, so the logical error is the chance +of two or three flips: `3 p^2 (1 - p) + p^3`. That beats `p` only below +`p = 1/2`, which is the threshold in its simplest form -- above it the +encoding makes things worse, and no amount of redundancy helps. + +Errors: +Returns an error unless `p` is a probability and the trial count is +positive. + +Rust: `quantum::algorithms::error_correction_3bit_flip_demo` + """ + ... + +def three_bit_code_logical_error(p: float) -> float: + """ +The exact logical error rate of the three-qubit code. + +Rust: `quantum::algorithms::three_bit_code_logical_error` + """ + ... + +def randomized_benchmarking_sim(depths: list[int], noise: float, trials: int, rng: Rng) -> list[tuple[int, float]]: + """ +Randomised benchmarking: the surviving fidelity after a random Clifford +sequence and its inverse, at several depths. + +Returns `(depth, fidelity)` pairs. The decay is exponential in the depth +with a rate set by the average gate error, and -- this is the point of the +technique -- the rate is insensitive to errors in preparation and +measurement, which contaminate every direct fidelity estimate. + +Errors: +Returns an error for a bad noise level or an empty depth list. + +Rust: `quantum::algorithms::randomized_benchmarking_sim` + """ + ... diff --git a/bindings/python/python/numeria/quantum/circuit.pyi b/bindings/python/python/numeria/quantum/circuit.pyi new file mode 100644 index 0000000..1f0ac1d --- /dev/null +++ b/bindings/python/python/numeria/quantum/circuit.pyi @@ -0,0 +1,372 @@ +""" +A state-vector quantum circuit simulator, with density matrices and noise channels. The representation is the whole story. An `n`-qubit pure state is a vector of `2^n` complex amplitudes, so the memory doubles with each qubit: thirty qubits is sixteen gigabytes and there is no cleverness that avoids it for a general state. That exponential is not a limitation of this implementation but the reason quantum computers are interesting, and it is why everything here is capped at a couple of dozen qubits. Applying a one-qubit gate does *not* cost `2^n x 2^n` work. The gate acts on one tensor factor, so the amplitudes split into `2^(n-1)` independent pairs and each pair gets a two-by-two multiply: `O(2^n)` in total. Building the full unitary and multiplying would be `O(4^n)` and is offered only for small circuits, where seeing the matrix is the point. Qubit `q` is bit `q` of the amplitude index, so `|q_2 q_1 q_0>` has index `4 q_2 + 2 q_1 + q_0`. The opposite convention is equally common and the two disagree on every multi-qubit gate, so it is stated here rather than left to be inferred. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.monte_carlo import Rng + +class Circuit: + """ +A sequence of operations on a fixed number of qubits. + +Rust: `quantum::circuit::Circuit` + """ + def __init__(self, n: int) -> None: ... + def gate(self, q: int, gate: Gate) -> Circuit: ... + def x(self, q: int) -> Circuit: ... + def y(self, q: int) -> Circuit: ... + def z(self, q: int) -> Circuit: ... + def h(self, q: int) -> Circuit: ... + def rx(self, q: int, theta: float) -> Circuit: ... + def ry(self, q: int, theta: float) -> Circuit: ... + def rz(self, q: int, theta: float) -> Circuit: ... + def phase(self, q: int, phi: float) -> Circuit: ... + def cx(self, control: int, target: int) -> Circuit: ... + def cz(self, control: int, target: int) -> Circuit: ... + def cphase(self, control: int, target: int, phi: float) -> Circuit: ... + def ccx(self, a: int, b: int, target: int) -> Circuit: ... + def swap(self, a: int, b: int) -> Circuit: ... + def barrier(self) -> Circuit: ... + def append(self, other: Circuit) -> Circuit: ... + def inverse(self) -> Circuit: ... + def gate_count(self) -> int: ... + def depth(self) -> int: ... + def run(self, initial: QState) -> QState: ... + def run_shots(self, shots: int, rng: Rng) -> list[tuple[int, int]]: ... + def unitary_small(self) -> list[list[complex]]: ... + def to_qasm_lite(self) -> str: ... + def draw_ascii(self) -> str: ... + @property + def n(self) -> int: ... + @property + def ops(self) -> list[Op]: ... + +class DensityMatrix: + """ +A mixed state of `n` qubits. + +Rust: `quantum::circuit::DensityMatrix` + """ + def __init__(self, n: int, rho: list[list[complex]]) -> None: ... + @staticmethod + def from_state(state: QState) -> DensityMatrix: ... + @staticmethod + def from_mixture(states: list[QState], weights: list[float]) -> DensityMatrix: ... + def trace(self) -> complex: ... + def purity(self) -> float: ... + def von_neumann_entropy(self) -> float: ... + def is_valid(self, tol: float) -> bool: ... + def apply_gate(self, q: int, gate: Gate) -> None: ... + def apply_channel(self, kraus: list[list[list[complex]]]) -> None: ... + def partial_trace(self, keep: list[int]) -> DensityMatrix: ... + @property + def n(self) -> int: ... + @property + def rho(self) -> list[list[complex]]: ... + +class Gate: + """ +A one-qubit gate: a two-by-two unitary. + +Rust: `quantum::circuit::Gate` + """ + def is_unitary(self, tol: float) -> bool: ... + def dagger(self) -> Gate: ... + @staticmethod + def identity() -> Gate: ... + @staticmethod + def x() -> Gate: ... + @staticmethod + def y() -> Gate: ... + @staticmethod + def z() -> Gate: ... + @staticmethod + def h() -> Gate: ... + @staticmethod + def s() -> Gate: ... + @staticmethod + def sdg() -> Gate: ... + @staticmethod + def t() -> Gate: ... + @staticmethod + def tdg() -> Gate: ... + @staticmethod + def rx(theta: float) -> Gate: ... + @staticmethod + def ry(theta: float) -> Gate: ... + @staticmethod + def rz(theta: float) -> Gate: ... + @staticmethod + def phase(phi: float) -> Gate: ... + @staticmethod + def u3(theta: float, phi: float, lambda_: float) -> Gate: ... + @staticmethod + def sqrt_x() -> Gate: ... + @property + def matrix(self) -> list[list[complex]]: ... + +class Op: + """ +One instruction in a circuit. + +Rust: `quantum::circuit::Op` + """ + ... + +class QState: + """ +A pure state of `n` qubits, as `2^n` amplitudes. + +Rust: `quantum::circuit::QState` + """ + def __init__(self, n: int, amps: list[complex]) -> None: ... + @staticmethod + def zero(n: int) -> QState: ... + @staticmethod + def basis(n: int, index: int) -> QState: ... + @staticmethod + def from_amps(amps: list[complex]) -> QState: ... + @staticmethod + def plus_all(n: int) -> QState: ... + def len(self) -> int: ... + def is_empty(self) -> bool: ... + def norm(self) -> float: ... + def normalize(self) -> None: ... + def probability(self, index: int) -> float: ... + def probabilities(self) -> list[float]: ... + def measure_all(self, rng: Rng) -> int: ... + def measure_qubit(self, q: int, rng: Rng) -> tuple[bool, QState]: ... + def sample_counts(self, shots: int, rng: Rng) -> list[tuple[int, int]]: ... + def expectation_z(self, q: int) -> float: ... + def expectation_pauli_string(self, pauli: str) -> float: ... + def inner(self, other: QState) -> complex: ... + def fidelity(self, other: QState) -> float: ... + def apply_single(self, q: int, gate: Gate) -> None: ... + def apply_controlled(self, control: int, target: int, gate: Gate) -> None: ... + def apply_ccx(self, a: int, b: int, target: int) -> None: ... + def apply_swap(self, a: int, b: int) -> None: ... + def reduced_density_matrix(self, keep: list[int]) -> list[list[complex]]: ... + def schmidt_coefficients(self, partition: list[int]) -> list[float]: ... + def entanglement_entropy(self, partition: list[int]) -> float: ... + def bloch_vector(self, q: int) -> tuple[float, float, float]: ... + @property + def n(self) -> int: ... + @property + def amps(self) -> list[complex]: ... + +def depolarizing_channel(p: float) -> list[list[list[complex]]]: + """ +The depolarising channel: with probability `p`, replace the qubit by the +maximally mixed state. + +The one channel that treats every direction alike, so it shrinks the Bloch +vector uniformly toward the origin without rotating it. + +Errors: +Returns an error unless `p` is a probability. + +Rust: `quantum::circuit::depolarizing_channel` + """ + ... + +def amplitude_damping(gamma: float) -> list[list[list[complex]]]: + """ +Amplitude damping: a qubit decaying from `|1>` to `|0>` with probability +`gamma`. + +Models spontaneous emission, and unlike the symmetric channels it has a +fixed point that is not the maximally mixed state: everything ends up in +the ground state. That asymmetry is why `T_1` and `T_2` are different +numbers. + +Errors: +Returns an error unless `gamma` is a probability. + +Rust: `quantum::circuit::amplitude_damping` + """ + ... + +def phase_damping(gamma: float) -> list[list[list[complex]]]: + """ +Phase damping: coherence lost without any energy exchange. + +The off-diagonal terms shrink and the populations do not move at all, so +the Bloch vector flattens onto the `z` axis. It is the purely quantum kind +of noise -- there is no classical process it corresponds to. + +Errors: +Returns an error unless `gamma` is a probability. + +Rust: `quantum::circuit::phase_damping` + """ + ... + +def bit_flip(p: float) -> list[list[list[complex]]]: + """ +The bit-flip channel. + +Errors: +Returns an error unless `p` is a probability. + +Rust: `quantum::circuit::bit_flip` + """ + ... + +def phase_flip(p: float) -> list[list[list[complex]]]: + """ +The phase-flip channel. + +Errors: +Returns an error unless `p` is a probability. + +Rust: `quantum::circuit::phase_flip` + """ + ... + +def bell_state(which: int) -> QState: + """ +One of the four Bell states, indexed zero to three. + +Errors: +Returns an error for an index above three. + +Rust: `quantum::circuit::bell_state` + """ + ... + +def ghz(n: int) -> QState: + """ +The `n`-qubit GHZ state. + +Maximally entangled and maximally fragile: losing one qubit leaves the +rest in a classical mixture with no entanglement at all, which is what +distinguishes it from the W state. + +Errors: +Returns an error for fewer than two qubits or more than the cap. + +Rust: `quantum::circuit::ghz` + """ + ... + +def w_state(n: int) -> QState: + """ +The `n`-qubit W state: one excitation shared equally. + +The complement of GHZ. Losing a qubit leaves the others still entangled, +so the two are inequivalent under local operations -- there is no way to +turn one into the other without communication, even probabilistically. + +Errors: +Returns an error for fewer than two qubits or more than the cap. + +Rust: `quantum::circuit::w_state` + """ + ... + +def random_state(n: int, rng: Rng) -> QState: + """ +A Haar-random pure state. + +Built from independent complex Gaussians, which is the standard trick: +normalising a Gaussian vector gives the uniform measure on the sphere, so +this really is Haar random and not merely "random looking". + +Errors: +Returns an error for a bad qubit count. + +Rust: `quantum::circuit::random_state` + """ + ... + +def chsh_value(state: QState, angles: tuple[float, float, float, float]) -> float: + """ +The CHSH correlation for a two-qubit state at four measurement angles. + +`S = E(a, b) - E(a, b') + E(a', b) + E(a', b')`. Any local hidden variable +model obeys `|S| <= 2`; quantum mechanics reaches `2 sqrt 2` on a Bell +state, and no theory obeying no-signalling can exceed `4`. The gap between +two and `2 sqrt 2` is the whole experimental content of Bell's theorem. + +Errors: +Returns an error unless the state has two qubits. + +Rust: `quantum::circuit::chsh_value` + """ + ... + +def chsh_optimal_angles() -> tuple[float, float, float, float]: + """ +The angles that maximise CHSH on a Bell state, as +`(a, a', b, b')` in radians. + +Rust: `quantum::circuit::chsh_optimal_angles` + """ + ... + +def quantum_teleportation_demo(theta: float, phi: float, rng: Rng) -> tuple[tuple[float, float, float], tuple[float, float, float]]: + """ +Teleports a one-qubit state and returns the input and output Bloch +vectors. + +The protocol consumes one Bell pair and two classical bits, and it moves +the state exactly -- not a copy, since the sender's qubit is destroyed by +the measurement, which is what keeps no-cloning intact. Without the +classical bits the receiver holds the maximally mixed state, so nothing +travels faster than light either. + +Errors: +Returns an error if the simulation fails. + +Rust: `quantum::circuit::quantum_teleportation_demo` + """ + ... + +def superdense_coding_demo(bits: tuple[bool, bool]) -> tuple[bool, bool]: + """ +Superdense coding: two classical bits carried by one qubit, given a +shared Bell pair. + +Returns the decoded bits, which must equal the encoded ones. The +bookkeeping is exact -- one qubit plus prior entanglement carries two +bits, and without the entanglement it carries one, which is Holevo's +bound. + +Errors: +Returns an error if the simulation fails. + +Rust: `quantum::circuit::superdense_coding_demo` + """ + ... + +def no_cloning_fidelity_bound() -> float: + """ +The best fidelity an approximate universal cloner can achieve: `5 / 6`. + +Exact cloning is impossible because it is not linear, and the optimal +approximation is bounded by this number, which is a theorem rather than an +engineering limit. + +Rust: `quantum::circuit::no_cloning_fidelity_bound` + """ + ... + +def pauli_decompose(h: list[list[complex]]) -> list[tuple[str, float]]: + """ +Decomposes a Hermitian matrix on one or two qubits into Pauli terms. + +The Pauli strings form an orthogonal basis under the Hilbert-Schmidt inner +product, so each coefficient is just `tr(P H) / d` -- no linear solve +needed. That orthogonality is what makes measuring a Hamiltonian on +hardware possible at all. + +Errors: +Returns an error unless the matrix is square with side two or four. + +Rust: `quantum::circuit::pauli_decompose` + """ + ... diff --git a/bindings/python/python/numeria/quantum/schrodinger.pyi b/bindings/python/python/numeria/quantum/schrodinger.pyi new file mode 100644 index 0000000..60e32b1 --- /dev/null +++ b/bindings/python/python/numeria/quantum/schrodinger.pyi @@ -0,0 +1,504 @@ +""" +Solvers for the Schrodinger equation, stationary and time dependent. The stationary problem is an eigenvalue problem and the time-dependent one is an initial value problem, and the two want different numerics. For the first, discretising the Hamiltonian gives a symmetric matrix whose eigenvalues converge to the true spectrum from below at second order in the grid; for the second, what matters is not local accuracy but *unitarity*, because an integrator that loses norm loses probability and one that gains it manufactures particles from nothing. Both methods offered here are unitary by construction rather than by accident: the split-operator method applies exponentials of Hermitian operators, and Crank-Nicolson applies a Cayley transform, which is unitary for any step size at all. Everything takes `hbar` and the mass explicitly, so `hbar = m = 1` is available for the cases with exact answers. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.linalg.matrix import Matrix +from numeria.quantum.wavefunction import Wavefunction1D + +class Basis: + """ +Which basis to expand the Hamiltonian in. + +Rust: `quantum::schrodinger::Basis` + """ + ... + +def tise_solve_fd(v: list[float], dx: float, mass: float, hbar: float, n_states: int) -> tuple[list[float], list[list[float]]]: + """ +The lowest `n_states` bound states on a grid, by second-order finite +differences with hard walls at the ends. + +Returns the energies in ascending order and the matching normalised +eigenvectors. The discrete Laplacian is tridiagonal and symmetric, so the +eigenproblem is solved directly rather than iteratively. + +The walls matter: this solves the problem on `[x_0, x_{n-1}]` with the +wavefunction pinned to zero just outside, so a state that has not decayed +by the edge of the grid is being confined by the box rather than by the +potential, and its energy is wrong. The error is `O(dx^2)` and one-sided: +the discrete Laplacian underestimates curvature, so the computed energies +sit below the true ones. + +Errors: +Returns an error for an empty potential, a non-positive spacing, mass or +`hbar`, or if the eigensolver fails. + +Rust: `quantum::schrodinger::tise_solve_fd` + """ + ... + +def tise_solve_numerov(v: Callable[[float], float], x_range: tuple[float, float], n: int, e_range: tuple[float, float], mass: float, hbar: float, n_states: int) -> list[tuple[float, list[float]]]: + """ +Bound-state energies by Numerov shooting with node counting. + +Integrates from both ends toward a matching point and looks for the energy +at which the logarithmic derivatives agree. Node counting is what makes +the search reliable: the number of zeros of the solution is a monotone +function of the trial energy, so it says *which* state a bracket contains +and turns a search over a continuum into a bisection per state. + +Numerov itself is worth the extra terms: it integrates `y'' = f y` to +fourth order using only three points, because the equation's lack of a +first-derivative term lets the `O(h^4)` error be absorbed into the +coefficients. + +Returns `(energy, wavefunction)` for each of the lowest `n_states` levels +found inside `e_range`. + +Errors: +Returns an error for a degenerate grid or an inverted energy range. + +Rust: `quantum::schrodinger::tise_solve_numerov` + """ + ... + +def tise_solve_matrix_basis(v: list[float], dx: float, x0: float, basis: Basis, n_basis: int, mass: float, hbar: float) -> tuple[list[float], Matrix]: + """ +Bound states by expanding the Hamiltonian in a fixed basis and +diagonalising. + +Rayleigh-Ritz: the energies are upper bounds on the eigenvalues of the +same Hamiltonian, and they fall monotonically as the basis grows. The +bound is against the *discretised* operator -- the same tridiagonal +`tise_solve_fd` uses -- not against the continuum, since a truncated +basis cannot bound what the grid has already changed. + +The basis is orthonormalised on the grid before use, and that is not +tidiness. Sampling a basis at finitely many points and cutting it off at +the ends leaves it non-orthogonal, so `H c = E c` is the wrong problem; +the right one is `H c = E S c` with the overlap matrix `S`. Solving the +former with a non-orthonormal basis breaks the bound in the worst way -- +it returns energies *below* the true ones, which looks like a better +answer rather than a wrong one. + +Returns the energies in ascending order and the coefficient matrix in the +orthonormalised basis, whose column `i` holds the expansion of state `i`. + +Errors: +Returns an error for an empty basis, a degenerate grid, an eigensolver +failure, or a basis that collapses to nothing on this grid. + +Rust: `quantum::schrodinger::tise_solve_matrix_basis` + """ + ... + +def tdse_split_operator(psi: Wavefunction1D, v: list[float], dt: float, steps: int, mass: float, hbar: float) -> None: + """ +Advances a wavefunction by the split-operator method. + +Strang splitting: a half step of the potential, a full step of the kinetic +term in momentum space, and another half step of the potential. Each +factor is the exponential of a Hermitian operator and so is exactly +unitary, which is why the norm is conserved to rounding however large the +step is. What the step size controls is the *commutator* error between the +two -- second order for Strang against first for the naive ordering -- so +too large a step gives a wrong answer of exactly the right length. + +Errors: +Returns an error for a mismatched potential, a non-power-of-two grid, or a +non-positive mass. + +Rust: `quantum::schrodinger::tdse_split_operator` + """ + ... + +def tdse_crank_nicolson(psi: Wavefunction1D, v: list[float], dt: float, steps: int, mass: float, hbar: float) -> None: + """ +Advances a wavefunction by Crank-Nicolson. + +Applies `(1 + i H dt / 2 hbar)^{-1} (1 - i H dt / 2 hbar)`, the Cayley +transform of the Hamiltonian. For Hermitian `H` that is exactly unitary at +every step size -- not approximately, and not only in the small-step limit +-- which is the reason to prefer it to an explicit scheme here. An explicit +Euler step on the same equation has modulus strictly greater than one for +every non-zero step and blows up. + +Unlike the split-operator method this needs no FFT, so it works on any +grid length, and it imposes hard walls at the ends rather than periodicity. + +Errors: +Returns an error for a mismatched potential, a non-positive mass, or a +singular system. + +Rust: `quantum::schrodinger::tdse_crank_nicolson` + """ + ... + +def absorbing_boundary_cap(n: int, width: int, strength: float) -> list[float]: + """ +Adds an imaginary absorbing layer of the given width and strength to the +two ends of a complex potential. + +A wavepacket that reaches the edge of a periodic grid wraps around and +interferes with itself, which looks exactly like physics and is not. An +absorbing layer removes the outgoing amplitude instead. The profile has to +turn on smoothly -- a sudden absorber reflects, which is the problem it +was added to solve -- so the strength here rises quadratically. + +Returns the imaginary part to be subtracted from the Hamiltonian. + +Errors: +Returns an error if the two layers would overlap or the strength is +negative. + +Rust: `quantum::schrodinger::absorbing_boundary_cap` + """ + ... + +def apply_absorber(psi: Wavefunction1D, cap: list[float], dt: float, hbar: float) -> None: + """ +Applies one step of an absorbing layer to a wavefunction, damping the +amplitude by `exp(-cap dt / hbar)`. + +Errors: +Returns an error if the layer has the wrong length. + +Rust: `quantum::schrodinger::apply_absorber` + """ + ... + +def transmission_coefficient(v: list[float], dx: float, energy: float, mass: float, hbar: float) -> float: + """ +The transmission probability through an arbitrary piecewise-constant +barrier, by the transfer matrix method. + +Each slice contributes a two-by-two matrix relating the amplitudes on its +two sides, and the product of them all relates the incoming wave to the +outgoing one. The method is exact for a piecewise-constant potential, so +its only error is the piecewise-constant approximation itself -- which +means a smooth barrier converges as the slices are refined, and a genuinely +rectangular one is exact at any resolution. + +Below the barrier the wavenumber is imaginary and the same algebra +continues to work, which is where tunnelling comes from: the exponentially +decaying solution inside is not zero at the far side. + +Errors: +Returns an error for an empty barrier, a non-positive width, mass or +`hbar`, or a non-positive energy. + +Rust: `quantum::schrodinger::transmission_coefficient` + """ + ... + +def tunneling_rectangular_exact(v0: float, width: float, energy: float, mass: float, hbar: float) -> float: + """ +The exact transmission probability through a rectangular barrier. + +Three regimes in one formula. Below the barrier the transmission falls +exponentially with width, which is tunnelling; above it the transmission +oscillates and returns to one at the resonances where the barrier is a +whole number of half-wavelengths, which is the Ramsauer-Townsend effect +and has no classical counterpart at all -- classically, anything above the +barrier passes with certainty at every energy. + +Errors: +Returns an error for a non-positive width, mass, `hbar` or energy. + +Rust: `quantum::schrodinger::tunneling_rectangular_exact` + """ + ... + +def wkb_tunneling(v: Callable[[float], float], energy: float, turning_points: tuple[float, float], mass: float, hbar: float, samples: int) -> float: + """ +The WKB tunnelling probability through a barrier between two turning +points. + +`exp(-2 integral kappa dx)` over the classically forbidden region. It is +the leading exponential only: the prefactor is missing, so it is accurate +for a thick barrier and wrong by a factor of order one for a thin one. It +also diverges from the truth near the barrier top, where the turning +points merge and the approximation's own assumption -- that the wavelength +varies slowly -- fails exactly where it matters. + +Errors: +Returns an error for an inverted interval or non-positive constants. + +Rust: `quantum::schrodinger::wkb_tunneling` + """ + ... + +def wkb_quantization(v: Callable[[float], float], n: int, e_range: tuple[float, float], x_range: tuple[float, float], mass: float, hbar: float, samples: int) -> float: + """ +The Bohr-Sommerfeld energy of the `n`-th level: the energy at which the +action enclosed by the classical orbit is `(n + 1/2) 2 pi hbar`. + +The half is the Maslov correction, one quarter of a cycle for each of the +two turning points. Without it the harmonic oscillator comes out with no +zero-point energy; with it the WKB spectrum of the oscillator is *exact* +at every level, which is a coincidence of the quadratic potential and not +a general property. + +Errors: +Returns an error if no bracketing energy is found in `e_range`. + +Rust: `quantum::schrodinger::wkb_quantization` + """ + ... + +def reflection_step_potential(v0: float, energy: float) -> float: + """ +The reflection probability at a potential step of height `v0`. + +Non-zero even when the particle has more than enough energy to pass, which +has no classical analogue: a classical particle rolling over a downward +step always continues. Reflection here comes from the impedance mismatch +between the two wavenumbers, exactly as for light at a glass surface. + +Errors: +Returns an error for a non-positive energy. + +Rust: `quantum::schrodinger::reflection_step_potential` + """ + ... + +def double_well_splitting(v: list[float], dx: float, mass: float, hbar: float) -> float: + """ +The energy splitting of the lowest doublet in a symmetric double well. + +The two lowest states are the symmetric and antisymmetric combinations of +the states localised in each well, and their energies differ by an amount +exponentially small in the barrier. A particle prepared in one well +oscillates to the other with period `2 pi hbar / splitting`, so the +splitting *is* the tunnelling rate -- a static spectral quantity carrying +entirely dynamical information. + +Errors: +Returns an error if the finite-difference solve fails. + +Rust: `quantum::schrodinger::double_well_splitting` + """ + ... + +def perturbation_theory_1st(states: list[list[float]], perturbation: list[float], dx: float) -> list[float]: + """ +First-order energy shifts: the expectation of the perturbation in each +unperturbed state. + +The whole of first order is a diagonal matrix element, which is why the +first-order shift of a state with a symmetry the perturbation breaks is so +often zero -- the integrand is odd. The Stark effect in hydrogen's ground +state is the standard case: no linear shift, because the ground state has +no permanent dipole. + +Errors: +Returns an error if a state has the wrong length. + +Rust: `quantum::schrodinger::perturbation_theory_1st` + """ + ... + +def perturbation_theory_2nd(states: list[list[float]], energies: list[float], perturbation: list[float], dx: float) -> list[float]: + """ +Second-order energy shifts. + +A sum over the other states of `||^2 / (E_n - E_m)`. The sign is +forced for the ground state: every other state lies above it, so every +term is negative and the ground state is always pushed *down* by a +perturbation at second order, whatever the perturbation is. That is +level repulsion, and it is why avoided crossings avoid. + +Errors: +Returns an error on a length mismatch or degenerate levels, which +non-degenerate perturbation theory cannot treat. + +Rust: `quantum::schrodinger::perturbation_theory_2nd` + """ + ... + +def stark_shift_perturbative(field: float, n: int, parabolic_difference: int) -> float: + """ +The linear Stark shift of a hydrogen level in atomic units. + +Zero for `n = 1` and `3 n (n_1 - n_2) / 2` times the field for the excited +levels, whose degeneracy the field lifts. The ground state's vanishing +first-order shift is the general rule -- a non-degenerate state with +definite parity has no permanent dipole -- and hydrogen's excited levels +are the exception because their accidental degeneracy mixes opposite +parities. + +`parabolic_difference` is `n_1 - n_2` in the parabolic quantum numbers. + +Errors: +Returns an error for `n = 0` or an out-of-range parabolic difference. + +Rust: `quantum::schrodinger::stark_shift_perturbative` + """ + ... + +def variational_ground_state(v: list[float], dx: float, x0: float, trial: Callable[[float, list[float]], float], params0: list[float], mass: float, hbar: float) -> tuple[float, list[float]]: + """ +The variational ground state: minimises the expected energy of a trial +wavefunction over its parameters. + +The bound is one-sided and it is exact: `` over *any* normalisable +trial state is at least the true ground energy, because expanding the +trial state in eigenstates writes `` as a weighted average of +eigenvalues. So a variational calculation can never accidentally report +too low an energy, and the only way to be wrong is to be too high. + +Returns the minimised energy and the parameters that achieve it. + +Errors: +Returns an error for an empty grid or parameter vector. + +Rust: `quantum::schrodinger::variational_ground_state` + """ + ... + +def imaginary_time_propagation(v: list[float], dx: float, dtau: float, steps: int, mass: float, hbar: float) -> tuple[float, list[float]]: + """ +The ground state by propagation in imaginary time. + +Replacing `t` with `-i tau` turns the oscillating phases `exp(-i E t)` +into decaying exponentials `exp(-E tau)`, so every excited component dies +faster than the ground state and what survives, renormalised, is the +ground state. The convergence rate is set by the gap `E_1 - E_0`, which +makes the method slow precisely for the nearly degenerate systems where +the answer is most delicate. + +Returns the ground energy and the normalised state. + +Errors: +Returns an error for a mismatched grid or non-positive constants. + +Rust: `quantum::schrodinger::imaginary_time_propagation` + """ + ... + +def ehrenfest_check(snapshots: list[Wavefunction1D], v: list[float], dt: float, hbar: float, mass: float) -> float: + """ +The largest discrepancy in Ehrenfest's theorem along a trajectory. + +`d

/dt = -`: the expectations obey Newton's second law exactly, +with the force *averaged over the packet* rather than evaluated at its +centre. Those two differ as soon as the potential is not quadratic, which +is the precise sense in which a quantum particle is not a classical one -- +and the reason a wavepacket in a harmonic well follows the classical orbit +forever while one in any other well does not. + +Errors: +Returns an error for fewer than three snapshots or a mismatched potential. + +Rust: `quantum::schrodinger::ehrenfest_check` + """ + ... + +def wavepacket_scattering(v: list[float], dx: float, x0: float, barrier_centre: float, k0: float, sigma: float, start: float, dt: float, steps: int, mass: float, hbar: float) -> tuple[float, float]: + """ +Scatters a wavepacket off a potential and returns the transmitted and +reflected probabilities. + +The packet carries a spread of momenta, so what comes back is the +transmission averaged over that spread rather than the value at the mean +momentum. A narrow packet in position is broad in momentum, so the sharper +the incident pulse the more the measured coefficient is smeared -- the +uncertainty relation showing up as an experimental resolution limit. + +Errors: +Returns an error for a mismatched grid or non-positive constants. + +Rust: `quantum::schrodinger::wavepacket_scattering` + """ + ... + +def gross_pitaevskii_1d(psi: Wavefunction1D, v: list[float], g: float, dt: float, steps: int, mass: float, hbar: float) -> None: + """ +One-dimensional Gross-Pitaevskii evolution by split-step. + +The condensate's mean field adds a term `g |psi|^2` to the potential, so +the equation is nonlinear and superposition fails. With `g < 0` the +attraction can balance dispersion exactly and the result is a bright +soliton that propagates without spreading -- which a free packet never +does, and which is the clearest signature that the nonlinearity is really +there. + +Errors: +Returns an error for a mismatched grid or non-positive constants. + +Rust: `quantum::schrodinger::gross_pitaevskii_1d` + """ + ... + +def soliton_bright_exact(x: float, t: float, amplitude: float, width: float, velocity: float, mass: float, hbar: float) -> complex: + """ +The exact bright soliton of the one-dimensional Gross-Pitaevskii equation +with `g < 0`, moving at speed `velocity`. + +`psi = sqrt(n0) sech((x - v t) / xi) exp(i(...))`. Its shape is preserved +exactly for all time, which is what "soliton" means and what distinguishes +it from an ordinary travelling wave. + +Panics: +Panics unless the amplitude and healing length are positive. + +Rust: `quantum::schrodinger::soliton_bright_exact` + """ + ... + +def revival_time(length: float, mass: float, hbar: float) -> float: + """ +The revival time of a particle in a box: the period after which every +phase returns to its start. + +The energies are `n^2` times a constant, so all the relative phases are +commensurate and the state reassembles exactly -- which is special to this +spectrum. At rational fractions of the revival time the state is a finite +superposition of displaced copies of itself, and plotting the density +against space and time produces the interference lattice known as a +quantum carpet. + +Panics: +Panics unless the width, mass and `hbar` are positive. + +Rust: `quantum::schrodinger::revival_time` + """ + ... + +def quantum_carpet(length: float, coefficients: list[complex], times: list[float], points: int, mass: float, hbar: float) -> list[list[float]]: + """ +The probability density of a box state at a sequence of times, one row per +time. + +`coefficients` gives the amplitude of each eigenstate, indexed from the +ground state. + +Errors: +Returns an error for an empty expansion or grid. + +Rust: `quantum::schrodinger::quantum_carpet` + """ + ... + +def zeno_survival(t: float, tau: float, measurements: int) -> float: + """ +The survival probability of a state under repeated projective measurement. + +With `measurements` checks spread over a total time `t`, the survival +probability is `(1 - (t / measurements)^2 / tau^2)^measurements`, which +tends to one as the measurements are made more often. That is the quantum +Zeno effect, and it turns on the *quadratic* short-time behaviour of the +survival probability: an exponential decay law would give the same answer +however often it was interrupted. + +Errors: +Returns an error for a non-positive Zeno time or no measurements. + +Rust: `quantum::schrodinger::zeno_survival` + """ + ... diff --git a/bindings/python/python/numeria/quantum/solid_state.pyi b/bindings/python/python/numeria/quantum/solid_state.pyi new file mode 100644 index 0000000..fa81b90 --- /dev/null +++ b/bindings/python/python/numeria/quantum/solid_state.pyi @@ -0,0 +1,554 @@ +""" +Electrons and phonons in crystals: bands, densities of states, transport, and the standard model systems. Bloch's theorem is the organising fact. A potential with a lattice translation symmetry has eigenstates labelled by a crystal momentum, so the infinite problem reduces to one over a single Brillouin zone -- and the spectrum breaks into bands separated by gaps. That the gaps exist at all is the reason there are insulators; that they are absent at the Fermi level is the reason there are metals; and everything about semiconductors is the behaviour of a gap small enough for temperature to matter. Functions take `hbar` and the masses explicitly where a natural-unit calculation is the point, and use SI constants where a number in electronvolts or siemens is wanted. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.monte_carlo import Rng + +def tight_binding_1d(t_hop: float, on_site: list[float], periodic: bool) -> tuple[list[float], list[list[float]]]: + """ +A one-dimensional tight-binding chain, returning the energies ascending +and the matching eigenvectors as rows. + +`on_site` gives each site's energy and `t_hop` the nearest-neighbour +amplitude. The whole band structure of a simple metal is this model with +the on-site energies equal. + +Errors: +Returns an error for fewer than two sites, more than five hundred, or an +eigensolver failure. + +Rust: `quantum::solid_state::tight_binding_1d` + """ + ... + +def tight_binding_band_1d(k: float, t_hop: float, a: float) -> float: + """ +The tight-binding band of an infinite chain: `-2 t cos(k a)`. + +The bandwidth is `4 t` whatever the lattice constant, and the effective +mass at the band bottom is `hbar^2 / (2 t a^2)` -- so a narrow band means +a heavy electron, which is the whole of why transition metal oxides +behave as they do. + +Rust: `quantum::solid_state::tight_binding_band_1d` + """ + ... + +def ssh_model(cells: int, t1: float, t2: float) -> tuple[list[float], list[list[float]]]: + """ +The Su-Schrieffer-Heeger model: a dimerised chain with alternating +hoppings. + +Returns the energies ascending and the eigenvectors as rows. The chain has +`2 n` sites, `n` unit cells of two. + +Errors: +Returns an error for a bad cell count or an eigensolver failure. + +Rust: `quantum::solid_state::ssh_model` + """ + ... + +def ssh_winding_number(t1: float, t2: float) -> int: + """ +The SSH winding number: one in the topological phase, zero otherwise. + +The invariant is a property of the *bulk* -- it is computed from the +Hamiltonian's winding in momentum space with no reference to any edge -- +and yet it predicts the number of protected edge states. That is the +bulk-boundary correspondence, and it is why topological states survive +disorder that would destroy an ordinary bound state. + +Rust: `quantum::solid_state::ssh_winding_number` + """ + ... + +def ssh_edge_states(cells: int, t1: float, t2: float) -> int: + """ +The number of near-zero-energy edge states of a finite SSH chain. + +Errors: +Returns an error for a bad cell count. + +Rust: `quantum::solid_state::ssh_edge_states` + """ + ... + +def tight_binding_square(nx: int, ny: int, t_hop: float) -> list[float]: + """ +The spectrum of a tight-binding square lattice with open boundaries. + +The eigenvalues are separable: `-2t(cos(k_x a) + cos(k_y a))` with the +allowed momenta set by the box, so no diagonalisation is needed. That +separability is exactly why the square lattice is the standard sanity +check for a lattice code. + +Errors: +Returns an error for a bad lattice size. + +Rust: `quantum::solid_state::tight_binding_square` + """ + ... + +def graphene_dispersion(kx: float, ky: float, t_hop: float) -> tuple[float, float]: + """ +The two graphene bands at a point of the Brillouin zone, in units where +the lattice constant is one. + +The bands touch at the corners of the zone, and near them the dispersion +is *linear* rather than quadratic -- the electrons behave as massless +Dirac particles. Nothing about that requires relativity; it is a +consequence of the honeycomb's two-atom basis and its symmetry. + +Rust: `quantum::solid_state::graphene_dispersion` + """ + ... + +def dirac_points_graphene() -> list[tuple[float, float]]: + """ +The six Dirac points of graphene, in the same units. + +Rust: `quantum::solid_state::dirac_points_graphene` + """ + ... + +def kronig_penney(v0: float, a: float, b: float, energy: float, mass: float, hbar: float) -> float: + """ +The Kronig-Penney dispersion function: the right-hand side of +`cos(k L) = f(E)`. + +Bands are where `|f| <= 1`, since only there does a real crystal momentum +exist. Where `|f| > 1` the momentum is complex and the states decay -- +that is a gap, and it is the whole mechanism by which a periodic potential +forbids energies. + +The well has width `a` and depth zero, the barrier width `b` and height +`v0`. + +Errors: +Returns an error for non-positive widths, mass, or `hbar`. + +Rust: `quantum::solid_state::kronig_penney` + """ + ... + +def kronig_penney_bands(v0: float, a: float, b: float, energy_range: tuple[float, float], samples: int, mass: float, hbar: float) -> list[tuple[float, float]]: + """ +The allowed energy bands of a Kronig-Penney lattice, as intervals. + +Errors: +Returns an error for a bad range or sample count. + +Rust: `quantum::solid_state::kronig_penney_bands` + """ + ... + +def density_of_states_1d_free(energy: float, mass: float, hbar: float) -> float: + """ +The free-electron density of states per unit volume in one dimension. + +Spin degeneracy is included, as it is in the two- and three-dimensional +versions below: integrating any of them up to the Fermi energy gives the +electron density directly, with no further factor of two. + +Diverges as `1 / sqrt(E)` at the band bottom -- a van Hove singularity, +and the reason one-dimensional systems are so unstable to any interaction +at all. + +Errors: +Returns an error for a non-positive mass or `hbar`. + +Rust: `quantum::solid_state::density_of_states_1d_free` + """ + ... + +def density_of_states_2d_free(energy: float, mass: float, hbar: float) -> float: + """ +The free-electron density of states in two dimensions: a constant. + +Energy independent above the band bottom, which is what makes a +two-dimensional electron gas the clean setting for the quantum Hall +effect. + +Errors: +Returns an error for a non-positive mass or `hbar`. + +Rust: `quantum::solid_state::density_of_states_2d_free` + """ + ... + +def density_of_states_3d_free(energy: float, mass: float, hbar: float) -> float: + """ +The free-electron density of states in three dimensions, going as +`sqrt(E)`. + +Errors: +Returns an error for a non-positive mass or `hbar`. + +Rust: `quantum::solid_state::density_of_states_3d_free` + """ + ... + +def dos_from_bands(levels: list[float], sigma: float, points: int) -> list[tuple[float, float]]: + """ +A density of states from a list of levels, broadened by a Gaussian. + +Errors: +Returns an error for an empty list, a non-positive width, or too few +points. + +Rust: `quantum::solid_state::dos_from_bands` + """ + ... + +def fermi_dirac(energy: float, mu: float, temperature: float) -> float: + """ +The Fermi-Dirac occupation. + +Errors: +Returns an error for a negative temperature. + +Rust: `quantum::solid_state::fermi_dirac` + """ + ... + +def bose_einstein(energy: float, mu: float, temperature: float) -> float: + """ +The Bose-Einstein occupation. + +Diverges as the energy approaches the chemical potential, which is +condensation: the ground state's occupation is not bounded by one, and in +three dimensions it takes a macroscopic share below a finite temperature. + +Errors: +Returns an error for a negative temperature or an energy at or below the +chemical potential. + +Rust: `quantum::solid_state::bose_einstein` + """ + ... + +def fermi_energy_free(density: float, mass: float) -> float: + """ +The Fermi energy of a free electron gas at the given number density. + +Errors: +Returns an error for a non-positive density or mass. + +Rust: `quantum::solid_state::fermi_energy_free` + """ + ... + +def sommerfeld_heat_capacity(temperature: float, fermi_temperature: float) -> float: + """ +The Sommerfeld electronic heat capacity per electron. + +Linear in temperature, and smaller than the classical `3k/2` by a factor +of order `T / T_F` -- which resolves the nineteenth-century puzzle of why +metals' electrons contribute almost nothing to the heat capacity despite +carrying the current. Only those within `kT` of the Fermi surface can +absorb energy at all. + +Errors: +Returns an error for a non-positive Fermi temperature. + +Rust: `quantum::solid_state::sommerfeld_heat_capacity` + """ + ... + +def debye_heat_capacity(temperature: float, debye_temperature: float) -> float: + """ +The Debye heat capacity per atom. + +Goes as `T^3` at low temperature and to the classical `3k` at high -- +Dulong and Petit's law. The cube is the count of phonon modes thermally +accessible, and it is one of the earliest quantitative successes of +quantum theory applied to solids. + +Errors: +Returns an error for a non-positive Debye temperature. + +Rust: `quantum::solid_state::debye_heat_capacity` + """ + ... + +def einstein_heat_capacity(temperature: float, einstein_temperature: float) -> float: + """ +The Einstein heat capacity per atom, from a single vibrational frequency. + +Falls exponentially at low temperature rather than as `T^3`, which is +exactly where the model fails and Debye's succeeds: a single frequency +leaves no low-energy modes to excite, and a real solid has acoustic +phonons of arbitrarily low frequency. + +Errors: +Returns an error for a non-positive Einstein temperature. + +Rust: `quantum::solid_state::einstein_heat_capacity` + """ + ... + +def phonon_dispersion_1d_monatomic(k: float, spring: float, mass: float, a: float) -> float: + """ +The phonon dispersion of a monatomic chain. + +Linear at long wavelength -- sound -- and flattening at the zone boundary, +where the group velocity vanishes and the mode becomes a standing wave. + +Panics: +Panics unless the spring constant and mass are positive. + +Rust: `quantum::solid_state::phonon_dispersion_1d_monatomic` + """ + ... + +def phonon_dispersion_1d_diatomic(k: float, spring: float, m1: float, m2: float, a: float) -> tuple[float, float]: + """ +The two phonon branches of a diatomic chain, acoustic first. + +The gap between them at the zone boundary is the mass difference made +audible: a diatomic crystal has optical modes that a monatomic one does +not, and they are what infrared spectroscopy sees. + +Panics: +Panics unless the spring constant and both masses are positive. + +Rust: `quantum::solid_state::phonon_dispersion_1d_diatomic` + """ + ... + +def bloch_oscillation_period(field: float, a: float) -> float: + """ +The Bloch oscillation period of an electron in a static field. + +An electron in a perfect crystal under a constant force does not +accelerate away: it traverses the Brillouin zone and comes back, so it +*oscillates*. Ordinary conductors never show this because scattering +intervenes long before a period completes; superlattices, with their much +smaller zones, do. + +Errors: +Returns an error for a non-positive field or lattice constant. + +Rust: `quantum::solid_state::bloch_oscillation_period` + """ + ... + +def landau_levels(field: float, n: int, mass: float) -> float: + """ +The energy of the `n`-th Landau level. + +Equally spaced by `hbar omega_c`, with a zero-point half. The spacing +depends on the field and not on the level, which is what makes the +magneto-oscillations periodic in `1 / B` and lets a Fermi surface be +measured. + +Errors: +Returns an error for a non-positive field or mass. + +Rust: `quantum::solid_state::landau_levels` + """ + ... + +def hofstadter_butterfly(q_max: int, k_samples: int) -> list[tuple[float, float]]: + """ +The Hofstadter spectrum: the energies of a square lattice at each rational +flux `p / q`, as `(flux, energy)` pairs. + +The famous butterfly. At flux `p / q` the magnetic unit cell holds `q` +sites, so the band splits into `q` sub-bands -- and because that count +depends on the *denominator*, the spectrum is discontinuous in the flux at +every rational. It is the first place a fractal appeared in a physical +spectrum. + +Errors: +Returns an error for a bad denominator bound or momentum sample count. + +Rust: `quantum::solid_state::hofstadter_butterfly` + """ + ... + +def quantum_hall_conductance(filled: int) -> float: + """ +The Hall conductance of `n` filled Landau levels, in siemens. + +Quantised in units of `e^2 / h` to a part in a billion, in samples whose +disorder is uncontrolled and whose geometry is irregular. That the answer +depends on nothing but fundamental constants is why it defines the ohm. + +Rust: `quantum::solid_state::quantum_hall_conductance` + """ + ... + +def drude_conductivity(density: float, tau: float, mass: float) -> float: + """ +The Drude conductivity. + +Errors: +Returns an error for a non-positive relaxation time or mass. + +Rust: `quantum::solid_state::drude_conductivity` + """ + ... + +def hall_coefficient(density: float, charge: float) -> float: + """ +The Hall coefficient of a single-carrier conductor. + +Its *sign* is the useful part: positive for holes and negative for +electrons, so a Hall measurement says which carries the current -- a fact +no conductivity measurement can supply. + +Errors: +Returns an error for zero density. + +Rust: `quantum::solid_state::hall_coefficient` + """ + ... + +def effective_mass_from_band(band: Callable[[float], float], k0: float, h: float) -> float: + """ +The effective mass at a point of a band, from its curvature. + +`m* = hbar^2 / (d^2 E / dk^2)`, which can be negative near a band top -- +and a negative effective mass is precisely what a hole is. + +Errors: +Returns an error for a non-positive step or a flat band. + +Rust: `quantum::solid_state::effective_mass_from_band` + """ + ... + +def semiconductor_carrier_density(gap_ev: float, temperature: float, m_electron: float, m_hole: float) -> float: + """ +The intrinsic carrier density of a semiconductor, per cubic metre. + +The exponential in half the gap is what makes semiconductor conductivity +so temperature sensitive: silicon's carrier density roughly doubles every +eight kelvin at room temperature. + +Errors: +Returns an error for a non-positive temperature or mass. + +Rust: `quantum::solid_state::semiconductor_carrier_density` + """ + ... + +def pn_junction_builtin(acceptors: float, donors: float, intrinsic: float, temperature: float) -> float: + """ +The built-in potential of a p-n junction, in volts. + +Errors: +Returns an error for non-positive doping, intrinsic density, or +temperature. + +Rust: `quantum::solid_state::pn_junction_builtin` + """ + ... + +def depletion_width(built_in: float, acceptors: float, donors: float, relative_permittivity: float) -> float: + """ +The depletion width of an abrupt p-n junction, in metres. + +Errors: +Returns an error for non-positive doping or permittivity. + +Rust: `quantum::solid_state::depletion_width` + """ + ... + +def bcs_gap_equation(temperature: float, critical_temperature: float) -> float: + """ +The BCS energy gap at temperature `t`, relative to its value at zero. + +Solved from the gap equation, which is self-consistent: the gap appears on +both sides, so it has the trivial solution zero above the critical +temperature and a non-zero one below. That the transition is continuous +and the gap opens as `sqrt(1 - T / Tc)` is a prediction of the theory, not +an input to it. + +Errors: +Returns an error for a non-positive critical temperature. + +Rust: `quantum::solid_state::bcs_gap_equation` + """ + ... + +def bcs_tc_from_coupling(coupling: float, debye_temperature: float) -> float: + """ +The BCS critical temperature from the coupling and the Debye frequency. + +`1.14 theta_D exp(-1 / lambda)`. The exponential in the reciprocal +coupling has no expansion about zero coupling, which is why +superconductivity could not be found by perturbation theory and took forty +years to explain. + +Errors: +Returns an error for a non-positive coupling or Debye temperature. + +Rust: `quantum::solid_state::bcs_tc_from_coupling` + """ + ... + +def josephson_current(critical_current: float, phase: float) -> float: + """ +The DC Josephson current across a junction. + +A supercurrent flows with no voltage at all, set only by the phase +difference across the barrier. It is the most direct evidence that the +superconducting order parameter has a phase and that the phase is +physical. + +Rust: `quantum::solid_state::josephson_current` + """ + ... + +def josephson_frequency(voltage: float) -> float: + """ +The AC Josephson frequency at a given voltage: `2 e V / h`. + +About 484 terahertz per volt, and known to a part in `10^10` -- which is +why the Josephson effect defines the volt. + +Rust: `quantum::solid_state::josephson_frequency` + """ + ... + +def anderson_localization_1d(n: int, disorder: float, energy: float, trials: int, rng: Rng) -> float: + """ +The localisation length of a disordered one-dimensional chain, in lattice +sites. + +Every state in one dimension is localised for any disorder whatever, which +is the sharpest statement in the subject: there is no mobility edge and no +metallic phase, however weak the randomness. The length is extracted as +the reciprocal Lyapunov exponent of the transfer matrix product. + +Errors: +Returns an error for a bad chain length, disorder, or trial count. + +Rust: `quantum::solid_state::anderson_localization_1d` + """ + ... + +def conductance_landauer(transmissions: list[float]) -> float: + """ +The Landauer conductance of a set of transmission channels, in siemens. + +Conductance is transmission: a ballistic channel with perfect transmission +carries `2 e^2 / h` and no more, so even a perfect wire has a finite +resistance. That resistance is not dissipation in the wire -- it is the +cost of matching a few channels to the infinitely many in the leads. + +Errors: +Returns an error if a transmission is outside `[0, 1]`. + +Rust: `quantum::solid_state::conductance_landauer` + """ + ... diff --git a/bindings/python/python/numeria/quantum/spin.pyi b/bindings/python/python/numeria/quantum/spin.pyi new file mode 100644 index 0000000..6c82fd8 --- /dev/null +++ b/bindings/python/python/numeria/quantum/spin.pyi @@ -0,0 +1,299 @@ +""" +Spin operators, quantum magnets, and magnetic resonance. Two quite different things live here. The first is many-body: a chain of coupled spins has a Hilbert space of dimension `2^n`, so exact diagonalisation stops at a dozen or so sites and everything past that is a matter of finding the small part of the space that matters. Lanczos does that for the ground state, and the reason it works is that the extreme eigenvalues of a large sparse matrix converge in a Krylov space of dimension far smaller than the matrix. The second is single-spin dynamics -- Larmor precession, Rabi flopping, echoes -- which is a two-level problem with closed-form answers and is interesting for the opposite reason: the classical Bloch equations describe it exactly, so it is where quantum mechanics is least mysterious. Spin-1/2 operators are `sigma / 2` throughout, and `hbar = 1` unless a function takes it explicitly. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.linalg.matrix import Matrix +from numeria.monte_carlo import Rng + +class SpinChain: + """ +An XXZ spin-1/2 chain in a longitudinal field. + +`H = sum_i [ j (Sx Sx + Sy Sy) + jz Sz Sz ] - h sum_i Sz`, with the spin +operators equal to half the Pauli matrices. + +Setting `j == jz` gives the isotropic Heisenberg model; `j == 0` gives the +classical Ising chain; and `jz == 0` gives the XX model, which is free +fermions in disguise. + +Rust: `quantum::spin::SpinChain` + """ + def __init__(self, n: int, j: float, jz: float, h_field: float, periodic: bool) -> None: ... + def apply(self, v: list[complex]) -> list[complex]: ... + def hamiltonian_dense(self) -> Matrix: ... + def spectrum_small(self) -> list[float]: ... + def ground_state_lanczos(self, iterations: int, rng: Rng) -> tuple[float, list[complex]]: ... + def magnetization(self, state: list[complex]) -> float: ... + def correlation(self, state: list[complex], i: int, j: int) -> float: ... + def structure_factor(self, state: list[complex], k: float) -> float: ... + def entanglement_entropy_cut(self, state: list[complex], cut: int) -> float: ... + def time_evolve_krylov(self, state: list[complex], t: float, steps: int) -> list[complex]: ... + @property + def n(self) -> int: ... + @property + def j(self) -> float: ... + @property + def jz(self) -> float: ... + @property + def h_field(self) -> float: ... + @property + def periodic(self) -> bool: ... + +def pauli_matrices() -> list[list[list[complex]]]: + """ +The three Pauli matrices, in the order `X`, `Y`, `Z`. + +Rust: `quantum::spin::pauli_matrices` + """ + ... + +def spin_operators(s: float) -> tuple[list[list[complex]], list[list[complex]], list[list[complex]]]: + """ +The spin operators `(Sx, Sy, Sz)` for any spin `s`, as +`(2s + 1)`-dimensional matrices. + +Built from the ladder operators, whose matrix elements +`sqrt(s(s+1) - m(m+1))` are what make the representation finite: the +coefficient vanishes exactly at the top of the ladder, so raising the +highest state gives zero rather than escaping the space. That single fact +is why angular momentum is quantised. + +Errors: +Returns an error unless `2s` is a non-negative integer no larger than 20. + +Rust: `quantum::spin::spin_operators` + """ + ... + +def spin_coherent_state(s: float, theta: float, phi: float) -> list[complex]: + """ +A spin coherent state: the state pointing along `(theta, phi)`. + +The closest a spin gets to a classical arrow. Its uncertainty is the +minimum the algebra allows, and it becomes classical as `s` grows -- the +relative uncertainty falls as `1 / sqrt(s)`, which is why a macroscopic +magnet has a definite direction and a single electron does not. + +Errors: +Returns an error for an invalid spin. + +Rust: `quantum::spin::spin_coherent_state` + """ + ... + +def heisenberg_2site_exact(j: float) -> list[float]: + """ +The spectrum of two Heisenberg-coupled spin-1/2 particles: a singlet and a +triplet. + +`S1 . S2 = (S^2 - S1^2 - S2^2) / 2`, so the energy depends only on the +total spin: `-3/4` for the singlet and `+1/4` for the threefold triplet, +times the coupling. The whole of chemical bonding in a two-electron +molecule is this splitting. + +Rust: `quantum::spin::heisenberg_2site_exact` + """ + ... + +def ising_transverse_field_dense(n: int, g: float, periodic: bool) -> Matrix: + """ +The transverse-field Ising chain as a dense matrix. + +`H = -sum_i sigma^z_i sigma^z_{i+1} - g sum_i sigma^x_i`, in Pauli +matrices rather than spin operators, which is the convention the exact +solution below uses. + +Errors: +Returns an error outside two to ten sites. + +Rust: `quantum::spin::ising_transverse_field_dense` + """ + ... + +def ising_transverse_field_apply(n: int, g: float, periodic: bool, v: list[complex]) -> list[complex]: + """ +Applies the transverse-field Ising Hamiltonian to a state vector. + +Matrix free, so the cost is `O(n 2^n)` rather than the `O(4^n)` of forming +the matrix -- which at ten sites is the difference between a megabyte and +a gigabyte, and between a Jacobi diagonalisation that finishes and one +that does not. + +Errors: +Returns an error for a bad site count or vector length. + +Rust: `quantum::spin::ising_transverse_field_apply` + """ + ... + +def ising_transverse_field_exact(n: int, g: float) -> float: + """ +The exact ground energy of the periodic transverse-field Ising chain, from +the Jordan-Wigner solution. + +The chain maps to free fermions, so the ground energy is a sum of +single-particle energies: `-sum_k sqrt(1 + g^2 - 2 g cos k)` over the +antiperiodic momenta `(2m + 1) pi / n`. That the interacting spin model +is secretly free is what makes it the standard testbed for quantum phase +transitions -- the critical point at `g = 1` is exactly known. + +Errors: +Returns an error for fewer than two sites. + +Rust: `quantum::spin::ising_transverse_field_exact` + """ + ... + +def itf_critical_point() -> float: + """ +The critical transverse field of the Ising chain, where the gap closes. + +Rust: `quantum::spin::itf_critical_point` + """ + ... + +def magnon_dispersion(j: float, k: float, s: float, a: float) -> float: + """ +The magnon dispersion of a ferromagnetic Heisenberg chain. + +`2 j s (1 - cos(k a))`, which vanishes as `k^2` at long wavelength. The +quadratic -- rather than linear -- dispersion is the signature of a +ferromagnet's broken symmetry, and it is why a ferromagnet's low- +temperature heat capacity goes as `T^(3/2)` while an antiferromagnet's +goes as `T^3`. + +Rust: `quantum::spin::magnon_dispersion` + """ + ... + +def larmor_frequency(b: float, gamma: float) -> float: + """ +The Larmor precession angle after a time `t` in a field of magnitude `b`. + +The precession rate depends on the field and the gyromagnetic ratio and +not at all on the angle, which is why a spin precesses at a fixed +frequency however it is tipped. + +Rust: `quantum::spin::larmor_frequency` + """ + ... + +def larmor_precession(m0: tuple[float, float, float], b: float, gamma: float, t: float) -> tuple[float, float, float]: + """ +The magnetisation vector after Larmor precession about the `z` axis. + +The sense is the one the Bloch equation `dM/dt = gamma M x B` gives: for a +positive gyromagnetic ratio and a field along `+z`, the vector turns +*clockwise* seen from `+z`, so the angular velocity is `-gamma B`. Half +the sign conventions in the literature differ, and the two disagree on +everything that depends on the direction of a rotation. + +Rust: `quantum::spin::larmor_precession` + """ + ... + +def rabi_oscillation(rabi: float, detuning: float, t: float) -> float: + """ +The excited-state probability of a driven two-level system: Rabi's +formula. + +`(omega^2 / Omega^2) sin^2(Omega t / 2)` with the generalised frequency +`Omega = sqrt(omega^2 + delta^2)`. Off resonance the oscillation is faster +and shallower, and the peak probability falls as the detuning grows -- +which is why a driven transition is a filter as well as a rotation. + +Errors: +Returns an error if the drive and detuning are both zero. + +Rust: `quantum::spin::rabi_oscillation` + """ + ... + +def ramsey_fringes(detuning: float, free_time: float, t2_star: float) -> float: + """ +Ramsey fringes: the signal after two pulses separated by a free evolution. + +The fringe spacing measures the detuning, and the envelope's decay +measures `T2*` -- the *inhomogeneous* dephasing time, which includes +static field variations that a spin echo can undo. That distinction is the +point of the technique. + +Rust: `quantum::spin::ramsey_fringes` + """ + ... + +def spin_echo_sim(t: float, t2: float, t2_star: float) -> float: + """ +The spin echo amplitude at time `t` after a refocusing pulse at `t / 2`. + +The echo removes static dephasing -- every spin that ran fast now runs +slow for an equal time -- so what survives decays at the true `T2` rather +than the much shorter `T2*`. The difference between them is entirely +reversible dephasing, which is why the echo can recover a signal that +looked lost. + +Rust: `quantum::spin::spin_echo_sim` + """ + ... + +def bloch_equations(m0: tuple[float, float, float], field: Callable[[float], tuple[float, float, float]], gamma: float, t1: float, t2: float, equilibrium: float, t_end: float, dt: float) -> list[tuple[float, float, float]]: + """ +Integrates the Bloch equations for a magnetisation in a time-dependent +field. + +`dM/dt = gamma M x B - (Mx, My) / T2 - (Mz - M0) / T1`. The two relaxation +times are independent parameters and `T2 <= 2 T1` always, since the +transverse components cannot survive the longitudinal decay. + +Errors: +Returns an error for non-positive times or steps. + +Rust: `quantum::spin::bloch_equations` + """ + ... + +def nmr_fid(frequencies: list[float], decay_times: list[float], samples: int, rate: float) -> list[float]: + """ +A free induction decay: the sum of decaying sinusoids one per chemical +environment, sampled at `rate`. + +The Fourier transform of this is the spectrum, which is how nuclear +magnetic resonance actually works: the signal is measured in time and the +chemistry is read in frequency. + +Errors: +Returns an error for mismatched lists or a non-positive rate. + +Rust: `quantum::spin::nmr_fid` + """ + ... + +def zeeman_splitting(b: float, g_factor: float, m_j: float) -> float: + """ +The Zeeman energy shift of a level in a magnetic field. + +Panics: +Never; the arithmetic is a product. + +Rust: `quantum::spin::zeeman_splitting` + """ + ... + +def hyperfine_hydrogen_21cm() -> float: + """ +The hydrogen hyperfine transition frequency in hertz: the 21 centimetre +line. + +The transition is forbidden to first order and has a mean lifetime of some +ten million years, so no laboratory sample of hydrogen would ever show it. +The galaxy has enough hydrogen that it is the brightest line in radio +astronomy. + +Rust: `quantum::spin::hyperfine_hydrogen_21cm` + """ + ... diff --git a/bindings/python/python/numeria/quantum/wavefunction.pyi b/bindings/python/python/numeria/quantum/wavefunction.pyi new file mode 100644 index 0000000..754aab9 --- /dev/null +++ b/bindings/python/python/numeria/quantum/wavefunction.pyi @@ -0,0 +1,235 @@ +""" +One-dimensional wavefunctions, the standard eigenstates, and phase-space distributions. Everything here works in whatever unit system the caller supplies through `hbar` and the masses, so the natural choice for testing -- `hbar = m = 1` -- is available alongside SI. That matters more than it sounds: the quantities that can be checked exactly, like the harmonic oscillator's `(n + 1/2) hbar omega` spectrum or a Gaussian's saturation of the uncertainty bound, are clearest when the constants are one, and a module that hard-codes SI cannot express them. The one thing worth stating up front is the discretisation. A wavefunction is represented by its samples on a uniform grid, and every integral below is the corresponding Riemann sum. That is exact for none of them and spectrally accurate for a smooth function that has decayed to nothing at both ends -- which is the condition the callers here are responsible for arranging, and the one under which the tests hold to the tolerances they state. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.statistics.distributions import Poisson + +class Wavefunction1D: + """ +A complex wavefunction sampled on a uniform grid. + +The grid runs from `x0` in steps of `dx`, so sample `k` sits at +`x0 + k * dx`. + +Rust: `quantum::wavefunction::Wavefunction1D` + """ + def __init__(self, psi: list[complex], dx: float, x0: float) -> None: ... + @staticmethod + def gaussian_packet(centre: float, k0: float, sigma: float, dx: float, x0: float, n: int) -> Wavefunction1D: ... + @staticmethod + def plane_wave(k: float, dx: float, x0: float, n: int) -> Wavefunction1D: ... + def len(self) -> int: ... + def is_empty(self) -> bool: ... + def x(self, k: int) -> float: ... + def norm(self) -> float: ... + def normalize(self) -> None: ... + def probability_density(self) -> list[float]: ... + def expectation_x(self) -> float: ... + def variance_x(self) -> float: ... + def wavenumbers(self) -> list[float]: ... + def momentum_space(self) -> list[complex]: ... + def expectation_k(self) -> float: ... + def variance_k(self) -> float: ... + def uncertainty_product(self, hbar: float) -> float: ... + def overlap(self, other: Wavefunction1D) -> complex: ... + def energy(self, v: list[float], hbar: float, mass: float) -> float: ... + def propagate_free(self, t: float, hbar: float, mass: float) -> Wavefunction1D: ... + @property + def psi(self) -> list[complex]: ... + @property + def dx(self) -> float: ... + @property + def x0(self) -> float: ... + +def hermite_polynomial(n: int, x: float) -> float: + """ +The physicists' Hermite polynomial `H_n(x)`. + +Evaluated by the upward recurrence `H_{n+1} = 2x H_n - 2n H_{n-1}` rather +than from the explicit sum, whose alternating terms cancel catastrophically: +at `n = 20` and moderate `x` the largest term exceeds the answer by many +orders of magnitude, and a direct sum loses every significant digit. + +Rust: `quantum::wavefunction::hermite_polynomial` + """ + ... + +def laguerre_associated(n: int, k: float, x: float) -> float: + """ +The associated Laguerre polynomial `L_n^k(x)`. + +Also by recurrence, and for the same reason. + +Rust: `quantum::wavefunction::laguerre_associated` + """ + ... + +def harmonic_oscillator_eigenstate(n: int, x: float, mass: float, omega: float, hbar: float) -> float: + """ +The `n`-th harmonic oscillator eigenstate, normalised on the whole line. + +The normalisation `(m omega / pi hbar)^(1/4) / sqrt(2^n n!)` is folded in +through logarithms, since `2^n n!` overflows a double at `n = 170` while +the state itself stays perfectly ordinary. + +Panics: +Panics unless the mass, frequency and `hbar` are positive. + +Rust: `quantum::wavefunction::harmonic_oscillator_eigenstate` + """ + ... + +def harmonic_oscillator_energy(n: int, omega: float, hbar: float) -> float: + """ +The energy of the `n`-th harmonic oscillator level: `(n + 1/2) hbar omega`. + +The half is the zero-point energy, and it is not a convention: the ground +state cannot sit at the bottom of the well without violating the +uncertainty relation, and `hbar omega / 2` is exactly what the relation +costs. + +Panics: +Panics unless `omega` and `hbar` are positive. + +Rust: `quantum::wavefunction::harmonic_oscillator_energy` + """ + ... + +def infinite_well_eigenstate(n: int, x: float, l: float) -> float: + """ +The `n`-th eigenstate of an infinite square well of width `l`, indexed +from one, and zero outside the well. + +Panics: +Panics unless `n >= 1` and the width is positive. + +Rust: `quantum::wavefunction::infinite_well_eigenstate` + """ + ... + +def infinite_well_energy(n: int, l: float, mass: float, hbar: float) -> float: + """ +The energy of the `n`-th infinite-well level. + +Panics: +Panics unless `n >= 1` and the width, mass and `hbar` are positive. + +Rust: `quantum::wavefunction::infinite_well_energy` + """ + ... + +def hydrogen_radial(n: int, l: int, r: float, a0: float) -> float: + """ +The hydrogen radial wavefunction `R_{n,l}(r)` in units of the Bohr radius +`a0`. + +Panics: +Panics unless `n >= 1`, `l < n` and `a0` is positive. + +Rust: `quantum::wavefunction::hydrogen_radial` + """ + ... + +def hydrogen_energy(n: int) -> float: + """ +The hydrogen energy level in electronvolts: `-13.6 / n^2`. + +Panics: +Panics unless `n >= 1`. + +Rust: `quantum::wavefunction::hydrogen_energy` + """ + ... + +def hydrogen_orbital_density(n: int, l: int, m: int, r: float, theta: float, phi: float, a0: float) -> float: + """ +The probability density of a real hydrogen orbital at a point in spherical +coordinates. + +Uses the real spherical harmonics, so `m` selects the real combinations +-- the `p_x`, `p_y`, `p_z` shapes rather than the complex `m` eigenstates. +The two bases span the same space and give the same total density in a +shell; they differ in the angular shape of an individual orbital, which is +exactly what chemistry draws. + +Panics: +Panics unless `n >= 1`, `l < n`, `|m| <= l` and `a0` is positive. + +Rust: `quantum::wavefunction::hydrogen_orbital_density` + """ + ... + +def coherent_state(alpha: complex, n_max: int) -> list[complex]: + """ +The Fock coefficients of a coherent state `|alpha>`, truncated at +`n_max` photons. + +A Poisson distribution over photon number with mean `|alpha|^2`. Coherent +states are the eigenstates of the annihilation operator, which is why +removing a photon from a laser beam leaves it unchanged, and why the +photon statistics of a laser are Poissonian rather than thermal. + +Errors: +Returns an error for an empty truncation. + +Rust: `quantum::wavefunction::coherent_state` + """ + ... + +def squeezed_state(r: float, phi: float, n_max: int) -> list[complex]: + """ +The Fock coefficients of a squeezed vacuum state, truncated at `n_max`. + +Only the even photon numbers are populated, because the squeezing operator +creates photons in pairs. That parity is the state's signature and is what +makes it useful: the noise removed from one quadrature has to go somewhere, +and it goes into the other. + +Errors: +Returns an error for an empty truncation. + +Rust: `quantum::wavefunction::squeezed_state` + """ + ... + +def wigner_function(psi: list[complex], dx: float, x0: float, x: float, p: float, hbar: float) -> float: + """ +The Wigner function of a wavefunction at a point of phase space. + +`W(x, p) = (1 / pi hbar) integral psi*(x + y) psi(x - y) e^{2 i p y / hbar} dy`. + +The nearest thing quantum mechanics has to a phase-space probability +density: its marginals are the true position and momentum distributions. +It is not a probability density, because it takes negative values -- and +where it does is exactly where the state has no classical description, so +the negativity is the useful part rather than a defect of the definition. + +Errors: +Returns an error for a non-positive spacing or `hbar`. + +Rust: `quantum::wavefunction::wigner_function` + """ + ... + +def husimi_q(psi: list[complex], dx: float, x0: float, x: float, p: float, sigma: float, hbar: float) -> float: + """ +The Husimi Q function: the Wigner function smoothed by a coherent state of +width `sigma`. + +Smoothing over a phase-space cell of the minimum allowed area is exactly +enough to remove the negativity, so `Q` is a genuine probability density. +What it buys in interpretability it loses in resolution: the interference +fringes that make the Wigner function negative are precisely what the +smoothing erases. + +Errors: +Returns an error for a non-positive spacing, width, or `hbar`. + +Rust: `quantum::wavefunction::husimi_q` + """ + ... diff --git a/bindings/python/python/numeria/quaternion.pyi b/bindings/python/python/numeria/quaternion.pyi new file mode 100644 index 0000000..4f57075 --- /dev/null +++ b/bindings/python/python/numeria/quaternion.pyi @@ -0,0 +1,62 @@ +""" +Unit quaternions for 3-D rotation. `Quaternion` with the full algebra -- Hamilton product, conjugate, inverse, norm and normalization -- and conversion to and from axis-angle, Euler angles and rotation matrices. Quaternions are used for orientation rather than Euler angles because they compose without gimbal lock and interpolate smoothly: `slerp` moves along the great circle at constant angular rate, and `nlerp` is the cheaper normalized-linear approximation to it. For the Lie-group view of the same object, and for rotations in four dimensions, see `manifold::lie`. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.math import Vec3 + +class Quaternion: + """ + +Rust: `quaternion::Quaternion` + """ + def __init__(self, w: float, x: float, y: float, z: float) -> None: ... + @staticmethod + def identity() -> Quaternion: ... + @staticmethod + def from_axis_angle(axis: Vec3 | Sequence[float], angle: float) -> Quaternion: ... + @staticmethod + def from_euler(roll: float, pitch: float, yaw: float) -> Quaternion: ... + def norm(self) -> float: ... + def normalize(self) -> Quaternion: ... + def conjugate(self) -> Quaternion: ... + def inverse(self) -> Quaternion: ... + def dot(self, other: Quaternion | Sequence[float]) -> float: ... + def rotate_vec(self, v: Vec3 | Sequence[float]) -> Vec3: ... + def to_rotation_matrix(self) -> list[list[float]]: ... + def to_axis_angle(self) -> tuple[Vec3, float]: ... + def to_euler(self) -> tuple[float, float, float]: ... + def angle_between(self, other: Quaternion | Sequence[float]) -> float: ... + def is_unit(self, tolerance: float) -> bool: ... + def __mul__(self, rhs: Quaternion | Sequence[float]) -> Quaternion: ... + def __add__(self, rhs: Quaternion | Sequence[float]) -> Quaternion: ... + def __sub__(self, rhs: Quaternion | Sequence[float]) -> Quaternion: ... + def __neg__(self) -> Quaternion: ... + @property + def w(self) -> float: ... + @property + def x(self) -> float: ... + @property + def y(self) -> float: ... + @property + def z(self) -> float: ... + +def slerp(q1: Quaternion | Sequence[float], q2: Quaternion | Sequence[float], t: float) -> Quaternion: + """ +Spherical linear interpolation between two quaternions at parameter t in [0, 1]. + +Rust: `quaternion::slerp` + """ + ... + +def nlerp(q1: Quaternion | Sequence[float], q2: Quaternion | Sequence[float], t: float) -> Quaternion: + """ +Normalized linear interpolation between two quaternions (cheaper than slerp). + +Rust: `quaternion::nlerp` + """ + ... diff --git a/bindings/python/python/numeria/radiation.pyi b/bindings/python/python/numeria/radiation.pyi new file mode 100644 index 0000000..f655138 --- /dev/null +++ b/bindings/python/python/numeria/radiation.pyi @@ -0,0 +1,130 @@ +""" +Thermal radiation and radiative transfer. The Stefan-Boltzmann law `j = σT⁴`, Wien's displacement of the spectral peak, and the colour and brightness temperatures that invert them. Transfer through an absorbing medium: optical depth, the Beer-Lambert law, and the photon mean free path. Radiation pressure for absorbing and reflecting surfaces. Surface exchange via Kirchhoff's law (emissivity equals absorptivity at equilibrium), view factors, and net radiative exchange between surfaces. For the Planck spectrum itself see `quantum`; for reactor and photon shielding see `neutronics`. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def total_emissive_power(temperature: float) -> float: + """ +Total emissive power of a perfect blackbody (ε=1): E = σT⁴ + +Rust: `radiation::total_emissive_power` + """ + ... + +def spectral_peak_frequency(temperature: float) -> float: + """ +Wien's law in the frequency domain: f_max = 5.879×10¹⁰ × T + +Rust: `radiation::spectral_peak_frequency` + """ + ... + +def color_temperature(peak_wavelength: float) -> float: + """ +Inverse Wien's law: T = b / λ_max (color temperature from peak wavelength) + +Rust: `radiation::color_temperature` + """ + ... + +def brightness_temperature(intensity: float, frequency: float) -> float: + """ +Brightness temperature via the Rayleigh-Jeans approximation: T_b = Ic² / (2kf²) + +Rust: `radiation::brightness_temperature` + """ + ... + +def optical_depth(absorption_coeff: float, path_length: float) -> float: + """ +Optical depth: τ = κ × s + +Rust: `radiation::optical_depth` + """ + ... + +def beer_lambert(initial_intensity: float, absorption_coeff: float, path_length: float) -> float: + """ +Beer-Lambert law: I = I₀ × e^(-κs) + +Rust: `radiation::beer_lambert` + """ + ... + +def mean_free_path_photon(absorption_coeff: float) -> float: + """ +Photon mean free path: l = 1/κ + +Rust: `radiation::mean_free_path_photon` + """ + ... + +def radiation_pressure(intensity: float) -> float: + """ +Radiation pressure for fully absorbed radiation: P = I/c + +Rust: `radiation::radiation_pressure` + """ + ... + +def radiation_pressure_reflected(intensity: float) -> float: + """ +Radiation pressure for fully reflected radiation: P = 2I/c + +Rust: `radiation::radiation_pressure_reflected` + """ + ... + +def emissivity_from_absorptivity(absorptivity: float) -> float: + """ +At thermal equilibrium, emissivity equals absorptivity: ε = α + +Rust: `radiation::emissivity_from_absorptivity` + """ + ... + +def view_factor_parallel_plates(width: float, height: float, separation: float) -> float: + """ +View factor for two identical, directly opposed, parallel rectangles of +width W and height H separated by distance D. + +Uses the exact analytical formula: +F = (2 / (πXY)) * [ ln(√((1+X²)(1+Y²)/(1+X²+Y²))) + + X√(1+Y²) atan(X/√(1+Y²)) + + Y√(1+X²) atan(Y/√(1+X²)) + - X atan(X) - Y atan(Y) ] +where X = W/D and Y = H/D. + +Rust: `radiation::view_factor_parallel_plates` + """ + ... + +def radiative_exchange(emissivity1: float, emissivity2: float, area: float, t1: float, t2: float) -> float: + """ +Radiative heat exchange between two infinite parallel gray surfaces: +Q = σA(T₁⁴ - T₂⁴) / (1/ε₁ + 1/ε₂ - 1) + +Rust: `radiation::radiative_exchange` + """ + ... + +def intensity_at_distance(luminosity: float, distance: float) -> float: + """ +Intensity at distance from a point source: I = L / (4πd²) + +Rust: `radiation::intensity_at_distance` + """ + ... + +def luminosity_from_intensity(intensity: float, distance: float) -> float: + """ +Luminosity from measured intensity and distance: L = I × 4πd² + +Rust: `radiation::luminosity_from_intensity` + """ + ... diff --git a/bindings/python/python/numeria/relativity.pyi b/bindings/python/python/numeria/relativity.pyi new file mode 100644 index 0000000..8496b67 --- /dev/null +++ b/bindings/python/python/numeria/relativity.pyi @@ -0,0 +1,155 @@ +""" +Special relativity. The Lorentz factor and the kinematic consequences -- time dilation, length contraction, the velocity-addition law that keeps `c` a limit, and the Lorentz transformation of position and time. Dynamics: relativistic momentum and kinetic energy, total and rest energy, and the energy-momentum relation `E² = (pc)² + (mc²)²`. The relativistic Doppler shift for approaching and receding sources. Also proper time and the spacetime interval, whose sign classifies a separation as timelike, spacelike or null -- the invariant that replaces separate notions of distance and duration. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.statistics.distributions import Beta + +def lorentz_factor(velocity: float) -> float: + """ +Lorentz factor: γ = 1 / sqrt(1 - v^2/c^2) + +Rust: `relativity::lorentz_factor` + """ + ... + +def beta(velocity: float) -> float: + """ +Beta factor: β = v / c + +Rust: `relativity::beta` + """ + ... + +def time_dilation(proper_time: float, velocity: float) -> float: + """ +Time dilation: Δt = γ * Δt_proper + +Rust: `relativity::time_dilation` + """ + ... + +def length_contraction(proper_length: float, velocity: float) -> float: + """ +Length contraction: L = L_proper / γ + +Rust: `relativity::length_contraction` + """ + ... + +def relativistic_momentum(mass: float, velocity: float) -> float: + """ +Relativistic momentum: p = γ * m * v + +Rust: `relativity::relativistic_momentum` + """ + ... + +def relativistic_kinetic_energy(mass: float, velocity: float) -> float: + """ +Relativistic kinetic energy: KE = (γ - 1) * m * c^2 + +Rust: `relativity::relativistic_kinetic_energy` + """ + ... + +def relativistic_total_energy(mass: float, velocity: float) -> float: + """ +Total relativistic energy: E = γ * m * c^2 + +Rust: `relativity::relativistic_total_energy` + """ + ... + +def rest_energy(mass: float) -> float: + """ +Rest energy: E = m * c^2 + +Rust: `relativity::rest_energy` + """ + ... + +def energy_from_momentum(momentum: float, mass: float) -> float: + """ +Energy-momentum relation: E^2 = (pc)^2 + (mc^2)^2 +Returns total energy given momentum and rest mass. + +Rust: `relativity::energy_from_momentum` + """ + ... + +def velocity_addition(v: float, u_prime: float) -> float: + """ +Relativistic velocity addition: u = (v + u') / (1 + v*u'/c^2) + +Rust: `relativity::velocity_addition` + """ + ... + +def lorentz_transform_x(x: float, v: float, t: float) -> float: + """ +Lorentz transformation of position: x' = γ * (x - v*t) + +Rust: `relativity::lorentz_transform_x` + """ + ... + +def lorentz_transform_t(t: float, v: float, x: float) -> float: + """ +Lorentz transformation of time: t' = γ * (t - v*x/c^2) + +Rust: `relativity::lorentz_transform_t` + """ + ... + +def relativistic_doppler_approaching(frequency: float, velocity: float) -> float: + """ +Relativistic Doppler effect (approaching): f' = f * sqrt((1+β)/(1-β)) + +Rust: `relativity::relativistic_doppler_approaching` + """ + ... + +def relativistic_doppler_receding(frequency: float, velocity: float) -> float: + """ +Relativistic Doppler effect (receding): f' = f * sqrt((1-β)/(1+β)) + +Rust: `relativity::relativistic_doppler_receding` + """ + ... + +def gravitational_redshift(emitted_freq: float, mass: float, radius: float) -> float: + """ +Gravitational redshift: f_obs = f_emit * sqrt(1 - 2GM/(rc^2)) + +Rust: `relativity::gravitational_redshift` + """ + ... + +def relativistic_mass(rest_mass: float, velocity: float) -> float: + """ +Relativistic mass (apparent mass at velocity v): m_rel = γ * m_0 + +Rust: `relativity::relativistic_mass` + """ + ... + +def proper_time(coordinate_time: float, velocity: float) -> float: + """ +Proper time interval from coordinate time: Δτ = Δt / γ + +Rust: `relativity::proper_time` + """ + ... + +def spacetime_interval_squared(dt: float, dx: float, dy: float, dz: float) -> float: + """ +Spacetime interval: s^2 = (cΔt)^2 - Δx^2 - Δy^2 - Δz^2 + +Rust: `relativity::spacetime_interval_squared` + """ + ... diff --git a/bindings/python/python/numeria/resonance/__init__.pyi b/bindings/python/python/numeria/resonance/__init__.pyi new file mode 100644 index 0000000..a583656 --- /dev/null +++ b/bindings/python/python/numeria/resonance/__init__.pyi @@ -0,0 +1,94 @@ +""" +Resonance and vibration: single and coupled oscillators, acoustic and electromagnetic cavities, nonlinear resonance, and structural dynamics. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from . import cavity, coupled, nonlinear, oscillator, structural +from numeria.resonance.cavity import BeamBc as BeamBc +from numeria.resonance.coupled import CoupledOscillators as CoupledOscillators +from numeria.resonance.oscillator import DampedOscillator as DampedOscillator +from numeria.resonance.oscillator import Damping as Damping +from numeria.resonance.structural import ModalModel as ModalModel +from numeria.resonance.cavity import PlateBc as PlateBc +from numeria.resonance.cavity import Rlc as Rlc +from numeria.resonance.nonlinear import autoresonance_threshold as autoresonance_threshold +from numeria.resonance.cavity import beam_mode_shape as beam_mode_shape +from numeria.resonance.cavity import beam_modes as beam_modes +from numeria.resonance.cavity import bell_modes_approx as bell_modes_approx +from numeria.resonance.oscillator import bode_plot as bode_plot +from numeria.resonance.cavity import cavity_photon_lifetime as cavity_photon_lifetime +from numeria.resonance.cavity import cavity_q as cavity_q +from numeria.resonance.cavity import chladni_pattern as chladni_pattern +from numeria.resonance.cavity import chladni_pattern_mixed as chladni_pattern_mixed +from numeria.resonance.structural import circle_fit as circle_fit +from numeria.resonance.cavity import circular_membrane_modes as circular_membrane_modes +from numeria.resonance.cavity import circular_membrane_shape as circular_membrane_shape +from numeria.resonance.cavity import conical_tube_modes as conical_tube_modes +from numeria.resonance.cavity import coupled_cavity_splitting as coupled_cavity_splitting +from numeria.resonance.cavity import cylindrical_cavity_modes as cylindrical_cavity_modes +from numeria.resonance.nonlinear import describing_function as describing_function +from numeria.resonance.nonlinear import duffing_backbone as duffing_backbone +from numeria.resonance.nonlinear import duffing_jump_frequencies as duffing_jump_frequencies +from numeria.resonance.nonlinear import duffing_poincare as duffing_poincare +from numeria.resonance.nonlinear import duffing_response_amplitude as duffing_response_amplitude +from numeria.resonance.nonlinear import duffing_simulate as duffing_simulate +from numeria.resonance.structural import experimental_modal_peak_picking as experimental_modal_peak_picking +from numeria.resonance.cavity import fabry_perot_finesse as fabry_perot_finesse +from numeria.resonance.cavity import fabry_perot_fsr as fabry_perot_fsr +from numeria.resonance.cavity import fabry_perot_transmission as fabry_perot_transmission +from numeria.resonance.nonlinear import fano_fit as fano_fit +from numeria.resonance.nonlinear import fano_lineshape as fano_lineshape +from numeria.resonance.nonlinear import frequency_pulling as frequency_pulling +from numeria.resonance.structural import half_power_bandwidth as half_power_bandwidth +from numeria.resonance.nonlinear import harmonic_balance as harmonic_balance +from numeria.resonance.cavity import helmholtz_q as helmholtz_q +from numeria.resonance.cavity import helmholtz_resonator as helmholtz_resonator +from numeria.resonance.coupled import huygens_sync_simulate as huygens_sync_simulate +from numeria.resonance.nonlinear import hysteresis_loop as hysteresis_loop +from numeria.resonance.cavity import inharmonicity_coefficient as inharmonicity_coefficient +from numeria.resonance.nonlinear import injection_locking_range as injection_locking_range +from numeria.resonance.nonlinear import kapitza_pendulum_stable as kapitza_pendulum_stable +from numeria.resonance.coupled import kuramoto as kuramoto +from numeria.resonance.coupled import kuramoto_critical_coupling as kuramoto_critical_coupling +from numeria.resonance.oscillator import lorentzian as lorentzian +from numeria.resonance.oscillator import lorentzian_fit as lorentzian_fit +from numeria.resonance.nonlinear import mathieu_stability as mathieu_stability +from numeria.resonance.nonlinear import mathieu_stability_chart as mathieu_stability_chart +from numeria.resonance.cavity import microwave_cavity_modes_rect as microwave_cavity_modes_rect +from numeria.resonance.oscillator import nyquist_plot as nyquist_plot +from numeria.resonance.structural import operational_deflection_shape as operational_deflection_shape +from numeria.resonance.nonlinear import parametric_resonance_threshold as parametric_resonance_threshold +from numeria.resonance.oscillator import q_from_ringdown as q_from_ringdown +from numeria.resonance.oscillator import q_from_spectrum as q_from_spectrum +from numeria.resonance.oscillator import quality_factor_combined as quality_factor_combined +from numeria.resonance.cavity import quarter_wave_resonator as quarter_wave_resonator +from numeria.resonance.cavity import rectangular_membrane_modes as rectangular_membrane_modes +from numeria.resonance.cavity import rectangular_plate_modes as rectangular_plate_modes +from numeria.resonance.oscillator import resonance_curve as resonance_curve +from numeria.resonance.cavity import resonance_overlap as resonance_overlap +from numeria.resonance.cavity import room_mode_density as room_mode_density +from numeria.resonance.cavity import room_modes as room_modes +from numeria.resonance.cavity import schroeder_frequency as schroeder_frequency +from numeria.resonance.structural import shock_response_spectrum as shock_response_spectrum +from numeria.resonance.cavity import stiff_string_modes as stiff_string_modes +from numeria.resonance.nonlinear import stochastic_resonance_snr as stochastic_resonance_snr +from numeria.resonance.structural import stochastic_subspace_identification as stochastic_subspace_identification +from numeria.resonance.cavity import string_mode_shape as string_mode_shape +from numeria.resonance.cavity import string_modes as string_modes +from numeria.resonance.nonlinear import subharmonic_response as subharmonic_response +from numeria.resonance.oscillator import transmissibility as transmissibility +from numeria.resonance.cavity import tube_end_correction as tube_end_correction +from numeria.resonance.cavity import tube_modes as tube_modes +from numeria.resonance.coupled import tuned_mass_damper_design as tuned_mass_damper_design +from numeria.resonance.cavity import tuning_fork_frequency as tuning_fork_frequency +from numeria.resonance.coupled import two_pendulums_coupled as two_pendulums_coupled +from numeria.resonance.nonlinear import van_der_pol_entrainment_range as van_der_pol_entrainment_range +from numeria.resonance.nonlinear import van_der_pol_limit_cycle_amplitude as van_der_pol_limit_cycle_amplitude +from numeria.resonance.nonlinear import van_der_pol_simulate as van_der_pol_simulate +from numeria.resonance.coupled import wilberforce_pendulum as wilberforce_pendulum + + diff --git a/bindings/python/python/numeria/resonance/cavity.pyi b/bindings/python/python/numeria/resonance/cavity.pyi new file mode 100644 index 0000000..fc8593f --- /dev/null +++ b/bindings/python/python/numeria/resonance/cavity.pyi @@ -0,0 +1,331 @@ +""" +Resonant cavities and structures: RLC circuits, Helmholtz resonators, strings, air columns, membranes, plates, beams, rooms, optical etalons, and microwave cavities. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.audio.tuning import Mode + +class BeamBc: + """ +Beam boundary conditions. + +Rust: `resonance::cavity::BeamBc` + """ + ... + +class PlateBc: + """ +Plate boundary conditions for `rectangular_plate_modes`. + +Rust: `resonance::cavity::PlateBc` + """ + ... + +class Rlc: + """ +Series/parallel RLC resonator. + +Rust: `resonance::cavity::Rlc` + """ + def __init__(self, r: float, l: float, c: float) -> None: ... + def series_impedance(self, omega: float) -> complex: ... + def parallel_impedance(self, omega: float) -> complex: ... + def resonant_frequency(self) -> float: ... + def q_series(self) -> float: ... + def q_parallel(self) -> float: ... + def bandwidth(self) -> float: ... + def damping_ratio(self) -> float: ... + def transfer_lowpass(self, omega: float) -> complex: ... + def transfer_bandpass(self, omega: float) -> complex: ... + def transfer_highpass(self, omega: float) -> complex: ... + def transfer_notch(self, omega: float) -> complex: ... + def step_response(self, t: float, v: float) -> float: ... + def energy(self, i: float, v: float) -> float: ... + @property + def r(self) -> float: ... + @property + def l(self) -> float: ... + @property + def c(self) -> float: ... + +def helmholtz_resonator(volume: float, neck_area: float, neck_length: float, c: float) -> float: + """ +Helmholtz resonance frequency (Hz) with a flanged-end correction of +1.7·r added to the neck length. + +Rust: `resonance::cavity::helmholtz_resonator` + """ + ... + +def helmholtz_q(volume: float, neck_area: float, neck_length: float, c: float) -> float: + """ +Radiation-limited quality factor of a Helmholtz resonator (flanged +baffle radiation resistance): Q = 2π·√(V·L_eff³/A³). + +Rust: `resonance::cavity::helmholtz_q` + """ + ... + +def string_modes(length: float, tension: float, mu: float, n: int) -> list[float]: + """ +Ideal string mode frequencies i·√(T/μ)/(2L), i = 1..=n. + +Rust: `resonance::cavity::string_modes` + """ + ... + +def string_mode_shape(length: float, n: int, x: float) -> float: + """ +Mode shape sin(nπx/L). + +Rust: `resonance::cavity::string_mode_shape` + """ + ... + +def stiff_string_modes(length: float, tension: float, mu: float, young: float, radius: float, n: int) -> list[float]: + """ +Stiff-string partials fₙ = n·f₁·√(1 + B·n²) with the piano +inharmonicity coefficient B (radius-based). + +Rust: `resonance::cavity::stiff_string_modes` + """ + ... + +def inharmonicity_coefficient(young: float, radius: float, tension: float, length: float) -> float: + """ +Piano-string inharmonicity B = π³·E·r⁴/(4·T·L²). + +Rust: `resonance::cavity::inharmonicity_coefficient` + """ + ... + +def tube_modes(length: float, c: float, open_open: bool, n: int) -> list[float]: + """ +Air-column modes: open-open i·c/(2L); open-closed odd harmonics +(2i−1)·c/(4L). Pass the end-corrected length. + +Rust: `resonance::cavity::tube_modes` + """ + ... + +def tube_end_correction(radius: float, flanged: bool) -> float: + """ +End correction of an open tube end: 0.85·r flanged, 0.61·r unflanged. + +Rust: `resonance::cavity::tube_end_correction` + """ + ... + +def conical_tube_modes(length: float, c: float, n: int) -> list[float]: + """ +Complete-cone modes: like an open-open pipe, i·c/(2L). + +Rust: `resonance::cavity::conical_tube_modes` + """ + ... + +def rectangular_membrane_modes(a: float, b: float, tension: float, sigma: float, max_m: int, max_n: int) -> list[tuple[int, int, float]]: + """ +Rectangular membrane modes (m, n, f) sorted by frequency: +f = (c/2)·√((m/a)² + (n/b)²), c = √(T/σ). + +Rust: `resonance::cavity::rectangular_membrane_modes` + """ + ... + +def circular_membrane_modes(radius: float, tension: float, sigma: float, max_m: int, max_n: int) -> list[tuple[int, int, float]]: + """ +Circular membrane modes (m angular, n radial, f) sorted by +frequency: f = α_mn·c/(2πR) with α_mn the n-th zero of J_m. + +Rust: `resonance::cavity::circular_membrane_modes` + """ + ... + +def circular_membrane_shape(radius: float, m: int, n: int, r: float, theta: float) -> float: + """ +Circular membrane mode shape J_m(α_mn·r/R)·cos(mθ). + +Rust: `resonance::cavity::circular_membrane_shape` + """ + ... + +def rectangular_plate_modes(a: float, b: float, h: float, young: float, nu: float, rho: float, bc: PlateBc, max_m: int, max_n: int) -> list[tuple[int, int, float]]: + """ +Thin rectangular plate modes (m, n, f Hz) sorted ascending. +Simply supported edges are exact; clamped edges use the separable +beam-function Rayleigh estimate (upper bound, a few % high). + +Rust: `resonance::cavity::rectangular_plate_modes` + """ + ... + +def chladni_pattern(a: float, b: float, m: int, n: int, res: int) -> ScalarField2: + """ +Chladni figure of a square-symmetric plate mode: the field +φ_mn + φ_nm with φ_mn = cos(mπx/a)cos(nπy/b) on a res×res grid +(nodal lines are the zero set). + +Rust: `resonance::cavity::chladni_pattern` + """ + ... + +def chladni_pattern_mixed(a: float, b: float, modes: list[tuple[int, int, float]], res: int) -> ScalarField2: + """ +General superposition Σ cᵢ·cos(mᵢπx/a)cos(nᵢπy/b). + +Rust: `resonance::cavity::chladni_pattern_mixed` + """ + ... + +def beam_modes(length: float, young: float, i_area: float, rho: float, area: float, bc: BeamBc, n: int) -> list[float]: + """ +Euler-Bernoulli beam natural frequencies (Hz): +f_i = λ_i²/(2πL²)·√(EI/(ρA)). + +Rust: `resonance::cavity::beam_modes` + """ + ... + +def beam_mode_shape(length: float, bc: BeamBc, n: int, x: float) -> float: + """ +Euler-Bernoulli beam mode shape at position x ∈ \\[0, L\\] +(unnormalized; standard clamped/simply-supported/free functions). + +Rust: `resonance::cavity::beam_mode_shape` + """ + ... + +def tuning_fork_frequency(length: float, thickness: float, young: float, rho: float) -> float: + """ +Tuning fork prong frequency: cantilever first mode of a rectangular +prong, f = (1.875²/2π)·(t/L²)·√(E/(12ρ)). + +Rust: `resonance::cavity::tuning_fork_frequency` + """ + ... + +def bell_modes_approx(radius: float, thickness: float, young: float, rho: float, nu: float, n: int) -> list[float]: + """ +Bell/ring flexural modes (thin-ring approximation), n = 2..: +f_n = n(n²−1)/√(n²+1) · (t/(2πR²))·√(E/(12ρ(1−ν²))). + +Rust: `resonance::cavity::bell_modes_approx` + """ + ... + +def room_modes(lx: float, ly: float, lz: float, c: float, max_n: int) -> list[tuple[int, int, int, float]]: + """ +All room modes (nx, ny, nz, f Hz) with indices up to max_n, sorted: +f = (c/2)·√((nx/lx)² + (ny/ly)² + (nz/lz)²). + +Rust: `resonance::cavity::room_modes` + """ + ... + +def room_mode_density(lx: float, ly: float, lz: float, c: float, f: float) -> float: + """ +Asymptotic modal density dN/df = 4πVf²/c³ + πSf/(2c²) + L/(8c). + +Rust: `resonance::cavity::room_mode_density` + """ + ... + +def schroeder_frequency(rt60: float, volume: float) -> float: + """ +Schroeder crossover frequency 2000·√(RT60/V) (Hz). + +Rust: `resonance::cavity::schroeder_frequency` + """ + ... + +def fabry_perot_transmission(wavelength: float, length: float, r: float, n_index: float) -> float: + """ +Fabry-Perot (Airy) intensity transmission for mirror reflectance r: +T = (1−r)²/((1−r)² + 4r·sin²(δ/2)), δ = 4πnL/λ. + +Rust: `resonance::cavity::fabry_perot_transmission` + """ + ... + +def fabry_perot_fsr(length: float, n_index: float, c: float) -> float: + """ +Free spectral range c/(2nL) (Hz). + +Rust: `resonance::cavity::fabry_perot_fsr` + """ + ... + +def fabry_perot_finesse(r: float) -> float: + """ +Finesse π√r/(1−r). + +Rust: `resonance::cavity::fabry_perot_finesse` + """ + ... + +def cavity_q(frequency: float, fwhm: float) -> float: + """ +Quality factor f/Δf. + +Rust: `resonance::cavity::cavity_q` + """ + ... + +def cavity_photon_lifetime(q: float, frequency: float) -> float: + """ +Photon lifetime Q/(2πf) (s). + +Rust: `resonance::cavity::cavity_photon_lifetime` + """ + ... + +def microwave_cavity_modes_rect(a: float, b: float, d: float, c: float, max_n: int) -> list[tuple[str, float]]: + """ +Rectangular microwave cavity modes (label, f Hz) up to index max_n, +sorted: f = (c/2)·√((m/a)² + (n/b)² + (p/d)²) with the standard TE +(p ≥ 1, m+n ≥ 1) and TM (m, n ≥ 1, p ≥ 0) index rules. + +Rust: `resonance::cavity::microwave_cavity_modes_rect` + """ + ... + +def cylindrical_cavity_modes(radius: float, height: float, c: float, max_n: int) -> list[tuple[str, float]]: + """ +Cylindrical cavity modes (label, f Hz), sorted: +TM_mnp uses J_m zeros (p ≥ 0), TE_mnp uses J′_m zeros (p ≥ 1); +f = (c/2π)·√((x/R)² + (pπ/H)²). + +Rust: `resonance::cavity::cylindrical_cavity_modes` + """ + ... + +def quarter_wave_resonator(length: float, c: float) -> float: + """ +Quarter-wave resonator fundamental c/(4L). + +Rust: `resonance::cavity::quarter_wave_resonator` + """ + ... + +def coupled_cavity_splitting(f0: float, coupling: float) -> tuple[float, float]: + """ +Mode splitting of two identical coupled cavities with coupling +coefficient κ: (f₀√(1−κ), f₀√(1+κ)). + +Rust: `resonance::cavity::coupled_cavity_splitting` + """ + ... + +def resonance_overlap(f1: float, q1: float, f2: float, q2: float) -> float: + """ +Lorentzian overlap of two resonances (1 when co-tuned, → 0 when far +apart relative to their combined half-widths). + +Rust: `resonance::cavity::resonance_overlap` + """ + ... diff --git a/bindings/python/python/numeria/resonance/coupled.pyi b/bindings/python/python/numeria/resonance/coupled.pyi new file mode 100644 index 0000000..2ff8971 --- /dev/null +++ b/bindings/python/python/numeria/resonance/coupled.pyi @@ -0,0 +1,106 @@ +""" +Coupled linear oscillators: normal modes, modal superposition, receptance, classic two-body systems, Kuramoto synchronization, and tuned-mass-damper design. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.linalg.matrix import Matrix + +class CoupledOscillators: + """ +N-degree-of-freedom system M·x″ + C·x′ + K·x = F. + +Rust: `resonance::coupled::CoupledOscillators` + """ + def __init__(self, masses: list[float], stiffness: Matrix | Sequence[Sequence[float]], damping: Matrix | Sequence[Sequence[float]]) -> None: ... + @staticmethod + def chain(n: int, m: float, k: float, k_coupling: float) -> CoupledOscillators: ... + @staticmethod + def chain_fixed_ends(n: int, m: float, k: float) -> CoupledOscillators: ... + @staticmethod + def ring(n: int, m: float, k: float) -> CoupledOscillators: ... + @staticmethod + def from_springs(masses: list[float], springs: list[tuple[int, int, float]]) -> CoupledOscillators: ... + def normal_modes(self) -> tuple[list[float], Matrix]: ... + def mode_shape(self, i: int) -> list[float]: ... + def modal_participation(self, x0: list[float]) -> list[float]: ... + def modal_damping_ratios(self) -> list[float]: ... + def response(self, x0: list[float], v0: list[float], t: float) -> list[float]: ... + def forced_response(self, force: list[float], omega: float) -> list[complex]: ... + def frequency_response_matrix(self, omega: float) -> Matrix: ... + def beat_frequency(self) -> Optional[float]: ... + def energy_transfer_time(self) -> Optional[float]: ... + def dispersion_relation(self, k_wave: float) -> float: ... + def simulate(self, x0: list[float], v0: list[float], t_end: float, dt: float) -> list[list[float]]: ... + def dunkerley_estimate(self) -> float: ... + def rayleigh_quotient(self, shape: list[float]) -> float: ... + def anti_resonance_frequencies(self, i: int, j: int) -> list[float]: ... + @property + def masses(self) -> list[float]: ... + @property + def stiffness(self) -> Matrix: ... + @property + def damping(self) -> Matrix: ... + +def two_pendulums_coupled(l: float, g: float, k: float, m: float) -> CoupledOscillators: + """ +Two pendulums (length l, mass m) coupled by a spring k: 2-dof +small-angle system in the displacement coordinates. + +Rust: `resonance::coupled::two_pendulums_coupled` + """ + ... + +def wilberforce_pendulum(m: float, k: float, i: float, kappa: float, eps: float) -> CoupledOscillators: + """ +Wilberforce pendulum: vertical bounce (mass m, spring k) coupled to +torsion (inertia i, stiffness kappa) through the cross term eps. + +Rust: `resonance::coupled::wilberforce_pendulum` + """ + ... + +def huygens_sync_simulate(omega1: float, omega2: float, coupling: float, t_end: float, dt: float) -> list[tuple[float, float]]: + """ +Two weakly coupled phase oscillators (Huygens' clocks abstraction): +φ̇₁ = ω₁ + κ·sin(φ₂−φ₁), φ̇₂ = ω₂ + κ·sin(φ₁−φ₂). Returns +(t, wrapped phase difference) per step. + +Rust: `resonance::coupled::huygens_sync_simulate` + """ + ... + +def kuramoto(n: int, k: float, omegas: list[float], theta0: list[float], t_end: float, dt: float) -> tuple[list[list[float]], list[float]]: + """ +Kuramoto model of n phase oscillators with global coupling K: +returns the phase history (per step) and the order parameter r(t). + +Panics: +Panics unless `omegas` and `theta0` both have length n. + +Rust: `resonance::coupled::kuramoto` + """ + ... + +def kuramoto_critical_coupling(omegas: list[float]) -> float: + """ +Critical coupling estimate K_c = 2/(π·g(0)) with the frequency +density at the center estimated by a Gaussian kernel (Silverman +bandwidth) around the mean frequency. + +Rust: `resonance::coupled::kuramoto_critical_coupling` + """ + ... + +def tuned_mass_damper_design(m_primary: float, k_primary: float, mass_ratio: float) -> tuple[float, float, float]: + """ +Den Hartog tuned-mass-damper design for an undamped primary +(m_primary, k_primary) and absorber mass ratio μ: returns the +absorber stiffness, damping, and optimal tuning ratio f = 1/(1+μ). + +Rust: `resonance::coupled::tuned_mass_damper_design` + """ + ... diff --git a/bindings/python/python/numeria/resonance/nonlinear.pyi b/bindings/python/python/numeria/resonance/nonlinear.pyi new file mode 100644 index 0000000..a5435ab --- /dev/null +++ b/bindings/python/python/numeria/resonance/nonlinear.pyi @@ -0,0 +1,227 @@ +""" +Nonlinear resonance: the Duffing and van der Pol oscillators, parametric (Mathieu) stability, Fano interference, synchronization pulling/locking, and generic harmonic-balance machinery. The Duffing convention throughout is x″ + δ·x′ + α·x + β·x³ = γ·cos(ωt). +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def duffing_response_amplitude(alpha: float, beta: float, delta: float, gamma: float, omega: float) -> list[float]: + """ +Steady single-harmonic response amplitudes of the Duffing oscillator +at drive frequency ω (harmonic balance): up to three coexisting +branches, ascending. + +Rust: `resonance::nonlinear::duffing_response_amplitude` + """ + ... + +def duffing_backbone(alpha: float, beta: float, amplitude: float) -> float: + """ +Backbone curve: free-vibration frequency at amplitude a, +ω = √(α + ¾βa²). + +Rust: `resonance::nonlinear::duffing_backbone` + """ + ... + +def duffing_jump_frequencies(alpha: float, beta: float, delta: float, gamma: float) -> Optional[tuple[float, float]]: + """ +Jump (saddle-node) frequencies of the forced Duffing sweep: the ω +interval where three branches coexist, found by scanning the +harmonic-balance solution count. None when no bistability exists. + +Rust: `resonance::nonlinear::duffing_jump_frequencies` + """ + ... + +def duffing_simulate(alpha: float, beta: float, delta: float, gamma: float, omega: float, x0: float, v0: float, t_end: float, dt: float) -> list[tuple[float, float, float]]: + """ +RK4 trajectory (t, x, v) of the forced Duffing oscillator. + +Rust: `resonance::nonlinear::duffing_simulate` + """ + ... + +def duffing_poincare(alpha: float, beta: float, delta: float, gamma: float, omega: float, x0: float, v0: float, n_points: int) -> list[tuple[float, float]]: + """ +Poincaré section of the Duffing oscillator: (x, v) sampled once per +forcing period, after discarding 100 transient periods. + +Rust: `resonance::nonlinear::duffing_poincare` + """ + ... + +def mathieu_stability(a: float, q: float) -> bool: + """ +Mathieu equation stability of x″ + (a − 2q·cos 2t)x = 0 by the +Floquet criterion |tr M| ≤ 2. + +Rust: `resonance::nonlinear::mathieu_stability` + """ + ... + +def mathieu_stability_chart(a_range: tuple[float, float], q_range: tuple[float, float], n: int) -> list[list[bool]]: + """ +Stability chart over a (rows) × q (columns) grids of n points each. + +Rust: `resonance::nonlinear::mathieu_stability_chart` + """ + ... + +def parametric_resonance_threshold(omega0: float, damping: float, n: int) -> float: + """ +Pump-amplitude threshold h_c of the n-th parametric instability +tongue for x″ + 2λx′ + ω₀²(1 + h·cos(Ωt))x = 0 with Ω = 2ω₀/n, +found by bisecting the damped Floquet spectral radius. + +Panics: +Panics unless n ≥ 1 and the parameters are positive. + +Rust: `resonance::nonlinear::parametric_resonance_threshold` + """ + ... + +def kapitza_pendulum_stable(l: float, g: float, a: float, omega: float) -> bool: + """ +Kapitza inverted pendulum stability: a²ω² > 2·g·l. + +Rust: `resonance::nonlinear::kapitza_pendulum_stable` + """ + ... + +def van_der_pol_simulate(mu: float, omega: float, x0: float, v0: float, t_end: float, dt: float) -> list[tuple[float, float, float]]: + """ +RK4 trajectory (t, x, v) of the van der Pol oscillator +x″ − μ(1 − x²)x′ + ω²x = 0. + +Rust: `resonance::nonlinear::van_der_pol_simulate` + """ + ... + +def van_der_pol_limit_cycle_amplitude(mu: float) -> float: + """ +Limit-cycle amplitude of the van der Pol oscillator (numerically +settled; → 2 as μ → 0). + +Rust: `resonance::nonlinear::van_der_pol_limit_cycle_amplitude` + """ + ... + +def van_der_pol_entrainment_range(mu: float, forcing_amp: float) -> tuple[float, float]: + """ +Adler entrainment (lock-in) band of a weakly forced van der Pol +oscillator with unit natural frequency: ω ∈ 1 ± F/4 for weak +forcing F on the a = 2 limit cycle. + +Rust: `resonance::nonlinear::van_der_pol_entrainment_range` + """ + ... + +def fano_lineshape(omega: float, omega0: float, gamma: float, q: float) -> float: + """ +Fano lineshape (q + ε)²/(1 + ε²), ε = 2(ω − ω₀)/γ (background → 1). + +Rust: `resonance::nonlinear::fano_lineshape` + """ + ... + +def fano_fit(omega: list[float], y: list[float]) -> tuple[float, float, float, float]: + """ +Fit A·(q+ε)²/(1+ε²) to data; returns (ω₀, γ, q, A). + +Panics: +Panics if the fit fails or fewer than 5 points are supplied. + +Rust: `resonance::nonlinear::fano_fit` + """ + ... + +def autoresonance_threshold(alpha: float, sweep_rate: float) -> float: + """ +Autoresonance capture threshold for a swept-drive Duffing-type +oscillator: ε_c = 0.41·(dω/dt)^(3/4)/√|α_nl| (Fajans-Friedland +scaling law). + +Rust: `resonance::nonlinear::autoresonance_threshold` + """ + ... + +def frequency_pulling(f0: float, q: float, coupling_detuning: float) -> float: + """ +Resonator frequency pulling by a detuned load: +f = f₀·(1 + Δ/(2Q)). + +Rust: `resonance::nonlinear::frequency_pulling` + """ + ... + +def injection_locking_range(f0: float, q: float, injection_ratio: float) -> float: + """ +Adler injection-locking half-range Δf = f₀·ρ/(2Q) for injection +amplitude ratio ρ. + +Rust: `resonance::nonlinear::injection_locking_range` + """ + ... + +def stochastic_resonance_snr(a: float, d: float, noise: float, omega: float) -> float: + """ +Weak-signal stochastic-resonance SNR of a bistable well +(McNamara-Wiesenfeld form): √2·(a·ΔU/D²)²·... reduced to the +standard shape SNR ∝ (a²ΔU²/D²)·e^(−ΔU/D), which is maximized at +D = ΔU/2. `omega` enters only beyond the adiabatic limit and is +ignored here. + +Rust: `resonance::nonlinear::stochastic_resonance_snr` + """ + ... + +def hysteresis_loop(f_sweep: list[float], response_up: list[float], response_down: list[float]) -> float: + """ +Enclosed area of a swept-response hysteresis loop (trapezoid of +up-sweep minus down-sweep). + +Panics: +Panics on mismatched lengths. + +Rust: `resonance::nonlinear::hysteresis_loop` + """ + ... + +def harmonic_balance(f: Callable[[float, float, float], float], omega: float, n_harmonics: int, amplitude_guess: float) -> list[complex]: + """ +Generic harmonic balance for x″ + f(x, x′, t) = 0 with f +2π/ω-periodic in t: Newton iteration on the truncated Fourier series +x(t) = c₀ + Σ_k \\[a_k cos kωt + b_k sin kωt\\], collocated at +4·n_harmonics + 2 points. Returns coefficients c_k = a_k − j·b_k +(c₀ real) for k = 0..=n_harmonics. + +Panics: +Panics if the Newton solve fails to converge. + +Rust: `resonance::nonlinear::harmonic_balance` + """ + ... + +def describing_function(nonlinearity: Callable[[float], float], amplitude: float) -> complex: + """ +Sinusoidal-input describing function of a static nonlinearity: +N(A) = (b₁ + j·a₁)/A from the first Fourier component of +f(A·sin θ). + +Rust: `resonance::nonlinear::describing_function` + """ + ... + +def subharmonic_response(alpha: float, beta: float, delta: float, gamma: float, omega: float) -> list[float]: + """ +Steady-state spectral amplitudes of the driven Duffing response at +\\[ω/3, ω/2, ω, 2ω, 3ω\\] — nonzero sub/superharmonic content flags +period-multiplied responses. + +Rust: `resonance::nonlinear::subharmonic_response` + """ + ... diff --git a/bindings/python/python/numeria/resonance/oscillator.pyi b/bindings/python/python/numeria/resonance/oscillator.pyi new file mode 100644 index 0000000..182f0e2 --- /dev/null +++ b/bindings/python/python/numeria/resonance/oscillator.pyi @@ -0,0 +1,140 @@ +""" +The damped harmonic oscillator m·x″ + c·x′ + k·x = F(t): closed-form responses in every damping regime, frequency-domain descriptions, and resonance measurement (Lorentzian fits, Q extraction). +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +class DampedOscillator: + """ +Mass-damper-spring oscillator m·x″ + c·x′ + k·x = F. + +Rust: `resonance::oscillator::DampedOscillator` + """ + def __init__(self, m: float, c: float, k: float) -> None: ... + def natural_frequency(self) -> float: ... + def damping_ratio(self) -> float: ... + def q_factor(self) -> float: ... + def damped_frequency(self) -> Optional[float]: ... + def regime(self) -> Damping: ... + def free_response(self, x0: float, v0: float, t: float) -> float: ... + def steady_state_amplitude(self, f0: float, omega: float) -> float: ... + def steady_state_phase(self, omega: float) -> float: ... + def transfer_function(self, s: complex) -> complex: ... + def frequency_response(self, omega: float) -> complex: ... + def resonant_frequency(self) -> Optional[float]: ... + def bandwidth(self) -> float: ... + def impulse_response(self, t: float) -> float: ... + def step_response(self, t: float) -> float: ... + def forced_response_numeric(self, force: Callable[[float], float], x0: float, v0: float, t_end: float, dt: float) -> list[tuple[float, float, float]]: ... + def energy(self, x: float, v: float) -> float: ... + def decay_time(self, fraction: float) -> float: ... + def logarithmic_decrement(self) -> float: ... + @staticmethod + def from_q(omega0: float, q: float, m: float) -> DampedOscillator: ... + @property + def m(self) -> float: ... + @property + def c(self) -> float: ... + @property + def k(self) -> float: ... + +class Damping: + """ +Damping regime classification. + +Rust: `resonance::oscillator::Damping` + """ + ... + +def bode_plot(tf: Callable[[complex], complex], omega: list[float]) -> tuple[list[float], list[float]]: + """ +Magnitude (dB) and phase (degrees) of a transfer function over a +frequency grid. + +Rust: `resonance::oscillator::bode_plot` + """ + ... + +def nyquist_plot(tf: Callable[[complex], complex], omega: list[float]) -> list[complex]: + """ +Nyquist locus H(jω) over a frequency grid. + +Rust: `resonance::oscillator::nyquist_plot` + """ + ... + +def lorentzian(omega: float, omega0: float, gamma: float) -> float: + """ +Area-normalized Lorentzian lineshape +(1/π)·(γ/2)/((ω−ω₀)² + (γ/2)²). + +Rust: `resonance::oscillator::lorentzian` + """ + ... + +def lorentzian_fit(omega: list[float], y: list[float]) -> tuple[float, float, float]: + """ +Fit y(ω) ≈ A·(γ/2)²/((ω−ω₀)² + (γ/2)²) (peak-amplitude Lorentzian) +by Levenberg-Marquardt; returns (ω₀, γ, A). + +Panics: +Panics if the fit fails to converge or fewer than 4 points are given. + +Rust: `resonance::oscillator::lorentzian_fit` + """ + ... + +def q_from_ringdown(x: list[float], fs: float) -> tuple[float, float]: + """ +Estimate (f₀ Hz, Q) from a free ring-down record: frequency from +interpolated zero crossings, decay rate from a log-linear fit to the +rectified peaks. + +Panics: +Panics if the record has fewer than 4 zero crossings. + +Rust: `resonance::oscillator::q_from_ringdown` + """ + ... + +def q_from_spectrum(f: list[float], psd: list[float]) -> tuple[float, float]: + """ +Estimate (f₀, Q) from a power spectrum by the −3 dB method with +linear interpolation of the half-power crossings. + +Panics: +Panics on an empty spectrum. + +Rust: `resonance::oscillator::q_from_spectrum` + """ + ... + +def resonance_curve(osc: DampedOscillator | Sequence[float], omega: list[float]) -> list[float]: + """ +Steady-state amplitude of the oscillator (unit force) at each ω. + +Rust: `resonance::oscillator::resonance_curve` + """ + ... + +def transmissibility(omega_ratio: float, zeta: float) -> float: + """ +Base-excitation transmissibility at frequency ratio r = ω/ω₀: +√((1+(2ζr)²)/((1−r²)² + (2ζr)²)). + +Rust: `resonance::oscillator::transmissibility` + """ + ... + +def quality_factor_combined(qs: list[float]) -> float: + """ +Combined quality factor of independent loss channels: +1/Q = Σ 1/Qᵢ. + +Rust: `resonance::oscillator::quality_factor_combined` + """ + ... diff --git a/bindings/python/python/numeria/resonance/structural.pyi b/bindings/python/python/numeria/resonance/structural.pyi new file mode 100644 index 0000000..05f680b --- /dev/null +++ b/bindings/python/python/numeria/resonance/structural.pyi @@ -0,0 +1,111 @@ +""" +Structural dynamics: finite-element bars and beams, modal analysis with general (consistent) mass matrices, Rayleigh damping, implicit time integration (Newmark-β, HHT-α), model reduction, response spectra, and experimental modal analysis tools. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.linalg.matrix import Matrix + +class ModalModel: + """ +Structural model M·x″ + C·x′ + K·x = F(t) with full matrices. + +Rust: `resonance::structural::ModalModel` + """ + def __init__(self, m: Matrix | Sequence[Sequence[float]], c: Matrix | Sequence[Sequence[float]], k: Matrix | Sequence[Sequence[float]]) -> None: ... + @staticmethod + def from_fem_1d_bar(n_elem: int, length: float, area: float, young: float, rho: float) -> ModalModel: ... + @staticmethod + def from_fem_beam(n_elem: int, length: float, young: float, i_area: float, rho: float, area: float) -> ModalModel: ... + @staticmethod + def from_lumped(masses: list[float], springs: list[tuple[int, int, float]], dampers: list[tuple[int, int, float]]) -> ModalModel: ... + def rayleigh_damping(self, alpha: float, beta: float) -> None: ... + def rayleigh_from_ratios(self, zeta1: float, f1: float, zeta2: float, f2: float) -> None: ... + def modes(self) -> tuple[list[float], Matrix]: ... + def damped_modes(self) -> list[tuple[complex, list[complex]]]: ... + def modal_damping_ratios(self) -> list[float]: ... + def frf(self, i: int, j: int, omega: float) -> complex: ... + def frf_matrix(self, omega: float) -> Matrix: ... + def newmark_beta(self, force: Callable[[float], list[float]], x0: list[float], v0: list[float], t_end: float, dt: float, beta: float, gamma: float) -> list[list[float]]: ... + def hht_alpha(self, force: Callable[[float], list[float]], x0: list[float], v0: list[float], t_end: float, dt: float, alpha: float) -> list[list[float]]: ... + def modal_truncation(self, n_modes: int) -> ModalModel: ... + def guyan_reduction(self, master_dofs: list[int]) -> ModalModel: ... + def response_spectrum(self, ground_accel: list[float], dt: float, zeta: float) -> list[tuple[float, float]]: ... + @staticmethod + def mac(phi1: list[float], phi2: list[float]) -> float: ... + def campbell_diagram(self, rpm_range: tuple[float, float], n: int, orders: list[float]) -> list[tuple[float, list[float]]]: ... + def critical_speeds(self, orders: list[float]) -> list[float]: ... + def resonance_margins(self, excitation_freqs: list[float]) -> list[float]: ... + @property + def m(self) -> Matrix: ... + @property + def c(self) -> Matrix: ... + @property + def k(self) -> Matrix: ... + +def experimental_modal_peak_picking(frf: list[complex], freqs: list[float]) -> list[tuple[float, float]]: + """ +Peak-picking experimental modal analysis on a receptance FRF: +(natural frequency, damping ratio) per resolved peak via half-power +bandwidths. + +Rust: `resonance::structural::experimental_modal_peak_picking` + """ + ... + +def half_power_bandwidth(frf_mag: list[float], freqs: list[float], peak_idx: int) -> float: + """ +Half-power (−3 dB) bandwidth around the FRF magnitude peak at +`peak_idx`, with linear interpolation; 0 when a crossing is missing. + +Rust: `resonance::structural::half_power_bandwidth` + """ + ... + +def circle_fit(frf: list[complex], freqs: list[float], window: int) -> tuple[float, float]: + """ +Kasa circle fit of an FRF arc in the Nyquist plane around a +resonance; returns (f₀, ζ) using the angular-sweep-rate maximum for +f₀ and the standard circle-fit damping formula. + +Panics: +Panics if fewer than 5 points are given. + +Rust: `resonance::structural::circle_fit` + """ + ... + +def operational_deflection_shape(responses: list[list[float]], omega: float, fs: float) -> list[complex]: + """ +Operational deflection shape: the complex amplitude of every +measured channel at frequency ω (single-bin correlation at fs). + +Rust: `resonance::structural::operational_deflection_shape` + """ + ... + +def stochastic_subspace_identification(outputs: list[list[float]], fs: float, order: int) -> list[tuple[float, float]]: + """ +Covariance-driven stochastic subspace identification: output-only +modal frequencies and damping ratios from response channels sampled +at fs. `order` is the state dimension (≥ 2 per expected mode). + +Panics: +Panics if the SVD or eigen machinery fails, or the data is shorter +than 4·order. + +Rust: `resonance::structural::stochastic_subspace_identification` + """ + ... + +def shock_response_spectrum(accel: list[float], dt: float, freqs: list[float], zeta: float) -> list[float]: + """ +Shock response spectrum: peak absolute SDOF displacement response at +each requested natural frequency (Hz) for a base acceleration pulse. + +Rust: `resonance::structural::shock_response_spectrum` + """ + ... diff --git a/bindings/python/python/numeria/rf.pyi b/bindings/python/python/numeria/rf.pyi new file mode 100644 index 0000000..1570824 --- /dev/null +++ b/bindings/python/python/numeria/rf.pyi @@ -0,0 +1,233 @@ +""" +Radio-frequency engineering: links, lines and noise. Link budgets built from free-space path loss, the Friis transmission equation, antenna gain and effective area, EIRP, beamwidth, directivity and fade margin. Transmission lines: characteristic impedance of coax, velocity factor, guide wavelength, and the mismatch quantities -- VSWR, return loss, mismatch loss. Conductors are covered by the skin depth `δ = √(2ρ/ωμ)`, which is why RF current flows in a thin surface layer. Noise and units: thermal noise power and floor in dBm, signal-to-noise ratio, the Shannon capacity of the resulting channel, and conversions between watts, dBm, ratios and decibels. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def wavelength_to_frequency(wavelength: float) -> float: + """ +Wavelength to frequency: f = c / λ + +Rust: `rf::wavelength_to_frequency` + """ + ... + +def frequency_to_wavelength(frequency: float) -> float: + """ +Frequency to wavelength: λ = c / f + +Rust: `rf::frequency_to_wavelength` + """ + ... + +def frequency_to_energy(frequency: float) -> float: + """ +Photon energy from frequency: E = hf + +Rust: `rf::frequency_to_energy` + """ + ... + +def free_space_path_loss(distance: float, frequency: float) -> float: + """ +Free-space path loss in dB: FSPL = 20log₁₀(d) + 20log₁₀(f) + 20log₁₀(4π/c) + +Rust: `rf::free_space_path_loss` + """ + ... + +def friis_received_power(pt: float, gt: float, gr: float, wavelength: float, distance: float) -> float: + """ +Friis transmission equation (linear): Pr = Pt × Gt × Gr × (λ/(4πd))² + +Rust: `rf::friis_received_power` + """ + ... + +def link_budget_db(pt_dbm: float, gt_dbi: float, gr_dbi: float, path_loss_db: float) -> float: + """ +Link budget in dB: Pr = Pt + Gt + Gr - PathLoss + +Rust: `rf::link_budget_db` + """ + ... + +def skin_depth_conductor(frequency: float, permeability: float, conductivity: float) -> float: + """ +Skin depth in a conductor: δ = 1 / √(πfμσ) + +Rust: `rf::skin_depth_conductor` + """ + ... + +def fade_margin_db(transmitted_dbm: float, received_dbm: float, sensitivity_dbm: float) -> float: + """ +Fade margin: FM = received_dBm - sensitivity_dBm + +Rust: `rf::fade_margin_db` + """ + ... + +def antenna_gain_from_area(effective_area: float, wavelength: float) -> float: + """ +Antenna gain from effective area: G = 4πA_e / λ² + +Rust: `rf::antenna_gain_from_area` + """ + ... + +def effective_area_from_gain(gain: float, wavelength: float) -> float: + """ +Effective aperture from gain: A_e = Gλ² / (4π) + +Rust: `rf::effective_area_from_gain` + """ + ... + +def half_wave_dipole_gain() -> float: + """ +Returns the half-wave dipole gain (linear): G ≈ 1.64 (2.15 dBi). + +Rust: `rf::half_wave_dipole_gain` + """ + ... + +def eirp(power: float, gain: float) -> float: + """ +Effective isotropic radiated power: EIRP = P × G + +Rust: `rf::eirp` + """ + ... + +def beamwidth_approximate(wavelength: float, aperture: float) -> float: + """ +Approximate antenna beamwidth in degrees: θ ≈ 70λ / D. + +Rust: `rf::beamwidth_approximate` + """ + ... + +def antenna_directivity(gain: float, efficiency: float) -> float: + """ +Antenna directivity from gain and efficiency: D = G / η + +Rust: `rf::antenna_directivity` + """ + ... + +def characteristic_impedance_coax(outer_radius: float, inner_radius: float, permittivity_rel: float) -> float: + """ +Characteristic impedance of coaxial cable: Z₀ = (138/√εr) × log₁₀(D/d). + +Rust: `rf::characteristic_impedance_coax` + """ + ... + +def velocity_factor(permittivity_rel: float) -> float: + """ +Velocity factor: VF = 1 / √εr + +Rust: `rf::velocity_factor` + """ + ... + +def wavelength_in_line(free_space_wavelength: float, velocity_factor: float) -> float: + """ +Wavelength in a transmission line: λ_line = λ₀ × VF + +Rust: `rf::wavelength_in_line` + """ + ... + +def vswr(reflection_coeff: float) -> float: + """ +Voltage standing wave ratio: VSWR = (1 + |Γ|) / (1 - |Γ|) + +Rust: `rf::vswr` + """ + ... + +def return_loss(reflection_coeff: float) -> float: + """ +Return loss in dB: RL = -20log₁₀(|Γ|) + +Rust: `rf::return_loss` + """ + ... + +def mismatch_loss(vswr: float) -> float: + """ +Mismatch loss in dB: ML = -10log₁₀(1 - ((VSWR-1)/(VSWR+1))²) + +Rust: `rf::mismatch_loss` + """ + ... + +def dbm_to_watts(dbm: float) -> float: + """ +Convert dBm to watts: P = 10^((dBm - 30) / 10) + +Rust: `rf::dbm_to_watts` + """ + ... + +def watts_to_dbm(watts: float) -> float: + """ +Convert watts to dBm: dBm = 10log₁₀(P) + 30 + +Rust: `rf::watts_to_dbm` + """ + ... + +def db_to_ratio(db: float) -> float: + """ +Convert dB to linear ratio: ratio = 10^(dB / 10) + +Rust: `rf::db_to_ratio` + """ + ... + +def ratio_to_db(ratio: float) -> float: + """ +Convert linear ratio to dB: dB = 10log₁₀(ratio) + +Rust: `rf::ratio_to_db` + """ + ... + +def noise_power(bandwidth: float, temperature: float) -> float: + """ +Thermal noise power: N = k_B × T × B + +Rust: `rf::noise_power` + """ + ... + +def snr_db(signal_power: float, noise_power: float) -> float: + """ +Signal-to-noise ratio in dB: SNR = 10log₁₀(S / N) + +Rust: `rf::snr_db` + """ + ... + +def thermal_noise_floor_dbm(bandwidth: float, temperature: float) -> float: + """ +Thermal noise floor in dBm: 10log₁₀(k_B × T × B) + 30 + +Rust: `rf::thermal_noise_floor_dbm` + """ + ... + +def shannon_capacity(bandwidth: float, snr_linear: float) -> float: + """ +Shannon-Hartley channel capacity: C = B × log₂(1 + SNR) + +Rust: `rf::shannon_capacity` + """ + ... diff --git a/bindings/python/python/numeria/signal_processing.pyi b/bindings/python/python/numeria/signal_processing.pyi new file mode 100644 index 0000000..9153221 --- /dev/null +++ b/bindings/python/python/numeria/signal_processing.pyi @@ -0,0 +1,149 @@ +""" +Time-domain signal operations and test waveforms. Convolution, cross- and autocorrelation, normalization, windowing, and the simple smoothers -- moving average, exponential moving average, and the median filter, which unlike the other two removes impulsive noise without smearing an edge. Waveform generators (sine, square, sawtooth, triangle, noise, chirp) provide test signals. This module is the elementary layer and re-exports the pieces of `transforms` and `dsp` most often wanted alongside it. For FFTs of any length go to `transforms::fft`; for filter *design* go to `dsp`. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.statistics.distributions import Exponential +from numeria.dsp.windows import blackman_window as blackman_window +from numeria.transforms.fft import fft as fft +from numeria.transforms.fft import fft_convolve as fft_convolve +from numeria.dsp.iir import first_order_highpass as first_order_highpass +from numeria.dsp.iir import first_order_lowpass as first_order_lowpass +from numeria.dsp.windows import hamming_window as hamming_window +from numeria.dsp.windows import hann_window as hann_window +from numeria.transforms.fft import ifft as ifft +from numeria.transforms.fft import next_power_of_two as next_power_of_two +from numeria.dsp.windows import rectangular_window as rectangular_window +from numeria.transforms.fft import rfft as rfft + +def convolve(signal: list[float], kernel: list[float]) -> list[float]: + """ +Linear convolution of signal with kernel: `y[n] = Σ s[i]·k[n-i]` + +Rust: `signal_processing::convolve` + """ + ... + +def cross_correlate(x: list[float], y: list[float]) -> list[float]: + """ +Cross-correlation of x and y via convolution with time-reversed y + +Rust: `signal_processing::cross_correlate` + """ + ... + +def auto_correlate(signal: list[float]) -> list[float]: + """ +Auto-correlation of a signal: cross-correlation of the signal with itself + +Rust: `signal_processing::auto_correlate` + """ + ... + +def normalize_signal(signal: MutableSequence[float]) -> None: + """ +Normalize signal amplitude to [-1, 1] by dividing by peak absolute value + +Rust: `signal_processing::normalize_signal` + """ + ... + +def apply_window(signal: list[float], window: list[float]) -> list[float]: + """ +Element-wise multiplication of signal by window coefficients + +Rust: `signal_processing::apply_window` + """ + ... + +def moving_average(signal: list[float], window_size: int) -> list[float]: + """ +Simple moving average filter with specified window size + +Rust: `signal_processing::moving_average` + """ + ... + +def exponential_moving_average(signal: list[float], alpha: float) -> list[float]: + """ +Exponential moving average filter: `y[n] = α·x[n] + (1-α)·y[n-1]` + +Rust: `signal_processing::exponential_moving_average` + """ + ... + +def median_filter(signal: list[float], window_size: int) -> list[float]: + """ +Median filter for impulse noise removal with specified window size + +Rust: `signal_processing::median_filter` + """ + ... + +def sine_wave(frequency: float, sample_rate: float, duration: float, amplitude: float) -> list[float]: + """ +Generate a sine wave: `x[n] = A·sin(2πf·n/fs)` for n samples over given duration + +Rust: `signal_processing::sine_wave` + """ + ... + +def square_wave(frequency: float, sample_rate: float, duration: float, amplitude: float) -> list[float]: + """ +Generate a square wave: +A for first half-period, -A for second half + +Rust: `signal_processing::square_wave` + """ + ... + +def sawtooth_wave(frequency: float, sample_rate: float, duration: float, amplitude: float) -> list[float]: + """ +Generate a sawtooth wave: linearly ramps from -A to +A each period + +Rust: `signal_processing::sawtooth_wave` + """ + ... + +def white_noise(n: int, amplitude: float, seed: int) -> list[float]: + """ +Generate deterministic pseudo-random white noise using a linear congruential generator + +Rust: `signal_processing::white_noise` + """ + ... + +def zero_crossings(signal: list[float]) -> int: + """ +Count the number of zero crossings (sign changes) in the signal + +Rust: `signal_processing::zero_crossings` + """ + ... + +def rms_level(signal: list[float]) -> float: + """ +Root mean square level: RMS = sqrt(Σx²/n) + +Rust: `signal_processing::rms_level` + """ + ... + +def peak_to_peak(signal: list[float]) -> float: + """ +Peak-to-peak amplitude: max(x) - min(x) + +Rust: `signal_processing::peak_to_peak` + """ + ... + +def crest_factor(signal: list[float]) -> float: + """ +Crest factor: ratio of peak absolute value to RMS level + +Rust: `signal_processing::crest_factor` + """ + ... diff --git a/bindings/python/python/numeria/sim/__init__.pyi b/bindings/python/python/numeria/sim/__init__.pyi new file mode 100644 index 0000000..5d4c1e2 --- /dev/null +++ b/bindings/python/python/numeria/sim/__init__.pyi @@ -0,0 +1,12 @@ +""" +Time-stepping simulation engines. Where the rest of the crate evaluates a relation, these advance a state forward in time: `rigid_body` for 3-D dynamics with quaternion orientation and Euler's equations, `fluid_sim` for shallow water and 2-D incompressible Euler, `heat_sim` for conduction and convection-diffusion, `wave_sim` for the wave equation with Mur absorbing boundaries, `em_sim` for FDTD electromagnetics, and `cloth_sim` for Verlet cloth and rope. These are compact, readable integrators intended for interactive use and for seeing the physics behave. For the research-grade schemes -- Riemann solvers, WENO, lattice Boltzmann, SPH -- see `cfd`; for finite elements and a Yee-grid FDTD with PML see `fem`. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from . import cloth_sim, em_sim, fluid_sim, heat_sim, rigid_body, wave_sim + + diff --git a/bindings/python/python/numeria/sim/cloth_sim.pyi b/bindings/python/python/numeria/sim/cloth_sim.pyi new file mode 100644 index 0000000..c4da57f --- /dev/null +++ b/bindings/python/python/numeria/sim/cloth_sim.pyi @@ -0,0 +1,91 @@ +""" +Verlet cloth and rope with spring constraints. Particles are advanced by position Verlet, which stores the previous position rather than a velocity: it is stable under stiff constraints and conserves energy far better than explicit Euler at the same step size, because velocity is inferred from the positions rather than integrated separately. Structural, shear and bend springs are then satisfied by iterated position projection -- more iterations gives a stiffer cloth -- with pinning, sphere and floor collision, and wind and gravity forces. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.math import Vec3 + +class MassSpringSystem: + """ + +Rust: `sim::cloth_sim::MassSpringSystem` + """ + def __init__(self, gravity: Vec3 | Sequence[float]) -> None: ... + def add_particle(self, p: Particle) -> int: ... + def add_spring(self, s: Spring) -> None: ... + def step_verlet(self, dt: float) -> None: ... + def step_with_constraints(self, dt: float, iterations: int) -> None: ... + def total_energy(self, dt: float) -> float: ... + def total_momentum(self, dt: float) -> Vec3: ... + @property + def particles(self) -> list[Particle]: ... + @property + def springs(self) -> list[Spring]: ... + @property + def gravity(self) -> Vec3: ... + @property + def time(self) -> float: ... + +class Particle: + """ + +Rust: `sim::cloth_sim::Particle` + """ + def __init__(self, position: Vec3 | Sequence[float], mass: float) -> None: ... + @staticmethod + def new_pinned(position: Vec3 | Sequence[float]) -> Particle: ... + @property + def position(self) -> Vec3: ... + @property + def previous_position(self) -> Vec3: ... + @property + def acceleration(self) -> Vec3: ... + @property + def mass(self) -> float: ... + @property + def pinned(self) -> bool: ... + +class Spring: + """ + +Rust: `sim::cloth_sim::Spring` + """ + def __init__(self, a: int, b: int, rest_length: float, stiffness: float, damping: float) -> None: ... + @property + def particle_a(self) -> int: ... + @property + def particle_b(self) -> int: ... + @property + def rest_length(self) -> float: ... + @property + def stiffness(self) -> float: ... + @property + def damping(self) -> float: ... + +def create_cloth_grid(width: int, height: int, spacing: float, mass: float, stiffness: float, damping: float) -> MassSpringSystem: + """ +Creates a rectangular cloth grid with structural and shear springs. + +Particles are laid out in the XY plane. The top row (y = (height-1)*spacing) +is pinned. Gravity points in -Y. + +Structural springs: horizontal and vertical neighbors (rest_length = spacing). +Shear springs: diagonal neighbors (rest_length = spacing * sqrt(2)). + +Rust: `sim::cloth_sim::create_cloth_grid` + """ + ... + +def create_rope(n_particles: int, spacing: float, mass: float, stiffness: float, damping: float) -> MassSpringSystem: + """ +Creates a 1D rope (linear chain of particles connected by springs). + +The first particle is pinned. Gravity points in -Y. + +Rust: `sim::cloth_sim::create_rope` + """ + ... diff --git a/bindings/python/python/numeria/sim/em_sim.pyi b/bindings/python/python/numeria/sim/em_sim.pyi new file mode 100644 index 0000000..d1fdec9 --- /dev/null +++ b/bindings/python/python/numeria/sim/em_sim.pyi @@ -0,0 +1,80 @@ +""" +FDTD electromagnetic simulation in one and two dimensions. Explicit leapfrog on a Yee-style grid: the electric and magnetic fields are staggered by half a cell and half a time step, so each is updated from the curl of the other and the scheme is second-order accurate with no matrix to solve. Supports dielectric media, hard and soft sources, PEC (perfectly conducting) walls and Mur first-order absorbing boundaries. Stability requires the Courant condition, and the limit is set by the fastest medium in the grid -- that is, the smallest relative permittivity. For a Yee grid with Berenger split-field PML, photonic band gaps and waveguide cutoff, see `fem::fdtd`. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +class Fdtd1D: + """ + +Rust: `sim::em_sim::Fdtd1D` + """ + def __init__(self, nx: int, dx: float) -> None: ... + def set_material(self, start: int, end: int, epsilon_r: float, mu_r: float, sigma: float) -> None: ... + def step(self) -> None: ... + def add_source_soft(self, position: int, value: float) -> None: ... + def add_source_hard(self, position: int, value: float) -> None: ... + @staticmethod + def gaussian_pulse(t: float, t0: float, spread: float) -> float: ... + @staticmethod + def sinusoidal_source(t: float, frequency: float) -> float: ... + def apply_abc_mur(self) -> None: ... + def total_energy(self) -> float: ... + @staticmethod + def stable_dt_for_dx(dx: float) -> float: ... + @property + def ez(self) -> list[float]: ... + @property + def hy(self) -> list[float]: ... + @property + def nx(self) -> int: ... + @property + def dx(self) -> float: ... + @property + def dt(self) -> float: ... + @property + def epsilon(self) -> list[float]: ... + @property + def mu(self) -> list[float]: ... + @property + def conductivity(self) -> list[float]: ... + @property + def time(self) -> float: ... + @property + def time_step(self) -> int: ... + +class Fdtd2D: + """ + +Rust: `sim::em_sim::Fdtd2D` + """ + def __init__(self, nx: int, ny: int, dx: float, dy: float) -> None: ... + def step(self) -> None: ... + def add_source(self, i: int, j: int, value: float) -> None: ... + def total_energy(self) -> float: ... + @staticmethod + def stable_dt_for_grid(dx: float, dy: float) -> float: ... + @property + def ez(self) -> list[float]: ... + @property + def hx(self) -> list[float]: ... + @property + def hy(self) -> list[float]: ... + @property + def nx(self) -> int: ... + @property + def ny(self) -> int: ... + @property + def dx(self) -> float: ... + @property + def dy(self) -> float: ... + @property + def dt(self) -> float: ... + @property + def epsilon(self) -> list[float]: ... + @property + def time_step(self) -> int: ... diff --git a/bindings/python/python/numeria/sim/fluid_sim.pyi b/bindings/python/python/numeria/sim/fluid_sim.pyi new file mode 100644 index 0000000..5ae5523 --- /dev/null +++ b/bindings/python/python/numeria/sim/fluid_sim.pyi @@ -0,0 +1,80 @@ +""" +Compact fluid solvers: column, shallow water, and 2-D Euler. A draining column for the simplest case, a 1-D shallow-water solver, and a 2-D incompressible Euler solver that advects velocity and then restores `∇·u = 0` by pressure projection -- subtracting the gradient of a pressure field found by solving a Poisson equation, which is what makes the result divergence-free. Written to be read and to run interactively. For well-balanced schemes, Riemann solvers and the rest of the research-grade machinery see `cfd`. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +class ColumnFluid: + """ + +Rust: `sim::fluid_sim::ColumnFluid` + """ + def __init__(self, width: int, dx: float, density: float, g: float) -> None: ... + def set_height(self, col: int, h: float) -> None: ... + def step(self, dt: float) -> None: ... + def total_volume(self) -> float: ... + @property + def heights(self) -> list[float]: ... + @property + def width(self) -> int: ... + @property + def dx(self) -> float: ... + @property + def density(self) -> float: ... + @property + def g(self) -> float: ... + +class EulerFluid2D: + """ + +Rust: `sim::fluid_sim::EulerFluid2D` + """ + def __init__(self, nx: int, ny: int, dx: float, dy: float, density: float) -> None: ... + def set_velocity(self, i: int, j: int, vx: float, vy: float) -> None: ... + def step(self, dt: float, gravity_x: float, gravity_y: float) -> None: ... + def divergence(self) -> float: ... + def kinetic_energy(self) -> float: ... + @property + def vx(self) -> list[float]: ... + @property + def vy(self) -> list[float]: ... + @property + def pressure(self) -> list[float]: ... + @property + def nx(self) -> int: ... + @property + def ny(self) -> int: ... + @property + def dx(self) -> float: ... + @property + def dy(self) -> float: ... + @property + def density(self) -> float: ... + +class ShallowWater1D: + """ + +Rust: `sim::fluid_sim::ShallowWater1D` + """ + def __init__(self, nx: int, dx: float, g: float) -> None: ... + def velocity(self, i: int) -> float: ... + def step_hll(self, dt: float) -> None: ... + def step_lax_friedrichs(self, dt: float) -> None: ... + def max_wave_speed(self) -> float: ... + def stable_dt(self) -> float: ... + def total_volume(self) -> float: ... + def total_energy(self) -> float: ... + @property + def h(self) -> list[float]: ... + @property + def hu(self) -> list[float]: ... + @property + def nx(self) -> int: ... + @property + def dx(self) -> float: ... + @property + def g(self) -> float: ... diff --git a/bindings/python/python/numeria/sim/heat_sim.pyi b/bindings/python/python/numeria/sim/heat_sim.pyi new file mode 100644 index 0000000..c885d32 --- /dev/null +++ b/bindings/python/python/numeria/sim/heat_sim.pyi @@ -0,0 +1,105 @@ +""" +Heat conduction and convection-diffusion on a grid. Explicit finite differences in two and three dimensions, with Dirichlet and Neumann boundaries, sources, and an advection term for convection-diffusion. Explicit stepping is only conditionally stable: the step must satisfy `α Δt / Δx² ≤ 1/4` in 2-D and `1/6` in 3-D, so halving the grid spacing quarters the allowable time step. The stability limit is provided as a function rather than left to the caller to remember. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +class ConvectionDiffusion1D: + """ +1D convection-diffusion (advection-diffusion) solver. + +PDE: ∂T/∂t + v·∂T/∂x = α·∂²T/∂x² + +Uses first-order upwind for the advection term and central differencing +for the diffusion term. + +Rust: `sim::heat_sim::ConvectionDiffusion1D` + """ + def __init__(self, nx: int, dx: float, velocity: float, diffusivity: float) -> None: ... + def step_upwind(self, dt: float) -> None: ... + def peclet_number(self) -> float: ... + def stable_dt(self) -> float: ... + @property + def field(self) -> list[float]: ... + @property + def nx(self) -> int: ... + @property + def dx(self) -> float: ... + @property + def velocity(self) -> float: ... + @property + def diffusivity(self) -> float: ... + +class HeatConduction2D: + """ +Heat conduction and convection-diffusion on a grid. + +Explicit finite differences in two and three dimensions, with Dirichlet +and Neumann boundaries, sources, and an advection term for +convection-diffusion. + +Explicit stepping is only conditionally stable: the step must satisfy +`α Δt / Δx² ≤ 1/4` in 2-D and `1/6` in 3-D, so halving the grid spacing +quarters the allowable time step. The stability limit is provided as a +function rather than left to the caller to remember. +2D heat conduction on a uniform Cartesian grid with Dirichlet boundaries. + +Rust: `sim::heat_sim::HeatConduction2D` + """ + def __init__(self, nx: int, ny: int, dx: float, dy: float, diffusivity: float) -> None: ... + def set_temperature(self, i: int, j: int, temp: float) -> None: ... + def get_temperature(self, i: int, j: int) -> float: ... + def step_explicit(self, dt: float) -> None: ... + def step_implicit_jacobi(self, dt: float, iterations: int) -> None: ... + def stable_dt(self) -> float: ... + def total_energy(self) -> float: ... + def max_temperature(self) -> float: ... + def min_temperature(self) -> float: ... + def average_temperature(self) -> float: ... + def step_with_source(self, dt: float, sources: list[float]) -> None: ... + @property + def temperature(self) -> list[float]: ... + @property + def nx(self) -> int: ... + @property + def ny(self) -> int: ... + @property + def dx(self) -> float: ... + @property + def dy(self) -> float: ... + @property + def diffusivity(self) -> float: ... + +class HeatConduction3D: + """ +3D heat conduction on a uniform Cartesian grid with Dirichlet boundaries. + +Rust: `sim::heat_sim::HeatConduction3D` + """ + def __init__(self, nx: int, ny: int, nz: int, dx: float, dy: float, dz: float, diffusivity: float) -> None: ... + def set_temperature(self, i: int, j: int, k: int, temp: float) -> None: ... + def get_temperature(self, i: int, j: int, k: int) -> float: ... + def step_explicit(self, dt: float) -> None: ... + def stable_dt(self) -> float: ... + def total_energy(self) -> float: ... + def average_temperature(self) -> float: ... + @property + def temperature(self) -> list[float]: ... + @property + def nx(self) -> int: ... + @property + def ny(self) -> int: ... + @property + def nz(self) -> int: ... + @property + def dx(self) -> float: ... + @property + def dy(self) -> float: ... + @property + def dz(self) -> float: ... + @property + def diffusivity(self) -> float: ... diff --git a/bindings/python/python/numeria/sim/rigid_body.pyi b/bindings/python/python/numeria/sim/rigid_body.pyi new file mode 100644 index 0000000..56d5d28 --- /dev/null +++ b/bindings/python/python/numeria/sim/rigid_body.pyi @@ -0,0 +1,76 @@ +""" +Rigid body dynamics in three dimensions. State is position, linear velocity, orientation as a unit quaternion, and angular velocity. Rotation uses a quaternion rather than Euler angles because it composes without gimbal lock and stays well conditioned under renormalization. Angular motion follows Euler's equations, which carry the `ω × Iω` term -- the reason a freely spinning body with three distinct moments of inertia tumbles rather than spinning steadily about an intermediate axis. Includes inertia tensors for the standard bodies, force and torque accumulation, sphere-sphere collision detection, and impulse-based collision response with restitution. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.quaternion import Quaternion +from numeria.math import Vec3 + +class RigidBody: + """ + +Rust: `sim::rigid_body::RigidBody` + """ + def __init__(self, mass: float, inertia: list[float]) -> None: ... + @staticmethod + def new_sphere(mass: float, radius: float) -> RigidBody: ... + @staticmethod + def new_box(mass: float, wx: float, wy: float, wz: float) -> RigidBody: ... + @staticmethod + def new_cylinder(mass: float, radius: float, height: float) -> RigidBody: ... + def apply_force(self, force: Vec3 | Sequence[float]) -> None: ... + def apply_force_at_point(self, force: Vec3 | Sequence[float], point: Vec3 | Sequence[float]) -> None: ... + def apply_torque(self, torque: Vec3 | Sequence[float]) -> None: ... + def clear_forces(self) -> None: ... + def step(self, dt: float) -> None: ... + def kinetic_energy(self) -> float: ... + def angular_momentum(self) -> Vec3: ... + def local_to_world(self, local_point: Vec3 | Sequence[float]) -> Vec3: ... + def world_to_local(self, world_point: Vec3 | Sequence[float]) -> Vec3: ... + def velocity_at_point(self, world_point: Vec3 | Sequence[float]) -> Vec3: ... + @property + def position(self) -> Vec3: ... + @property + def velocity(self) -> Vec3: ... + @property + def mass(self) -> float: ... + @property + def orientation(self) -> Quaternion: ... + @property + def angular_velocity(self) -> Vec3: ... + @property + def inertia(self) -> list[float]: ... + +class RigidBodySystem: + """ + +Rust: `sim::rigid_body::RigidBodySystem` + """ + def __init__(self, gravity: Vec3 | Sequence[float]) -> None: ... + def step(self, dt: float) -> None: ... + def total_energy(self) -> float: ... + def total_momentum(self) -> Vec3: ... + @property + def gravity(self) -> Vec3: ... + @property + def time(self) -> float: ... + +def sphere_sphere_collision(a: RigidBody, radius_a: float, b: RigidBody, radius_b: float) -> Optional[tuple[Vec3, float]]: + """ +Detect sphere-sphere overlap, returning (contact_normal, penetration_depth) or None. + +Rust: `sim::rigid_body::sphere_sphere_collision` + """ + ... + +def resolve_collision(a: RigidBody, b: RigidBody, normal: Vec3 | Sequence[float], restitution: float) -> None: + """ +Resolve a collision between two rigid bodies using impulse-based response. + +Rust: `sim::rigid_body::resolve_collision` + """ + ... diff --git a/bindings/python/python/numeria/sim/wave_sim.pyi b/bindings/python/python/numeria/sim/wave_sim.pyi new file mode 100644 index 0000000..58ee936 --- /dev/null +++ b/bindings/python/python/numeria/sim/wave_sim.pyi @@ -0,0 +1,79 @@ +""" +The wave equation in one and two dimensions. Explicit second-order finite differences on `∂²u/∂t² = c²∇²u`, with fixed, free, and Mur first-order absorbing boundaries. The absorbing condition passes a normally-incident wave out of the domain exactly and degrades with the angle of incidence. Stability requires the Courant number `r = cΔt/Δx` to satisfy `r ≤ 1` in 1-D and `r ≤ 1/√2` in 2-D. At exactly `r = 1` in one dimension the scheme is an exact shift and has no dispersion error at all. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +class WaveEquation1D: + """ +The wave equation in one and two dimensions. + +Explicit second-order finite differences on `∂²u/∂t² = c²∇²u`, with +fixed, free, and Mur first-order absorbing boundaries. The absorbing +condition passes a normally-incident wave out of the domain exactly and +degrades with the angle of incidence. + +Stability requires the Courant number `r = cΔt/Δx` to satisfy `r ≤ 1` +in 1-D and `r ≤ 1/√2` in 2-D. At exactly `r = 1` in one dimension the +scheme is an exact shift and has no dispersion error at all. +1D wave equation solver on a uniform grid with fixed (Dirichlet) endpoints. + +Rust: `sim::wave_sim::WaveEquation1D` + """ + def __init__(self, nx: int, dx: float, wave_speed: float) -> None: ... + def set_initial(self, displacement: list[float], velocity: list[float], dt: float) -> None: ... + def step(self, dt: float) -> None: ... + def step_absorbing(self, dt: float) -> None: ... + def courant_number(self, dt: float) -> float: ... + def stable_dt(self) -> float: ... + def total_energy(self, dt: float) -> float: ... + def add_source(self, position: int, amplitude: float) -> None: ... + @property + def u_current(self) -> list[float]: ... + @property + def u_previous(self) -> list[float]: ... + @property + def nx(self) -> int: ... + @property + def dx(self) -> float: ... + @property + def wave_speed(self) -> float: ... + @property + def time(self) -> float: ... + +class WaveEquation2D: + """ +2D wave equation solver on a uniform grid with fixed (Dirichlet) boundaries. + +Grid layout: row-major, index = j * nx + i (i along x, j along y). + +Rust: `sim::wave_sim::WaveEquation2D` + """ + def __init__(self, nx: int, ny: int, dx: float, dy: float, wave_speed: float) -> None: ... + def set_damping(self, damping: float) -> None: ... + def step(self, dt: float) -> None: ... + def stable_dt(self) -> float: ... + def total_energy(self, dt: float) -> float: ... + def set_point(self, i: int, j: int, value: float) -> None: ... + @property + def u_current(self) -> list[float]: ... + @property + def u_previous(self) -> list[float]: ... + @property + def nx(self) -> int: ... + @property + def ny(self) -> int: ... + @property + def dx(self) -> float: ... + @property + def dy(self) -> float: ... + @property + def wave_speed(self) -> float: ... + @property + def time(self) -> float: ... + @property + def damping(self) -> float: ... diff --git a/bindings/python/python/numeria/solid_mechanics.pyi b/bindings/python/python/numeria/solid_mechanics.pyi new file mode 100644 index 0000000..bb2ae61 --- /dev/null +++ b/bindings/python/python/numeria/solid_mechanics.pyi @@ -0,0 +1,194 @@ +""" +Strength of materials: stress, strain, elastic constants and beams. Engineering and true stress and strain, and the elastic constants with the identities that connect them -- any two of `E`, `G`, `K` and `ν` determine the other two for an isotropic material, and the conversions are all here. Beam bending: cantilever and simply-supported deflections, bending moment and stress, and second moments of area for rectangular and circular sections. Design closes with the von Mises equivalent stress, the safety factor, and strain energy density. For the tensor formulation and yield surfaces see `continuum_mechanics`; for finite-element beams and modal analysis see `resonance::structural`. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.statistics.distributions import Poisson + +def tensile_stress(force: float, area: float) -> float: + """ +Tensile stress: σ = F/A + +Rust: `solid_mechanics::tensile_stress` + """ + ... + +def tensile_strain(delta_l: float, original_l: float) -> float: + """ +Tensile strain: ε = ΔL/L₀ + +Rust: `solid_mechanics::tensile_strain` + """ + ... + +def shear_stress(force: float, area: float) -> float: + """ +Shear stress: τ = F/A + +Rust: `solid_mechanics::shear_stress` + """ + ... + +def shear_strain(displacement: float, height: float) -> float: + """ +Shear strain: γ = Δx/h + +Rust: `solid_mechanics::shear_strain` + """ + ... + +def volumetric_strain(delta_v: float, original_v: float) -> float: + """ +Volumetric strain: εv = ΔV/V₀ + +Rust: `solid_mechanics::volumetric_strain` + """ + ... + +def true_stress(engineering_stress: float, engineering_strain: float) -> float: + """ +True stress from engineering values: σ_true = σ_eng(1 + ε_eng) + +Rust: `solid_mechanics::true_stress` + """ + ... + +def true_strain(engineering_strain: float) -> float: + """ +True strain from engineering strain: ε_true = ln(1 + ε_eng) + +Rust: `solid_mechanics::true_strain` + """ + ... + +def youngs_modulus(stress: float, strain: float) -> float: + """ +Young's modulus (elastic modulus): E = σ/ε + +Rust: `solid_mechanics::youngs_modulus` + """ + ... + +def shear_modulus(shear_stress: float, shear_strain: float) -> float: + """ +Shear modulus: G = τ/γ + +Rust: `solid_mechanics::shear_modulus` + """ + ... + +def bulk_modulus(pressure: float, volumetric_strain: float) -> float: + """ +Bulk modulus: K = -P/εv + +Rust: `solid_mechanics::bulk_modulus` + """ + ... + +def poisson_ratio_from_moduli(e: float, g: float) -> float: + """ +Poisson's ratio from elastic moduli: ν = E/(2G) - 1 + +Rust: `solid_mechanics::poisson_ratio_from_moduli` + """ + ... + +def e_from_k_and_g(bulk: float, shear: float) -> float: + """ +Young's modulus from bulk and shear moduli: E = 9KG/(3K + G) + +Rust: `solid_mechanics::e_from_k_and_g` + """ + ... + +def bulk_from_e_and_nu(e: float, nu: float) -> float: + """ +Bulk modulus from Young's modulus and Poisson's ratio: K = E/(3(1 - 2ν)) + +Rust: `solid_mechanics::bulk_from_e_and_nu` + """ + ... + +def shear_from_e_and_nu(e: float, nu: float) -> float: + """ +Shear modulus from Young's modulus and Poisson's ratio: G = E/(2(1 + ν)) + +Rust: `solid_mechanics::shear_from_e_and_nu` + """ + ... + +def beam_deflection_cantilever_point(force: float, length: float, e: float, i: float) -> float: + """ +Cantilever beam tip deflection under point load: δ = FL³/(3EI) + +Rust: `solid_mechanics::beam_deflection_cantilever_point` + """ + ... + +def beam_deflection_simply_supported_center(force: float, length: float, e: float, i: float) -> float: + """ +Simply supported beam center deflection under point load: δ = FL³/(48EI) + +Rust: `solid_mechanics::beam_deflection_simply_supported_center` + """ + ... + +def bending_moment(force: float, distance: float) -> float: + """ +Bending moment: M = F·d + +Rust: `solid_mechanics::bending_moment` + """ + ... + +def bending_stress(moment: float, y: float, i: float) -> float: + """ +Bending stress at distance y from neutral axis: σ = My/I + +Rust: `solid_mechanics::bending_stress` + """ + ... + +def second_moment_rectangle(width: float, height: float) -> float: + """ +Second moment of area for a rectangle: I = bh³/12 + +Rust: `solid_mechanics::second_moment_rectangle` + """ + ... + +def second_moment_circle(radius: float) -> float: + """ +Second moment of area for a circle: I = πr⁴/4 + +Rust: `solid_mechanics::second_moment_circle` + """ + ... + +def von_mises_stress(s1: float, s2: float, s3: float) -> float: + """ +Von Mises equivalent stress: σ_vm = √(((σ₁-σ₂)² + (σ₂-σ₃)² + (σ₃-σ₁)²)/2) + +Rust: `solid_mechanics::von_mises_stress` + """ + ... + +def safety_factor(yield_strength: float, applied_stress: float) -> float: + """ +Safety factor: n = σ_yield/σ_applied + +Rust: `solid_mechanics::safety_factor` + """ + ... + +def strain_energy_density(stress: float, strain: float) -> float: + """ +Elastic strain energy density: u = σε/2 + +Rust: `solid_mechanics::strain_energy_density` + """ + ... diff --git a/bindings/python/python/numeria/spatial/__init__.pyi b/bindings/python/python/numeria/spatial/__init__.pyi new file mode 100644 index 0000000..119320c --- /dev/null +++ b/bindings/python/python/numeria/spatial/__init__.pyi @@ -0,0 +1,45 @@ +""" +Spatial data structures, transforms, geometric primitives, and queries. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from . import bvh, contain, distance, frame, intersect, kdtree, mat4, octree, primitives, projective, sdf, transform2d +from numeria.spatial.primitives import Aabb as Aabb +from numeria.spatial.transform2d import Affine2 as Affine2 +from numeria.spatial.bvh import Bvh as Bvh +from numeria.spatial.primitives import Capsule as Capsule +from numeria.spatial.primitives import Circle as Circle +from numeria.spatial.primitives import Cylinder as Cylinder +from numeria.spatial.frame import Frame as Frame +from numeria.spatial.projective import Homography as Homography +from numeria.spatial.kdtree import KdTree as KdTree +from numeria.spatial.kdtree import KdTree2 as KdTree2 +from numeria.spatial.mat4 import Mat4 as Mat4 +from numeria.spatial.primitives import Obb as Obb +from numeria.spatial.octree import Octree as Octree +from numeria.spatial.primitives import Plane as Plane +from numeria.spatial.primitives import Polygon2 as Polygon2 +from numeria.spatial.primitives import Polyline as Polyline +from numeria.spatial.primitives import Ray as Ray +from numeria.spatial.intersect import RayHit as RayHit +from numeria.spatial.primitives import Rect as Rect +from numeria.spatial.primitives import Segment as Segment +from numeria.spatial.primitives import Segment2 as Segment2 +from numeria.spatial.kdtree import SpatialHash as SpatialHash +from numeria.spatial.primitives import Sphere as Sphere +from numeria.spatial.primitives import Triangle as Triangle +from numeria.spatial.primitives import Triangle2 as Triangle2 +from numeria.spatial.projective import are_collinear as are_collinear +from numeria.spatial.projective import cross_ratio as cross_ratio +from numeria.spatial.projective import dehomogenize as dehomogenize +from numeria.spatial.projective import line_through as line_through +from numeria.spatial.projective import lines_intersect as lines_intersect +from numeria.spatial.projective import point_h as point_h +from numeria.spatial.projective import point_on_line as point_on_line +from numeria.spatial.projective import rectify_quad_to_rect as rectify_quad_to_rect + + diff --git a/bindings/python/python/numeria/spatial/bvh.pyi b/bindings/python/python/numeria/spatial/bvh.pyi new file mode 100644 index 0000000..dbf42af --- /dev/null +++ b/bindings/python/python/numeria/spatial/bvh.pyi @@ -0,0 +1,34 @@ +""" +Bounding volume hierarchy over axis-aligned boxes. Built top-down with binned surface-area-heuristic splits (12 bins; Wald 2007), falling back to a median split when SAH finds no gain. Leaves store index ranges into a permutation of the input. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.spatial.primitives import Aabb +from numeria.spatial.primitives import Ray +from numeria.spatial.primitives import Sphere +from numeria.spatial.primitives import Triangle +from numeria.math import Vec3 + +class Bvh: + """ +Binary BVH; query methods return primitive indices into the +original input slice. + +Rust: `spatial::bvh::Bvh` + """ + @staticmethod + def build(bounds: list[Aabb]) -> Bvh: ... + @staticmethod + def build_triangles(tris: list[Triangle]) -> Bvh: ... + def query_ray(self, r: Ray, max_t: float) -> list[int]: ... + def query_aabb(self, b: Aabb) -> list[int]: ... + def query_sphere(self, s: Sphere) -> list[int]: ... + def closest_hit(self, r: Ray, tris: list[Triangle]) -> Optional[tuple[int, RayHit]]: ... + def closest_point(self, p: Vec3 | Sequence[float], tris: list[Triangle]) -> tuple[int, Vec3, float]: ... + def self_overlaps(self) -> list[tuple[int, int]]: ... + def refit(self, bounds: list[Aabb]) -> None: ... + def depth(self) -> int: ... diff --git a/bindings/python/python/numeria/spatial/contain.pyi b/bindings/python/python/numeria/spatial/contain.pyi new file mode 100644 index 0000000..5debe73 --- /dev/null +++ b/bindings/python/python/numeria/spatial/contain.pyi @@ -0,0 +1,194 @@ +""" +Orientation predicates and containment tests. `orient2d_exact` follows Shewchuk's approach: a floating-point filter with a proven error bound, falling back to exact expansion arithmetic (error-free two_sum / two_product transforms) when the filter cannot decide (Shewchuk, "Adaptive precision floating-point arithmetic and fast robust geometric predicates", 1997). +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.spatial.primitives import Aabb +from numeria.spatial.primitives import Capsule +from numeria.spatial.primitives import Cylinder +from numeria.spatial.primitives import Obb +from numeria.spatial.primitives import Polygon2 +from numeria.spatial.primitives import Sphere +from numeria.spatial.primitives import Triangle +from numeria.spatial.primitives import Triangle2 +from numeria.math import Vec2 +from numeria.math import Vec3 + +def orient2d(a: Vec2 | Sequence[float], b: Vec2 | Sequence[float], c: Vec2 | Sequence[float]) -> float: + """ +Twice the signed area of (a, b, c): positive for a CCW turn. + +Rust: `spatial::contain::orient2d` + """ + ... + +def orient3d(a: Vec3 | Sequence[float], b: Vec3 | Sequence[float], c: Vec3 | Sequence[float], d: Vec3 | Sequence[float]) -> float: + """ +Six times the signed volume of tetrahedron (a, b, c, d): positive +when d lies on the positive side of the CCW plane (a, b, c). + +Rust: `spatial::contain::orient3d` + """ + ... + +def in_circle(a: Vec2 | Sequence[float], b: Vec2 | Sequence[float], c: Vec2 | Sequence[float], d: Vec2 | Sequence[float]) -> float: + """ +In-circle predicate: > 0 iff d lies inside the circumcircle of the +CCW triangle (a, b, c) (4×4 determinant form). + +Rust: `spatial::contain::in_circle` + """ + ... + +def in_sphere(a: Vec3 | Sequence[float], b: Vec3 | Sequence[float], c: Vec3 | Sequence[float], d: Vec3 | Sequence[float], e: Vec3 | Sequence[float]) -> float: + """ +In-sphere predicate: > 0 iff e lies inside the circumsphere of the +positively oriented tetrahedron (a, b, c, d). + +Rust: `spatial::contain::in_sphere` + """ + ... + +def orient2d_exact(a: Vec2 | Sequence[float], b: Vec2 | Sequence[float], c: Vec2 | Sequence[float]) -> int: + """ +Exact sign of orient2d: −1, 0, or 1, never wrong. + +A Shewchuk-style floating-point filter answers the easy cases; the +hard ones are decided by exact expansion evaluation of the 6-term +determinant ax·by − ax·cy + bx·cy − bx·ay + cx·ay − cx·by. + +Rust: `spatial::contain::orient2d_exact` + """ + ... + +def point_in_triangle_2d(p: Vec2 | Sequence[float], t: Triangle2) -> bool: + """ +Point in 2-D triangle (boundary counts as inside), robust to either +winding. + +Rust: `spatial::contain::point_in_triangle_2d` + """ + ... + +def point_in_triangle(p: Vec3 | Sequence[float], t: Triangle, tol: float) -> bool: + """ +Point in 3-D triangle: within `tol` of the plane and barycentrics +in [0, 1]. + +Rust: `spatial::contain::point_in_triangle` + """ + ... + +def point_in_polygon_2d(p: Vec2 | Sequence[float], poly: Polygon2) -> bool: + """ +Even-odd (crossing number) point-in-polygon test. + +Rust: `spatial::contain::point_in_polygon_2d` + """ + ... + +def winding_number_2d(p: Vec2 | Sequence[float], poly: Polygon2) -> int: + """ +Winding number of a polygon about p (0 for outside points of simple +polygons; ±1 inside depending on orientation). + +Rust: `spatial::contain::winding_number_2d` + """ + ... + +def point_in_convex_polygon_2d(p: Vec2 | Sequence[float], poly: Polygon2) -> bool: + """ +O(log n) point-in-convex-polygon by binary search on the fan from +vertex 0 (polygon must be convex and CCW). + +Rust: `spatial::contain::point_in_convex_polygon_2d` + """ + ... + +def point_in_convex_hull_3d(p: Vec3 | Sequence[float], hull_tris: list[Triangle]) -> bool: + """ +Point inside a convex hull given as outward-oriented triangles: on +or behind every face plane. + +Rust: `spatial::contain::point_in_convex_hull_3d` + """ + ... + +def point_in_mesh(p: Vec3 | Sequence[float], tris: list[Triangle]) -> bool: + """ +Generalized winding number test for closed (possibly non-convex) +triangle meshes: the summed signed solid angle is ±4π inside and ~0 +outside (van Oosterom & Strackee 1983 per-triangle solid angle). + +Rust: `spatial::contain::point_in_mesh` + """ + ... + +def point_in_aabb(p: Vec3 | Sequence[float], b: Aabb) -> bool: + """ +Point in AABB (closed). + +Rust: `spatial::contain::point_in_aabb` + """ + ... + +def point_in_obb(p: Vec3 | Sequence[float], b: Obb) -> bool: + """ +Point in OBB: local coordinates within the half extents. + +Rust: `spatial::contain::point_in_obb` + """ + ... + +def point_in_sphere(p: Vec3 | Sequence[float], s: Sphere) -> bool: + """ +Point in sphere (closed). + +Rust: `spatial::contain::point_in_sphere` + """ + ... + +def point_in_capsule(p: Vec3 | Sequence[float], c: Capsule) -> bool: + """ +Point in capsule: within radius of the core segment. + +Rust: `spatial::contain::point_in_capsule` + """ + ... + +def point_in_cylinder(p: Vec3 | Sequence[float], c: Cylinder) -> bool: + """ +Point in finite cylinder: axial span and radial distance. + +Rust: `spatial::contain::point_in_cylinder` + """ + ... + +def point_in_tetrahedron(p: Vec3 | Sequence[float], a: Vec3 | Sequence[float], b: Vec3 | Sequence[float], c: Vec3 | Sequence[float], d: Vec3 | Sequence[float]) -> bool: + """ +Point in tetrahedron: consistent orientation with respect to all +four faces. + +Rust: `spatial::contain::point_in_tetrahedron` + """ + ... + +def aabb_contains_aabb(outer: Aabb, inner: Aabb) -> bool: + """ +Full containment of one AABB in another (closed). + +Rust: `spatial::contain::aabb_contains_aabb` + """ + ... + +def sphere_contains_aabb(s: Sphere, b: Aabb) -> bool: + """ +Sphere containing an entire AABB (all corners inside). + +Rust: `spatial::contain::sphere_contains_aabb` + """ + ... diff --git a/bindings/python/python/numeria/spatial/distance.pyi b/bindings/python/python/numeria/spatial/distance.pyi new file mode 100644 index 0000000..bb6b65e --- /dev/null +++ b/bindings/python/python/numeria/spatial/distance.pyi @@ -0,0 +1,179 @@ +""" +Closest-point queries and set distances. References: Ericson, *Real-Time Collision Detection*, ch. 5 (point and segment queries); Eiter & Mannila 1994 (discrete Fréchet distance). +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.spatial.primitives import Aabb +from numeria.spatial.primitives import Obb +from numeria.spatial.primitives import Polygon2 +from numeria.spatial.primitives import Polyline +from numeria.spatial.primitives import Segment +from numeria.spatial.primitives import Sphere +from numeria.spatial.primitives import Triangle +from numeria.math import Vec2 +from numeria.math import Vec3 + +def closest_point_segment(p: Vec3 | Sequence[float], s: Segment) -> tuple[Vec3, float]: + """ +Closest point on a segment and its parameter t ∈ [0, 1]. + +Rust: `spatial::distance::closest_point_segment` + """ + ... + +def closest_point_segment_2d(p: Vec2 | Sequence[float], s: Segment2) -> tuple[Vec2, float]: + """ +2-D closest point on a segment and its parameter. + +Rust: `spatial::distance::closest_point_segment_2d` + """ + ... + +def closest_point_triangle(p: Vec3 | Sequence[float], t: Triangle) -> Vec3: + """ +Closest point on a triangle (Ericson RTCD §5.1.5 Voronoi-region +walk). + +Rust: `spatial::distance::closest_point_triangle` + """ + ... + +def closest_point_plane(p: Vec3 | Sequence[float], pl: Plane) -> Vec3: + """ +Orthogonal projection onto a plane. + +Rust: `spatial::distance::closest_point_plane` + """ + ... + +def closest_point_aabb(p: Vec3 | Sequence[float], b: Aabb) -> Vec3: + """ +Componentwise clamp onto an AABB. + +Rust: `spatial::distance::closest_point_aabb` + """ + ... + +def closest_point_obb(p: Vec3 | Sequence[float], b: Obb) -> Vec3: + """ +Clamp in the box's local frame (RTCD §5.1.4). + +Rust: `spatial::distance::closest_point_obb` + """ + ... + +def closest_point_sphere(p: Vec3 | Sequence[float], s: Sphere) -> Vec3: + """ +Closest point on a sphere's surface (center maps to +radius·x̂). + +Rust: `spatial::distance::closest_point_sphere` + """ + ... + +def closest_points_segments(s1: Segment, s2: Segment) -> tuple[Vec3, Vec3, float]: + """ +Closest points between two segments and their distance +(RTCD §5.1.9). + +Rust: `spatial::distance::closest_points_segments` + """ + ... + +def closest_points_lines(p1: Vec3 | Sequence[float], d1: Vec3 | Sequence[float], p2: Vec3 | Sequence[float], d2: Vec3 | Sequence[float]) -> Optional[tuple[Vec3, Vec3]]: + """ +Closest points between two infinite lines; `None` when parallel. + +Rust: `spatial::distance::closest_points_lines` + """ + ... + +def distance_point_segment(p: Vec3 | Sequence[float], s: Segment) -> float: + """ +Distance from a point to a segment. + +Rust: `spatial::distance::distance_point_segment` + """ + ... + +def distance_point_triangle(p: Vec3 | Sequence[float], t: Triangle) -> float: + """ +Distance from a point to a triangle. + +Rust: `spatial::distance::distance_point_triangle` + """ + ... + +def distance_point_polyline(p: Vec3 | Sequence[float], pl: Polyline) -> tuple[float, int, float]: + """ +Distance from a point to a polyline, with the segment index and +parameter of the closest point. + +Panics: +Panics on a polyline with no segments. + +Rust: `spatial::distance::distance_point_polyline` + """ + ... + +def distance_point_polygon_2d(p: Vec2 | Sequence[float], poly: Polygon2) -> float: + """ +Unsigned distance from a point to the boundary of a polygon. + +Rust: `spatial::distance::distance_point_polygon_2d` + """ + ... + +def distance_segment_triangle(s: Segment, t: Triangle) -> float: + """ +Distance between a segment and a triangle: minimum over segment vs +the three edges and the endpoints vs the face. + +Rust: `spatial::distance::distance_segment_triangle` + """ + ... + +def distance_aabb_aabb(a: Aabb, b: Aabb) -> float: + """ +Distance between two AABBs (0 when overlapping). + +Rust: `spatial::distance::distance_aabb_aabb` + """ + ... + +def hausdorff_distance(a: list[Vec3 | Sequence[float]], b: list[Vec3 | Sequence[float]]) -> float: + """ +Symmetric Hausdorff distance between two 3-D point sets. + +Panics: +Panics when either set is empty. + +Rust: `spatial::distance::hausdorff_distance` + """ + ... + +def hausdorff_distance_2d(a: list[Vec2 | Sequence[float]], b: list[Vec2 | Sequence[float]]) -> float: + """ +Symmetric Hausdorff distance between two 2-D point sets. + +Panics: +Panics when either set is empty. + +Rust: `spatial::distance::hausdorff_distance_2d` + """ + ... + +def frechet_distance_2d(a: list[Vec2 | Sequence[float]], b: list[Vec2 | Sequence[float]]) -> float: + """ +Discrete Fréchet distance between two 2-D polygonal curves +(Eiter & Mannila dynamic program). + +Panics: +Panics when either curve is empty. + +Rust: `spatial::distance::frechet_distance_2d` + """ + ... diff --git a/bindings/python/python/numeria/spatial/frame.pyi b/bindings/python/python/numeria/spatial/frame.pyi new file mode 100644 index 0000000..1fbccda --- /dev/null +++ b/bindings/python/python/numeria/spatial/frame.pyi @@ -0,0 +1,40 @@ +""" +Rigid coordinate frames (origin + unit-quaternion rotation). `to_world(p_local) = origin + R·p_local`; composition and inverses follow the usual rigid-motion group structure SE(3). +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.quaternion import Quaternion +from numeria.math import Vec3 + +class Frame: + """ +A rigid frame: position and orientation of a local coordinate +system expressed in world coordinates. + +Rust: `spatial::frame::Frame` + """ + @staticmethod + def identity() -> Frame: ... + def __init__(self, origin: Vec3 | Sequence[float], rotation: Quaternion | Sequence[float]) -> None: ... + @staticmethod + def from_axes(origin: Vec3 | Sequence[float], x: Vec3 | Sequence[float], y: Vec3 | Sequence[float]) -> Frame: ... + def to_local(self, world_p: Vec3 | Sequence[float]) -> Vec3: ... + def to_world(self, local_p: Vec3 | Sequence[float]) -> Vec3: ... + def to_local_vector(self, v: Vec3 | Sequence[float]) -> Vec3: ... + def to_world_vector(self, v: Vec3 | Sequence[float]) -> Vec3: ... + def compose(self, child: Frame) -> Frame: ... + def inverse(self) -> Frame: ... + def relative_to(self, other: Frame) -> Frame: ... + def interpolate(self, other: Frame, t: float) -> Frame: ... + def to_mat4(self) -> Mat4: ... + def x_axis(self) -> Vec3: ... + def y_axis(self) -> Vec3: ... + def z_axis(self) -> Vec3: ... + @property + def origin(self) -> Vec3: ... + @property + def rotation(self) -> Quaternion: ... diff --git a/bindings/python/python/numeria/spatial/intersect.pyi b/bindings/python/python/numeria/spatial/intersect.pyi new file mode 100644 index 0000000..722c007 --- /dev/null +++ b/bindings/python/python/numeria/spatial/intersect.pyi @@ -0,0 +1,234 @@ +""" +Intersection tests between the spatial primitives. References: Ericson, *Real-Time Collision Detection* (RTCD); Möller & Trumbore 1997 (ray-triangle); Akenine-Möller 2001 (triangle-box SAT). Ray parameters are along the (normalized) ray direction; only t ≥ 0 counts as a hit. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.spatial.primitives import Aabb +from numeria.spatial.primitives import Capsule +from numeria.spatial.primitives import Circle +from numeria.spatial.primitives import Cylinder +from numeria.spatial.primitives import Obb +from numeria.spatial.primitives import Polygon2 +from numeria.spatial.primitives import Ray +from numeria.spatial.primitives import Rect +from numeria.spatial.primitives import Sphere +from numeria.spatial.primitives import Triangle +from numeria.math import Vec2 +from numeria.math import Vec3 + +class RayHit: + """ +A ray hit: parameter, position, and surface normal (facing the ray). + +Rust: `spatial::intersect::RayHit` + """ + def __init__(self, t: float, point: Vec3 | Sequence[float], normal: Vec3 | Sequence[float]) -> None: ... + @property + def t(self) -> float: ... + @property + def point(self) -> Vec3: ... + @property + def normal(self) -> Vec3: ... + +def ray_sphere(r: Ray, s: Sphere) -> Optional[RayHit]: + """ +Ray vs sphere: nearest hit with t ≥ 0 (RTCD §5.3.2). + +Rust: `spatial::intersect::ray_sphere` + """ + ... + +def ray_plane(r: Ray, p: Plane) -> Optional[RayHit]: + """ +Ray vs plane: `None` when parallel or hitting behind the origin. + +Rust: `spatial::intersect::ray_plane` + """ + ... + +def ray_triangle(r: Ray, t: Triangle, cull_backface: bool) -> Optional[tuple[RayHit, tuple[float, float, float]]]: + """ +Möller-Trumbore ray-triangle intersection; also returns the +barycentric coordinates (u, v, w) of the hit with respect to +(a, b, c). With `cull_backface`, only front faces (CCW seen from +the ray origin) hit. + +Rust: `spatial::intersect::ray_triangle` + """ + ... + +def ray_aabb(r: Ray, b: Aabb) -> Optional[tuple[float, float]]: + """ +Slab-method ray vs AABB: (t_enter, t_exit) of the overlap with +t ≥ 0, `None` on a miss (RTCD §5.3.3). + +Rust: `spatial::intersect::ray_aabb` + """ + ... + +def ray_obb(r: Ray, b: Obb) -> Optional[tuple[float, float]]: + """ +Ray vs OBB: the slab test in the box's local frame. + +Rust: `spatial::intersect::ray_obb` + """ + ... + +def ray_capsule(r: Ray, c: Capsule) -> Optional[RayHit]: + """ +Ray vs capsule: cylinder body plus spherical caps, nearest hit. + +Rust: `spatial::intersect::ray_capsule` + """ + ... + +def ray_cylinder(r: Ray, c: Cylinder) -> Optional[RayHit]: + """ +Ray vs finite cylinder: lateral surface and both cap disks. + +Rust: `spatial::intersect::ray_cylinder` + """ + ... + +def segment_segment_2d(s1: Segment2, s2: Segment2) -> Optional[Vec2]: + """ +Proper 2-D segment intersection (interiors cross): the point, or +`None` for disjoint, touching, or collinear segments. + +Rust: `spatial::intersect::segment_segment_2d` + """ + ... + +def segment_segment_2d_params(s1: Segment2, s2: Segment2) -> Optional[tuple[float, float]]: + """ +Parameters (t, u) with s1(t) = s2(u), both in [0, 1] (endpoints +included); `None` for parallel/collinear or non-intersecting pairs. + +Rust: `spatial::intersect::segment_segment_2d_params` + """ + ... + +def line_circle(p: Vec2 | Sequence[float], dir: Vec2 | Sequence[float], c: Circle) -> Optional[tuple[Vec2, Vec2]]: + """ +Infinite line (point + direction) vs circle: the two intersection +points ordered along the direction (equal at tangency). + +Rust: `spatial::intersect::line_circle` + """ + ... + +def circle_circle(a: Circle, b: Circle) -> Optional[tuple[Vec2, Vec2]]: + """ +Circle-circle intersection points; `None` when separate, nested, or +coincident. + +Rust: `spatial::intersect::circle_circle` + """ + ... + +def sphere_sphere(a: Sphere, b: Sphere) -> bool: + """ +Sphere overlap test. + +Rust: `spatial::intersect::sphere_sphere` + """ + ... + +def sphere_sphere_contact(a: Sphere, b: Sphere) -> Optional[tuple[Vec3, float]]: + """ +Sphere contact: unit normal from a toward b and penetration depth, +`Some` only when overlapping. + +Rust: `spatial::intersect::sphere_sphere_contact` + """ + ... + +def aabb_aabb(a: Aabb, b: Aabb) -> bool: + """ +AABB overlap test (closed). + +Rust: `spatial::intersect::aabb_aabb` + """ + ... + +def rect_rect(a: Rect, b: Rect) -> bool: + """ +Rectangle overlap test (closed). + +Rust: `spatial::intersect::rect_rect` + """ + ... + +def sphere_aabb(s: Sphere, b: Aabb) -> bool: + """ +Sphere vs AABB (closest-point distance). + +Rust: `spatial::intersect::sphere_aabb` + """ + ... + +def sphere_triangle(s: Sphere, t: Triangle) -> Optional[tuple[Vec3, float]]: + """ +Sphere vs triangle: contact point on the triangle and penetration +depth when overlapping. + +Rust: `spatial::intersect::sphere_triangle` + """ + ... + +def obb_obb(a: Obb, b: Obb) -> bool: + """ +OBB-OBB overlap by the separating axis theorem over the 15 +candidate axes (RTCD §4.4.1). + +Rust: `spatial::intersect::obb_obb` + """ + ... + +def triangle_triangle(a: Triangle, b: Triangle) -> bool: + """ +Triangle-triangle overlap by SAT over both normals and the nine +edge-pair cross products, with a coplanar 2-D SAT fallback +(equivalent to the interval tests of Möller 1997). + +Rust: `spatial::intersect::triangle_triangle` + """ + ... + +def plane_plane(a: Plane, b: Plane) -> Optional[Ray]: + """ +Plane-plane intersection line; `None` for (near-)parallel planes. + +Rust: `spatial::intersect::plane_plane` + """ + ... + +def three_planes(a: Plane, b: Plane, c: Plane) -> Optional[Vec3]: + """ +Common point of three planes; `None` when any pair is parallel or +the normals are linearly dependent. + +Rust: `spatial::intersect::three_planes` + """ + ... + +def triangle_aabb(t: Triangle, b: Aabb) -> bool: + """ +Triangle vs AABB by the 13-axis SAT of Akenine-Möller. + +Rust: `spatial::intersect::triangle_aabb` + """ + ... + +def polygon_polygon_2d(a: Polygon2, b: Polygon2) -> bool: + """ +Polygon overlap: SAT when both are convex, otherwise any edge +crossing or mutual containment. + +Rust: `spatial::intersect::polygon_polygon_2d` + """ + ... diff --git a/bindings/python/python/numeria/spatial/kdtree.pyi b/bindings/python/python/numeria/spatial/kdtree.pyi new file mode 100644 index 0000000..748acac --- /dev/null +++ b/bindings/python/python/numeria/spatial/kdtree.pyi @@ -0,0 +1,51 @@ +""" +k-d trees (3-D and 2-D) with median splits, plus a uniform spatial hash for broadphase neighbor queries. Reference: Bentley 1975; Friedman, Bentley & Finkel 1977 (nearest neighbor search with bounds pruning). +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.math import Vec2 +from numeria.math import Vec3 + +class KdTree: + """ +Median-split k-d tree over `Vec3` points. + +Rust: `spatial::kdtree::KdTree` + """ + @staticmethod + def build(points: list[Vec3 | Sequence[float]]) -> KdTree: ... + def nearest(self, p: Vec3 | Sequence[float]) -> Optional[tuple[int, float]]: ... + def k_nearest(self, p: Vec3 | Sequence[float], k: int) -> list[tuple[int, float]]: ... + def within_radius(self, p: Vec3 | Sequence[float], r: float) -> list[tuple[int, float]]: ... + def all_pairs_within(self, r: float) -> list[tuple[int, int]]: ... + +class KdTree2: + """ +Median-split k-d tree over `Vec2` points. + +Rust: `spatial::kdtree::KdTree2` + """ + @staticmethod + def build(points: list[Vec2 | Sequence[float]]) -> KdTree2: ... + def nearest(self, p: Vec2 | Sequence[float]) -> Optional[tuple[int, float]]: ... + def k_nearest(self, p: Vec2 | Sequence[float], k: int) -> list[tuple[int, float]]: ... + def within_radius(self, p: Vec2 | Sequence[float], r: float) -> list[tuple[int, float]]: ... + def all_pairs_within(self, r: float) -> list[tuple[int, int]]: ... + +class SpatialHash: + """ +Uniform-grid spatial hash for 3-D points; O(1) insert, sphere +queries visit only overlapping cells. + +Rust: `spatial::kdtree::SpatialHash` + """ + def __init__(self, cell: float) -> None: ... + def insert(self, i: int, p: Vec3 | Sequence[float]) -> None: ... + def query_sphere(self, p: Vec3 | Sequence[float], r: float) -> list[int]: ... + def clear(self) -> None: ... + @property + def cell(self) -> float: ... diff --git a/bindings/python/python/numeria/spatial/mat4.pyi b/bindings/python/python/numeria/spatial/mat4.pyi new file mode 100644 index 0000000..3d0faef --- /dev/null +++ b/bindings/python/python/numeria/spatial/mat4.pyi @@ -0,0 +1,53 @@ +""" +4×4 homogeneous transform matrix (row-major storage, column-vector convention: p' = M·p). References: Foley et al., *Computer Graphics: Principles and Practice*; the OpenGL clip-space conventions for `perspective` and `orthographic` (z mapped to [−1, 1]). +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.linalg import Mat3 +from numeria.quaternion import Quaternion +from numeria.math import Vec3 + +class Mat4: + """ +4×4 matrix, row-major: `data[row][col]`. + +Rust: `spatial::mat4::Mat4` + """ + @staticmethod + def identity() -> Mat4: ... + @staticmethod + def from_rows(r0: list[float], r1: list[float], r2: list[float], r3: list[float]) -> Mat4: ... + @staticmethod + def translation(t: Vec3 | Sequence[float]) -> Mat4: ... + @staticmethod + def scaling(s: Vec3 | Sequence[float]) -> Mat4: ... + @staticmethod + def rotation(q: Quaternion | Sequence[float]) -> Mat4: ... + @staticmethod + def from_mat3(m: Mat3) -> Mat4: ... + @staticmethod + def from_trs(t: Vec3 | Sequence[float], r: Quaternion | Sequence[float], s: Vec3 | Sequence[float]) -> Mat4: ... + @staticmethod + def look_at(eye: Vec3 | Sequence[float], target: Vec3 | Sequence[float], up: Vec3 | Sequence[float]) -> Mat4: ... + @staticmethod + def perspective(fov_y_rad: float, aspect: float, near: float, far: float) -> Mat4: ... + @staticmethod + def orthographic(l: float, r: float, b: float, t: float, near: float, far: float) -> Mat4: ... + def mul(self, other: Mat4) -> Mat4: ... + def transform_point(self, p: Vec3 | Sequence[float]) -> Vec3: ... + def transform_vector(self, v: Vec3 | Sequence[float]) -> Vec3: ... + def transform_homogeneous(self, p: list[float]) -> list[float]: ... + def transpose(self) -> Mat4: ... + def determinant(self) -> float: ... + def inverse(self) -> Optional[Mat4]: ... + def inverse_affine(self) -> Optional[Mat4]: ... + def to_mat3(self) -> Mat3: ... + def decompose_trs(self) -> Optional[tuple[Vec3, Quaternion, Vec3]]: ... + def normal_matrix(self) -> Mat3: ... + def __mul__(self, rhs: Mat4) -> Mat4: ... + @property + def data(self) -> list[list[float]]: ... diff --git a/bindings/python/python/numeria/spatial/octree.pyi b/bindings/python/python/numeria/spatial/octree.pyi new file mode 100644 index 0000000..9b237c0 --- /dev/null +++ b/bindings/python/python/numeria/spatial/octree.pyi @@ -0,0 +1,32 @@ +""" +Barnes-Hut octree for N-body force approximation. Direct summation costs O(N²). The octree groups distant bodies and treats each group as a single mass at its centre of mass, which brings the cost to O(N log N). The approximation is controlled by `theta`: a node is used as a whole when its width divided by the distance to it is below that threshold. Smaller `theta` is more accurate and slower, and `theta = 0` degenerates to direct summation. The conventional default of 0.5 is `BH_THETA`. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.astrophysics.nbody import Body +from numeria.math import Vec3 + +class Octree: + """ + +Rust: `spatial::octree::Octree` + """ + @staticmethod + def build(bodies: list[Body]) -> Octree: ... + def compute_acceleration(self, bodies: list[Body], idx: int, theta: float, softening: float) -> Vec3: ... + +def compute_all_accelerations(bodies: list[Body], theta: float, softening: float) -> list[Vec3]: + """ +Computes accelerations for all bodies, using direct summation below the crossover threshold or Barnes-Hut above it. + +Rust: `spatial::octree::compute_all_accelerations` + """ + ... + +BH_THETA: float + +BH_CROSSOVER: int diff --git a/bindings/python/python/numeria/spatial/primitives.pyi b/bindings/python/python/numeria/spatial/primitives.pyi new file mode 100644 index 0000000..f052d5c --- /dev/null +++ b/bindings/python/python/numeria/spatial/primitives.pyi @@ -0,0 +1,266 @@ +""" +Geometric primitive types shared by the intersection, distance, containment, and acceleration modules. Conventions: plane as n·p + d = 0 with unit normal; ray directions normalized by the constructor; polygons CCW-positive. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.linalg import Mat3 +from numeria.math import Vec2 +from numeria.math import Vec3 + +class Aabb: + """ +Axis-aligned bounding box. + +Rust: `spatial::primitives::Aabb` + """ + def __init__(self, min: Vec3 | Sequence[float], max: Vec3 | Sequence[float]) -> None: ... + @staticmethod + def from_points(points: list[Vec3 | Sequence[float]]) -> Aabb: ... + def union(self, other: Aabb) -> Aabb: ... + def intersection(self, other: Aabb) -> Optional[Aabb]: ... + def center(self) -> Vec3: ... + def extents(self) -> Vec3: ... + def surface_area(self) -> float: ... + def volume(self) -> float: ... + def contains_point(self, p: Vec3 | Sequence[float]) -> bool: ... + def expand(self, margin: float) -> Aabb: ... + def corners(self) -> list[Vec3]: ... + def transform(self, m: Mat4) -> Aabb: ... + @property + def min(self) -> Vec3: ... + @property + def max(self) -> Vec3: ... + +class Capsule: + """ +Capsule: segment swept by a sphere. + +Rust: `spatial::primitives::Capsule` + """ + def __init__(self, a: Vec3 | Sequence[float], b: Vec3 | Sequence[float], radius: float) -> None: ... + @property + def a(self) -> Vec3: ... + @property + def b(self) -> Vec3: ... + @property + def radius(self) -> float: ... + +class Circle: + """ +Circle in the plane. + +Rust: `spatial::primitives::Circle` + """ + def __init__(self, center: Vec2 | Sequence[float], radius: float) -> None: ... + @property + def center(self) -> Vec2: ... + @property + def radius(self) -> float: ... + +class Cylinder: + """ +Finite cylinder between two cap centers. + +Rust: `spatial::primitives::Cylinder` + """ + def __init__(self, a: Vec3 | Sequence[float], b: Vec3 | Sequence[float], radius: float) -> None: ... + @property + def a(self) -> Vec3: ... + @property + def b(self) -> Vec3: ... + @property + def radius(self) -> float: ... + +class Obb: + """ +Oriented bounding box: rotation columns are the local axes. + +Rust: `spatial::primitives::Obb` + """ + def __init__(self, center: Vec3 | Sequence[float], half_extents: Vec3 | Sequence[float], rotation: Mat3) -> None: ... + def axes(self) -> list[Vec3]: ... + def corners(self) -> list[Vec3]: ... + def to_aabb(self) -> Aabb: ... + @staticmethod + def from_points_pca(points: list[Vec3 | Sequence[float]]) -> Obb: ... + @property + def center(self) -> Vec3: ... + @property + def half_extents(self) -> Vec3: ... + @property + def rotation(self) -> Mat3: ... + +class Plane: + """ +Plane n·p + d = 0 with unit normal. + +Rust: `spatial::primitives::Plane` + """ + def __init__(self, normal: Vec3 | Sequence[float], d: float) -> None: ... + @staticmethod + def from_point_normal(p: Vec3 | Sequence[float], normal: Vec3 | Sequence[float]) -> Plane: ... + @staticmethod + def from_three_points(a: Vec3 | Sequence[float], b: Vec3 | Sequence[float], c: Vec3 | Sequence[float]) -> Optional[Plane]: ... + def signed_distance(self, p: Vec3 | Sequence[float]) -> float: ... + def project(self, p: Vec3 | Sequence[float]) -> Vec3: ... + def flip(self) -> Plane: ... + @property + def normal(self) -> Vec3: ... + @property + def d(self) -> float: ... + +class Polygon2: + """ +Simple polygon in the plane (implicitly closed). + +Rust: `spatial::primitives::Polygon2` + """ + def __init__(self, vertices: list[Vec2 | Sequence[float]]) -> None: ... + def area_signed(self) -> float: ... + def area(self) -> float: ... + def perimeter(self) -> float: ... + def centroid(self) -> Vec2: ... + def is_convex(self) -> bool: ... + def is_ccw(self) -> bool: ... + def reverse(self) -> None: ... + def bounding_rect(self) -> Rect: ... + def is_simple(self) -> bool: ... + @property + def vertices(self) -> list[Vec2]: ... + +class Polyline: + """ +3-D polyline (open or closed). + +Rust: `spatial::primitives::Polyline` + """ + def __init__(self, points: list[Vec3 | Sequence[float]], closed: bool) -> None: ... + def segment_count(self) -> int: ... + def segment(self, i: int) -> Segment: ... + def length(self) -> float: ... + def point_at_arclength(self, s: float) -> Vec3: ... + def tangent_at(self, s: float) -> Vec3: ... + def resample(self, spacing: float) -> Polyline: ... + def bounding_box(self) -> Aabb: ... + @property + def points(self) -> list[Vec3]: ... + @property + def closed(self) -> bool: ... + +class Ray: + """ +Ray with normalized direction. + +Rust: `spatial::primitives::Ray` + """ + def __init__(self, origin: Vec3 | Sequence[float], dir: Vec3 | Sequence[float]) -> None: ... + def at(self, t: float) -> Vec3: ... + @property + def origin(self) -> Vec3: ... + @property + def dir(self) -> Vec3: ... + +class Rect: + """ +Axis-aligned rectangle. + +Rust: `spatial::primitives::Rect` + """ + def __init__(self, min: Vec2 | Sequence[float], max: Vec2 | Sequence[float]) -> None: ... + @staticmethod + def from_points(points: list[Vec2 | Sequence[float]]) -> Rect: ... + def union(self, other: Rect) -> Rect: ... + def intersection(self, other: Rect) -> Optional[Rect]: ... + def center(self) -> Vec2: ... + def extents(self) -> Vec2: ... + def area(self) -> float: ... + def perimeter(self) -> float: ... + def contains_point(self, p: Vec2 | Sequence[float]) -> bool: ... + def expand(self, margin: float) -> Rect: ... + def corners(self) -> list[Vec2]: ... + @property + def min(self) -> Vec2: ... + @property + def max(self) -> Vec2: ... + +class Segment: + """ +3-D line segment. + +Rust: `spatial::primitives::Segment` + """ + def __init__(self, a: Vec3 | Sequence[float], b: Vec3 | Sequence[float]) -> None: ... + @property + def a(self) -> Vec3: ... + @property + def b(self) -> Vec3: ... + +class Segment2: + """ +2-D line segment. + +Rust: `spatial::primitives::Segment2` + """ + def __init__(self, a: Vec2 | Sequence[float], b: Vec2 | Sequence[float]) -> None: ... + @property + def a(self) -> Vec2: ... + @property + def b(self) -> Vec2: ... + +class Sphere: + """ +Sphere. + +Rust: `spatial::primitives::Sphere` + """ + def __init__(self, center: Vec3 | Sequence[float], radius: float) -> None: ... + @property + def center(self) -> Vec3: ... + @property + def radius(self) -> float: ... + +class Triangle: + """ +3-D triangle. + +Rust: `spatial::primitives::Triangle` + """ + def __init__(self, a: Vec3 | Sequence[float], b: Vec3 | Sequence[float], c: Vec3 | Sequence[float]) -> None: ... + def normal(self) -> Vec3: ... + def area(self) -> float: ... + def centroid(self) -> Vec3: ... + def barycentric(self, p: Vec3 | Sequence[float]) -> tuple[float, float, float]: ... + def from_barycentric(self, u: float, v: float, w: float) -> Vec3: ... + def circumcenter(self) -> Vec3: ... + def incenter(self) -> Vec3: ... + def is_degenerate(self, tol: float) -> bool: ... + def to_plane(self) -> Optional[Plane]: ... + @property + def a(self) -> Vec3: ... + @property + def b(self) -> Vec3: ... + @property + def c(self) -> Vec3: ... + +class Triangle2: + """ +2-D triangle. + +Rust: `spatial::primitives::Triangle2` + """ + def __init__(self, a: Vec2 | Sequence[float], b: Vec2 | Sequence[float], c: Vec2 | Sequence[float]) -> None: ... + def area_signed(self) -> float: ... + def centroid(self) -> Vec2: ... + def barycentric(self, p: Vec2 | Sequence[float]) -> tuple[float, float, float]: ... + def circumcircle(self) -> Circle: ... + def is_ccw(self) -> bool: ... + @property + def a(self) -> Vec2: ... + @property + def b(self) -> Vec2: ... + @property + def c(self) -> Vec2: ... diff --git a/bindings/python/python/numeria/spatial/projective.pyi b/bindings/python/python/numeria/spatial/projective.pyi new file mode 100644 index 0000000..28e7829 --- /dev/null +++ b/bindings/python/python/numeria/spatial/projective.pyi @@ -0,0 +1,98 @@ +""" +Homogeneous 2-D projective geometry: points, lines, cross ratios, and plane homographies (Hartley & Zisserman, *Multiple View Geometry*, ch. 2 and 4). +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.math import Vec2 + +class Homography: + """ +Plane projective transform p' ~ H·p. + +Rust: `spatial::projective::Homography` + """ + @staticmethod + def from_four_points(src: list[Vec2 | Sequence[float]], dst: list[Vec2 | Sequence[float]]) -> Optional[Homography]: ... + def apply(self, p: Vec2 | Sequence[float]) -> Optional[Vec2]: ... + def inverse(self) -> Optional[Homography]: ... + def compose(self, other: Homography) -> Homography: ... + def vanishing_point(self, direction: Vec2 | Sequence[float]) -> Optional[Vec2]: ... + @property + def h(self) -> list[list[float]]: ... + +def point_h(p: Vec2 | Sequence[float]) -> list[float]: + """ +Lifts a Euclidean point to homogeneous coordinates (w = 1). + +Rust: `spatial::projective::point_h` + """ + ... + +def dehomogenize(h: list[float]) -> Optional[Vec2]: + """ +Projects back to Euclidean coordinates; `None` for points at +infinity (w ≈ 0). + +Rust: `spatial::projective::dehomogenize` + """ + ... + +def line_through(a: Vec2 | Sequence[float], b: Vec2 | Sequence[float]) -> list[float]: + """ +The line through two points: l = a × b. + +Rust: `spatial::projective::line_through` + """ + ... + +def lines_intersect(l1: list[float], l2: list[float]) -> list[float]: + """ +Intersection of two lines: p = l₁ × l₂ (possibly at infinity for +parallel lines). + +Rust: `spatial::projective::lines_intersect` + """ + ... + +def point_on_line(p: list[float], l: list[float], tol: float) -> bool: + """ +Incidence test: p·l = 0 within tol (both scale-normalized). + +Rust: `spatial::projective::point_on_line` + """ + ... + +def are_collinear(a: Vec2 | Sequence[float], b: Vec2 | Sequence[float], c: Vec2 | Sequence[float], tol: float) -> bool: + """ +Collinearity of three Euclidean points (twice the triangle area +below tol, scale-normalized). + +Rust: `spatial::projective::are_collinear` + """ + ... + +def cross_ratio(a: float, b: float, c: float, d: float) -> float: + """ +Cross ratio of four collinear parameters: +(a, b; c, d) = ((a−c)(b−d)) / ((a−d)(b−c)). + +Panics: +Panics when the denominator vanishes (repeated points). + +Rust: `spatial::projective::cross_ratio` + """ + ... + +def rectify_quad_to_rect(quad: list[Vec2 | Sequence[float]], width: float, height: float) -> Optional[Homography]: + """ +Homography mapping an arbitrary quad (CCW or CW consistent order) +onto the axis-aligned rectangle [0, w]×[0, h] with corner order +(0,0), (w,0), (w,h), (0,h). + +Rust: `spatial::projective::rectify_quad_to_rect` + """ + ... diff --git a/bindings/python/python/numeria/spatial/sdf.pyi b/bindings/python/python/numeria/spatial/sdf.pyi new file mode 100644 index 0000000..dd98ff6 --- /dev/null +++ b/bindings/python/python/numeria/spatial/sdf.pyi @@ -0,0 +1,331 @@ +""" +Signed distance fields: primitives, combinators, domain operators, and queries (sphere tracing, normals, AO, soft shadows). Primitive formulas follow Inigo Quilez's reference catalogue (iquilezles.org/articles/distfunctions). Negative inside, positive outside; all primitive SDFs are exact unless noted. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.spatial.primitives import Aabb +from numeria.spatial.primitives import Capsule +from numeria.spatial.primitives import Circle +from numeria.geometry.geodesy import Ellipsoid +from numeria.spatial.primitives import Polygon2 +from numeria.spatial.primitives import Ray +from numeria.spatial.primitives import Rect +from numeria.spatial.primitives import Sphere +from numeria.math import Vec2 +from numeria.math import Vec3 + +def sd_sphere(p: Vec3 | Sequence[float], r: float) -> float: + """ +Sphere of radius r at the origin: |p| − r. + +Rust: `spatial::sdf::sd_sphere` + """ + ... + +def sd_box(p: Vec3 | Sequence[float], half: Vec3 | Sequence[float]) -> float: + """ +Axis-aligned box with the given half extents. + +Rust: `spatial::sdf::sd_box` + """ + ... + +def sd_rounded_box(p: Vec3 | Sequence[float], half: Vec3 | Sequence[float], r: float) -> float: + """ +Box with edges rounded by radius r. + +Rust: `spatial::sdf::sd_rounded_box` + """ + ... + +def sd_torus(p: Vec3 | Sequence[float], major: float, minor: float) -> float: + """ +Torus in the xz-plane: major radius to the tube center, minor tube +radius. + +Rust: `spatial::sdf::sd_torus` + """ + ... + +def sd_capsule(p: Vec3 | Sequence[float], a: Vec3 | Sequence[float], b: Vec3 | Sequence[float], r: float) -> float: + """ +Capsule between a and b with radius r. + +Rust: `spatial::sdf::sd_capsule` + """ + ... + +def sd_cylinder(p: Vec3 | Sequence[float], a: Vec3 | Sequence[float], b: Vec3 | Sequence[float], r: float) -> float: + """ +Finite capped cylinder between a and b with radius r (exact). + +Rust: `spatial::sdf::sd_cylinder` + """ + ... + +def sd_cone(p: Vec3 | Sequence[float], angle: float, h: float) -> float: + """ +Infinite-precision capped cone with apex at the origin opening +along −y: half-angle `angle`, height h (IQ's sdCone, exact). + +Rust: `spatial::sdf::sd_cone` + """ + ... + +def sd_plane(p: Vec3 | Sequence[float], n: Vec3 | Sequence[float], d: float) -> float: + """ +Half-space n·p + d = 0 (n need not be unit; it is normalized). + +Rust: `spatial::sdf::sd_plane` + """ + ... + +def sd_ellipsoid(p: Vec3 | Sequence[float], radii: Vec3 | Sequence[float]) -> float: + """ +Ellipsoid with the given semi-axes (IQ's bound-improved +approximation; not exact away from the axes). + +Rust: `spatial::sdf::sd_ellipsoid` + """ + ... + +def sd_octahedron(p: Vec3 | Sequence[float], s: float) -> float: + """ +Regular octahedron with "radius" s (exact). + +Rust: `spatial::sdf::sd_octahedron` + """ + ... + +def sd_circle(p: Vec2 | Sequence[float], r: float) -> float: + """ +Circle of radius r at the origin. + +Rust: `spatial::sdf::sd_circle` + """ + ... + +def sd_rect(p: Vec2 | Sequence[float], half: Vec2 | Sequence[float]) -> float: + """ +Axis-aligned rectangle with the given half extents. + +Rust: `spatial::sdf::sd_rect` + """ + ... + +def sd_segment_2d(p: Vec2 | Sequence[float], a: Vec2 | Sequence[float], b: Vec2 | Sequence[float]) -> float: + """ +Unsigned distance to a 2-D segment minus nothing (a "line" SDF). + +Rust: `spatial::sdf::sd_segment_2d` + """ + ... + +def sd_polygon_2d(p: Vec2 | Sequence[float], poly: Polygon2) -> float: + """ +Signed distance to a simple polygon (negative inside). + +Rust: `spatial::sdf::sd_polygon_2d` + """ + ... + +def sd_hexagon(p: Vec2 | Sequence[float], r: float) -> float: + """ +Regular hexagon with circumscribed radius derived from apothem r +(IQ's sdHexagon: r is the apothem / inradius). + +Rust: `spatial::sdf::sd_hexagon` + """ + ... + +def sd_star(p: Vec2 | Sequence[float], r: float, n: int, m: float) -> float: + """ +n-pointed star with outer radius r and inner-radius factor set by m +(IQ's sdStar; m between 2 and n controls pointiness). + +Rust: `spatial::sdf::sd_star` + """ + ... + +def op_union(a: float, b: float) -> float: + """ +Union: min(a, b). + +Rust: `spatial::sdf::op_union` + """ + ... + +def op_subtract(a: float, b: float) -> float: + """ +Subtraction (a minus b): max(a, −b). + +Rust: `spatial::sdf::op_subtract` + """ + ... + +def op_intersect(a: float, b: float) -> float: + """ +Intersection: max(a, b). + +Rust: `spatial::sdf::op_intersect` + """ + ... + +def op_smooth_union(a: float, b: float, k: float) -> float: + """ +Polynomial smooth union with blending radius k. + +Rust: `spatial::sdf::op_smooth_union` + """ + ... + +def op_smooth_subtract(a: float, b: float, k: float) -> float: + """ +Smooth subtraction. + +Rust: `spatial::sdf::op_smooth_subtract` + """ + ... + +def op_smooth_intersect(a: float, b: float, k: float) -> float: + """ +Smooth intersection. + +Rust: `spatial::sdf::op_smooth_intersect` + """ + ... + +def op_round(d: float, r: float) -> float: + """ +Rounds a shape outward by r. + +Rust: `spatial::sdf::op_round` + """ + ... + +def op_onion(d: float, thickness: float) -> float: + """ +Hollows a shape into a shell of the given thickness. + +Rust: `spatial::sdf::op_onion` + """ + ... + +def op_repeat(p: Vec3 | Sequence[float], period: Vec3 | Sequence[float]) -> Vec3: + """ +Infinite domain repetition with the given period per axis +(returns the point folded into the central cell). + +Rust: `spatial::sdf::op_repeat` + """ + ... + +def op_repeat_limited(p: Vec3 | Sequence[float], period: Vec3 | Sequence[float], count: list[int]) -> Vec3: + """ +Limited repetition: at most `count` cells either side per axis. + +Rust: `spatial::sdf::op_repeat_limited` + """ + ... + +def op_mirror(p: Vec3 | Sequence[float], axes: list[bool]) -> Vec3: + """ +Mirror the chosen axes (|x| fold). + +Rust: `spatial::sdf::op_mirror` + """ + ... + +def op_twist(p: Vec3 | Sequence[float], k: float) -> Vec3: + """ +Twist about the y axis by k radians per unit height. + +Rust: `spatial::sdf::op_twist` + """ + ... + +def op_bend(p: Vec3 | Sequence[float], k: float) -> Vec3: + """ +Bend about the z axis with curvature k. + +Rust: `spatial::sdf::op_bend` + """ + ... + +def op_elongate(p: Vec3 | Sequence[float], h: Vec3 | Sequence[float]) -> Vec3: + """ +Elongation: stretches the shape by clamping the sample point. + +Rust: `spatial::sdf::op_elongate` + """ + ... + +def op_polar_repeat_2d(p: Vec2 | Sequence[float], n: int) -> Vec2: + """ +Polar repetition: folds the plane into one of n angular sectors. + +Rust: `spatial::sdf::op_polar_repeat_2d` + """ + ... + +def sdf_normal(f: Callable[[Vec3 | Sequence[float]], float], p: Vec3 | Sequence[float], eps: float) -> Vec3: + """ +Central-difference gradient normalized to a surface normal. + +Rust: `spatial::sdf::sdf_normal` + """ + ... + +def sdf_raymarch(f: Callable[[Vec3 | Sequence[float]], float], r: Ray, max_dist: float, eps: float, max_steps: int) -> Optional[RayHit]: + """ +Sphere tracing: march the ray by the SDF value until |f| < eps. + +Rust: `spatial::sdf::sdf_raymarch` + """ + ... + +def sdf_to_grid(f: Callable[[Vec3 | Sequence[float]], float], bounds: Aabb, res: list[int]) -> list[float]: + """ +Samples the SDF on a regular grid (x-fastest order: +`data[k*ny*nx + j*nx + i]`), suitable for marching cubes. + +Panics: +Panics if any resolution is < 2. + +Rust: `spatial::sdf::sdf_to_grid` + """ + ... + +def sdf_to_grid_2d(f: Callable[[Vec2 | Sequence[float]], float], bounds: Rect, res: list[int]) -> list[float]: + """ +2-D grid sampling (row-major, `data[j*nx + i]`). + +Panics: +Panics if any resolution is < 2. + +Rust: `spatial::sdf::sdf_to_grid_2d` + """ + ... + +def sdf_ambient_occlusion(f: Callable[[Vec3 | Sequence[float]], float], p: Vec3 | Sequence[float], n: Vec3 | Sequence[float], steps: int, step_size: float) -> float: + """ +Screen-space-style ambient occlusion: samples the SDF along the +normal; 1 = fully open, 0 = fully occluded. + +Rust: `spatial::sdf::sdf_ambient_occlusion` + """ + ... + +def sdf_soft_shadow(f: Callable[[Vec3 | Sequence[float]], float], origin: Vec3 | Sequence[float], dir: Vec3 | Sequence[float], k: float) -> float: + """ +IQ soft shadow: marches from `origin` along `dir` and darkens by the +closest approach scaled by k (larger k = harder shadow). Returns a +factor in [0, 1]. + +Rust: `spatial::sdf::sdf_soft_shadow` + """ + ... diff --git a/bindings/python/python/numeria/spatial/transform2d.pyi b/bindings/python/python/numeria/spatial/transform2d.pyi new file mode 100644 index 0000000..beda083 --- /dev/null +++ b/bindings/python/python/numeria/spatial/transform2d.pyi @@ -0,0 +1,42 @@ +""" +2-D affine transforms stored as 3×3 homogeneous matrices (last row 0 0 1), column-vector convention: p' = M·p. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.math import Vec2 + +class Affine2: + """ +Affine map of the plane. + +Rust: `spatial::transform2d::Affine2` + """ + @staticmethod + def identity() -> Affine2: ... + @staticmethod + def translation(t: Vec2 | Sequence[float]) -> Affine2: ... + @staticmethod + def rotation(angle: float) -> Affine2: ... + @staticmethod + def rotation_about(angle: float, center: Vec2 | Sequence[float]) -> Affine2: ... + @staticmethod + def scaling(sx: float, sy: float) -> Affine2: ... + @staticmethod + def shear(kx: float, ky: float) -> Affine2: ... + @staticmethod + def reflection(line_through_origin_dir: Vec2 | Sequence[float]) -> Affine2: ... + @staticmethod + def from_three_points(src: list[Vec2 | Sequence[float]], dst: list[Vec2 | Sequence[float]]) -> Optional[Affine2]: ... + def compose(self, other: Affine2) -> Affine2: ... + def apply(self, p: Vec2 | Sequence[float]) -> Vec2: ... + def apply_vector(self, v: Vec2 | Sequence[float]) -> Vec2: ... + def inverse(self) -> Optional[Affine2]: ... + def decompose(self) -> tuple[Vec2, float, Vec2, float]: ... + def is_rigid(self, tol: float) -> bool: ... + def is_similarity(self, tol: float) -> bool: ... + @property + def m(self) -> list[list[float]]: ... diff --git a/bindings/python/python/numeria/special/__init__.pyi b/bindings/python/python/numeria/special/__init__.pyi new file mode 100644 index 0000000..62efcb6 --- /dev/null +++ b/bindings/python/python/numeria/special/__init__.pyi @@ -0,0 +1,42 @@ +""" +Special functions: error function family, gamma family, and beta functions. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from . import bessel, beta, elliptic, erf, expint, gamma, legendre +from numeria.special.bessel import bessel_i0 as bessel_i0 +from numeria.special.bessel import bessel_i1 as bessel_i1 +from numeria.special.bessel import bessel_j0 as bessel_j0 +from numeria.special.bessel import bessel_j1 as bessel_j1 +from numeria.special.bessel import bessel_j_zeros as bessel_j_zeros +from numeria.special.bessel import bessel_jn as bessel_jn +from numeria.special.bessel import bessel_k0 as bessel_k0 +from numeria.special.bessel import bessel_k1 as bessel_k1 +from numeria.special.bessel import bessel_y0 as bessel_y0 +from numeria.special.bessel import bessel_y1 as bessel_y1 +from numeria.special.bessel import bessel_yn as bessel_yn +from numeria.special.beta import beta_inc as beta_inc +from numeria.special.expint import e1 as e1 +from numeria.special.elliptic import ellipse_perimeter_exact as ellipse_perimeter_exact +from numeria.special.elliptic import elliptic_e as elliptic_e +from numeria.special.elliptic import elliptic_e_inc as elliptic_e_inc +from numeria.special.elliptic import elliptic_f as elliptic_f +from numeria.special.elliptic import elliptic_k as elliptic_k +from numeria.special.erf import erfc as erfc +from numeria.special.erf import erfinv as erfinv +from numeria.special.expint import exponential_integral as exponential_integral +from numeria.special.gamma import gamma_p as gamma_p +from numeria.special.gamma import gamma_q as gamma_q +from numeria.special.legendre import gauss_legendre_nodes as gauss_legendre_nodes +from numeria.special.elliptic import jacobi_elliptic as jacobi_elliptic +from numeria.special.legendre import legendre_p as legendre_p +from numeria.special.legendre import legendre_p_assoc as legendre_p_assoc +from numeria.special.gamma import lgamma as lgamma +from numeria.special.elliptic import pendulum_period_exact as pendulum_period_exact +from numeria.special.legendre import spherical_harmonic_real as spherical_harmonic_real + + diff --git a/bindings/python/python/numeria/special/bessel.pyi b/bindings/python/python/numeria/special/bessel.pyi new file mode 100644 index 0000000..174b24c --- /dev/null +++ b/bindings/python/python/numeria/special/bessel.pyi @@ -0,0 +1,125 @@ +""" +Bessel functions of integer order. J and Y use the rational approximations of Numerical Recipes §6.5 (about 1e-8 absolute accuracy); higher orders use upward recurrence for Y and Miller's downward-recurrence algorithm for J when the argument is smaller than the order. I and K follow the polynomial approximations of Abramowitz & Stegun §9.8 (as in NR §6.6). +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def bessel_j0(x: float) -> float: + """ +Bessel function of the first kind, order 0: J₀(x). + +Rust: `special::bessel::bessel_j0` + """ + ... + +def bessel_j1(x: float) -> float: + """ +Bessel function of the first kind, order 1: J₁(x). + +Rust: `special::bessel::bessel_j1` + """ + ... + +def bessel_jn(n: int, x: float) -> float: + """ +Bessel function of the first kind, integer order n: Jₙ(x). +Upward recurrence for x > n; Miller's downward algorithm otherwise. + +Rust: `special::bessel::bessel_jn` + """ + ... + +def bessel_y0(x: float) -> float: + """ +Bessel function of the second kind, order 0: Y₀(x). + +Panics: +Panics unless x > 0. + +Rust: `special::bessel::bessel_y0` + """ + ... + +def bessel_y1(x: float) -> float: + """ +Bessel function of the second kind, order 1: Y₁(x). + +Panics: +Panics unless x > 0. + +Rust: `special::bessel::bessel_y1` + """ + ... + +def bessel_yn(n: int, x: float) -> float: + """ +Bessel function of the second kind, integer order n: Yₙ(x), by +stable upward recurrence. + +Panics: +Panics unless x > 0. + +Rust: `special::bessel::bessel_yn` + """ + ... + +def bessel_i0(x: float) -> float: + """ +Modified Bessel function of the first kind, order 0: I₀(x). + +Rust: `special::bessel::bessel_i0` + """ + ... + +def bessel_i1(x: float) -> float: + """ +Modified Bessel function of the first kind, order 1: I₁(x). + +Rust: `special::bessel::bessel_i1` + """ + ... + +def bessel_k0(x: float) -> float: + """ +Modified Bessel function of the second kind, order 0: K₀(x). + +Panics: +Panics unless x > 0. + +Rust: `special::bessel::bessel_k0` + """ + ... + +def bessel_k1(x: float) -> float: + """ +Modified Bessel function of the second kind, order 1: K₁(x). + +Panics: +Panics unless x > 0. + +Rust: `special::bessel::bessel_k1` + """ + ... + +def bessel_j_zeros(n: int, count: int) -> list[float]: + """ +First `count` positive zeros of Jₙ, found by scanning for sign +changes (step π/4 starting past x = n) and refining each bracket +with Brent's method. + +Rust: `special::bessel::bessel_j_zeros` + """ + ... + +def bessel_j_zeros_checked(n: int, count: int) -> list[float]: + """ +Convenience alias family used by some references: Jₙ zeros with a +`Result` wrapper for invalid counts. + +Rust: `special::bessel::bessel_j_zeros_checked` + """ + ... diff --git a/bindings/python/python/numeria/special/beta.pyi b/bindings/python/python/numeria/special/beta.pyi new file mode 100644 index 0000000..d194105 --- /dev/null +++ b/bindings/python/python/numeria/special/beta.pyi @@ -0,0 +1,33 @@ +""" +Beta function and regularized incomplete beta. B(a,b) = Γ(a)Γ(b)/Γ(a+b), evaluated in log space. The regularized incomplete beta I_x(a,b) uses the modified Lentz continued fraction of Numerical Recipes §6.4. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.statistics.distributions import Beta + +def beta(a: float, b: float) -> float: + """ +Complete beta function B(a,b) = Γ(a)Γ(b)/Γ(a+b). + +Panics: +Panics unless a > 0 and b > 0. + +Rust: `special::beta::beta` + """ + ... + +def beta_inc(a: float, b: float, x: float) -> float: + """ +Regularized incomplete beta function I_x(a,b), the CDF of the Beta +distribution. + +Panics: +Panics unless a > 0, b > 0, and x ∈ [0, 1]. + +Rust: `special::beta::beta_inc` + """ + ... diff --git a/bindings/python/python/numeria/special/elliptic.pyi b/bindings/python/python/numeria/special/elliptic.pyi new file mode 100644 index 0000000..38c496b --- /dev/null +++ b/bindings/python/python/numeria/special/elliptic.pyi @@ -0,0 +1,96 @@ +""" +Elliptic integrals and physical applications. Complete integrals use the arithmetic-geometric mean (Abramowitz & Stegun §17.6); incomplete integrals use the Carlson symmetric forms R_F and R_D (Carlson 1979; NR §6.11). The parameter is m = k². +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def elliptic_k(m: float) -> float: + """ +Complete elliptic integral of the first kind K(m), parameter m = k²: +K(m) = ∫₀^{π/2} dθ/√(1 − m·sin²θ) = π / (2·AGM(1, √(1−m))). + +Panics: +Panics unless 0 ≤ m < 1. + +Rust: `special::elliptic::elliptic_k` + """ + ... + +def elliptic_e(m: float) -> float: + """ +Complete elliptic integral of the second kind E(m), parameter m = k²: +E(m) = ∫₀^{π/2} √(1 − m·sin²θ) dθ, via the AGM with the +c²-correction sum: E = K·(1 − Σ 2^{n−1}·cₙ²). + +Panics: +Panics unless 0 ≤ m ≤ 1 (E(1) = 1 exactly). + +Rust: `special::elliptic::elliptic_e` + """ + ... + +def elliptic_f(phi: float, m: float) -> float: + """ +Incomplete elliptic integral of the first kind F(φ | m) via Carlson +R_F: F = sinφ·R_F(cos²φ, 1 − m·sin²φ, 1). + +Panics: +Panics unless 0 ≤ φ ≤ π/2 and m·sin²φ < 1. + +Rust: `special::elliptic::elliptic_f` + """ + ... + +def elliptic_e_inc(phi: float, m: float) -> float: + """ +Incomplete elliptic integral of the second kind E(φ | m) via Carlson +forms: E = sinφ·R_F − (m/3)·sin³φ·R_D. + +Panics: +Panics unless 0 ≤ φ ≤ π/2 and m·sin²φ < 1. + +Rust: `special::elliptic::elliptic_e_inc` + """ + ... + +def pendulum_period_exact(length: float, g: float, amplitude_rad: float) -> float: + """ +Exact large-amplitude pendulum period: +T = 4·√(L/g)·K(sin²(θ₀/2)). + +Reduces to 2π√(L/g) as the amplitude → 0. Fails with +`InvalidArgument` for non-positive length/gravity or amplitude +outside [0, π). + +Rust: `special::elliptic::pendulum_period_exact` + """ + ... + +def ellipse_perimeter_exact(a: float, b: float) -> float: + """ +Exact ellipse perimeter: P = 4·a·E(m) with m = 1 − (b/a)² for +a ≥ b (arguments may be given in either order). + +Panics: +Panics unless both semi-axes are positive. + +Rust: `special::elliptic::ellipse_perimeter_exact` + """ + ... + +def jacobi_elliptic(u: float, m: float) -> tuple[float, float, float]: + """ +Jacobi elliptic functions (sn, cn, dn) of real argument u with +parameter m = k², by the descending Gauss/AGM transformation +(Abramowitz & Stegun §16.4). + +Panics: +Panics unless 0 ≤ m ≤ 1. + +Rust: `special::elliptic::jacobi_elliptic` + """ + ... diff --git a/bindings/python/python/numeria/special/erf.pyi b/bindings/python/python/numeria/special/erf.pyi new file mode 100644 index 0000000..4d84121 --- /dev/null +++ b/bindings/python/python/numeria/special/erf.pyi @@ -0,0 +1,36 @@ +""" +Error function family. `erf`/`erfc` implement W. J. Cody's rational Chebyshev approximations ("Rational Chebyshev approximation for the error function", Math. Comp. 23, 1969; the SPECFUN `CALERF` algorithm), accurate to full double precision. `erfinv` uses M. Giles' polynomial approximation ("Approximating the erfinv function", GPU Computing Gems, 2012) polished with Newton steps on `erf`. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def erf(x: float) -> float: + """ +Error function erf(x) = (2/√π)·∫₀ˣ e^(−t²) dt, full double precision. + +Rust: `special::erf::erf` + """ + ... + +def erfc(x: float) -> float: + """ +Complementary error function erfc(x) = 1 − erf(x), computed without +cancellation for large positive x. + +Rust: `special::erf::erfc` + """ + ... + +def erfinv(p: float) -> float: + """ +Inverse error function: erfinv(erf(x)) = x for p ∈ (−1, 1). + +Returns ±∞ at p = ±1 and NaN outside [−1, 1]. + +Rust: `special::erf::erfinv` + """ + ... diff --git a/bindings/python/python/numeria/special/expint.pyi b/bindings/python/python/numeria/special/expint.pyi new file mode 100644 index 0000000..f9524e7 --- /dev/null +++ b/bindings/python/python/numeria/special/expint.pyi @@ -0,0 +1,31 @@ +""" +Exponential integrals Ei(x) and E1(x). +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.statistics.distributions import Exponential + +def e1(x: float) -> float: + """ +Exponential integral E1(x) = ∫_x^∞ e^{-t}/t dt for x > 0. + +Power series for x ≤ 1, continued fraction (modified Lentz) for x > 1. +Returns infinity at x = 0 and NaN for x < 0. + +Rust: `special::expint::e1` + """ + ... + +def exponential_integral(x: float) -> float: + """ +Exponential integral Ei(x) (Cauchy principal value for x > 0). + +For x < 0, Ei(x) = -E1(-x). Returns -infinity at x = 0. + +Rust: `special::expint::exponential_integral` + """ + ... diff --git a/bindings/python/python/numeria/special/gamma.pyi b/bindings/python/python/numeria/special/gamma.pyi new file mode 100644 index 0000000..86cf41f --- /dev/null +++ b/bindings/python/python/numeria/special/gamma.pyi @@ -0,0 +1,55 @@ +""" +Gamma function family. `gamma` uses the Lanczos approximation (g = 7, n = 9); `lgamma` is the same approximation carried in log space so it does not overflow up to very large arguments. The regularized incomplete functions P(a,x) (`gamma_p`) and Q(a,x) (`gamma_q`) follow Numerical Recipes ch. 6.2 (series for x < a+1, Lentz continued fraction otherwise). +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.statistics.distributions import Gamma + +def gamma(z: float) -> float: + """ +Gamma function Γ(z) via the Lanczos approximation, with the +reflection formula for z < 0.5. + +Rust: `special::gamma::gamma` + """ + ... + +def lgamma(z: float) -> float: + """ +Natural log of |Γ(z)|, computed in log space so it stays finite up to +z ≈ 1e300 (e.g. `lgamma(1e6)` is exact to ~1e-13 relative). + +Panics: +Panics for z ≤ 0 (poles and the reflection region are out of scope +for the real-valued solvers this supports). + +Rust: `special::gamma::lgamma` + """ + ... + +def gamma_p(a: float, x: float) -> float: + """ +Regularized lower incomplete gamma P(a,x) = γ(a,x)/Γ(a). + +Panics: +Panics unless a > 0 and x ≥ 0. + +Rust: `special::gamma::gamma_p` + """ + ... + +def gamma_q(a: float, x: float) -> float: + """ +Regularized upper incomplete gamma Q(a,x) = 1 − P(a,x), computed by +continued fraction for x > a + 1 so large-x values keep precision. + +Panics: +Panics unless a > 0 and x ≥ 0. + +Rust: `special::gamma::gamma_q` + """ + ... diff --git a/bindings/python/python/numeria/special/legendre.pyi b/bindings/python/python/numeria/special/legendre.pyi new file mode 100644 index 0000000..4c1d31f --- /dev/null +++ b/bindings/python/python/numeria/special/legendre.pyi @@ -0,0 +1,56 @@ +""" +Legendre polynomials, associated Legendre functions, real spherical harmonics, and Gauss-Legendre quadrature nodes. References: Abramowitz & Stegun ch. 8; Numerical Recipes §6.8 (`plgndr`) and §4.5 (`gauleg`). +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def legendre_p(n: int, x: float) -> float: + """ +Legendre polynomial Pₙ(x) by the Bonnet recurrence +(n+1)·P_{n+1} = (2n+1)·x·Pₙ − n·P_{n−1}. + +Rust: `special::legendre::legendre_p` + """ + ... + +def legendre_p_assoc(l: int, m: int, x: float) -> float: + """ +Associated Legendre function Pₗᵐ(x) with the Condon-Shortley phase, +for |x| ≤ 1. Negative m uses +Pₗ^{−m} = (−1)ᵐ (l−m)!/(l+m)! Pₗᵐ. + +Panics: +Panics unless |m| ≤ l and |x| ≤ 1. + +Rust: `special::legendre::legendre_p_assoc` + """ + ... + +def spherical_harmonic_real(l: int, m: int, theta: float, phi: float) -> float: + """ +Real spherical harmonic Yₗₘ(θ, φ) (orthonormal on the sphere): +m > 0 pairs with cos(mφ), m < 0 with sin(|m|φ). + +Panics: +Panics unless |m| ≤ l. + +Rust: `special::legendre::spherical_harmonic_real` + """ + ... + +def gauss_legendre_nodes(n: int) -> tuple[list[float], list[float]]: + """ +Nodes and weights of the n-point Gauss-Legendre rule on [−1, 1] +(NR `gauleg`: Newton iteration on Pₙ from the Chebyshev-like initial +guess). Integrates polynomials up to degree 2n−1 exactly. + +Panics: +Panics if n = 0. + +Rust: `special::legendre::gauss_legendre_nodes` + """ + ... diff --git a/bindings/python/python/numeria/statistical_mechanics/__init__.pyi b/bindings/python/python/numeria/statistical_mechanics/__init__.pyi new file mode 100644 index 0000000..942a388 --- /dev/null +++ b/bindings/python/python/numeria/statistical_mechanics/__init__.pyi @@ -0,0 +1,181 @@ +""" +Statistical mechanics: the elementary relations here, with lattice models and Monte Carlo in submodules. The roadmap calls this area `statmech`; it lives under the existing `statistical_mechanics` module instead, so that there is one home for the subject rather than two. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from . import ising, kinetics, lattice_models, md + +def einstein_diffusion(temperature: float, dynamic_viscosity: float, particle_radius: float) -> float: + """ +Stokes-Einstein diffusion coefficient: D = k_B × T / (6π × μ × r) + +Rust: `statistical_mechanics::einstein_diffusion` + """ + ... + +def mean_square_displacement(diffusion_coeff: float, time: float, dimensions: int) -> float: + """ +Mean square displacement: ⟨r²⟩ = 2nDt where n = number of spatial dimensions + +Rust: `statistical_mechanics::mean_square_displacement` + """ + ... + +def rms_displacement(diffusion_coeff: float, time: float, dimensions: int) -> float: + """ +Root-mean-square displacement: √(⟨r²⟩) + +Rust: `statistical_mechanics::rms_displacement` + """ + ... + +def fick_first_law(diffusion_coeff: float, concentration_gradient: float) -> float: + """ +Fick's first law: J = -D × (dc/dx) + +Rust: `statistical_mechanics::fick_first_law` + """ + ... + +def fick_second_law_step_1d(concentrations: MutableSequence[float], dx: float, dt: float, diffusion_coeff: float) -> None: + """ +Fick's second law via explicit finite-difference step in 1D. +Updates `concentrations` in place. Boundary cells (first and last) are held fixed. + +Rust: `statistical_mechanics::fick_second_law_step_1d` + """ + ... + +def diffusion_length(diffusion_coeff: float, time: float) -> float: + """ +Characteristic diffusion length: L = √(2Dt) + +Rust: `statistical_mechanics::diffusion_length` + """ + ... + +def diffusion_time(diffusion_coeff: float, length: float) -> float: + """ +Time required to diffuse a given length: t = L² / (2D) + +Rust: `statistical_mechanics::diffusion_time` + """ + ... + +def maxwell_speed_distribution(mass: float, temperature: float, speed: float) -> float: + """ +Maxwell speed distribution: f(v) = 4π × (m/(2πk_BT))^(3/2) × v² × exp(-mv²/(2k_BT)) + +Rust: `statistical_mechanics::maxwell_speed_distribution` + """ + ... + +def most_probable_speed(mass: float, temperature: float) -> float: + """ +Most probable speed: v_p = √(2k_BT / m) + +Rust: `statistical_mechanics::most_probable_speed` + """ + ... + +def mean_speed(mass: float, temperature: float) -> float: + """ +Mean speed: v̄ = √(8k_BT / (πm)) + +Rust: `statistical_mechanics::mean_speed` + """ + ... + +def rms_speed_maxwell(mass: float, temperature: float) -> float: + """ +RMS speed from Maxwell-Boltzmann: v_rms = √(3k_BT / m) + +Rust: `statistical_mechanics::rms_speed_maxwell` + """ + ... + +def equipartition_energy(degrees_of_freedom: int, temperature: float) -> float: + """ +Equipartition energy: E = (f/2) × k_B × T + +Rust: `statistical_mechanics::equipartition_energy` + """ + ... + +def equipartition_heat_capacity(degrees_of_freedom: int) -> float: + """ +Equipartition heat capacity per particle: Cv = (f/2) × k_B + +Rust: `statistical_mechanics::equipartition_heat_capacity` + """ + ... + +def boltzmann_factor(energy: float, temperature: float) -> float: + """ +Boltzmann factor: exp(-E / (k_B × T)) + +Rust: `statistical_mechanics::boltzmann_factor` + """ + ... + +def boltzmann_probability(energy: float, temperature: float, partition_function: float) -> float: + """ +Boltzmann probability: P = exp(-E/(k_BT)) / Z + +Rust: `statistical_mechanics::boltzmann_probability` + """ + ... + +def partition_function_harmonic(temperature: float, frequency: float) -> float: + """ +Partition function for a quantum harmonic oscillator: Z = 1 / (1 - exp(-hf/(k_BT))) + +Rust: `statistical_mechanics::partition_function_harmonic` + """ + ... + +def mean_energy_harmonic(temperature: float, frequency: float) -> float: + """ +Mean energy of a quantum harmonic oscillator (includes zero-point energy): +⟨E⟩ = hf / (exp(hf/(k_BT)) - 1) + hf/2 + +Rust: `statistical_mechanics::mean_energy_harmonic` + """ + ... + +def debye_temperature(max_frequency: float) -> float: + """ +Debye temperature: Θ_D = h × f_max / k_B + +Rust: `statistical_mechanics::debye_temperature` + """ + ... + +def debye_heat_capacity_high_t(n_atoms: float) -> float: + """ +Dulong-Petit limit (high-T Debye heat capacity): Cv = 3Nk_B + +Rust: `statistical_mechanics::debye_heat_capacity_high_t` + """ + ... + +def debye_heat_capacity_low_t(n_atoms: float, temperature: float, debye_temp: float) -> float: + """ +Low-temperature Debye heat capacity: Cv = (12/5)π⁴Nk_B(T/Θ_D)³ + +Rust: `statistical_mechanics::debye_heat_capacity_low_t` + """ + ... + +def einstein_heat_capacity(n_atoms: float, temperature: float, einstein_temp: float) -> float: + """ +Einstein model heat capacity: +Cv = 3Nk_B × (Θ_E/T)² × exp(Θ_E/T) / (exp(Θ_E/T) - 1)² + +Rust: `statistical_mechanics::einstein_heat_capacity` + """ + ... diff --git a/bindings/python/python/numeria/statistical_mechanics/ising.pyi b/bindings/python/python/numeria/statistical_mechanics/ising.pyi new file mode 100644 index 0000000..c22110f --- /dev/null +++ b/bindings/python/python/numeria/statistical_mechanics/ising.pyi @@ -0,0 +1,336 @@ +""" +The Ising model and its relatives, by Monte Carlo. The two-dimensional Ising model is the one interacting system with a phase transition that is solved exactly, so it is where a Monte Carlo code can be checked against arithmetic rather than against another Monte Carlo code. Onsager's solution gives the critical temperature, the energy and the spontaneous magnetisation in closed form, and any sampler that disagrees with them is wrong. The algorithmic point of the module is the contrast between the two updates. Metropolis flips one spin at a time, so near the critical temperature -- where the correlation length diverges and whole regions must turn over together -- successive configurations stay correlated for a time growing as the system size to a power near two. Wolff builds a cluster whose size is itself set by the correlation length and flips it whole, which all but removes that critical slowing down. The two sample the same distribution; they differ only in how long it takes. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.statistics.distributions import Exponential +from numeria.monte_carlo import Rng +from numeria.stochastic.timeseries import Var + +class Ising2D: + """ +A square-lattice Ising model with nearest-neighbour coupling. + +`H = -j sum_ s_i s_j - h sum_i s_i` with spins `+/-1`, and `beta` the +inverse temperature in units where Boltzmann's constant is one. + +Rust: `statistical_mechanics::ising::Ising2D` + """ + def __init__(self, n: int, spins: list[int], j: float, h: float, beta: float, periodic: bool) -> None: ... + @staticmethod + def cold(n: int, j: float, h: float, beta: float, periodic: bool) -> Ising2D: ... + @staticmethod + def random(n: int, j: float, h: float, beta: float, periodic: bool, rng: Rng) -> Ising2D: ... + def energy(self) -> float: ... + def energy_per_site(self) -> float: ... + def magnetization(self) -> float: ... + def metropolis_sweep(self, rng: Rng) -> None: ... + def heat_bath_sweep(self, rng: Rng) -> None: ... + def wolff_cluster_step(self, rng: Rng) -> int: ... + def sample(self, sweeps: int, thermalize: int, measure_every: int, use_wolff: bool, rng: Rng) -> IsingStats: ... + def correlation_function(self, r: int) -> float: ... + def sample_correlations(self, updates: int, use_wolff: bool, rng: Rng) -> tuple[list[float], float]: ... + @staticmethod + def correlation_length_estimate(correlations: list[float], background: float) -> float: ... + def autocorrelation_time(self, updates: int, use_wolff: bool, rng: Rng) -> tuple[float, float]: ... + @property + def n(self) -> int: ... + @property + def spins(self) -> list[int]: ... + @property + def j(self) -> float: ... + @property + def h(self) -> float: ... + @property + def beta(self) -> float: ... + @property + def periodic(self) -> bool: ... + +class IsingStats: + """ +Summary statistics from a Monte Carlo run. + +Rust: `statistical_mechanics::ising::IsingStats` + """ + def __init__(self, e_mean: float, e_var: float, m_mean: float, m_abs: float, susceptibility: float, heat_capacity: float, binder_cumulant: float, samples: int) -> None: ... + @property + def e_mean(self) -> float: ... + @property + def e_var(self) -> float: ... + @property + def m_mean(self) -> float: ... + @property + def m_abs(self) -> float: ... + @property + def susceptibility(self) -> float: ... + @property + def heat_capacity(self) -> float: ... + @property + def binder_cumulant(self) -> float: ... + @property + def samples(self) -> int: ... + +class Potts2D: + """ +The `q`-state Potts model on a square lattice. + +Generalises Ising, which is the two-state case. The transition turns +first order above `q = 4` in two dimensions, which is why the model is the +standard example that the *order* of a transition is not a detail of the +interaction but a consequence of the symmetry. + +Rust: `statistical_mechanics::ising::Potts2D` + """ + def __init__(self, q: int, n: int, states: list[int], j: float, beta: float) -> None: ... + @staticmethod + def random(q: int, n: int, j: float, beta: float, rng: Rng) -> Potts2D: ... + def energy(self) -> float: ... + def metropolis_sweep(self, rng: Rng) -> None: ... + def order_parameter(self) -> float: ... + @property + def q(self) -> int: ... + @property + def n(self) -> int: ... + @property + def states(self) -> list[int]: ... + @property + def j(self) -> float: ... + @property + def beta(self) -> float: ... + +class XyModel2D: + """ +The two-dimensional XY model: continuous spins on a square lattice. + +It has no ordered phase at any positive temperature -- a continuous +symmetry cannot break in two dimensions -- and yet it has a transition, +where vortices unbind. That the transition exists without an order +parameter is what makes it interesting. + +Rust: `statistical_mechanics::ising::XyModel2D` + """ + def __init__(self, n: int, theta: list[float], j: float, beta: float) -> None: ... + @staticmethod + def random(n: int, j: float, beta: float, rng: Rng) -> XyModel2D: ... + def energy(self) -> float: ... + def metropolis_sweep(self, rng: Rng, step: float) -> None: ... + def plaquette_vorticity(self, row: int, column: int) -> int: ... + def vortex_count(self) -> tuple[int, int]: ... + @staticmethod + def kt_transition_estimate(j: float) -> float: ... + @property + def n(self) -> int: ... + @property + def theta(self) -> list[float]: ... + @property + def j(self) -> float: ... + @property + def beta(self) -> float: ... + +def ising_tc_exact() -> float: + """ +The exact critical temperature of the two-dimensional Ising model: +`2 / ln(1 + sqrt 2)`. + +About 2.269. Kramers and Wannier found it from a duality argument years +before Onsager solved the model, without ever computing the free energy -- +the self-dual point has to be the transition if there is only one. + +Rust: `statistical_mechanics::ising::ising_tc_exact` + """ + ... + +def onsager_magnetization(beta: float, j: float) -> float: + """ +Onsager's spontaneous magnetisation, zero above the critical temperature. + +`(1 - sinh^-4(2 beta j))^(1/8)`. The exponent one eighth is the critical +exponent beta, and its being a simple fraction rather than the one half +that mean-field theory predicts is the whole reason the exact solution +mattered. + +Errors: +Returns an error for a non-positive coupling or inverse temperature. + +Rust: `statistical_mechanics::ising::onsager_magnetization` + """ + ... + +def onsager_energy(beta: float, j: float) -> float: + """ +Onsager's energy per site of the infinite lattice. + +Involves a complete elliptic integral, which is where the logarithmic +divergence of the heat capacity at the critical point comes from: the +integral's derivative diverges exactly at the self-dual point. + +Errors: +Returns an error for a non-positive coupling or inverse temperature. + +Rust: `statistical_mechanics::ising::onsager_energy` + """ + ... + +def ising_1d_exact(beta: float, j: float, h: float) -> tuple[float, float]: + """ +The one-dimensional Ising chain by transfer matrix, returning the free +energy per site and the magnetisation per site. + +The chain has no transition at any positive temperature, which is Ising's +own result and the reason he thought the model uninteresting. The transfer +matrix shows why: the free energy is the logarithm of the larger +eigenvalue of a two-by-two matrix with strictly positive entries, and such +an eigenvalue is analytic in the temperature. + +Errors: +Returns an error for a non-positive inverse temperature. + +Rust: `statistical_mechanics::ising::ising_1d_exact` + """ + ... + +def partition_function_exact_small(energy: Callable[[int], float], sites: int, beta: float) -> float: + """ +The partition function of a small system by direct enumeration. + +Exponential in the site count, so it stops at about twenty-four spins -- +but within that range it is exact, which makes it the reference every +sampler here is checked against. + +Errors: +Returns an error above twenty-four sites or for a non-positive beta. + +Rust: `statistical_mechanics::ising::partition_function_exact_small` + """ + ... + +def free_energy_from_z(z: float, beta: float) -> float: + """ +The free energy from a partition function. + +Errors: +Returns an error for a non-positive partition function or beta. + +Rust: `statistical_mechanics::ising::free_energy_from_z` + """ + ... + +def thermodynamics_exact_small(energy: Callable[[int], float], sites: int, beta: float) -> tuple[float, float]: + """ +The mean energy and entropy of a small system by enumeration. + +Errors: +Returns an error on the same conditions as +`partition_function_exact_small`. + +Rust: `statistical_mechanics::ising::thermodynamics_exact_small` + """ + ... + +def potts_tc_exact(q: int) -> float: + """ +The exact critical temperature of the `q`-state Potts model in two +dimensions: `1 / ln(1 + sqrt q)`. + +Reduces to the Ising value at `q = 2`, as it must. + +Errors: +Returns an error for fewer than two states. + +Rust: `statistical_mechanics::ising::potts_tc_exact` + """ + ... + +def wang_landau(energy: Callable[[int], int], sites: int, flatness: float, final_modification: float, max_steps: int, rng: Rng) -> list[float]: + """ +Wang-Landau sampling: the density of states as a function of energy. + +Rather than sampling the Boltzmann distribution at one temperature, this +performs a random walk in *energy* with acceptance `min(1, g(E_old) / +g(E_new))`, refining the estimate `g` as it goes so that the walk flattens +its own histogram. The result gives every temperature at once, which is +what a canonical simulation cannot do: it converges on the *entropy*, not +on an average. + +Returns the logarithm of the density of states, indexed by the energy +level offset from the minimum. + +Errors: +Returns an error for bad parameters or an energy range that does not fit. + +Rust: `statistical_mechanics::ising::wang_landau` + """ + ... + +def canonical_from_dos(log_g: list[float], lowest_energy: float, step: float, beta: float) -> tuple[float, float]: + """ +Canonical averages reconstructed from a density of states. + +The whole point of Wang-Landau: one run gives every temperature. Returns +the mean energy and the heat capacity at the given inverse temperature. + +Errors: +Returns an error for an empty density or a non-positive beta. + +Rust: `statistical_mechanics::ising::canonical_from_dos` + """ + ... + +def parallel_tempering_ising(n: int, j: float, betas: list[float], sweeps: int, thermalize: int, rng: Rng) -> tuple[list[IsingStats], float]: + """ +Parallel tempering: several replicas at different temperatures, with +neighbouring pairs occasionally swapped. + +The swap acceptance `min(1, exp((beta_i - beta_j)(E_i - E_j)))` preserves +each replica's own equilibrium distribution while letting a cold replica +escape a local minimum by wandering up to a hot temperature and back. It +is the standard answer to a rugged landscape, and it costs nothing in +correctness -- the swaps satisfy detailed balance on the joint system. + +Returns the statistics for each temperature and the swap acceptance rate. + +Errors: +Returns an error for fewer than two temperatures or bad sweep counts. + +Rust: `statistical_mechanics::ising::parallel_tempering_ising` + """ + ... + +def binder_crossing(temperatures: list[float], curves: list[list[float]]) -> float: + """ +The Binder crossing estimate of the critical temperature. + +The Binder cumulant is dimensionless, so its finite-size corrections +cancel at the critical point and curves for different lattice sizes cross +there. That makes it far more accurate than looking for a peak in the +susceptibility, whose position drifts with the size. + +`curves[i]` is the cumulant of lattice `sizes[i]` at each of the given +temperatures. + +Errors: +Returns an error for mismatched lengths or fewer than two sizes. + +Rust: `statistical_mechanics::ising::binder_crossing` + """ + ... + +def fluctuation_dissipation_check(stats: IsingStats, beta: float, sites: int) -> float: + """ +The fluctuation-dissipation check: the heat capacity computed from the +energy variance against the same quantity differentiated numerically. + +Returns the relative discrepancy. The identity `C = beta^2 Var(E)` is not +a modelling assumption but a consequence of the Boltzmann distribution, so +a sampler that violates it is not sampling that distribution. + +Errors: +Returns an error for a non-positive beta or a zero heat capacity. + +Rust: `statistical_mechanics::ising::fluctuation_dissipation_check` + """ + ... diff --git a/bindings/python/python/numeria/statistical_mechanics/kinetics.pyi b/bindings/python/python/numeria/statistical_mechanics/kinetics.pyi new file mode 100644 index 0000000..65b5e51 --- /dev/null +++ b/bindings/python/python/numeria/statistical_mechanics/kinetics.pyi @@ -0,0 +1,681 @@ +""" +Chemical kinetics: rate laws, deterministic and stochastic reaction networks, enzyme saturation, equilibrium composition, oscillating mechanisms, nucleation and transformation, and the acid-base and electrochemical relations that share their arithmetic. # What lives here and what lives in `chemistry` The elementary single-formula relations -- the Arrhenius rate, the equilibrium constant from a free energy, the Nernst potential, pH from a proton concentration -- are already in `chemistry`, and are not duplicated. This module is the part that needs a solver: networks integrated in time, fits inverted from data, compositions found by root-finding, and the stochastic algorithms. # Units Concentrations are molar, times are seconds, energies are joules per mole and temperatures are kelvin, so `R` rather than `k_B` appears throughout. The one exception is `kramers_rate_check`, which follows its own literature convention of barrier heights in units of `k_B T`; it is marked at the function. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.finance.options import Barrier +from numeria.fractals.automata import Brusselator +from numeria.linalg.matrix import Matrix +from numeria.statistics.distributions import Poisson +from numeria.monte_carlo import Rng + +class Inhibition: + """ +Which way an inhibitor acts. + +Rust: `statistical_mechanics::kinetics::Inhibition` + """ + ... + +class Reaction: + """ +One elementary reaction, as species indices with their stoichiometric +coefficients. + +Rust: `statistical_mechanics::kinetics::Reaction` + """ + def __init__(self, reactants: list[tuple[int, int]], products: list[tuple[int, int]]) -> None: ... + def order(self) -> int: ... + def net_change(self, species: int) -> list[int]: ... + @property + def reactants(self) -> list[tuple[int, int]]: ... + @property + def products(self) -> list[tuple[int, int]]: ... + +def stoichiometry_matrix(reactions: list[Reaction], species: int) -> Matrix: + """ +The stoichiometry matrix: species by reaction, each entry the net change +in that species when that reaction fires once. + +Errors: +Returns an error for no reactions, no species, or a species index outside +the declared count. + +Rust: `statistical_mechanics::kinetics::stoichiometry_matrix` + """ + ... + +def mass_action_rates(reactions: list[Reaction], k: list[float], concentrations: list[float]) -> list[float]: + """ +The deterministic mass-action rate of each reaction at a composition. + +`v_j = k_j prod_i c_i^m_ij`. Note the contrast with the stochastic +propensity in `gillespie_ssa`, which uses a falling factorial rather +than a power: a bimolecular reaction of a species with itself has rate +`k c^2` in the continuum and `k x (x - 1) / 2` in molecule counts, and +the two agree only when the count is large. Conflating them is the +classic way to get a stochastic simulation that quietly disagrees with +its own rate equations. + +Errors: +Returns an error for a rate constant per reaction mismatch, a negative +rate constant, or a species index outside the composition. + +Rust: `statistical_mechanics::kinetics::mass_action_rates` + """ + ... + +def rate_equations(stoich: Matrix | Sequence[Sequence[float]], rates: Callable[[list[float]], list[float]], c0: list[float], t_end: float, rtol: float) -> list[tuple[float, list[float]]]: + """ +Integrates a reaction network in time with an adaptive implicit method. + +Chemical networks are almost always stiff -- a fast pre-equilibrium +alongside a slow overall conversion means the fastest and slowest +timescales differ by orders of magnitude -- and an explicit integrator is +then limited by the *fastest* one long after it has ceased to matter. +The step here is backward Euler -- A-stable, and L-stable, so a mode far +faster than the step is damped rather than merely bounded -- taken once +at the full step and twice at half. The difference is the local error +estimate, and their Richardson combination `2 y_half - y_full` is the +second-order value actually kept. + +A multistep formula would be the conventional choice and is the wrong one +here: BDF2 assumes a uniform step, and an adaptive controller varies it +every step, so the history it is handed is at the wrong spacing and the +resulting inconsistency dominates the error estimate. A one-step method +with Richardson has no history to get wrong. + +The step is limited by the solution's own timescale as well as by the +error estimate, and that second limit is not redundant. On an +*oscillatory* system step doubling can be fooled outright: an L-stable +method damps hard at a step much longer than the period, so the coarse +and fine solutions both collapse toward the fixed point, agree closely +with each other, and report a small error -- whereupon the controller +grows the step further. A run can end up stepping clean over whole +oscillations while its error estimate reports success. Bounding the step +by `|c| / |dc/dt|` prevents that, because it looks at the dynamics rather +than at the difference between two equally wrong answers. + +Returns `(time, composition)` at each accepted step. + +Errors: +Returns an error for a mismatched initial composition, a non-positive +end time or tolerance, or if the Newton iteration inside a step fails to +converge even at the smallest permitted step. + +Rust: `statistical_mechanics::kinetics::rate_equations` + """ + ... + +def gillespie_ssa(reactions: list[Reaction], k: list[float], x0: list[int], t_end: float, max_events: int, rng: Rng) -> list[tuple[float, list[int]]]: + """ +Gillespie's direct method: an exact realisation of the chemical master +equation. + +Exact in a strong sense -- the trajectory is drawn from the true +distribution of the jump process, with no time discretisation at all. +The waiting time to the next event is exponential with rate equal to the +total propensity, and which reaction fires is chosen in proportion to +its own. Returns `(time, counts)` after each event, including the +initial state. + +Errors: +Returns an error for a malformed network or a non-positive end time. + +Rust: `statistical_mechanics::kinetics::gillespie_ssa` + """ + ... + +def tau_leaping(reactions: list[Reaction], k: list[float], x0: list[int], t_end: float, tau: float, rng: Rng) -> list[tuple[float, list[int]]]: + """ +Explicit tau-leaping: many reaction events per step, each count drawn +from a Poisson distribution. + +Trades exactness for speed. Over a leap of `tau` the propensities are +held fixed, so the number of firings of reaction `j` is Poisson with +mean `a_j tau` -- correct only while `tau` is short enough that the +propensities really do not change much, which is the whole art of the +method. Too long a leap drives species negative; this implementation +rejects a leap that would and retries it at half the length rather than +clamping, since clamping silently changes the reaction network. + +Errors: +Returns an error for a malformed network, a non-positive end time or +leap. + +Rust: `statistical_mechanics::kinetics::tau_leaping` + """ + ... + +def michaelis_menten(s: float, vmax: float, km: float) -> float: + """ +The Michaelis-Menten rate `v = vmax s / (km + s)`. + +Rust: `statistical_mechanics::kinetics::michaelis_menten` + """ + ... + +def hill_equation(s: float, vmax: float, k: float, n: float) -> float: + """ +The Hill rate `v = vmax s^n / (k^n + s^n)`. + +The exponent is a measure of cooperativity, not a molecularity: a Hill +coefficient of 2.8 for haemoglobin does not mean 2.8 oxygen molecules +bind at once, it means four sites bind with positive cooperativity and +the two-state fit lands there. + +Rust: `statistical_mechanics::kinetics::hill_equation` + """ + ... + +def mm_fit(s: list[float], v: list[float]) -> tuple[float, float]: + """ +Fits `vmax` and `km` to saturation data by least squares on the +*residuals of the rate itself*, by Gauss-Newton. + +Deliberately not the Lineweaver-Burk fit. Inverting the data transforms +the error along with it, so the points at the lowest substrate -- where +the relative error is largest -- become the ones with the largest +leverage, and the fitted `vmax` is biased. The double-reciprocal plot +remains useful for *seeing* the mechanism, which is what +`lineweaver_burk` is for; it is not the way to get the numbers. + +Errors: +Returns an error for fewer than three points, mismatched lengths, +negative concentrations or rates, or a fit that does not converge. + +Rust: `statistical_mechanics::kinetics::mm_fit` + """ + ... + +def lineweaver_burk(s: list[float], v: list[float]) -> tuple[list[tuple[float, float]], float, float]: + """ +The double-reciprocal transform: `(1/s, 1/v)` for each point, plus the +straight line through them as `(slope, intercept)`. + +The line has slope `km / vmax` and intercept `1 / vmax`. Useful for +reading a mechanism off a plot -- competitive, uncompetitive and +non-competitive inhibition give visibly different families of lines -- +and a poor way to extract the constants; see `mm_fit`. + +Errors: +Returns an error for fewer than two points, mismatched lengths, or a +non-positive concentration or rate, which the transform cannot represent. + +Rust: `statistical_mechanics::kinetics::lineweaver_burk` + """ + ... + +def hill_fit(s: list[float], v: list[float]) -> tuple[float, float, float]: + """ +Fits the Hill parameters `(vmax, k, n)` by Gauss-Newton. + +Errors: +Returns an error for fewer than four points, mismatched lengths, or +non-positive data. + +Rust: `statistical_mechanics::kinetics::hill_fit` + """ + ... + +def enzyme_inhibition(s: float, i: float, vmax: float, km: float, ki: float, kind: Inhibition) -> float: + """ +The inhibited Michaelis-Menten rate. + +The three mechanisms are distinguished by *which* constant moves, not by +how much the rate falls -- which is why a single rate measurement can +never identify the mechanism and a substrate series can. + +Errors: +Returns an error for a non-positive `km` or inhibition constant, or a +negative concentration. + +Rust: `statistical_mechanics::kinetics::enzyme_inhibition` + """ + ... + +def steady_state_approx_check(e0: float, s0: float, k1: float, k_minus1: float, k2: float, t_end: float) -> float: + """ +How far a mechanism is from its steady-state approximation, as the +largest relative difference in the intermediate's concentration. + +The approximation holds when the intermediate is consumed as fast as it +is made, which for Michaelis-Menten means the enzyme is scarce beside the +substrate. Returns the discrepancy so the caller can see *whether* it +holds rather than assuming it. + +Errors: +Returns an error for non-positive rate constants or concentrations. + +Rust: `statistical_mechanics::kinetics::steady_state_approx_check` + """ + ... + +def equilibrium_composition(stoich: Matrix | Sequence[Sequence[float]], k_eq: list[float], totals: list[tuple[list[float], float]]) -> list[float]: + """ +The equilibrium composition of a set of reactions with known constants, +found by minimising the total residual of the mass-action and +conservation conditions. + +Each reaction contributes `prod c^nu = k_eq` and each conserved element +contributes a total. Solved by Newton on the logarithms of the +concentrations, which keeps every one positive without a constraint -- +a composition can approach zero but never reach or cross it, which is +what the physical problem requires and what an unconstrained solve on the +concentrations themselves does not respect. + +`totals` is one row per conserved quantity, giving each species' content +and the total amount. + +Errors: +Returns an error for mismatched shapes, a non-positive constant or total, +or a system that does not converge. + +Rust: `statistical_mechanics::kinetics::equilibrium_composition` + """ + ... + +def oscillating_brusselator(a: float, b: float, c0: tuple[float, float], t_end: float) -> list[tuple[float, list[float]]]: + """ +The Brusselator, integrated in time. + +`A -> X`, `2X + Y -> 3X`, `B + X -> Y + D`, `X -> E`, with `A` and `B` +held fixed. The steady state `(a, b/a)` loses stability in a Hopf +bifurcation exactly at `b = 1 + a^2`, and above it the system settles +onto a limit cycle whose amplitude does not depend on where it started. +That sharp threshold is what makes it the standard test of an oscillating +mechanism: the transition is a property of the equations, not of the +integrator. + +Errors: +Returns an error for non-positive parameters or a bad initial state. + +Rust: `statistical_mechanics::kinetics::oscillating_brusselator` + """ + ... + +def brusselator_oscillates(a: float, b: float) -> bool: + """ +Whether the Brusselator oscillates at these parameters: `b > 1 + a^2`. + +Rust: `statistical_mechanics::kinetics::brusselator_oscillates` + """ + ... + +def oregonator(epsilon: float, delta: float, q: float, f: float, c0: tuple[float, float, float], t_end: float) -> list[tuple[float, list[float]]]: + """ +The Oregonator, the Field-Noyes reduction of the Belousov-Zhabotinsky +reaction, in its scaled form. + +Genuinely stiff: `epsilon` and `delta` are of order `10^-2` and `10^-4`, +so the three variables move on timescales four orders of magnitude +apart, and an explicit integrator would be pinned to the fastest one for +the whole run. This is the case the implicit solver in +`rate_equations` exists for. + +Errors: +Returns an error for non-positive parameters or a bad initial state. + +Rust: `statistical_mechanics::kinetics::oregonator` + """ + ... + +def lotka_volterra_chemical(a: float, k1: float, k2: float, k3: float, c0: tuple[float, float], t_end: float) -> tuple[list[tuple[float, list[float]]], list[float]]: + """ +The chemical Lotka-Volterra mechanism: `A + X -> 2X`, `X + Y -> 2Y`, +`Y -> B`, with `A` held fixed. + +Returns the trajectory together with the conserved quantity +`V = k2 x + k2 y - k3 ln x - k1 a ln y`, which is constant along every +orbit. That constant is the reason the orbits are closed curves rather +than a limit cycle: the system is conservative, and unlike the +Brusselator its amplitude *does* depend on where it started. Reporting +it lets a caller see the integrator's drift directly. + +Errors: +Returns an error for non-positive parameters or a non-positive initial +state, for which the conserved quantity is undefined. + +Rust: `statistical_mechanics::kinetics::lotka_volterra_chemical` + """ + ... + +def autocatalysis_ignition(a0: float, b0: float, k: float) -> float: + """ +The ignition time of an autocatalytic reaction `A + B -> 2B`, defined as +the moment the product passes half its final amount. + +The closed form is the logistic inflection: with `a0 + b0` conserved, +`t = ln(a0 / b0) / (k (a0 + b0))`. The induction period is set by how +*little* product there is at the start, which is why an autocatalytic +reaction can sit apparently inert for a long time and then go over in a +moment. + +Errors: +Returns an error for a non-positive rate constant or a non-positive +initial amount of either species. + +Rust: `statistical_mechanics::kinetics::autocatalysis_ignition` + """ + ... + +def chain_reaction_criticality(k_branch: float, k_term: float) -> float: + """ +Whether a branching chain reaction runs away, and by how much: the +branching ratio `k_branch / k_term`. + +Above one the chain carriers multiply and the reaction accelerates +without bound; below one it dies out. The threshold is exactly one and +nothing continuous separates the two behaviours, which is why an +explosion limit is a sharp line in pressure and temperature rather than +a gradual onset. + +Errors: +Returns an error for a non-positive termination rate. + +Rust: `statistical_mechanics::kinetics::chain_reaction_criticality` + """ + ... + +def eyring(delta_h: float, delta_s: float, t: float) -> float: + """ +The Eyring rate `(k_B T / h) exp(dS/R) exp(-dH/RT)`. + +Differs from Arrhenius in what the prefactor means: here it is +`k_B T / h`, a universal frequency of about `6 x 10^12` per second at +room temperature, and all the chemistry sits in the entropy of +activation. The two forms fit the same data equally well and disagree +about why. + +Errors: +Returns an error for a non-positive temperature. + +Rust: `statistical_mechanics::kinetics::eyring` + """ + ... + +def transition_state_theory_rate(delta_g: float, t: float, transmission: float) -> float: + """ +Transition-state theory with a transmission coefficient. + +`k = kappa (k_B T / h) exp(-dG/RT)`. The coefficient is the fraction of +trajectories that cross the barrier and *stay* crossed; transition-state +theory assumes it is one, which makes the theory an upper bound on the +true rate rather than an estimate of it. + +Errors: +Returns an error for a non-positive temperature or a coefficient outside +zero to one. + +Rust: `statistical_mechanics::kinetics::transition_state_theory_rate` + """ + ... + +def kramers_rate_check(gamma: float, barrier_frequency: float) -> float: + """ +The Kramers rate in the moderate-to-high friction regime, relative to the +transition-state result. + +`k / k_TST = sqrt(1 + (gamma / 2 omega_b)^2) - gamma / (2 omega_b)`, +which is at most one and falls toward `omega_b / gamma` as the friction +grows: a solvent that couples strongly to the reaction coordinate makes +recrossing likely, and every recrossing is a barrier passage that did not +produce a product. This is the transmission coefficient that +`transition_state_theory_rate` takes on faith. + +Barrier frequency and friction are in the same units; the ratio is what +matters. + +Errors: +Returns an error for a non-positive barrier frequency or a negative +friction. + +Rust: `statistical_mechanics::kinetics::kramers_rate_check` + """ + ... + +def kinetic_isotope_effect_estimate(nu_light: float, nu_heavy: float, t: float) -> float: + """ +The semiclassical kinetic isotope effect from the change in zero-point +energy alone. + +`k_light / k_heavy = exp(h (nu_light - nu_heavy) / (2 k_B T))`. The +hydrogen-deuterium maximum near seven at room temperature comes out of +this and nothing else; a measured ratio well above it is evidence of +tunnelling, which this estimate deliberately omits so that the excess is +visible rather than absorbed into a fitted parameter. + +Frequencies are in reciprocal centimetres. + +Errors: +Returns an error for a non-positive temperature or frequency. + +Rust: `statistical_mechanics::kinetics::kinetic_isotope_effect_estimate` + """ + ... + +def temperature_jump_relaxation(k_forward: float, k_reverse: float) -> float: + """ +The relaxation time of a reaction perturbed from equilibrium by a +temperature jump. + +For `A <-> B` the relaxation is a single exponential with rate +`k_forward + k_reverse` -- the *sum*, not either one. That is what makes +the technique work: a single measured relaxation gives the sum, the +equilibrium constant gives the ratio, and together they give both rate +constants, which no steady-state measurement can separate. + +Errors: +Returns an error if both rate constants are zero or either is negative. + +Rust: `statistical_mechanics::kinetics::temperature_jump_relaxation` + """ + ... + +def nucleation_rate_cnt(barrier: float, prefactor: float, t: float) -> float: + """ +The classical nucleation rate `J = A exp(-dG* / k_B T)`. + +The exponent is enormous and its argument is a cube over a square, so +the rate spans dozens of orders of magnitude over a small change in +supersaturation. That extreme sensitivity is the physics, not a defect of +the model: it is why nucleation appears to have a threshold. + +Errors: +Returns an error for a non-positive temperature or prefactor. + +Rust: `statistical_mechanics::kinetics::nucleation_rate_cnt` + """ + ... + +def nucleation_barrier(surface_tension: float, number_density: float, driving_force: float) -> float: + """ +The classical nucleation barrier for a spherical nucleus: +`16 pi sigma^3 / (3 (n dmu)^2)`. + +Errors: +Returns an error for a non-positive surface tension, density or driving +force. + +Rust: `statistical_mechanics::kinetics::nucleation_barrier` + """ + ... + +def jmak_avrami(t: float, k: float, n: float) -> float: + """ +The Johnson-Mehl-Avrami-Kolmogorov transformed fraction +`1 - exp(-(k t)^n)`. + +The exponent carries the mechanism: roughly 4 for three-dimensional +growth from a constant nucleation rate, 3 when all sites nucleate at +once, and lower for growth confined to a plane or a line. The point of +fitting it is to read the dimensionality off the kinetics. + +Rust: `statistical_mechanics::kinetics::jmak_avrami` + """ + ... + +def avrami_fit(times: list[float], fraction: list[float]) -> tuple[float, float]: + """ +Fits `(k, n)` to transformed-fraction data. + +The double logarithm `ln(-ln(1 - x)) = n ln t + n ln k` makes the fit +linear and exact, which is the one case where a transform of the data is +the right thing to do: the relation is exactly linear in the transformed +variables, so no error is being reshaped, only re-expressed. + +Errors: +Returns an error for fewer than two usable points -- a fraction of zero +or one carries no information, since the transform sends it to infinity. + +Rust: `statistical_mechanics::kinetics::avrami_fit` + """ + ... + +def photochemistry_quantum_yield(molecules: float, photons_absorbed: float) -> float: + """ +The quantum yield: molecules transformed per photon absorbed. + +A yield above one is not an error -- a chain reaction initiated by one +photon can transform thousands of molecules -- so no upper bound is +imposed. + +Errors: +Returns an error for a non-positive photon count or a negative product +count. + +Rust: `statistical_mechanics::kinetics::photochemistry_quantum_yield` + """ + ... + +def ph_from_equilibria(acids: list[tuple[float, float]], base_conc: float) -> float: + """ +The pH of a solution of one or more acids, by solving the full charge +balance rather than any approximation. + +Each acid is `(pKa, total concentration)`; `base_conc` is added strong +base. The equation solved is +`[H+] + [base] = K_w/[H+] + sum_a C_a K_a / (K_a + [H+])`, +which includes the water autoprotolysis and the depletion of the acid as +it dissociates. Neither can be dropped in general: the usual +`sqrt(K_a C)` shortcut assumes both, and it fails for a dilute acid +(where water dominates) and for a strong one (where the acid is nearly +all dissociated and the depletion is the whole story). Solved by +bisection on `pH`, which cannot diverge because the balance is monotone +in `[H+]`. + +Errors: +Returns an error for a negative concentration or an empty system with no +base. + +Rust: `statistical_mechanics::kinetics::ph_from_equilibria` + """ + ... + +def titration_curve(acid_pka: float, acid_conc: float, acid_volume: float, base_conc: float, volume_max: float, points: int) -> list[tuple[float, float]]: + """ +A titration curve: pH against the volume of strong base added. + +Returns `(volume added, pH)` at each of `points` steps up to +`volume_max`. Dilution is accounted for -- both the acid and the base +are diluted by the growing total volume -- which is what puts the +equivalence point of a weak acid above pH 7 rather than at it. + +Errors: +Returns an error for a non-positive volume, concentration or point +count. + +Rust: `statistical_mechanics::kinetics::titration_curve` + """ + ... + +def buffer_henderson_hasselbalch(pka: float, ratio: float) -> float: + """ +Henderson-Hasselbalch: `pH = pKa + log10(base / acid)`. + +An approximation, and one whose failure is predictable: it assumes the +dissociation does not appreciably change either concentration, so it is +accurate within about a unit of the pKa and wrong outside that. Compare +against `ph_from_equilibria`, which makes no such assumption. + +Errors: +Returns an error for a non-positive ratio. + +Rust: `statistical_mechanics::kinetics::buffer_henderson_hasselbalch` + """ + ... + +def debye_huckel_activity(z: float, ionic_strength: float) -> float: + """ +The Debye-Huckel activity coefficient of an ion. + +The extended law `log10 gamma = -A z^2 sqrt(I) / (1 + sqrt I)`, with +`A = 0.509` for water at 25 degrees. The limiting law without the +denominator is only good below about `I = 0.01`; the extended form holds +to roughly `I = 0.1`, and above that no simple expression does. + +Errors: +Returns an error for a negative ionic strength. + +Rust: `statistical_mechanics::kinetics::debye_huckel_activity` + """ + ... + +def nernst(e0: float, z: float, ratio: float, t: float) -> float: + """ +The Nernst potential from a concentration ratio. + +A thin wrapper on `chemistry::nernst_potential` in the form the +kinetics literature uses. At 25 degrees and one electron the slope is +59.16 mV per decade, which is the number every ion-selective electrode +is calibrated against. + +Errors: +Returns an error for a non-positive temperature, electron count or +ratio. + +Rust: `statistical_mechanics::kinetics::nernst` + """ + ... + +def butler_volmer(i0: float, alpha: float, eta: float, z: float, t: float) -> float: + """ +The Butler-Volmer current density +`i0 (exp(alpha z F eta / RT) - exp(-(1 - alpha) z F eta / RT))`. + +At small overpotential the two exponentials cancel to leading order and +the current is *linear* in `eta` with a slope `i0 z F / RT` -- the +charge-transfer resistance. At large overpotential one term dominates +and the relation becomes the logarithmic Tafel law. Both limits come out +of the same expression, which is why fitting a Tafel slope to +near-equilibrium data gives a meaningless exchange current. + +Errors: +Returns an error for a non-positive temperature or exchange current, an +asymmetry outside zero to one, or a non-positive electron count. + +Rust: `statistical_mechanics::kinetics::butler_volmer` + """ + ... + +def cottrell_current(z: float, area: float, concentration: float, diffusivity: float, t: float) -> float: + """ +The Cottrell current `z F A c sqrt(D / (pi t))` for a diffusion-limited +electrode. + +Falls as the inverse square root of time, not exponentially: the +depletion layer grows as `sqrt(D t)`, so the gradient that drives the +current thins in proportion. The same square root governs every +semi-infinite diffusion problem. + +Errors: +Returns an error for a non-positive time, area, diffusion coefficient, +concentration or electron count. + +Rust: `statistical_mechanics::kinetics::cottrell_current` + """ + ... diff --git a/bindings/python/python/numeria/statistical_mechanics/lattice_models.pyi b/bindings/python/python/numeria/statistical_mechanics/lattice_models.pyi new file mode 100644 index 0000000..ac572d5 --- /dev/null +++ b/bindings/python/python/numeria/statistical_mechanics/lattice_models.pyi @@ -0,0 +1,288 @@ +""" +Lattice models: percolation, walks, growth, and avalanches. These are the systems where critical behaviour appears without any Hamiltonian or temperature at all. Percolation has a sharp threshold and a divergent cluster size, self-avoiding walks have a non-trivial exponent that mean-field theory gets wrong, and a growing interface roughens with exponents shared by systems that have nothing physically in common. That last fact -- universality -- is what makes the subject more than a collection of models: the exponents depend on dimension and symmetry, and on essentially nothing else. Everything here is on a square lattice unless said otherwise, and the random routines take the crate's deterministic generator so a run can be repeated exactly. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.monte_carlo import Rng + +def percolation_site(n: int, p: float, rng: Rng) -> tuple[list[bool], bool]: + """ +Site percolation on a square lattice: occupy each site with probability +`p` and report whether an occupied cluster spans top to bottom. + +Returns the grid and whether it spans. The transition is sharp only in the +infinite lattice; on a finite one the spanning probability rises smoothly +through the threshold over a width that shrinks as the lattice grows, +which is finite-size scaling in its simplest visible form. + +Errors: +Returns an error for a bad lattice size or a probability outside `[0, 1]`. + +Rust: `statistical_mechanics::lattice_models::percolation_site` + """ + ... + +def percolation_bond(n: int, p: float, rng: Rng) -> bool: + """ +Bond percolation on a square lattice: open each bond with probability `p` +and report whether the lattice spans. + +The bond threshold in two dimensions is exactly one half, by a duality +argument -- the dual of an open bond is a closed one, so the model is +self-dual at `p = 1/2` and the transition can be nowhere else. The site +threshold has no such argument and is only known numerically. + +Errors: +Returns an error for a bad lattice size or probability. + +Rust: `statistical_mechanics::lattice_models::percolation_bond` + """ + ... + +def percolation_threshold_binary_search(n: int, trials: int, rng: Rng) -> float: + """ +Estimates the site percolation threshold by bisection on the spanning +probability. + +The true two-dimensional value is about 0.592746, and unlike the bond +threshold it has no closed form. A finite lattice puts the half-spanning +point slightly off it, and the offset shrinks as the lattice grows. + +Errors: +Returns an error for a bad lattice size or trial count. + +Rust: `statistical_mechanics::lattice_models::percolation_threshold_binary_search` + """ + ... + +def cluster_size_distribution(grid: list[bool], n: int) -> list[int]: + """ +The sizes of every occupied cluster, descending. + +Errors: +Returns an error if the grid is not square. + +Rust: `statistical_mechanics::lattice_models::cluster_size_distribution` + """ + ... + +def self_avoiding_walk_count(n: int) -> int: + """ +The number of self-avoiding walks of `n` steps from the origin on the +square lattice. + +Counted by exhaustive backtracking, so it is exact and exponential -- +which is the state of the art: no formula is known, and the published +counts come from much cleverer enumerations of the same kind. + +Errors: +Returns an error above eighteen steps, where the count exceeds what this +enumeration will finish. + +Rust: `statistical_mechanics::lattice_models::self_avoiding_walk_count` + """ + ... + +def saw_sample_rosenbluth(n: int, rng: Rng) -> tuple[list[tuple[int, int]], float]: + """ +One self-avoiding walk sampled by the Rosenbluth method, with its weight. + +Growing a walk step by step and refusing to revisit gives a *biased* +sample: walks that had few choices are over-represented. The Rosenbluth +weight -- the product of the available choices at each step -- corrects +exactly for that, so weighted averages are unbiased. The method's known +weakness is that the weights become very unequal for long walks, so the +effective sample size collapses even though the estimator stays unbiased. + +Returns the path and its weight; a walk that traps itself returns a weight +of zero. + +Errors: +Returns an error for an excessive step count. + +Rust: `statistical_mechanics::lattice_models::saw_sample_rosenbluth` + """ + ... + +def connective_constant_estimate(counts: list[int]) -> float: + """ +An estimate of the connective constant from exact walk counts. + +`mu = lim c_n^(1/n)`, about 2.63816 on the square lattice. + +Two corrections have to be removed and they are removed differently. The +counts alternate with parity -- `c_n / c_(n-1)` oscillates between about +2.694 and 2.702 at these lengths -- so the ratio is taken two steps at a +time, `sqrt(c_n / c_(n-2))`, which averages the parity out rather than +amplifying it. What remains behaves as `mu (1 + (gamma - 1) / n)` because +`c_n ~ A mu^n n^(gamma - 1)`, and one Richardson step on `1 / n` cancels +it whatever the unknown coefficient. Applying Richardson to the raw +consecutive ratios instead makes matters *worse*, since it differences two +numbers of opposite parity and doubles the oscillation. + +Errors: +Returns an error for fewer than five counts, or a zero count. + +Rust: `statistical_mechanics::lattice_models::connective_constant_estimate` + """ + ... + +def random_walk_lattice(steps: int, dimensions: int, rng: Rng) -> list[list[int]]: + """ +A simple random walk on the `d`-dimensional cubic lattice. + +Errors: +Returns an error for zero dimensions or an excessive step count. + +Rust: `statistical_mechanics::lattice_models::random_walk_lattice` + """ + ... + +def return_probability(dimensions: int) -> float: + """ +Polya's return probability for a simple random walk in `d` dimensions. + +One in one and two dimensions and less than one from three up. The +dimension at which a walk stops returning is not a matter of degree: in +two dimensions the walker returns with certainty and in three it escapes +with probability about 0.66, and nothing continuous separates them. + +Errors: +Returns an error for zero dimensions or above eight. + +Rust: `statistical_mechanics::lattice_models::return_probability` + """ + ... + +def polymer_end_to_end(samples: list[tuple[list[tuple[int, int]], float]]) -> float: + """ +The mean squared end-to-end distance of a set of weighted walks. + +Errors: +Returns an error for an empty sample or zero total weight. + +Rust: `statistical_mechanics::lattice_models::polymer_end_to_end` + """ + ... + +def flory_exponent_estimate(lengths: list[int], squared: list[float]) -> float: + """ +The Flory exponent fitted from end-to-end distances at several lengths. + +` ~ n^(2 nu)` with `nu = 3/4` exactly in two dimensions -- a result +of Nienhuis, and one that Flory's own mean-field argument happens to get +right in this dimension and wrong in three. + +Errors: +Returns an error for fewer than two lengths or a non-positive distance. + +Rust: `statistical_mechanics::lattice_models::flory_exponent_estimate` + """ + ... + +def dimer_count_kasteleyn(m: int, n: int) -> float: + """ +The number of perfect matchings of an `m` by `n` grid, by Kasteleyn's +formula. + +`prod_{j,k} (4 cos^2(pi j / (m+1)) + 4 cos^2(pi k / (n+1)))^(1/4)`. The +remarkable part is that a counting problem which is `#P`-complete on a +general graph is *polynomial* on a planar one, because the count becomes a +Pfaffian once the edges are oriented correctly. + +Returned as a float, since the count outgrows a `u64` by about the twelve +by twelve grid; it is exact to rounding and the caller can round it. + +Errors: +Returns an error for a zero dimension or an odd number of cells, which +admits no perfect matching at all. + +Rust: `statistical_mechanics::lattice_models::dimer_count_kasteleyn` + """ + ... + +def kpz_growth_ballistic(width: int, depositions: int, rng: Rng) -> list[float]: + """ +Ballistic deposition on a line, returning the final interface heights. + +A particle falls on a random column and sticks at the first point where it +touches the deposit, which may be the side of a neighbouring column rather +than the top of its own. That sideways sticking is the whole model: without +it the interface stays flat, and with it the interface roughens with the +Kardar-Parisi-Zhang exponents. + +Errors: +Returns an error for a bad width or an excessive time. + +Rust: `statistical_mechanics::lattice_models::kpz_growth_ballistic` + """ + ... + +def interface_width(heights: list[float]) -> float: + """ +The width of an interface: the standard deviation of its heights. + +Errors: +Returns an error for an empty interface. + +Rust: `statistical_mechanics::lattice_models::interface_width` + """ + ... + +def growth_exponent_estimate(times: list[float], widths: list[float]) -> float: + """ +The growth exponent `beta`, fitted from the width against time. + +`W ~ t^beta` before the width saturates, with `beta = 1/3` in the +one-dimensional KPZ class. The fit must stay inside the growth regime: +once the correlation length reaches the system size the width stops +growing altogether, and including saturated points drags the exponent +toward zero. + +Errors: +Returns an error for fewer than three points or a non-positive width. + +Rust: `statistical_mechanics::lattice_models::growth_exponent_estimate` + """ + ... + +def sandpile_avalanche_distribution(n: int, drops: int, rng: Rng) -> list[int]: + """ +The Abelian sandpile: drop grains at random sites and record the size of +each avalanche. + +The pile organises itself to the critical state without any parameter +being tuned, which is what "self-organised criticality" means: the +avalanche sizes come out power-law distributed whatever the initial +condition, with no temperature or field set by hand. + +Errors: +Returns an error for a bad lattice size or drop count. + +Rust: `statistical_mechanics::lattice_models::sandpile_avalanche_distribution` + """ + ... + +def power_law_fit_clauset(data: list[float], x_min: float) -> tuple[float, float]: + """ +Clauset's maximum-likelihood power-law fit above a cutoff, with the +Kolmogorov-Smirnov distance to the fitted law. + +Fitting a straight line to a log-log histogram is the traditional method +and it is badly biased: the bins in the tail hold few points, and least +squares weights them as heavily as the bins that hold thousands. The +maximum-likelihood estimator has a closed form for a continuous power law +and no such problem. + +Returns `(alpha, ks_distance)`. + +Errors: +Returns an error for a non-positive cutoff or too few points above it. + +Rust: `statistical_mechanics::lattice_models::power_law_fit_clauset` + """ + ... diff --git a/bindings/python/python/numeria/statistical_mechanics/md.pyi b/bindings/python/python/numeria/statistical_mechanics/md.pyi new file mode 100644 index 0000000..044fd70 --- /dev/null +++ b/bindings/python/python/numeria/statistical_mechanics/md.pyi @@ -0,0 +1,328 @@ +""" +Molecular dynamics: pair potentials, a cell-list force evaluation, a symplectic integrator, thermostats and barostats, and the structural and transport measurements taken from a trajectory. # Units Everything here is in *reduced* Lennard-Jones units: `sigma`, `eps`, the particle mass and Boltzmann's constant are all one unless the caller says otherwise, so a temperature is an energy and a pressure is an energy per volume. This is not a convenience -- mixing SI constants into a molecular dynamics run is how the field's worst bugs happen, because the equations of motion are dimensionally consistent under any consistent choice and silently wrong under an inconsistent one. See `lj_reduced_units_note`. The roadmap gives `MdSystem` a `SpatialHash` field. The general-purpose hash in `spatial::kdtree` owns a copy of every position and knows nothing about periodic images, so this module carries its own cell list instead: it is rebuilt each step from the live positions and wraps at the box boundary, which is what the minimum-image convention needs. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.monte_carlo import Rng +from numeria.statistics.inference import TestResult +from numeria.stochastic.timeseries import Var +from numeria.math import Vec3 + +class MdSample: + """ +One record from a trajectory. + +Rust: `statistical_mechanics::md::MdSample` + """ + def __init__(self, time: float, kinetic: float, potential: float, total: float, temperature: float, pressure: float) -> None: ... + @property + def time(self) -> float: ... + @property + def kinetic(self) -> float: ... + @property + def potential(self) -> float: ... + @property + def total(self) -> float: ... + @property + def temperature(self) -> float: ... + @property + def pressure(self) -> float: ... + +class MdSystem: + """ +A box of particles interacting through one pair potential. + +Rust: `statistical_mechanics::md::MdSystem` + """ + def __init__(self, pos: list[Vec3 | Sequence[float]], vel: list[Vec3 | Sequence[float]], mass: list[float], box_size: Vec3 | Sequence[float], periodic: bool, potential: Potential, cutoff: float) -> None: ... + @staticmethod + def lattice_fcc(cells: int, density: float, temperature: float, eps: float, sigma: float, rng: Rng) -> MdSystem: ... + def len(self) -> int: ... + def is_empty(self) -> bool: ... + def volume(self) -> float: ... + def wrap(self, p: Vec3 | Sequence[float]) -> Vec3: ... + def minimum_image(self, d: Vec3 | Sequence[float]) -> Vec3: ... + def remove_drift(self) -> None: ... + def rescale_to_temperature(self, target: float) -> None: ... + def forces(self) -> list[Vec3]: ... + def refresh_forces(self) -> None: ... + def potential_energy(self) -> float: ... + def kinetic_energy(self) -> float: ... + def total_momentum(self) -> Vec3: ... + def degrees_of_freedom(self) -> float: ... + def temperature(self) -> float: ... + def pressure_virial(self) -> float: ... + def sample(self) -> MdSample: ... + def step_velocity_verlet(self, dt: float) -> None: ... + def thermostat_berendsen(self, t_target: float, tau: float, dt: float) -> None: ... + def thermostat_nose_hoover(self, t_target: float, q: float, dt: float) -> None: ... + def thermostat_langevin(self, t_target: float, gamma: float, dt: float, rng: Rng) -> None: ... + def barostat_berendsen(self, p_target: float, compressibility: float, tau: float, dt: float) -> None: ... + def equilibrate(self, steps: int, dt: float, t_target: float, rng: Rng) -> None: ... + def run_nve(self, steps: int, dt: float) -> list[MdSample]: ... + def run_trajectory(self, steps: int, dt: float, stride: int) -> tuple[list[list[Vec3]], list[list[Vec3]]]: ... + def rdf(self, bins: int, r_max: float) -> list[float]: ... + def structure_factor(self, k_values: list[float]) -> list[float]: ... + def maxwell_boltzmann_check(self) -> TestResult: ... + def melting_indicator_lindemann(self, traj: list[list[Vec3 | Sequence[float]]]) -> float: ... + @staticmethod + def msd(traj: list[list[Vec3 | Sequence[float]]]) -> list[float]: ... + @staticmethod + def diffusion_coefficient(msd: list[float], dt: float) -> float: ... + @staticmethod + def vacf(traj_vel: list[list[Vec3 | Sequence[float]]]) -> list[float]: ... + @staticmethod + def vdos_from_vacf(vacf: list[float], dt: float) -> list[float]: ... + @property + def pos(self) -> list[Vec3]: ... + @property + def unwrapped(self) -> list[Vec3]: ... + @property + def vel(self) -> list[Vec3]: ... + @property + def mass(self) -> list[float]: ... + @property + def charge(self) -> list[float]: ... + @property + def box_size(self) -> Vec3: ... + @property + def periodic(self) -> bool: ... + @property + def potential(self) -> Potential: ... + @property + def cutoff(self) -> float: ... + @property + def time(self) -> float: ... + @property + def nose_hoover_zeta(self) -> float: ... + +class Potential: + """ +A pair potential, as a function of the separation alone. + +Each variant supplies both the energy and the force so that they cannot +drift apart: a force that is not the negative gradient of the energy in +use will conserve nothing, and the failure looks exactly like an +integrator bug. + +Rust: `statistical_mechanics::md::Potential` + """ + def evaluate(self, r: float, qi: float, qj: float) -> tuple[float, float]: ... + def is_charged(self) -> bool: ... + +def energy_drift(samples: list[MdSample | Sequence[float]]) -> float: + """ +The secular drift of the total energy over a record, relative to its mean. + +This is the slope of a least-squares line through the total energy, +multiplied by the elapsed time -- not the spread. A symplectic +integrator's energy *oscillates* with an amplitude set by the step size +and does not go anywhere; reporting that oscillation as drift would +condemn a correct integrator, and reporting the maximum deviation would +do the same. What distinguishes a good integrator from a bad one is +whether the oscillation has a trend under it. + +Errors: +Returns an error for fewer than three samples or a zero time span. + +Rust: `statistical_mechanics::md::energy_drift` + """ + ... + +def lj_reduced_units_note() -> str: + """ +What the reduced units in this module mean. + +Rust: `statistical_mechanics::md::lj_reduced_units_note` + """ + ... + +def lj_phase_point(t_star: float, rho_star: float) -> str: + """ +A rough phase from the Lennard-Jones phase diagram. + +Boundaries taken from the accepted triple point near `(T* = 0.69, +rho* = 0.84)` and critical point near `(T* = 1.32, rho* = 0.31)`. It is a +classification, not an equation of state, and near a boundary it should +not be trusted over an actual measurement. + +Rust: `statistical_mechanics::md::lj_phase_point` + """ + ... + +def ewald_sum_energy_lite(charges: list[float], pos: list[Vec3 | Sequence[float]], box_l: float, alpha: float, k_max: int) -> float: + """ +The Ewald energy of a set of point charges in a periodic box. + +A charged system cannot be truncated: the Coulomb sum is only +conditionally convergent, so its value depends on the order of +summation and a spherical cutoff gives a different -- wrong -- answer. +Ewald splits the sum with a Gaussian screen into a real-space part that +converges quickly and a reciprocal-space part that does the same, plus +the self-energy of the screens. + +Errors: +Returns an error for mismatched lengths, a non-positive box or splitting +parameter, an empty system, or a net charge, for which the sum is not +defined without a neutralising background. + +Rust: `statistical_mechanics::md::ewald_sum_energy_lite` + """ + ... + +def harmonic_crystal_heat_capacity_check(energies: list[float], temperature: float, particles: int) -> float: + """ +The heat capacity per particle of a system from its energy fluctuations, +in units of Boltzmann's constant. + +`C_v = Var(E) / (k T^2)`. A classical harmonic crystal must return three: +each particle has three quadratic kinetic and three quadratic potential +degrees of freedom, and equipartition gives `k/2` to each. That is the +Dulong-Petit law, and it is the check this function exists for -- a +simulation that reports anything else at a temperature well above the +Debye temperature has a bug, not a discovery. + +Errors: +Returns an error for fewer than two energies, no particles, or a +non-positive temperature. + +Rust: `statistical_mechanics::md::harmonic_crystal_heat_capacity_check` + """ + ... + +def virial_coefficient_b2(potential: Potential, t: float, r_max: float, n: int) -> float: + """ +The second virial coefficient by numerical integration of the Mayer +function. + +`B2(T) = -2 pi int_0^rmax (exp(-u(r)/T) - 1) r^2 dr`. It changes sign at +the Boyle temperature, where attraction and repulsion cancel and the gas +is ideal to first order in the density -- about `T* = 3.418` for +Lennard-Jones. + +Errors: +Returns an error for a non-positive temperature or range, or an odd or +too-small interval count. + +Rust: `statistical_mechanics::md::virial_coefficient_b2` + """ + ... + +def mean_free_path(density: float, sigma: float) -> float: + """ +The mean free path `1 / (sqrt 2 n sigma)`, with `sigma` the collision +cross-section. + +The `sqrt 2` is not decoration: it accounts for the *relative* motion of +the two colliding particles, and dropping it overestimates the path by +forty per cent. + +Errors: +Returns an error for a non-positive density or cross-section. + +Rust: `statistical_mechanics::md::mean_free_path` + """ + ... + +def collision_rate(density: float, sigma: float, mean_speed: float) -> float: + """ +The collision rate per particle, `sqrt 2 n sigma v_mean`. + +Errors: +Returns an error for a non-positive density, cross-section or speed. + +Rust: `statistical_mechanics::md::collision_rate` + """ + ... + +def green_kubo_viscosity_lite(stress_xy: list[float], dt: float, volume: float, temperature: float) -> float: + """ +The shear viscosity from a Green-Kubo integral of the off-diagonal +stress autocorrelation. + +`eta = V / (k T) int_0^inf dt`, integrated up to the +first lag at which the estimated correlation stops being positive. + +That truncation is not an optimisation. Past a few correlation times the +estimate of `` is noise of a size set by the sample count, and +integrating thousands of such lags accumulates a random walk whose spread +is comparable to the whole integral -- for an exponential correlation +with a thirty-sample time and thirty thousand samples, the tail +contributes as much scatter as the signal contains. Integrating to the +end of the record therefore returns a number that is mostly noise, which +looks like a plausible viscosity and is not one. + +The cost of truncating is a known one: stopping at the first zero +crossing loses the part of the tail already below the noise floor, so the +result is a few per cent low. That is the accepted trade, and it is the +direction of the remaining error -- a short record still *underestimates* +rather than scattering, because the tail it cannot see carries real +weight. + +Errors: +Returns an error for fewer than two samples or a non-positive step, +volume or temperature. + +Rust: `statistical_mechanics::md::green_kubo_viscosity_lite` + """ + ... + +def umbrella_sampling_pmf(histograms: list[list[float]], centers: list[float], k: float, bin_lo: float, bin_width: float, temperature: float) -> list[float]: + """ +A potential of mean force from umbrella-sampling histograms, by +self-consistent WHAM. + +Each window is biased by `k (x - centre)^2 / 2`, and the windows have to +be combined by solving for one free-energy offset per window: simply +unbiasing each histogram and averaging leaves the offsets arbitrary, and +the resulting curve has a step at every window boundary. + +`histograms[w][b]` is the count in bin `b` of window `w`; bin `b` is +centred at `bin_lo + (b + 0.5) * bin_width`. + +Errors: +Returns an error for no windows, mismatched lengths, a non-positive bin +width, force constant or temperature, or if the iteration does not +converge. + +Rust: `statistical_mechanics::md::umbrella_sampling_pmf` + """ + ... + +def steered_pull(force_along: Callable[[float], float], start: float, speed: float, k: float, dt: float, steps: int) -> list[float]: + """ +A steered-molecular-dynamics pull: a harmonic restraint whose centre +moves at constant speed, returning the accumulated work at each step. + +The work is *not* the free-energy difference. It exceeds it by the +dissipation, and only in the reversible limit do the two coincide -- +which is what Jarzynski's equality repairs, by averaging `exp(-W/kT)` +over repeated pulls rather than averaging the work itself. + +Errors: +Returns an error for a non-positive step, force constant or step count. + +Rust: `statistical_mechanics::md::steered_pull` + """ + ... + +def jarzynski_free_energy(work: list[float], temperature: float) -> float: + """ +Jarzynski's estimate of the free-energy difference from a set of +non-equilibrium work values. + +`exp(-dF/kT) = `. The average is dominated by the rare +trajectories with the *smallest* work, which is why the estimator is +notoriously hard to converge: the trajectories that matter most are the +ones sampled least. + +Errors: +Returns an error for no work values or a non-positive temperature. + +Rust: `statistical_mechanics::md::jarzynski_free_energy` + """ + ... diff --git a/bindings/python/python/numeria/statistics/__init__.pyi b/bindings/python/python/numeria/statistics/__init__.pyi new file mode 100644 index 0000000..a254e8b --- /dev/null +++ b/bindings/python/python/numeria/statistics/__init__.pyi @@ -0,0 +1,78 @@ +""" +Statistics: descriptive measures, probability distributions, and Fourier utilities. Submodules are re-exported so historical paths such as `statistics::mean` keep working. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from . import descriptive, distributions, fourier, inference, resampling +from numeria.statistics.distributions import Beta as Beta +from numeria.statistics.distributions import Binomial as Binomial +from numeria.statistics.resampling import BootstrapResult as BootstrapResult +from numeria.statistics.distributions import ChiSquared as ChiSquared +from numeria.statistics.distributions import Exponential as Exponential +from numeria.statistics.distributions import FDist as FDist +from numeria.statistics.distributions import Gamma as Gamma +from numeria.statistics.distributions import LogNormal as LogNormal +from numeria.statistics.distributions import Normal as Normal +from numeria.statistics.distributions import Poisson as Poisson +from numeria.statistics.distributions import StudentT as StudentT +from numeria.statistics.inference import TestResult as TestResult +from numeria.statistics.distributions import Weibull as Weibull +from numeria.statistics.inference import anova_one_way as anova_one_way +from numeria.statistics.resampling import bootstrap as bootstrap +from numeria.statistics.resampling import bootstrap_bca as bootstrap_bca +from numeria.statistics.inference import chi_squared_gof as chi_squared_gof +from numeria.statistics.inference import chi_squared_independence as chi_squared_independence +from numeria.statistics.distributions import chi_squared_pdf as chi_squared_pdf +from numeria.statistics.inference import confidence_interval_mean as confidence_interval_mean +from numeria.statistics.descriptive import correlation as correlation +from numeria.statistics.descriptive import covariance as covariance +from numeria.statistics.fourier import dft as dft +from numeria.statistics.fourier import dominant_frequency as dominant_frequency +from numeria.statistics.descriptive import error_propagation_product as error_propagation_product +from numeria.statistics.descriptive import error_propagation_sum as error_propagation_sum +from numeria.statistics.distributions import exponential_cdf as exponential_cdf +from numeria.statistics.distributions import exponential_pdf as exponential_pdf +from numeria.statistics.distributions import gaussian as gaussian +from numeria.statistics.distributions import gaussian_cdf as gaussian_cdf +from numeria.statistics.distributions import gaussian_cdf_approx as gaussian_cdf_approx +from numeria.statistics.fourier import inverse_dft as inverse_dft +from numeria.statistics.resampling import jackknife as jackknife +from numeria.statistics.inference import ks_test_one_sample as ks_test_one_sample +from numeria.statistics.inference import ks_test_two_sample as ks_test_two_sample +from numeria.statistics.descriptive import mean as mean +from numeria.statistics.descriptive import median as median +from numeria.statistics.inference import pearson_test as pearson_test +from numeria.statistics.resampling import permutation_test as permutation_test +from numeria.statistics.distributions import poisson_pmf as poisson_pmf +from numeria.statistics.fourier import power_spectrum as power_spectrum +from numeria.statistics.descriptive import sample_std_deviation as sample_std_deviation +from numeria.statistics.descriptive import sample_variance as sample_variance +from numeria.statistics.descriptive import std_deviation as std_deviation +from numeria.statistics.inference import t_test_one_sample as t_test_one_sample +from numeria.statistics.inference import t_test_paired as t_test_paired +from numeria.statistics.inference import t_test_two_sample as t_test_two_sample +from numeria.statistics.descriptive import variance as variance +from numeria.statistics.descriptive import weighted_mean as weighted_mean +from numeria.statistics.descriptive import weighted_mean_error as weighted_mean_error + +def factorial(n: int) -> float: + """ +Compute factorial of n: n! = 1 × 2 × ... × n + +Rust: `statistics::factorial` + """ + ... + +def gamma_lanczos(z: float) -> float: + """ +Compute the gamma function via Lanczos approximation: Γ(z). +Thin wrapper over `special::gamma::gamma`, kept for +backwards compatibility. + +Rust: `statistics::gamma_lanczos` + """ + ... diff --git a/bindings/python/python/numeria/statistics/descriptive.pyi b/bindings/python/python/numeria/statistics/descriptive.pyi new file mode 100644 index 0000000..f052577 --- /dev/null +++ b/bindings/python/python/numeria/statistics/descriptive.pyi @@ -0,0 +1,107 @@ +""" +Descriptive statistics, error propagation, and weighted means. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def mean(data: list[float]) -> float: + """ +Arithmetic mean of a data set: μ = (Σxᵢ) / n +The sum is computed with Neumaier compensated summation. + +Rust: `statistics::descriptive::mean` + """ + ... + +def variance(data: list[float]) -> float: + """ +Population variance: σ² = Σ(xᵢ - μ)² / n +The sum of squared deviations is computed with Neumaier compensated summation. + +Rust: `statistics::descriptive::variance` + """ + ... + +def std_deviation(data: list[float]) -> float: + """ +Population standard deviation: σ = sqrt(σ²) + +Rust: `statistics::descriptive::std_deviation` + """ + ... + +def sample_variance(data: list[float]) -> float: + """ +Sample variance with Bessel's correction: s² = Σ(xᵢ - x̄)² / (n - 1) + +Rust: `statistics::descriptive::sample_variance` + """ + ... + +def sample_std_deviation(data: list[float]) -> float: + """ +Sample standard deviation: s = sqrt(s²) + +Rust: `statistics::descriptive::sample_std_deviation` + """ + ... + +def median(data: MutableSequence[float]) -> float: + """ +Median of a data set (sorts the slice in place) + +Rust: `statistics::descriptive::median` + """ + ... + +def covariance(x: list[float], y: list[float]) -> float: + """ +Population covariance of two data sets: cov(X,Y) = Σ(xᵢ - μₓ)(yᵢ - μᵧ) / n + +Rust: `statistics::descriptive::covariance` + """ + ... + +def correlation(x: list[float], y: list[float]) -> float: + """ +Pearson correlation coefficient: r = cov(X,Y) / (σₓ · σᵧ) + +Rust: `statistics::descriptive::correlation` + """ + ... + +def error_propagation_sum(errors: list[float]) -> float: + """ +Error propagation for sums: δ_total = sqrt(Σδᵢ²) + +Rust: `statistics::descriptive::error_propagation_sum` + """ + ... + +def error_propagation_product(values: list[float], relative_errors: list[float]) -> float: + """ +Error propagation for products using relative errors: δ_rel = sqrt(Σ(δᵢ/vᵢ)²) + +Rust: `statistics::descriptive::error_propagation_product` + """ + ... + +def weighted_mean(values: list[float], weights: list[float]) -> float: + """ +Weighted mean: x̄_w = Σ(wᵢ·xᵢ) / Σwᵢ + +Rust: `statistics::descriptive::weighted_mean` + """ + ... + +def weighted_mean_error(weights: list[float]) -> float: + """ +Weighted mean uncertainty: δ = 1 / sqrt(Σwᵢ) + +Rust: `statistics::descriptive::weighted_mean_error` + """ + ... diff --git a/bindings/python/python/numeria/statistics/distributions.pyi b/bindings/python/python/numeria/statistics/distributions.pyi new file mode 100644 index 0000000..07b047c --- /dev/null +++ b/bindings/python/python/numeria/statistics/distributions.pyi @@ -0,0 +1,196 @@ +""" +Probability distributions: densities, mass functions, and CDFs. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +class Beta: + """ +Beta distribution on [0, 1] with shape parameters (a, b). + +Rust: `statistics::distributions::Beta` + """ + def __init__(self, a: float, b: float) -> None: ... + @property + def a(self) -> float: ... + @property + def b(self) -> float: ... + +class Binomial: + """ +Binomial distribution (discrete) with n trials and success +probability p; CDF via the regularized incomplete beta. + +Rust: `statistics::distributions::Binomial` + """ + def __init__(self, n: int, p: float) -> None: ... + def pmf(self, k: int) -> float: ... + @property + def n(self) -> int: ... + @property + def p(self) -> float: ... + +class ChiSquared: + """ +Chi-squared distribution with k degrees of freedom; CDF via P(k/2, x/2). + +Rust: `statistics::distributions::ChiSquared` + """ + def __init__(self, k: float) -> None: ... + @property + def k(self) -> float: ... + +class Exponential: + """ +Exponential distribution with the given rate λ (mean 1/λ), wrapping +the module's free `exponential_pdf`/`exponential_cdf`. + +Rust: `statistics::distributions::Exponential` + """ + def __init__(self, rate: float) -> None: ... + @property + def rate(self) -> float: ... + +class FDist: + """ +Fisher-Snedecor F distribution with (d1, d2) degrees of freedom. + +Rust: `statistics::distributions::FDist` + """ + def __init__(self, d1: float, d2: float) -> None: ... + @property + def d1(self) -> float: ... + @property + def d2(self) -> float: ... + +class Gamma: + """ +Gamma distribution with shape α and rate β (mean α/β). + +Rust: `statistics::distributions::Gamma` + """ + def __init__(self, shape: float, rate: float) -> None: ... + @property + def shape(self) -> float: ... + @property + def rate(self) -> float: ... + +class LogNormal: + """ +Log-normal distribution: ln X ~ N(μ, σ²). + +Rust: `statistics::distributions::LogNormal` + """ + def __init__(self, mu: float, sigma: float) -> None: ... + @property + def mu(self) -> float: ... + @property + def sigma(self) -> float: ... + +class Normal: + """ +Normal distribution N(μ, σ²); quantile via `erfinv`. + +Rust: `statistics::distributions::Normal` + """ + def __init__(self, mu: float, sigma: float) -> None: ... + @property + def mu(self) -> float: ... + @property + def sigma(self) -> float: ... + +class Poisson: + """ +Poisson distribution (discrete); `pdf` is the mass at round(x) and +`cdf` uses P(X ≤ k) = Q(k+1, λ). + +Rust: `statistics::distributions::Poisson` + """ + def __init__(self, lambda_: float) -> None: ... + def pmf(self, k: int) -> float: ... + @property + def lambda_(self) -> float: ... + +class StudentT: + """ +Student's t distribution with ν degrees of freedom; CDF via the +regularized incomplete beta function. + +Rust: `statistics::distributions::StudentT` + """ + def __init__(self, nu: float) -> None: ... + @property + def nu(self) -> float: ... + +class Weibull: + """ +Weibull distribution with shape k and scale λ. + +Rust: `statistics::distributions::Weibull` + """ + def __init__(self, k: float, lambda_: float) -> None: ... + @property + def k(self) -> float: ... + @property + def lambda_(self) -> float: ... + +def gaussian(x: float, mu: float, sigma: float) -> float: + """ +Gaussian probability density function: f(x) = (1/(σ√(2π))) · exp(-½((x-μ)/σ)²) + +Rust: `statistics::distributions::gaussian` + """ + ... + +def gaussian_cdf(x: float, mu: float, sigma: float) -> float: + """ +Gaussian CDF: Φ(x) = ½·erfc(−(x−μ)/(σ√2)), full double precision. + +Rust: `statistics::distributions::gaussian_cdf` + """ + ... + +def gaussian_cdf_approx(x: float, mu: float, sigma: float) -> float: + """ +Deprecated alias of `gaussian_cdf`; the historical Abramowitz & +Stegun approximation has been replaced by the exact erfc form. + +Rust: `statistics::distributions::gaussian_cdf_approx` + """ + ... + +def poisson_pmf(k: int, lambda_: float) -> float: + """ +Poisson probability mass function: P(k;λ) = λᵏ · e⁻λ / k! + +Rust: `statistics::distributions::poisson_pmf` + """ + ... + +def exponential_pdf(x: float, lambda_: float) -> float: + """ +Exponential probability density function: f(x;λ) = λ · e⁻ˡˣ for x ≥ 0 + +Rust: `statistics::distributions::exponential_pdf` + """ + ... + +def exponential_cdf(x: float, lambda_: float) -> float: + """ +Exponential cumulative distribution function: F(x;λ) = 1 - e⁻ˡˣ for x ≥ 0 + +Rust: `statistics::distributions::exponential_cdf` + """ + ... + +def chi_squared_pdf(x: float, k: int) -> float: + """ +Chi-squared PDF: f(x;k) = x^(k/2-1)·e^(-x/2) / (2^(k/2)·Γ(k/2)) + +Rust: `statistics::distributions::chi_squared_pdf` + """ + ... diff --git a/bindings/python/python/numeria/statistics/fourier.pyi b/bindings/python/python/numeria/statistics/fourier.pyi new file mode 100644 index 0000000..744d032 --- /dev/null +++ b/bindings/python/python/numeria/statistics/fourier.pyi @@ -0,0 +1,44 @@ +""" +Discrete Fourier transform utilities. These are thin wrappers over `transforms::fft` (Step 0 of roadmap Part 3): every length now runs in O(n log n) via the mixed-radix / Bluestein FFT while keeping the original `(re, im)` tuple API. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def dft(signal: list[float]) -> list[tuple[float, float]]: + """ +Discrete Fourier Transform: `X[k] = Σ x[n]·e^(-j2πkn/N)`, returns (real, imag) pairs + +Rust: `statistics::fourier::dft` + """ + ... + +def inverse_dft(spectrum: list[tuple[float, float]]) -> list[float]: + """ +Inverse DFT: `x[n] = (1/N)·Σ X[k]·e^(j2πkn/N)` + +Rust: `statistics::fourier::inverse_dft` + """ + ... + +def power_spectrum(signal: list[float]) -> list[float]: + """ +Power spectrum: `|X[k]|² = Re² + Im²` for each frequency bin. + +Uses the real FFT and reconstructs the upper half from conjugate +symmetry. Output length always equals `signal.len()`. + +Rust: `statistics::fourier::power_spectrum` + """ + ... + +def dominant_frequency(signal: list[float], sample_rate: float) -> float: + """ +Find the dominant frequency in a signal: f_peak = k_max · f_s / N + +Rust: `statistics::fourier::dominant_frequency` + """ + ... diff --git a/bindings/python/python/numeria/statistics/inference.pyi b/bindings/python/python/numeria/statistics/inference.pyi new file mode 100644 index 0000000..4e39089 --- /dev/null +++ b/bindings/python/python/numeria/statistics/inference.pyi @@ -0,0 +1,163 @@ +""" +Hypothesis tests and confidence intervals. p-values come from the crate's own distribution CDFs (Student t, chi-squared, F) and the asymptotic Kolmogorov distribution Q_KS(λ) = 2·Σ (−1)^{j−1} e^{−2j²λ²} (NR §14.3). All t-type tests report two-sided p-values. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.linalg.matrix import Matrix + +class TestResult: + """ +Outcome of a hypothesis test. For tests with two df parameters +(ANOVA, independence tables) `df` is the numerator / primary df; +the p-value always accounts for the full parameterization. + +Rust: `statistics::inference::TestResult` + """ + def __init__(self, statistic: float, p_value: float, df: float) -> None: ... + @property + def statistic(self) -> float: ... + @property + def p_value(self) -> float: ... + @property + def df(self) -> float: ... + +def t_test_one_sample(x: list[float], mu0: float) -> TestResult: + """ +One-sample t test of H₀: μ = mu0. + +R cross-check: `t.test(c(1,2,3,4,5), mu=2)` gives +t = 1.4142, df = 4, p-value = 0.2302. + +Panics: +Panics unless x has at least 2 elements. + +Rust: `statistics::inference::t_test_one_sample` + """ + ... + +def t_test_two_sample(x: list[float], y: list[float], equal_var: bool) -> TestResult: + """ +Two-sample t test of H₀: μₓ = μᵧ. `equal_var` selects the pooled +test; otherwise Welch's test with Welch-Satterthwaite df. + +Analytic cross-check: x = 1..5 vs y = 2,4,…,10 with equal variances +gives t = −3/√2.5 = −1.897367 on 8 df (p ≈ 0.094); Welch df is +exactly 6.25/1.0625 = 5.882353. + +Panics: +Panics unless both samples have at least 2 elements. + +Rust: `statistics::inference::t_test_two_sample` + """ + ... + +def t_test_paired(x: list[float], y: list[float]) -> TestResult: + """ +Paired t test: one-sample test on the pairwise differences. + +Panics: +Panics unless the samples are the same length with n ≥ 2. + +Rust: `statistics::inference::t_test_paired` + """ + ... + +def chi_squared_gof(observed: list[float], expected: list[float]) -> TestResult: + """ +Chi-squared goodness-of-fit test: Σ (O − E)²/E with k − 1 df. + +R cross-check: `chisq.test(c(10,20,30,40), p=rep(0.25,4))` gives +X-squared = 20, df = 3, p-value = 0.0001697. + +Panics: +Panics unless the slices match in length (≥ 2) and all expected +counts are positive. + +Rust: `statistics::inference::chi_squared_gof` + """ + ... + +def chi_squared_independence(table: Matrix | Sequence[Sequence[float]]) -> TestResult: + """ +Chi-squared test of independence on an r×c contingency table; +expected counts from the margins, (r−1)(c−1) df (reported as `df`). + +Panics: +Panics unless the table is at least 2×2 with non-negative entries +and positive margins. + +Rust: `statistics::inference::chi_squared_independence` + """ + ... + +def ks_test_one_sample(x: list[float], cdf: Callable[[float], float]) -> TestResult: + """ +One-sample Kolmogorov-Smirnov test of x against a continuous CDF. +`statistic` is Dₙ; `df` reports the sample size; p uses the NR +asymptotic correction λ = (√n + 0.12 + 0.11/√n)·D. + +Panics: +Panics if x is empty. + +Rust: `statistics::inference::ks_test_one_sample` + """ + ... + +def ks_test_two_sample(x: list[float], y: list[float]) -> TestResult: + """ +Two-sample Kolmogorov-Smirnov test. `df` reports the effective +sample size n₁n₂/(n₁+n₂). + +Panics: +Panics if either sample is empty. + +Rust: `statistics::inference::ks_test_two_sample` + """ + ... + +def anova_one_way(groups: list[list[float]]) -> TestResult: + """ +One-way ANOVA. `statistic` is F; `df` reports the between-groups +(numerator) df k − 1; the p-value uses F(k − 1, N − k). + +Analytic cross-check: groups (1,2,3), (2,3,4), (5,6,7) give +SS_between = 26, SS_within = 6, F = 13 on (2, 6) df. + +Panics: +Panics unless there are ≥ 2 groups, each non-empty, with more total +observations than groups. + +Rust: `statistics::inference::anova_one_way` + """ + ... + +def confidence_interval_mean(x: list[float], level: float) -> tuple[float, float]: + """ +Two-sided confidence interval for the mean at the given level +(e.g. 0.95): x̄ ± t·s/√n. + +Panics: +Panics unless n ≥ 2 and level ∈ (0, 1). + +Rust: `statistics::inference::confidence_interval_mean` + """ + ... + +def pearson_test(x: list[float], y: list[float]) -> TestResult: + """ +Test of H₀: ρ = 0 from the Pearson correlation: +t = r·√((n−2)/(1−r²)) with n − 2 df. + +R cross-check: `cor.test(c(1,2,3,4,5), c(2,1,4,3,5))` gives +r = 0.8, t = 2.3094, df = 3, p-value = 0.1041. + +Panics: +Panics unless both slices have equal length n ≥ 3. + +Rust: `statistics::inference::pearson_test` + """ + ... diff --git a/bindings/python/python/numeria/statistics/resampling.pyi b/bindings/python/python/numeria/statistics/resampling.pyi new file mode 100644 index 0000000..83640cd --- /dev/null +++ b/bindings/python/python/numeria/statistics/resampling.pyi @@ -0,0 +1,80 @@ +""" +Resampling methods: bootstrap, BCa bootstrap, permutation tests, and the jackknife. Reference: Efron & Tibshirani, *An Introduction to the Bootstrap* (1993), ch. 6 (percentile), ch. 14 (BCa), ch. 15 (permutation). +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.monte_carlo import Rng + +class BootstrapResult: + """ +Bootstrap summary: point estimate on the original data, bootstrap +standard error, and the confidence interval bounds. + +Rust: `statistics::resampling::BootstrapResult` + """ + def __init__(self, estimate: float, se: float, ci_low: float, ci_high: float) -> None: ... + @property + def estimate(self) -> float: ... + @property + def se(self) -> float: ... + @property + def ci_low(self) -> float: ... + @property + def ci_high(self) -> float: ... + +def bootstrap(data: list[float], statistic: Callable[[list[float]], float], n_resamples: int, level: float, rng: Rng) -> BootstrapResult: + """ +Percentile bootstrap for an arbitrary statistic at the given +confidence level (e.g. 0.95). + +Panics: +Panics unless data is non-empty, n_resamples ≥ 2, and +level ∈ (0, 1). + +Rust: `statistics::resampling::bootstrap` + """ + ... + +def bootstrap_bca(data: list[float], statistic: Callable[[list[float]], float], n_resamples: int, level: float, rng: Rng) -> BootstrapResult: + """ +Bias-corrected and accelerated (BCa) bootstrap: percentile interval +with the bias correction z₀ from the replicate distribution and the +acceleration a from the jackknife influence values. + +Panics: +Panics unless data has ≥ 2 points, n_resamples ≥ 2, and +level ∈ (0, 1). + +Rust: `statistics::resampling::bootstrap_bca` + """ + ... + +def permutation_test(x: list[float], y: list[float], statistic: Callable[[list[float], list[float]], float], n_perm: int, rng: Rng) -> float: + """ +Two-sample permutation test. `statistic` maps (x, y) to a test +statistic (e.g. difference of means); the returned p-value is the +fraction of label permutations with |T*| ≥ |T| (with the +1 +continuity correction). + +Panics: +Panics unless both samples are non-empty and n_perm ≥ 1. + +Rust: `statistics::resampling::permutation_test` + """ + ... + +def jackknife(data: list[float], statistic: Callable[[list[float]], float]) -> tuple[float, float]: + """ +Jackknife estimate and standard error of a statistic: +SE² = (n−1)/n · Σ (θ₍ᵢ₎ − θ̄)². + +Panics: +Panics unless data has at least 2 points. + +Rust: `statistics::resampling::jackknife` + """ + ... diff --git a/bindings/python/python/numeria/stochastic/__init__.pyi b/bindings/python/python/numeria/stochastic/__init__.pyi new file mode 100644 index 0000000..2b5c4c4 --- /dev/null +++ b/bindings/python/python/numeria/stochastic/__init__.pyi @@ -0,0 +1,12 @@ +""" +Stochastic processes: Markov chains, Markov chain Monte Carlo, and hidden state models. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from . import extreme, hmm, markov, point_process, queueing, rmt, sde, timeseries + + diff --git a/bindings/python/python/numeria/stochastic/extreme.pyi b/bindings/python/python/numeria/stochastic/extreme.pyi new file mode 100644 index 0000000..4508a5e --- /dev/null +++ b/bindings/python/python/numeria/stochastic/extreme.pyi @@ -0,0 +1,478 @@ +""" +Extreme value theory and copulas: the distribution of maxima, the distribution of exceedances, and the dependence structure between them. Ordinary statistics describes the middle of a distribution, where there is data. Extreme value theory describes the edge, where by construction there is almost none, and it does so by an argument that parallels the central limit theorem. Just as a normalised *sum* of independent variables has only one possible limit whatever the summands, a normalised *maximum* has only three -- Gumbel, Frechet, Weibull -- and the generalised extreme value family holds all three, distinguished by the sign of a single shape parameter. That is what licenses extrapolating past the largest observation: the tail shape is not assumed, it is forced. Two routes lead to the same place. Taking the maximum of each block and fitting a GEV throws away every observation but one per block. Taking every exceedance over a high threshold instead keeps far more of the data, and the Pickands-Balkema-de Haan theorem says those exceedances follow a generalised Pareto distribution with the *same* shape parameter. The threshold approach is usually the better estimator; the block approach is easier to explain and needs no threshold chosen. The shape parameter is the whole story. Negative means a bounded tail with a finite upper endpoint; zero means an exponential tail, where every moment exists; positive means a power-law tail, where moments beyond `1/xi` do not. A hundred-year return level computed under the wrong sign is not slightly wrong. Copulas answer the other half of the question. Marginal tails say how extreme each variable gets; a copula says whether they get extreme together. The distinction matters because correlation does not capture it: a Gaussian copula has zero tail dependence at any correlation below one, so two variables can be strongly correlated in the body and yet asymptotically independent in the tail, which is precisely the failure mode a correlation-based risk model cannot see. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.patterns.tilings import Archimedean +from numeria.linalg.matrix import Matrix +from numeria.monte_carlo import Rng + +class CopulaFamily: + """ +The Archimedean and elliptical families supported here. + +Rust: `stochastic::extreme::CopulaFamily` + """ + ... + +def gev_pdf(x: float, mu: float, sigma: float, xi: float) -> float: + """ +The generalised extreme value density. + +Outside the support -- above the upper endpoint when `xi < 0`, below the +lower one when `xi > 0` -- the density is zero. + +Panics: +Panics unless `sigma` is positive. + +Rust: `stochastic::extreme::gev_pdf` + """ + ... + +def gev_cdf(x: float, mu: float, sigma: float, xi: float) -> float: + """ +The generalised extreme value distribution function, +`exp(-[1 + xi (x - mu)/sigma]^(-1/xi))`. + +Panics: +Panics unless `sigma` is positive. + +Rust: `stochastic::extreme::gev_cdf` + """ + ... + +def gev_quantile(p: float, mu: float, sigma: float, xi: float) -> float: + """ +The GEV quantile at probability `p`. + +`mu + (sigma / xi) [(-ln p)^(-xi) - 1]`, or the Gumbel form +`mu - sigma ln(-ln p)` when the shape vanishes. + +Panics: +Panics unless `sigma` is positive and `p` lies strictly in `(0, 1)`. + +Rust: `stochastic::extreme::gev_quantile` + """ + ... + +def gev_fit(maxima: list[float]) -> tuple[float, float, float]: + """ +Fits a GEV to block maxima by maximum likelihood, returning +`(location, scale, shape)`. + +The scale is optimised on the log scale so it cannot go negative, and the +likelihood is infinite wherever an observation would fall outside the +support, which keeps the search inside the feasible region without an +explicit constraint. Started from the moment-matched Gumbel fit, which is +the shape-zero member of the family and a reliable neighbourhood to +descend from. + +Errors: +Returns an error for fewer than ten observations, or if no feasible +parameter set is found. + +Rust: `stochastic::extreme::gev_fit` + """ + ... + +def gumbel_fit(maxima: list[float]) -> tuple[float, float]: + """ +Fits a Gumbel distribution -- the GEV with shape fixed at zero -- by +maximum likelihood, returning `(location, scale)`. + +Worth fitting separately rather than reading off a GEV fit: with the shape +pinned, the two remaining parameters are far better determined, and the +difference in log-likelihood against the free-shape fit is the natural +test of whether the tail is exponential. + +Errors: +Returns an error for fewer than five observations or a constant sample. + +Rust: `stochastic::extreme::gumbel_fit` + """ + ... + +def gpd_pdf(y: float, sigma: float, xi: float) -> float: + """ +The generalised Pareto density for an exceedance `y > 0`. + +Panics: +Panics unless `sigma` is positive. + +Rust: `stochastic::extreme::gpd_pdf` + """ + ... + +def gpd_cdf(y: float, sigma: float, xi: float) -> float: + """ +The generalised Pareto distribution function, +`1 - (1 + xi y / sigma)^(-1/xi)`. + +Panics: +Panics unless `sigma` is positive. + +Rust: `stochastic::extreme::gpd_cdf` + """ + ... + +def gpd_quantile(p: float, sigma: float, xi: float) -> float: + """ +The generalised Pareto quantile at probability `p`. + +Panics: +Panics unless `sigma` is positive and `p` lies in `[0, 1)`. + +Rust: `stochastic::extreme::gpd_quantile` + """ + ... + +def gpd_fit(exceedances: list[float]) -> tuple[float, float]: + """ +Fits a generalised Pareto distribution to threshold exceedances by +maximum likelihood, returning `(scale, shape)`. + +The exceedances must already be measured from the threshold, so they are +all positive. This is the peaks-over-threshold half of the theory: by +Pickands-Balkema-de Haan the shape here is the same shape a GEV fit to +block maxima of the same data would find, but estimated from every large +observation rather than one per block. + +Errors: +Returns an error for fewer than ten exceedances, a non-positive +exceedance, or a failure to find feasible parameters. + +Rust: `stochastic::extreme::gpd_fit` + """ + ... + +def mean_residual_life(x: list[float], thresholds: list[float]) -> list[float]: + """ +The mean excess over each threshold: the average of `x - u` across the +observations that exceed `u`. + +The standard threshold-selection diagnostic. If the exceedances over some +`u` follow a generalised Pareto, the mean excess above any higher +threshold is `(sigma + xi u) / (1 - xi)` -- *linear* in the threshold. So +the point above which the plot straightens is the point above which the +asymptotic theory has taken hold, and a slope of zero means an +exponential tail. + +A threshold exceeded by nothing yields NaN, which is reported rather than +silently dropped. + +Rust: `stochastic::extreme::mean_residual_life` + """ + ... + +def hill_estimator(x: list[float], k: int) -> float: + """ +The Hill estimator of the tail index from the `k` largest observations. + +`(1/k) sum_{i=1}^{k} ln X_(i) - ln X_(k+1)`, where `X_(1)` is the largest. +Estimates `xi` for a heavy tail, and only for a heavy one: the derivation +assumes a regularly varying tail, so a negative or zero shape is outside +its scope and the estimator will still return a positive number there. + +Choosing `k` is the usual bias-variance trade: too small and the estimate +is noisy, too large and observations from the body contaminate it. + +Panics: +Panics unless `1 <= k < n` and all of the top `k + 1` observations are +positive. + +Rust: `stochastic::extreme::hill_estimator` + """ + ... + +def return_level(mu: float, sigma: float, xi: float, period: float) -> float: + """ +The level exceeded on average once every `period` blocks, under a GEV fit. + +The quantile at `1 - 1/period`. A hundred-year level is not the largest +value seen in a century; it is the level with a one-in-a-hundred chance of +being exceeded in any given year. + +Panics: +Panics unless `sigma` is positive and `period` exceeds one. + +Rust: `stochastic::extreme::return_level` + """ + ... + +def return_period(mu: float, sigma: float, xi: float, level: float) -> float: + """ +The average number of blocks between exceedances of `level`, the exact +inverse of `return_level`. + +Infinite for a level at or above the finite upper endpoint of a bounded +tail, which is the honest answer: such a level is never exceeded. + +Panics: +Panics unless `sigma` is positive. + +Rust: `stochastic::extreme::return_period` + """ + ... + +def block_maxima(x: list[float], block: int) -> list[float]: + """ +The maximum of each consecutive block of `block` observations. + +A trailing partial block is dropped: its maximum is drawn from fewer +observations and is not comparable with the rest, and including it biases +the fit downward. + +Panics: +Panics if `block` is zero. + +Rust: `stochastic::extreme::block_maxima` + """ + ... + +def extremal_index(x: list[float], threshold: float) -> float: + """ +The extremal index by the Ferro-Segers intervals estimator. + +Roughly the reciprocal of the mean cluster size: 1 when exceedances arrive +independently, below 1 when they arrive in bursts. It matters because +clustering does not change *how many* exceedances there are but does +change how many *distinct events* they represent, and a return period +computed as though every exceedance were its own event overstates the +frequency by exactly this factor. + +The intervals estimator works from the gaps between exceedances rather +than from a declustering rule, so it needs no run length chosen. + +Returns 1 when there are too few exceedances to say anything. + +Panics: +Panics if `x` is empty. + +Rust: `stochastic::extreme::extremal_index` + """ + ... + +def kendall_tau(x: list[float], y: list[float]) -> float: + """ +Kendall's tau: the probability of concordance minus the probability of +discordance, estimated over all pairs. + +Ties in either coordinate contribute nothing to either count. Unlike +Pearson correlation this depends only on the ranks, so it is invariant +under any increasing transformation of either variable -- which is exactly +what makes it a property of the copula rather than of the margins, and +what lets a copula parameter be recovered from it. + +Panics: +Panics unless the series have equal length and at least two points. + +Rust: `stochastic::extreme::kendall_tau` + """ + ... + +def spearman_rho(x: list[float], y: list[float]) -> float: + """ +Spearman's rho: Pearson correlation applied to the ranks. + +Like Kendall's tau it is a function of the copula alone, but it weights +the whole distribution more evenly, so the two disagree in a way that is +itself informative about the shape of the dependence. + +Panics: +Panics unless the series have equal length and at least two points. + +Rust: `stochastic::extreme::spearman_rho` + """ + ... + +def copula_gaussian_sample(corr: Matrix | Sequence[Sequence[float]], n: int, rng: Rng) -> list[list[float]]: + """ +Samples `n` points from a Gaussian copula with the given correlation +matrix. + +Draws from a multivariate normal by a Cholesky factor and maps each margin +through the standard normal distribution function, which is what leaves +uniform margins and keeps only the dependence. + +Errors: +Returns an error if the matrix is not a valid correlation matrix -- not +square, not symmetric, or not positive definite. + +Rust: `stochastic::extreme::copula_gaussian_sample` + """ + ... + +def copula_t_sample(corr: Matrix | Sequence[Sequence[float]], df: float, n: int, rng: Rng) -> list[list[float]]: + """ +Samples `n` points from a `t` copula with `df` degrees of freedom. + +The same construction as the Gaussian copula but with a shared chi-squared +scaling across all coordinates. That single shared factor is what creates +tail dependence: occasionally it is small, every coordinate is inflated at +once, and the sample lands in a corner. The Gaussian copula has no such +mechanism, which is why its tail dependence is exactly zero. + +Errors: +Returns an error for an invalid correlation matrix or `df` below one. + +Rust: `stochastic::extreme::copula_t_sample` + """ + ... + +def copula_clayton(theta: float, n: int, rng: Rng) -> list[list[float]]: + """ +Samples `n` pairs from a bivariate Clayton copula by conditional +inversion. + +`theta > 0`. Clayton concentrates its dependence in the *lower* tail: its +coefficient of lower tail dependence is `2^(-1/theta)`, while the upper is +zero. That asymmetry is the reason to reach for it -- joint crashes +without joint booms. + +Panics: +Panics unless `theta` is positive. + +Rust: `stochastic::extreme::copula_clayton` + """ + ... + +def copula_gumbel(theta: float, n: int, rng: Rng) -> list[list[float]]: + """ +Samples `n` pairs from a bivariate Gumbel copula. + +`theta >= 1`. The mirror image of Clayton: upper tail dependence +`2 - 2^(1/theta)` and none in the lower tail. + +The conditional distribution has no closed-form inverse, so this uses the +Marshall-Olkin frailty construction instead. The Gumbel generator is the +Laplace transform of a positive stable law, so drawing one such variate +and dividing two independent exponentials by it produces the copula +directly. The stable variate comes from Kanter's algorithm. + +Panics: +Panics unless `theta >= 1`. + +Rust: `stochastic::extreme::copula_gumbel` + """ + ... + +def copula_frank(theta: float, n: int, rng: Rng) -> list[list[float]]: + """ +Samples `n` pairs from a bivariate Frank copula by conditional inversion. + +`theta` may be any non-zero real: positive for positive dependence, +negative for negative. Frank is the symmetric Archimedean copula, with no +tail dependence in either direction -- useful precisely when dependence in +the body should not imply dependence in the extremes. + +Panics: +Panics if `theta` is zero, where the family degenerates to independence. + +Rust: `stochastic::extreme::copula_frank` + """ + ... + +def copula_tau(family: CopulaFamily, theta: float) -> float: + """ +Kendall's tau implied by a copula family at parameter `theta`. + +Each family has a closed-form relation, which is what makes inversion +possible: `2 arcsin(rho) / pi` for the Gaussian, `theta / (theta + 2)` for +Clayton, `1 - 1/theta` for Gumbel, and for Frank +`1 - 4 (1 - D_1(theta)) / theta` with `D_1` the Debye function. + +Rust: `stochastic::extreme::copula_tau` + """ + ... + +def copula_fit_tau(data: list[list[float]], family: CopulaFamily) -> float: + """ +Fits a copula parameter by inverting Kendall's tau. + +The method of moments applied to a rank statistic: measure tau from the +data, then solve the family's tau-theta relation for theta. It needs no +likelihood and no numerical optimisation for three of the four families, +and because tau depends only on the ranks the answer is unaffected by +whatever the margins happen to be -- which is the entire point of +separating a copula from its margins. + +`data` holds one row per observation with two columns. + +Errors: +Returns an error for the wrong shape, or for a sample tau outside the +range the family can represent -- Clayton and Gumbel model only positive +dependence, so a negative tau has no solution. + +Rust: `stochastic::extreme::copula_fit_tau` + """ + ... + +def empirical_copula(data: list[list[float]]) -> list[list[float]]: + """ +The pseudo-observations of a sample: each column replaced by its ranks +divided by `n + 1`. + +This is the empirical copula transform. Dividing by `n + 1` rather than +`n` keeps every value strictly inside `(0, 1)`, which matters because the +copula densities and tail statistics below take logarithms of them. +Whatever the marginal distributions were, the result has approximately +uniform margins and retains exactly the original dependence. + +Errors: +Returns an error for empty or ragged input. + +Rust: `stochastic::extreme::empirical_copula` + """ + ... + +def tail_dependence_coefficient(data: list[list[float]], q: float) -> tuple[float, float]: + """ +Empirical coefficients of `(lower, upper)` tail dependence at quantile +level `q`. + +The lower coefficient estimates `P(V <= q | U <= q)` and the upper +`P(V > q | U > q)`, both computed on pseudo-observations so the margins +are irrelevant. Only one of the pair is informative at any given `q`: read +the lower coefficient at a small `q` and the upper at a `q` near one. At +`q = 0.01` the upper coefficient is the probability both variables exceed +their first percentile, which is close to one for any sample and says +nothing about the tail. As `q` approaches its limit these tend to the theoretical +coefficients: `2^(-1/theta)` and 0 for Clayton, 0 and `2 - 2^(1/theta)` +for Gumbel, and 0 for both under any Gaussian copula with correlation +below one. + +The last of those is the practically important one. Two variables can have +a correlation of 0.9 and still, under a Gaussian copula, become +independent in the limit of extreme events. + +Errors: +Returns an error for the wrong shape or a `q` outside `(0, 1)`. + +Rust: `stochastic::extreme::tail_dependence_coefficient` + """ + ... + +def pickands_dependence(data: list[list[float]], t: float) -> float: + """ +The Pickands dependence function estimated at `t`, for a bivariate +extreme-value copula. + +An extreme-value copula is determined entirely by a convex function `A` on +`[0, 1]` satisfying `max(t, 1-t) <= A(t) <= 1`. The two bounds are the two +extremes of dependence: `A == 1` is independence, and `A(t) = max(t, 1-t)` +is perfect dependence. Everything in between is a real dependence +structure, and `A` is the whole of it. + +Estimated by Pickands' original construction, the reciprocal of the mean +of `min(xi/(1-t), eta/t)` over the transformed data. + +Errors: +Returns an error for the wrong shape or a `t` outside `(0, 1)`. + +Rust: `stochastic::extreme::pickands_dependence` + """ + ... diff --git a/bindings/python/python/numeria/stochastic/hmm.pyi b/bindings/python/python/numeria/stochastic/hmm.pyi new file mode 100644 index 0000000..81fb0ea --- /dev/null +++ b/bindings/python/python/numeria/stochastic/hmm.pyi @@ -0,0 +1,180 @@ +""" +Hidden state models: hidden Markov models, smoothing, and particle filters. The common thread is a state that evolves as a Markov chain and is never observed directly -- only through emissions that depend on it. Three questions follow, and each has its own algorithm. *How likely is this observation sequence?* is answered by summing over every possible state path, which the forward recursion does in linear time by never enumerating the paths. *Which single path best explains it?* is answered by Viterbi, the same recursion with the sum replaced by a maximum. *What parameters make it likeliest?* is answered by Baum-Welch, which is expectation-maximisation applied to the first two. The discrete and Gaussian models here differ only in what an emission is. The Kalman smoother and the particle filter answer the same questions for a continuous state: exactly, when the model is linear and Gaussian, and by sampling when it is not. Everything works in logs or with explicit scaling, because the probability of a sequence of a few hundred observations underflows a double long before the algorithm finishes. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.control_systems.kalman import KalmanFilter +from numeria.linalg.matrix import Matrix +from numeria.monte_carlo import Rng + +class FilterStep: + """ +One step of a Kalman filter's output: the state estimate and its +covariance, before and after the measurement. + +Rust: `stochastic::hmm::FilterStep` + """ + def __init__(self, predicted: list[float], predicted_cov: Matrix | Sequence[Sequence[float]], filtered: list[float], filtered_cov: Matrix | Sequence[Sequence[float]]) -> None: ... + @property + def predicted(self) -> list[float]: ... + @property + def predicted_cov(self) -> Matrix: ... + @property + def filtered(self) -> list[float]: ... + @property + def filtered_cov(self) -> Matrix: ... + +class GaussianHmm: + """ +A hidden Markov model whose emissions are one-dimensional Gaussians. + +Rust: `stochastic::hmm::GaussianHmm` + """ + def __init__(self, a: Matrix | Sequence[Sequence[float]], means: list[float], vars: list[float], pi: list[float]) -> None: ... + def n_states(self) -> int: ... + def emission(self, i: int, x: float) -> float: ... + def forward(self, obs: list[float]) -> tuple[float, Matrix]: ... + def backward(self, obs: list[float]) -> Matrix: ... + def viterbi(self, obs: list[float]) -> tuple[float, list[int]]: ... + def baum_welch(self, obs: list[float], iters: int, tol: float) -> float: ... + def simulate(self, n: int, rng: Rng) -> tuple[list[int], list[float]]: ... + @property + def a(self) -> Matrix: ... + @property + def means(self) -> list[float]: ... + @property + def vars(self) -> list[float]: ... + @property + def pi(self) -> list[float]: ... + +class Hmm: + """ +A hidden Markov model with discrete emissions. + +Rust: `stochastic::hmm::Hmm` + """ + def __init__(self, a: Matrix | Sequence[Sequence[float]], b: Matrix | Sequence[Sequence[float]], pi: list[float]) -> None: ... + def n_states(self) -> int: ... + def n_symbols(self) -> int: ... + @staticmethod + def random_init(n_states: int, n_symbols: int, rng: Rng) -> Hmm: ... + def forward(self, obs: list[int]) -> tuple[float, Matrix]: ... + def backward(self, obs: list[int]) -> Matrix: ... + def log_likelihood(self, obs: list[int]) -> float: ... + def viterbi(self, obs: list[int]) -> tuple[float, list[int]]: ... + def posteriors(self, obs: list[int]) -> Matrix: ... + def posterior_decode(self, obs: list[int]) -> list[int]: ... + def baum_welch(self, sequences: list[list[int]], iters: int, tol: float) -> float: ... + def simulate(self, n: int, rng: Rng) -> tuple[list[int], list[int]]: ... + @property + def a(self) -> Matrix: ... + @property + def b(self) -> Matrix: ... + @property + def pi(self) -> list[float]: ... + +class ParticleFilter: + """ +A bootstrap particle filter: a cloud of weighted samples standing in for +the state distribution. + +Where the Kalman filter propagates a mean and a covariance -- which is +exactly right if everything is linear and Gaussian and wrong otherwise -- +this propagates samples, so it can represent any shape at all. The price +is variance, and the need to resample: without it the weight concentrates +on one particle and the rest of the cloud stops contributing. + +Rust: `stochastic::hmm::ParticleFilter` + """ + def update(self, likelihood: Callable[[list[float]], float]) -> None: ... + def resample_systematic(self, rng: Rng) -> None: ... + def estimate(self) -> list[float]: ... + def effective_n(self) -> float: ... + @property + def particles(self) -> list[list[float]]: ... + @property + def weights(self) -> list[float]: ... + +def kalman_filter_sequence(kf: KalmanFilter, measurements: list[list[float]]) -> list[FilterStep]: + """ +Runs a Kalman filter over a sequence of measurements, keeping every +intermediate so a smoother can walk back through them. + +Errors: +Returns an error if any linear solve fails. + +Rust: `stochastic::hmm::kalman_filter_sequence` + """ + ... + +def rts_smooth(kf: KalmanFilter, steps: list[FilterStep]) -> tuple[list[list[float]], list[Matrix]]: + """ +The Rauch-Tung-Striebel smoother: the best estimate of each state given +*all* the data, not just the data up to that point. + +A backward pass over the filter's output. Each smoothed estimate is the +filtered one corrected by how much the next step's smoothed estimate +disagreed with what the filter predicted, weighted by the gain +`P F' Ppred^-1`. Because it conditions on strictly more information than +the filter does, the smoothed covariance is never larger -- which is the +property the tests check, and the reason to run it at all. + +Errors: +Returns an error if any linear solve fails. + +Panics: +Panics on an empty sequence. + +Rust: `stochastic::hmm::rts_smooth` + """ + ... + +def rts_lag_one_covariances(kf: KalmanFilter, steps: list[FilterStep], smoothed_cov: list[Matrix | Sequence[Sequence[float]]]) -> list[Matrix]: + """ +The lag-one smoothed cross-covariances, which the +expectation-maximisation step needs and the plain smoother does not +return. + +`lag[k]` is the smoothed covariance between the state at `k` and the one +at `k - 1`, with `lag[0]` unused. Without it the process-noise estimate +has no way to know how correlated consecutive smoothed states are, and +treating them as independent inflates the residual it is built from. + +Errors: +Returns an error if any linear solve fails. + +Rust: `stochastic::hmm::rts_lag_one_covariances` + """ + ... + +def em_kalman(initial: KalmanFilter, measurements: list[list[float]], iters: int) -> KalmanFilter: + """ +Learns a Kalman filter's process and measurement noise from data, by +expectation-maximisation. + +The smoother gives the expected states and their covariances; those give +the noise covariances in closed form; those give a better smoother. As +with Baum-Welch, each round cannot lower the likelihood and the answer +depends on where it started. The dynamics and observation matrices are +taken as known, which is the usual situation -- they are physics, while +the noise is a fudge factor nobody knows. + +The covariance terms in the maximisation are not optional. The residual +of the smoothed states against the dynamics understates the process noise +on its own, because the smoothed states are shrunk towards each other; +the smoothed covariances are what put back the uncertainty that shrinkage +hid. + +Errors: +Returns an error if any linear solve fails. + +Panics: +Panics on an empty measurement sequence. + +Rust: `stochastic::hmm::em_kalman` + """ + ... diff --git a/bindings/python/python/numeria/stochastic/markov.pyi b/bindings/python/python/numeria/stochastic/markov.pyi new file mode 100644 index 0000000..dc0a51e --- /dev/null +++ b/bindings/python/python/numeria/stochastic/markov.pyi @@ -0,0 +1,93 @@ +""" +Finite Markov chains and Markov chain Monte Carlo. A Markov chain is a square matrix whose rows sum to one, and almost everything about it follows from linear algebra applied to that matrix. The long-run behaviour is an eigenvector; how fast it is reached is the gap between the leading eigenvalue and the next; expected hitting times are the solution of a linear system; and the answer to "what happens after `n` steps" is a matrix power. Markov chain Monte Carlo runs the idea backwards. Given a distribution you can evaluate but not sample from, build a chain whose stationary distribution is that one, and run it. Metropolis-Hastings does this by proposing a move and accepting it with a probability that makes detailed balance hold; Hamiltonian Monte Carlo does it by simulating a physical trajectory that conserves energy, so the acceptance probability stays near one even for a long move. The samplers are only ever asymptotically correct, so the diagnostics -- effective sample size, the Gelman-Rubin statistic, the autocorrelation time -- are not optional extras but the only evidence that a run has converged. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.graph.core import Graph +from numeria.linalg.matrix import Matrix +from numeria.monte_carlo import Rng + +class MarkovChain: + """ +A finite Markov chain, held as its row-stochastic transition matrix. + +Rust: `stochastic::markov::MarkovChain` + """ + def __init__(self, p: Matrix | Sequence[Sequence[float]]) -> None: ... + def n(self) -> int: ... + @staticmethod + def from_counts(transitions: Matrix | Sequence[Sequence[float]]) -> MarkovChain: ... + @staticmethod + def from_sequence(states: list[int], n_states: int) -> MarkovChain: ... + def step_dist(self, dist: list[float]) -> list[float]: ... + def n_step(self, n: int) -> Matrix: ... + def stationary(self) -> list[float]: ... + def simulate(self, start: int, steps: int, rng: Rng) -> list[int]: ... + def is_irreducible(self) -> bool: ... + def period(self, state: int) -> int: ... + def is_aperiodic(self) -> bool: ... + def classify_states(self) -> list[StateClass]: ... + def absorbing_probabilities(self) -> Matrix: ... + def fundamental_matrix(self) -> Matrix: ... + def expected_steps_to_absorption(self) -> list[float]: ... + def hitting_time(self, from_: int, target: list[int]) -> float: ... + def hitting_probability(self, from_: int, target: list[int]) -> float: ... + def return_time(self, state: int) -> float: ... + def mfpt_matrix(self) -> Matrix: ... + @staticmethod + def total_variation_distance(a: list[float], b: list[float]) -> float: ... + def mixing_time(self, eps: float) -> int: ... + def spectral_gap(self) -> float: ... + def reversible_check(self, pi: list[float], tol: float) -> bool: ... + def entropy_rate(self) -> float: ... + def coupling_from_the_past_small(self, rng: Rng) -> int: ... + @staticmethod + def pagerank_chain(g: Graph, damping: float) -> MarkovChain: ... + @property + def p(self) -> Matrix: ... + +class Mcmc: + """ +Samplers that build a chain whose stationary distribution is a target you +can evaluate but not sample from directly. + +Every method here takes the *log* of the target, unnormalised. Logs +because the density of anything interesting underflows; unnormalised +because the normalising constant is exactly the thing that is usually +impossible to compute, and none of these methods needs it -- they see the +target only through ratios, in which it cancels. + +Rust: `stochastic::markov::Mcmc` + """ + @staticmethod + def metropolis_hastings(log_target: Callable[[list[float]], float], x0: list[float], proposal_std: float, n: int, burn: int, rng: Rng) -> list[list[float]]: ... + @staticmethod + def adaptive_metropolis(log_target: Callable[[list[float]], float], x0: list[float], proposal_std: float, n: int, burn: int, rng: Rng) -> list[list[float]]: ... + @staticmethod + def hamiltonian_mc(log_target: Callable[[list[float]], float], grad: Callable[[list[float]], list[float]], x0: list[float], step: float, n_leapfrog: int, n: int, burn: int, rng: Rng) -> list[list[float]]: ... + @staticmethod + def nuts_lite(log_target: Callable[[list[float]], float], grad: Callable[[list[float]], list[float]], x0: list[float], step: float, max_depth: int, n: int, burn: int, rng: Rng) -> list[list[float]]: ... + @staticmethod + def slice_sampler(log_target_1d: Callable[[float], float], x0: float, w: float, n: int, rng: Rng) -> list[float]: ... + @staticmethod + def parallel_tempering(log_target: Callable[[list[float]], float], temps: list[float], x0: list[float], proposal_std: float, n: int, burn: int, rng: Rng) -> list[list[float]]: ... + @staticmethod + def autocorrelation_time(chain: list[float]) -> float: ... + @staticmethod + def effective_sample_size(chain: list[float]) -> float: ... + @staticmethod + def gelman_rubin(chains: list[list[float]]) -> float: ... + @staticmethod + def simulated_annealing(energy: Callable[[list[float]], float], x0: list[float], schedule: Callable[[int], float], n: int, rng: Rng) -> tuple[list[float], float]: ... + +class StateClass: + """ +How a state behaves in the long run. + +Rust: `stochastic::markov::StateClass` + """ + ... diff --git a/bindings/python/python/numeria/stochastic/point_process.pyi b/bindings/python/python/numeria/stochastic/point_process.pyi new file mode 100644 index 0000000..a384921 --- /dev/null +++ b/bindings/python/python/numeria/stochastic/point_process.pyi @@ -0,0 +1,374 @@ +""" +Point processes: random collections of points in time or space. The Poisson process is the reference against which every other is described. It has no memory -- the chance of an event in the next instant does not depend on what happened before -- and everything else follows: counts in disjoint sets are independent and Poisson, waiting times are exponential, and given the count in an interval the points are uniformly scattered in it. The other processes here are departures from that in one of two directions. *Clustered* processes -- Hawkes, Cox, Matern, Thomas -- put more points near other points, either because events trigger events or because the rate is itself random. *Regular* processes have points that avoid each other. Ripley's `K` function and the pair correlation measure which of the three a pattern is, by comparing what is seen at each distance against what a Poisson process would give. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.statistics.distributions import Poisson +from numeria.spatial.primitives import Rect +from numeria.monte_carlo import Rng +from numeria.statistics.inference import TestResult +from numeria.math import Vec2 + +def poisson_process(rate: float, t_end: float, rng: Rng) -> list[float]: + """ +Event times of a homogeneous Poisson process on `[0, t_end]`. + +Generated from exponential waiting times, which is the process's own +definition rather than a device: the memorylessness of the exponential is +exactly the memorylessness of the process. + +Panics: +Panics unless the rate is non-negative and `t_end` is positive. + +Rust: `stochastic::point_process::poisson_process` + """ + ... + +def poisson_inhomogeneous(rate_fn: Callable[[float], float], rate_max: float, t_end: float, rng: Rng) -> list[float]: + """ +Event times of a Poisson process whose rate varies with time, by thinning. + +Generate a homogeneous process at the maximum rate, then keep each point +with probability equal to the ratio of the true rate there to the +maximum. Lewis and Shedler's construction, and it is exact rather than an +approximation: the retained points have precisely the right intensity, +whatever shape the rate function has. + +Panics: +Panics unless `rate_max` is positive, `t_end` is positive, or if the rate +function exceeds the stated maximum, which would make the thinning wrong +rather than merely inefficient. + +Rust: `stochastic::point_process::poisson_inhomogeneous` + """ + ... + +def poisson_2d(rate: float, region: Rect, rng: Rng) -> list[Vec2]: + """ +A Poisson point pattern in a rectangle. + +The count is Poisson with mean `rate` times the area, and given the count +the points are independent and uniform -- which is the cleanest statement +of what complete spatial randomness means. + +Panics: +Panics unless the rate is non-negative and the rectangle has positive +area. + +Rust: `stochastic::point_process::poisson_2d` + """ + ... + +def poisson_3d(rate: float, min: tuple[float, float, float], max: tuple[float, float, float], rng: Rng) -> list[tuple[float, float, float]]: + """ +A Poisson point pattern in a box. + +Panics: +Panics unless the rate is non-negative and every side is positive. + +Rust: `stochastic::point_process::poisson_3d` + """ + ... + +def hawkes_intensity(events: list[float], mu: float, alpha: float, beta: float, t: float) -> float: + """ +The conditional intensity of a Hawkes process with an exponential kernel. + +`mu + sum over past events of alpha exp(-beta (t - t_i))`. Each event +raises the chance of the next, and the excitation decays; the process is +its own trigger, which is what makes it a model for earthquakes and for +order flow alike. + +Rust: `stochastic::point_process::hawkes_intensity` + """ + ... + +def hawkes_branching_ratio(alpha: float, beta: float) -> float: + """ +The branching ratio `alpha / beta`: the expected number of events each +event directly triggers. + +Below one the process is stationary; at or above one it explodes, because +each generation of offspring is at least as large as the last. It is the +mean of a Galton-Watson offspring distribution wearing different clothes. + +Rust: `stochastic::point_process::hawkes_branching_ratio` + """ + ... + +def hawkes_process(mu: float, alpha: float, beta: float, t_end: float, rng: Rng) -> list[float]: + """ +A Hawkes process with an exponential kernel, by Ogata's thinning. + +The intensity only ever falls between events, so it can be bounded by its +value just after the last one; propose from a homogeneous process at that +bound and accept in proportion. Rebounding after each event is what keeps +the acceptance rate high. + +Panics: +Panics unless `mu` and `beta` are positive, `alpha` is non-negative, and +the branching ratio is below one -- above it the process explodes and no +simulation terminates. + +Rust: `stochastic::point_process::hawkes_process` + """ + ... + +def hawkes_log_likelihood(events: list[float], t_end: float, mu: float, alpha: float, beta: float) -> float: + """ +The log-likelihood of a Hawkes process with an exponential kernel. + +`sum log lambda(t_i) - integral lambda`. The integral has a closed form +for this kernel, and the sum can be accumulated in one pass by the same +recursion, so the whole thing is linear in the event count rather than +quadratic. + +Rust: `stochastic::point_process::hawkes_log_likelihood` + """ + ... + +def hawkes_fit_mle(events: list[float], t_end: float) -> tuple[float, float, float]: + """ +Maximum likelihood estimates of a Hawkes process's parameters. + +Returns `(mu, alpha, beta)`, found by a coordinate search over the +log-likelihood. The likelihood is not concave in these coordinates, so +this is a local optimiser started from moment-based guesses rather than a +guarantee. + +Panics: +Panics unless there are at least two events and `t_end` is positive. + +Rust: `stochastic::point_process::hawkes_fit_mle` + """ + ... + +def matern_cluster_process(parent_rate: float, cluster_radius: float, daughter_mean: float, region: Rect, rng: Rng) -> list[Vec2]: + """ +A Matern cluster process: Poisson parents, each surrounded by a Poisson +number of daughters uniformly inside a disc. + +Only the daughters are returned. Parents outside the region still throw +daughters into it, so they are generated over a margin as wide as the +cluster radius; omitting that margin would thin the pattern near the +edges and is the standard way a clustered simulation comes out wrong. + +Panics: +Panics unless the rates and the radius are positive and the region has +positive area. + +Rust: `stochastic::point_process::matern_cluster_process` + """ + ... + +def thomas_process(parent_rate: float, spread: float, daughter_mean: float, region: Rect, rng: Rng) -> list[Vec2]: + """ +A Thomas process: the same as Matern, with daughters scattered by a +Gaussian instead of uniformly in a disc. + +The Gaussian has no hard edge, so the clusters blend rather than ending +abruptly; the margin is taken at four standard deviations, past which the +contribution is negligible. + +Panics: +Panics unless the rates and the spread are positive. + +Rust: `stochastic::point_process::thomas_process` + """ + ... + +def ripley_k(points: list[Vec2 | Sequence[float]], region: Rect, r_values: list[float]) -> list[float]: + """ +Ripley's `K` function: the expected number of further points within `r` of +a typical point, divided by the intensity. + +For complete spatial randomness it is `pi r^2` at every distance, because +the expected count in a disc is the intensity times its area and the +division cancels the intensity. Above that means clustering and below +means regularity, so the whole diagnostic is a comparison against a +parabola. + +Edge effects are handled by Ripley's isotropic correction: a point near +the boundary sees only part of its own circle, so each neighbour is +weighted by the reciprocal of the fraction of that circle lying inside +the region. Without it every pattern looks regular near the edges. + +The correction is trustworthy only while the radius stays well inside the +window -- a quarter of the shorter side is the usual limit. Beyond that a +point near a corner has most of its circle outside, the weight it earns is +large, and the estimate becomes both noisy and biased upward. + +Panics: +Panics unless the region has positive area and the radii are positive. + +Rust: `stochastic::point_process::ripley_k` + """ + ... + +def l_function(points: list[Vec2 | Sequence[float]], region: Rect, r_values: list[float]) -> list[float]: + """ +Besag's `L` function: `sqrt(K / pi)`, which is `r` itself under complete +spatial randomness. + +The point of the transformation is that a straight line is far easier to +read a departure from than a parabola, and it stabilises the variance +along the way. + +Panics: +Panics under the same conditions as `ripley_k`. + +Rust: `stochastic::point_process::l_function` + """ + ... + +def pair_correlation(points: list[Vec2 | Sequence[float]], region: Rect, r: float, dr: float) -> float: + """ +The pair correlation function: the density of points at distance `r` from +a typical point, relative to the intensity. + +One everywhere under complete spatial randomness. Where `K` accumulates +everything within `r` and so smears features together, this looks at a +shell of width `dr` and shows the distance at which clustering actually +happens. + +Panics: +Panics unless the region has positive area and `r` and `dr` are positive +with `dr` below `r`. + +Rust: `stochastic::point_process::pair_correlation` + """ + ... + +def nearest_neighbor_index(points: list[Vec2 | Sequence[float]], region: Rect) -> float: + """ +The Clark-Evans nearest neighbour index: the mean nearest-neighbour +distance divided by what a Poisson pattern of the same intensity would +give. + +One for complete spatial randomness, below one for clustering, above for +regularity. The expected distance under randomness is +`1 / (2 sqrt(intensity))`, which follows from the void probability: the +chance that the nearest neighbour is beyond `r` is the chance a disc of +radius `r` is empty. + +Panics: +Panics unless the region has positive area and there are at least two +points. + +Rust: `stochastic::point_process::nearest_neighbor_index` + """ + ... + +def quadrat_test(points: list[Vec2 | Sequence[float]], region: Rect, nx: int, ny: int) -> TestResult: + """ +The quadrat test: divide the region into cells and test whether the counts +look Poisson. + +Under complete spatial randomness every cell has the same expected count, +so a chi-squared goodness-of-fit against a flat expectation is the test. +It sees departures in the *variance* of the counts and is blind to +anything at a scale finer than a cell, which is why it is a first look +rather than a conclusion. + +Errors: +Returns an error unless the grid is at least two by two and there are at +least as many points as cells. + +Rust: `stochastic::point_process::quadrat_test` + """ + ... + +def ks_test_exponential_interarrivals(events: list[float]) -> TestResult: + """ +Tests whether the gaps between events look exponential, which is what a +Poisson process requires. + +A Kolmogorov-Smirnov test against the exponential distribution with the +observed mean. A small p-value says the process is not Poisson; a large +one says only that this particular test did not notice. + +Errors: +Returns an error unless there are at least three events with positive +gaps. + +Rust: `stochastic::point_process::ks_test_exponential_interarrivals` + """ + ... + +def branching_process_gw(offspring_pmf: list[float], generations: int, rng: Rng) -> list[int]: + """ +A Galton-Watson branching process: the population size at each +generation. + +Every individual independently has a random number of offspring from the +same distribution. The population dies out with probability one when the +mean offspring count is at most one -- including exactly one, which is the +surprise: a population that replaces itself on average still goes extinct +unless the count is deterministic. + +A supercritical population is held once it passes two thousand: beyond +that its extinction probability is smaller than any double can represent, +so the remaining generations carry no information and every one of them +would cost time proportional to the population. + +Panics: +Panics unless the offspring distribution is a probability vector. + +Rust: `stochastic::point_process::branching_process_gw` + """ + ... + +def extinction_probability(offspring_pgf_coeffs: list[float]) -> float: + """ +The extinction probability of a branching process: the smallest fixed +point of the offspring generating function in `[0, 1]`. + +One when the mean offspring count is at most one, and strictly below one +above it. The fixed point equation says that a lineage dies out exactly +when every one of its founder's children's lineages does, which is the +whole argument in one line. + +Panics: +Panics unless the coefficients are a probability vector. + +Rust: `stochastic::point_process::extinction_probability` + """ + ... + +def yule_process(birth_rate: float, t_end: float, rng: Rng) -> list[float]: + """ +A Yule process: pure birth, each individual splitting at a constant rate. + +Returns the times at which the population grew. The population at time `t` +is geometric with mean `exp(birth_rate t)`, which is the continuous-time +analogue of a branching process that never dies. + +Panics: +Panics unless the rate and the horizon are positive. + +Rust: `stochastic::point_process::yule_process` + """ + ... + +def birth_death_simulate(birth: float, death: float, n0: int, t_end: float, rng: Rng) -> list[tuple[float, int]]: + """ +A linear birth-death process, by Gillespie's direct method. + +Returns `(time, population)` after each event. The population dies out +with probability one when the death rate is at least the birth rate, and +with probability `(death / birth)^n0` when it is not -- which is the +branching process's extinction probability again, in continuous time. + +A population past a thousand is held, for the reason +`branching_process_gw` gives. + +Panics: +Panics unless the rates are non-negative and the horizon is positive. + +Rust: `stochastic::point_process::birth_death_simulate` + """ + ... diff --git a/bindings/python/python/numeria/stochastic/queueing.pyi b/bindings/python/python/numeria/stochastic/queueing.pyi new file mode 100644 index 0000000..1e41e5e --- /dev/null +++ b/bindings/python/python/numeria/stochastic/queueing.pyi @@ -0,0 +1,341 @@ +""" +Queueing theory: birth-death queues, Erlang loss and delay formulas, networks of queues, and continuous-time Markov chains. Almost every closed form here is a birth-death chain in disguise. A queue with Poisson arrivals and exponential service moves up one state at rate `lambda` and down one at a rate set by how many servers are busy, so the stationary distribution telescopes into a product of ratios and the means follow by summation. The Erlang formulas are the two boundary cases of that product: B when a full system turns customers away, C when it makes them wait. Two results tie the whole module together and are worth stating because the tests lean on them. Little's law, `L = lambda W`, holds for every model below -- it is a statement about areas under a sample path and assumes nothing about the arrival or service distributions. And the Pollaczek-Khinchine formula shows what the exponential assumption was buying: for a single server the mean queue depends on the service distribution only through its first two moments, so M/D/1 has exactly half the queue of M/M/1 at the same load. Where a model has no closed form the module simulates it instead. The event-driven simulator tracks the number in system by integrating over a merged event list rather than by invoking Little's law, so comparing its output against `lambda W` is a real check rather than a tautology. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.stochastic.markov import MarkovChain +from numeria.linalg.matrix import Matrix +from numeria.statistics.distributions import Poisson +from numeria.monte_carlo import Rng + +class Ctmc: + """ +A continuous-time Markov chain, held as its generator matrix. + +Rows of `q` sum to zero: the off-diagonal entries are transition rates and +the diagonal is minus their total, so `-q_ii` is the rate of leaving state +`i`. Where a discrete chain asks "what is the next state", a generator +asks "how long until something happens, and what". + +Rust: `stochastic::queueing::Ctmc` + """ + def __init__(self, q: Matrix | Sequence[Sequence[float]]) -> None: ... + def n(self) -> int: ... + def mean_holding_times(self) -> list[float]: ... + def embedded_chain(self) -> MarkovChain: ... + def stationary(self) -> list[float]: ... + def simulate(self, start: int, t_end: float, rng: Rng) -> list[tuple[float, int]]: ... + def first_passage(self, from_: int, to: int) -> float: ... + @property + def q(self) -> Matrix: ... + +class QueueMetrics: + """ +The standard summary of a queue in steady state. + +`l` and `lq` count customers, `w` and `wq` measure time. The two pairs are +linked by Little's law at the *effective* arrival rate, which differs from +the offered rate whenever the system turns customers away. + +Rust: `stochastic::queueing::QueueMetrics` + """ + def __init__(self, rho: float, l: float, lq: float, w: float, wq: float, p0: float, lambda_eff: float, model: QueueModel) -> None: ... + def pn(self, n: int) -> float: ... + @property + def rho(self) -> float: ... + @property + def l(self) -> float: ... + @property + def lq(self) -> float: ... + @property + def w(self) -> float: ... + @property + def wq(self) -> float: ... + @property + def p0(self) -> float: ... + @property + def lambda_eff(self) -> float: ... + @property + def model(self) -> QueueModel: ... + +class QueueModel: + """ +Which birth-death chain a set of metrics came from. + +Carried alongside the means so that `QueueMetrics::pn` can report the +exact stationary probability of `n` in the system. Models with no +product-form state distribution report `QueueModel::MeanValueOnly`. + +Rust: `stochastic::queueing::QueueModel` + """ + ... + +class QueueSimResult: + """ +What an event-driven run measured. + +The time averages come from integrating the sample path over a merged +list of arrival and departure events, independently of the customer +averages, so `l` and `lambda_eff * w` are two separate measurements of the +same quantity rather than one derived from the other. + +Rust: `stochastic::queueing::QueueSimResult` + """ + def __init__(self, l: float, lq: float, w: float, wq: float, rho: float, lambda_eff: float, served: int) -> None: ... + @property + def l(self) -> float: ... + @property + def lq(self) -> float: ... + @property + def w(self) -> float: ... + @property + def wq(self) -> float: ... + @property + def rho(self) -> float: ... + @property + def lambda_eff(self) -> float: ... + @property + def served(self) -> int: ... + +def mm1(lambda_: float, mu: float) -> QueueMetrics: + """ +A single-server queue with Poisson arrivals and exponential service. + +The stationary distribution is geometric, `p_n = (1 - rho) rho^n`, which +gives `L = rho / (1 - rho)` directly. + +Panics: +Panics unless `lambda` and `mu` are positive. + +Rust: `stochastic::queueing::mm1` + """ + ... + +def mmc(lambda_: float, mu: float, c: int) -> QueueMetrics: + """ +`c` parallel servers, Poisson arrivals, exponential service, no limit on +the queue. The probability an arrival has to wait is Erlang C. + +Unstable loads (`lambda >= c mu`) return infinite means with `rho >= 1`; +the queue really does grow without bound there, so that is the answer +rather than an error. + +Panics: +Panics unless `lambda` and `mu` are positive and `c >= 1`. + +Rust: `stochastic::queueing::mmc` + """ + ... + +def mm1k(lambda_: float, mu: float, k: int) -> QueueMetrics: + """ +A single server with room for `k` customers in total. Arrivals that find +the system full are lost, so the effective arrival rate is `lambda (1 - p_k)` +and the queue is stable at any load. + +Panics: +Panics unless `lambda` and `mu` are positive and `k >= 1`. + +Rust: `stochastic::queueing::mm1k` + """ + ... + +def mmck(lambda_: float, mu: float, c: int, k: int) -> QueueMetrics: + """ +`c` servers with room for `k` in total, `k >= c`. Arrivals finding the +system full are lost. + +Panics: +Panics unless `lambda` and `mu` are positive and `c <= k`. + +Rust: `stochastic::queueing::mmck` + """ + ... + +def mm_inf(lambda_: float, mu: float) -> QueueMetrics: + """ +Unlimited servers: every arrival enters service at once. The number in +system is Poisson with mean `lambda / mu`, so nobody ever waits. + +Panics: +Panics unless `lambda` and `mu` are positive. + +Rust: `stochastic::queueing::mm_inf` + """ + ... + +def erlang_b(offered_load: float, c: int) -> float: + """ +Erlang's loss formula: the fraction of calls blocked by `c` trunks under +an offered load of `a` erlangs. + +Computed by the recursion `B_c = a B_{c-1} / (c + a B_{c-1})` rather than +the ratio of factorial sums. The two agree exactly in real arithmetic, but +the direct form overflows near `c = 170` while the recursion stays in +`[0, 1]` at every step and is accurate for any `c`. + +Panics: +Panics if `a` is negative. + +Rust: `stochastic::queueing::erlang_b` + """ + ... + +def erlang_c(load: float, c: int) -> float: + """ +Erlang's delay formula: the probability an arrival to an `M/M/c` queue +finds every server busy and has to wait. + +Returns 1 for a saturated system. Related to the loss formula by +`C = B / (1 - rho (1 - B))`, which is how it is evaluated here. + +Panics: +Panics if `load` is negative or `c` is zero. + +Rust: `stochastic::queueing::erlang_c` + """ + ... + +def erlang_b_inverse_capacity(load: float, blocking_target: float) -> int: + """ +The smallest number of trunks that holds blocking at or below +`blocking_target` for the given offered load. + +Steps the Erlang B recursion upward, which is monotone decreasing in `c`, +so the first `c` that clears the target is the smallest one. + +Panics: +Panics unless the target is in `(0, 1]` and the load is non-negative. + +Rust: `stochastic::queueing::erlang_b_inverse_capacity` + """ + ... + +def mg1_pollaczek_khinchine(lambda_: float, service_mean: float, service_var: float) -> QueueMetrics: + """ +The Pollaczek-Khinchine mean-value formula for a single server with +Poisson arrivals and a general service distribution. + +`Lq = lambda^2 (var + mean^2) / (2 (1 - rho))`. The service distribution +enters only through its first two moments: exponential service has +`var = mean^2` and recovers M/M/1, while deterministic service has +`var = 0` and halves the queue. + +Panics: +Panics unless `lambda` and `service_mean` are positive and the variance is +non-negative. + +Rust: `stochastic::queueing::mg1_pollaczek_khinchine` + """ + ... + +def gg1_kingman_approx(lambda_: float, mu: float, ca2: float, cs2: float) -> float: + """ +Kingman's diffusion approximation for the mean wait in a G/G/1 queue, +given the squared coefficients of variation of the interarrival and +service times. + +`Wq ~ (rho / (1 - rho)) ((ca2 + cs2) / 2) (1 / mu)`. It is exact for +M/M/1, where both coefficients are one and the middle factor drops out, +and is asymptotically exact as `rho -> 1` for any distribution. + +Panics: +Panics unless the rates are positive and the coefficients non-negative. + +Rust: `stochastic::queueing::gg1_kingman_approx` + """ + ... + +def littles_law_check(l: float, lambda_: float, w: float) -> float: + """ +The residual `L - lambda W`, which any consistent set of steady-state +numbers must drive to zero. + +Little's law is a pathwise identity, not a distributional one, so this is +a genuine check on measured or simulated quantities rather than an +assumption about the model. + +Rust: `stochastic::queueing::littles_law_check` + """ + ... + +def jackson_network(routing: Matrix | Sequence[Sequence[float]], external: list[float], service: list[float], servers: list[int]) -> list[QueueMetrics]: + """ +An open Jackson network of `M/M/c` nodes. + +`routing[i][j]` is the probability a customer leaving node `i` goes to +node `j`; whatever is left over departs the network. Total arrival rates +solve the traffic equations `lambda_j = external_j + sum_i lambda_i r_ij`, +after which Jackson's theorem says each node behaves in steady state +exactly like an isolated `M/M/c_j` queue at its own total rate -- even +though the internal arrival streams are not Poisson. + +Errors: +Returns `GeomError::InvalidArgument` if the shapes disagree, if a +routing row sums past one, or if the traffic equations are singular. + +Rust: `stochastic::queueing::jackson_network` + """ + ... + +def priority_queue_simulate(lambdas: list[float], mus: list[float], c: int, t_end: float, rng: Rng) -> list[QueueSimResult]: + """ +A non-preemptive priority queue with `c` servers and one exponential +class per entry of `lambdas`. + +Class 0 has the highest priority. A waiting customer of a higher class is +always taken next, but a job already in service runs to completion. +Returns one result per class. + +The discipline is work-conserving, so Kleinrock's conservation law applies: +`sum_k rho_k Wq_k` is the same here as under plain FIFO, however the +priorities are arranged. Only the split between classes changes. + +Panics: +Panics unless the rate vectors match in length, are positive, and +`c >= 1`. + +Rust: `stochastic::queueing::priority_queue_simulate` + """ + ... + +def uniformization(q_matrix: Matrix | Sequence[Sequence[float]], p0: list[float], t: float, eps: float) -> list[float]: + """ +Transient distribution of a continuous-time chain by uniformization. + +Writes `P(t) = exp(Qt)` as a Poisson mixture of powers of a discrete +chain: pick a rate `L` at least as large as every exit rate, set +`P = I + Q/L`, and then `p(t) = sum_k e^{-Lt} (Lt)^k / k! * p0 P^k`. Every +term is a probability vector and every weight is positive, so unlike a +truncated matrix exponential the partial sums never go negative, however +stiff the generator. + +The sum is truncated when the remaining Poisson mass falls below `eps`. + +Errors: +Returns `GeomError::InvalidArgument` if `p0` is the wrong length, is not +a distribution, or if `t` or `eps` are not positive. + +Rust: `stochastic::queueing::uniformization` + """ + ... + +def queue_transient_mm1(lambda_: float, mu: float, n0: int, t: float) -> list[float]: + """ +The distribution of the number in an M/M/1 queue at time `t`, starting +from exactly `n0` customers. + +The state space is truncated well above the point where the stationary +geometric tail is negligible, then run through `uniformization`. Returns +the probability of each state from 0 up to the truncation point. + +Errors: +Returns an error if the rates are not positive or the transient solve fails. + +Rust: `stochastic::queueing::queue_transient_mm1` + """ + ... diff --git a/bindings/python/python/numeria/stochastic/rmt.pyi b/bindings/python/python/numeria/stochastic/rmt.pyi new file mode 100644 index 0000000..63f129f --- /dev/null +++ b/bindings/python/python/numeria/stochastic/rmt.pyi @@ -0,0 +1,328 @@ +""" +Random matrix theory: the classical ensembles, their limiting spectral laws, and the local statistics that distinguish correlated spectra from uncorrelated ones. The subject rests on a surprise: the eigenvalues of a large random matrix are not themselves random in any useful sense. Their *density* converges to a fixed shape that does not depend on the distribution of the entries -- Wigner's semicircle for a symmetric matrix, Marchenko-Pastur for a sample covariance -- and their *spacings* converge to a distribution that depends only on the symmetry class. Universality is what makes the subject applicable: a spectrum can be compared against these laws without knowing anything about the mechanism that produced it. The practical payoff is a null hypothesis. Eigenvalues of independent variables repel each other, in a way that independent *points* do not, so the spacing distribution separates a spectrum with genuine level correlations from a Poisson process of unrelated levels. In finance the same statement is a filter: any eigenvalue of a sample correlation matrix that falls inside the Marchenko-Pastur band is consistent with pure noise and carries no information about the correlations being estimated. Two conventions are fixed throughout. Ensembles are scaled so their limiting support stays put as `n` grows -- otherwise the semicircle's radius would drift and nothing would converge to compare against. And spacings are always measured on *unfolded* eigenvalues, rescaled to unit mean density, since the raw spacings of a semicircular spectrum are much tighter in the middle than at the edges and their distribution would say more about the density than about the correlations. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.linalg.matrix import Matrix +from numeria.statistics.distributions import Poisson +from numeria.monte_carlo import Rng + +def goe_sample(n: int, rng: Rng) -> Matrix: + """ +A sample from the Gaussian orthogonal ensemble: a symmetric matrix whose +entries are Gaussian, independent up to the symmetry constraint. + +Scaled so the spectrum fills `[-2, 2]` in the large-`n` limit: off-diagonal +entries have variance `1/n` and diagonal entries `2/n`. The factor of two +on the diagonal is not decorative -- it is what makes the distribution +invariant under orthogonal conjugation, which is the defining property of +the ensemble and the reason its spectral statistics are universal. + +Panics: +Panics if `n` is zero. + +Rust: `stochastic::rmt::goe_sample` + """ + ... + +def gue_sample(n: int, rng: Rng) -> tuple[Matrix, Matrix]: + """ +A sample from the Gaussian unitary ensemble, returned as +`(real part, imaginary part)` of a Hermitian matrix. + +The real part is symmetric and the imaginary part antisymmetric with a +zero diagonal, which together is what "Hermitian" means for a matrix held +in two real halves. Scaled to the same `[-2, 2]` support as +`goe_sample`: each independent real degree of freedom carries variance +`1/(2n)`, so `E|H_ij|^2 = 1/n` off the diagonal. + +Panics: +Panics if `n` is zero. + +Rust: `stochastic::rmt::gue_sample` + """ + ... + +def ginibre_sample(n: int, rng: Rng) -> Matrix: + """ +A sample from the Ginibre ensemble: every entry independent Gaussian, with +no symmetry imposed at all. + +Its eigenvalues are complex and fill the unit disc rather than an +interval, which is the point of the ensemble -- non-normality changes the +spectral picture completely. + +Panics: +Panics if `n` is zero. + +Rust: `stochastic::rmt::ginibre_sample` + """ + ... + +def wishart_sample(n: int, p: int, rng: Rng) -> Matrix: + """ +A sample covariance matrix built from `p` independent variables observed +`n` times, each observation standard Gaussian. + +Returns `X' X / n` where `X` is `n` by `p`, so the population covariance +is the identity and every departure from it in the sample is estimation +noise. That noise is exactly what `marchenko_pastur` describes. + +Panics: +Panics if either dimension is zero. + +Rust: `stochastic::rmt::wishart_sample` + """ + ... + +def wigner_semicircle(x: float, r: float) -> float: + """ +Wigner's semicircle density on `[-r, r]`. + +`f(x) = 2 sqrt(r^2 - x^2) / (pi r^2)`, zero outside. The limiting +eigenvalue density of a symmetric random matrix, whatever the entry +distribution, provided the entries are independent with finite variance -- +the first and simplest statement of universality in the subject. + +Panics: +Panics unless `r` is positive. + +Rust: `stochastic::rmt::wigner_semicircle` + """ + ... + +def marchenko_pastur(x: float, ratio: float, sigma2: float) -> float: + """ +The Marchenko-Pastur density for a sample covariance matrix. + +`ratio` is `p / n`, the number of variables over the number of +observations, and `sigma2` the population variance. Support is +`[sigma2 (1 -+ sqrt(ratio))^2]`; the density there is +`sqrt((b - x)(x - a)) / (2 pi ratio sigma2 x)`. + +This is the shape a covariance matrix of *independent* variables takes. +The width of the band is the whole point: at `ratio = 0.5` the sample +eigenvalues spread over roughly `[0.09, 2.9]` even though every population +eigenvalue is exactly 1. + +The point mass at zero when `ratio > 1` (more variables than +observations, so the matrix is singular) is not part of the density and is +not reported here. + +Panics: +Panics unless `ratio` and `sigma2` are positive. + +Rust: `stochastic::rmt::marchenko_pastur` + """ + ... + +def mp_edges(ratio: float, sigma2: float) -> tuple[float, float]: + """ +The two edges of the Marchenko-Pastur support, +`sigma2 (1 -+ sqrt(ratio))^2`. + +Any sample eigenvalue between these is consistent with pure noise. + +Panics: +Panics unless `ratio` and `sigma2` are positive. + +Rust: `stochastic::rmt::mp_edges` + """ + ... + +def eigenvalue_spacing_distribution(eigs: list[float]) -> list[float]: + """ +Gaps between consecutive eigenvalues after unfolding to unit mean density. + +Unfolding is not a cosmetic step. The raw gaps of a semicircular spectrum +are far tighter near zero than near the edges, so their distribution would +mostly reflect that varying density rather than the correlations between +levels. Mapping each eigenvalue through a smooth estimate of its own +cumulative count removes the density and leaves the local statistics, +which is what the surmises below describe. + +The smooth estimate here is the empirical staircase itself, smoothed by +averaging over a window that grows as the square root of the sample -- the +standard compromise between following the density and following the +fluctuations one is trying to measure. + +Returns `eigs.len() - 1` gaps with mean 1. An empty or single-element +input gives an empty result. + +Rust: `stochastic::rmt::eigenvalue_spacing_distribution` + """ + ... + +def wigner_surmise_goe(s: float) -> float: + """ +Wigner's surmise for the orthogonal class: +`(pi/2) s exp(-pi s^2 / 4)`. + +The spacing distribution of a two-by-two GOE matrix, which turns out to +approximate the large-`n` answer to within a percent. Its defining feature +is the linear vanishing at `s = 0`: eigenvalues of a real symmetric random +matrix repel, so exact degeneracies have probability zero and near ones +are rare. + +Rust: `stochastic::rmt::wigner_surmise_goe` + """ + ... + +def wigner_surmise_gue(s: float) -> float: + """ +Wigner's surmise for the unitary class: +`(32 / pi^2) s^2 exp(-4 s^2 / pi)`. + +The repulsion is quadratic rather than linear -- a complex Hermitian +matrix has twice as many degrees of freedom to tune away from a +degeneracy, so near-degeneracies are suppressed harder than in the +orthogonal class. + +Rust: `stochastic::rmt::wigner_surmise_gue` + """ + ... + +def poisson_spacing(s: float) -> float: + """ +The spacing density of uncorrelated levels: `exp(-s)`. + +A Poisson process of points has no repulsion at all, so its density is +maximal at zero. This is the null the surmises above are contrasted +against, and the contrast at small `s` is the whole diagnostic. + +Rust: `stochastic::rmt::poisson_spacing` + """ + ... + +def spectral_rigidity(eigs: list[float], l: float) -> float: + """ +The spectral rigidity `Delta_3(L)`: the mean-square deviation of the +unfolded counting function from the best straight line over a window of +length `L`. + +Where the spacing distribution measures correlations between *neighbours*, +rigidity measures them over a stretch of `L` levels, and it is the more +discriminating of the two. Uncorrelated levels give `L / 15`, growing +linearly; a correlated spectrum gives roughly `ln(L) / pi^2`, growing so +slowly that at `L = 20` the two differ by an order of magnitude. + +Averaged over windows starting across the spectrum. + +Panics: +Panics unless `l` is positive. + +Rust: `stochastic::rmt::spectral_rigidity` + """ + ... + +def tracy_widom_beta1_approx(x: float) -> float: + """ +An approximation to the Tracy-Widom distribution function for the +orthogonal class, the law of the largest eigenvalue after edge scaling. + +Represented as a shifted gamma matched to the first three cumulants of +`TW_1` (mean `-1.2065`, variance `1.6078`, skewness `0.2935`), which is the +standard closed-form stand-in: exact evaluation needs the Hastings-McLeod +solution of Painleve II. Accurate to a few parts in a thousand through the +body, degrading in the far tails, where the true law decays like +`exp(-|x|^3/24)` on the left and `exp(-(2/3) x^{3/2})` on the right. + +The distribution matters because the largest eigenvalue does not +fluctuate on the scale of the spectrum: it sits within `n^{-2/3}` of the +edge, so a spike only a little above the Marchenko-Pastur edge is still +strong evidence of real signal. + +Rust: `stochastic::rmt::tracy_widom_beta1_approx` + """ + ... + +def participation_ratio(vec: list[float]) -> float: + """ +The inverse participation ratio of a vector: `sum v_i^4 / (sum v_i^2)^2`. + +A measure of how many components carry the weight. A vector concentrated +on one component scores 1; one spread evenly over `n` scores `1/n`. For +eigenvectors it separates localised states from extended ones, and a GOE +eigenvector -- uniform on the sphere -- sits at `3/n`, the extra factor +being the fourth moment of a Gaussian. + +Returns zero for a zero vector. + +Rust: `stochastic::rmt::participation_ratio` + """ + ... + +def level_spacing_ratio(eigs: list[float]) -> float: + """ +The mean ratio of consecutive level spacings, +``. + +The great virtue of this statistic is that it needs no unfolding: a ratio +of adjacent gaps is insensitive to the local density, which cancels. That +removes the one genuinely arbitrary step in spacing analysis. The limiting +values are 0.5307 for the orthogonal class, 0.5996 for the unitary, and +`2 ln 2 - 1 = 0.3863` for uncorrelated levels. + +Returns zero for fewer than three eigenvalues. + +Rust: `stochastic::rmt::level_spacing_ratio` + """ + ... + +def correlation_matrix_denoise_mp(corr: Matrix | Sequence[Sequence[float]], t_over_n: float) -> Matrix: + """ +Cleans a sample correlation matrix by replacing every eigenvalue inside +the Marchenko-Pastur band with their common average. + +`t_over_n` is the number of observations divided by the number of +variables, so the band is set by `ratio = 1 / t_over_n`. Eigenvalues below +the upper edge are indistinguishable from the noise a correlation matrix +of independent variables would produce, and estimating each of them +separately fits that noise. Replacing them by their mean keeps the trace +-- so the cleaned matrix still has unit diagonal on average and remains a +correlation matrix -- while discarding the structure that was not there. + +The eigenvalues above the edge, and their eigenvectors, are left alone. + +Errors: +Returns an error if the matrix is not square and symmetric, if `t_over_n` +is not positive, or if the eigen-decomposition fails to converge. + +Rust: `stochastic::rmt::correlation_matrix_denoise_mp` + """ + ... + +def symmetric_spectrum(a: Matrix | Sequence[Sequence[float]]) -> list[float]: + """ +The eigenvalues of a symmetric matrix, sorted ascending. + +A convenience over `eigen_symmetric` for the spectral statistics above, +which never need the eigenvectors. + +Errors: +Returns an error if the matrix is not symmetric or the solver fails. + +Rust: `stochastic::rmt::symmetric_spectrum` + """ + ... + +def hermitian_spectrum(re: Matrix | Sequence[Sequence[float]], im: Matrix | Sequence[Sequence[float]]) -> list[float]: + """ +The eigenvalues of a Hermitian matrix held as `(real, imaginary)` parts. + +Uses the standard real embedding: the `2n`-by-`2n` real symmetric matrix +`[[Re, -Im], [Im, Re]]` has exactly the eigenvalues of `H`, each appearing +twice. Returns the `n` distinct ones by taking every second value of the +sorted `2n`, which is what lets a real symmetric solver handle the unitary +ensemble without any complex arithmetic. + +Errors: +Returns an error if the two halves disagree in shape or the solver fails. + +Rust: `stochastic::rmt::hermitian_spectrum` + """ + ... diff --git a/bindings/python/python/numeria/stochastic/sde.pyi b/bindings/python/python/numeria/stochastic/sde.pyi new file mode 100644 index 0000000..9c41e50 --- /dev/null +++ b/bindings/python/python/numeria/stochastic/sde.pyi @@ -0,0 +1,569 @@ +""" +Stochastic differential equations: simulation, convergence, and the densities the paths are distributed by. An equation `dX = mu dt + sigma dW` is not an ordinary differential equation with noise added. Brownian motion is nowhere differentiable, and `dW` has magnitude of order `sqrt(dt)` rather than `dt`, so a term that would be second order in a deterministic expansion is first order here. That is the whole content of Ito's lemma, and it is why the numerical schemes are not the familiar ones: Euler-Maruyama looks like Euler's method but converges at half its order, and recovering first order needs the Milstein correction, which is precisely the term Ito's lemma says is missing. *Strong* convergence is about paths -- how close a simulated path is to the exact path driven by the same noise -- and *weak* convergence is about distributions, how close the expectation of a function is. They are genuinely different: Euler-Maruyama is strong order one half and weak order one. Which one matters depends on the question, and both are measured here rather than asserted. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.statistics.distributions import Poisson +from numeria.monte_carlo import Rng + +class HestonParams: + """ +Parameters of the Heston stochastic volatility model. + +Rust: `stochastic::sde::HestonParams` + """ + def __init__(self, mu: float, kappa: float, theta: float, xi: float, rho: float) -> None: ... + @property + def mu(self) -> float: ... + @property + def kappa(self) -> float: ... + @property + def theta(self) -> float: ... + @property + def xi(self) -> float: ... + @property + def rho(self) -> float: ... + +def brownian_motion(n: int, dt: float, rng: Rng) -> list[float]: + """ +A Brownian path of `n + 1` points at spacing `dt`, starting at zero. + +Increments are independent Gaussians of variance `dt`, which is the +definition. Everything else in the module is built on this or on the +same increments used differently. + +Panics: +Panics unless `dt` is positive. + +Rust: `stochastic::sde::brownian_motion` + """ + ... + +def brownian_bridge(n: int, dt: float, x0: float, x1: float, rng: Rng) -> list[float]: + """ +A Brownian bridge: a path pinned at both ends. + +Built by taking a free Brownian path and subtracting the linear +interpolation of its own endpoint error. The result has variance +`t (T - t) / T` -- zero at both ends and largest in the middle -- which is +what conditioning on the destination does to the uncertainty. + +Panics: +Panics unless `dt` is positive and `n` is at least one. + +Rust: `stochastic::sde::brownian_bridge` + """ + ... + +def brownian_2d(n: int, dt: float, rng: Rng) -> list[tuple[float, float]]: + """ +A Brownian path in two dimensions, as independent coordinates. + +Panics: +Panics unless `dt` is positive. + +Rust: `stochastic::sde::brownian_2d` + """ + ... + +def brownian_3d(n: int, dt: float, rng: Rng) -> list[tuple[float, float, float]]: + """ +A Brownian path in three dimensions. + +Panics: +Panics unless `dt` is positive. + +Rust: `stochastic::sde::brownian_3d` + """ + ... + +def geometric_brownian(x0: float, mu: float, sigma: float, n: int, dt: float, rng: Rng) -> list[float]: + """ +Geometric Brownian motion, simulated by exact log-space steps. + +`dS = mu S dt + sigma S dW`. Its logarithm is Brownian with drift +`mu - sigma^2 / 2`, so the process can be stepped exactly rather than +approximated -- and the `- sigma^2 / 2` is Ito's correction, the +difference between the drift of the process and the drift of its +logarithm. Simulating in log space also guarantees the path stays +positive, which a naive Euler step does not. + +Panics: +Panics unless `dt` is positive and `x0` is positive. + +Rust: `stochastic::sde::geometric_brownian` + """ + ... + +def gbm_exact(x0: float, mu: float, sigma: float, t: float, z: float) -> float: + """ +The exact solution of geometric Brownian motion at time `t`, given the +standard normal `z` that drives it. + +The closed form the schemes are measured against. Passing the driving +normal in rather than drawing it is what lets a numerical path and the +exact path share the same noise, which is what strong convergence means. + +Rust: `stochastic::sde::gbm_exact` + """ + ... + +def ornstein_uhlenbeck(x0: float, theta: float, mu: float, sigma: float, n: int, dt: float, rng: Rng) -> list[float]: + """ +An Ornstein-Uhlenbeck path, stepped exactly. + +`dX = theta (mu - X) dt + sigma dW`: a Brownian particle pulled back +towards `mu` at a rate proportional to its distance. Unlike Brownian +motion it has a stationary distribution -- Gaussian with mean `mu` and +variance `sigma^2 / (2 theta)` -- because the restoring pull eventually +balances the noise. The transition density is Gaussian in closed form, so +this is exact at any step size. + +Panics: +Panics unless `dt` and `theta` are positive. + +Rust: `stochastic::sde::ornstein_uhlenbeck` + """ + ... + +def ou_exact_step(x: float, theta: float, mu: float, sigma: float, dt: float, z: float) -> float: + """ +One exact Ornstein-Uhlenbeck step, given the standard normal driving it. + +Panics: +Panics unless `theta` and `dt` are positive. + +Rust: `stochastic::sde::ou_exact_step` + """ + ... + +def euler_maruyama(mu: Callable[[float, float], float], sigma: Callable[[float, float], float], x0: float, t_end: float, n: int, rng: Rng) -> list[float]: + """ +The Euler-Maruyama scheme for a scalar equation. + +`X_{k+1} = X_k + mu dt + sigma sqrt(dt) Z`. The obvious discretisation, +and strong order one half rather than the order one Euler's method +achieves without noise -- because the neglected term involves +`(dW)^2`, which is of order `dt` rather than `dt^2`. + +Panics: +Panics unless `n` is positive and `t_end` is positive. + +Rust: `stochastic::sde::euler_maruyama` + """ + ... + +def milstein(mu: Callable[[float, float], float], sigma: Callable[[float, float], float], dsigma_dx: Callable[[float, float], float], x0: float, t_end: float, n: int, rng: Rng) -> list[float]: + """ +The Milstein scheme, which restores strong order one. + +Adds `0.5 sigma sigma' ((dW)^2 - dt)` to the Euler step. That term is +exactly what Ito's lemma says the expansion of `sigma(X)` contributes at +first order and Euler-Maruyama drops; putting it back doubles the +convergence rate for the price of one derivative. + +Panics: +Panics unless `n` and `t_end` are positive. + +Rust: `stochastic::sde::milstein` + """ + ... + +def stochastic_heun(mu: Callable[[float, float], float], sigma: Callable[[float, float], float], x0: float, t_end: float, n: int, rng: Rng) -> list[float]: + """ +The stochastic Heun scheme, which converges to the *Stratonovich* +solution. + +A predictor-corrector: step forward, evaluate the coefficients there too, +and average. In the deterministic case that is the trapezoidal rule; with +noise it changes which stochastic integral is being computed. The +Stratonovich integral evaluates the integrand at the midpoint of each +interval rather than the left end, which makes the ordinary chain rule +hold and Ito's correction vanish -- and makes the answer differ from the +Ito one by `0.5 sigma sigma'`. + +Panics: +Panics unless `n` and `t_end` are positive. + +Rust: `stochastic::sde::stochastic_heun` + """ + ... + +def srk_order_1_5(mu: Callable[[float, float], float], sigma: float, x0: float, t_end: float, n: int, rng: Rng) -> list[float]: + """ +A stochastic Runge-Kutta scheme of strong order one and a half for +additive noise. + +With `sigma` constant the double stochastic integrals that ordinarily +block high-order schemes reduce to two correlated Gaussians, which can be +drawn directly. Both are drawn here, so the extra half order is real +rather than a relabelled Milstein. + +Panics: +Panics unless `n` and `t_end` are positive. + +Rust: `stochastic::sde::srk_order_1_5` + """ + ... + +def strong_convergence_order(errors: list[float], dts: list[float]) -> float: + """ +The measured strong convergence order of a scheme. + +`errors` are mean absolute path errors against the exact solution, one +per step size in `dts`. The order is the slope of the error against the +step on log axes, by least squares. Measuring it rather than assuming it +is the only way to notice that a scheme has been implemented at the wrong +order, which looks like nothing at all at a single step size. + +Panics: +Panics unless the two slices have the same length, at least two entries, +and all values are positive. + +Rust: `stochastic::sde::strong_convergence_order` + """ + ... + +def weak_convergence_order(errors: list[float], dts: list[float]) -> float: + """ +The measured weak convergence order, from errors in an expectation. + +The same regression on a different error. A scheme can be weak order one +while being strong order a half, which is not a contradiction: getting +the distribution right is easier than getting each path right. + +Panics: +Panics under the same conditions as `strong_convergence_order`. + +Rust: `stochastic::sde::weak_convergence_order` + """ + ... + +def cir_process(x0: float, kappa: float, theta: float, sigma: float, n: int, dt: float, rng: Rng) -> list[float]: + """ +A Cox-Ingersoll-Ross path by the full truncation scheme. + +`dX = kappa (theta - X) dt + sigma sqrt(X) dW`. The square root makes the +noise vanish at zero, so the exact process never goes negative -- but a +discretisation can step below zero and then take the root of a negative +number. + +Full truncation lets the *internal* state go negative and applies +`max(X, 0)` only inside the coefficients, reporting the truncated value. +Clipping the state itself instead -- reflecting at zero -- is the obvious +alternative and a much worse one: every reflection injects probability +mass that the exact process does not have, and the bias grows rather than +shrinks as the step is refined, because a finer step visits the boundary +more often. Full truncation has the smallest measured bias of the +published variants, which is why it is the one in use. + +Panics: +Panics unless `dt`, `kappa` and `theta` are positive and `x0` is +non-negative. + +Rust: `stochastic::sde::cir_process` + """ + ... + +def heston_paths(s0: float, v0: float, params: HestonParams | Sequence[float], n: int, dt: float, rng: Rng) -> tuple[list[float], list[float]]: + """ +Heston paths: an asset whose variance is itself a Cox-Ingersoll-Ross +process. + +The correlation between the two noises is what makes the model useful. +A negative `rho` means variance rises when the price falls, which +reproduces the skew that a constant-volatility model cannot. + +Panics: +Panics unless `dt` and `s0` are positive, `v0` is non-negative, and the +correlation lies in `[-1, 1]`. + +Rust: `stochastic::sde::heston_paths` + """ + ... + +def jump_diffusion_merton(x0: float, mu: float, sigma: float, lambda_: float, jump_mu: float, jump_sigma: float, n: int, dt: float, rng: Rng) -> list[float]: + """ +Merton's jump diffusion: geometric Brownian motion with Poisson jumps of +lognormal size. + +The jumps put weight in the tails that a diffusion cannot, which is what +the model exists for. Between jumps it is exactly geometric Brownian +motion, and the compensator `lambda (exp(jump_mu + jump_sigma^2/2) - 1)` +is subtracted from the drift so the expected return is `mu` whether or +not a jump lands. + +Panics: +Panics unless `dt` and `x0` are positive and `lambda` is non-negative. + +Rust: `stochastic::sde::jump_diffusion_merton` + """ + ... + +def levy_stable_sample(alpha: float, beta: float, rng: Rng) -> float: + """ +A draw from a stable distribution by the Chambers-Mallows-Stuck method. + +The stable laws are the only possible limits of normalised sums, and only +the Gaussian among them has finite variance. `alpha` is the tail index: +two gives a Gaussian, one with `beta` zero gives Cauchy, and anything +below two has infinite variance and a tail decaying like a power rather +than an exponential. `beta` is the skew. + +Panics: +Panics unless `alpha` is in `(0, 2]` and `beta` is in `[-1, 1]`. + +Rust: `stochastic::sde::levy_stable_sample` + """ + ... + +def fractional_brownian(h: float, n: int, rng: Rng) -> list[float]: + """ +Fractional Brownian motion with Hurst parameter `h`, by the Davies-Harte +method. + +Increments are correlated rather than independent: `h` above a half gives +a path that persists, below a half one that reverses, and exactly a half +gives ordinary Brownian motion. Davies and Harte's method embeds the +covariance into a circulant matrix, whose eigenvalues a Fourier transform +supplies, so an exact sample costs one transform instead of a Cholesky +factorisation. + +Panics: +Panics unless `h` is in `(0, 1)` and `n` is positive. Falls back to a +Cholesky construction if the circulant embedding is not non-negative +definite, which can happen near the ends of the range. + +Rust: `stochastic::sde::fractional_brownian` + """ + ... + +def hurst_exponent_rs(x: list[float]) -> float: + """ +The Hurst exponent by rescaled range analysis. + +Split the series into blocks of several sizes, and for each measure the +range of the cumulative deviation from the block mean divided by the +block's standard deviation. That ratio grows like the block size to the +power `H`, and the slope on log axes is the estimate. Hurst found the +relation studying Nile flood records; the point is that it needs no model +of the process at all. + +Panics: +Panics unless the series has at least sixteen points. + +Rust: `stochastic::sde::hurst_exponent_rs` + """ + ... + +def hurst_dfa(x: list[float]) -> float: + """ +The Hurst exponent by detrended fluctuation analysis. + +Integrate the series, split it into windows, remove a linear trend from +each, and measure the residual fluctuation against the window size. The +detrending is what lets it work on data with a slow drift, which +rescaled range analysis mistakes for persistence. + +The input should be the *increments* -- fractional Gaussian noise, not +fractional Brownian motion. Feeding it an already-integrated series +returns `H + 1`, since the routine integrates once itself. + +Windows shorter than sixteen points are skipped. Removing a straight line +from eight points takes out a real part of the fluctuation along with the +trend, which biases the exponent up by several hundredths -- enough to +make white noise look persistent. + +Panics: +Panics unless the series has at least thirty-two points. + +Rust: `stochastic::sde::hurst_dfa` + """ + ... + +def first_passage_time_sim(barrier: float, drift: float, diffusion: float, t_end: float, dt: float, n_paths: int, rng: Rng) -> list[float]: + """ +First passage times of a drifting Brownian motion to a barrier, by +simulation. + +Returns one time per path that reached the barrier; paths that did not are +omitted, so a short horizon returns fewer times than paths. + +Panics: +Panics unless `dt`, `t_end` and `n_paths` are positive. + +Rust: `stochastic::sde::first_passage_time_sim` + """ + ... + +def first_passage_bm_exact(barrier: float, drift: float, diffusion: float, t: float) -> float: + """ +The exact density of the first passage time of a drifting Brownian motion +to a barrier. + +The inverse Gaussian density. It has a closed form because the reflection +principle turns the question "did the path ever reach the barrier" into a +statement about where the reflected path ended, which is an ordinary +Gaussian probability. + +Panics: +Panics unless `t` and the barrier are positive. + +Rust: `stochastic::sde::first_passage_bm_exact` + """ + ... + +def first_passage_bm_cdf(barrier: float, drift: float, diffusion: float, t: float) -> float: + """ +The probability that a drifting Brownian motion has reached the barrier +by time `t`. + +`Phi((mu t - b) / (sigma sqrt t)) + exp(2 mu b / sigma^2) +Phi((-mu t - b) / (sigma sqrt t))`. The second term is the reflection +principle's contribution: paths that crossed and came back are counted by +reflecting them about the barrier, which maps them onto paths that ended +beyond it. With a non-positive drift the limit as `t` grows is +`exp(2 mu b / sigma^2)` rather than one, since such a path may never +arrive at all. + +Panics: +Panics unless `t` and the barrier are positive. + +Rust: `stochastic::sde::first_passage_bm_cdf` + """ + ... + +def feynman_kac_check(s0: float, strike: float, rate: float, sigma: float, t: float, n_paths: int, rng: Rng) -> tuple[float, float]: + """ +Checks the Feynman-Kac correspondence: the expectation of a payoff along +simulated paths against the solution of the matching partial differential +equation. + +Returns `(monte_carlo, closed_form)` for a European call under geometric +Brownian motion, where the closed form is Black-Scholes. That the two +agree is not a coincidence -- Feynman-Kac says the expectation of a +terminal payoff over the paths of a diffusion *is* the solution of the +backward equation, which is what turns an option price into a partial +differential equation and back. + +Panics: +Panics unless the parameters are positive. + +Rust: `stochastic::sde::feynman_kac_check` + """ + ... + +def ito_isometry_check(sigma: Callable[[float], float], t: float, steps: int, n_paths: int, rng: Rng) -> tuple[float, float]: + """ +Checks Ito's isometry: the variance of a stochastic integral equals the +integral of the squared integrand. + +Returns `(measured, expected)`. The isometry is what makes stochastic +integration work at all -- it says the map from integrands to integrals +preserves the `L^2` norm, so the integral can be defined for any +square-integrable integrand by taking limits. + +Panics: +Panics unless `t`, `n_paths` and `steps` are positive. + +Rust: `stochastic::sde::ito_isometry_check` + """ + ... + +def langevin_underdamped(x0: float, v0: float, gamma: float, temp: float, mass: float, force: Callable[[float], float], n: int, dt: float, rng: Rng) -> list[tuple[float, float]]: + """ +Underdamped Langevin dynamics by the BAOAB splitting. + +A particle in a force field with friction and thermal noise. The +integrator splits the dynamics into a drift, a kick and an +Ornstein-Uhlenbeck step on the velocity, and applies them in the +palindromic order B-A-O-A-B. The symmetry is what gives it the best known +accuracy for configurational averages: at any step size it samples +positions from very nearly the right distribution, even where the +velocities are visibly wrong. + +Returns position and velocity at each step. `temp` is in energy units, so +the equipartition result is ` = temp / mass`. + +Panics: +Panics unless `dt`, `mass` and `gamma` are positive and `temp` is +non-negative. + +Rust: `stochastic::sde::langevin_underdamped` + """ + ... + +def fokker_planck_1d(p0: list[float], mu: Callable[[float], float], sigma: Callable[[float], float], x_min: float, dx: float, dt: float, steps: int) -> list[float]: + """ +One step of the Fokker-Planck equation by the Chang-Cooper scheme. + +The density evolves as `dp/dt = -d(mu p)/dx + 0.5 d^2(sigma^2 p)/dx^2`. +Chang and Cooper's discretisation weights the drift term so that the +scheme's own stationary solution is the exact one -- an ordinary centred +difference relaxes to a slightly wrong density and stays there, which is +the failure this scheme exists to avoid. Zero-flux boundaries, so the +total probability is conserved exactly. + +Panics: +Panics unless `dx`, `dt` are positive and the density has at least three +points. + +Rust: `stochastic::sde::fokker_planck_1d` + """ + ... + +def stationary_density_1d(mu: Callable[[float], float], sigma: Callable[[float], float], x_range: tuple[float, float], n: int) -> list[float]: + """ +The stationary density of a one-dimensional diffusion, in closed form. + +`p(x) proportional to exp(2 integral mu / sigma^2) / sigma^2`. It is the +zero-flux solution: the drift's tendency to push probability one way +exactly balances diffusion's tendency to spread it, at every point rather +than on average. Returned normalised over the grid. + +Panics: +Panics unless the range is increasing and `n` is at least two. + +Rust: `stochastic::sde::stationary_density_1d` + """ + ... + +def kramers_escape_rate(barrier_height: float, temp: float, omega_well: float, omega_barrier: float, gamma: float) -> float: + """ +Kramers' escape rate from a potential well over a barrier. + +`(omega_well omega_barrier / (2 pi gamma)) exp(-barrier / temp)` in the +high-friction limit. The exponential is Arrhenius and is the part everyone +knows; Kramers' contribution was the prefactor, which says the rate falls +as friction rises, because a strongly damped particle takes longer to +diffuse across the barrier top even once it has the energy. + +Panics: +Panics unless the temperature, friction and both frequencies are +positive. + +Rust: `stochastic::sde::kramers_escape_rate` + """ + ... + +def stochastic_resonance_sim(x0: float, amplitude: float, frequency: float, temp: float, n: int, dt: float, rng: Rng) -> list[float]: + """ +Simulates a bistable system driven by a weak periodic force and noise, and +returns the path. + +Stochastic resonance is the phenomenon that adding noise can *improve* the +response to a signal too weak to drive the system on its own: the noise +supplies the energy to cross the barrier, and the signal decides when. The +effect is largest at an intermediate noise level, which is what a sweep +over `temp` shows. + +Panics: +Panics unless `dt` is positive and `temp` is non-negative. + +Rust: `stochastic::sde::stochastic_resonance_sim` + """ + ... diff --git a/bindings/python/python/numeria/stochastic/timeseries.pyi b/bindings/python/python/numeria/stochastic/timeseries.pyi new file mode 100644 index 0000000..ac48ec2 --- /dev/null +++ b/bindings/python/python/numeria/stochastic/timeseries.pyi @@ -0,0 +1,657 @@ +""" +Time series analysis: correlation structure, stationarity, ARMA models, smoothing, volatility, and change detection. A time series differs from a sample only in that the order matters, and every tool here is a way of asking how much it matters. The autocorrelation function measures it directly; the partial autocorrelation strips out what is already explained by the lags in between; the spectral density says the same thing in the frequency domain. An ARMA model is a compact parameterisation of that structure, and its impulse-response weights are the bridge between the two views -- they generate the autocovariances, the forecast error variances, and the spectral density alike. Stationarity is the assumption the whole apparatus rests on, so it is tested rather than assumed. The augmented Dickey-Fuller test takes a unit root as the null and looks for evidence against it; the KPSS test takes stationarity as the null and looks for evidence against *that*. They are deliberately opposed: agreeing on a rejection is much stronger evidence than either alone, and disagreement is a signal that the series is neither cleanly one nor the other. The p-values for both come from tabulated quantiles of their non-standard null distributions, interpolated. Neither statistic is asymptotically normal or chi-squared -- a Dickey-Fuller `t`-ratio is not a `t` at all -- so a p-value computed from a standard distribution would be wrong rather than approximate. The tables are documented where they are used. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.linalg.matrix import Matrix +from numeria.monte_carlo import Rng +from numeria.statistics.inference import TestResult + +class Arima: + """ +An ARIMA model: an `Arma` fitted to the `d`-th difference. + +Rust: `stochastic::timeseries::Arima` + """ + @staticmethod + def fit(x: list[float], p: int, d: int, q: int) -> Arima: ... + def forecast(self, h: int) -> tuple[list[float], list[float]]: ... + @property + def d(self) -> int: ... + @property + def arma(self) -> Arma: ... + @property + def initial(self) -> list[float]: ... + +class Arma: + """ +An autoregressive moving-average model. + +The process is written around its mean: +`(x_t - mu) = sum_i phi_i (x_{t-i} - mu) + e_t + sum_j theta_j e_{t-j}`, +with `e_t` white noise of variance `sigma2`. The sign convention on the +moving-average side is the additive one, matching the Box-Jenkins form. + +Rust: `stochastic::timeseries::Arma` + """ + def __init__(self, ar: list[float], ma: list[float], sigma2: float, mean: float) -> None: ... + def p(self) -> int: ... + def q(self) -> int: ... + def residuals(self, x: list[float]) -> list[float]: ... + @staticmethod + def fit_css(x: list[float], p: int, q: int) -> Arma: ... + @staticmethod + def fit_hannan_rissanen(x: list[float], p: int, q: int) -> Arma: ... + def simulate(self, n: int, rng: Rng) -> list[float]: ... + def impulse_response(self, n: int) -> list[float]: ... + def spectral_density(self, freqs: list[float]) -> list[float]: ... + def roots_check(self) -> tuple[bool, bool]: ... + def log_likelihood(self, x: list[float]) -> float: ... + def n_params(self) -> int: ... + def aic(self, x: list[float]) -> float: ... + def bic(self, x: list[float]) -> float: ... + def forecast(self, x: list[float], h: int) -> tuple[list[float], list[float]]: ... + @property + def ar(self) -> list[float]: ... + @property + def ma(self) -> list[float]: ... + @property + def sigma2(self) -> float: ... + @property + def mean(self) -> float: ... + +class Garch11: + """ +A GARCH(1,1) volatility model: +`sigma_t^2 = omega + alpha r_{t-1}^2 + beta sigma_{t-1}^2`. + +The single most used model in the family, because two parameters are +enough to reproduce the two features that matter: volatility clusters, and +it mean-reverts. `alpha + beta` is the persistence, and the model is +stationary only while that sum is below one. + +Rust: `stochastic::timeseries::Garch11` + """ + def __init__(self, omega: float, alpha: float, beta: float) -> None: ... + def persistence(self) -> float: ... + def unconditional_variance(self) -> float: ... + def conditional_variance(self, returns: list[float]) -> list[float]: ... + @staticmethod + def fit(returns: list[float]) -> Garch11: ... + def simulate(self, n: int, rng: Rng) -> list[float]: ... + def forecast_variance(self, returns: list[float], h: int) -> list[float]: ... + @property + def omega(self) -> float: ... + @property + def alpha(self) -> float: ... + @property + def beta(self) -> float: ... + +class HwState: + """ +The smoothing state left behind by `holt_winters`, enough to continue +the recursion or to forecast forward. + +Rust: `stochastic::timeseries::HwState` + """ + def __init__(self, level: float, trend: float, seasonal: list[float], multiplicative: bool) -> None: ... + def forecast(self, h: int) -> list[float]: ... + @property + def level(self) -> float: ... + @property + def trend(self) -> float: ... + @property + def seasonal(self) -> list[float]: ... + @property + def multiplicative(self) -> bool: ... + +class Sarima: + """ +A seasonal ARIMA model, `(p, d, q) x (P, D, Q)_s`. + +Fitted by applying the seasonal difference `D` times and the ordinary +difference `d` times, then estimating the non-seasonal and seasonal +polynomials on the doubly differenced series. The seasonal part is modelled +as an ARMA in lags that are multiples of `s`. + +Rust: `stochastic::timeseries::Sarima` + """ + @staticmethod + def fit(x: list[float], p: int, d: int, q: int, seasonal_p: int, seasonal_d: int, seasonal_q: int, s: int) -> Sarima: ... + def forecast(self, h: int) -> list[float]: ... + @property + def d(self) -> int: ... + @property + def seasonal_d(self) -> int: ... + @property + def s(self) -> int: ... + @property + def arma(self) -> Arma: ... + +class Var: + """ +A vector autoregression: each series regressed on `p` lags of every series. + +Rust: `stochastic::timeseries::Var` + """ + def k(self) -> int: ... + def p(self) -> int: ... + @staticmethod + def fit(data: list[list[float]], p: int) -> Var: ... + def forecast(self, data: list[list[float]], h: int) -> list[list[float]]: ... + def impulse_response(self, h: int) -> list[Matrix]: ... + def granger_matrix(self, data: list[list[float]]) -> Matrix: ... + @property + def coeffs(self) -> list[Matrix]: ... + @property + def intercept(self) -> list[float]: ... + +def acf(x: list[float], max_lag: int) -> list[float]: + """ +Sample autocorrelation at lags `0..=max_lag`. + +Uses the divide-by-`n` estimator rather than dividing each lag by its own +count. That biases individual lags toward zero, but it is the choice that +makes the resulting sequence positive semi-definite, which is what lets +`pacf` and the Yule-Walker equations be solved at all. The +divide-by-`n-k` version can produce a sequence no stationary process +possesses, and Durbin-Levinson then divides by a negative variance. + +Element 0 is 1 by construction. + +Panics: +Panics unless the series has at least two points and `max_lag < n`. + +Rust: `stochastic::timeseries::acf` + """ + ... + +def pacf(x: list[float], max_lag: int) -> list[float]: + """ +Sample partial autocorrelation at lags `0..=max_lag`, by the +Durbin-Levinson recursion. + +The partial autocorrelation at lag `k` is the correlation between `x_t` +and `x_{t-k}` once the intervening lags are projected out -- equivalently, +the last coefficient of the best linear predictor of order `k`. For an +AR(p) process it is exactly zero beyond lag `p`, which is what makes it +the tool for choosing `p`. + +Element 0 is 1, matching `acf`. + +Panics: +Panics under the same conditions as `acf`. + +Rust: `stochastic::timeseries::pacf` + """ + ... + +def cross_correlation_lags(x: list[float], y: list[float], max_lag: int) -> list[float]: + """ +Cross-correlation of `x` and `y` at lags `-max_lag..=max_lag`. + +Element `max_lag + k` is the correlation between `x_t` and `y_{t+k}`, so a +peak at positive `k` means `x` leads `y` by `k` steps. + +Panics: +Panics unless both series have the same length, at least two points, and +`max_lag < n`. + +Rust: `stochastic::timeseries::cross_correlation_lags` + """ + ... + +def ljung_box(x: list[float], lags: int) -> TestResult: + """ +The Ljung-Box portmanteau test for autocorrelation up to lag `lags`. + +`Q = n(n+2) sum_{k=1}^{h} r_k^2 / (n-k)`, which is asymptotically +chi-squared on `h` degrees of freedom under the null that the series is +uncorrelated. A small p-value says the series has structure a white-noise +model would not produce. + +Panics: +Panics unless `lags >= 1` and `lags < n`. + +Rust: `stochastic::timeseries::ljung_box` + """ + ... + +def difference(x: list[float], d: int) -> list[float]: + """ +The `d`-th successive difference of `x`, shortening it by `d`. + +Panics: +Panics if `d >= x.len()`. + +Rust: `stochastic::timeseries::difference` + """ + ... + +def seasonal_difference(x: list[float], s: int) -> list[float]: + """ +The seasonal difference `x_t - x_{t-s}`, shortening the series by `s`. + +Panics: +Panics unless `1 <= s < x.len()`. + +Rust: `stochastic::timeseries::seasonal_difference` + """ + ... + +def undifference(diffed: list[float], initial: list[float]) -> list[float]: + """ +Rebuilds a series from its differences. + +`initial` holds the first element of each successive difference, lowest +order first: `initial[j]` is `difference(x, j)[0]`, so `initial[0]` is +`x[0]`. Its length sets the differencing order being undone. Exactly +inverts `difference`. + +Panics: +Panics if `initial` is empty. + +Rust: `stochastic::timeseries::undifference` + """ + ... + +def adf_test(x: list[float], lags: int) -> TestResult: + """ +The augmented Dickey-Fuller test for a unit root, with a constant and no +trend. + +Regresses `dy_t` on `y_{t-1}`, a constant, and `lags` lagged differences; +the statistic is the `t`-ratio on the `y_{t-1}` coefficient. The null is +that a unit root is present, so a *small* p-value is evidence the series +is stationary. `df` reports the residual degrees of freedom. + +The p-value is interpolated from the module's table of Dickey-Fuller +quantiles; see the note on that table for why a `t` distribution would be +the wrong reference. + +Errors: +Returns an error if the series is too short for the requested lag order or +the regression is rank deficient. + +Rust: `stochastic::timeseries::adf_test` + """ + ... + +def kpss_test(x: list[float]) -> TestResult: + """ +The KPSS test for level stationarity. + +The statistic is `sum_t S_t^2 / (n^2 s^2(l))`, where `S_t` is the partial +sum of deviations from the mean and `s^2(l)` is a Newey-West long-run +variance with the usual `l = floor(4 (n/100)^{1/4})` bandwidth. Here +stationarity is the *null*, so a small p-value is evidence against it -- +the opposite polarity to `adf_test`, which is the point of running both. + +`df` is reported as the bandwidth actually used. + +Errors: +Returns an error for a series shorter than four points or one with no +variation at all. + +Rust: `stochastic::timeseries::kpss_test` + """ + ... + +def auto_arima(x: list[float], max_p: int, max_d: int, max_q: int) -> Arima: + """ +Selects `(p, d, q)` by minimising AIC over a grid, choosing `d` by +differencing until an augmented Dickey-Fuller test rejects a unit root. + +Differencing order is settled first and separately, because AIC cannot +compare across it: differencing changes the data the likelihood is +computed on, so the numbers are not on the same scale. + +Errors: +Returns an error if no candidate model in the grid can be fitted. + +Rust: `stochastic::timeseries::auto_arima` + """ + ... + +def exponential_smoothing(x: list[float], alpha: float) -> list[float]: + """ +Simple exponential smoothing: `s_t = alpha x_t + (1 - alpha) s_{t-1}`, +seeded at `x_0`. + +The smoothed value is a geometrically weighted average of the whole past, +and the weights sum to one, so a constant series is reproduced exactly at +any `alpha`. + +Panics: +Panics unless `x` is non-empty and `alpha` is in `[0, 1]`. + +Rust: `stochastic::timeseries::exponential_smoothing` + """ + ... + +def double_exponential(x: list[float], alpha: float, beta: float) -> list[float]: + """ +Holt's linear method: a smoothed level and a smoothed slope. + +Element `t` of the result is the one-step-ahead prediction of `x[t]`, made +from the state after seeing `x[t-1]` -- the same convention as +`holt_winters`. Unlike simple smoothing this tracks a linear trend +without lagging behind it. + +The state is seeded one step *before* the data: the slope from the first +two points, and a level back-extrapolated so that `level + trend` equals +`x[0]`. Seeding the level at `x[0]` itself, as is often done, puts the +state half a step ahead of where the recursion expects it and leaves a +transient that takes tens of observations to decay -- on an exact straight +line, which the method should reproduce perfectly from the first step. + +Panics: +Panics unless `x` has at least two points and both parameters lie in +`[0, 1]`. + +Rust: `stochastic::timeseries::double_exponential` + """ + ... + +def holt_winters(x: list[float], alpha: float, beta: float, gamma: float, season_len: int, multiplicative: bool) -> tuple[list[float], HwState]: + """ +Holt-Winters triple exponential smoothing. + +Tracks a level, a slope, and a set of seasonal factors, each updated by +its own smoothing constant. Returns the one-step-ahead fitted values +alongside the final state. + +The seasonal factors are initialised from the first complete season and, +in the additive case, centred so they sum to zero -- otherwise the level +and the seasonal component are not separately identified and the pair can +drift apart while their sum stays right. + +Panics: +Panics unless the series covers at least two full seasons, `season_len` is +at least 2, and all three parameters lie in `[0, 1]`. + +Rust: `stochastic::timeseries::holt_winters` + """ + ... + +def holt_winters_optimize(x: list[float], season_len: int) -> tuple[float, float, float]: + """ +Chooses `(alpha, beta, gamma)` by minimising the one-step-ahead sum of +squared errors over a coarse grid followed by a local refinement. + +A grid rather than a gradient method: the Holt-Winters error surface is +not convex in the three constants and has flat regions near the corners of +the unit cube, where a local method started badly will simply stop. + +Panics: +Panics under the same conditions as `holt_winters`. + +Rust: `stochastic::timeseries::holt_winters_optimize` + """ + ... + +def ewma_variance(returns: list[float], lambda_: float) -> list[float]: + """ +The RiskMetrics exponentially weighted variance, +`v_t = lambda v_{t-1} + (1 - lambda) r_{t-1}^2`. + +A GARCH(1,1) with `omega = 0` and unit persistence: no mean reversion, so +the variance wanders rather than settling. + +Panics: +Panics unless `returns` is non-empty and `lambda` lies in `[0, 1)`. + +Rust: `stochastic::timeseries::ewma_variance` + """ + ... + +def arch_lm_test(returns: list[float], lags: int) -> TestResult: + """ +Engle's ARCH LM test for conditional heteroskedasticity. + +Regresses squared returns on their own lags; the statistic `n R^2` is +asymptotically chi-squared on `lags` degrees of freedom under the null of +no ARCH effect. A small p-value says the size of a return predicts the +size of the next one, which is precisely what a GARCH model is for. + +Errors: +Returns an error if the series is too short or the regression is +degenerate. + +Rust: `stochastic::timeseries::arch_lm_test` + """ + ... + +def granger_causality(x: list[float], y: list[float], lags: int) -> TestResult: + """ +Tests whether `x` Granger-causes `y`: whether past `x` improves a forecast +of `y` that already uses past `y`. + +An `F` test of the restricted regression of `y` on its own lags against +the unrestricted one that adds the lags of `x`. The name is a term of art +-- it is predictive precedence, not causation, and a common driver of both +series will produce it. + +Errors: +Returns an error if the series differ in length, are too short, or either +regression is degenerate. + +Rust: `stochastic::timeseries::granger_causality` + """ + ... + +def cointegration_engle_granger(x: list[float], y: list[float]) -> TestResult: + """ +The Engle-Granger two-step test for cointegration between `x` and `y`. + +Regresses `y` on `x` with an intercept, then tests the residual for a unit +root. Rejecting means some linear combination of two individually +non-stationary series is stationary -- they share a stochastic trend. + +The p-value comes from the module's Engle-Granger table rather than its +plain Dickey-Fuller one: the residual is fitted rather than observed, and the +regression has already worked to make it look stationary, so the null +distribution sits further left. Using the ordinary table here is a common +way to find cointegration that is not there. + +Errors: +Returns an error if the series differ in length, are too short, or the +first-stage regression is degenerate. + +Rust: `stochastic::timeseries::cointegration_engle_granger` + """ + ... + +def seasonal_decompose_stl_lite(x: list[float], period: int) -> tuple[list[float], list[float], list[float]]: + """ +Additive seasonal decomposition into `(trend, seasonal, residual)`. + +The trend is a centred moving average over one full period; the seasonal +component is the average detrended value at each phase, centred to sum to +zero; the residual is whatever is left. Near the ends, where the moving +average has no window, the trend is held at the nearest value it does +have -- so the three components add back to the input exactly at every +index, which is the property that makes the decomposition usable rather +than merely indicative. + +Panics: +Panics unless `period >= 2` and the series covers at least two periods. + +Rust: `stochastic::timeseries::seasonal_decompose_stl_lite` + """ + ... + +def changepoint_pelt(x: list[float], penalty: float) -> list[int]: + """ +Change-in-mean detection by PELT (pruned exact linear time). + +Finds the segmentation minimising the total within-segment sum of squares +plus `penalty` per changepoint. Unlike binary segmentation this is exact: +dynamic programming considers every segmentation, and the pruning step +discards only candidates that provably cannot start an optimal segment, +so the answer is the global optimum rather than a greedy approximation. + +Returns the interior changepoint indices, each the first index of a new +segment, in increasing order. + +Panics: +Panics if `penalty` is negative. + +Rust: `stochastic::timeseries::changepoint_pelt` + """ + ... + +def changepoint_binary_segmentation(x: list[float], max_k: int) -> list[int]: + """ +Change-in-mean detection by recursive binary segmentation. + +Splits at the point giving the largest reduction in sum of squares, then +recurses into both halves, stopping at `max_k` changepoints. Greedy rather +than exact -- it can miss a pair of changes whose individual effects +cancel -- but it is fast and needs no penalty to be chosen. + +Returns changepoint indices in increasing order. + +Rust: `stochastic::timeseries::changepoint_binary_segmentation` + """ + ... + +def cusum(x: list[float], target: float, k: float) -> tuple[list[float], list[float]]: + """ +Two-sided cumulative sum control statistics, `(upper, lower)`. + +`S+_t = max(0, S+_{t-1} + (x_t - target) - k)` and the mirror image for +the lower arm. The slack `k` is what stops the statistic drifting on +ordinary noise: with `k` set to half the shift worth detecting, the +statistic stays near zero while the process is on target and climbs +roughly linearly once it is not. + +Panics: +Panics if `k` is negative. + +Rust: `stochastic::timeseries::cusum` + """ + ... + +def matrix_profile_lite(x: list[float], m: int) -> tuple[list[float], list[int]]: + """ +The matrix profile of `x` for subsequences of length `m`: +`(distance to the nearest other subsequence, its index)`. + +Distances are z-normalised Euclidean, so a match is about shape rather +than level or amplitude. Overlapping neighbours are excluded -- a +subsequence's closest match is always the one shifted by one sample, which +says nothing -- using the usual exclusion zone of half the window. + +The smallest entries locate the repeated motifs; the largest locates the +discord, the least-like-anything-else stretch. + +Panics: +Panics unless `m >= 2` and the series holds at least two non-overlapping +windows. + +Rust: `stochastic::timeseries::matrix_profile_lite` + """ + ... + +def sample_entropy(x: list[float], m: int, tol: float) -> float: + """ +Sample entropy: the negative log probability that two sequences matching +for `m` points go on matching for `m + 1`. + +`tol` is given as a multiple of the series standard deviation. Unlike +`approximate_entropy` the self-match is excluded, which removes the bias +that otherwise makes a short series look more regular than it is. + +Returns infinity when no `m+1`-length match occurs at all, which is the +honest answer -- the estimator has run out of data rather than found zero +probability. + +Panics: +Panics unless `m >= 1`, `tol > 0`, and the series holds at least `m + 2` +points. + +Rust: `stochastic::timeseries::sample_entropy` + """ + ... + +def approximate_entropy(x: list[float], m: int, tol: float) -> float: + """ +Approximate entropy, the older cousin of `sample_entropy`. + +Includes the self-match, which guarantees the logarithm is defined but +biases the estimate toward regularity, the more so the shorter the series. +Kept because it is what a great deal of published work reports. + +Panics: +Panics under the same conditions as `sample_entropy`. + +Rust: `stochastic::timeseries::approximate_entropy` + """ + ... + +def permutation_entropy(x: list[float], order: int, delay: int) -> float: + """ +Permutation entropy: the Shannon entropy of the ordinal patterns of length +`order` sampled at spacing `delay`, normalised to `[0, 1]`. + +Only the ranking within each window matters, so the measure is invariant +to any monotone transformation of the series and needs no tolerance +parameter. A monotone series visits one pattern and scores 0; independent +noise visits all `order!` patterns equally and scores 1. + +Panics: +Panics unless `order` is between 2 and 8, `delay >= 1`, and the series is +long enough to hold at least two windows. + +Rust: `stochastic::timeseries::permutation_entropy` + """ + ... + +def surrogate_test_iaaft(x: list[float], statistic: Callable[[list[float]], float], n_surrogates: int, rng: Rng) -> float: + """ +A surrogate-data test: how extreme `statistic(x)` is against the +distribution it takes on IAAFT surrogates of `x`. + +The surrogates share the series' amplitude distribution and power +spectrum, hence all of its linear structure. Rejecting therefore points at +something a linear Gaussian process could not produce -- nonlinearity -- +rather than merely at "not white noise", which is what a test against +shuffled data would show. + +Returns the two-sided rank p-value `(1 + #{|s_i - mean| >= |s_x - mean|}) / +(1 + n)`, which is exact for finite `n` rather than asymptotic. + +Panics: +Panics if `n_surrogates` is zero or the series is shorter than four points. + +Rust: `stochastic::timeseries::surrogate_test_iaaft` + """ + ... + +def state_space_local_level(x: list[float]) -> tuple[list[float], float, float]: + """ +The local level model: `x_t = mu_t + e_t`, `mu_t = mu_{t-1} + n_t`. + +Returns `(smoothed level, signal variance, observation variance)`. The two +variances are estimated by maximising the Gaussian likelihood from the +Kalman filter; only their ratio -- the signal-to-noise ratio, or hyper- +parameter `q` -- affects the filtered path, so it is that ratio the +optimiser searches over, with the overall scale then available in closed +form. + +The model is the state-space form of simple exponential smoothing: the +steady-state Kalman gain *is* the smoothing constant, so an estimated `q` +and an estimated `alpha` carry the same information. + +Errors: +Returns an error for a series shorter than five points or with no +variation. + +Rust: `stochastic::timeseries::state_space_local_level` + """ + ... diff --git a/bindings/python/python/numeria/thermodynamics.pyi b/bindings/python/python/numeria/thermodynamics.pyi new file mode 100644 index 0000000..396ee5d --- /dev/null +++ b/bindings/python/python/numeria/thermodynamics.pyi @@ -0,0 +1,428 @@ +""" +Thermodynamics: gases, heat transfer, cycles and phase change. The ideal gas law in each of its four solved forms, and the kinetic picture behind it -- average kinetic energy, RMS speed, mean free path. Work and entropy change along isothermal, isobaric and adiabatic paths. Heat transfer by all three mechanisms: Fourier conduction (with an explicit 1-D stepper and its stability limit), Newton's law of cooling and convection, and radiative exchange. The dimensionless groups that classify convection -- Grashof, Rayleigh, Prandtl, Nusselt, Biot -- are here too. Cycles through the Carnot efficiency and the coefficients of performance for refrigerators and heat pumps; phase change through latent heat, Clausius-Clapeyron, boiling-point elevation, freezing-point depression, and wet-steam quality. Temperature scale conversions round it out. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def ideal_gas_pressure(moles: float, temperature: float, volume: float) -> float: + """ +Ideal gas law: PV = nRT. Solve for pressure: P = nRT / V + +Rust: `thermodynamics::ideal_gas_pressure` + """ + ... + +def ideal_gas_volume(moles: float, temperature: float, pressure: float) -> float: + """ +Solve for volume: V = nRT / P + +Rust: `thermodynamics::ideal_gas_volume` + """ + ... + +def ideal_gas_temperature(pressure: float, volume: float, moles: float) -> float: + """ +Solve for temperature: T = PV / (nR) + +Rust: `thermodynamics::ideal_gas_temperature` + """ + ... + +def ideal_gas_moles(pressure: float, volume: float, temperature: float) -> float: + """ +Number of moles: n = PV / (RT) + +Rust: `thermodynamics::ideal_gas_moles` + """ + ... + +def average_kinetic_energy(temperature: float) -> float: + """ +Average kinetic energy of a gas molecule: KE = (3/2) * k_B * T + +Rust: `thermodynamics::average_kinetic_energy` + """ + ... + +def rms_speed(temperature: float, molecular_mass: float) -> float: + """ +RMS speed of gas molecules: v_rms = sqrt(3 * k_B * T / m) + +Rust: `thermodynamics::rms_speed` + """ + ... + +def mean_free_path(molecular_diameter: float, number_density: float) -> float: + """ +Mean free path: λ = 1 / (√2 * π * d^2 * n/V) + +Rust: `thermodynamics::mean_free_path` + """ + ... + +def heat_transfer(mass: float, specific_heat: float, delta_temp: float) -> float: + """ +Heat transfer: Q = m * c * ΔT + +Rust: `thermodynamics::heat_transfer` + """ + ... + +def heat_conduction_rate(conductivity: float, area: float, delta_temp: float, thickness: float) -> float: + """ +Heat conduction (Fourier's law): Q/t = k * A * ΔT / d + +Rust: `thermodynamics::heat_conduction_rate` + """ + ... + +def heat_radiation_power(emissivity: float, area: float, temperature: float) -> float: + """ +Heat radiation (Stefan-Boltzmann law): P = ε * σ * A * T^4 + +Rust: `thermodynamics::heat_radiation_power` + """ + ... + +def net_radiation_power(emissivity: float, area: float, t_hot: float, t_cold: float) -> float: + """ +Net radiative heat transfer: P = ε * σ * A * (T_hot^4 - T_cold^4) + +Rust: `thermodynamics::net_radiation_power` + """ + ... + +def newton_cooling(t_initial: float, t_environment: float, cooling_constant: float, time: float) -> float: + """ +Newton's law of cooling: dT/dt = -k * (T - T_env) +Returns temperature at time t: T(t) = T_env + (T0 - T_env) * e^(-k*t) + +Rust: `thermodynamics::newton_cooling` + """ + ... + +def work_isothermal(moles: float, temperature: float, v1: float, v2: float) -> float: + """ +Work done by an ideal gas during isothermal expansion: W = nRT * ln(V2/V1) + +Rust: `thermodynamics::work_isothermal` + """ + ... + +def work_isobaric(pressure: float, delta_v: float) -> float: + """ +Work done during isobaric (constant pressure) process: W = P * ΔV + +Rust: `thermodynamics::work_isobaric` + """ + ... + +def work_adiabatic(p1: float, v1: float, p2: float, v2: float, gamma: float) -> float: + """ +Work done during adiabatic process: W = (P1*V1 - P2*V2) / (γ - 1) + +Rust: `thermodynamics::work_adiabatic` + """ + ... + +def adiabatic_final_pressure(p1: float, v1: float, v2: float, gamma: float) -> float: + """ +Adiabatic relation: P1 * V1^γ = P2 * V2^γ → P2 = P1 * (V1/V2)^γ + +Rust: `thermodynamics::adiabatic_final_pressure` + """ + ... + +def entropy_change_isothermal(heat: float, temperature: float) -> float: + """ +Entropy change for heat transfer at constant temperature: ΔS = Q / T + +Rust: `thermodynamics::entropy_change_isothermal` + """ + ... + +def entropy_change_ideal_gas(moles: float, cv: float, t1: float, t2: float, v1: float, v2: float) -> float: + """ +Entropy change for an ideal gas: ΔS = n*Cv*ln(T2/T1) + n*R*ln(V2/V1) + +Rust: `thermodynamics::entropy_change_ideal_gas` + """ + ... + +def carnot_efficiency(t_cold: float, t_hot: float) -> float: + """ +Carnot efficiency: η = 1 - T_cold / T_hot + +Rust: `thermodynamics::carnot_efficiency` + """ + ... + +def thermal_efficiency(work: float, heat_input: float) -> float: + """ +Thermal efficiency: η = W / Q_hot + +Rust: `thermodynamics::thermal_efficiency` + """ + ... + +def cop_refrigerator(heat_removed: float, work: float) -> float: + """ +Coefficient of performance (refrigerator): COP = Q_cold / W + +Rust: `thermodynamics::cop_refrigerator` + """ + ... + +def cop_heat_pump(heat_delivered: float, work: float) -> float: + """ +Coefficient of performance (heat pump): COP = Q_hot / W + +Rust: `thermodynamics::cop_heat_pump` + """ + ... + +def latent_heat(mass: float, specific_latent_heat: float) -> float: + """ +Heat for phase change: Q = m * L (latent heat) + +Rust: `thermodynamics::latent_heat` + """ + ... + +def clausius_clapeyron(p1: float, t1: float, t2: float, molar_latent_heat: float) -> float: + """ +Clausius-Clapeyron (approximate): ln(P2/P1) = (L/R) * (1/T1 - 1/T2) +Returns P2 given P1, T1, T2, and molar latent heat L. + +Rust: `thermodynamics::clausius_clapeyron` + """ + ... + +def convective_heat_rate(h: float, area: float, delta_temp: float) -> float: + """ +Newton's law of convection: Q/t = h×A×ΔT + +Rust: `thermodynamics::convective_heat_rate` + """ + ... + +def thermal_diffusivity(conductivity: float, density: float, specific_heat: float) -> float: + """ +Thermal diffusivity: α = k/(ρ×cₚ) + +Rust: `thermodynamics::thermal_diffusivity` + """ + ... + +def grashof_number(g: float, beta: float, delta_temp: float, length: float, kinematic_viscosity: float) -> float: + """ +Grashof number: Gr = gβΔTL³/ν² + +Rust: `thermodynamics::grashof_number` + """ + ... + +def rayleigh_number(grashof: float, prandtl: float) -> float: + """ +Rayleigh number: Ra = Gr × Pr + +Rust: `thermodynamics::rayleigh_number` + """ + ... + +def prandtl_number(kinematic_viscosity: float, thermal_diffusivity: float) -> float: + """ +Prandtl number: Pr = ν/α + +Rust: `thermodynamics::prandtl_number` + """ + ... + +def nusselt_number(h: float, length: float, conductivity: float) -> float: + """ +Nusselt number: Nu = hL/k + +Rust: `thermodynamics::nusselt_number` + """ + ... + +def biot_number(h: float, length: float, conductivity: float) -> float: + """ +Biot number: Bi = hL/k (external convection vs internal conduction) + +Rust: `thermodynamics::biot_number` + """ + ... + +def heat_equation_step_1d(temperatures: MutableSequence[float], dx: float, dt: float, diffusivity: float) -> None: + """ +Explicit finite difference: T_i^(n+1) = T_i^n + α×dt/dx² × (T_(i+1) - 2T_i + T_(i-1)) +Fixed boundary conditions (first and last elements unchanged). + +Rust: `thermodynamics::heat_equation_step_1d` + """ + ... + +def heat_equation_stability(dx: float, diffusivity: float) -> float: + """ +Maximum stable time step for explicit finite difference: dt_max = dx²/(2α) + +Rust: `thermodynamics::heat_equation_stability` + """ + ... + +def wien_displacement(temperature: float) -> float: + """ +Wien's displacement law: λ_max = b/T where b = 2.898e-3 m·K + +Rust: `thermodynamics::wien_displacement` + """ + ... + +def spectral_exitance(wavelength: float, temperature: float) -> float: + """ +Planck's law: M = (2πhc²/λ⁵) × 1/(e^(hc/λkT) - 1) + +Rust: `thermodynamics::spectral_exitance` + """ + ... + +def radiative_equilibrium_temperature(luminosity: float, distance: float, albedo: float) -> float: + """ +Radiative equilibrium temperature: T = ((L(1-a))/(16πσd²))^(1/4) + +Rust: `thermodynamics::radiative_equilibrium_temperature` + """ + ... + +def celsius_to_kelvin(c: float) -> float: + """ +Celsius to Kelvin: K = C + 273.15 + +Rust: `thermodynamics::celsius_to_kelvin` + """ + ... + +def kelvin_to_celsius(k: float) -> float: + """ +Kelvin to Celsius: C = K - 273.15 + +Rust: `thermodynamics::kelvin_to_celsius` + """ + ... + +def celsius_to_fahrenheit(c: float) -> float: + """ +Celsius to Fahrenheit: F = C × 9/5 + 32 + +Rust: `thermodynamics::celsius_to_fahrenheit` + """ + ... + +def fahrenheit_to_celsius(f: float) -> float: + """ +Fahrenheit to Celsius: C = (F - 32) × 5/9 + +Rust: `thermodynamics::fahrenheit_to_celsius` + """ + ... + +def fahrenheit_to_kelvin(f: float) -> float: + """ +Fahrenheit to Kelvin via Celsius + +Rust: `thermodynamics::fahrenheit_to_kelvin` + """ + ... + +def kelvin_to_fahrenheit(k: float) -> float: + """ +Kelvin to Fahrenheit via Celsius + +Rust: `thermodynamics::kelvin_to_fahrenheit` + """ + ... + +def celsius_to_rankine(c: float) -> float: + """ +Celsius to Rankine: R = (C + 273.15) × 9/5 + +Rust: `thermodynamics::celsius_to_rankine` + """ + ... + +def rankine_to_celsius(r: float) -> float: + """ +Rankine to Celsius: C = R × 5/9 - 273.15 + +Rust: `thermodynamics::rankine_to_celsius` + """ + ... + +def boiling_point_elevation(kb: float, molality: float) -> float: + """ +Boiling point elevation: ΔTb = Kb × m + +Rust: `thermodynamics::boiling_point_elevation` + """ + ... + +def freezing_point_depression(kf: float, molality: float) -> float: + """ +Freezing point depression: ΔTf = Kf × m + +Rust: `thermodynamics::freezing_point_depression` + """ + ... + +def saturation_pressure(t: float, a: float, b: float, c: float) -> float: + """ +Antoine equation: log10(P) = A - B/(C+T), returns P + +Rust: `thermodynamics::saturation_pressure` + """ + ... + +def heat_of_vaporization_trouton(boiling_point_k: float) -> float: + """ +Trouton's rule: ΔHvap ≈ 88 × Tb (J/mol) + +Rust: `thermodynamics::heat_of_vaporization_trouton` + """ + ... + +def superheat_degree(actual_temp: float, saturation_temp: float) -> float: + """ +Degree of superheat: ΔT = T_actual - T_sat + +Rust: `thermodynamics::superheat_degree` + """ + ... + +def subcool_degree(saturation_temp: float, actual_temp: float) -> float: + """ +Degree of subcooling: ΔT = T_sat - T_actual + +Rust: `thermodynamics::subcool_degree` + """ + ... + +def quality(mass_vapor: float, mass_total: float) -> float: + """ +Steam quality (dryness fraction): x = m_vapor / m_total + +Rust: `thermodynamics::quality` + """ + ... + +def specific_enthalpy_wet(hf: float, hfg: float, quality: float) -> float: + """ +Specific enthalpy of wet steam: h = hf + x × hfg + +Rust: `thermodynamics::specific_enthalpy_wet` + """ + ... diff --git a/bindings/python/python/numeria/transforms/__init__.pyi b/bindings/python/python/numeria/transforms/__init__.pyi new file mode 100644 index 0000000..232a921 --- /dev/null +++ b/bindings/python/python/numeria/transforms/__init__.pyi @@ -0,0 +1,124 @@ +""" +Discrete transforms: FFT (any length), DCT/DST, STFT, wavelets, Hilbert, Laplace inversion, Radon, and spectral estimation. The radix-2 FFT that used to live in `signal_processing::fft` moved here; the old paths re-export everything so no caller changes. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from . import dct, fft, hilbert, laplace, radon, spectral, stft, wavelet +from numeria.transforms.dct import Bc as Bc +from numeria.transforms.radon import FbpFilter as FbpFilter +from numeria.transforms.fft import FftPlan as FftPlan +from numeria.transforms.wavelet import Mother as Mother +from numeria.transforms.wavelet import PadMode as PadMode +from numeria.transforms.stft import Stft as Stft +from numeria.transforms.wavelet import Threshold as Threshold +from numeria.transforms.wavelet import Wavelet as Wavelet +from numeria.transforms.radon import abel_transform as abel_transform +from numeria.transforms.hilbert import am_demodulate as am_demodulate +from numeria.transforms.hilbert import analytic_signal as analytic_signal +from numeria.transforms.spectral import ar_psd as ar_psd +from numeria.transforms.spectral import burg_ar as burg_ar +from numeria.transforms.spectral import cepstrum_power as cepstrum_power +from numeria.transforms.spectral import cepstrum_real as cepstrum_real +from numeria.transforms.stft import chirp_z as chirp_z +from numeria.transforms.spectral import coherence as coherence +from numeria.transforms.stft import constant_q_transform as constant_q_transform +from numeria.transforms.spectral import cross_spectral_density as cross_spectral_density +from numeria.transforms.wavelet import cwt as cwt +from numeria.transforms.dct import dct_2d as dct_2d +from numeria.transforms.dct import dct_compress as dct_compress +from numeria.transforms.dct import dct_i as dct_i +from numeria.transforms.dct import dct_ii as dct_ii +from numeria.transforms.dct import dct_iii as dct_iii +from numeria.transforms.dct import dct_iv as dct_iv +from numeria.transforms.dct import dct_poisson_1d as dct_poisson_1d +from numeria.transforms.spectral import detrend as detrend +from numeria.transforms.laplace import digital_freq_response as digital_freq_response +from numeria.transforms.spectral import dpss as dpss +from numeria.transforms.dct import dst_i as dst_i +from numeria.transforms.dct import dst_ii as dst_ii +from numeria.transforms.stft import dtmf_decode as dtmf_decode +from numeria.transforms.wavelet import dwt as dwt +from numeria.transforms.wavelet import dwt_2d as dwt_2d +from numeria.transforms.hilbert import empirical_mode_decomposition as empirical_mode_decomposition +from numeria.transforms.hilbert import envelope as envelope +from numeria.transforms.fft import fft_2d as fft_2d +from numeria.transforms.fft import fft_3d as fft_3d +from numeria.transforms.fft import fft_any as fft_any +from numeria.transforms.fft import fft_convolve as fft_convolve +from numeria.transforms.fft import fft_convolve_2d as fft_convolve_2d +from numeria.transforms.fft import fft_correlate as fft_correlate +from numeria.transforms.fft import fft_differentiate as fft_differentiate +from numeria.transforms.fft import fft_freqs as fft_freqs +from numeria.transforms.fft import fft_integrate as fft_integrate +from numeria.transforms.fft import fft_interpolate as fft_interpolate +from numeria.transforms.fft import fft_poisson_2d as fft_poisson_2d +from numeria.transforms.fft import fft_shift as fft_shift +from numeria.transforms.hilbert import fm_demodulate as fm_demodulate +from numeria.transforms.laplace import fractional_fourier as fractional_fourier +from numeria.transforms.stft import goertzel as goertzel +from numeria.transforms.stft import goertzel_bank as goertzel_bank +from numeria.transforms.radon import hankel_transform as hankel_transform +from numeria.transforms.dct import hartley as hartley +from numeria.transforms.hilbert import hilbert_fir as hilbert_fir +from numeria.transforms.hilbert import hilbert_huang_spectrum as hilbert_huang_spectrum +from numeria.transforms.radon import hough_circles as hough_circles +from numeria.transforms.radon import hough_lines as hough_lines +from numeria.transforms.dct import idct_2d as idct_2d +from numeria.transforms.dct import idct_ii as idct_ii +from numeria.transforms.wavelet import idwt as idwt +from numeria.transforms.wavelet import idwt_2d as idwt_2d +from numeria.transforms.fft import ifft as ifft +from numeria.transforms.fft import ifft_2d as ifft_2d +from numeria.transforms.fft import ifft_3d as ifft_3d +from numeria.transforms.fft import ifft_any as ifft_any +from numeria.transforms.laplace import impulse_response_from_tf as impulse_response_from_tf +from numeria.transforms.hilbert import instantaneous_frequency as instantaneous_frequency +from numeria.transforms.hilbert import instantaneous_phase as instantaneous_phase +from numeria.transforms.radon import inverse_abel as inverse_abel +from numeria.transforms.laplace import inverse_laplace_stehfest as inverse_laplace_stehfest +from numeria.transforms.laplace import inverse_laplace_talbot as inverse_laplace_talbot +from numeria.transforms.radon import inverse_radon_fbp as inverse_radon_fbp +from numeria.transforms.radon import inverse_radon_sart as inverse_radon_sart +from numeria.transforms.fft import irfft as irfft +from numeria.transforms.hilbert import kramers_kronig as kramers_kronig +from numeria.transforms.laplace import laplace_numeric as laplace_numeric +from numeria.transforms.wavelet import lifting_dwt_53 as lifting_dwt_53 +from numeria.transforms.wavelet import lifting_idwt_53 as lifting_idwt_53 +from numeria.transforms.spectral import lomb_scargle as lomb_scargle +from numeria.transforms.stft import mel_filterbank as mel_filterbank +from numeria.transforms.stft import mel_spectrogram as mel_spectrogram +from numeria.transforms.hilbert import minimum_phase_from_magnitude as minimum_phase_from_magnitude +from numeria.transforms.wavelet import multiresolution_analysis as multiresolution_analysis +from numeria.transforms.spectral import multitaper as multitaper +from numeria.transforms.spectral import music as music +from numeria.transforms.fft import next_power_of_two as next_power_of_two +from numeria.transforms.spectral import periodogram as periodogram +from numeria.transforms.spectral import power_law_fit as power_law_fit +from numeria.transforms.stft import reassigned_spectrogram as reassigned_spectrogram +from numeria.transforms.fft import rfft as rfft +from numeria.transforms.fft import rfft_2d as rfft_2d +from numeria.transforms.laplace import s_domain_freq_response as s_domain_freq_response +from numeria.transforms.wavelet import scale_to_frequency as scale_to_frequency +from numeria.transforms.wavelet import scalogram as scalogram +from numeria.transforms.radon import shepp_logan_phantom as shepp_logan_phantom +from numeria.transforms.spectral import spectral_entropy as spectral_entropy +from numeria.transforms.spectral import spectral_flatness as spectral_flatness +from numeria.transforms.stft import spectrogram as spectrogram +from numeria.transforms.hilbert import ssb_modulate as ssb_modulate +from numeria.transforms.spectral import transfer_function_estimate as transfer_function_estimate +from numeria.transforms.wavelet import wavedec as wavedec +from numeria.transforms.wavelet import wavelet_compress as wavelet_compress +from numeria.transforms.wavelet import wavelet_denoise as wavelet_denoise +from numeria.transforms.wavelet import wavelet_filters as wavelet_filters +from numeria.transforms.wavelet import wavelet_packet_decompose as wavelet_packet_decompose +from numeria.transforms.wavelet import waverec as waverec +from numeria.transforms.spectral import welch as welch +from numeria.transforms.spectral import yule_walker_ar as yule_walker_ar +from numeria.transforms.laplace import z_transform_eval as z_transform_eval +from numeria.transforms.stft import zoom_fft as zoom_fft + + diff --git a/bindings/python/python/numeria/transforms/dct.pyi b/bindings/python/python/numeria/transforms/dct.pyi new file mode 100644 index 0000000..26e84f3 --- /dev/null +++ b/bindings/python/python/numeria/transforms/dct.pyi @@ -0,0 +1,129 @@ +""" +Discrete cosine, sine, and Hartley transforms. Conventions match scipy's unnormalized (`norm=None`) definitions: * DCT-I: y\\[k\\] = x\\[0\\] + (−1)^k x\\[N−1\\] + 2 Σ_{n=1}^{N−2} x\\[n\\] cos(πkn/(N−1)) * DCT-II: y\\[k\\] = 2 Σ x\\[n\\] cos(πk(2n+1)/(2N)) * DCT-III: y\\[k\\] = x\\[0\\] + 2 Σ_{n≥1} x\\[n\\] cos(πn(2k+1)/(2N)) * DCT-IV: y\\[k\\] = 2 Σ x\\[n\\] cos(π(2k+1)(2n+1)/(4N)) * DST-I: y\\[k\\] = 2 Σ x\\[n\\] sin(π(k+1)(n+1)/(N+1)) * DST-II: y\\[k\\] = 2 Σ x\\[n\\] sin(π(k+1)(2n+1)/(2N)) Everything runs in O(n log n) through the any-length FFT. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.statistics.distributions import Poisson + +class Bc: + """ +Boundary condition for `dct_poisson_1d`. + +Rust: `transforms::dct::Bc` + """ + ... + +def dct_i(x: list[float]) -> list[float]: + """ +DCT-I of length N ≥ 2 (even symmetry about both endpoints). + +Panics: +Panics if `x.len() < 2`. + +Rust: `transforms::dct::dct_i` + """ + ... + +def dct_ii(x: list[float]) -> list[float]: + """ +DCT-II (the "standard" DCT). + +Rust: `transforms::dct::dct_ii` + """ + ... + +def dct_iii(x: list[float]) -> list[float]: + """ +DCT-III (the unnormalized inverse of DCT-II). + +Rust: `transforms::dct::dct_iii` + """ + ... + +def dct_iv(x: list[float]) -> list[float]: + """ +DCT-IV (its own inverse up to a factor 2N). + +Rust: `transforms::dct::dct_iv` + """ + ... + +def idct_ii(x: list[float]) -> list[float]: + """ +Inverse of `dct_ii`: x = dct_iii(y) / (2N). + +Rust: `transforms::dct::idct_ii` + """ + ... + +def dst_i(x: list[float]) -> list[float]: + """ +DST-I (odd symmetry about both virtual endpoints); its own inverse up +to a factor 2(N+1). + +Rust: `transforms::dct::dst_i` + """ + ... + +def dst_ii(x: list[float]) -> list[float]: + """ +DST-II, via the identity DST-II(x)\\[k\\] = DCT-II(x·(−1)^n)\\[N−1−k\\]. + +Rust: `transforms::dct::dst_ii` + """ + ... + +def dct_2d(x: list[float], w: int, h: int) -> list[float]: + """ +Separable 2D DCT-II of row-major data (index = y·w + x). + +Panics: +Panics unless `x.len() == w * h`. + +Rust: `transforms::dct::dct_2d` + """ + ... + +def idct_2d(x: list[float], w: int, h: int) -> list[float]: + """ +Inverse of `dct_2d`. + +Panics: +Panics unless `x.len() == w * h`. + +Rust: `transforms::dct::idct_2d` + """ + ... + +def hartley(x: list[float]) -> list[float]: + """ +Discrete Hartley transform: H\\[k\\] = Σ x\\[n\\]·cas(2πkn/N) with +cas θ = cos θ + sin θ. Self-inverse up to a factor N. + +Rust: `transforms::dct::hartley` + """ + ... + +def dct_compress(x: list[float], keep_fraction: float) -> list[float]: + """ +Lossy compression demo: keep the largest `keep_fraction` of DCT-II +coefficients (by magnitude), zero the rest, and reconstruct. + +Rust: `transforms::dct::dct_compress` + """ + ... + +def dct_poisson_1d(rhs: list[float], dx: float, bc: Bc) -> list[float]: + """ +Solve the 1D Poisson problem u'' = rhs on a uniform grid with +homogeneous boundary conditions, diagonalizing the discrete +three-point Laplacian with the DST-I (Dirichlet) or DCT-II (Neumann). +The discrete residual is at roundoff. + +Rust: `transforms::dct::dct_poisson_1d` + """ + ... diff --git a/bindings/python/python/numeria/transforms/fft.pyi b/bindings/python/python/numeria/transforms/fft.pyi new file mode 100644 index 0000000..7fe73a9 --- /dev/null +++ b/bindings/python/python/numeria/transforms/fft.pyi @@ -0,0 +1,243 @@ +""" +Fast Fourier transforms. The power-of-two core is the iterative radix-2 Cooley-Tukey from Press et al., *Numerical Recipes*, §12.2. `fft_any` extends it to arbitrary lengths with a recursive mixed-radix 2/3/5 decomposition and a Bluestein chirp-z fallback for lengths with other prime factors. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.statistics.distributions import Poisson + +class FftPlan: + """ +Precomputed twiddle factors and bit-reversal permutation for repeated +power-of-two FFTs of one size. + +Rust: `transforms::fft::FftPlan` + """ + def __init__(self, n: int) -> None: ... + def len(self) -> int: ... + def is_empty(self) -> bool: ... + def execute(self, x: MutableSequence[complex]) -> None: ... + def execute_inverse(self, x: MutableSequence[complex]) -> None: ... + +def next_power_of_two(n: int) -> int: + """ +Smallest power of two ≥ n (returns 1 for n = 0). + +Rust: `transforms::fft::next_power_of_two` + """ + ... + +def fft(input: list[complex]) -> list[complex]: + """ +Forward FFT: `X[k] = Σ x[n]·e^(−j2πkn/N)`. + +Panics: +Panics unless `input.len()` is a power of two. Use `fft_any` for +arbitrary lengths. + +Rust: `transforms::fft::fft` + """ + ... + +def ifft(input: list[complex]) -> list[complex]: + """ +Inverse FFT: `x[n] = (1/N)·Σ X[k]·e^(j2πkn/N)`. + +Panics: +Panics unless `input.len()` is a power of two. Use `ifft_any` for +arbitrary lengths. + +Rust: `transforms::fft::ifft` + """ + ... + +def fft_any(x: list[complex]) -> list[complex]: + """ +Forward DFT of any length: mixed radix 2/3/5 with a Bluestein +fallback for lengths containing other prime factors. O(n log n). + +Rust: `transforms::fft::fft_any` + """ + ... + +def ifft_any(x: list[complex]) -> list[complex]: + """ +Inverse DFT of any length (includes the 1/n scaling). + +Rust: `transforms::fft::ifft_any` + """ + ... + +def rfft(input: list[float]) -> list[complex]: + """ +FFT of a real signal, returning the n/2 + 1 non-redundant bins +(bins `k > n/2` satisfy `X[n−k] = X[k]*`). Any length. + +Rust: `transforms::fft::rfft` + """ + ... + +def irfft(x: list[complex], n: int) -> list[float]: + """ +Inverse of `rfft`: rebuilds the full conjugate-symmetric spectrum +and returns the length-n real signal. + +Panics: +Panics unless `x.len() == n / 2 + 1`. + +Rust: `transforms::fft::irfft` + """ + ... + +def fft_2d(x: list[complex], w: int, h: int) -> list[complex]: + """ +2D FFT of row-major data (index = y·w + x): transform rows, then columns. + +Panics: +Panics unless `x.len() == w * h`. + +Rust: `transforms::fft::fft_2d` + """ + ... + +def ifft_2d(x: list[complex], w: int, h: int) -> list[complex]: + """ +Inverse 2D FFT (includes the 1/(w·h) scaling). + +Panics: +Panics unless `x.len() == w * h`. + +Rust: `transforms::fft::ifft_2d` + """ + ... + +def fft_3d(x: list[complex], nx: int, ny: int, nz: int) -> list[complex]: + """ +3D FFT of data indexed as `(z·ny + y)·nx + x`. + +Panics: +Panics unless `x.len() == nx * ny * nz`. + +Rust: `transforms::fft::fft_3d` + """ + ... + +def ifft_3d(x: list[complex], nx: int, ny: int, nz: int) -> list[complex]: + """ +Inverse 3D FFT (includes the 1/(nx·ny·nz) scaling). + +Panics: +Panics unless `x.len() == nx * ny * nz`. + +Rust: `transforms::fft::ifft_3d` + """ + ... + +def rfft_2d(x: list[float], w: int, h: int) -> list[complex]: + """ +2D FFT of real row-major data, keeping only the non-redundant half +along x: output is row-major with width `w/2 + 1` and height `h` +(full transform along y). + +Panics: +Panics unless `x.len() == w * h`. + +Rust: `transforms::fft::rfft_2d` + """ + ... + +def fft_shift(x: MutableSequence[complex]) -> None: + """ +Swap spectrum halves in place so the zero-frequency bin moves to the +center (numpy `fftshift`; for odd n the extra bin lands left of center). + +Rust: `transforms::fft::fft_shift` + """ + ... + +def fft_freqs(n: int, dt: float) -> list[float]: + """ +Frequencies (Hz) of the DFT bins for sample spacing `dt`, in FFT +order: 0, 1/(n·dt), …, then the negative frequencies. + +Rust: `transforms::fft::fft_freqs` + """ + ... + +def fft_convolve_2d(a: list[float], b: list[float], w: int, h: int) -> list[float]: + """ +Circular (periodic) 2D convolution of two w×h real fields via FFT. + +Panics: +Panics unless both inputs have `w * h` samples. + +Rust: `transforms::fft::fft_convolve_2d` + """ + ... + +def fft_convolve(a: list[float], b: list[float]) -> list[float]: + """ +Linear convolution of two real signals via zero-padded FFT. +Matches `signal_processing::convolve` (output length a + b − 1). + +Rust: `transforms::fft::fft_convolve` + """ + ... + +def fft_correlate(a: list[float], b: list[float]) -> list[float]: + """ +Cross-correlation via FFT; matches +`signal_processing::cross_correlate` (length a + b − 1). + +Rust: `transforms::fft::fft_correlate` + """ + ... + +def fft_interpolate(x: list[float], factor: int) -> list[float]: + """ +Band-limited (sinc) interpolation by an integer factor: zero-pad the +spectrum and inverse transform at length n·factor. + +Panics: +Panics if `factor == 0`. + +Rust: `transforms::fft::fft_interpolate` + """ + ... + +def fft_differentiate(x: list[float], dt: float) -> list[float]: + """ +Spectral derivative of a periodic signal sampled at spacing `dt`: +multiply each bin by jω and transform back (Nyquist bin zeroed). + +Rust: `transforms::fft::fft_differentiate` + """ + ... + +def fft_integrate(x: list[float], dt: float) -> list[float]: + """ +Spectral antiderivative of a periodic signal: divide each nonzero bin +by jω; the DC bin is zeroed, so the result is the zero-mean periodic +antiderivative of the mean-removed input. + +Rust: `transforms::fft::fft_integrate` + """ + ... + +def fft_poisson_2d(rhs: list[float], w: int, h: int, dx: float) -> list[float]: + """ +Solve the periodic Poisson problem ∇²u = rhs on a w×h grid with +spacing `dx`, using the eigenvalues of the discrete 5-point Laplacian +so the discrete residual is at roundoff. The k=0 mode is set to zero +(the mean-free solution; a pure-Neumann/periodic problem only +determines u up to a constant, and requires a mean-free rhs). + +Panics: +Panics unless `rhs.len() == w * h`. + +Rust: `transforms::fft::fft_poisson_2d` + """ + ... diff --git a/bindings/python/python/numeria/transforms/hilbert.pyi b/bindings/python/python/numeria/transforms/hilbert.pyi new file mode 100644 index 0000000..2bfecff --- /dev/null +++ b/bindings/python/python/numeria/transforms/hilbert.pyi @@ -0,0 +1,130 @@ +""" +Hilbert transform, analytic signals, modulation, empirical mode decomposition, and causality (Kramers-Kronig) tools. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def analytic_signal(x: list[float]) -> list[complex]: + """ +Analytic signal x + j·H(x) by the FFT method: double the positive +frequencies, zero the negative ones. + +Rust: `transforms::hilbert::analytic_signal` + """ + ... + +def hilbert(x: list[float]) -> list[float]: + """ +Hilbert transform: the quadrature (imaginary) part of the analytic +signal — hilbert(cos ωt) = sin ωt. + +Rust: `transforms::hilbert::hilbert` + """ + ... + +def envelope(x: list[float]) -> list[float]: + """ +Instantaneous amplitude |x + jH(x)|. + +Rust: `transforms::hilbert::envelope` + """ + ... + +def instantaneous_phase(x: list[float]) -> list[float]: + """ +Unwrapped instantaneous phase of the analytic signal. + +Rust: `transforms::hilbert::instantaneous_phase` + """ + ... + +def instantaneous_frequency(x: list[float], fs: float) -> list[float]: + """ +Instantaneous frequency in Hz (central difference of the unwrapped +phase). + +Rust: `transforms::hilbert::instantaneous_frequency` + """ + ... + +def hilbert_fir(n_taps: int) -> list[float]: + """ +FIR Hilbert transformer kernel (odd taps, antisymmetric, windowed). + +Panics: +Panics unless `n_taps` is odd. + +Rust: `transforms::hilbert::hilbert_fir` + """ + ... + +def ssb_modulate(x: list[float], fc: float, fs: float, upper: bool) -> list[float]: + """ +Single-sideband modulation: upper sideband is x·cos − H(x)·sin, +lower is x·cos + H(x)·sin (carrier fc Hz at sample rate fs). + +Rust: `transforms::hilbert::ssb_modulate` + """ + ... + +def am_demodulate(x: list[float]) -> list[float]: + """ +AM envelope demodulation: the analytic-signal envelope (carrier plus +modulation; subtract the mean to recover the AC message). + +Rust: `transforms::hilbert::am_demodulate` + """ + ... + +def fm_demodulate(x: list[float], fs: float) -> list[float]: + """ +FM demodulation: instantaneous frequency of the analytic signal (Hz). + +Rust: `transforms::hilbert::fm_demodulate` + """ + ... + +def empirical_mode_decomposition(x: list[float], max_imfs: int, sift_tol: float) -> list[list[float]]: + """ +Empirical mode decomposition (Huang sifting): returns the IMFs plus +the final residual as the last entry, so the components sum to x. + +Rust: `transforms::hilbert::empirical_mode_decomposition` + """ + ... + +def hilbert_huang_spectrum(x: list[float], fs: float, max_imfs: int) -> list[tuple[list[float], list[float]]]: + """ +Hilbert-Huang spectrum: per IMF (excluding the residual), the +instantaneous frequency track and amplitude envelope. + +Rust: `transforms::hilbert::hilbert_huang_spectrum` + """ + ... + +def kramers_kronig(im: list[float], omega: list[float]) -> list[float]: + """ +Kramers-Kronig relation: real part of a causal response from its +imaginary part sampled on `omega` (ω ≥ 0), by principal-value +trapezoid integration of (2/π)∫ ω′·Im(ω′)/(ω′² − ω²) dω′. + +Panics: +Panics if the lengths differ. + +Rust: `transforms::hilbert::kramers_kronig` + """ + ... + +def minimum_phase_from_magnitude(mag: list[float]) -> list[complex]: + """ +Minimum-phase spectrum with the given magnitude, by the real-cepstrum +method: fold the causal cepstrum and re-exponentiate. `mag` samples +|H| on the full FFT circle (length n). + +Rust: `transforms::hilbert::minimum_phase_from_magnitude` + """ + ... diff --git a/bindings/python/python/numeria/transforms/laplace.pyi b/bindings/python/python/numeria/transforms/laplace.pyi new file mode 100644 index 0000000..cac3c28 --- /dev/null +++ b/bindings/python/python/numeria/transforms/laplace.pyi @@ -0,0 +1,99 @@ +""" +Laplace-domain tools: numerical inverse transforms (fixed-Talbot and Gaver-Stehfest), the z-transform, transfer-function responses, and a discrete fractional Fourier transform. Polynomial coefficient conventions: s-domain polynomials are highest-power-first (like `numerical::polynomial_roots`); digital filter coefficient arrays are in z⁻¹ powers (b\\[0\\] + b\\[1\\]z⁻¹ + …). +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def inverse_laplace_talbot(f: Callable[[complex], complex], t: float, m: int) -> float: + """ +Fixed-Talbot inverse Laplace transform (Abate & Valkó 2004) with m +contour nodes: f(t) from F(s) for t > 0. + +Panics: +Panics if `t <= 0` or `m < 2`. + +Rust: `transforms::laplace::inverse_laplace_talbot` + """ + ... + +def inverse_laplace_stehfest(f: Callable[[float], float], t: float, n: int) -> float: + """ +Gaver-Stehfest inverse Laplace transform: needs only real F(s) +evaluations. `n` must be even (12-16 is typical; larger n needs more +precision than f64 can give). + +Panics: +Panics if `t <= 0`, n is odd, or n > 18. + +Rust: `transforms::laplace::inverse_laplace_stehfest` + """ + ... + +def laplace_numeric(f: Callable[[float], float], s: float, t_max: float, n: int) -> float: + """ +Forward Laplace transform F(s) = ∫₀^tmax f(t)e^(−st) dt by composite +Simpson quadrature (n panels, n rounded up to even). + +Rust: `transforms::laplace::laplace_numeric` + """ + ... + +def z_transform_eval(x: list[float], z: complex) -> complex: + """ +Evaluate the (one-sided) z-transform X(z) = Σ x\\[n\\]·z^(−n). + +Rust: `transforms::laplace::z_transform_eval` + """ + ... + +def impulse_response_from_tf(num: list[float], den: list[float], n: int) -> list[float]: + """ +Impulse response of the digital transfer function +H(z) = (num\\[0\\] + num\\[1\\]z⁻¹ + …)/(den\\[0\\] + den\\[1\\]z⁻¹ + …), +by running the difference equation for n samples. + +Panics: +Panics if `den` is empty or `den[0] == 0`. + +Rust: `transforms::laplace::impulse_response_from_tf` + """ + ... + +def s_domain_freq_response(num: list[float], den: list[float], omega: list[float]) -> list[complex]: + """ +Continuous-time frequency response H(jω) of num(s)/den(s) +(highest-power-first coefficients) at each ω. + +Rust: `transforms::laplace::s_domain_freq_response` + """ + ... + +def digital_freq_response(b: list[float], a: list[float], n_points: int) -> tuple[list[float], list[complex]]: + """ +Digital frequency response of b(z⁻¹)/a(z⁻¹) at `n_points` normalized +frequencies spanning [0, 0.5]; returns (frequencies, response). + +Panics: +Panics if `n_points < 2`. + +Rust: `transforms::laplace::digital_freq_response` + """ + ... + +def fractional_fourier(x: list[complex], alpha: float) -> list[complex]: + """ +Discrete fractional Fourier transform of angle `alpha` (α = π/2 is +the unitary DFT), via the eigendecomposition of the Candan-Kutay- +Ozaktas commuting matrix: F^α = Σ_k e^(−i·k·α)·u_k·(u_kᵀx). O(n²) +after an O(n³) eigen solve, intended for moderate n. + +Errors: +Returns an error if the eigen decomposition fails to converge. + +Rust: `transforms::laplace::fractional_fourier` + """ + ... diff --git a/bindings/python/python/numeria/transforms/radon.pyi b/bindings/python/python/numeria/transforms/radon.pyi new file mode 100644 index 0000000..11a3fc4 --- /dev/null +++ b/bindings/python/python/numeria/transforms/radon.pyi @@ -0,0 +1,107 @@ +""" +Radon transform and tomographic reconstruction, plus Hankel/Abel transforms and Hough voting. Images are row-major (index = y·w + x) with the projection geometry centered on the image; a projection at angle θ integrates along lines perpendicular to the direction (cos θ, sin θ). +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.spatial.primitives import Ray + +class FbpFilter: + """ +Filter kernels for filtered back-projection. + +Rust: `transforms::radon::FbpFilter` + """ + ... + +def radon(img: list[float], w: int, h: int, angles: list[float], n_rays: int) -> list[list[float]]: + """ +Forward Radon transform: one projection (length `n_rays`) per angle +(radians). Ray offsets span the image diagonal; line integrals use +unit-pixel steps with bilinear interpolation. + +Panics: +Panics unless `img.len() == w * h`. + +Rust: `transforms::radon::radon` + """ + ... + +def inverse_radon_fbp(sino: list[list[float]], angles: list[float], out: int, filter: FbpFilter) -> list[float]: + """ +Filtered back-projection onto an `out`×`out` image (pixels outside +the inscribed circle are zero). `sino` is \\[angle\\]\\[ray\\] as produced +by `radon` on a square image of side `out`. + +Rust: `transforms::radon::inverse_radon_fbp` + """ + ... + +def inverse_radon_sart(sino: list[list[float]], angles: list[float], out: int, iters: int) -> list[float]: + """ +Simultaneous algebraic reconstruction (SART): iterate over angles, +forward-project the estimate, and back-project the normalized ray +residuals. + +Rust: `transforms::radon::inverse_radon_sart` + """ + ... + +def shepp_logan_phantom(n: int) -> list[float]: + """ +The classic Shepp-Logan head phantom on an n×n grid (values in the +original low-contrast scale). + +Rust: `transforms::radon::shepp_logan_phantom` + """ + ... + +def hankel_transform(f: Callable[[float], float], k: float, order: int, r_max: float, n: int) -> float: + """ +Hankel transform of order `order`: ∫₀^rmax f(r)·J_ν(k·r)·r dr by +composite Simpson quadrature with n panels. + +Rust: `transforms::radon::hankel_transform` + """ + ... + +def abel_transform(f: Callable[[float], float], y: float, r_max: float, n: int) -> float: + """ +Forward Abel transform F(y) = 2∫_y^rmax f(r)·r/√(r²−y²) dr, computed +singularity-free with the substitution r = √(y² + u²). + +Rust: `transforms::radon::abel_transform` + """ + ... + +def inverse_abel(data: list[float], dr: float) -> list[float]: + """ +Inverse Abel transform of a projection sampled at y_i = i·dr: +f(r) = −(1/π)∫_r^R F′(y)/√(y²−r²) dy, with a central-difference F′ +and the same singularity-removing substitution. + +Rust: `transforms::radon::inverse_abel` + """ + ... + +def hough_lines(edges: list[bool], w: int, h: int, n_theta: int, n_rho: int) -> list[list[int]]: + """ +Hough line accumulator: votes\\[θ\\]\\[ρ\\] with θ over \\[0, π) in +`n_theta` steps and ρ over \\[−D, D\\] (D = image diagonal) in `n_rho` +bins. + +Rust: `transforms::radon::hough_lines` + """ + ... + +def hough_circles(edges: list[bool], w: int, h: int, r_min: int, r_max: int) -> list[tuple[int, int, int, int]]: + """ +Hough circle detection: returns candidate (cx, cy, r, votes) sorted +by votes, keeping local maxima with at least half the top vote. + +Rust: `transforms::radon::hough_circles` + """ + ... diff --git a/bindings/python/python/numeria/transforms/spectral.pyi b/bindings/python/python/numeria/transforms/spectral.pyi new file mode 100644 index 0000000..e62d05b --- /dev/null +++ b/bindings/python/python/numeria/transforms/spectral.pyi @@ -0,0 +1,195 @@ +""" +Spectral estimation: periodogram, Welch averaging, multitaper (DPSS), parametric AR models (Burg, Yule-Walker), MUSIC, cross-spectra, coherence, cepstra, Lomb-Scargle, and spectrum descriptors. All PSDs are one-sided densities in units²/Hz: integrating them over frequency (trapezoid over the returned grid) recovers the signal's variance/power. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.dsp.windows import WindowKind + +def periodogram(x: list[float], fs: float, window_kind: WindowKind) -> tuple[list[float], list[float]]: + """ +Windowed periodogram: (frequencies, one-sided PSD). + +Panics: +Panics on an empty signal. + +Rust: `transforms::spectral::periodogram` + """ + ... + +def welch(x: list[float], fs: float, nperseg: int, noverlap: int, window_kind: WindowKind) -> tuple[list[float], list[float]]: + """ +Welch's method: average windowed periodograms of overlapping +segments (detrended by segment mean removal). + +Panics: +Panics unless `0 < noverlap < nperseg <= x.len()`. + +Rust: `transforms::spectral::welch` + """ + ... + +def dpss(n: int, nw: float, k: int) -> list[list[float]]: + """ +Discrete prolate spheroidal (Slepian) sequences: the first k tapers +of length n at time-bandwidth product nw, from the tridiagonal +eigenproblem. Each taper has unit energy; sign convention: positive +mean (even tapers) / positive first lag (odd tapers). + +Panics: +Panics if `k == 0`, `k > n`, or the eigen solve fails. + +Rust: `transforms::spectral::dpss` + """ + ... + +def multitaper(x: list[float], fs: float, nw: float, k: int) -> tuple[list[float], list[float]]: + """ +Thomson multitaper PSD estimate with k DPSS tapers. + +Rust: `transforms::spectral::multitaper` + """ + ... + +def burg_ar(x: list[float], order: int) -> tuple[list[float], float]: + """ +Burg's method AR(p) fit: returns (a, σ²) for the model +x\\[n\\] = Σ a\\[k\\]·x\\[n−1−k\\] + e\\[n\\] with prediction-error variance σ². + +Panics: +Panics unless `0 < order < x.len()`. + +Rust: `transforms::spectral::burg_ar` + """ + ... + +def yule_walker_ar(x: list[float], order: int) -> tuple[list[float], float]: + """ +Yule-Walker AR(p) fit via the autocorrelation method (Levinson-style +dense solve): same conventions as `burg_ar`. + +Panics: +Panics unless `0 < order < x.len()` and the autocorrelation system is +nonsingular. + +Rust: `transforms::spectral::yule_walker_ar` + """ + ... + +def ar_psd(coeffs: list[float], sigma2: float, fs: float, n: int) -> tuple[list[float], list[float]]: + """ +One-sided PSD of an AR model (a, σ²) on n frequency points up to +Nyquist: σ²/(fs·|1 − Σ a\\[k\\] e^(−jω(k+1))|²), doubled off DC/Nyquist. + +Rust: `transforms::spectral::ar_psd` + """ + ... + +def music(x: list[float], n_sources: int, order: int, fs: float, n: int) -> tuple[list[float], list[float]]: + """ +MUSIC pseudospectrum for real sinusoids: correlation matrix of +dimension `order`, signal subspace of dimension 2·n_sources, and the +noise-subspace projection evaluated at n frequencies up to Nyquist. + +Panics: +Panics unless `2*n_sources < order < x.len()`. + +Rust: `transforms::spectral::music` + """ + ... + +def cross_spectral_density(x: list[float], y: list[float], fs: float, nperseg: int) -> tuple[list[float], list[complex]]: + """ +Welch-averaged cross-spectral density S_xy(f) = E\\[X*(f)·Y(f)\\] +(Hann window, 50% overlap): (frequencies, complex CSD). + +Panics: +Panics unless both signals have at least `nperseg` samples. + +Rust: `transforms::spectral::cross_spectral_density` + """ + ... + +def coherence(x: list[float], y: list[float], fs: float, nperseg: int) -> tuple[list[float], list[float]]: + """ +Magnitude-squared coherence |S_xy|²/(S_xx·S_yy) on the Welch grid. + +Rust: `transforms::spectral::coherence` + """ + ... + +def transfer_function_estimate(input: list[float], output: list[float], fs: float, nperseg: int) -> tuple[list[float], list[complex]]: + """ +H1 transfer-function estimate S_xy/S_xx from input to output. + +Rust: `transforms::spectral::transfer_function_estimate` + """ + ... + +def cepstrum_real(x: list[float]) -> list[float]: + """ +Real cepstrum: IFFT of log |X(f)| (real part). + +Rust: `transforms::spectral::cepstrum_real` + """ + ... + +def cepstrum_power(x: list[float]) -> list[float]: + """ +Power cepstrum: IFFT of log |X(f)|², i.e. twice the real cepstrum. + +Rust: `transforms::spectral::cepstrum_power` + """ + ... + +def lomb_scargle(t: list[float], y: list[float], freqs: list[float]) -> list[float]: + """ +Lomb-Scargle normalized periodogram for unevenly sampled data at the +requested frequencies (Hz). Values are in the classical normalization +(power / 2σ²). + +Panics: +Panics if `t` and `y` lengths differ or fewer than 2 samples. + +Rust: `transforms::spectral::lomb_scargle` + """ + ... + +def spectral_entropy(psd: list[float]) -> float: + """ +Spectral entropy of a PSD, normalized to \\[0, 1\\]. + +Rust: `transforms::spectral::spectral_entropy` + """ + ... + +def spectral_flatness(psd: list[float]) -> float: + """ +Spectral flatness (Wiener entropy): geometric over arithmetic mean. + +Rust: `transforms::spectral::spectral_flatness` + """ + ... + +def detrend(x: list[float], order: int) -> list[float]: + """ +Remove a least-squares polynomial trend of the given order. + +Panics: +Panics if the fit system is singular (order too high for the data). + +Rust: `transforms::spectral::detrend` + """ + ... + +def power_law_fit(f: list[float], psd: list[float], f_min: float, f_max: float) -> tuple[float, float]: + """ +Fit PSD ≈ A·f^(−α) over \\[f_min, f_max\\] by log-log linear regression; +returns (α, A). + +Rust: `transforms::spectral::power_law_fit` + """ + ... diff --git a/bindings/python/python/numeria/transforms/stft.pyi b/bindings/python/python/numeria/transforms/stft.pyi new file mode 100644 index 0000000..c9e0c5a --- /dev/null +++ b/bindings/python/python/numeria/transforms/stft.pyi @@ -0,0 +1,127 @@ +""" +Short-time Fourier transform, spectrograms, Goertzel, chirp-z, and constant-Q analysis. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.dsp.windows import WindowKind + +class Stft: + """ +Short-time Fourier transform configuration: an analysis window, hop +size in samples, and FFT length (≥ window length; frames are +zero-padded up to it). + +Rust: `transforms::stft::Stft` + """ + def __init__(self, window: list[float], hop: int, n_fft: int) -> None: ... + def forward(self, x: list[float]) -> list[list[complex]]: ... + def inverse(self, frames: list[list[complex]]) -> list[float]: ... + @staticmethod + def magnitude(frames: list[list[complex]]) -> list[list[float]]: ... + @staticmethod + def power_db(frames: list[list[complex]]) -> list[list[float]]: ... + def times(self, n_samples: int, fs: float) -> list[float]: ... + def freqs(self, fs: float) -> list[float]: ... + def is_cola(self) -> bool: ... + @property + def window(self) -> list[float]: ... + @property + def hop(self) -> int: ... + @property + def n_fft(self) -> int: ... + +def spectrogram(x: list[float], fs: float, n_fft: int, hop: int, window_kind: WindowKind) -> tuple[list[float], list[float], list[list[float]]]: + """ +Power spectrogram: (frame times, bin frequencies, |X|² per frame). + +Rust: `transforms::stft::spectrogram` + """ + ... + +def mel_filterbank(n_fft: int, fs: float, n_mels: int, fmin: float, fmax: float) -> list[list[float]]: + """ +Triangular mel filterbank: `n_mels` rows of n_fft/2 + 1 weights. + +Panics: +Panics if the frequency range is empty or fmax exceeds Nyquist. + +Rust: `transforms::stft::mel_filterbank` + """ + ... + +def mel_spectrogram(x: list[float], fs: float, n_fft: int, hop: int, n_mels: int, fmin: float, fmax: float) -> list[list[float]]: + """ +Mel-scale power spectrogram: one vector of `n_mels` band energies per +frame (Hann window). + +Rust: `transforms::stft::mel_spectrogram` + """ + ... + +def goertzel(x: list[float], target_freq: float, fs: float) -> tuple[float, float]: + """ +Goertzel single-bin DFT at an arbitrary frequency: returns the +(magnitude, phase) of Σ x\\[n\\]·e^(−jωn), ω = 2π·target/fs. + +Rust: `transforms::stft::goertzel` + """ + ... + +def goertzel_bank(x: list[float], freqs: list[float], fs: float) -> list[float]: + """ +Goertzel magnitudes for a set of frequencies. + +Rust: `transforms::stft::goertzel_bank` + """ + ... + +def dtmf_decode(x: list[float], fs: float) -> Optional[str]: + """ +Decode one DTMF digit from a tone burst; None when no clear +row/column pair dominates. + +Rust: `transforms::stft::dtmf_decode` + """ + ... + +def chirp_z(x: list[complex], m: int, w: complex, a: complex) -> list[complex]: + """ +Chirp-z transform: X\\[k\\] = Σ x\\[n\\]·a^(−n)·w^(nk) for k = 0..m−1, +evaluated in O((n+m) log(n+m)) by Bluestein's substitution. + +Rust: `transforms::stft::chirp_z` + """ + ... + +def zoom_fft(x: list[float], fs: float, f_lo: float, f_hi: float, m: int) -> list[complex]: + """ +Zoom FFT: m spectrum samples evenly spaced over [f_lo, f_hi] Hz. + +Rust: `transforms::stft::zoom_fft` + """ + ... + +def reassigned_spectrogram(x: list[float], fs: float, n_fft: int, hop: int) -> list[tuple[float, float, float]]: + """ +Time-frequency reassigned spectrogram (Hann window): each bin's +energy is moved to its instantaneous time and frequency. Returns +(time s, frequency Hz, power) for every bin above −80 dB of the peak. + +Rust: `transforms::stft::reassigned_spectrogram` + """ + ... + +def constant_q_transform(x: list[float], fs: float, fmin: float, bins_per_octave: int, n_bins: int) -> list[list[float]]: + """ +Constant-Q transform magnitudes: bins at fmin·2^(k/bins_per_octave), +each analyzed with its own Hann-windowed complex kernel whose length +keeps Q constant. Returns one vector of `n_bins` magnitudes per hop +of half the longest kernel. + +Rust: `transforms::stft::constant_q_transform` + """ + ... diff --git a/bindings/python/python/numeria/transforms/wavelet.pyi b/bindings/python/python/numeria/transforms/wavelet.pyi new file mode 100644 index 0000000..5c1ee57 --- /dev/null +++ b/bindings/python/python/numeria/transforms/wavelet.pyi @@ -0,0 +1,202 @@ +""" +Discrete and continuous wavelet transforms. Filter banks, boundary handling, and coefficient lengths follow the PyWavelets conventions (dwt output length ⌊(n + L − 1)/2⌋, idwt output length 2·len − L + 2), so round trips are exact for every wavelet and padding mode. The CWT follows Torrence & Compo (1998). +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +class Mother: + """ +Mother wavelets for the CWT (Torrence & Compo definitions): +Morlet(ω₀), Mexican hat (DOG order 2), Paul(m), DOG(m). + +Rust: `transforms::wavelet::Mother` + """ + ... + +class PadMode: + """ +Signal extension at the boundaries. + +Rust: `transforms::wavelet::PadMode` + """ + ... + +class Threshold: + """ +Detail-coefficient thresholding rules for `wavelet_denoise`. + +Rust: `transforms::wavelet::Threshold` + """ + ... + +class Wavelet: + """ +Wavelet families: Haar, Daubechies 1–20, symlets 2–20, coiflets 1–5, +and the biorthogonal spline family (bior p.q as in PyWavelets). + +Rust: `transforms::wavelet::Wavelet` + """ + ... + +def wavelet_filters(w: Wavelet) -> tuple[list[float], list[float], list[float], list[float]]: + """ +Decomposition and reconstruction filters (dec_lo, dec_hi, rec_lo, +rec_hi), quadrature-mirror related for the orthogonal families. + +Panics: +Panics for an unsupported order (Db/Sym > 20, Coif > 5, or a bior +pair outside the standard set). + +Rust: `transforms::wavelet::wavelet_filters` + """ + ... + +def dwt(x: list[float], w: Wavelet, mode: PadMode) -> tuple[list[float], list[float]]: + """ +One-level DWT: (approximation, detail), each of length ⌊(n+L−1)/2⌋. + +Rust: `transforms::wavelet::dwt` + """ + ... + +def idwt(a: list[float], d: list[float], w: Wavelet, mode: PadMode) -> list[float]: + """ +One-level inverse DWT; output length 2·len − L + 2. `mode` is +accepted for API symmetry (reconstruction itself needs no padding). + +Panics: +Panics if the approximation and detail lengths differ. + +Rust: `transforms::wavelet::idwt` + """ + ... + +def wavedec(x: list[float], w: Wavelet, levels: int, mode: PadMode) -> list[list[float]]: + """ +Multilevel decomposition: returns \\[a_L, d_L, d_{L−1}, …, d_1\\]. + +Rust: `transforms::wavelet::wavedec` + """ + ... + +def waverec(coeffs: list[list[float]], w: Wavelet, mode: PadMode) -> list[float]: + """ +Multilevel reconstruction (inverse of `wavedec`). + +Rust: `transforms::wavelet::waverec` + """ + ... + +def dwt_2d(img: list[float], w: int, h: int, wavelet: Wavelet) -> tuple[list[float], list[float], list[float], list[float]]: + """ +One-level separable 2D DWT with symmetric extension (rows along x +first, then columns): returns (LL, LH, HL, HH) where the first +letter is the x (row-direction) channel. Sub-band dims are +⌊(w+L−1)/2⌋ × ⌊(h+L−1)/2⌋. + +Panics: +Panics unless `img.len() == w * h`. + +Rust: `transforms::wavelet::dwt_2d` + """ + ... + +def idwt_2d(ll: list[float], lh: list[float], hl: list[float], hh: list[float], w: int, h: int, wavelet: Wavelet) -> list[float]: + """ +Inverse of `dwt_2d`; `w` and `h` are the original image dimensions. + +Rust: `transforms::wavelet::idwt_2d` + """ + ... + +def wavelet_denoise(x: list[float], w: Wavelet, levels: int, t: Threshold) -> list[float]: + """ +Wavelet shrinkage denoising: decompose, threshold the detail bands, +reconstruct (trimmed to the input length). + +Rust: `transforms::wavelet::wavelet_denoise` + """ + ... + +def wavelet_compress(x: list[float], w: Wavelet, levels: int, keep_fraction: float) -> list[float]: + """ +Keep the largest `keep_fraction` of all coefficients (approximation +always kept), zero the rest, and reconstruct. + +Rust: `transforms::wavelet::wavelet_compress` + """ + ... + +def cwt(x: list[float], scales: list[float], mother: Mother, fs: float) -> list[list[complex]]: + """ +Continuous wavelet transform. `scales` are in samples; row s of the +output holds W(s, t) at every sample. Computed in the Fourier domain +(Torrence & Compo eq. 4) with unit-energy normalization √(2πs). + +Rust: `transforms::wavelet::cwt` + """ + ... + +def scalogram(x: list[float], scales: list[float], mother: Mother, fs: float) -> list[list[float]]: + """ +|CWT|² per scale and sample. + +Rust: `transforms::wavelet::scalogram` + """ + ... + +def scale_to_frequency(scale: float, mother: Mother, fs: float) -> float: + """ +Equivalent Fourier frequency (Hz) of a CWT scale in samples +(Torrence & Compo table 1). + +Rust: `transforms::wavelet::scale_to_frequency` + """ + ... + +def wavelet_packet_decompose(x: list[float], w: Wavelet, levels: int) -> list[list[float]]: + """ +Full wavelet-packet tree at the given depth: 2^levels leaves in +natural (frequency-ordered-by-index) order, symmetric extension. + +Rust: `transforms::wavelet::wavelet_packet_decompose` + """ + ... + +def lifting_dwt_53(x: MutableSequence[int]) -> None: + """ +Lossless integer 5/3 (LeGall) lifting DWT, in place: the first half +becomes the approximation, the second half the detail. + +Panics: +Panics unless the length is even and ≥ 2. + +Rust: `transforms::wavelet::lifting_dwt_53` + """ + ... + +def lifting_idwt_53(x: MutableSequence[int]) -> None: + """ +Exact inverse of `lifting_dwt_53`. + +Panics: +Panics unless the length is even and ≥ 2. + +Rust: `transforms::wavelet::lifting_idwt_53` + """ + ... + +def multiresolution_analysis(x: list[float], w: Wavelet, levels: int) -> list[list[float]]: + """ +Multiresolution analysis: the input split into levels+1 additive +components (details from coarse to fine, then the approximation +first). Component 0 is the level-L approximation signal; component k +(k ≥ 1) is the detail at level L+1−k. The components sum to x. + +Rust: `transforms::wavelet::multiresolution_analysis` + """ + ... diff --git a/bindings/python/python/numeria/trigonometry.pyi b/bindings/python/python/numeria/trigonometry.pyi new file mode 100644 index 0000000..d23d7a3 --- /dev/null +++ b/bindings/python/python/numeria/trigonometry.pyi @@ -0,0 +1,274 @@ +""" +Triangle solving, trigonometric identities, and hyperbolic functions. The laws of sines and cosines in both directions -- side from angles and angle from sides -- and the SAS triangle area. The identities are provided as functions rather than left to the caller to expand: sum and difference, double and half angle, and product-to-sum. The hyperbolic family includes the reciprocals (`sech`, `csch`, `coth`) and inverses that `f64` does not provide directly. Angle utilities close the module: normalization to `[0, 2π)` or `(−π, π]`, the signed shortest difference between two angles, and classification as acute, right or obtuse. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.spatial.primitives import Triangle + +def law_of_cosines_side(a: float, b: float, angle_c: float) -> float: + """ +Compute unknown side via law of cosines: c = sqrt(a² + b² - 2ab·cos(C)) + +Rust: `trigonometry::law_of_cosines_side` + """ + ... + +def law_of_cosines_angle(a: float, b: float, c: float) -> float: + """ +Compute unknown angle via law of cosines: C = arccos((a² + b² - c²) / (2ab)) + +Rust: `trigonometry::law_of_cosines_angle` + """ + ... + +def law_of_sines_side(a: float, angle_a: float, angle_b: float) -> float: + """ +Compute unknown side via law of sines: b = a·sin(B) / sin(A) + +Rust: `trigonometry::law_of_sines_side` + """ + ... + +def law_of_sines_angle(a: float, b: float, angle_a: float) -> float: + """ +Compute unknown angle via law of sines: B = arcsin(b·sin(A) / a) + +Rust: `trigonometry::law_of_sines_angle` + """ + ... + +def triangle_area_sas(a: float, b: float, angle_c: float) -> float: + """ +Triangle area using two sides and included angle: A = ½·a·b·sin(C) + +Rust: `trigonometry::triangle_area_sas` + """ + ... + +def sin_sum(a: float, b: float) -> float: + """ +Sine of sum identity: sin(a+b) = sin(a)cos(b) + cos(a)sin(b) + +Rust: `trigonometry::sin_sum` + """ + ... + +def cos_sum(a: float, b: float) -> float: + """ +Cosine of sum identity: cos(a+b) = cos(a)cos(b) - sin(a)sin(b) + +Rust: `trigonometry::cos_sum` + """ + ... + +def sin_diff(a: float, b: float) -> float: + """ +Sine of difference identity: sin(a-b) = sin(a)cos(b) - cos(a)sin(b) + +Rust: `trigonometry::sin_diff` + """ + ... + +def cos_diff(a: float, b: float) -> float: + """ +Cosine of difference identity: cos(a-b) = cos(a)cos(b) + sin(a)sin(b) + +Rust: `trigonometry::cos_diff` + """ + ... + +def tan_sum(a: float, b: float) -> float: + """ +Tangent of sum identity: tan(a+b) = (tan(a) + tan(b)) / (1 - tan(a)tan(b)) + +Rust: `trigonometry::tan_sum` + """ + ... + +def double_angle_sin(a: float) -> float: + """ +Double-angle sine identity: sin(2a) = 2·sin(a)·cos(a) + +Rust: `trigonometry::double_angle_sin` + """ + ... + +def double_angle_cos(a: float) -> float: + """ +Double-angle cosine identity: cos(2a) = cos²(a) - sin²(a) + +Rust: `trigonometry::double_angle_cos` + """ + ... + +def half_angle_sin(a: float) -> float: + """ +Half-angle sine identity: sin(a/2) = sqrt(|1 - cos(a)| / 2) + +Rust: `trigonometry::half_angle_sin` + """ + ... + +def half_angle_cos(a: float) -> float: + """ +Half-angle cosine identity: cos(a/2) = sqrt(|1 + cos(a)| / 2) + +Rust: `trigonometry::half_angle_cos` + """ + ... + +def product_to_sum_sin_sin(a: float, b: float) -> float: + """ +Product-to-sum for sin·sin: sin(a)sin(b) = ½[cos(a-b) - cos(a+b)] + +Rust: `trigonometry::product_to_sum_sin_sin` + """ + ... + +def product_to_sum_cos_cos(a: float, b: float) -> float: + """ +Product-to-sum for cos·cos: cos(a)cos(b) = ½[cos(a-b) + cos(a+b)] + +Rust: `trigonometry::product_to_sum_cos_cos` + """ + ... + +def sinh(x: float) -> float: + """ +Hyperbolic sine: sinh(x) = (eˣ - e⁻ˣ) / 2 + +Rust: `trigonometry::sinh` + """ + ... + +def cosh(x: float) -> float: + """ +Hyperbolic cosine: cosh(x) = (eˣ + e⁻ˣ) / 2 + +Rust: `trigonometry::cosh` + """ + ... + +def tanh(x: float) -> float: + """ +Hyperbolic tangent: tanh(x) = sinh(x) / cosh(x) + +Rust: `trigonometry::tanh` + """ + ... + +def sech(x: float) -> float: + """ +Hyperbolic secant: sech(x) = 1 / cosh(x) + +Rust: `trigonometry::sech` + """ + ... + +def csch(x: float) -> float: + """ +Hyperbolic cosecant: csch(x) = 1 / sinh(x) + +Rust: `trigonometry::csch` + """ + ... + +def coth(x: float) -> float: + """ +Hyperbolic cotangent: coth(x) = cosh(x) / sinh(x) + +Rust: `trigonometry::coth` + """ + ... + +def asinh(x: float) -> float: + """ +Inverse hyperbolic sine: asinh(x) = ln(x + sqrt(x² + 1)) + +Rust: `trigonometry::asinh` + """ + ... + +def acosh(x: float) -> float: + """ +Inverse hyperbolic cosine: acosh(x) = ln(x + sqrt(x² - 1)) + +Rust: `trigonometry::acosh` + """ + ... + +def atanh(x: float) -> float: + """ +Inverse hyperbolic tangent: atanh(x) = ½·ln((1+x) / (1-x)) + +Rust: `trigonometry::atanh` + """ + ... + +def normalize_angle(angle: float) -> float: + """ +Normalize angle to the range [0, 2π) + +Rust: `trigonometry::normalize_angle` + """ + ... + +def normalize_angle_signed(angle: float) -> float: + """ +Normalize angle to the range [-π, π) + +Rust: `trigonometry::normalize_angle_signed` + """ + ... + +def angular_difference(a: float, b: float) -> float: + """ +Signed shortest angular difference from a to b: normalize(b - a) in [-π, π) + +Rust: `trigonometry::angular_difference` + """ + ... + +def is_acute(angle: float) -> bool: + """ +Check whether angle (in radians) is acute: 0 < angle < π/2 + +Rust: `trigonometry::is_acute` + """ + ... + +def is_right(angle: float, tolerance: float) -> bool: + """ +Check whether angle (in radians) is a right angle within tolerance: |angle - π/2| < tol + +Rust: `trigonometry::is_right` + """ + ... + +def is_obtuse(angle: float) -> bool: + """ +Check whether angle (in radians) is obtuse: π/2 < angle < π + +Rust: `trigonometry::is_obtuse` + """ + ... + +def complementary(angle: float) -> float: + """ +Complementary angle: π/2 - angle + +Rust: `trigonometry::complementary` + """ + ... + +def supplementary(angle: float) -> float: + """ +Supplementary angle: π - angle + +Rust: `trigonometry::supplementary` + """ + ... diff --git a/bindings/python/python/numeria/units/__init__.pyi b/bindings/python/python/numeria/units/__init__.pyi new file mode 100644 index 0000000..593d3ac --- /dev/null +++ b/bindings/python/python/numeria/units/__init__.pyi @@ -0,0 +1,650 @@ +""" +Unit conversions, dimensional analysis and the CODATA constants. The flat conversion functions below are the original contents of this module and are unchanged. What sits alongside them now is the typed machinery: `quantity` carries a value together with its seven SI exponents so that adding a length to a time is a compile-time-shaped error rather than a silent number, and `dimensional` does the analysis those exponents make possible -- Buckingham's theorem over exact rationals, the named dimensionless groups, natural units and the Planck scale. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from . import dimensional, quantity + +def meters_to_feet(m: float) -> float: + """ +Convert meters to feet: ft = m × 3.28084 + +Rust: `units::meters_to_feet` + """ + ... + +def feet_to_meters(ft: float) -> float: + """ +Convert feet to meters: m = ft / 3.28084 + +Rust: `units::feet_to_meters` + """ + ... + +def meters_to_inches(m: float) -> float: + """ +Convert meters to inches: in = m × 39.3701 + +Rust: `units::meters_to_inches` + """ + ... + +def inches_to_meters(i: float) -> float: + """ +Convert inches to meters: m = in / 39.3701 + +Rust: `units::inches_to_meters` + """ + ... + +def km_to_miles(km: float) -> float: + """ +Convert kilometers to miles: mi = km × 0.621371 + +Rust: `units::km_to_miles` + """ + ... + +def miles_to_km(mi: float) -> float: + """ +Convert miles to kilometers: km = mi / 0.621371 + +Rust: `units::miles_to_km` + """ + ... + +def meters_to_au(m: float) -> float: + """ +Convert meters to astronomical units: AU = m / 1.496×10¹¹ + +Rust: `units::meters_to_au` + """ + ... + +def au_to_meters(au: float) -> float: + """ +Convert astronomical units to meters: m = AU × 1.496×10¹¹ + +Rust: `units::au_to_meters` + """ + ... + +def meters_to_light_years(m: float) -> float: + """ +Convert meters to light-years: ly = m / 9.461×10¹⁵ + +Rust: `units::meters_to_light_years` + """ + ... + +def light_years_to_meters(ly: float) -> float: + """ +Convert light-years to meters: m = ly × 9.461×10¹⁵ + +Rust: `units::light_years_to_meters` + """ + ... + +def meters_to_parsec(m: float) -> float: + """ +Convert meters to parsecs: pc = m / 3.086×10¹⁶ + +Rust: `units::meters_to_parsec` + """ + ... + +def parsec_to_meters(pc: float) -> float: + """ +Convert parsecs to meters: m = pc × 3.086×10¹⁶ + +Rust: `units::parsec_to_meters` + """ + ... + +def angstrom_to_meters(a: float) -> float: + """ +Convert angstroms to meters: m = Å × 10⁻¹⁰ + +Rust: `units::angstrom_to_meters` + """ + ... + +def meters_to_angstrom(m: float) -> float: + """ +Convert meters to angstroms: Å = m / 10⁻¹⁰ + +Rust: `units::meters_to_angstrom` + """ + ... + +def nautical_miles_to_meters(nm: float) -> float: + """ +Convert nautical miles to meters: m = nmi × 1852 + +Rust: `units::nautical_miles_to_meters` + """ + ... + +def meters_to_nautical_miles(m: float) -> float: + """ +Convert meters to nautical miles: nmi = m / 1852 + +Rust: `units::meters_to_nautical_miles` + """ + ... + +def kg_to_lbs(kg: float) -> float: + """ +Convert kilograms to pounds: lb = kg × 2.20462 + +Rust: `units::kg_to_lbs` + """ + ... + +def lbs_to_kg(lbs: float) -> float: + """ +Convert pounds to kilograms: kg = lb / 2.20462 + +Rust: `units::lbs_to_kg` + """ + ... + +def kg_to_solar_masses(kg: float) -> float: + """ +Convert kilograms to solar masses: M☉ = kg / 1.989×10³⁰ + +Rust: `units::kg_to_solar_masses` + """ + ... + +def solar_masses_to_kg(sm: float) -> float: + """ +Convert solar masses to kilograms: kg = M☉ × 1.989×10³⁰ + +Rust: `units::solar_masses_to_kg` + """ + ... + +def amu_to_kg(amu: float) -> float: + """ +Convert atomic mass units to kilograms: kg = amu × 1.66054×10⁻²⁷ + +Rust: `units::amu_to_kg` + """ + ... + +def kg_to_amu(kg: float) -> float: + """ +Convert kilograms to atomic mass units: amu = kg / 1.66054×10⁻²⁷ + +Rust: `units::kg_to_amu` + """ + ... + +def joules_to_ev(j: float) -> float: + """ +Convert joules to electron-volts: eV = J / e + +Rust: `units::joules_to_ev` + """ + ... + +def ev_to_joules(ev: float) -> float: + """ +Convert electron-volts to joules: J = eV × e + +Rust: `units::ev_to_joules` + """ + ... + +def joules_to_calories(j: float) -> float: + """ +Convert joules to calories: cal = J / 4.184 + +Rust: `units::joules_to_calories` + """ + ... + +def calories_to_joules(cal: float) -> float: + """ +Convert calories to joules: J = cal × 4.184 + +Rust: `units::calories_to_joules` + """ + ... + +def joules_to_kwh(j: float) -> float: + """ +Convert joules to kilowatt-hours: kWh = J / 3.6×10⁶ + +Rust: `units::joules_to_kwh` + """ + ... + +def kwh_to_joules(kwh: float) -> float: + """ +Convert kilowatt-hours to joules: J = kWh × 3.6×10⁶ + +Rust: `units::kwh_to_joules` + """ + ... + +def joules_to_btu(j: float) -> float: + """ +Convert joules to British thermal units: BTU = J / 1055.06 + +Rust: `units::joules_to_btu` + """ + ... + +def btu_to_joules(btu: float) -> float: + """ +Convert British thermal units to joules: J = BTU × 1055.06 + +Rust: `units::btu_to_joules` + """ + ... + +def ev_to_wavelength(ev: float) -> float: + """ +Converts photon energy in eV to wavelength in meters via λ = hc/E. + +Rust: `units::ev_to_wavelength` + """ + ... + +def wavelength_to_ev(wavelength: float) -> float: + """ +Converts photon wavelength in meters to energy in eV via E = hc/λ. + +Rust: `units::wavelength_to_ev` + """ + ... + +def pa_to_atm(pa: float) -> float: + """ +Convert pascals to atmospheres: atm = Pa / 101325 + +Rust: `units::pa_to_atm` + """ + ... + +def atm_to_pa(atm: float) -> float: + """ +Convert atmospheres to pascals: Pa = atm × 101325 + +Rust: `units::atm_to_pa` + """ + ... + +def pa_to_bar(pa: float) -> float: + """ +Convert pascals to bar: bar = Pa / 10⁵ + +Rust: `units::pa_to_bar` + """ + ... + +def bar_to_pa(bar: float) -> float: + """ +Convert bar to pascals: Pa = bar × 10⁵ + +Rust: `units::bar_to_pa` + """ + ... + +def pa_to_psi(pa: float) -> float: + """ +Convert pascals to pounds per square inch: psi = Pa / 6894.76 + +Rust: `units::pa_to_psi` + """ + ... + +def psi_to_pa(psi: float) -> float: + """ +Convert pounds per square inch to pascals: Pa = psi × 6894.76 + +Rust: `units::psi_to_pa` + """ + ... + +def pa_to_mmhg(pa: float) -> float: + """ +Convert pascals to millimeters of mercury: mmHg = Pa / 133.322 + +Rust: `units::pa_to_mmhg` + """ + ... + +def mmhg_to_pa(mmhg: float) -> float: + """ +Convert millimeters of mercury to pascals: Pa = mmHg × 133.322 + +Rust: `units::mmhg_to_pa` + """ + ... + +def degrees_to_radians(deg: float) -> float: + """ +Convert degrees to radians: rad = deg × π / 180 + +Rust: `units::degrees_to_radians` + """ + ... + +def radians_to_degrees(rad: float) -> float: + """ +Convert radians to degrees: deg = rad × 180 / π + +Rust: `units::radians_to_degrees` + """ + ... + +def rpm_to_rad_per_sec(rpm: float) -> float: + """ +Convert revolutions per minute to radians per second: ω = rpm × 2π / 60 + +Rust: `units::rpm_to_rad_per_sec` + """ + ... + +def rad_per_sec_to_rpm(omega: float) -> float: + """ +Convert radians per second to revolutions per minute: rpm = ω × 60 / (2π) + +Rust: `units::rad_per_sec_to_rpm` + """ + ... + +def seconds_to_years(s: float) -> float: + """ +Convert seconds to years: yr = s / 3.1557×10⁷ + +Rust: `units::seconds_to_years` + """ + ... + +def years_to_seconds(yr: float) -> float: + """ +Convert years to seconds: s = yr × 3.1557×10⁷ + +Rust: `units::years_to_seconds` + """ + ... + +def mps_to_kmh(mps: float) -> float: + """ +Convert meters per second to kilometers per hour: km/h = m/s × 3.6 + +Rust: `units::mps_to_kmh` + """ + ... + +def kmh_to_mps(kmh: float) -> float: + """ +Convert kilometers per hour to meters per second: m/s = km/h / 3.6 + +Rust: `units::kmh_to_mps` + """ + ... + +def mps_to_mph(mps: float) -> float: + """ +Convert meters per second to miles per hour: mph = m/s × 2.23694 + +Rust: `units::mps_to_mph` + """ + ... + +def mph_to_mps(mph: float) -> float: + """ +Convert miles per hour to meters per second: m/s = mph / 2.23694 + +Rust: `units::mph_to_mps` + """ + ... + +def mps_to_knots(mps: float) -> float: + """ +Convert meters per second to knots: kt = m/s × 1.94384 + +Rust: `units::mps_to_knots` + """ + ... + +def knots_to_mps(kt: float) -> float: + """ +Convert knots to meters per second: m/s = kt / 1.94384 + +Rust: `units::knots_to_mps` + """ + ... + +def mps_to_mach(mps: float, speed_of_sound: float) -> float: + """ +Convert meters per second to Mach number: M = v / v_sound + +Rust: `units::mps_to_mach` + """ + ... + +def mach_to_mps(mach: float, speed_of_sound: float) -> float: + """ +Convert Mach number to meters per second: v = M × v_sound + +Rust: `units::mach_to_mps` + """ + ... + +def watts_to_horsepower(w: float) -> float: + """ +Convert watts to mechanical horsepower: hp = W / 745.7 + +Rust: `units::watts_to_horsepower` + """ + ... + +def horsepower_to_watts(hp: float) -> float: + """ +Convert mechanical horsepower to watts: W = hp × 745.7 + +Rust: `units::horsepower_to_watts` + """ + ... + +def watts_to_btu_per_hour(w: float) -> float: + """ +Convert watts to BTU per hour: BTU/h = W / 0.293071 + +Rust: `units::watts_to_btu_per_hour` + """ + ... + +def btu_per_hour_to_watts(btu_hr: float) -> float: + """ +Convert BTU per hour to watts: W = BTU/h × 0.293071 + +Rust: `units::btu_per_hour_to_watts` + """ + ... + +def watts_to_tons_refrigeration(w: float) -> float: + """ +Convert watts to tons of refrigeration: TR = W / 3516.85 + +Rust: `units::watts_to_tons_refrigeration` + """ + ... + +def tons_refrigeration_to_watts(tons: float) -> float: + """ +Convert tons of refrigeration to watts: W = TR × 3516.85 + +Rust: `units::tons_refrigeration_to_watts` + """ + ... + +def watts_to_kcal_per_hour(w: float) -> float: + """ +Convert watts to kilocalories per hour: kcal/h = W / 1.163 + +Rust: `units::watts_to_kcal_per_hour` + """ + ... + +def kcal_per_hour_to_watts(kcal_hr: float) -> float: + """ +Convert kilocalories per hour to watts: W = kcal/h × 1.163 + +Rust: `units::kcal_per_hour_to_watts` + """ + ... + +def kilowatts_to_watts(kw: float) -> float: + """ +Convert kilowatts to watts: W = kW × 10³ + +Rust: `units::kilowatts_to_watts` + """ + ... + +def watts_to_kilowatts(w: float) -> float: + """ +Convert watts to kilowatts: kW = W × 10⁻³ + +Rust: `units::watts_to_kilowatts` + """ + ... + +def megawatts_to_watts(mw: float) -> float: + """ +Convert megawatts to watts: W = MW × 10⁶ + +Rust: `units::megawatts_to_watts` + """ + ... + +def watts_to_megawatts(w: float) -> float: + """ +Convert watts to megawatts: MW = W × 10⁻⁶ + +Rust: `units::watts_to_megawatts` + """ + ... + +def watt_hours_to_joules(wh: float) -> float: + """ +Convert watt-hours to joules: J = Wh × 3600 + +Rust: `units::watt_hours_to_joules` + """ + ... + +def joules_to_watt_hours(j: float) -> float: + """ +Convert joules to watt-hours: Wh = J / 3600 + +Rust: `units::joules_to_watt_hours` + """ + ... + +def amp_hours_to_coulombs(ah: float) -> float: + """ +Convert ampere-hours to coulombs: C = Ah × 3600 + +Rust: `units::amp_hours_to_coulombs` + """ + ... + +def coulombs_to_amp_hours(c: float) -> float: + """ +Convert coulombs to ampere-hours: Ah = C / 3600 + +Rust: `units::coulombs_to_amp_hours` + """ + ... + +def kg_tnt_to_joules(kg: float) -> float: + """ +Convert kg of TNT equivalent to joules: J = kg × 4.184×10⁶ + +Rust: `units::kg_tnt_to_joules` + """ + ... + +def joules_to_kg_tnt(j: float) -> float: + """ +Convert joules to kg of TNT equivalent: kg = J / 4.184×10⁶ + +Rust: `units::joules_to_kg_tnt` + """ + ... + +def kg_coal_to_joules(kg: float) -> float: + """ +Convert kg of coal equivalent to joules: J = kg × 2.9×10⁷ + +Rust: `units::kg_coal_to_joules` + """ + ... + +def joules_to_kg_coal(j: float) -> float: + """ +Convert joules to kg of coal equivalent: kg = J / 2.9×10⁷ + +Rust: `units::joules_to_kg_coal` + """ + ... + +def kg_oil_to_joules(kg: float) -> float: + """ +Convert kg of oil equivalent to joules: J = kg × 4.187×10⁷ + +Rust: `units::kg_oil_to_joules` + """ + ... + +def joules_to_kg_oil(j: float) -> float: + """ +Convert joules to kg of oil equivalent: kg = J / 4.187×10⁷ + +Rust: `units::joules_to_kg_oil` + """ + ... + +def kg_hydrogen_to_joules(kg: float) -> float: + """ +Convert kg of hydrogen to joules: J = kg × 1.42×10⁸ + +Rust: `units::kg_hydrogen_to_joules` + """ + ... + +def joules_to_kg_hydrogen(j: float) -> float: + """ +Convert joules to kg of hydrogen equivalent: kg = J / 1.42×10⁸ + +Rust: `units::joules_to_kg_hydrogen` + """ + ... + +def liters_gasoline_to_joules(liters: float) -> float: + """ +Convert liters of gasoline to joules: J = L × 3.4×10⁷ + +Rust: `units::liters_gasoline_to_joules` + """ + ... + +def joules_to_liters_gasoline(j: float) -> float: + """ +Convert joules to liters of gasoline equivalent: L = J / 3.4×10⁷ + +Rust: `units::joules_to_liters_gasoline` + """ + ... diff --git a/bindings/python/python/numeria/units/dimensional.pyi b/bindings/python/python/numeria/units/dimensional.pyi new file mode 100644 index 0000000..2bb7331 --- /dev/null +++ b/bindings/python/python/numeria/units/dimensional.pyi @@ -0,0 +1,161 @@ +""" +Dimensional analysis: Buckingham's theorem, the named groups, natural units and the Planck scale. # Buckingham's theorem is a rank computation A physical relation among `n` quantities built from `r` independent dimensions can be rewritten as a relation among exactly `n - r` dimensionless groups. That is not a heuristic: the dimension vectors form the columns of a matrix, a dimensionless product of powers is a vector in its null space, and the dimension of a null space is the column count minus the rank. Every part of it is linear algebra over the rationals. Which is why `buckingham_pi` works in `Rational` rather than in floating point. An exponent vector is *exactly* in the null space or it is not, and a group whose dimensions cancel to `1e-16` instead of to zero is not a dimensionless group -- it is a rounding error that will be reported as physics. The returned exponents are exact rationals for the same reason: the Reynolds number's exponents happen to be integers, but the null space basis of a general problem is not integral, and rounding it would silently change the group. The theorem says how many groups there are, not which ones. Any basis of the null space works, and the conventional groups -- Reynolds, Froude, Mach -- are particular choices made for physical reasons that the algebra knows nothing about. `dimensionless_groups_named` lists those conventions; `buckingham_pi` finds a basis and makes no claim that it is the one anybody would name. # Natural units are a change of bookkeeping, not of physics Setting `hbar = c = 1` makes length, time and mass powers of a single unit, conventionally energy: `[L] = [T] = [E]^-1` and `[M] = [E]`. Nothing physical changes -- the dimensionless combinations are the same -- but a quantity's dimension collapses to one integer, its energy power, and `natural_units_convert` returns the magnitude in `eV` to that power. Electromagnetic and thermal dimensions need further conventions to absorb, so a dimension involving amperes, kelvin, moles or candela is refused rather than guessed at. # Checking a formula is not the same as evaluating it `dimensional_check_formula` walks a symbolic expression and asks whether it is dimensionally coherent: that every term of every sum agrees, and that nothing dimensioned is handed to a sine or an exponential. Neither question can be answered by running the formula, because both sides of `x + v` are perfectly good floats. It is the check a physicist does by eye before believing an algebra step, done mechanically, and it catches the dropped factor that numerical testing cannot. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + +from numeria.units.quantity import Dim +from numeria.exact.symbolic import Expr + +def buckingham_pi(dims: list[Dim]) -> list[list[Fraction]]: + """ +A basis for the dimensionless groups of a set of quantities. + +Returns exactly `n - rank` vectors of `n` exact rational exponents. +The product of the quantities raised to those exponents is +dimensionless, exactly. + +Any basis of the null space is a valid answer and this one is +whichever the elimination produces; see the module note on why that +is not the same as producing the groups anybody has named. + +Errors: + +`DimError::Malformed` if given no quantities. + +Rust: `units::dimensional::buckingham_pi` + """ + ... + +def is_dimensionless_group(dims: list[Dim], exponents: list[Fraction]) -> bool: + """ +Checks that a vector of exponents really does cancel every dimension. + +Exact: the sum of each row is compared against zero, not against a +tolerance. + +Errors: + +`DimError::Malformed` if the lengths disagree. + +Rust: `units::dimensional::is_dimensionless_group` + """ + ... + +def dimensionless_groups_named() -> list[tuple[str, str, str]]: + """ +The dimensionless groups that have names, with their formulas and +what each compares. + +The formulas are the conventional ones. Each is *a* member of its +problem's null space rather than the only one -- see the module note. + +Rust: `units::dimensional::dimensionless_groups_named` + """ + ... + +def natural_units_power(dim: Dim) -> int: + """ +The power of energy a dimension corresponds to when `hbar = c = 1`. + +`[M] = [E]`, `[L] = [T] = [E]^-1`, so the power is +`kg - m - s`. + +Errors: + +`DimError::Mismatch` if the dimension involves amperes, kelvin, +moles or candela, which need further conventions to absorb and are +refused rather than guessed at. + +Rust: `units::dimensional::natural_units_power` + """ + ... + +def natural_units_convert(value: float, dim: Dim) -> float: + """ +Expresses an SI magnitude in electron volts to the power +`natural_units_power` gives. + +Errors: + +As `natural_units_power`. + +Rust: `units::dimensional::natural_units_convert` + """ + ... + +def planck_units() -> list[tuple[str, float, str]]: + """ +The Planck units, as `(name, value, unit)`. + +Each is built from `hbar`, `c` and `G` alone, which is the point: +they are the only combination of those three with the dimensions of a +length, a time, a mass and so on, so they are the scale at which +gravity and quantum mechanics are the same size. The defining +relations are checked in the tests against the CODATA values rather +than the numbers being copied in. + +Rust: `units::dimensional::planck_units` + """ + ... + +def dimensional_check_formula(expr: Expr, var_dims: list[tuple[str, Dim]]) -> Dim: + """ +The dimension of a symbolic expression, given the dimension of every +variable in it. + +This is the check a physicist runs before believing an algebra step, +done mechanically. It is worth having as code because the two rules +it enforces are the ones a hand derivation drops: + +* **Every term of a sum has to have the same dimension.** A length + plus a time is not a longer length, it is a mistake, and it is the + mistake a dropped factor produces. +* **A transcendental function's argument has to be dimensionless.** + `sin`, `exp` and `ln` are defined by their power series, and a + series adds `x` to `x^3` to `x^5`, so `x` can only be a pure + number. `exp(-t/tau)` is meaningful and `exp(-t)` is not, and the + difference is the missing timescale. + +Neither rule can be checked by evaluating the formula: both sides of +`x + v` are finite floats. They are properties of the expression, and +this walks the expression. + +`var_dims` maps each variable name to its dimension; the first +matching entry wins. Numeric literals are dimensionless. + +Exponents: + +`Pow(b, e)` needs `e` to be a literal number, because the dimension +of `b^e` depends on the *value* of `e` and not on its dimension. +When the base is dimensionless the exponent may be anything +dimensionless -- `2^n` is a pure number whatever `n` is -- but when +the base carries dimensions the exponent must be a literal -- an +`Expr::Rat` or an `Expr::Const` -- and the base's exponents must +all be divisible by the literal's denominator. + +A `Const` is read as the dyadic rational it exactly is, which needs +no guessing: `0.5` is one half, so `Pow(x, 0.5)` is a square root +and behaves like one. `0.1` is not one tenth, it is the +power-of-two fraction the float holds, and no dimension is +divisible by that denominator, so `l^0.1` is reported as a root +that does not exist rather than quietly rounded into one that +does. + +Errors: + +`DimError::Mismatch` when the terms of a sum disagree or a +transcendental is handed something dimensioned; +`DimError::UnknownVar` for a variable missing from `var_dims`; +`DimError::NotAPerfectRoot` for a root that does not come out +exactly; `DimError::Malformed` for an exponent that is not a +literal; `DimError::Overflow` if an exponent leaves `i8`. + +Examples: + +Rust: `units::dimensional::dimensional_check_formula` + """ + ... diff --git a/bindings/python/python/numeria/units/quantity.pyi b/bindings/python/python/numeria/units/quantity.pyi new file mode 100644 index 0000000..4bc7c2f --- /dev/null +++ b/bindings/python/python/numeria/units/quantity.pyi @@ -0,0 +1,205 @@ +""" +Values that carry their dimensions. # Why a number alone is not a measurement The two most expensive unit mistakes on record -- the Mars Climate Orbiter's pound-seconds fed to a newton-second interface, and the Gimli Glider's kilograms of fuel loaded as pounds -- were both arithmetic that a computer performed correctly on numbers that meant something other than what the receiving code assumed. Neither was a rounding error and neither would have been caught by testing the arithmetic. A `Quantity` carries seven small integers alongside its value: the exponents of metre, kilogram, second, ampere, kelvin, mole and candela. Addition then checks that the two exponent vectors agree and refuses if they do not, multiplication adds them, and taking a square root fails unless every one of them is even. None of this is approximate -- the exponents are integers and the checks are exact. # The gram is the prefixable unit, not the kilogram The SI base unit of mass is the kilogram, which is the only base unit whose name already contains a prefix. The prefix system therefore attaches to the *gram*: `mg` is a milligram and not a milli-kilogram, and `kg` parses here as kilo applied to gram. The unit table stores the gram at `1e-3`, which makes `kg` come out at exactly one and the oddity disappear. # Parsing a unit is ambiguous and the rule has to be stated `m` is both the metre and the milli prefix, `T` is both the tesla and tera, `min` starts with the milli prefix followed by `in`. The rule used is: try the whole token as a unit name first, and only if that fails split off a prefix. So `m` is a metre, `mm` is a millimetre, `min` is a minute, and `T` is a tesla. It is a rule rather than a deduction, and any other rule would give different answers for the same strings. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +class Dim: + """ +The seven SI base exponents. + +Rust: `units::quantity::Dim` + """ + def __init__(self, m: int, kg: int, s: int, a: int, k: int, mol: int, cd: int) -> None: ... + def exponents(self) -> list[int]: ... + def is_dimensionless(self) -> bool: ... + def mul(self, other: Dim) -> Dim: ... + def div(self, other: Dim) -> Dim: ... + def pow(self, n: int) -> Dim: ... + def sqrt(self) -> Dim: ... + @property + def m(self) -> int: ... + @property + def kg(self) -> int: ... + @property + def s(self) -> int: ... + @property + def a(self) -> int: ... + @property + def k(self) -> int: ... + @property + def mol(self) -> int: ... + @property + def cd(self) -> int: ... + +class Quantity: + """ +A value together with its dimension. + +Rust: `units::quantity::Quantity` + """ + @staticmethod + def number(v: float) -> Quantity: ... + def __init__(self, value: float, dim: Dim) -> None: ... + def add(self, other: Quantity) -> Quantity: ... + def sub(self, other: Quantity) -> Quantity: ... + def mul(self, other: Quantity) -> Quantity: ... + def div(self, other: Quantity) -> Quantity: ... + def pow(self, n: int) -> Quantity: ... + def sqrt(self) -> Quantity: ... + def to(self, unit: str) -> float: ... + def format_si(self) -> str: ... + @staticmethod + def meters(v: float) -> Quantity: ... + @staticmethod + def kilometers(v: float) -> Quantity: ... + @staticmethod + def millimeters(v: float) -> Quantity: ... + @staticmethod + def feet(v: float) -> Quantity: ... + @staticmethod + def inches(v: float) -> Quantity: ... + @staticmethod + def miles(v: float) -> Quantity: ... + @staticmethod + def kg(v: float) -> Quantity: ... + @staticmethod + def grams(v: float) -> Quantity: ... + @staticmethod + def pounds(v: float) -> Quantity: ... + @staticmethod + def seconds(v: float) -> Quantity: ... + @staticmethod + def minutes(v: float) -> Quantity: ... + @staticmethod + def hours(v: float) -> Quantity: ... + @staticmethod + def days(v: float) -> Quantity: ... + @staticmethod + def amperes(v: float) -> Quantity: ... + @staticmethod + def kelvin(v: float) -> Quantity: ... + @staticmethod + def moles(v: float) -> Quantity: ... + @staticmethod + def candela(v: float) -> Quantity: ... + @staticmethod + def hertz(v: float) -> Quantity: ... + @staticmethod + def newtons(v: float) -> Quantity: ... + @staticmethod + def pascals(v: float) -> Quantity: ... + @staticmethod + def joules(v: float) -> Quantity: ... + @staticmethod + def watts(v: float) -> Quantity: ... + @staticmethod + def coulombs(v: float) -> Quantity: ... + @staticmethod + def volts(v: float) -> Quantity: ... + @staticmethod + def farads(v: float) -> Quantity: ... + @staticmethod + def ohms(v: float) -> Quantity: ... + @staticmethod + def teslas(v: float) -> Quantity: ... + @staticmethod + def webers(v: float) -> Quantity: ... + @staticmethod + def henries(v: float) -> Quantity: ... + @staticmethod + def electron_volts(v: float) -> Quantity: ... + @staticmethod + def kilowatt_hours(v: float) -> Quantity: ... + @property + def value(self) -> float: ... + @property + def dim(self) -> Dim: ... + +def parse_unit(text: str) -> tuple[float, Dim]: + """ +Parses a unit expression such as `m/s^2`, `kg*m^2/s^3` or `J s`. + +Multiplication is written `*` or a space, and division `/`. A `/` +applies to the single term that follows it and nothing more, so +`J/mol/K` is joules per mole per kelvin. Parentheses are **not** +supported: `J/(mol K)` is rejected as an unknown unit rather than +quietly parsed as something else, which is the safer of the two ways +to not support them. + +Errors: + +`DimError::UnknownUnit` for an unrecognised name, or +`DimError::Malformed` for a broken exponent. + +Rust: `units::quantity::parse_unit` + """ + ... + +def parse_quantity(text: str) -> Quantity: + """ +Parses a quantity such as `"9.81 m/s^2"` or `"3 kWh"`. + +Errors: + +`DimError::Malformed` if there is no number, and whatever +`parse_unit` reports for the rest. + +Rust: `units::quantity::parse_quantity` + """ + ... + +def unit_convert(value: float, from_: str, to: str) -> float: + """ +Converts a value between two named units. + +Errors: + +`DimError::UnknownUnit` for an unrecognised name, or +`DimError::Mismatch` if the two measure different things -- which +is the whole point of the function rather than an edge case. + +Rust: `units::quantity::unit_convert` + """ + ... + +def si_prefixes_format(value: float) -> tuple[float, str]: + """ +Formats a number with the SI prefix that brings it into `[1, 1000)`. + +Returns the scaled number and the prefix, so that a caller can put +the unit after it. Zero and anything non-finite are returned with no +prefix, there being no sensible one. + +Rust: `units::quantity::si_prefixes_format` + """ + ... + +def constants_codata() -> list[tuple[str, float, str]]: + """ +The 2022 CODATA constants, as `(name, value, unit)`. + +Seven of these are exact by definition rather than measured: the +2019 revision of the SI fixed `c`, `h`, `e`, `k`, `N_A`, the +caesium hyperfine frequency and the luminous efficacy, and defined +the kilogram, ampere, kelvin, mole and candela in terms of them. The +gravitational constant is not among them and remains the worst known +of the fundamental constants by a wide margin -- about one part in +forty thousand, against one part in `1e10` for the fine-structure +constant. + +Rust: `units::quantity::constants_codata` + """ + ... + +def codata(name: str) -> Optional[float]: + """ +Looks a CODATA constant up by name. + +Rust: `units::quantity::codata` + """ + ... diff --git a/bindings/python/python/numeria/vector_calculus.pyi b/bindings/python/python/numeria/vector_calculus.pyi new file mode 100644 index 0000000..f9f4fa3 --- /dev/null +++ b/bindings/python/python/numeria/vector_calculus.pyi @@ -0,0 +1,149 @@ +""" +Vector calculus operators and field theory for physics grids. Provides discrete differential operators (gradient, laplacian, divergence, curl) on 2D and 3D uniform grids, point-wise numerical differentiation via function pointers, line/surface integrals, and a Jacobi Poisson solver. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def gradient_3d(field: list[float], nx: int, ny: int, nz: int, dx: float, dy: float, dz: float) -> list[tuple[float, float, float]]: + """ +Compute the gradient of a scalar field on a uniform 3-D grid. + +`field` is row-major with index mapping `i*ny*nz + j*nz + k`. +Returns a `Vec` of `(∂f/∂x, ∂f/∂y, ∂f/∂z)` tuples, one per grid point. + +Rust: `vector_calculus::gradient_3d` + """ + ... + +def laplacian_3d(field: list[float], nx: int, ny: int, nz: int, dx: float, dy: float, dz: float) -> list[float]: + """ +Compute the Laplacian (∇²f) of a scalar field on a uniform 3-D grid. + +Rust: `vector_calculus::laplacian_3d` + """ + ... + +def divergence_3d(fx: list[float], fy: list[float], fz: list[float], nx: int, ny: int, nz: int, dx: float, dy: float, dz: float) -> list[float]: + """ +Divergence of a vector field on a uniform 3-D grid. + +Each component (`fx`, `fy`, `fz`) is a flat array with the same index mapping +as scalar fields. + +Rust: `vector_calculus::divergence_3d` + """ + ... + +def curl_3d(fx: list[float], fy: list[float], fz: list[float], nx: int, ny: int, nz: int, dx: float, dy: float, dz: float) -> tuple[list[float], list[float], list[float]]: + """ +Curl of a vector field on a uniform 3-D grid. + +Returns `(curl_x, curl_y, curl_z)` as separate flat arrays. + +Rust: `vector_calculus::curl_3d` + """ + ... + +def gradient_2d(field: list[float], nx: int, ny: int, dx: float, dy: float) -> list[tuple[float, float]]: + """ +Gradient of a scalar field on a uniform 2-D grid. + +Index mapping: `i*ny + j`. Returns `(∂f/∂x, ∂f/∂y)` per grid point. + +Rust: `vector_calculus::gradient_2d` + """ + ... + +def laplacian_2d(field: list[float], nx: int, ny: int, dx: float, dy: float) -> list[float]: + """ +Laplacian of a scalar field on a uniform 2-D grid. + +Rust: `vector_calculus::laplacian_2d` + """ + ... + +def divergence_2d(fx: list[float], fy: list[float], nx: int, ny: int, dx: float, dy: float) -> list[float]: + """ +Divergence of a 2-D vector field. + +Rust: `vector_calculus::divergence_2d` + """ + ... + +def curl_2d(fx: list[float], fy: list[float], nx: int, ny: int, dx: float, dy: float) -> list[float]: + """ +Scalar curl of a 2-D vector field: ∂Fy/∂x - ∂Fx/∂y. + +Rust: `vector_calculus::curl_2d` + """ + ... + +def poisson_jacobi_2d(rhs: list[float], nx: int, ny: int, dx: float, dy: float, max_iter: int, tol: float) -> list[float]: + """ +Solve ∇²φ = ρ on a 2-D grid with zero (Dirichlet) boundary +conditions. + +The interior unknowns are assembled into a sparse SPD system +(−∇²φ = −ρ, 5-point stencil) and solved with Jacobi-preconditioned +conjugate gradient (`linalg::sparse::pcg_jacobi`); if CG does not +reach `tol` within `max_iter` iterations, the classic point-Jacobi +sweep is used as a fallback so the historical behavior (best-effort +answer, never an error) is preserved. + +Rust: `vector_calculus::poisson_jacobi_2d` + """ + ... + +def line_integral(f: Callable[[float, float, float], tuple[float, float, float]], path: list[tuple[float, float, float]]) -> float: + """ +Numerical line integral ∫F·dr along a piecewise-linear path in 3-D. + +`f` returns the vector field value (Fx, Fy, Fz) at a given point. +`path` is an ordered list of waypoints. The integral is evaluated at the +midpoint of each segment. + +Rust: `vector_calculus::line_integral` + """ + ... + +def flux_integral_2d(fn_field: Callable[[float, float], tuple[float, float]], path: list[tuple[float, float]]) -> float: + """ +Flux integral ∫F·n̂ ds along a 2-D curve. + +The outward normal is computed by rotating each segment's tangent 90 degrees +clockwise: tangent (tx, ty) -> normal (ty, -tx). The integral is evaluated +at the midpoint of each segment. + +Rust: `vector_calculus::flux_integral_2d` + """ + ... + +def numerical_gradient(f: Callable[[float, float, float], float], x: float, y: float, z: float, h: float) -> tuple[float, float, float]: + """ +Finite-difference gradient of a scalar function at a point. + +Rust: `vector_calculus::numerical_gradient` + """ + ... + +def numerical_laplacian(f: Callable[[float, float, float], float], x: float, y: float, z: float, h: float) -> float: + """ +Finite-difference Laplacian of a scalar function at a point. + +Rust: `vector_calculus::numerical_laplacian` + """ + ... + +def numerical_divergence(fx: Callable[[float, float, float], float], fy: Callable[[float, float, float], float], fz: Callable[[float, float, float], float], x: float, y: float, z: float, h: float) -> float: + """ +Finite-difference divergence of a vector field at a point. + +Each component of the field is given as a separate function. + +Rust: `vector_calculus::numerical_divergence` + """ + ... diff --git a/bindings/python/python/numeria/waves.pyi b/bindings/python/python/numeria/waves.pyi new file mode 100644 index 0000000..2717633 --- /dev/null +++ b/bindings/python/python/numeria/waves.pyi @@ -0,0 +1,402 @@ +""" +Wave propagation: mechanical, acoustic and seismic. The kinematic relations (`v = fλ` and the wavenumber-frequency pair), displacement, energy density and intensity, and the inverse-square falloff of a spherical wave. The Doppler effect in both classical and relativistic forms, with the Mach cone angle for supersonic sources. Standing waves on strings and in open and closed pipes, beats, and superposition. Boundaries are handled by impedance: the reflection and transmission coefficients follow from the impedance mismatch, which is also why they carry a sign. Acoustics covers the speed of sound in a gas, sound pressure level and the decibel scale, and absorption and penetration depth. Seismology covers P-, S-, Rayleigh and Love wave speeds. Diffraction closes with the Fraunhofer single-slit pattern, the Airy disk radius and the Fresnel number. +""" +# @generated by bindings/python/generate.py -- do not edit. +from __future__ import annotations +from collections.abc import Callable, Sequence +from fractions import Fraction +from typing import Any, Optional + + +def wave_speed(frequency: float, wavelength: float) -> float: + """ +Wave speed: v = f * λ + +Rust: `waves::wave_speed` + """ + ... + +def wavelength(speed: float, frequency: float) -> float: + """ +Wavelength from speed and frequency: λ = v / f + +Rust: `waves::wavelength` + """ + ... + +def frequency(speed: float, wavelength: float) -> float: + """ +Frequency from speed and wavelength: f = v / λ + +Rust: `waves::frequency` + """ + ... + +def period(frequency: float) -> float: + """ +Period: T = 1 / f + +Rust: `waves::period` + """ + ... + +def angular_frequency(frequency: float) -> float: + """ +Angular frequency: ω = 2πf + +Rust: `waves::angular_frequency` + """ + ... + +def wave_number(wavelength: float) -> float: + """ +Wave number: k = 2π / λ + +Rust: `waves::wave_number` + """ + ... + +def wave_displacement(amplitude: float, wave_number: float, x: float, angular_freq: float, t: float, phase: float) -> float: + """ +Transverse wave displacement: y(x,t) = A * sin(kx - ωt + φ) + +Rust: `waves::wave_displacement` + """ + ... + +def wave_energy_density(amplitude: float, frequency: float, linear_density: float) -> float: + """ +Energy of a wave (proportional): E ∝ A^2 * f^2 +Returns the energy for a given amplitude and frequency (with a constant factor). + +Rust: `waves::wave_energy_density` + """ + ... + +def wave_intensity(power: float, area: float) -> float: + """ +Intensity of a wave: I = P / A (power per unit area) + +Rust: `waves::wave_intensity` + """ + ... + +def spherical_wave_intensity(power: float, distance: float) -> float: + """ +Intensity falls off with distance (spherical wave): I = P / (4πr^2) + +Rust: `waves::spherical_wave_intensity` + """ + ... + +def decibel_level(intensity: float, reference_intensity: float) -> float: + """ +Decibel level: β = 10 * log10(I / I_0) + +Rust: `waves::decibel_level` + """ + ... + +def intensity_from_decibels(decibels: float, reference_intensity: float) -> float: + """ +Intensity from decibel level: I = I_0 * 10^(β/10) + +Rust: `waves::intensity_from_decibels` + """ + ... + +def doppler_frequency(source_freq: float, wave_speed: float, observer_velocity: float, source_velocity: float) -> float: + """ +Doppler effect (sound): f' = f * (v + v_observer) / (v + v_source) +Convention: positive v_observer = observer moving toward source, +positive v_source = source moving away from observer. + +Rust: `waves::doppler_frequency` + """ + ... + +def relativistic_doppler(source_freq: float, beta: float) -> float: + """ +Relativistic Doppler effect: f' = f * sqrt((1 + β) / (1 - β)) +where β = v/c, positive β = approaching. + +Rust: `waves::relativistic_doppler` + """ + ... + +def mach_cone_angle(mach: float) -> float: + """ +Mach cone half-angle: sin(θ) = v_sound / v_object = 1/M + +Rust: `waves::mach_cone_angle` + """ + ... + +def standing_wave_frequency(harmonic: int, wave_speed: float, length: float) -> float: + """ +Frequencies of standing waves on a string fixed at both ends: +f_n = n * v / (2L) + +Rust: `waves::standing_wave_frequency` + """ + ... + +def string_fundamental(length: float, tension: float, linear_density: float) -> float: + """ +Fundamental frequency of a string: f = (1/(2L)) * sqrt(T/μ) +T = tension, μ = linear mass density + +Rust: `waves::string_fundamental` + """ + ... + +def open_pipe_frequency(harmonic: int, sound_speed: float, length: float) -> float: + """ +Standing waves in an open pipe: f_n = n * v / (2L) (all harmonics) + +Rust: `waves::open_pipe_frequency` + """ + ... + +def closed_pipe_frequency(odd_harmonic: int, sound_speed: float, length: float) -> float: + """ +Standing waves in a closed pipe: f_n = n * v / (4L) (odd harmonics only) + +Rust: `waves::closed_pipe_frequency` + """ + ... + +def beat_frequency(f1: float, f2: float) -> float: + """ +Beat frequency: f_beat = |f1 - f2| + +Rust: `waves::beat_frequency` + """ + ... + +def superposition_amplitude(a1: float, a2: float, phase_diff: float) -> float: + """ +Superposition of two waves at a point (same frequency): +A_resultant = sqrt(A1^2 + A2^2 + 2*A1*A2*cos(Δφ)) + +Rust: `waves::superposition_amplitude` + """ + ... + +def speed_of_sound_gas(gamma: float, temperature: float, molar_mass: float) -> float: + """ +Speed of sound in an ideal gas: v = sqrt(γ * R * T / M) +γ = heat capacity ratio, M = molar mass + +Rust: `waves::speed_of_sound_gas` + """ + ... + +def wave_speed_string(tension: float, linear_density: float) -> float: + """ +Wave speed on a string: v = sqrt(T / μ) + +Rust: `waves::wave_speed_string` + """ + ... + +def phase_velocity(angular_freq: float, wave_number: float) -> float: + """ +Phase velocity: v_p = ω/k + +Rust: `waves::phase_velocity` + """ + ... + +def group_velocity(d_omega: float, d_k: float) -> float: + """ +Group velocity: v_g = dω/dk + +Rust: `waves::group_velocity` + """ + ... + +def wave_impedance(density: float, wave_speed: float) -> float: + """ +Acoustic impedance: Z = ρv + +Rust: `waves::wave_impedance` + """ + ... + +def reflection_coefficient(z1: float, z2: float) -> float: + """ +Amplitude reflection coefficient: R = (Z2 - Z1)/(Z2 + Z1) + +Rust: `waves::reflection_coefficient` + """ + ... + +def transmission_coefficient(z1: float, z2: float) -> float: + """ +Amplitude transmission coefficient: T = 2Z2/(Z1 + Z2) + +Rust: `waves::transmission_coefficient` + """ + ... + +def intensity_reflection(z1: float, z2: float) -> float: + """ +Intensity reflection coefficient: R_I = ((Z2 - Z1)/(Z2 + Z1))² + +Rust: `waves::intensity_reflection` + """ + ... + +def intensity_transmission(z1: float, z2: float) -> float: + """ +Intensity transmission coefficient: T_I = 4Z1Z2/(Z1 + Z2)² + +Rust: `waves::intensity_transmission` + """ + ... + +def attenuated_amplitude(initial: float, attenuation_coeff: float, distance: float) -> float: + """ +Attenuated amplitude: A = A₀ × e^(-αx) + +Rust: `waves::attenuated_amplitude` + """ + ... + +def absorption_coefficient_from_db(db_per_meter: float) -> float: + """ +Absorption coefficient from dB/m: α = dB × ln(10)/20 + +Rust: `waves::absorption_coefficient_from_db` + """ + ... + +def penetration_depth(attenuation_coeff: float) -> float: + """ +Penetration depth (skin depth): δ = 1/α + +Rust: `waves::penetration_depth` + """ + ... + +def sound_pressure_level(pressure: float, reference: float) -> float: + """ +Sound pressure level: SPL = 20 × log10(p/p_ref) + +Rust: `waves::sound_pressure_level` + """ + ... + +def acoustic_power(pressure: float, area: float, impedance: float) -> float: + """ +Acoustic power: P = p²A/Z + +Rust: `waves::acoustic_power` + """ + ... + +def resonant_frequency_tube_open(length: float, speed: float) -> float: + """ +Resonant frequency of open tube: f = v/(2L) + +Rust: `waves::resonant_frequency_tube_open` + """ + ... + +def resonant_frequency_tube_closed(length: float, speed: float) -> float: + """ +Resonant frequency of closed tube: f = v/(4L) + +Rust: `waves::resonant_frequency_tube_closed` + """ + ... + +def acoustic_intensity_from_pressure(pressure: float, impedance: float) -> float: + """ +Acoustic intensity from pressure: I = p²/Z + +Rust: `waves::acoustic_intensity_from_pressure` + """ + ... + +def wavelength_in_medium(frequency: float, speed_in_medium: float) -> float: + """ +Wavelength in a medium: λ = v/f + +Rust: `waves::wavelength_in_medium` + """ + ... + +def p_wave_speed(bulk_modulus: float, shear_modulus: float, density: float) -> float: + """ +P-wave speed: vp = √((K + 4G/3)/ρ) + +Rust: `waves::p_wave_speed` + """ + ... + +def s_wave_speed(shear_modulus: float, density: float) -> float: + """ +S-wave speed: vs = √(G/ρ) + +Rust: `waves::s_wave_speed` + """ + ... + +def rayleigh_wave_speed(shear_speed: float, poisson_ratio: float) -> float: + """ +Rayleigh wave speed approximation: vR ≈ vs × (0.862 + 1.14ν)/(1 + ν) + +Rust: `waves::rayleigh_wave_speed` + """ + ... + +def love_wave_speed_range(shear_speed_layer: float, shear_speed_halfspace: float) -> tuple[float, float]: + """ +Love wave speed range: between vs_layer and vs_halfspace + +Rust: `waves::love_wave_speed_range` + """ + ... + +def path_difference_constructive(order: int, wavelength: float) -> float: + """ +Constructive interference path difference: Δ = mλ + +Rust: `waves::path_difference_constructive` + """ + ... + +def path_difference_destructive(order: int, wavelength: float) -> float: + """ +Destructive interference path difference: Δ = (m + 0.5)λ + +Rust: `waves::path_difference_destructive` + """ + ... + +def fraunhofer_single_slit_intensity(angle: float, slit_width: float, wavelength: float) -> float: + """ +Fraunhofer single-slit intensity: I/I₀ = (sin(β)/β)² where β = πa sin(θ)/λ +Returns 1.0 at θ = 0 (central maximum). + +Rust: `waves::fraunhofer_single_slit_intensity` + """ + ... + +def airy_disk_radius(wavelength: float, focal_length: float, aperture: float) -> float: + """ +Airy disk radius: r = 1.22λf/D + +Rust: `waves::airy_disk_radius` + """ + ... + +def fresnel_number(aperture: float, distance: float, wavelength: float) -> float: + """ +Fresnel number: F = a²/(λL) + +Rust: `waves::fresnel_number` + """ + ... diff --git a/bindings/python/rustscan.py b/bindings/python/rustscan.py new file mode 100644 index 0000000..95866fe --- /dev/null +++ b/bindings/python/rustscan.py @@ -0,0 +1,944 @@ +"""A small Rust source reader: enough of the grammar to list a crate's public API. + +This is not a Rust parser. It is a scanner that knows where the code is -- +that is, which byte offsets are inside a string, a character literal or a +comment -- and can therefore match braces reliably, and on top of that a +walker that recognises the handful of item forms this crate actually uses: +modules, `use`, structs, enums, functions, `impl` blocks, constants and +type aliases. + +Being a scanner rather than a parser is a deliberate limit. It cannot +resolve a macro, and it does not try to; a generic bound spanning several +lines it records verbatim rather than interpreting. Everything it cannot +place it reports as unhandled instead of guessing, so the generator that +consumes it can decide what to skip and say why. +""" + +from __future__ import annotations + +import os +import re +from dataclasses import dataclass, field +from typing import Iterator + + +# ── Masking: which offsets are code ───────────────────────────────────── + + +def code_mask(src: str) -> bytearray: + """Return a byte-per-character mask: 1 where `src` is code, 0 elsewhere. + + Doc comments (`///`, `//!`, `/** */`, `/*! */`) are masked out like any + other comment; they are recovered separately by `doc_before`, which + reads the raw text. Masking them keeps a `{` inside a doctest from + unbalancing an item's braces. + """ + n = len(src) + mask = bytearray(b"\x01" * n) + i = 0 + while i < n: + c = src[i] + if c == "/" and i + 1 < n and src[i + 1] == "/": + j = src.find("\n", i) + j = n if j < 0 else j + for k in range(i, j): + mask[k] = 0 + i = j + elif c == "/" and i + 1 < n and src[i + 1] == "*": + # Rust block comments nest. + depth = 1 + j = i + 2 + while j < n and depth: + if src[j] == "/" and j + 1 < n and src[j + 1] == "*": + depth += 1 + j += 2 + elif src[j] == "*" and j + 1 < n and src[j + 1] == "/": + depth -= 1 + j += 2 + else: + j += 1 + for k in range(i, min(j, n)): + mask[k] = 0 + i = j + elif c == "r" and _raw_string_start(src, i): + hashes = 0 + j = i + 1 + while j < n and src[j] == "#": + hashes += 1 + j += 1 + close = '"' + "#" * hashes + end = src.find(close, j + 1) + end = n if end < 0 else end + len(close) + for k in range(i, end): + mask[k] = 0 + i = end + elif c == '"': + j = i + 1 + while j < n: + if src[j] == "\\": + j += 2 + continue + if src[j] == '"': + j += 1 + break + j += 1 + for k in range(i, min(j, n)): + mask[k] = 0 + i = j + elif c == "'": + # A quote is either a char literal or a lifetime. Only the + # literal forms hide code. + m = re.match(r"'(?:\\.|[^\\'])'", src[i : i + 8]) + if m: + for k in range(i, i + m.end()): + mask[k] = 0 + i += m.end() + else: + i += 1 + else: + i += 1 + return mask + + +def _raw_string_start(src: str, i: int) -> bool: + j = i + 1 + while j < len(src) and src[j] == "#": + j += 1 + return j < len(src) and src[j] == '"' + + +def match_brace(src: str, mask: bytearray, open_at: int) -> int: + """Index just past the `}` closing the `{` at `open_at`.""" + pairs = {"{": "}", "(": ")", "[": "]"} + closer = pairs[src[open_at]] + opener = src[open_at] + depth = 0 + i = open_at + n = len(src) + while i < n: + if mask[i]: + if src[i] == opener: + depth += 1 + elif src[i] == closer: + depth -= 1 + if depth == 0: + return i + 1 + i += 1 + return n + + +def find_code(src: str, mask: bytearray, ch: str, start: int, end: int) -> int: + for i in range(start, min(end, len(src))): + if mask[i] and src[i] == ch: + return i + return -1 + + +# ── Items ─────────────────────────────────────────────────────────────── + + +@dataclass +class Field: + name: str + ty: str + public: bool + + +@dataclass +class Variant: + name: str + payload: str # "" for a unit variant, else the tuple/struct body + + +@dataclass +class Struct: + name: str + module: str + file: str + doc: str + attrs: list[str] + fields: list[Field] + kind: str # "named" | "tuple" | "unit" + generics: str + + @property + def path(self) -> str: + return f"{self.module}::{self.name}" if self.module else self.name + + def derives(self) -> set[str]: + out: set[str] = set() + for a in self.attrs: + m = re.match(r"derive\((.*)\)$", a, re.S) + if m: + out |= {x.strip() for x in m.group(1).split(",") if x.strip()} + return out + + +@dataclass +class Enum: + name: str + module: str + file: str + doc: str + attrs: list[str] + variants: list[Variant] + generics: str + + @property + def path(self) -> str: + return f"{self.module}::{self.name}" if self.module else self.name + + def derives(self) -> set[str]: + out: set[str] = set() + for a in self.attrs: + m = re.match(r"derive\((.*)\)$", a, re.S) + if m: + out |= {x.strip() for x in m.group(1).split(",") if x.strip()} + return out + + +@dataclass +class Func: + name: str + module: str + file: str + doc: str + attrs: list[str] + args: list[tuple[str, str]] # (pattern, type); `self` appears as ("self", "") + ret: str + generics: str + where_clause: str + self_kind: str # "" | "self" | "&self" | "&mut self" + impl_type: str # "" for a free function, else the receiver type as written + impl_trait: str # non-empty when the impl block implements a trait + is_const: bool + is_unsafe: bool + + @property + def is_method(self) -> bool: + return bool(self.impl_type) + + +@dataclass +class Const: + name: str + module: str + file: str + doc: str + ty: str + value: str + owner: str = "" # the impl type, for an associated constant + + @property + def path(self) -> str: + return f"{self.module}::{self.name}" if self.module else self.name + + +@dataclass +class Alias: + name: str + module: str + target: str + + +@dataclass +class Crate: + structs: list[Struct] = field(default_factory=list) + enums: list[Enum] = field(default_factory=list) + funcs: list[Func] = field(default_factory=list) + consts: list[Const] = field(default_factory=list) + aliases: list[Alias] = field(default_factory=list) + traits: set[str] = field(default_factory=set) + # file -> {short name: full path} + uses: dict[str, dict[str, str]] = field(default_factory=dict) + # file -> [module paths brought in by a glob import] + glob_uses: dict[str, list[str]] = field(default_factory=dict) + # module path -> {name exposed there: the path it re-exports} + pub_uses: dict[str, dict[str, str]] = field(default_factory=dict) + # module path -> the `//!` summary + module_docs: dict[str, str] = field(default_factory=dict) + unhandled: list[str] = field(default_factory=list) + + +ATTR_RE = re.compile(r"#!?\[") +DOC_LINE_RE = re.compile(r"^\s*//[/!] ?(.*)$") + + +def _doc_and_attrs(src: str, mask: bytearray, start: int, stop: int) -> tuple[str, list[str], int]: + """Read doc comments and attributes ending at `stop`, scanning from `start`. + + Returns the doc text, the attribute bodies, and the offset where the + item's own tokens begin. + """ + docs: list[str] = [] + attrs: list[str] = [] + i = start + while i < stop: + # Whitespace. + if src[i].isspace(): + i += 1 + continue + # Comment (doc or not). + if src[i] == "/" and i + 1 < stop and src[i + 1] == "/": + j = src.find("\n", i) + j = stop if j < 0 else j + line = src[i:j] + m = re.match(r"//[/!] ?(.*)$", line) + if m: + docs.append(m.group(1)) + elif not line.startswith("////"): + docs.clear() if False else None + i = j + 1 + continue + if src[i] == "/" and i + 1 < stop and src[i + 1] == "*": + depth, j = 1, i + 2 + while j < stop and depth: + if src[j : j + 2] == "/*": + depth += 1 + j += 2 + elif src[j : j + 2] == "*/": + depth -= 1 + j += 2 + else: + j += 1 + i = j + continue + if src[i] == "#": + m = ATTR_RE.match(src, i) + if m: + open_at = m.end() - 1 + close = match_brace(src, mask, open_at) + attrs.append(src[open_at + 1 : close - 1].strip()) + i = close + continue + break + return "\n".join(docs).strip(), attrs, i + + +def _split_top(text: str) -> list[str]: + """Split on commas that are not nested inside <>, (), [] or {}.""" + out, depth, cur = [], 0, "" + angle = 0 + i = 0 + while i < len(text): + c = text[i] + if c in "([{": + depth += 1 + elif c in ")]}": + depth -= 1 + elif c == "<": + angle += 1 + elif c == ">": + # `->` is not a closing angle bracket. + if i and text[i - 1] == "-": + pass + else: + angle -= 1 + if c == "," and depth == 0 and angle <= 0: + out.append(cur.strip()) + cur = "" + else: + cur += c + i += 1 + if cur.strip(): + out.append(cur.strip()) + return out + + +def _split_generics(text: str, start: int) -> tuple[str, int]: + """Read a `<...>` generic list starting at `start`; return it and the end.""" + if start >= len(text) or text[start] != "<": + return "", start + depth, i = 0, start + while i < len(text): + if text[i] == "<": + depth += 1 + elif text[i] == ">": + depth -= 1 + if depth == 0: + return text[start : i + 1], i + 1 + elif text[i] == "-" and i + 1 < len(text) and text[i + 1] == ">": + i += 1 + i += 1 + return text[start:], len(text) + + +def parse_file(path: str, module: str, crate: Crate) -> None: + with open(path, encoding="utf-8") as fh: + src = fh.read() + mask = code_mask(src) + crate.uses.setdefault(path, {}) + crate.glob_uses.setdefault(path, []) + # The `//!` header, taken before anything masks it away. + header: list[str] = [] + for line in src.splitlines(): + s = line.strip() + if s.startswith("//!"): + header.append(s[3:].strip()) + elif s and not s.startswith("//"): + break + if header: + crate.module_docs[module] = " ".join(header).strip() + _walk(src, mask, 0, len(src), module, path, crate, in_impl=None) + + +def _walk( + src: str, + mask: bytearray, + start: int, + end: int, + module: str, + path: str, + crate: Crate, + in_impl, +) -> None: + i = start + while i < end: + doc, attrs, i = _doc_and_attrs(src, mask, i, end) + if i >= end: + return + # Where does this item's head end? At `{`, `(`, `;` or `=`. + head_start = i + rest = src[i:end] + if not rest.strip(): + return + + cfg_test = any(a.startswith("cfg(test") or a == "test" for a in attrs) + cfg_kani = any("kani" in a for a in attrs) + hidden = any(a.startswith("doc(hidden") for a in attrs) + + tok = re.match(r"[A-Za-z_][A-Za-z0-9_]*|.", rest) + word = tok.group(0) if tok else "" + + # Visibility prefix. + vis = "" + vm = re.match(r"pub(\s*\([^)]*\))?\s+", rest) + if vm: + vis = "pub" if not vm.group(1) else f"pub{vm.group(1).strip()}" + after_vis = i + vm.end() + else: + after_vis = i + body = src[after_vis:end] + kw = re.match( + r"(unsafe\s+|const\s+|async\s+|extern\s+\"[^\"]*\"\s+|default\s+)*" + r"(mod|use|struct|enum|fn|impl|const|static|type|trait|union|macro_rules)\b", + body, + ) + if not kw: + # Something we do not recognise: skip to the next top-level + # boundary so one odd item cannot derail the rest of the file. + nxt = _skip_item(src, mask, i, end) + if nxt <= i: + return + snippet = src[i:nxt].strip().splitlines() + if snippet: + crate.unhandled.append(f"{path}: {snippet[0][:70]}") + i = nxt + continue + + prefixes = kw.group(0)[: kw.start(2)] + kind = kw.group(2) + after_kw = after_vis + kw.end() + + if kind == "use": + semi = find_code(src, mask, ";", after_kw, end) + if semi < 0: + return + _record_use(src[after_kw:semi], module, path, crate, vis == "pub") + i = semi + 1 + continue + + if kind == "mod": + m = re.match(r"\s*([A-Za-z_][A-Za-z0-9_]*)\s*", src[after_kw:end]) + if not m: + i = _skip_item(src, mask, i, end) + continue + name = m.group(1) + j = after_kw + m.end() + if j < end and src[j] == ";": + i = j + 1 + continue + if j < end and src[j] == "{": + close = match_brace(src, mask, j) + if not (cfg_test or cfg_kani or name == "tests"): + sub = f"{module}::{name}" if module else name + inner_doc: list[str] = [] + for line in src[j + 1 : close].splitlines(): + s = line.strip() + if s.startswith("//!"): + inner_doc.append(s[3:].strip()) + elif s and not s.startswith("//"): + break + if inner_doc: + crate.module_docs[sub] = " ".join(inner_doc) + elif doc: + crate.module_docs[sub] = doc.splitlines()[0] + if vis == "pub": + _walk(src, mask, j + 1, close - 1, sub, path, crate, in_impl) + i = close + continue + i = _skip_item(src, mask, i, end) + continue + + if kind in ("struct", "union"): + i = _parse_struct( + src, mask, after_kw, end, module, path, crate, doc, attrs, vis, cfg_test or hidden + ) + continue + + if kind == "enum": + i = _parse_enum( + src, mask, after_kw, end, module, path, crate, doc, attrs, vis, cfg_test or hidden + ) + continue + + if kind == "trait": + m = re.match(r"\s*([A-Za-z_][A-Za-z0-9_]*)", src[after_kw:end]) + if m: + crate.traits.add(m.group(1)) + i = _skip_item(src, mask, i, end) + continue + + if kind == "fn": + i = _parse_fn( + src, + mask, + after_kw, + end, + module, + path, + crate, + doc, + attrs, + vis, + prefixes, + in_impl, + cfg_test or hidden, + ) + continue + + if kind == "impl": + i = _parse_impl(src, mask, after_kw, end, module, path, crate, cfg_test or cfg_kani) + continue + + if kind in ("const", "static"): + m = re.match(r"\s*(?:mut\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*:\s*", src[after_kw:end]) + eq = find_code(src, mask, "=", after_kw, end) + semi = _stmt_end(src, mask, after_kw, end) + if m and vis == "pub" and not cfg_test and eq > 0: + ty = src[after_kw + m.end() : eq].strip() + crate.consts.append( + Const( + name=m.group(1), + module=module, + file=path, + doc=doc, + ty=ty, + value=src[eq + 1 : semi].strip(), + owner=in_impl[0] if in_impl else "", + ) + ) + i = semi + 1 + continue + + if kind == "type": + m = re.match(r"\s*([A-Za-z_][A-Za-z0-9_]*)", src[after_kw:end]) + eq = find_code(src, mask, "=", after_kw, end) + semi = _stmt_end(src, mask, after_kw, end) + if m and eq > 0 and eq < semi: + crate.aliases.append( + Alias(name=m.group(1), module=module, target=src[eq + 1 : semi].strip()) + ) + i = semi + 1 + continue + + i = _skip_item(src, mask, i, end) + + +def _stmt_end(src: str, mask: bytearray, start: int, end: int) -> int: + """Offset of the `;` ending a statement, skipping balanced brackets.""" + i = start + while i < end: + if mask[i]: + if src[i] in "{([": + i = match_brace(src, mask, i) + continue + if src[i] == ";": + return i + i += 1 + return end + + +def _skip_item(src: str, mask: bytearray, start: int, end: int) -> int: + """Skip one item: to the end of its block, or to its terminating `;`.""" + i = start + while i < end: + if mask[i]: + if src[i] == "{": + return match_brace(src, mask, i) + if src[i] == ";": + return i + 1 + if src[i] in "([": + i = match_brace(src, mask, i) + continue + i += 1 + return end + + +def _record_use(text: str, module: str, path: str, crate: Crate, is_pub: bool = False) -> None: + text = " ".join(text.split()) + if is_pub: + crate.pub_uses.setdefault(module, {}) + + def expand(prefix: str, body: str) -> None: + body = body.strip() + if body.startswith("{") and body.endswith("}"): + for part in _split_top(body[1:-1]): + expand(prefix, part) + return + if "::{" in body: + head, tail = body.split("::{", 1) + expand(f"{prefix}::{head}".strip(":"), "{" + tail) + return + if body == "*": + crate.glob_uses[path].append(_normalise(prefix, module)) + return + if " as " in body: + target, alias = body.split(" as ", 1) + full = _normalise(f"{prefix}::{target}".strip(":"), module) + crate.uses[path][alias.strip()] = full + if is_pub: + crate.pub_uses[module][alias.strip()] = full + return + if not body: + return + full = _normalise(f"{prefix}::{body}".strip(":"), module) + crate.uses[path][body.split("::")[-1]] = full + if is_pub: + crate.pub_uses[module][body.split("::")[-1]] = full + + expand("", text) + + +def _normalise(p: str, module: str) -> str: + p = p.strip().strip(":") + if p.startswith("crate::"): + return p[len("crate::") :] + if p.startswith("self::"): + return f"{module}::{p[len('self::'):]}" if module else p[len("self::") :] + if p.startswith("super::"): + parent = "::".join(module.split("::")[:-1]) + rest = p[len("super::") :] + while rest.startswith("super::"): + parent = "::".join(parent.split("::")[:-1]) + rest = rest[len("super::") :] + return f"{parent}::{rest}" if parent else rest + return p + + +def _parse_struct(src, mask, i, end, module, path, crate, doc, attrs, vis, skip) -> int: + m = re.match(r"\s*([A-Za-z_][A-Za-z0-9_]*)", src[i:end]) + if not m: + return _skip_item(src, mask, i, end) + name = m.group(1) + j = i + m.end() + generics, j = _split_generics(src[:end], j) + while j < end and src[j].isspace(): + j += 1 + fields: list[Field] = [] + if j < end and src[j] == "{": + close = match_brace(src, mask, j) + inner = src[j + 1 : close - 1] + imask = mask[j + 1 : close - 1] + for part in _split_fields(inner, imask): + fm = re.match(r"\s*((?:pub(?:\s*\([^)]*\))?\s+)?)([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(.+)$", part, re.S) + if fm: + fields.append( + Field( + name=fm.group(2), + ty=" ".join(fm.group(3).split()), + public=fm.group(1).strip().startswith("pub"), + ) + ) + kind = "named" + nxt = close + elif j < end and src[j] == "(": + close = match_brace(src, mask, j) + inner = src[j + 1 : close - 1] + for k, part in enumerate(_split_top(inner)): + pm = re.match(r"\s*((?:pub(?:\s*\([^)]*\))?\s+)?)(.+)$", part, re.S) + if pm: + fields.append( + Field( + name=str(k), + ty=" ".join(pm.group(2).split()), + public=pm.group(1).strip().startswith("pub"), + ) + ) + kind = "tuple" + nxt = _stmt_end(src, mask, close, end) + 1 + else: + kind = "unit" + nxt = _stmt_end(src, mask, j, end) + 1 + if vis == "pub" and not skip: + crate.structs.append( + Struct( + name=name, + module=module, + file=path, + doc=doc, + attrs=attrs, + fields=fields, + kind=kind, + generics=generics, + ) + ) + return nxt + + +def _split_fields(inner: str, imask: bytearray) -> list[str]: + """Split a struct body on commas that are outside comments and nesting.""" + out, cur, depth = [], "", 0 + angle = 0 + for idx, c in enumerate(inner): + live = imask[idx] if idx < len(imask) else 1 + if live: + if c in "([{": + depth += 1 + elif c in ")]}": + depth -= 1 + elif c == "<": + angle += 1 + elif c == ">" and not (idx and inner[idx - 1] == "-"): + angle -= 1 + if c == "," and depth == 0 and angle <= 0: + out.append(cur) + cur = "" + continue + cur += c if live else " " + if cur.strip(): + out.append(cur) + return out + + +def _parse_enum(src, mask, i, end, module, path, crate, doc, attrs, vis, skip) -> int: + m = re.match(r"\s*([A-Za-z_][A-Za-z0-9_]*)", src[i:end]) + if not m: + return _skip_item(src, mask, i, end) + name = m.group(1) + j = i + m.end() + generics, j = _split_generics(src[:end], j) + while j < end and src[j].isspace(): + j += 1 + variants: list[Variant] = [] + if j < end and src[j] == "{": + close = match_brace(src, mask, j) + inner = src[j + 1 : close - 1] + imask = mask[j + 1 : close - 1] + for part in _split_fields(inner, imask): + vm = re.match(r"\s*([A-Za-z_][A-Za-z0-9_]*)\s*(.*)$", part, re.S) + if vm: + variants.append(Variant(name=vm.group(1), payload=vm.group(2).strip())) + nxt = close + else: + nxt = _stmt_end(src, mask, j, end) + 1 + if vis == "pub" and not skip: + crate.enums.append( + Enum( + name=name, + module=module, + file=path, + doc=doc, + attrs=attrs, + variants=variants, + generics=generics, + ) + ) + return nxt + + +def _parse_impl(src, mask, i, end, module, path, crate, skip) -> int: + generics, j = _split_generics(src[:end], _skip_ws(src, i, end)) + open_at = find_code(src, mask, "{", j, end) + if open_at < 0: + return _skip_item(src, mask, i, end) + head = src[j:open_at].strip() + close = match_brace(src, mask, open_at) + if skip: + return close + # `impl Trait for Type` vs `impl Type`. + trait_name = "" + target = head + fm = re.search(r"\bfor\b", head) + if fm: + trait_name = head[: fm.start()].strip() + target = head[fm.end() :].strip() + target = re.sub(r"\bwhere\b.*$", "", target, flags=re.S).strip() + base = re.match(r"([A-Za-z_][A-Za-z0-9_:]*)", target) + target_name = base.group(1).split("::")[-1] if base else target + _walk( + src, + mask, + open_at + 1, + close - 1, + module, + path, + crate, + in_impl=(target_name, trait_name, generics), + ) + return close + + +def _fn_tail(src: str, mask: bytearray, start: int, end: int) -> tuple[int, int]: + """Offsets of the body's `{` and of a declaration's `;` after an arg list. + + Either may be -1. Balanced `[...]` and `(...)` are stepped over, so the + `;` in a `-> [f64; 6]` return type is not mistaken for the end of a + declaration. + """ + i = start + while i < end: + if mask[i]: + c = src[i] + if c in "([": + i = match_brace(src, mask, i) + continue + if c == "{": + return i, -1 + if c == ";": + return -1, i + i += 1 + return -1, -1 + + +def _skip_ws(src: str, i: int, end: int) -> int: + while i < end and src[i].isspace(): + i += 1 + return i + + +def _parse_fn( + src, mask, i, end, module, path, crate, doc, attrs, vis, prefixes, in_impl, skip +) -> int: + m = re.match(r"\s*([A-Za-z_][A-Za-z0-9_]*)", src[i:end]) + if not m: + return _skip_item(src, mask, i, end) + name = m.group(1) + j = i + m.end() + generics, j = _split_generics(src[:end], _skip_ws(src, j, end)) + j = _skip_ws(src, j, end) + if j >= end or src[j] != "(": + return _skip_item(src, mask, i, end) + close = match_brace(src, mask, j) + arglist = src[j + 1 : close - 1] + amask = mask[j + 1 : close - 1] + clean = "".join(c if amask[k] else " " for k, c in enumerate(arglist)) + # Return type and where clause run to the body (or the `;` of a decl). + # A return type may itself contain brackets and semicolons -- `[f64; 6]` + # is the common case here -- so step over balanced brackets rather than + # taking the first `;` or `{` literally. + body_open, semi = _fn_tail(src, mask, close, end) + if body_open < 0 or (0 <= semi < body_open): + tail_end = semi if semi >= 0 else end + nxt = tail_end + 1 + else: + tail_end = body_open + nxt = match_brace(src, mask, body_open) + tail = src[close:tail_end] + tail = "".join(c if mask[close + k] else " " for k, c in enumerate(tail)) + where_clause = "" + wm = re.search(r"\bwhere\b", tail) + if wm: + where_clause = tail[wm.end() :].strip() + tail = tail[: wm.start()] + ret = "" + rm = re.search(r"->", tail) + if rm: + ret = " ".join(tail[rm.end() :].split()).strip() + + in_trait_impl = bool(in_impl and in_impl[1]) + if skip or (vis != "pub" and not in_trait_impl): + return nxt + + args: list[tuple[str, str]] = [] + self_kind = "" + for part in _split_top(clean): + part = part.strip() + if not part or part.startswith("#["): + continue + sm = re.match(r"^(&\s*(?:'[a-z_]+\s*)?(mut\s+)?)?(mut\s+)?self$", part) + if sm: + if not sm.group(1): + self_kind = "self" + elif sm.group(2): + self_kind = "&mut self" + else: + self_kind = "&self" + continue + if ":" not in part: + continue + pat, ty = part.split(":", 1) + args.append((pat.strip(), " ".join(ty.split()).strip())) + + impl_type, impl_trait = ("", "") + if in_impl: + impl_type, impl_trait = in_impl[0], in_impl[1] + crate.funcs.append( + Func( + name=name, + module=module, + file=path, + doc=doc, + attrs=attrs, + args=args, + ret=ret, + generics=generics, + where_clause=where_clause, + self_kind=self_kind, + impl_type=impl_type, + impl_trait=impl_trait, + is_const="const" in prefixes, + is_unsafe="unsafe" in prefixes, + ) + ) + return nxt + + +# ── Crate walk ────────────────────────────────────────────────────────── + + +def module_for(root: str, path: str) -> str: + rel = os.path.relpath(path, root) + parts = rel.replace(os.sep, "/").split("/") + if parts[-1] in ("lib.rs", "main.rs"): + parts = parts[:-1] + elif parts[-1] == "mod.rs": + parts = parts[:-1] + else: + parts[-1] = parts[-1][:-3] + return "::".join(parts) + + +def scan_crate(src_root: str) -> Crate: + crate = Crate() + files = [] + for dirpath, _dirnames, filenames in os.walk(src_root): + for fn in filenames: + if fn.endswith(".rs"): + files.append(os.path.join(dirpath, fn)) + for path in sorted(files): + module = module_for(src_root, path) + if module.split("::")[0] == "verification": + continue + parse_file(path, module, crate) + return crate + + +def iter_public_modules(crate: Crate) -> Iterator[str]: + seen = set() + for coll in (crate.structs, crate.enums, crate.funcs, crate.consts): + for item in coll: + mod = getattr(item, "module", "") + if mod and mod not in seen: + seen.add(mod) + yield mod diff --git a/bindings/python/src/generated/m_acoustics.rs b/bindings/python/src/generated/m_acoustics.rs new file mode 100644 index 0000000..fa7397b --- /dev/null +++ b/bindings/python/src/generated/m_acoustics.rs @@ -0,0 +1,415 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Sabine reverberation time: T60 = 0.161 V / A (seconds). +/// +/// Rust: `acoustics::sabine_reverberation` +#[pyfunction] +#[pyo3(name = "sabine_reverberation", signature = (volume, total_absorption))] +pub fn pyfn_sabine_reverberation(volume: f64, total_absorption: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::acoustics::sabine_reverberation(volume, total_absorption)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Eyring reverberation time: T60 = 0.161 V / (-S × ln(1 - ā)) (seconds). +/// +/// Rust: `acoustics::eyring_reverberation` +#[pyfunction] +#[pyo3(name = "eyring_reverberation", signature = (volume, surface_area, avg_absorption_coeff))] +pub fn pyfn_eyring_reverberation(volume: f64, surface_area: f64, avg_absorption_coeff: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::acoustics::eyring_reverberation(volume, surface_area, avg_absorption_coeff)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Total absorption area: A = Σ(Si × αi). +/// Each tuple is (area_m2, absorption_coefficient). +/// +/// Rust: `acoustics::total_absorption` +#[pyfunction] +#[pyo3(name = "total_absorption", signature = (surfaces))] +pub fn pyfn_total_absorption<'py>(py: Python<'py>, surfaces: Vec<(f64, f64)>) -> PyResult { + let surfaces = surfaces.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::acoustics::total_absorption(&surfaces))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Room constant: R = S × ā / (1 - ā). +/// +/// Rust: `acoustics::room_constant` +#[pyfunction] +#[pyo3(name = "room_constant", signature = (surface_area, avg_absorption))] +pub fn pyfn_room_constant(surface_area: f64, avg_absorption: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::acoustics::room_constant(surface_area, avg_absorption)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Critical distance: dc = √(Q × R / (16π)). +/// +/// Rust: `acoustics::critical_distance` +#[pyfunction] +#[pyo3(name = "critical_distance", signature = (room_constant, directivity))] +pub fn pyfn_critical_distance(room_constant: f64, directivity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::acoustics::critical_distance(room_constant, directivity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Axial/tangential/oblique room mode frequency: +/// f = (c/2) × √((nx/L)² + (ny/W)² + (nz/H)²). +/// +/// Rust: `acoustics::room_mode_frequency` +#[pyfunction] +#[pyo3(name = "room_mode_frequency", signature = (length, width, height, nx, ny, nz, speed))] +pub fn pyfn_room_mode_frequency(length: f64, width: f64, height: f64, nx: u32, ny: u32, nz: u32, speed: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::acoustics::room_mode_frequency(length, width, height, nx, ny, nz, speed)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Energetic sum of two decibel levels: 10 × log₁₀(10^(dB1/10) + 10^(dB2/10)). +/// +/// Rust: `acoustics::add_db` +#[pyfunction] +#[pyo3(name = "add_db", signature = (db1, db2))] +pub fn pyfn_add_db(db1: f64, db2: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::acoustics::add_db(db1, db2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Energetic sum of multiple decibel levels. +/// +/// Rust: `acoustics::add_db_multiple` +#[pyfunction] +#[pyo3(name = "add_db_multiple", signature = (levels))] +pub fn pyfn_add_db_multiple<'py>(py: Python<'py>, levels: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::acoustics::add_db_multiple(&levels))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Subtract background noise: 10 × log₁₀(10^(total/10) - 10^(bg/10)). +/// +/// Rust: `acoustics::subtract_db` +#[pyfunction] +#[pyo3(name = "subtract_db", signature = (total_db, background_db))] +pub fn pyfn_subtract_db(total_db: f64, background_db: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::acoustics::subtract_db(total_db, background_db)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Inverse-square distance attenuation: L2 = L1 - 20 × log₁₀(d2 / d1). +/// +/// Rust: `acoustics::distance_attenuation` +#[pyfunction] +#[pyo3(name = "distance_attenuation", signature = (db_at_ref, ref_distance, distance))] +pub fn pyfn_distance_attenuation(db_at_ref: f64, ref_distance: f64, distance: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::acoustics::distance_attenuation(db_at_ref, ref_distance, distance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// A-weighting filter approximation (dBA relative weighting at a given frequency). +/// +/// Rust: `acoustics::a_weighting` +#[pyfunction] +#[pyo3(name = "a_weighting", signature = (frequency))] +pub fn pyfn_a_weighting(frequency: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::acoustics::a_weighting(frequency)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Rough equal-loudness approximation in phon. +/// At 1 kHz the phon value equals the SPL. At other frequencies a simple +/// A-weighting-derived correction is applied. This is NOT a full ISO 226 +/// implementation. +/// +/// Rust: `acoustics::equal_loudness_phon` +#[pyfunction] +#[pyo3(name = "equal_loudness_phon", signature = (spl, frequency))] +pub fn pyfn_equal_loudness_phon(spl: f64, frequency: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::acoustics::equal_loudness_phon(spl, frequency)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Bark critical-band rate: z = 13 × atan(0.76 f/1000) + 3.5 × atan((f/7500)²). +/// +/// Rust: `acoustics::bark_scale` +#[pyfunction] +#[pyo3(name = "bark_scale", signature = (frequency))] +pub fn pyfn_bark_scale(frequency: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::acoustics::bark_scale(frequency)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Mel scale: m = 2595 × log₁₀(1 + f/700). +/// +/// Rust: `acoustics::mel_scale` +#[pyfunction] +#[pyo3(name = "mel_scale", signature = (frequency))] +pub fn pyfn_mel_scale(frequency: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::acoustics::mel_scale(frequency)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Inverse mel scale: f = 700 × (10^(m/2595) - 1). +/// +/// Rust: `acoustics::frequency_from_mel` +#[pyfunction] +#[pyo3(name = "frequency_from_mel", signature = (mel))] +pub fn pyfn_frequency_from_mel(mel: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::acoustics::frequency_from_mel(mel)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Noise reduction through a partition: NR = TL + 10 × log₁₀(A / S). +/// +/// Rust: `acoustics::noise_reduction` +#[pyfunction] +#[pyo3(name = "noise_reduction", signature = (tl, receiving_absorption, common_area))] +pub fn pyfn_noise_reduction(tl: f64, receiving_absorption: f64, common_area: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::acoustics::noise_reduction(tl, receiving_absorption, common_area)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Single-panel mass law transmission loss: TL = 20 × log₁₀(m × f) - 47. +/// +/// Rust: `acoustics::transmission_loss_mass_law` +#[pyfunction] +#[pyo3(name = "transmission_loss_mass_law", signature = (surface_density, frequency))] +pub fn pyfn_transmission_loss_mass_law(surface_density: f64, frequency: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::acoustics::transmission_loss_mass_law(surface_density, frequency)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Rough STC estimate (≈ TL at 500 Hz). +/// +/// Rust: `acoustics::sound_transmission_class_estimate` +#[pyfunction] +#[pyo3(name = "sound_transmission_class_estimate", signature = (tl_500))] +pub fn pyfn_sound_transmission_class_estimate(tl_500: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::acoustics::sound_transmission_class_estimate(tl_500)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Simplified atmospheric absorption coefficient in dB/km. +/// α ≈ 0.01 × (f/1000)^1.7 × (1 + 0.01×(T-20)) × (1 - 0.005×RH). +/// +/// Rust: `acoustics::atmospheric_absorption_coeff` +#[pyfunction] +#[pyo3(name = "atmospheric_absorption_coeff", signature = (frequency, temperature, humidity))] +pub fn pyfn_atmospheric_absorption_coeff(frequency: f64, temperature: f64, humidity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::acoustics::atmospheric_absorption_coeff(frequency, temperature, humidity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Simplified excess ground attenuation (dB) for propagation over soft ground. +/// Uses a basic geometric model based on path-length difference. +/// +/// Rust: `acoustics::ground_effect_excess` +#[pyfunction] +#[pyo3(name = "ground_effect_excess", signature = (distance, source_height, receiver_height))] +pub fn pyfn_ground_effect_excess(distance: f64, source_height: f64, receiver_height: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::acoustics::ground_effect_excess(distance, source_height, receiver_height)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Nth harmonic frequency: f_n = n × f_fundamental +/// +/// Rust: `acoustics::harmonic_frequency` +#[pyfunction] +#[pyo3(name = "harmonic_frequency", signature = (fundamental, n))] +pub fn pyfn_harmonic_frequency(fundamental: f64, n: u32) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::acoustics::harmonic_frequency(fundamental, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Generate harmonic series up to max_harmonic: [f, 2f, 3f, ..., nf] +/// +/// Rust: `acoustics::harmonic_series` +#[pyfunction] +#[pyo3(name = "harmonic_series", signature = (fundamental, max_harmonic))] +pub fn pyfn_harmonic_series<'py>(py: Python<'py>, fundamental: f64, max_harmonic: u32) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::acoustics::harmonic_series(fundamental, max_harmonic))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Frequency ratio between two musical interval semitones (equal temperament): +/// ratio = 2^(semitones/12) +/// +/// Rust: `acoustics::equal_temperament_ratio` +#[pyfunction] +#[pyo3(name = "equal_temperament_ratio", signature = (semitones))] +pub fn pyfn_equal_temperament_ratio(semitones: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::acoustics::equal_temperament_ratio(semitones)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Frequency of a note in equal temperament given A4=440Hz reference: +/// f = 440 × 2^((midi_note - 69)/12) where midi_note 69 = A4 +/// +/// Rust: `acoustics::midi_to_frequency` +#[pyfunction] +#[pyo3(name = "midi_to_frequency", signature = (midi_note))] +pub fn pyfn_midi_to_frequency(midi_note: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::acoustics::midi_to_frequency(midi_note)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// MIDI note number from frequency: n = 69 + 12×log₂(f/440) +/// +/// Rust: `acoustics::frequency_to_midi` +#[pyfunction] +#[pyo3(name = "frequency_to_midi", signature = (frequency))] +pub fn pyfn_frequency_to_midi(frequency: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::acoustics::frequency_to_midi(frequency)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Cents difference between two frequencies: c = 1200 × log₂(f2/f1) +/// +/// Rust: `acoustics::cents` +#[pyfunction] +#[pyo3(name = "cents", signature = (f1, f2))] +pub fn pyfn_cents(f1: f64, f2: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::acoustics::cents(f1, f2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Frequency of a circular membrane mode (drum): +/// f_mn = (α_mn × v) / (2π × r) +/// where α_mn are zeros of Bessel functions. Common modes: +/// (0,1)=2.405, (1,1)=3.832, (2,1)=5.136, (0,2)=5.520 +/// +/// Rust: `acoustics::circular_membrane_frequency` +#[pyfunction] +#[pyo3(name = "circular_membrane_frequency", signature = (bessel_zero, wave_speed, radius))] +pub fn pyfn_circular_membrane_frequency(bessel_zero: f64, wave_speed: f64, radius: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::acoustics::circular_membrane_frequency(bessel_zero, wave_speed, radius)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Rectangular plate fundamental frequency: +/// f = (π/(2L²)) × √(D/(ρh)) where D = Eh³/(12(1-ν²)) +/// Simplified: takes flexural rigidity D, density×thickness (ρh), and length +/// +/// Rust: `acoustics::rectangular_plate_fundamental` +#[pyfunction] +#[pyo3(name = "rectangular_plate_fundamental", signature = (length, flexural_rigidity, mass_per_area))] +pub fn pyfn_rectangular_plate_fundamental(length: f64, flexural_rigidity: f64, mass_per_area: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::acoustics::rectangular_plate_fundamental(length, flexural_rigidity, mass_per_area)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Harmonic distortion: THD = √(Σ V_n²) / V_1 for n=2..N +/// Takes fundamental amplitude and harmonic amplitudes [2nd, 3rd, ...] +/// +/// Rust: `acoustics::total_harmonic_distortion` +#[pyfunction] +#[pyo3(name = "total_harmonic_distortion", signature = (fundamental_amplitude, harmonic_amplitudes))] +pub fn pyfn_total_harmonic_distortion<'py>(py: Python<'py>, fundamental_amplitude: f64, harmonic_amplitudes: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::acoustics::total_harmonic_distortion(fundamental_amplitude, &harmonic_amplitudes))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Synthesize a waveform from harmonics at a given time: +/// y(t) = Σ a_n × sin(2π × n × f × t + φ_n) +/// Each tuple is (harmonic_number, amplitude, phase) +/// +/// Rust: `acoustics::harmonic_synthesis` +#[pyfunction] +#[pyo3(name = "harmonic_synthesis", signature = (fundamental, harmonics, t))] +pub fn pyfn_harmonic_synthesis<'py>(py: Python<'py>, fundamental: f64, harmonics: Vec<(u32, f64, f64)>, t: f64) -> PyResult { + let harmonics = harmonics.into_iter().map(|__e| (__e.0, __e.1, __e.2)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::acoustics::harmonic_synthesis(fundamental, &harmonics, t))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Inharmonicity coefficient for a stiff string (piano): +/// f_n = n × f₁ × √(1 + B × n²) where B = π³Ed⁴/(64TL²) +/// Takes the precomputed inharmonicity coefficient B +/// +/// Rust: `acoustics::inharmonic_frequency` +#[pyfunction] +#[pyo3(name = "inharmonic_frequency", signature = (fundamental, n, b_coeff))] +pub fn pyfn_inharmonic_frequency(fundamental: f64, n: u32, b_coeff: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::acoustics::inharmonic_frequency(fundamental, n, b_coeff)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_sabine_reverberation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_eyring_reverberation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_total_absorption, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_room_constant, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_critical_distance, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_room_mode_frequency, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_add_db, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_add_db_multiple, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_subtract_db, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_distance_attenuation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_a_weighting, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_equal_loudness_phon, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bark_scale, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mel_scale, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_frequency_from_mel, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_noise_reduction, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_transmission_loss_mass_law, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sound_transmission_class_estimate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_atmospheric_absorption_coeff, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ground_effect_excess, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_harmonic_frequency, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_harmonic_series, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_equal_temperament_ratio, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_midi_to_frequency, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_frequency_to_midi, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cents, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_circular_membrane_frequency, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rectangular_plate_fundamental, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_total_harmonic_distortion, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_harmonic_synthesis, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_inharmonic_frequency, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_astrophysics.rs b/bindings/python/src/generated/m_astrophysics.rs new file mode 100644 index 0000000..c02bbf8 --- /dev/null +++ b/bindings/python/src/generated/m_astrophysics.rs @@ -0,0 +1,22 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + Ok(()) +} diff --git a/bindings/python/src/generated/m_astrophysics__collisions.rs b/bindings/python/src/generated/m_astrophysics__collisions.rs new file mode 100644 index 0000000..a5142a6 --- /dev/null +++ b/bindings/python/src/generated/m_astrophysics__collisions.rs @@ -0,0 +1,135 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Computes the impact angle between two colliding bodies: θ = acos(v_radial / |v_rel|). +/// +/// Rust: `astrophysics::collisions::impact_angle` +#[pyfunction] +#[pyo3(name = "impact_angle", signature = (pos1, vel1, pos2, vel2))] +pub fn pyfn_impact_angle(pos1: crate::generated::types::PyVec3Arg, vel1: crate::generated::types::PyVec3Arg, pos2: crate::generated::types::PyVec3Arg, vel2: crate::generated::types::PyVec3Arg) -> PyResult { + let pos1 = pos1.0; + let vel1 = vel1.0; + let pos2 = pos2.0; + let vel2 = vel2.0; + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::collisions::impact_angle(pos1, vel1, pos2, vel2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Computes the relative impact speed between two bodies: |v1 - v2|. +/// +/// Rust: `astrophysics::collisions::impact_speed` +#[pyfunction] +#[pyo3(name = "impact_speed", signature = (vel1, vel2))] +pub fn pyfn_impact_speed(vel1: crate::generated::types::PyVec3Arg, vel2: crate::generated::types::PyVec3Arg) -> PyResult { + let vel1 = vel1.0; + let vel2 = vel2.0; + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::collisions::impact_speed(vel1, vel2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Computes the post-merger velocity via conservation of momentum: v_cm = (m1 v1 + m2 v2) / (m1 + m2). +/// +/// Rust: `astrophysics::collisions::merge_velocity` +#[pyfunction] +#[pyo3(name = "merge_velocity", signature = (m1, v1, m2, v2))] +pub fn pyfn_merge_velocity(m1: f64, v1: crate::generated::types::PyVec3Arg, m2: f64, v2: crate::generated::types::PyVec3Arg) -> PyResult { + let v1 = v1.0; + let v2 = v2.0; + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::collisions::merge_velocity(m1, v1, m2, v2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Computes the merged body radius assuming volume conservation: r = (r1³ + r2³)^(1/3). +/// +/// Rust: `astrophysics::collisions::merge_radius` +#[pyfunction] +#[pyo3(name = "merge_radius", signature = (r1, r2))] +pub fn pyfn_merge_radius(r1: f64, r2: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::collisions::merge_radius(r1, r2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Computes the kinetic energy available in the center-of-mass frame: KE_cm = Σ ½m_i |v_i - v_cm|². +/// +/// Rust: `astrophysics::collisions::collision_energy` +#[pyfunction] +#[pyo3(name = "collision_energy", signature = (m1, v1, m2, v2))] +pub fn pyfn_collision_energy(m1: f64, v1: crate::generated::types::PyVec3Arg, m2: f64, v2: crate::generated::types::PyVec3Arg) -> PyResult { + let v1 = v1.0; + let v2 = v2.0; + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::collisions::collision_energy(m1, v1, m2, v2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Computes the surface escape speed: v_esc = √(2GM/r). +/// +/// Rust: `astrophysics::collisions::escape_speed` +#[pyfunction] +#[pyo3(name = "escape_speed", signature = (mass, radius))] +pub fn pyfn_escape_speed(mass: f64, radius: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::collisions::escape_speed(mass, radius)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Returns debris generation parameters (count, speed, mass fraction, temperature) for a given collision type. +/// +/// Rust: `astrophysics::collisions::debris_params` +#[pyfunction] +#[pyo3(name = "debris_params", signature = (kind))] +pub fn pyfn_debris_params(kind: crate::generated::types::PyCollisionKind) -> PyResult { + let kind = kind.to_rust(); + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::collisions::debris_params(kind)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyDebrisParams { inner: __v }) +} + +/// Resolves a collision between two bodies, computing the merged properties and debris parameters. +/// +/// Rust: `astrophysics::collisions::resolve_collision` +#[pyfunction] +#[pyo3(name = "resolve_collision", signature = (m1, r1, v1, m2, r2, v2, kind))] +pub fn pyfn_resolve_collision(m1: f64, r1: f64, v1: crate::generated::types::PyVec3Arg, m2: f64, r2: f64, v2: crate::generated::types::PyVec3Arg, kind: crate::generated::types::PyCollisionKind) -> PyResult { + let v1 = v1.0; + let v2 = v2.0; + let kind = kind.to_rust(); + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::collisions::resolve_collision(m1, r1, v1, m2, r2, v2, kind)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCollisionResult { inner: __v }) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_impact_angle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_impact_speed, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_merge_velocity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_merge_radius, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_collision_energy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_escape_speed, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_debris_params, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_resolve_collision, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_astrophysics__coords.rs b/bindings/python/src/generated/m_astrophysics__coords.rs new file mode 100644 index 0000000..8b926ee --- /dev/null +++ b/bindings/python/src/generated/m_astrophysics__coords.rs @@ -0,0 +1,316 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Converts equatorial coordinates to horizontal, returning +/// `(azimuth, altitude)` in radians. +/// +/// Azimuth is measured from north through east, which is the navigator's +/// convention; astronomers sometimes measure from south, and the two +/// differ by half a turn. Altitude is positive above the horizon. +/// +/// The local hour angle `lst - ra` is what carries the time dependence: +/// it is zero when the object is due south, so an object is highest +/// exactly then. Everything else is one spherical triangle. +/// +/// No refraction. Near the horizon the atmosphere lifts an object by +/// about half a degree -- more than the Sun's own diameter -- so a +/// computed altitude of zero is a body that has already visibly set. +/// +/// Errors: +/// Returns an error for a non-finite input or a latitude outside +/// `[-pi/2, pi/2]`. +/// +/// Rust: `astrophysics::coords::equatorial_to_horizontal` +#[pyfunction] +#[pyo3(name = "equatorial_to_horizontal", signature = (right_ascension, declination, latitude, local_sidereal_time))] +pub fn pyfn_equatorial_to_horizontal(right_ascension: f64, declination: f64, latitude: f64, local_sidereal_time: f64) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::coords::equatorial_to_horizontal(right_ascension, declination, latitude, local_sidereal_time)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// Converts horizontal coordinates back to equatorial, returning +/// `(right ascension, declination)`. +/// +/// The exact inverse of `equatorial_to_horizontal`, which is worth +/// having as a separate function precisely so the pair can be checked +/// against each other. +/// +/// Errors: +/// As `equatorial_to_horizontal`, with the altitude taking the place of +/// the declination. +/// +/// Rust: `astrophysics::coords::horizontal_to_equatorial` +#[pyfunction] +#[pyo3(name = "horizontal_to_equatorial", signature = (azimuth, altitude, latitude, local_sidereal_time))] +pub fn pyfn_horizontal_to_equatorial(azimuth: f64, altitude: f64, latitude: f64, local_sidereal_time: f64) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::coords::horizontal_to_equatorial(azimuth, altitude, latitude, local_sidereal_time)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// Converts ecliptic coordinates to equatorial, returning +/// `(right ascension, declination)`. +/// +/// A rotation by the obliquity about the vernal equinox, and nothing +/// more. The ecliptic frame is where the planets nearly lie -- their +/// latitudes are a few degrees at most -- which is why an ephemeris +/// computes there and converts at the end. +/// +/// Errors: +/// Returns an error for a non-finite angle or a latitude outside +/// `[-pi/2, pi/2]`. +/// +/// Rust: `astrophysics::coords::ecliptic_to_equatorial` +#[pyfunction] +#[pyo3(name = "ecliptic_to_equatorial", signature = (ecliptic_longitude, ecliptic_latitude, obliquity))] +pub fn pyfn_ecliptic_to_equatorial(ecliptic_longitude: f64, ecliptic_latitude: f64, obliquity: f64) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::coords::ecliptic_to_equatorial(ecliptic_longitude, ecliptic_latitude, obliquity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// Converts equatorial coordinates to ecliptic, returning +/// `(longitude, latitude)`. +/// +/// Errors: +/// As `ecliptic_to_equatorial`. +/// +/// Rust: `astrophysics::coords::equatorial_to_ecliptic` +#[pyfunction] +#[pyo3(name = "equatorial_to_ecliptic", signature = (right_ascension, declination, obliquity))] +pub fn pyfn_equatorial_to_ecliptic(right_ascension: f64, declination: f64, obliquity: f64) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::coords::equatorial_to_ecliptic(right_ascension, declination, obliquity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// The mean obliquity of the ecliptic at a Julian date, by the IAU 1980 +/// polynomial. +/// +/// It decreases by about 47 arcseconds a century, which over the span of +/// recorded astronomy is enough to matter: the tropics have moved +/// measurably since the term was coined. +/// +/// Errors: +/// Returns an error for a non-finite or out-of-range Julian date. +/// +/// Rust: `astrophysics::coords::mean_obliquity` +#[pyfunction] +#[pyo3(name = "mean_obliquity", signature = (jd))] +pub fn pyfn_mean_obliquity(jd: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::coords::mean_obliquity(jd)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Precesses equatorial coordinates from J2000 to another epoch, to first +/// order in the precession angles. +/// +/// The equinox itself moves, at about 50 arcseconds a year, so a +/// catalogue position is meaningless without the epoch it belongs to. +/// This is the rigorous rotation truncated to its linear terms, which is +/// good to an arcsecond over a century and degrades quadratically beyond. +/// +/// It is a coordinate change, not a motion: the star has not moved, the +/// grid has. +/// +/// Errors: +/// Returns an error for a non-finite coordinate, a declination outside +/// `[-pi/2, pi/2]`, or an out-of-range date. +/// +/// Rust: `astrophysics::coords::precession_approx` +#[pyfunction] +#[pyo3(name = "precession_approx", signature = (right_ascension, declination, jd))] +pub fn pyfn_precession_approx(right_ascension: f64, declination: f64, jd: f64) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::coords::precession_approx(right_ascension, declination, jd)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// The Sun's apparent geocentric position, returning +/// `(right ascension, declination, distance in astronomical units)`. +/// +/// The low-precision series from the Astronomical Almanac: a mean +/// longitude, a mean anomaly, and two terms of the equation of centre. +/// Good to about a hundredth of a degree for a couple of centuries either +/// side of J2000, which is a hundredth of the Sun's own diameter. +/// +/// The declination is what drives the seasons, and it reaches the +/// obliquity at the solstices and zero at the equinoxes -- which is what +/// makes those the definitions of the days rather than consequences of +/// them. +/// +/// Errors: +/// Returns an error for a non-finite or out-of-range Julian date. +/// +/// Rust: `astrophysics::coords::sun_position_approx` +#[pyfunction] +#[pyo3(name = "sun_position_approx", signature = (jd))] +pub fn pyfn_sun_position_approx(jd: f64) -> PyResult<(f64, f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::coords::sun_position_approx(jd)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// The Moon's apparent geocentric position, returning +/// `(right ascension, declination, distance in kilometres)`. +/// +/// A handful of the largest terms in longitude, latitude and distance: +/// the evection, the variation, the annual equation and the principal +/// latitude term. Good to a few tenths of a degree, which is about the +/// Moon's own diameter -- enough to say where it is in the sky and not +/// enough to predict an occultation. +/// +/// The Moon is the hardest classical ephemeris there is. Its orbit is +/// perturbed by the Sun at the percent level, and the full theory runs to +/// thousands of terms; what is kept here is the first page of a long +/// book. +/// +/// Errors: +/// Returns an error for a non-finite or out-of-range Julian date. +/// +/// Rust: `astrophysics::coords::moon_position_approx` +#[pyfunction] +#[pyo3(name = "moon_position_approx", signature = (jd))] +pub fn pyfn_moon_position_approx(jd: f64) -> PyResult<(f64, f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::coords::moon_position_approx(jd)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// A planet's heliocentric position, returning +/// `(ecliptic longitude, ecliptic latitude, distance in AU)`. +/// +/// Mean elements advanced linearly in time, Kepler's equation solved, and +/// the result rotated into the ecliptic. There are no mutual +/// perturbations at all, which is what makes this "low precision": the +/// inner planets come out within a fraction of a degree over a few +/// centuries around J2000, and Jupiter and Saturn drift by degrees over +/// the same span because they pull on each other and this does not know +/// it. +/// +/// The elements are the Standish set, whose stated validity is 1800 to +/// 2050. Outside that window the answer degrades quickly and silently, +/// which is a property of the data rather than of the arithmetic. +/// +/// Errors: +/// Returns an error for a non-finite or out-of-range Julian date, or a +/// Kepler solve that fails. +/// +/// Rust: `astrophysics::coords::planet_position_low_precision` +#[pyfunction] +#[pyo3(name = "planet_position_low_precision", signature = (planet, jd))] +pub fn pyfn_planet_position_low_precision(planet: crate::generated::types::PyPlanet, jd: f64) -> PyResult<(f64, f64, f64)> { + let planet = planet.to_rust(); + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::coords::planet_position_low_precision(planet, jd)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// The rise and set times of a body of fixed equatorial coordinates on a +/// given day, as Julian dates, or `None` if it never crosses the horizon. +/// +/// `standard_altitude` is the altitude counted as the horizon: zero for a +/// point source ignoring refraction, about -0.0145 radians (-50 +/// arcminutes) for the Sun's upper limb with mean refraction. +/// +/// `None` covers both circumpolar cases -- a body permanently up, and one +/// permanently down -- which are the same arithmetic: the required hour +/// angle has no cosine. That is the polar day and the polar night, and +/// which one it is can be told from the altitude at transit. +/// +/// The coordinates are held fixed over the day, which is fine for a star +/// and an approximation for the Sun, whose declination moves by up to +/// 0.4 degrees between rise and set near an equinox. +/// +/// Errors: +/// Returns an error for a non-finite input, a latitude or declination out +/// of range, or an out-of-range date. +/// +/// Rust: `astrophysics::coords::rise_set_times` +#[pyfunction] +#[pyo3(name = "rise_set_times", signature = (right_ascension, declination, latitude, longitude, jd, standard_altitude))] +pub fn pyfn_rise_set_times(right_ascension: f64, declination: f64, latitude: f64, longitude: f64, jd: f64, standard_altitude: f64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::coords::rise_set_times(right_ascension, declination, latitude, longitude, jd, standard_altitude)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.map(|__x| (__x.0, __x.1))) +} + +/// Parses a two-line element set into its fields. +/// +/// **Parsing only.** The elements are not propagated, and they must not be +/// propagated by anything in this crate. A TLE's numbers are not osculating +/// orbital elements: they are *mean* elements in the specific sense defined +/// by the SGP4/SDP4 theory, with the periodic variations that theory models +/// already removed. Feeding them to a Kepler propagator -- including +/// `astrophysics::kepler::propagate_kepler` -- gives an answer +/// that looks reasonable and is wrong by kilometres within hours, because +/// the removed terms are exactly what would need adding back. +/// +/// SGP4 is therefore not "a better propagator to add later"; it is the +/// definition of what the numbers mean. Implementing it is a substantial +/// piece of work with its own deep-space branch, and it is out of scope +/// here rather than approximated. +/// +/// The exponential fields (`bstar` and the second derivative) use the +/// format's assumed-decimal-point convention: `12345-3` means +/// `0.12345e-3`. +/// +/// Errors: +/// Returns an error for lines of the wrong length or line number, a field +/// that will not parse, a checksum mismatch, or an epoch out of range. +/// +/// Rust: `astrophysics::coords::tle_parse_lite` +#[pyfunction] +#[pyo3(name = "tle_parse_lite", signature = (line1, line2))] +pub fn pyfn_tle_parse_lite(line1: String, line2: String) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::coords::tle_parse_lite(&line1, &line2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyTleElements { inner: __v }) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_equatorial_to_horizontal, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_horizontal_to_equatorial, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ecliptic_to_equatorial, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_equatorial_to_ecliptic, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mean_obliquity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_precession_approx, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sun_position_approx, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_moon_position_approx, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_planet_position_low_precision, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rise_set_times, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tle_parse_lite, m)?)?; + m.add("OBLIQUITY_J2000", rust_physics_engine::astrophysics::coords::OBLIQUITY_J2000)?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_astrophysics__gravitational_waves.rs b/bindings/python/src/generated/m_astrophysics__gravitational_waves.rs new file mode 100644 index 0000000..611e50a --- /dev/null +++ b/bindings/python/src/generated/m_astrophysics__gravitational_waves.rs @@ -0,0 +1,119 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Computes gravitational wave luminosity for a circular binary: L = (32/5) G⁴m₁²m₂²(m₁+m₂) / (c⁵ a⁵). +/// +/// Rust: `astrophysics::gravitational_waves::gw_luminosity` +#[pyfunction] +#[pyo3(name = "gw_luminosity", signature = (m1, m2, separation))] +pub fn pyfn_gw_luminosity(m1: f64, m2: f64, separation: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::gravitational_waves::gw_luminosity(m1, m2, separation)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Computes the gravitational wave frequency (twice the orbital frequency): f_gw = (1/π)√(G(m₁+m₂)/a³). +/// +/// Rust: `astrophysics::gravitational_waves::gw_frequency` +#[pyfunction] +#[pyo3(name = "gw_frequency", signature = (m1, m2, separation))] +pub fn pyfn_gw_frequency(m1: f64, m2: f64, separation: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::gravitational_waves::gw_frequency(m1, m2, separation)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Computes the dimensionless gravitational wave strain amplitude: h = 4G²m₁m₂ / (c⁴ a D). +/// +/// Rust: `astrophysics::gravitational_waves::gw_strain` +#[pyfunction] +#[pyo3(name = "gw_strain", signature = (m1, m2, separation, distance_to_observer))] +pub fn pyfn_gw_strain(m1: f64, m2: f64, separation: f64, distance_to_observer: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::gravitational_waves::gw_strain(m1, m2, separation, distance_to_observer)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Computes the Peters inspiral time for a circular binary: t = (5/256) c⁵ a⁴ / (G³ m₁ m₂ (m₁+m₂)). +/// +/// Rust: `astrophysics::gravitational_waves::inspiral_time` +#[pyfunction] +#[pyo3(name = "inspiral_time", signature = (m1, m2, separation))] +pub fn pyfn_inspiral_time(m1: f64, m2: f64, separation: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::gravitational_waves::inspiral_time(m1, m2, separation)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Computes the chirp mass of a binary system: M_c = (m₁ m₂)^(3/5) / (m₁ + m₂)^(1/5). +/// +/// Rust: `astrophysics::gravitational_waves::chirp_mass` +#[pyfunction] +#[pyo3(name = "chirp_mass", signature = (m1, m2))] +pub fn pyfn_chirp_mass(m1: f64, m2: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::gravitational_waves::chirp_mass(m1, m2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Computes the Schwarzschild ISCO radius: r_isco = 6GM/c². +/// +/// Rust: `astrophysics::gravitational_waves::innermost_stable_circular_orbit` +#[pyfunction] +#[pyo3(name = "innermost_stable_circular_orbit", signature = (total_mass))] +pub fn pyfn_innermost_stable_circular_orbit(total_mass: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::gravitational_waves::innermost_stable_circular_orbit(total_mass)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Finds the pair of bodies with the highest gravitational wave luminosity, returning their indices and luminosity. +/// +/// Rust: `astrophysics::gravitational_waves::find_strongest_source` +#[pyfunction] +#[pyo3(name = "find_strongest_source", signature = (masses, positions))] +pub fn pyfn_find_strongest_source<'py>(py: Python<'py>, masses: Vec, positions: Vec) -> PyResult> { + let positions = positions.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::astrophysics::gravitational_waves::find_strongest_source(&masses, &positions))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| (__x.0, __x.1, __x.2))) +} + +/// Estimates the energy radiated during merger: E = η M c², where η = m₁m₂/(m₁+m₂)². +/// +/// Rust: `astrophysics::gravitational_waves::merger_energy` +#[pyfunction] +#[pyo3(name = "merger_energy", signature = (m1, m2))] +pub fn pyfn_merger_energy(m1: f64, m2: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::gravitational_waves::merger_energy(m1, m2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_gw_luminosity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gw_frequency, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gw_strain, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_inspiral_time, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_chirp_mass, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_innermost_stable_circular_orbit, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_find_strongest_source, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_merger_energy, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_astrophysics__habitable_zone.rs b/bindings/python/src/generated/m_astrophysics__habitable_zone.rs new file mode 100644 index 0000000..5e7e321 --- /dev/null +++ b/bindings/python/src/generated/m_astrophysics__habitable_zone.rs @@ -0,0 +1,98 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Computes the inner edge of the habitable zone in AU: d_inner = √L × 0.95. +/// +/// Rust: `astrophysics::habitable_zone::habitable_zone_inner` +#[pyfunction] +#[pyo3(name = "habitable_zone_inner", signature = (luminosity_solar))] +pub fn pyfn_habitable_zone_inner(luminosity_solar: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::habitable_zone::habitable_zone_inner(luminosity_solar)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Computes the outer edge of the habitable zone in AU: d_outer = √L × 1.37. +/// +/// Rust: `astrophysics::habitable_zone::habitable_zone_outer` +#[pyfunction] +#[pyo3(name = "habitable_zone_outer", signature = (luminosity_solar))] +pub fn pyfn_habitable_zone_outer(luminosity_solar: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::habitable_zone::habitable_zone_outer(luminosity_solar)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Returns the (inner, outer) habitable zone boundaries in AU for a given stellar luminosity in solar units. +/// +/// Rust: `astrophysics::habitable_zone::habitable_zone` +#[pyfunction] +#[pyo3(name = "habitable_zone", signature = (luminosity_solar))] +pub fn pyfn_habitable_zone(luminosity_solar: f64) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::habitable_zone::habitable_zone(luminosity_solar)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Returns true if a body at the given distance (AU) lies within the habitable zone. +/// +/// Rust: `astrophysics::habitable_zone::is_in_habitable_zone` +#[pyfunction] +#[pyo3(name = "is_in_habitable_zone", signature = (luminosity_solar, distance_au))] +pub fn pyfn_is_in_habitable_zone(luminosity_solar: f64, distance_au: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::habitable_zone::is_in_habitable_zone(luminosity_solar, distance_au)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Estimates stellar luminosity from mass using the mass-luminosity relation: L = M^3.5 (in solar units). +/// +/// Rust: `astrophysics::habitable_zone::luminosity_from_mass` +#[pyfunction] +#[pyo3(name = "luminosity_from_mass", signature = (mass_solar))] +pub fn pyfn_luminosity_from_mass(mass_solar: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::habitable_zone::luminosity_from_mass(mass_solar)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Computes stellar luminosity from the Stefan-Boltzmann law: L = R² (T/T_sun)⁴ (in solar units). +/// +/// Rust: `astrophysics::habitable_zone::luminosity_from_temperature_radius` +#[pyfunction] +#[pyo3(name = "luminosity_from_temperature_radius", signature = (temperature, radius_solar))] +pub fn pyfn_luminosity_from_temperature_radius(temperature: f64, radius_solar: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::habitable_zone::luminosity_from_temperature_radius(temperature, radius_solar)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_habitable_zone_inner, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_habitable_zone_outer, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_habitable_zone, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_is_in_habitable_zone, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_luminosity_from_mass, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_luminosity_from_temperature_radius, m)?)?; + m.add("SOLAR_LUMINOSITY", rust_physics_engine::astrophysics::habitable_zone::SOLAR_LUMINOSITY)?; + m.add("SOLAR_TEMPERATURE", rust_physics_engine::astrophysics::habitable_zone::SOLAR_TEMPERATURE)?; + m.add("HZ_INNER_COEFFICIENT", rust_physics_engine::astrophysics::habitable_zone::HZ_INNER_COEFFICIENT)?; + m.add("HZ_OUTER_COEFFICIENT", rust_physics_engine::astrophysics::habitable_zone::HZ_OUTER_COEFFICIENT)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_astrophysics__kepler.rs b/bindings/python/src/generated/m_astrophysics__kepler.rs new file mode 100644 index 0000000..d79d5b1 --- /dev/null +++ b/bindings/python/src/generated/m_astrophysics__kepler.rs @@ -0,0 +1,236 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Solves `M = E - e sin E` for the eccentric anomaly. +/// +/// Newton's method from a seed that keeps it in the basin: for nearly +/// circular orbits `M` itself is already close, and for high +/// eccentricities the standard `M + e sin M` correction is not -- near +/// periapsis at `e = 0.99` the function is almost flat in `E` and a naive +/// seed sends the first step far outside `[0, 2 pi)`. The seed here is +/// Danby's, which is chosen to converge for every eccentricity below one. +/// +/// Returns the anomaly in `[0, 2 pi]`. +/// +/// Errors: +/// Returns an error for an eccentricity outside `[0, 1)`, a non-finite +/// mean anomaly or tolerance, a non-positive tolerance, or an iteration +/// that fails to converge. +/// +/// Rust: `astrophysics::kepler::kepler_solve_elliptic` +#[pyfunction] +#[pyo3(name = "kepler_solve_elliptic", signature = (mean_anomaly, e, tol))] +pub fn pyfn_kepler_solve_elliptic(mean_anomaly: f64, e: f64, tol: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::kepler::kepler_solve_elliptic(mean_anomaly, e, tol)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Solves the hyperbolic Kepler equation `M = e sinh H - H`. +/// +/// The hyperbolic form has no periodicity to wrap, and `sinh` grows +/// exponentially, so a poor seed overflows rather than merely converging +/// slowly. The seed here is logarithmic for large `M`, which is where the +/// solution actually lives. +/// +/// Errors: +/// Returns an error for an eccentricity at or below one, a non-finite +/// mean anomaly or tolerance, a non-positive tolerance, or an iteration +/// that fails to converge. +/// +/// Rust: `astrophysics::kepler::kepler_solve_hyperbolic` +#[pyfunction] +#[pyo3(name = "kepler_solve_hyperbolic", signature = (mean_anomaly, e, tol))] +pub fn pyfn_kepler_solve_hyperbolic(mean_anomaly: f64, e: f64, tol: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::kepler::kepler_solve_hyperbolic(mean_anomaly, e, tol)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The true anomaly corresponding to an eccentric anomaly. +/// +/// `tan(nu/2) = sqrt((1+e)/(1-e)) tan(E/2)`, evaluated through `atan2` so +/// it stays correct across all four quadrants rather than losing a half +/// turn where the tangent wraps. +/// +/// Errors: +/// Returns an error for an eccentricity outside `[0, 1)` or a non-finite +/// anomaly. +/// +/// Rust: `astrophysics::kepler::true_from_eccentric` +#[pyfunction] +#[pyo3(name = "true_from_eccentric", signature = (eccentric, e))] +pub fn pyfn_true_from_eccentric(eccentric: f64, e: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::kepler::true_from_eccentric(eccentric, e)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The eccentric anomaly corresponding to a true anomaly. +/// +/// Errors: +/// As `true_from_eccentric`. +/// +/// Rust: `astrophysics::kepler::eccentric_from_true` +#[pyfunction] +#[pyo3(name = "eccentric_from_true", signature = (true_anomaly, e))] +pub fn pyfn_eccentric_from_true(true_anomaly: f64, e: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::kepler::eccentric_from_true(true_anomaly, e)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The mean anomaly corresponding to an eccentric anomaly: Kepler's +/// equation read forwards, which needs no solving at all. +/// +/// Errors: +/// As `true_from_eccentric`. +/// +/// Rust: `astrophysics::kepler::mean_from_eccentric` +#[pyfunction] +#[pyo3(name = "mean_from_eccentric", signature = (eccentric, e))] +pub fn pyfn_mean_from_eccentric(eccentric: f64, e: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::kepler::mean_from_eccentric(eccentric, e)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The orbital period `2 pi sqrt(a^3 / mu)`. +/// +/// Errors: +/// Returns an error for a non-positive semi-major axis or gravitational +/// parameter, which is to say for an unbound orbit, where there is no +/// period. +/// +/// Rust: `astrophysics::kepler::orbit_period` +#[pyfunction] +#[pyo3(name = "orbit_period", signature = (a, mu))] +pub fn pyfn_orbit_period(a: f64, mu: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::kepler::orbit_period(a, mu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The vis-viva speed at radius `r` on an orbit of semi-major axis `a`: +/// `sqrt(mu (2/r - 1/a))`. +/// +/// The equation is conservation of energy rearranged, and it holds for +/// every conic: a positive `a` for an ellipse, negative for a hyperbola, +/// and the parabolic limit `1/a = 0` giving escape speed. That one formula +/// covers all three is the reason it is the workhorse of manoeuvre +/// planning. +/// +/// The formula knows about energy, not about geometry: it returns a speed +/// for any radius up to `2a`, which for a bound orbit reaches past +/// apoapsis at `a(1+e)`. Radii between the two are not on the orbit and +/// the number returned there is the speed a body of that energy *would* +/// have, not one anything reaches. Beyond `2a` the kinetic energy would be +/// negative and there is no answer at all. +/// +/// Errors: +/// Returns an error for a non-positive radius or gravitational parameter, +/// a NaN input, or a radius beyond `2a` on a bound orbit, where the speed +/// would be imaginary. +/// +/// Rust: `astrophysics::kepler::vis_viva` +#[pyfunction] +#[pyo3(name = "vis_viva", signature = (r, a, mu))] +pub fn pyfn_vis_viva(r: f64, a: f64, mu: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::kepler::vis_viva(r, a, mu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The state vectors implied by a set of elements: the inverse of +/// `OrbitalElements::from_state_vectors`. +/// +/// The position and velocity are built in the perifocal frame, where the +/// orbit is a plane conic with periapsis along the x axis, and then +/// rotated into the reference frame by the three Euler angles. Doing it +/// this way rather than by direct formulae is what keeps the retrograde +/// and equatorial cases right: the rotation is the same in every case, +/// and only the angles differ. +/// +/// Errors: +/// Returns an error for a non-positive gravitational parameter, a +/// non-finite element, a negative eccentricity, or a semi-latus rectum +/// that comes out non-positive -- which happens for a degenerate orbit +/// with no extent. +/// +/// Rust: `astrophysics::kepler::state_from_elements` +#[pyfunction] +#[pyo3(name = "state_from_elements", signature = (elements, mu))] +pub fn pyfn_state_from_elements(elements: crate::generated::types::PyOrbitalElementsArg, mu: f64) -> PyResult<(crate::generated::types::PyVec3, crate::generated::types::PyVec3)> { + let elements = elements.0; + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::kepler::state_from_elements(&elements, mu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((crate::generated::types::PyVec3 { inner: __v.0 }, crate::generated::types::PyVec3 { inner: __v.1 })) +} + +/// Propagates a two-body state forward by `dt` using Lagrange's f and g +/// functions. +/// +/// The trick is that the new position is a *linear combination of the old +/// position and velocity*: `r = f r0 + g v0`, with `f` and `g` scalars +/// depending only on the change in eccentric anomaly. The orbit plane is +/// therefore preserved exactly by construction, whatever the arithmetic +/// does -- which is why this is used in preference to integrating the +/// equations of motion when the two-body assumption holds. +/// +/// Elliptic and hyperbolic orbits are handled by their own anomaly +/// solvers. A parabolic orbit -- eccentricity exactly one -- has neither +/// and is refused rather than approximated. +/// +/// Errors: +/// Returns an error for a non-positive gravitational parameter, a +/// non-finite input, a degenerate or parabolic orbit, or an anomaly +/// solver that does not converge. +/// +/// Rust: `astrophysics::kepler::propagate_kepler` +#[pyfunction] +#[pyo3(name = "propagate_kepler", signature = (r0, v0, dt, mu))] +pub fn pyfn_propagate_kepler(r0: crate::generated::types::PyVec3Arg, v0: crate::generated::types::PyVec3Arg, dt: f64, mu: f64) -> PyResult<(crate::generated::types::PyVec3, crate::generated::types::PyVec3)> { + let r0 = r0.0; + let v0 = v0.0; + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::kepler::propagate_kepler(r0, v0, dt, mu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((crate::generated::types::PyVec3 { inner: __v.0 }, crate::generated::types::PyVec3 { inner: __v.1 })) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_kepler_solve_elliptic, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kepler_solve_hyperbolic, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_true_from_eccentric, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_eccentric_from_true, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mean_from_eccentric, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_orbit_period, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_vis_viva, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_state_from_elements, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_propagate_kepler, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_astrophysics__lagrange.rs b/bindings/python/src/generated/m_astrophysics__lagrange.rs new file mode 100644 index 0000000..dfc5892 --- /dev/null +++ b/bindings/python/src/generated/m_astrophysics__lagrange.rs @@ -0,0 +1,73 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Computes the Hill sphere radius: r_H = d (m / 3M)^(1/3). +/// +/// Rust: `astrophysics::lagrange::hill_radius` +#[pyfunction] +#[pyo3(name = "hill_radius", signature = (distance, body_mass, primary_mass))] +pub fn pyfn_hill_radius(distance: f64, body_mass: f64, primary_mass: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::lagrange::hill_radius(distance, body_mass, primary_mass)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Computes all five Lagrange points (L1-L5) for a two-body system in the co-rotating frame. +/// +/// Rust: `astrophysics::lagrange::lagrange_points` +#[pyfunction] +#[pyo3(name = "lagrange_points", signature = (primary_pos, primary_mass, body_pos, body_vel, body_mass))] +pub fn pyfn_lagrange_points(primary_pos: crate::generated::types::PyVec3Arg, primary_mass: f64, body_pos: crate::generated::types::PyVec3Arg, body_vel: crate::generated::types::PyVec3Arg, body_mass: f64) -> PyResult> { + let primary_pos = primary_pos.0; + let body_pos = body_pos.0; + let body_vel = body_vel.0; + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::lagrange::lagrange_points(primary_pos, primary_mass, body_pos, body_vel, body_mass)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) +} + +/// Computes the circular orbital velocity: v_c = √(μ/r). +/// +/// Rust: `astrophysics::lagrange::circular_velocity` +#[pyfunction] +#[pyo3(name = "circular_velocity", signature = (mu, distance))] +pub fn pyfn_circular_velocity(mu: f64, distance: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::lagrange::circular_velocity(mu, distance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Computes the ratio of current speed to escape velocity: v / v_esc. +/// +/// Rust: `astrophysics::lagrange::escape_ratio` +#[pyfunction] +#[pyo3(name = "escape_ratio", signature = (speed, escape_vel))] +pub fn pyfn_escape_ratio(speed: f64, escape_vel: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::lagrange::escape_ratio(speed, escape_vel)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_hill_radius, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lagrange_points, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_circular_velocity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_escape_ratio, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_astrophysics__lambert.rs b/bindings/python/src/generated/m_astrophysics__lambert.rs new file mode 100644 index 0000000..f7f51bb --- /dev/null +++ b/bindings/python/src/generated/m_astrophysics__lambert.rs @@ -0,0 +1,132 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// The Stumpff function `C(z)`. +/// +/// `(1 - cos sqrt(z))/z` for positive `z` and the hyperbolic analogue for +/// negative, both of which are `0/0` at the origin. The series +/// `1/2 - z/24 + z^2/720 - ...` is used near zero, where the closed forms +/// lose their leading digits to cancellation, and the positive branch is +/// evaluated as `2 sin^2(sqrt(z)/2)/z` so that it stays accurate at the +/// other end of the range as well. +/// +/// Rust: `astrophysics::lambert::stumpff_c` +#[pyfunction] +#[pyo3(name = "stumpff_c", signature = (z))] +pub fn pyfn_stumpff_c(z: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::lambert::stumpff_c(z)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Stumpff function `S(z)`. +/// +/// `(sqrt(z) - sin sqrt(z))/z^(3/2)` for positive `z`, with the series +/// `1/6 - z/120 + z^2/5040 - ...` near the origin for the same reason as +/// `stumpff_c` -- and worse, since the numerator there is a difference +/// of two nearly equal quantities that agree to three orders. +/// +/// Rust: `astrophysics::lambert::stumpff_s` +#[pyfunction] +#[pyo3(name = "stumpff_s", signature = (z))] +pub fn pyfn_stumpff_s(z: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::lambert::stumpff_s(z)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Solves Lambert's problem by universal variables, returning the +/// departure and arrival velocities. +/// +/// `prograde` selects the transfer direction: true takes the short way +/// round in the sense of increasing right ascension, false the long way. +/// The two are genuinely different orbits with different flight paths and +/// different costs, and which one is wanted is not deducible from the +/// endpoints -- the transfer angle is `theta` one way and `2 pi - theta` +/// the other. +/// +/// The iteration is bisection on `z`. Bisection rather than Newton because +/// the flight time is monotone in `z`, so bisection cannot fail, and the +/// derivative a Newton step needs is itself delicate near the parabolic +/// point. +/// +/// Accuracy degrades as the transfer angle approaches `pi`. The +/// velocities are recovered as `(r2 - f r1)/g`, and near a half turn +/// `f` approaches one with `r2` near `-r1`, so the numerator is a +/// difference of nearly equal vectors. Over three thousand randomised +/// geometries the worst departure velocity was off by a part in 1e8, and +/// that case had a transfer angle of 179.99 degrees. Exactly `pi` is +/// refused; the approach to it is merely imprecise. +/// +/// Errors: +/// Returns an error for a non-positive gravitational parameter or flight +/// time, a position at the origin, or a transfer angle of zero or exactly +/// `pi`, where the plane is undefined and infinitely many orbits connect +/// the points. +/// +/// Rust: `astrophysics::lambert::lambert_universal` +#[pyfunction] +#[pyo3(name = "lambert_universal", signature = (r1, r2, tof, mu, prograde))] +pub fn pyfn_lambert_universal(r1: crate::generated::types::PyVec3Arg, r2: crate::generated::types::PyVec3Arg, tof: f64, mu: f64, prograde: bool) -> PyResult<(crate::generated::types::PyVec3, crate::generated::types::PyVec3)> { + let r1 = r1.0; + let r2 = r2.0; + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::lambert::lambert_universal(r1, r2, tof, mu, prograde)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((crate::generated::types::PyVec3 { inner: __v.0 }, crate::generated::types::PyVec3 { inner: __v.1 })) +} + +/// A porkchop grid of departure characteristic energies. +/// +/// Entry `[i][j]` is the departure `C3 = v_infinity^2` for leaving +/// `departures[i]` and arriving at `arrivals[j]`, with the flight time +/// taken as the difference of their epochs. `None` marks a pair with no +/// transfer: a non-positive flight time, a degenerate geometry, or a +/// duration outside what one revolution allows. +/// +/// `C3` rather than delta-v because it is what a launch vehicle's +/// performance is quoted against: the energy left over after escaping, +/// which is what the upper stage must supply. The characteristic ridges +/// and islands of a real porkchop plot come from the two branches of the +/// transfer -- Type I below a half revolution and Type II above -- meeting +/// where the transfer angle passes `pi` and the solution degenerates. +/// +/// Errors: +/// Returns an error for an empty grid, a non-positive gravitational +/// parameter, or more than a million cells. +/// +/// Rust: `astrophysics::lambert::porkchop_data` +#[pyfunction] +#[pyo3(name = "porkchop_data", signature = (departures, arrivals, mu, prograde))] +pub fn pyfn_porkchop_data<'py>(py: Python<'py>, departures: Vec<(f64, crate::generated::types::PyVec3Arg, crate::generated::types::PyVec3Arg)>, arrivals: Vec<(f64, crate::generated::types::PyVec3Arg, crate::generated::types::PyVec3Arg)>, mu: f64, prograde: bool) -> PyResult>>> { + let departures = departures.into_iter().map(|__e| (__e.0, __e.1.0, __e.2.0)).collect::>(); + let arrivals = arrivals.into_iter().map(|__e| (__e.0, __e.1.0, __e.2.0)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::astrophysics::lambert::porkchop_data(&departures, &arrivals, mu, prograde))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| __x.into_iter().map(|__x| __x.map(|__x| __x)).collect::>()).collect::>()) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_stumpff_c, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_stumpff_s, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lambert_universal, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_porkchop_data, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_astrophysics__magnetosphere.rs b/bindings/python/src/generated/m_astrophysics__magnetosphere.rs new file mode 100644 index 0000000..80e8354 --- /dev/null +++ b/bindings/python/src/generated/m_astrophysics__magnetosphere.rs @@ -0,0 +1,110 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Estimates a celestial body's magnetic dipole moment based on body type, mass, and temperature. +/// +/// Rust: `astrophysics::magnetosphere::magnetic_moment` +#[pyfunction] +#[pyo3(name = "magnetic_moment", signature = (body_type, mass, temperature))] +pub fn pyfn_magnetic_moment(body_type: crate::generated::types::PyCelestialBodyType, mass: f64, temperature: f64) -> PyResult { + let body_type = body_type.to_rust(); + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::magnetosphere::magnetic_moment(body_type, mass, temperature)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Computes the magnetosphere standoff radius from the magnetic moment and body radius. +/// +/// Rust: `astrophysics::magnetosphere::magnetosphere_radius` +#[pyfunction] +#[pyo3(name = "magnetosphere_radius", signature = (collision_radius, moment))] +pub fn pyfn_magnetosphere_radius(collision_radius: f64, moment: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::magnetosphere::magnetosphere_radius(collision_radius, moment)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Computes the magnetic dipole field at a point: B = (3(m·r̂)r̂ - m) / r³. +/// +/// Rust: `astrophysics::magnetosphere::dipole_field` +#[pyfunction] +#[pyo3(name = "dipole_field", signature = (center, moment_vec, point))] +pub fn pyfn_dipole_field(center: crate::generated::types::PyVec3Arg, moment_vec: crate::generated::types::PyVec3Arg, point: crate::generated::types::PyVec3Arg) -> PyResult { + let center = center.0; + let moment_vec = moment_vec.0; + let point = point.0; + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::magnetosphere::dipole_field(center, moment_vec, point)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Computes the superposition of multiple magnetic dipole fields at a point. +/// +/// Rust: `astrophysics::magnetosphere::total_field` +#[pyfunction] +#[pyo3(name = "total_field", signature = (centers, moments, point))] +pub fn pyfn_total_field(centers: Vec, moments: Vec, point: crate::generated::types::PyVec3Arg) -> PyResult { + let centers = centers.into_iter().map(|__e| __e.0).collect::>(); + let moments = moments.into_iter().map(|__e| __e.0).collect::>(); + let point = point.0; + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::magnetosphere::total_field(¢ers, &moments, point)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Traces a magnetic field line from a seed point using adaptive Euler stepping, returning positions and field strengths. +/// +/// Rust: `astrophysics::magnetosphere::trace_field_line` +#[pyfunction] +#[pyo3(name = "trace_field_line", signature = (centers, moments, seed, forward, step_size, max_distance, max_points, min_field_strength, body_radii))] +pub fn pyfn_trace_field_line(centers: Vec, moments: Vec, seed: crate::generated::types::PyVec3Arg, forward: bool, step_size: f64, max_distance: f64, max_points: usize, min_field_strength: f64, body_radii: Vec) -> PyResult> { + let centers = centers.into_iter().map(|__e| __e.0).collect::>(); + let moments = moments.into_iter().map(|__e| __e.0).collect::>(); + let seed = seed.0; + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::magnetosphere::trace_field_line(¢ers, &moments, seed, forward, step_size, max_distance, max_points, min_field_strength, &body_radii)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (crate::generated::types::PyVec3 { inner: __x.0 }, __x.1)).collect::>()) +} + +/// Generates seed points on a sphere using the golden-angle spiral for uniform distribution. +/// +/// Rust: `astrophysics::magnetosphere::generate_seed_points` +#[pyfunction] +#[pyo3(name = "generate_seed_points", signature = (center, radius, num_seeds))] +pub fn pyfn_generate_seed_points(center: crate::generated::types::PyVec3Arg, radius: f64, num_seeds: usize) -> PyResult> { + let center = center.0; + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::magnetosphere::generate_seed_points(center, radius, num_seeds)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_magnetic_moment, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_magnetosphere_radius, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dipole_field, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_total_field, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_trace_field_line, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_generate_seed_points, m)?)?; + m.add("DEFAULT_MAX_LINES_PER_BODY", rust_physics_engine::astrophysics::magnetosphere::DEFAULT_MAX_LINES_PER_BODY)?; + m.add("DEFAULT_POINTS_PER_LINE", rust_physics_engine::astrophysics::magnetosphere::DEFAULT_POINTS_PER_LINE)?; + m.add("DEFAULT_MIN_FIELD_STRENGTH", rust_physics_engine::astrophysics::magnetosphere::DEFAULT_MIN_FIELD_STRENGTH)?; + m.add("SOLAR_TEMPERATURE", rust_physics_engine::astrophysics::magnetosphere::SOLAR_TEMPERATURE)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_astrophysics__maneuvers.rs b/bindings/python/src/generated/m_astrophysics__maneuvers.rs new file mode 100644 index 0000000..32e521f --- /dev/null +++ b/bindings/python/src/generated/m_astrophysics__maneuvers.rs @@ -0,0 +1,245 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// The delta-v of a combined speed change and plane change, by the law of +/// cosines. +/// +/// `sqrt(v1^2 + v2^2 - 2 v1 v2 cos(di))`. Doing both at once is always +/// cheaper than doing them one after the other, because the two vector +/// changes partly cancel -- the triangle inequality, applied to velocity. +/// The saving is largest when the plane change is large, which is why an +/// inclination change is combined with an apoapsis burn wherever the +/// mission allows. +/// +/// Errors: +/// Returns an error for a negative speed or a non-finite input. +/// +/// Rust: `astrophysics::maneuvers::combined_maneuver` +#[pyfunction] +#[pyo3(name = "combined_maneuver", signature = (v1, v2, plane_change))] +pub fn pyfn_combined_maneuver(v1: f64, v2: f64, plane_change: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::maneuvers::combined_maneuver(v1, v2, plane_change)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The radius of a body's sphere of influence: +/// `a (m_body / m_primary)^(2/5)`. +/// +/// Inside it the body's gravity dominates the primary's for the purposes +/// of a patched-conic approximation, and outside it does not. The +/// two-fifths power is not the equal-force radius, which would be a +/// square root: it comes from comparing the *perturbing* accelerations +/// rather than the direct ones, and it is the boundary at which +/// switching which body you orbit makes the smaller error. +/// +/// The sphere is a fiction. Gravity has no boundary, and a real +/// trajectory feels both bodies throughout; the patched conic is an +/// approximation whose error is largest exactly at the crossing, where +/// the neglected body's pull is at its relative peak. +/// +/// Errors: +/// Returns an error for a non-positive distance or mass, or a body more +/// massive than its primary. +/// +/// Rust: `astrophysics::maneuvers::sphere_of_influence` +#[pyfunction] +#[pyo3(name = "sphere_of_influence", signature = (distance, body_mass, primary_mass))] +pub fn pyfn_sphere_of_influence(distance: f64, body_mass: f64, primary_mass: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::maneuvers::sphere_of_influence(distance, body_mass, primary_mass)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The delta-v to leave a circular parking orbit on a hyperbola with the +/// given excess speed: `sqrt(v_infinity^2 + 2 mu / r) - sqrt(mu / r)`. +/// +/// The first term is the speed needed at radius `r` to arrive at infinity +/// still moving at `v_infinity`; the second is what a circular orbit +/// already provides. The gap is small compared with either, which is the +/// Oberth effect in its most practical form: escaping from low orbit +/// costs about 0.41 of the circular speed, and the deeper the parking +/// orbit the smaller that fraction becomes. +/// +/// Errors: +/// Returns an error for a non-positive radius or gravitational parameter, +/// a negative excess speed, or a non-finite input. +/// +/// Rust: `astrophysics::maneuvers::patched_conic_escape` +#[pyfunction] +#[pyo3(name = "patched_conic_escape", signature = (parking_radius, mu, v_infinity))] +pub fn pyfn_patched_conic_escape(parking_radius: f64, mu: f64, v_infinity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::maneuvers::patched_conic_escape(parking_radius, mu, v_infinity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The turn angle of a hyperbolic flyby: +/// `2 arcsin(1 / (1 + r_p v_inf^2 / mu))`. +/// +/// A gravity assist changes the direction of the excess velocity, not its +/// magnitude -- in the *planet's* frame the spacecraft arrives and leaves +/// at the same speed. The gain is in the sun's frame, where rotating the +/// excess velocity vector adds or subtracts from the planet's orbital +/// motion, and the planet loses exactly as much momentum as the +/// spacecraft gains. +/// +/// The turn is largest for a slow approach and a close pass. A fast +/// spacecraft is barely deflected, which is why an assist buys less the +/// more energy you already have. +/// +/// Errors: +/// Returns an error for a non-positive periapsis, gravitational parameter +/// or excess speed, or a non-finite input. +/// +/// Rust: `astrophysics::maneuvers::gravity_assist_deflection` +#[pyfunction] +#[pyo3(name = "gravity_assist_deflection", signature = (v_infinity, periapsis, mu))] +pub fn pyfn_gravity_assist_deflection(v_infinity: f64, periapsis: f64, mu: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::maneuvers::gravity_assist_deflection(v_infinity, periapsis, mu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The speed after an impulsive burn of `delta_v` made at radius `r`, +/// through the energy it buys. +/// +/// The point of the function is the comparison it makes possible: the same +/// delta-v spent at two radii leaves the craft with different energies, +/// and the difference is `v dv` -- large where `v` is large, which is deep +/// in the well. Burning at periapsis rather than apoapsis can double the +/// escape energy for the same fuel. +/// +/// Errors: +/// Returns an error for a non-positive radius or gravitational parameter, +/// a negative speed, or a non-finite input. +/// +/// Rust: `astrophysics::maneuvers::oberth_effect_dv` +#[pyfunction] +#[pyo3(name = "oberth_effect_dv", signature = (speed, delta_v, radius, mu))] +pub fn pyfn_oberth_effect_dv(speed: f64, delta_v: f64, radius: f64, mu: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::maneuvers::oberth_effect_dv(speed, delta_v, radius, mu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The nodal regression rate from the Earth's oblateness, in radians per +/// second. +/// +/// `-3/2 n J2 (R/p)^2 cos(i)`, with `p = a(1 - e^2)` and `n` the mean +/// motion. The `cos i` is what makes the whole thing useful: the drift is +/// westward for a prograde orbit, zero at exactly ninety degrees, and +/// eastward beyond it. A retrograde orbit at the right inclination +/// therefore drifts eastward at precisely the rate the Earth goes round +/// the sun -- see `sun_synchronous_inclination`. +/// +/// J2 dominates every other perturbation in low orbit by three orders of +/// magnitude, which is why a first-order treatment of it is worth more +/// than a careful treatment of anything else. +/// +/// Errors: +/// Returns an error for a non-positive semi-major axis, body radius or +/// gravitational parameter, an eccentricity outside `[0, 1)`, or a +/// non-finite input. +/// +/// Rust: `astrophysics::maneuvers::j2_raan_drift` +#[pyfunction] +#[pyo3(name = "j2_raan_drift", signature = (a, e, inclination, j2, body_radius, mu))] +pub fn pyfn_j2_raan_drift(a: f64, e: f64, inclination: f64, j2: f64, body_radius: f64, mu: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::maneuvers::j2_raan_drift(a, e, inclination, j2, body_radius, mu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The inclination at which J2 makes an orbit sun-synchronous. +/// +/// The node must drift eastward by one turn a year, which is +/// `1.991e-7 rad/s`. Solving `j2_raan_drift` for the inclination gives +/// a value just past ninety degrees -- about 98 degrees for a low orbit -- +/// and it must be retrograde, since a prograde orbit's node drifts the +/// wrong way. +/// +/// The orbit is sun-synchronous in the sense that it crosses the equator +/// at the same local solar time every pass, which is what makes imaging +/// comparable between days. It says nothing about lighting at high +/// latitudes, where the geometry differs. +/// +/// Errors: +/// Returns an error for a non-positive semi-major axis, body radius or +/// gravitational parameter, an eccentricity outside `[0, 1)`, or an orbit +/// for which no inclination gives the required drift -- which happens +/// when the orbit is too high for J2 to turn it fast enough. +/// +/// Rust: `astrophysics::maneuvers::sun_synchronous_inclination` +#[pyfunction] +#[pyo3(name = "sun_synchronous_inclination", signature = (a, e, j2, body_radius, mu, drift_per_second))] +pub fn pyfn_sun_synchronous_inclination(a: f64, e: f64, j2: f64, body_radius: f64, mu: f64, drift_per_second: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::maneuvers::sun_synchronous_inclination(a, e, j2, body_radius, mu, drift_per_second)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The ground track of an orbit: `(longitude, latitude)` in radians at +/// each sample, accounting for the body turning underneath. +/// +/// The longitude drift per orbit is what makes a track a spiral rather +/// than a closed curve: the body turns by `rotation_rate * period` while +/// the orbit plane stays put, so each pass crosses the equator further +/// west. A track closes only when the period is a rational fraction of +/// the rotation, which is what a repeat-ground-track orbit is designed +/// for. +/// +/// The latitude never exceeds the inclination, and reaches it exactly +/// twice per orbit. That bound is the reason a polar orbit is needed to +/// see the poles at all. +/// +/// Errors: +/// Returns an error for a bad state, a non-positive gravitational +/// parameter, no samples, more than a million, or a propagation failure. +/// +/// Rust: `astrophysics::maneuvers::ground_track` +#[pyfunction] +#[pyo3(name = "ground_track", signature = (r0, v0, mu, rotation_rate, duration, samples))] +pub fn pyfn_ground_track<'py>(py: Python<'py>, r0: crate::generated::types::PyVec3Arg, v0: crate::generated::types::PyVec3Arg, mu: f64, rotation_rate: f64, duration: f64, samples: usize) -> PyResult> { + let r0 = r0.0; + let v0 = v0.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::astrophysics::maneuvers::ground_track(r0, v0, mu, rotation_rate, duration, samples))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_combined_maneuver, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sphere_of_influence, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_patched_conic_escape, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gravity_assist_deflection, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_oberth_effect_dv, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_j2_raan_drift, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sun_synchronous_inclination, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ground_track, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_astrophysics__nbody.rs b/bindings/python/src/generated/m_astrophysics__nbody.rs new file mode 100644 index 0000000..940148e --- /dev/null +++ b/bindings/python/src/generated/m_astrophysics__nbody.rs @@ -0,0 +1,135 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Computes gravitational acceleration on body `idx` via direct O(N) pairwise summation with Plummer softening. +/// +/// Rust: `astrophysics::nbody::compute_acceleration` +#[pyfunction] +#[pyo3(name = "compute_acceleration", signature = (bodies, idx, softening))] +pub fn pyfn_compute_acceleration(bodies: Vec, idx: usize, softening: f64) -> PyResult { + let bodies = bodies.into_iter().map(|__e| __e.inner).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::nbody::compute_acceleration(&bodies, idx, softening)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Initializes acceleration vectors for all bodies by computing pairwise gravitational interactions. +/// +/// Rust: `astrophysics::nbody::init_accelerations` +#[pyfunction] +#[pyo3(name = "init_accelerations", signature = (bodies, softening))] +pub fn pyfn_init_accelerations<'py>(bodies: pyo3::Bound<'py, pyo3::PyAny>, softening: f64) -> PyResult<()> { + let mut bodies__v: Vec = bodies.extract::>()?.into_iter().map(|__e| __e.inner).collect(); + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::nbody::init_accelerations(&mut bodies__v, softening)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + crate::runtime::coerce::write_back_objects(&bodies, bodies__v.into_iter().map(|__e| crate::generated::types::PyBody { inner: __e }).collect::>())?; + Ok(()) +} + +/// Performs one velocity Verlet integration step (kick-drift-kick) by +/// delegating to the generic symplectic integrator +/// `numerical::ode::symplectic::velocity_verlet` over the flattened +/// phase-space state. +/// +/// Rust: `astrophysics::nbody::step_verlet` +#[pyfunction] +#[pyo3(name = "step_verlet", signature = (bodies, dt, softening))] +pub fn pyfn_step_verlet<'py>(bodies: pyo3::Bound<'py, pyo3::PyAny>, dt: f64, softening: f64) -> PyResult<()> { + let mut bodies__v: Vec = bodies.extract::>()?.into_iter().map(|__e| __e.inner).collect(); + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::nbody::step_verlet(&mut bodies__v, dt, softening)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + crate::runtime::coerce::write_back_objects(&bodies, bodies__v.into_iter().map(|__e| crate::generated::types::PyBody { inner: __e }).collect::>())?; + Ok(()) +} + +/// Computes the total kinetic energy of all bodies: KE = Σ ½m_i v_i². +/// +/// Rust: `astrophysics::nbody::kinetic_energy` +#[pyfunction] +#[pyo3(name = "kinetic_energy", signature = (bodies))] +pub fn pyfn_kinetic_energy<'py>(py: Python<'py>, bodies: Vec) -> PyResult { + let bodies = bodies.into_iter().map(|__e| __e.inner).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::astrophysics::nbody::kinetic_energy(&bodies))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Computes the total gravitational potential energy: PE = -Σ G m_i m_j / r_ij (with Plummer softening). +/// +/// Rust: `astrophysics::nbody::potential_energy` +#[pyfunction] +#[pyo3(name = "potential_energy", signature = (bodies, softening))] +pub fn pyfn_potential_energy<'py>(py: Python<'py>, bodies: Vec, softening: f64) -> PyResult { + let bodies = bodies.into_iter().map(|__e| __e.inner).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::astrophysics::nbody::potential_energy(&bodies, softening))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Computes the total mechanical energy as the sum of kinetic and potential energy. +/// +/// Rust: `astrophysics::nbody::total_energy` +#[pyfunction] +#[pyo3(name = "total_energy", signature = (bodies, softening))] +pub fn pyfn_total_energy<'py>(py: Python<'py>, bodies: Vec, softening: f64) -> PyResult { + let bodies = bodies.into_iter().map(|__e| __e.inner).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::astrophysics::nbody::total_energy(&bodies, softening))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Computes the center of mass position: R_cm = Σ(m_i r_i) / Σ(m_i). +/// +/// Rust: `astrophysics::nbody::center_of_mass` +#[pyfunction] +#[pyo3(name = "center_of_mass", signature = (bodies))] +pub fn pyfn_center_of_mass(bodies: Vec) -> PyResult { + let bodies = bodies.into_iter().map(|__e| __e.inner).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::nbody::center_of_mass(&bodies)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Computes the total linear momentum: p = Σ(m_i v_i). +/// +/// Rust: `astrophysics::nbody::total_momentum` +#[pyfunction] +#[pyo3(name = "total_momentum", signature = (bodies))] +pub fn pyfn_total_momentum(bodies: Vec) -> PyResult { + let bodies = bodies.into_iter().map(|__e| __e.inner).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::nbody::total_momentum(&bodies)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_compute_acceleration, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_init_accelerations, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_step_verlet, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kinetic_energy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_potential_energy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_total_energy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_center_of_mass, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_total_momentum, m)?)?; + m.add("DEFAULT_DT", rust_physics_engine::astrophysics::nbody::DEFAULT_DT)?; + m.add("DEFAULT_SOFTENING", rust_physics_engine::astrophysics::nbody::DEFAULT_SOFTENING)?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_astrophysics__orbital_elements.rs b/bindings/python/src/generated/m_astrophysics__orbital_elements.rs new file mode 100644 index 0000000..66cf501 --- /dev/null +++ b/bindings/python/src/generated/m_astrophysics__orbital_elements.rs @@ -0,0 +1,217 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Computes specific orbital energy: ε = v²/2 - μ/r. +/// +/// Rust: `astrophysics::orbital_elements::specific_orbital_energy` +#[pyfunction] +#[pyo3(name = "specific_orbital_energy", signature = (mu, r, v))] +pub fn pyfn_specific_orbital_energy(mu: f64, r: f64, v: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::orbital_elements::specific_orbital_energy(mu, r, v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Computes specific angular momentum vector: h = r × v. +/// +/// Rust: `astrophysics::orbital_elements::specific_angular_momentum` +#[pyfunction] +#[pyo3(name = "specific_angular_momentum", signature = (position, velocity))] +pub fn pyfn_specific_angular_momentum(position: crate::generated::types::PyVec3Arg, velocity: crate::generated::types::PyVec3Arg) -> PyResult { + let position = position.0; + let velocity = velocity.0; + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::orbital_elements::specific_angular_momentum(position, velocity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Computes the eccentricity vector: e = (v × h)/μ - r̂, pointing toward periapsis. +/// +/// Rust: `astrophysics::orbital_elements::eccentricity_vector` +#[pyfunction] +#[pyo3(name = "eccentricity_vector", signature = (position, velocity, mu))] +pub fn pyfn_eccentricity_vector(position: crate::generated::types::PyVec3Arg, velocity: crate::generated::types::PyVec3Arg, mu: f64) -> PyResult { + let position = position.0; + let velocity = velocity.0; + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::orbital_elements::eccentricity_vector(position, velocity, mu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Computes the orbital eccentricity as the magnitude of the eccentricity vector: e = |e_vec|. +/// +/// Rust: `astrophysics::orbital_elements::eccentricity` +#[pyfunction] +#[pyo3(name = "eccentricity", signature = (position, velocity, mu))] +pub fn pyfn_eccentricity(position: crate::generated::types::PyVec3Arg, velocity: crate::generated::types::PyVec3Arg, mu: f64) -> PyResult { + let position = position.0; + let velocity = velocity.0; + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::orbital_elements::eccentricity(position, velocity, mu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Computes the semi-major axis from the vis-viva relation: a = -μ/(2ε). Returns infinity for parabolic orbits. +/// +/// Rust: `astrophysics::orbital_elements::semi_major_axis` +#[pyfunction] +#[pyo3(name = "semi_major_axis", signature = (mu, energy))] +pub fn pyfn_semi_major_axis(mu: f64, energy: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::orbital_elements::semi_major_axis(mu, energy)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Computes the semi-minor axis: b = a√(1 - e²). +/// +/// Rust: `astrophysics::orbital_elements::semi_minor_axis` +#[pyfunction] +#[pyo3(name = "semi_minor_axis", signature = (semi_major, ecc))] +pub fn pyfn_semi_minor_axis(semi_major: f64, ecc: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::orbital_elements::semi_minor_axis(semi_major, ecc)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Computes the periapsis distance: r_p = a(1 - e). +/// +/// Rust: `astrophysics::orbital_elements::periapsis` +#[pyfunction] +#[pyo3(name = "periapsis", signature = (semi_major, ecc))] +pub fn pyfn_periapsis(semi_major: f64, ecc: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::orbital_elements::periapsis(semi_major, ecc)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Computes the apoapsis distance: r_a = a(1 + e). +/// +/// Rust: `astrophysics::orbital_elements::apoapsis` +#[pyfunction] +#[pyo3(name = "apoapsis", signature = (semi_major, ecc))] +pub fn pyfn_apoapsis(semi_major: f64, ecc: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::orbital_elements::apoapsis(semi_major, ecc)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Computes the true anomaly ν from state vectors: ν = acos(e · r / (|e||r|)), adjusted for quadrant. +/// +/// Rust: `astrophysics::orbital_elements::true_anomaly` +#[pyfunction] +#[pyo3(name = "true_anomaly", signature = (position, velocity, mu))] +pub fn pyfn_true_anomaly(position: crate::generated::types::PyVec3Arg, velocity: crate::generated::types::PyVec3Arg, mu: f64) -> PyResult { + let position = position.0; + let velocity = velocity.0; + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::orbital_elements::true_anomaly(position, velocity, mu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Computes the orbital inclination: i = acos(h_z / |h|). +/// +/// Rust: `astrophysics::orbital_elements::inclination` +#[pyfunction] +#[pyo3(name = "inclination", signature = (angular_momentum))] +pub fn pyfn_inclination(angular_momentum: crate::generated::types::PyVec3Arg) -> PyResult { + let angular_momentum = angular_momentum.0; + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::orbital_elements::inclination(angular_momentum)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Computes the longitude of the ascending node Ω from the nodal vector n = (-h_y, h_x, 0). +/// +/// Rust: `astrophysics::orbital_elements::longitude_of_ascending_node` +#[pyfunction] +#[pyo3(name = "longitude_of_ascending_node", signature = (angular_momentum))] +pub fn pyfn_longitude_of_ascending_node(angular_momentum: crate::generated::types::PyVec3Arg) -> PyResult { + let angular_momentum = angular_momentum.0; + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::orbital_elements::longitude_of_ascending_node(angular_momentum)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Computes the argument of periapsis ω: ω = acos(n · e / (|n||e|)), adjusted for quadrant. +/// +/// Rust: `astrophysics::orbital_elements::argument_of_periapsis` +#[pyfunction] +#[pyo3(name = "argument_of_periapsis", signature = (angular_momentum, ecc_vec))] +pub fn pyfn_argument_of_periapsis(angular_momentum: crate::generated::types::PyVec3Arg, ecc_vec: crate::generated::types::PyVec3Arg) -> PyResult { + let angular_momentum = angular_momentum.0; + let ecc_vec = ecc_vec.0; + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::orbital_elements::argument_of_periapsis(angular_momentum, ecc_vec)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Generates 3D points along an elliptical orbit using the conic section r = p/(1 + e cos θ). +/// +/// Rust: `astrophysics::orbital_elements::orbit_points_ellipse` +#[pyfunction] +#[pyo3(name = "orbit_points_ellipse", signature = (elements, mu, num_points))] +pub fn pyfn_orbit_points_ellipse(elements: crate::generated::types::PyOrbitalElementsArg, mu: f64, num_points: usize) -> PyResult> { + let elements = elements.0; + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::orbital_elements::orbit_points_ellipse(&elements, mu, num_points)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) +} + +/// Generates 3D points along a hyperbolic orbit trajectory using r = p/(1 + e cos θ) with θ bounded by the asymptotes. +/// +/// Rust: `astrophysics::orbital_elements::orbit_points_hyperbola` +#[pyfunction] +#[pyo3(name = "orbit_points_hyperbola", signature = (elements, mu, num_points))] +pub fn pyfn_orbit_points_hyperbola(elements: crate::generated::types::PyOrbitalElementsArg, mu: f64, num_points: usize) -> PyResult> { + let elements = elements.0; + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::orbital_elements::orbit_points_hyperbola(&elements, mu, num_points)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) +} + +/// Returns true if the specific orbital energy indicates a bound orbit: ε < 0. +/// +/// Rust: `astrophysics::orbital_elements::is_bound` +#[pyfunction] +#[pyo3(name = "is_bound", signature = (energy))] +pub fn pyfn_is_bound(energy: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::orbital_elements::is_bound(energy)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_specific_orbital_energy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_specific_angular_momentum, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_eccentricity_vector, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_eccentricity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_semi_major_axis, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_semi_minor_axis, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_periapsis, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_apoapsis, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_true_anomaly, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_inclination, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_longitude_of_ascending_node, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_argument_of_periapsis, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_orbit_points_ellipse, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_orbit_points_hyperbola, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_is_bound, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_astrophysics__tidal.rs b/bindings/python/src/generated/m_astrophysics__tidal.rs new file mode 100644 index 0000000..826c0c6 --- /dev/null +++ b/bindings/python/src/generated/m_astrophysics__tidal.rs @@ -0,0 +1,108 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Computes the Newtonian tidal acceleration magnitude: a_tidal = 2GM/r³. +/// +/// Rust: `astrophysics::tidal::tidal_acceleration_magnitude` +#[pyfunction] +#[pyo3(name = "tidal_acceleration_magnitude", signature = (primary_mass, distance))] +pub fn pyfn_tidal_acceleration_magnitude(primary_mass: f64, distance: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::tidal::tidal_acceleration_magnitude(primary_mass, distance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Computes tidal acceleration with a GR correction factor: a_tidal / (1 - r_s/r). +/// +/// Rust: `astrophysics::tidal::tidal_acceleration_gr_corrected` +#[pyfunction] +#[pyo3(name = "tidal_acceleration_gr_corrected", signature = (primary_mass, distance, schwarzschild_radius))] +pub fn pyfn_tidal_acceleration_gr_corrected(primary_mass: f64, distance: f64, schwarzschild_radius: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::tidal::tidal_acceleration_gr_corrected(primary_mass, distance, schwarzschild_radius)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Computes the rigid-body Roche limit: d = R_p (2 ρ_p / ρ_s)^(1/3). +/// +/// Rust: `astrophysics::tidal::roche_limit_rigid` +#[pyfunction] +#[pyo3(name = "roche_limit_rigid", signature = (primary_radius, primary_density, satellite_density))] +pub fn pyfn_roche_limit_rigid(primary_radius: f64, primary_density: f64, satellite_density: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::tidal::roche_limit_rigid(primary_radius, primary_density, satellite_density)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Computes the fluid-body Roche limit: d = 2.44 R_p (ρ_p / ρ_s)^(1/3). +/// +/// Rust: `astrophysics::tidal::roche_limit_fluid` +#[pyfunction] +#[pyo3(name = "roche_limit_fluid", signature = (primary_radius, primary_density, satellite_density))] +pub fn pyfn_roche_limit_fluid(primary_radius: f64, primary_density: f64, satellite_density: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::tidal::roche_limit_fluid(primary_radius, primary_density, satellite_density)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Computes the ratio of tidal force to self-gravity on a body's surface: (a_tidal × R_body) / (GM_body / R_body²). +/// +/// Rust: `astrophysics::tidal::tidal_force_ratio` +#[pyfunction] +#[pyo3(name = "tidal_force_ratio", signature = (primary_mass, body_mass, body_radius, distance))] +pub fn pyfn_tidal_force_ratio(primary_mass: f64, body_mass: f64, body_radius: f64, distance: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::tidal::tidal_force_ratio(primary_mass, body_mass, body_radius, distance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Computes the tidal tensor eigenvalues (radial, tangential): (2GM/r³, -GM/r³). +/// +/// Rust: `astrophysics::tidal::tidal_tensor_eigenvalues` +#[pyfunction] +#[pyo3(name = "tidal_tensor_eigenvalues", signature = (primary_mass, distance))] +pub fn pyfn_tidal_tensor_eigenvalues(primary_mass: f64, distance: f64) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::tidal::tidal_tensor_eigenvalues(primary_mass, distance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Computes the Roche potential in the co-rotating frame: Φ = -Gm₁/r₁ - Gm₂/r₂ - ½ω²r_com². +/// +/// Rust: `astrophysics::tidal::roche_potential` +#[pyfunction] +#[pyo3(name = "roche_potential", signature = (x, z, m1, pos1, m2, pos2))] +pub fn pyfn_roche_potential(x: f64, z: f64, m1: f64, pos1: crate::generated::types::PyVec3Arg, m2: f64, pos2: crate::generated::types::PyVec3Arg) -> PyResult { + let pos1 = pos1.0; + let pos2 = pos2.0; + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::tidal::roche_potential(x, z, m1, pos1, m2, pos2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_tidal_acceleration_magnitude, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tidal_acceleration_gr_corrected, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_roche_limit_rigid, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_roche_limit_fluid, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tidal_force_ratio, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tidal_tensor_eigenvalues, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_roche_potential, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_astrophysics__time_systems.rs b/bindings/python/src/generated/m_astrophysics__time_systems.rs new file mode 100644 index 0000000..c4f7ea5 --- /dev/null +++ b/bindings/python/src/generated/m_astrophysics__time_systems.rs @@ -0,0 +1,146 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// The Julian date of a Gregorian calendar moment. +/// +/// Uses the standard Fliegel-Van Flandern arithmetic, shifting January +/// and February into the previous year so the leap-day irregularity falls +/// at the end. The count begins at noon, not midnight -- a convention +/// from before electric light, kept because it puts a single night's +/// observations inside one Julian day. +/// +/// Proleptic Gregorian throughout: dates before the 1582 reform are given +/// the Gregorian rule rather than the Julian one, which is what almost +/// every astronomical application wants and is not what a historian +/// wants. +/// +/// Errors: +/// Returns an error for a month outside 1..=12, a day outside 1..=31, a +/// time component out of range, or a non-finite second. +/// +/// Rust: `astrophysics::time_systems::julian_date` +#[pyfunction] +#[pyo3(name = "julian_date", signature = (year, month, day, hour, minute, second))] +pub fn pyfn_julian_date(year: i32, month: u32, day: u32, hour: u32, minute: u32, second: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::time_systems::julian_date(year, month, day, hour, minute, second)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The Gregorian calendar moment of a Julian date, as +/// `(year, month, day, hour, minute, second)`. +/// +/// The inverse of `julian_date`, proleptic Gregorian throughout to keep +/// it so, and exact to the limits of the representation: a Julian date near the present carries about 2.5 +/// million days, so a double resolves it to some 20 microseconds. That is +/// why serious work splits the date into an integer part and a fraction, +/// which this does not. +/// +/// Errors: +/// Returns an error for a non-finite Julian date or one outside the range +/// the arithmetic covers. +/// +/// Rust: `astrophysics::time_systems::jd_to_calendar` +#[pyfunction] +#[pyo3(name = "jd_to_calendar", signature = (jd))] +pub fn pyfn_jd_to_calendar(jd: f64) -> PyResult<(i32, u32, u32, u32, u32, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::time_systems::jd_to_calendar(jd)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1, __v.2, __v.3, __v.4, __v.5)) +} + +/// Greenwich mean sidereal time in radians, from a Julian date. +/// +/// The IAU 1982 polynomial in Julian centuries from J2000. The linear +/// coefficient, `8_640_184.812_866` seconds per century, is the whole +/// content: divided by the century's 36525 days it says the Earth gains +/// about 236.6 seconds of sidereal time per solar day, which is the four +/// minutes by which the stars rise earlier each night. +/// +/// "Mean" means the equinox is the smoothly precessing one, without +/// nutation. Apparent sidereal time adds the equation of the equinoxes, +/// up to about a second of time, which matters for pointing a large +/// telescope and not for anything here. +/// +/// Errors: +/// Returns an error for a non-finite or out-of-range Julian date. +/// +/// Rust: `astrophysics::time_systems::gmst` +#[pyfunction] +#[pyo3(name = "gmst", signature = (jd))] +pub fn pyfn_gmst(jd: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::time_systems::gmst(jd)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Local mean sidereal time: Greenwich's plus the observer's longitude. +/// +/// East longitude is positive. The result is what an object's right +/// ascension must equal for it to be due south, which is what makes it +/// the natural clock for an observatory. +/// +/// Errors: +/// As `gmst`, plus a non-finite longitude. +/// +/// Rust: `astrophysics::time_systems::local_sidereal` +#[pyfunction] +#[pyo3(name = "local_sidereal", signature = (jd, longitude))] +pub fn pyfn_local_sidereal(jd: f64, longitude: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::time_systems::local_sidereal(jd, longitude)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The Julian date of a two-line element set's epoch field. +/// +/// TLEs carry the epoch as `YYDDD.DDDDDDDD`: a two-digit year and the +/// fractional day of that year. The two-digit year is resolved by the +/// convention the format itself uses -- 57 through 99 mean the twentieth +/// century and 00 through 56 the twenty-first, chosen because Sputnik +/// went up in 1957 and nothing older has a TLE. +/// +/// Errors: +/// Returns an error for a non-finite epoch, a year outside 0..=99, or a +/// day of year outside `[1, 367)`. +/// +/// Rust: `astrophysics::time_systems::tle_epoch_to_jd` +#[pyfunction] +#[pyo3(name = "tle_epoch_to_jd", signature = (epoch))] +pub fn pyfn_tle_epoch_to_jd(epoch: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::time_systems::tle_epoch_to_jd(epoch)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_julian_date, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_jd_to_calendar, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gmst, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_local_sidereal, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tle_epoch_to_jd, m)?)?; + m.add("J2000", rust_physics_engine::astrophysics::time_systems::J2000)?; + m.add("JULIAN_CENTURY", rust_physics_engine::astrophysics::time_systems::JULIAN_CENTURY)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_atmosphere.rs b/bindings/python/src/generated/m_atmosphere.rs new file mode 100644 index 0000000..c78b110 --- /dev/null +++ b/bindings/python/src/generated/m_atmosphere.rs @@ -0,0 +1,244 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Environment Canada / NWS wind chill formula. +/// Valid for temperature <= 10°C and wind speed >= 4.8 km/h. +/// Returns the wind chill temperature in °C. +/// +/// Rust: `atmosphere::wind_chill` +#[pyfunction] +#[pyo3(name = "wind_chill", signature = (temperature_c, wind_speed_kmh))] +pub fn pyfn_wind_chill(temperature_c: f64, wind_speed_kmh: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::atmosphere::wind_chill(temperature_c, wind_speed_kmh)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert Beaufort scale number to approximate wind speed in m/s. +/// Uses v = 0.836 × B^(3/2). +/// +/// Rust: `atmosphere::beaufort_to_speed` +#[pyfunction] +#[pyo3(name = "beaufort_to_speed", signature = (beaufort))] +pub fn pyfn_beaufort_to_speed(beaufort: u32) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::atmosphere::beaufort_to_speed(beaufort)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert wind speed in m/s to Beaufort scale number (clamped 0–12). +/// Inverse of `beaufort_to_speed`. +/// +/// Rust: `atmosphere::speed_to_beaufort` +#[pyfunction] +#[pyo3(name = "speed_to_beaufort", signature = (speed_ms))] +pub fn pyfn_speed_to_beaufort(speed_ms: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::atmosphere::speed_to_beaufort(speed_ms)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Vertical wind shear: dv/dz = (v_top - v_bottom) / Δz. +/// Returns shear in s⁻¹. +/// +/// Rust: `atmosphere::wind_shear` +#[pyfunction] +#[pyo3(name = "wind_shear", signature = (v_top, v_bottom, height_diff))] +pub fn pyfn_wind_shear(v_top: f64, v_bottom: f64, height_diff: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::atmosphere::wind_shear(v_top, v_bottom, height_diff)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Wind power density: P/A = ½ρv³ (W/m²). +/// +/// Rust: `atmosphere::wind_power_density` +#[pyfunction] +#[pyo3(name = "wind_power_density", signature = (density, velocity))] +pub fn pyfn_wind_power_density(density: f64, velocity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::atmosphere::wind_power_density(density, velocity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Coriolis parameter: f = 2Ω sin(φ). +/// +/// Rust: `atmosphere::coriolis_parameter` +#[pyfunction] +#[pyo3(name = "coriolis_parameter", signature = (latitude_rad))] +pub fn pyfn_coriolis_parameter(latitude_rad: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::atmosphere::coriolis_parameter(latitude_rad)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Coriolis acceleration: a = 2Ωv sin(φ). +/// +/// Rust: `atmosphere::coriolis_acceleration` +#[pyfunction] +#[pyo3(name = "coriolis_acceleration", signature = (velocity, latitude_rad))] +pub fn pyfn_coriolis_acceleration(velocity: f64, latitude_rad: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::atmosphere::coriolis_acceleration(velocity, latitude_rad)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Barometric pressure at a given height using the hypsometric equation. +/// P = P₀ × exp(-Mgh / (RT)) +/// +/// Rust: `atmosphere::barometric_pressure` +#[pyfunction] +#[pyo3(name = "barometric_pressure", signature = (p0, molar_mass, g, height, temperature))] +pub fn pyfn_barometric_pressure(p0: f64, molar_mass: f64, g: f64, height: f64, temperature: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::atmosphere::barometric_pressure(p0, molar_mass, g, height, temperature)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Dry adiabatic lapse rate: Γ = g / cp (K/m). +/// +/// Rust: `atmosphere::dry_adiabatic_lapse_rate` +#[pyfunction] +#[pyo3(name = "dry_adiabatic_lapse_rate", signature = (g, cp))] +pub fn pyfn_dry_adiabatic_lapse_rate(g: f64, cp: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::atmosphere::dry_adiabatic_lapse_rate(g, cp)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Temperature at altitude: T = T₀ - Γ × h. +/// +/// Rust: `atmosphere::temperature_at_altitude` +#[pyfunction] +#[pyo3(name = "temperature_at_altitude", signature = (t0, lapse_rate, altitude))] +pub fn pyfn_temperature_at_altitude(t0: f64, lapse_rate: f64, altitude: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::atmosphere::temperature_at_altitude(t0, lapse_rate, altitude)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Pressure altitude using the standard atmosphere simplification: +/// h = (T₀ / Γ) × (1 - (P / P₀)^0.1903) +/// +/// Rust: `atmosphere::pressure_altitude` +#[pyfunction] +#[pyo3(name = "pressure_altitude", signature = (p0, pressure, lapse_rate, t0))] +pub fn pyfn_pressure_altitude(p0: f64, pressure: f64, lapse_rate: f64, t0: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::atmosphere::pressure_altitude(p0, pressure, lapse_rate, t0)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Density altitude from pressure altitude and temperature deviation: +/// DA = PA + 36.576 × (T - T_std) meters. +/// +/// Rust: `atmosphere::density_altitude` +#[pyfunction] +#[pyo3(name = "density_altitude", signature = (pressure_alt, temperature_c, standard_temp_c))] +pub fn pyfn_density_altitude(pressure_alt: f64, temperature_c: f64, standard_temp_c: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::atmosphere::density_altitude(pressure_alt, temperature_c, standard_temp_c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Scale height: H = RT / (Mg). +/// +/// Rust: `atmosphere::scale_height` +#[pyfunction] +#[pyo3(name = "scale_height", signature = (temperature, molar_mass, g))] +pub fn pyfn_scale_height(temperature: f64, molar_mass: f64, g: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::atmosphere::scale_height(temperature, molar_mass, g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Dew point via the Magnus formula. +/// α = (a×T)/(b+T) + ln(RH), then Td = (b×α)/(a-α). +/// `relative_humidity` is fractional (0.0–1.0). +/// +/// Rust: `atmosphere::dew_point` +#[pyfunction] +#[pyo3(name = "dew_point", signature = (temperature_c, relative_humidity))] +pub fn pyfn_dew_point(temperature_c: f64, relative_humidity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::atmosphere::dew_point(temperature_c, relative_humidity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Relative humidity from temperature and dew point (returns fractional 0.0–1.0). +/// RH = exp((a×Td)/(b+Td) - (a×T)/(b+T)) +/// +/// Rust: `atmosphere::relative_humidity` +#[pyfunction] +#[pyo3(name = "relative_humidity", signature = (temperature_c, dew_point_c))] +pub fn pyfn_relative_humidity(temperature_c: f64, dew_point_c: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::atmosphere::relative_humidity(temperature_c, dew_point_c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Heat index via the Rothfusz regression (simplified). +/// Takes temperature in °C and relative_humidity as percentage (0–100). +/// +/// Rust: `atmosphere::heat_index` +#[pyfunction] +#[pyo3(name = "heat_index", signature = (temperature_c, relative_humidity))] +pub fn pyfn_heat_index(temperature_c: f64, relative_humidity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::atmosphere::heat_index(temperature_c, relative_humidity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Absolute humidity in g/m³. +/// AH = (6.112 × e^(17.67T/(T+243.5)) × RH × 2.1674) / (273.15 + T) +/// `relative_humidity` is fractional (0.0–1.0). +/// +/// Rust: `atmosphere::absolute_humidity` +#[pyfunction] +#[pyo3(name = "absolute_humidity", signature = (relative_humidity, temperature_c))] +pub fn pyfn_absolute_humidity(relative_humidity: f64, temperature_c: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::atmosphere::absolute_humidity(relative_humidity, temperature_c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_wind_chill, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_beaufort_to_speed, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_speed_to_beaufort, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wind_shear, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wind_power_density, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_coriolis_parameter, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_coriolis_acceleration, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_barometric_pressure, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dry_adiabatic_lapse_rate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_temperature_at_altitude, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pressure_altitude, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_density_altitude, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_scale_height, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dew_point, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_relative_humidity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_heat_index, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_absolute_humidity, m)?)?; + m.add("EARTH_ROTATION_RATE", rust_physics_engine::atmosphere::EARTH_ROTATION_RATE)?; + m.add("STANDARD_PRESSURE", rust_physics_engine::atmosphere::STANDARD_PRESSURE)?; + m.add("STANDARD_TEMPERATURE", rust_physics_engine::atmosphere::STANDARD_TEMPERATURE)?; + m.add("DRY_AIR_MOLAR_MASS", rust_physics_engine::atmosphere::DRY_AIR_MOLAR_MASS)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_audio.rs b/bindings/python/src/generated/m_audio.rs new file mode 100644 index 0000000..c02bbf8 --- /dev/null +++ b/bindings/python/src/generated/m_audio.rs @@ -0,0 +1,22 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + Ok(()) +} diff --git a/bindings/python/src/generated/m_audio__analysis.rs b/bindings/python/src/generated/m_audio__analysis.rs new file mode 100644 index 0000000..74a75d6 --- /dev/null +++ b/bindings/python/src/generated/m_audio__analysis.rs @@ -0,0 +1,772 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Raw (biased, un-normalized) autocorrelation of `x` computed with FFTs; +/// returns lags 0..x.len(). +/// +/// Rust: `audio::analysis::autocorrelation_fft` +#[pyfunction] +#[pyo3(name = "autocorrelation_fft", signature = (x))] +pub fn pyfn_autocorrelation_fft<'py>(py: Python<'py>, x: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::autocorrelation_fft(&x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// YIN pitch detector. Returns (frequency, confidence in 0..1) or `None` +/// when no lag drops below `threshold` (unvoiced). +/// +/// Rust: `audio::analysis::pitch_yin` +#[pyfunction] +#[pyo3(name = "pitch_yin", signature = (x, fs, f_min, f_max, threshold))] +pub fn pyfn_pitch_yin<'py>(py: Python<'py>, x: Vec, fs: f64, f_min: f64, f_max: f64, threshold: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::pitch_yin(&x, fs, f_min, f_max, threshold))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| (__x.0, __x.1))) +} + +/// Autocorrelation pitch: highest normalized-autocorrelation peak in the +/// lag range; `None` if the peak is weak (< 0.3). +/// +/// Rust: `audio::analysis::pitch_autocorrelation` +#[pyfunction] +#[pyo3(name = "pitch_autocorrelation", signature = (x, fs, f_min, f_max))] +pub fn pyfn_pitch_autocorrelation<'py>(py: Python<'py>, x: Vec, fs: f64, f_min: f64, f_max: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::pitch_autocorrelation(&x, fs, f_min, f_max))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// Cepstral pitch: peak of the real cepstrum in the expected quefrency +/// range. +/// +/// Rust: `audio::analysis::pitch_cepstral` +#[pyfunction] +#[pyo3(name = "pitch_cepstral", signature = (x, fs, f_min, f_max))] +pub fn pyfn_pitch_cepstral<'py>(py: Python<'py>, x: Vec, fs: f64, f_min: f64, f_max: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::pitch_cepstral(&x, fs, f_min, f_max))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// Harmonic product spectrum pitch estimate. +/// +/// Rust: `audio::analysis::pitch_hps` +#[pyfunction] +#[pyo3(name = "pitch_hps", signature = (x, fs, n_harmonics))] +pub fn pyfn_pitch_hps<'py>(py: Python<'py>, x: Vec, fs: f64, n_harmonics: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::pitch_hps(&x, fs, n_harmonics))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// McLeod pitch method (NSDF). Returns (frequency, clarity). +/// +/// Rust: `audio::analysis::pitch_mpm` +#[pyfunction] +#[pyo3(name = "pitch_mpm", signature = (x, fs))] +pub fn pyfn_pitch_mpm<'py>(py: Python<'py>, x: Vec, fs: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::pitch_mpm(&x, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| (__x.0, __x.1))) +} + +/// Frame-wise pitch track: (time s, f0) per hop, 2048-sample frames. +/// +/// Rust: `audio::analysis::pitch_track` +#[pyfunction] +#[pyo3(name = "pitch_track", signature = (x, fs, hop, method))] +pub fn pyfn_pitch_track<'py>(py: Python<'py>, x: Vec, fs: f64, hop: usize, method: crate::generated::types::PyPitchMethod) -> PyResult)>> { + let method = method.to_rust(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::pitch_track(&x, fs, hop, method))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1.map(|__x| __x))).collect::>()) +} + +/// Segment a pitch track into notes: (start time, duration, MIDI note). +/// Runs of at least 3 voiced frames on the same rounded MIDI number +/// become one note. +/// +/// Rust: `audio::analysis::pitch_to_midi_track` +#[pyfunction] +#[pyo3(name = "pitch_to_midi_track", signature = (track))] +pub fn pyfn_pitch_to_midi_track<'py>(py: Python<'py>, track: Vec<(f64, Option)>) -> PyResult> { + let track = track.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::pitch_to_midi_track(&track))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, __x.2)).collect::>()) +} + +/// Spectral-flux onset strength envelope (one value per STFT frame). +/// +/// Rust: `audio::analysis::onset_strength` +#[pyfunction] +#[pyo3(name = "onset_strength", signature = (x, fs, n_fft, hop))] +pub fn pyfn_onset_strength<'py>(py: Python<'py>, x: Vec, fs: f64, n_fft: usize, hop: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::onset_strength(&x, fs, n_fft, hop))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Pick onsets from a strength envelope: local maxima above `threshold` +/// separated by at least `min_gap` frames. +/// +/// Rust: `audio::analysis::onset_detect` +#[pyfunction] +#[pyo3(name = "onset_detect", signature = (strength, threshold, min_gap))] +pub fn pyfn_onset_detect<'py>(py: Python<'py>, strength: Vec, threshold: f64, min_gap: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::onset_detect(&strength, threshold, min_gap))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// High-frequency-content onset function: Σ k |X_k|² per frame. +/// +/// Rust: `audio::analysis::onset_hfc` +#[pyfunction] +#[pyo3(name = "onset_hfc", signature = (x, fs, n_fft, hop))] +pub fn pyfn_onset_hfc<'py>(py: Python<'py>, x: Vec, fs: f64, n_fft: usize, hop: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::onset_hfc(&x, fs, n_fft, hop))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Complex-domain onset function: deviation of each frame from the +/// magnitude/phase prediction of the previous frames. +/// +/// Rust: `audio::analysis::onset_complex_domain` +#[pyfunction] +#[pyo3(name = "onset_complex_domain", signature = (x, fs, n_fft, hop))] +pub fn pyfn_onset_complex_domain<'py>(py: Python<'py>, x: Vec, fs: f64, n_fft: usize, hop: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::onset_complex_domain(&x, fs, n_fft, hop))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Tempo (BPM) from an onset-strength envelope sampled at `fs` frames/s, +/// via the autocorrelation peak in the 40-240 BPM range (preferring the +/// shortest strong lag, i.e. the fastest consistent pulse). +/// +/// Rust: `audio::analysis::tempo_estimate` +#[pyfunction] +#[pyo3(name = "tempo_estimate", signature = (onsets, fs))] +pub fn pyfn_tempo_estimate<'py>(py: Python<'py>, onsets: Vec, fs: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::tempo_estimate(&onsets, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Beat tracking by dynamic programming (Ellis 2007): onset envelope, +/// global tempo, then a penalized best-predecessor recursion. Returns +/// beat times in seconds. +/// +/// Rust: `audio::analysis::beat_track` +#[pyfunction] +#[pyo3(name = "beat_track", signature = (x, fs))] +pub fn pyfn_beat_track<'py>(py: Python<'py>, x: Vec, fs: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::beat_track(&x, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// MFCCs: log-mel spectrogram followed by a DCT-II, keeping `n_mfcc` +/// coefficients per frame. +/// +/// Rust: `audio::analysis::mfcc` +#[pyfunction] +#[pyo3(name = "mfcc", signature = (x, fs, n_fft, hop, n_mels, n_mfcc))] +pub fn pyfn_mfcc<'py>(py: Python<'py>, x: Vec, fs: f64, n_fft: usize, hop: usize, n_mels: usize, n_mfcc: usize) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::mfcc(&x, fs, n_fft, hop, n_mels, n_mfcc))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Regression delta features over ±`width` frames. +/// +/// Rust: `audio::analysis::delta_features` +#[pyfunction] +#[pyo3(name = "delta_features", signature = (f, width))] +pub fn pyfn_delta_features<'py>(py: Python<'py>, f: Vec>, width: usize) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::delta_features(&f, width))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Linear prediction by the autocorrelation method (Levinson-Durbin). +/// Returns the coefficients of A(z) = 1 + a₁z⁻¹ + … (length order+1, +/// leading 1) and the residual gain √E. +/// +/// Rust: `audio::analysis::lpc` +#[pyfunction] +#[pyo3(name = "lpc", signature = (x, order))] +pub fn pyfn_lpc<'py>(py: Python<'py>, x: Vec, order: usize) -> PyResult<(Vec, f64)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::lpc(&x, order))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Formants (frequency, bandwidth in Hz) from LPC coefficients, via the +/// roots of A(z). +/// +/// Rust: `audio::analysis::lpc_to_formants` +#[pyfunction] +#[pyo3(name = "lpc_to_formants", signature = (coeffs, fs))] +pub fn pyfn_lpc_to_formants<'py>(py: Python<'py>, coeffs: Vec, fs: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::lpc_to_formants(&coeffs, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Frame-wise formant tracking (25 ms Hamming frames, pre-emphasis). +/// +/// Rust: `audio::analysis::formant_track` +#[pyfunction] +#[pyo3(name = "formant_track", signature = (x, fs, order, hop))] +pub fn pyfn_formant_track<'py>(py: Python<'py>, x: Vec, fs: f64, order: usize, hop: usize) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::formant_track(&x, fs, order, hop))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.into_iter().map(|__x| (__x.0, __x.1)).collect::>()).collect::>()) +} + +/// LPC envelope magnitude spectrum: gain/|A(e^{jω})| at `n` frequencies +/// from 0 to fs/2. +/// +/// Rust: `audio::analysis::lpc_spectrum` +#[pyfunction] +#[pyo3(name = "lpc_spectrum", signature = (coeffs, gain, n, fs))] +pub fn pyfn_lpc_spectrum<'py>(py: Python<'py>, coeffs: Vec, gain: f64, n: usize, fs: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::lpc_spectrum(&coeffs, gain, n, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Line spectral pairs (radian frequencies in (0, π), sorted) of an LPC +/// polynomial with leading 1. The order must be even. +/// +/// Rust: `audio::analysis::lpc_to_lsp` +#[pyfunction] +#[pyo3(name = "lpc_to_lsp", signature = (coeffs))] +pub fn pyfn_lpc_to_lsp<'py>(py: Python<'py>, coeffs: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::lpc_to_lsp(&coeffs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Reconstruct LPC coefficients (leading 1) from line spectral pairs. +/// +/// Rust: `audio::analysis::lsp_to_lpc` +#[pyfunction] +#[pyo3(name = "lsp_to_lpc", signature = (lsp))] +pub fn pyfn_lsp_to_lpc<'py>(py: Python<'py>, lsp: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::lsp_to_lpc(&lsp))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Amplitude-weighted mean frequency. +/// +/// Rust: `audio::analysis::spectral_centroid` +#[pyfunction] +#[pyo3(name = "spectral_centroid", signature = (mag, freqs))] +pub fn pyfn_spectral_centroid<'py>(py: Python<'py>, mag: Vec, freqs: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::spectral_centroid(&mag, &freqs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Standard deviation of the spectral distribution. +/// +/// Rust: `audio::analysis::spectral_spread` +#[pyfunction] +#[pyo3(name = "spectral_spread", signature = (mag, freqs))] +pub fn pyfn_spectral_spread<'py>(py: Python<'py>, mag: Vec, freqs: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::spectral_spread(&mag, &freqs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Third standardized moment of the spectral distribution. +/// +/// Rust: `audio::analysis::spectral_skewness` +#[pyfunction] +#[pyo3(name = "spectral_skewness", signature = (mag, freqs))] +pub fn pyfn_spectral_skewness<'py>(py: Python<'py>, mag: Vec, freqs: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::spectral_skewness(&mag, &freqs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Fourth standardized moment of the spectral distribution. +/// +/// Rust: `audio::analysis::spectral_kurtosis` +#[pyfunction] +#[pyo3(name = "spectral_kurtosis", signature = (mag, freqs))] +pub fn pyfn_spectral_kurtosis<'py>(py: Python<'py>, mag: Vec, freqs: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::spectral_kurtosis(&mag, &freqs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Frequency below which `pct` (0..1) of the spectral energy lies. +/// +/// Rust: `audio::analysis::spectral_rolloff` +#[pyfunction] +#[pyo3(name = "spectral_rolloff", signature = (mag, freqs, pct))] +pub fn pyfn_spectral_rolloff<'py>(py: Python<'py>, mag: Vec, freqs: Vec, pct: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::spectral_rolloff(&mag, &freqs, pct))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Half-wave rectified spectral flux between consecutive magnitude +/// frames. +/// +/// Rust: `audio::analysis::spectral_flux` +#[pyfunction] +#[pyo3(name = "spectral_flux", signature = (prev, cur))] +pub fn pyfn_spectral_flux<'py>(py: Python<'py>, prev: Vec, cur: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::spectral_flux(&prev, &cur))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Geometric-to-arithmetic mean ratio (1 = white, 0 = tonal). +/// +/// Rust: `audio::analysis::spectral_flatness_mag` +#[pyfunction] +#[pyo3(name = "spectral_flatness_mag", signature = (mag))] +pub fn pyfn_spectral_flatness_mag<'py>(py: Python<'py>, mag: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::spectral_flatness_mag(&mag))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Peak-to-mean spectral ratio. +/// +/// Rust: `audio::analysis::spectral_crest` +#[pyfunction] +#[pyo3(name = "spectral_crest", signature = (mag))] +pub fn pyfn_spectral_crest<'py>(py: Python<'py>, mag: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::spectral_crest(&mag))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Linear-regression slope of magnitude vs frequency. +/// +/// Rust: `audio::analysis::spectral_slope` +#[pyfunction] +#[pyo3(name = "spectral_slope", signature = (mag, freqs))] +pub fn pyfn_spectral_slope<'py>(py: Python<'py>, mag: Vec, freqs: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::spectral_slope(&mag, &freqs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Spectral decrease (perceptual measure of how fast magnitude falls off +/// with bin index). +/// +/// Rust: `audio::analysis::spectral_decrease` +#[pyfunction] +#[pyo3(name = "spectral_decrease", signature = (mag))] +pub fn pyfn_spectral_decrease<'py>(py: Python<'py>, mag: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::spectral_decrease(&mag))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Normalized Shannon entropy of the magnitude distribution (0..1). +/// +/// Rust: `audio::analysis::spectral_entropy_mag` +#[pyfunction] +#[pyo3(name = "spectral_entropy_mag", signature = (mag))] +pub fn pyfn_spectral_entropy_mag<'py>(py: Python<'py>, mag: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::spectral_entropy_mag(&mag))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Frame-wise spectral descriptor track. +/// +/// Rust: `audio::analysis::spectral_features_track` +#[pyfunction] +#[pyo3(name = "spectral_features_track", signature = (x, fs, n_fft, hop))] +pub fn pyfn_spectral_features_track(x: Vec, fs: f64, n_fft: usize, hop: usize) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::analysis::spectral_features_track(&x, fs, n_fft, hop)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PySpectralFeatures { inner: __x }).collect::>()) +} + +/// Frame-wise zero-crossing rate (fraction of adjacent sample pairs that +/// change sign). +/// +/// Rust: `audio::analysis::zero_crossing_rate` +#[pyfunction] +#[pyo3(name = "zero_crossing_rate", signature = (x, frame, hop))] +pub fn pyfn_zero_crossing_rate<'py>(py: Python<'py>, x: Vec, frame: usize, hop: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::zero_crossing_rate(&x, frame, hop))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Harmonic-to-noise ratio (dB) from the normalized autocorrelation at +/// the period of `f0`. +/// +/// Rust: `audio::analysis::harmonic_to_noise_ratio` +#[pyfunction] +#[pyo3(name = "harmonic_to_noise_ratio", signature = (x, fs, f0))] +pub fn pyfn_harmonic_to_noise_ratio<'py>(py: Python<'py>, x: Vec, fs: f64, f0: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::harmonic_to_noise_ratio(&x, fs, f0))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Piano-style inharmonicity coefficient B fitted from the measured +/// partial frequencies: f_k ≈ k f0 √(1 + B k²). +/// +/// Rust: `audio::analysis::inharmonicity_measure` +#[pyfunction] +#[pyo3(name = "inharmonicity_measure", signature = (x, fs, f0))] +pub fn pyfn_inharmonicity_measure<'py>(py: Python<'py>, x: Vec, fs: f64, f0: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::inharmonicity_measure(&x, fs, f0))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Frame-wise 12-bin chroma (C, C#, …, B), energy-normalized per frame. +/// +/// Rust: `audio::analysis::chroma` +#[pyfunction] +#[pyo3(name = "chroma", signature = (x, fs, n_fft, hop))] +pub fn pyfn_chroma<'py>(py: Python<'py>, x: Vec, fs: f64, n_fft: usize, hop: usize) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::chroma(&x, fs, n_fft, hop))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.to_vec()).collect::>()) +} + +/// Template chord match on one chroma frame: returns e.g. "C", "Am", +/// "Bdim", "Faug". +/// +/// Rust: `audio::analysis::chord_estimate` +#[pyfunction] +#[pyo3(name = "chord_estimate", signature = (chroma_frame))] +pub fn pyfn_chord_estimate<'py>(py: Python<'py>, chroma_frame: Vec) -> PyResult { + let chroma_frame = <[f64; 12]>::try_from(chroma_frame).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 12 values, got {}", __v.len())))?; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::chord_estimate(&chroma_frame))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_string()) +} + +/// Approximate loudness in sones (Zwicker-style power law on the overall +/// level, full scale taken as 94 dB SPL). +/// +/// Rust: `audio::analysis::loudness_sone` +#[pyfunction] +#[pyo3(name = "loudness_sone", signature = (x, fs))] +pub fn pyfn_loudness_sone<'py>(py: Python<'py>, x: Vec, fs: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::loudness_sone(&x, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Approximate sharpness in acum (Bark-weighted specific-loudness +/// centroid, von Bismarck weighting). +/// +/// Rust: `audio::analysis::sharpness` +#[pyfunction] +#[pyo3(name = "sharpness", signature = (x, fs))] +pub fn pyfn_sharpness<'py>(py: Python<'py>, x: Vec, fs: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::sharpness(&x, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Approximate roughness: fraction of envelope fluctuation energy in the +/// 20-300 Hz modulation range. +/// +/// Rust: `audio::analysis::roughness` +#[pyfunction] +#[pyo3(name = "roughness", signature = (x, fs))] +pub fn pyfn_roughness<'py>(py: Python<'py>, x: Vec, fs: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::roughness(&x, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Approximate fluctuation strength: fraction of envelope fluctuation +/// energy in the 1-10 Hz modulation range (maximal near 4 Hz). +/// +/// Rust: `audio::analysis::fluctuation_strength` +#[pyfunction] +#[pyo3(name = "fluctuation_strength", signature = (x, fs))] +pub fn pyfn_fluctuation_strength<'py>(py: Python<'py>, x: Vec, fs: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::fluctuation_strength(&x, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Silent regions as (start, end) sample ranges: block RMS below +/// `threshold_db` (dBFS) for at least `min_len` samples. +/// +/// Rust: `audio::analysis::silence_detect` +#[pyfunction] +#[pyo3(name = "silence_detect", signature = (x, threshold_db, min_len))] +pub fn pyfn_silence_detect<'py>(py: Python<'py>, x: Vec, threshold_db: f64, min_len: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::silence_detect(&x, threshold_db, min_len))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Transient (attack) sample positions from a high-frequency-content +/// envelope with an adaptive threshold. +/// +/// Rust: `audio::analysis::transient_detect` +#[pyfunction] +#[pyo3(name = "transient_detect", signature = (x, fs))] +pub fn pyfn_transient_detect<'py>(py: Python<'py>, x: Vec, fs: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::transient_detect(&x, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// SNR (dB) of a signal given a noise-only reference segment. +/// +/// Rust: `audio::analysis::estimate_snr` +#[pyfunction] +#[pyo3(name = "estimate_snr", signature = (x, noise_segment))] +pub fn pyfn_estimate_snr<'py>(py: Python<'py>, x: Vec, noise_segment: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::estimate_snr(&x, &noise_segment))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// THD+N as a linear ratio: √((P_total − P_fund)/P_fund). +/// +/// Rust: `audio::analysis::thd_n` +#[pyfunction] +#[pyo3(name = "thd_n", signature = (x, fs, f0))] +pub fn pyfn_thd_n<'py>(py: Python<'py>, x: Vec, fs: f64, f0: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::thd_n(&x, fs, f0))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// SINAD in dB: 10 log₁₀(P_fund / (P_total − P_fund)). +/// +/// Rust: `audio::analysis::sinad` +#[pyfunction] +#[pyo3(name = "sinad", signature = (x, fs, f0))] +pub fn pyfn_sinad<'py>(py: Python<'py>, x: Vec, fs: f64, f0: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::sinad(&x, fs, f0))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Effective number of bits from a SINAD measurement (dB). +/// +/// Rust: `audio::analysis::enob` +#[pyfunction] +#[pyo3(name = "enob", signature = (sinad_db))] +pub fn pyfn_enob(sinad_db: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::analysis::enob(sinad_db)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Deconvolve a Farina sweep measurement: convolve the recording with the +/// inverse sweep and align so the direct impulse response starts at 0. +/// +/// Rust: `audio::analysis::impulse_response_from_sweep` +#[pyfunction] +#[pyo3(name = "impulse_response_from_sweep", signature = (recorded, inverse_sweep))] +pub fn pyfn_impulse_response_from_sweep<'py>(py: Python<'py>, recorded: Vec, inverse_sweep: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::impulse_response_from_sweep(&recorded, &inverse_sweep))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// RT60 via Schroeder backward integration, extrapolated from the +/// −5..−25 dB decay slope. +/// +/// Rust: `audio::analysis::rt60_from_ir` +#[pyfunction] +#[pyo3(name = "rt60_from_ir", signature = (ir, fs))] +pub fn pyfn_rt60_from_ir<'py>(py: Python<'py>, ir: Vec, fs: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::rt60_from_ir(&ir, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Early decay time: the 0..−10 dB slope extrapolated to 60 dB. +/// +/// Rust: `audio::analysis::edt_from_ir` +#[pyfunction] +#[pyo3(name = "edt_from_ir", signature = (ir, fs))] +pub fn pyfn_edt_from_ir<'py>(py: Python<'py>, ir: Vec, fs: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::edt_from_ir(&ir, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Clarity C50 (dB): early (< 50 ms) to late energy ratio. +/// +/// Rust: `audio::analysis::c50` +#[pyfunction] +#[pyo3(name = "c50", signature = (ir, fs))] +pub fn pyfn_c50<'py>(py: Python<'py>, ir: Vec, fs: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::c50(&ir, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Clarity C80 (dB): early (< 80 ms) to late energy ratio. +/// +/// Rust: `audio::analysis::c80` +#[pyfunction] +#[pyo3(name = "c80", signature = (ir, fs))] +pub fn pyfn_c80<'py>(py: Python<'py>, ir: Vec, fs: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::c80(&ir, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Definition D50: fraction of energy arriving within 50 ms. +/// +/// Rust: `audio::analysis::d50` +#[pyfunction] +#[pyo3(name = "d50", signature = (ir, fs))] +pub fn pyfn_d50<'py>(py: Python<'py>, ir: Vec, fs: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::d50(&ir, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Single-band STI approximation from the impulse response's modulation +/// transfer function at the 14 standard modulation frequencies. +/// +/// Rust: `audio::analysis::sti_approx` +#[pyfunction] +#[pyo3(name = "sti_approx", signature = (ir, fs))] +pub fn pyfn_sti_approx<'py>(py: Python<'py>, ir: Vec, fs: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::sti_approx(&ir, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Local maxima above `threshold`, greedily thinned so accepted peaks are +/// at least `min_distance` apart (strongest first). Returns sorted +/// indices. +/// +/// Rust: `audio::analysis::peak_pick` +#[pyfunction] +#[pyo3(name = "peak_pick", signature = (x, threshold, min_distance))] +pub fn pyfn_peak_pick<'py>(py: Python<'py>, x: Vec, threshold: f64, min_distance: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::peak_pick(&x, threshold, min_distance))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Dynamic time warping between two feature sequences (Euclidean local +/// cost). Returns (total cost, warping path from (0,0) to (n-1,m-1)). +/// +/// Rust: `audio::analysis::dynamic_time_warping` +#[pyfunction] +#[pyo3(name = "dynamic_time_warping", signature = (a, b))] +pub fn pyfn_dynamic_time_warping<'py>(py: Python<'py>, a: Vec>, b: Vec>) -> PyResult<(f64, Vec<(usize, usize)>)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::dynamic_time_warping(&a, &b))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1.into_iter().map(|__x| (__x.0, __x.1)).collect::>())) +} + +/// Shazam-style constellation fingerprint: spectrogram peaks paired into +/// (f_anchor, f_target, Δt) hashes. +/// +/// Rust: `audio::analysis::audio_fingerprint` +#[pyfunction] +#[pyo3(name = "audio_fingerprint", signature = (x, fs))] +pub fn pyfn_audio_fingerprint<'py>(py: Python<'py>, x: Vec, fs: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::analysis::audio_fingerprint(&x, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_autocorrelation_fft, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pitch_yin, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pitch_autocorrelation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pitch_cepstral, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pitch_hps, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pitch_mpm, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pitch_track, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pitch_to_midi_track, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_onset_strength, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_onset_detect, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_onset_hfc, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_onset_complex_domain, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tempo_estimate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_beat_track, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mfcc, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_delta_features, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lpc, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lpc_to_formants, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_formant_track, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lpc_spectrum, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lpc_to_lsp, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lsp_to_lpc, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spectral_centroid, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spectral_spread, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spectral_skewness, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spectral_kurtosis, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spectral_rolloff, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spectral_flux, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spectral_flatness_mag, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spectral_crest, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spectral_slope, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spectral_decrease, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spectral_entropy_mag, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spectral_features_track, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_zero_crossing_rate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_harmonic_to_noise_ratio, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_inharmonicity_measure, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_chroma, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_chord_estimate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_loudness_sone, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sharpness, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_roughness, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fluctuation_strength, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_silence_detect, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_transient_detect, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_estimate_snr, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_thd_n, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sinad, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_enob, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_impulse_response_from_sweep, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rt60_from_ir, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_edt_from_ir, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_c50, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_c80, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_d50, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sti_approx, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_peak_pick, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dynamic_time_warping, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_audio_fingerprint, m)?)?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_audio__effects.rs b/bindings/python/src/generated/m_audio__effects.rs new file mode 100644 index 0000000..8d40400 --- /dev/null +++ b/bindings/python/src/generated/m_audio__effects.rs @@ -0,0 +1,307 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Convolution reverb via the partitioned FFT convolver (matches direct +/// convolution; output length x + ir − 1). +/// +/// Rust: `audio::effects::convolution_reverb` +#[pyfunction] +#[pyo3(name = "convolution_reverb", signature = (x, ir))] +pub fn pyfn_convolution_reverb<'py>(py: Python<'py>, x: Vec, ir: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::effects::convolution_reverb(&x, &ir))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Synthetic exponential-decay impulse response with optional discrete +/// early reflections (time s, gain). +/// +/// Rust: `audio::effects::synthesize_ir_exponential` +#[pyfunction] +#[pyo3(name = "synthesize_ir_exponential", signature = (rt60, fs, early_reflections, rng))] +pub fn pyfn_synthesize_ir_exponential(rt60: f64, fs: f64, early_reflections: Vec<(f64, f64)>, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let early_reflections = early_reflections.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::audio::effects::synthesize_ir_exponential(rt60, fs, &early_reflections, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// tanh soft clipper. +/// +/// Rust: `audio::effects::distortion_soft_clip` +#[pyfunction] +#[pyo3(name = "distortion_soft_clip", signature = (x, drive))] +pub fn pyfn_distortion_soft_clip(x: f64, drive: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::effects::distortion_soft_clip(x, drive)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Hard clipper at ±threshold. +/// +/// Rust: `audio::effects::distortion_hard_clip` +#[pyfunction] +#[pyo3(name = "distortion_hard_clip", signature = (x, threshold))] +pub fn pyfn_distortion_hard_clip(x: f64, threshold: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::effects::distortion_hard_clip(x, threshold)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Asymmetric "tube" shaper (bias shifts the operating point). +/// +/// Rust: `audio::effects::distortion_tube` +#[pyfunction] +#[pyo3(name = "distortion_tube", signature = (x, drive, bias))] +pub fn pyfn_distortion_tube(x: f64, drive: f64, bias: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::effects::distortion_tube(x, drive, bias)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Foldback distortion. +/// +/// Rust: `audio::effects::distortion_foldback` +#[pyfunction] +#[pyo3(name = "distortion_foldback", signature = (x, threshold))] +pub fn pyfn_distortion_foldback(x: f64, threshold: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::effects::distortion_foldback(x, threshold)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Run a memoryless nonlinearity oversampled by `factor` (anti-aliased: +/// upsample, apply, decimate). +/// +/// Rust: `audio::effects::oversample_process` +#[pyfunction] +#[pyo3(name = "oversample_process", signature = (x, factor, f))] +pub fn pyfn_oversample_process(x: Vec, factor: usize, f: pyo3::Py) -> PyResult> { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::audio::effects::oversample_process(&x, factor, &f)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Haas effect: (dry, delayed) pair for pseudo-stereo width. +/// +/// Rust: `audio::effects::haas_delay` +#[pyfunction] +#[pyo3(name = "haas_delay", signature = (x, ms, fs))] +pub fn pyfn_haas_delay<'py>(py: Python<'py>, x: Vec, ms: f64, fs: f64) -> PyResult<(Vec, Vec)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::effects::haas_delay(&x, ms, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Delay-line (Doppler) pitch shifter with two crossfaded taps. +/// +/// Rust: `audio::effects::pitch_shift_simple` +#[pyfunction] +#[pyo3(name = "pitch_shift_simple", signature = (x, semitones, fs))] +pub fn pyfn_pitch_shift_simple<'py>(py: Python<'py>, x: Vec, semitones: f64, fs: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::effects::pitch_shift_simple(&x, semitones, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Apply a gain in dB in place. +/// +/// Rust: `audio::effects::gain_db` +#[pyfunction] +#[pyo3(name = "gain_db", signature = (x, db))] +pub fn pyfn_gain_db<'py>(x: pyo3::Bound<'py, pyo3::PyAny>, db: f64) -> PyResult<()> { + let mut x__v: Vec = x.extract()?; + let __r = crate::runtime::guard(|| rust_physics_engine::audio::effects::gain_db(&mut x__v, db)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + crate::runtime::coerce::write_back(&x, &x__v)?; + Ok(()) +} + +/// Normalize the peak to `target_db` (dBFS) in place. +/// +/// Rust: `audio::effects::normalize_peak` +#[pyfunction] +#[pyo3(name = "normalize_peak", signature = (x, target_db))] +pub fn pyfn_normalize_peak<'py>(x: pyo3::Bound<'py, pyo3::PyAny>, target_db: f64) -> PyResult<()> { + let mut x__v: Vec = x.extract()?; + let __r = crate::runtime::guard(|| rust_physics_engine::audio::effects::normalize_peak(&mut x__v, target_db)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + crate::runtime::coerce::write_back(&x, &x__v)?; + Ok(()) +} + +/// Normalize the RMS to `target_db` in place. +/// +/// Rust: `audio::effects::normalize_rms` +#[pyfunction] +#[pyo3(name = "normalize_rms", signature = (x, target_db))] +pub fn pyfn_normalize_rms<'py>(x: pyo3::Bound<'py, pyo3::PyAny>, target_db: f64) -> PyResult<()> { + let mut x__v: Vec = x.extract()?; + let __r = crate::runtime::guard(|| rust_physics_engine::audio::effects::normalize_rms(&mut x__v, target_db)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + crate::runtime::coerce::write_back(&x, &x__v)?; + Ok(()) +} + +/// Integrated loudness (LUFS) per ITU-R BS.1770-4: K-weighting, 400 ms +/// blocks with 75% overlap, absolute −70 LUFS and relative −10 LU +/// gating. +/// +/// Rust: `audio::effects::measure_lufs` +#[pyfunction] +#[pyo3(name = "measure_lufs", signature = (x, fs))] +pub fn pyfn_measure_lufs<'py>(py: Python<'py>, x: Vec, fs: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::effects::measure_lufs(&x, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Normalize integrated loudness to `target_lufs` in place. +/// +/// Rust: `audio::effects::normalize_lufs` +#[pyfunction] +#[pyo3(name = "normalize_lufs", signature = (x, target_lufs, fs))] +pub fn pyfn_normalize_lufs<'py>(x: pyo3::Bound<'py, pyo3::PyAny>, target_lufs: f64, fs: f64) -> PyResult<()> { + let mut x__v: Vec = x.extract()?; + let __r = crate::runtime::guard(|| rust_physics_engine::audio::effects::normalize_lufs(&mut x__v, target_lufs, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + crate::runtime::coerce::write_back(&x, &x__v)?; + Ok(()) +} + +/// Inter-sample true peak (4× oversampled), linear. +/// +/// Rust: `audio::effects::true_peak` +#[pyfunction] +#[pyo3(name = "true_peak", signature = (x, fs))] +pub fn pyfn_true_peak<'py>(py: Python<'py>, x: Vec, fs: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::effects::true_peak(&x, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// TPDF dither to `bits` (quantized output in −1..1). +/// +/// Rust: `audio::effects::dither_tpdf` +#[pyfunction] +#[pyo3(name = "dither_tpdf", signature = (x, bits, rng))] +pub fn pyfn_dither_tpdf(x: Vec, bits: u32, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::audio::effects::dither_tpdf(&x, bits, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// First-order noise-shaped dither (error feedback pushes quantization +/// noise upward in frequency). +/// +/// Rust: `audio::effects::noise_shaping_dither` +#[pyfunction] +#[pyo3(name = "noise_shaping_dither", signature = (x, bits, rng))] +pub fn pyfn_noise_shaping_dither(x: Vec, bits: u32, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::audio::effects::noise_shaping_dither(&x, bits, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Remove the mean in place. +/// +/// Rust: `audio::effects::dc_offset_remove` +#[pyfunction] +#[pyo3(name = "dc_offset_remove", signature = (x))] +pub fn pyfn_dc_offset_remove<'py>(x: pyo3::Bound<'py, pyo3::PyAny>) -> PyResult<()> { + let mut x__v: Vec = x.extract()?; + let __r = crate::runtime::guard(|| rust_physics_engine::audio::effects::dc_offset_remove(&mut x__v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + crate::runtime::coerce::write_back(&x, &x__v)?; + Ok(()) +} + +/// Replace samples whose second difference exceeds `threshold` with a +/// linear interpolation of their neighbors (simple click repair). +/// +/// Rust: `audio::effects::declick` +#[pyfunction] +#[pyo3(name = "declick", signature = (x, threshold))] +pub fn pyfn_declick<'py>(py: Python<'py>, x: Vec, threshold: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::effects::declick(&x, threshold))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Spectral gate denoiser: attenuate STFT bins that fall below the +/// noise profile (per-bin magnitude) plus `threshold_db`. +/// +/// Rust: `audio::effects::spectral_gate` +#[pyfunction] +#[pyo3(name = "spectral_gate", signature = (x, noise_profile, threshold_db, n_fft, hop))] +pub fn pyfn_spectral_gate<'py>(py: Python<'py>, x: Vec, noise_profile: Vec, threshold_db: f64, n_fft: usize, hop: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::effects::spectral_gate(&x, &noise_profile, threshold_db, n_fft, hop))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_convolution_reverb, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_synthesize_ir_exponential, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_distortion_soft_clip, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_distortion_hard_clip, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_distortion_tube, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_distortion_foldback, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_oversample_process, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_haas_delay, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pitch_shift_simple, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gain_db, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_normalize_peak, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_normalize_rms, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_measure_lufs, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_normalize_lufs, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_true_peak, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dither_tpdf, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_noise_shaping_dither, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dc_offset_remove, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_declick, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spectral_gate, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_audio__envelope.rs b/bindings/python/src/generated/m_audio__envelope.rs new file mode 100644 index 0000000..0bb9938 --- /dev/null +++ b/bindings/python/src/generated/m_audio__envelope.rs @@ -0,0 +1,148 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Peak envelope follower with attack/release time constants (ms). +/// +/// Rust: `audio::envelope::envelope_follower` +#[pyfunction] +#[pyo3(name = "envelope_follower", signature = (x, attack_ms, release_ms, fs))] +pub fn pyfn_envelope_follower<'py>(py: Python<'py>, x: Vec, attack_ms: f64, release_ms: f64, fs: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::envelope::envelope_follower(&x, attack_ms, release_ms, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Sliding-window peak magnitude (centered). +/// +/// Rust: `audio::envelope::peak_envelope` +#[pyfunction] +#[pyo3(name = "peak_envelope", signature = (x, window))] +pub fn pyfn_peak_envelope<'py>(py: Python<'py>, x: Vec, window: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::envelope::peak_envelope(&x, window))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Sliding-window RMS (centered). +/// +/// Rust: `audio::envelope::rms_envelope` +#[pyfunction] +#[pyo3(name = "rms_envelope", signature = (x, window))] +pub fn pyfn_rms_envelope<'py>(py: Python<'py>, x: Vec, window: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::envelope::rms_envelope(&x, window))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// e^(−t/τ) sampled for n samples. +/// +/// Rust: `audio::envelope::exponential_decay_envelope` +#[pyfunction] +#[pyo3(name = "exponential_decay_envelope", signature = (n, tau, fs))] +pub fn pyfn_exponential_decay_envelope<'py>(py: Python<'py>, n: usize, tau: f64, fs: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::envelope::exponential_decay_envelope(n, tau, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Multiply a signal by an envelope in place. +/// +/// Rust: `audio::envelope::apply_envelope` +#[pyfunction] +#[pyo3(name = "apply_envelope", signature = (x, env))] +pub fn pyfn_apply_envelope<'py>(x: pyo3::Bound<'py, pyo3::PyAny>, env: Vec) -> PyResult<()> { + let mut x__v: Vec = x.extract()?; + let __r = crate::runtime::guard(|| rust_physics_engine::audio::envelope::apply_envelope(&mut x__v, &env)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + crate::runtime::coerce::write_back(&x, &x__v)?; + Ok(()) +} + +/// Fade in the first n samples in place. +/// +/// Rust: `audio::envelope::fade_in` +#[pyfunction] +#[pyo3(name = "fade_in", signature = (x, n, shape))] +pub fn pyfn_fade_in<'py>(x: pyo3::Bound<'py, pyo3::PyAny>, n: usize, shape: crate::generated::types::PyFadeShape) -> PyResult<()> { + let mut x__v: Vec = x.extract()?; + let shape = shape.to_rust(); + let __r = crate::runtime::guard(|| rust_physics_engine::audio::envelope::fade_in(&mut x__v, n, shape)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + crate::runtime::coerce::write_back(&x, &x__v)?; + Ok(()) +} + +/// Fade out the last n samples in place. +/// +/// Rust: `audio::envelope::fade_out` +#[pyfunction] +#[pyo3(name = "fade_out", signature = (x, n, shape))] +pub fn pyfn_fade_out<'py>(x: pyo3::Bound<'py, pyo3::PyAny>, n: usize, shape: crate::generated::types::PyFadeShape) -> PyResult<()> { + let mut x__v: Vec = x.extract()?; + let shape = shape.to_rust(); + let __r = crate::runtime::guard(|| rust_physics_engine::audio::envelope::fade_out(&mut x__v, n, shape)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + crate::runtime::coerce::write_back(&x, &x__v)?; + Ok(()) +} + +/// Full-length crossfade from a to b. +/// +/// Panics: +/// Panics if the inputs differ in length. +/// +/// Rust: `audio::envelope::crossfade` +#[pyfunction] +#[pyo3(name = "crossfade", signature = (a, b, shape))] +pub fn pyfn_crossfade<'py>(py: Python<'py>, a: Vec, b: Vec, shape: crate::generated::types::PyFadeShape) -> PyResult> { + let shape = shape.to_rust(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::envelope::crossfade(&a, &b, shape))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Pitch glide trajectory (Hz per sample): linear or exponential +/// (constant cents/second) from one frequency to another. +/// +/// Rust: `audio::envelope::portamento` +#[pyfunction] +#[pyo3(name = "portamento", signature = (from_hz, to_hz, n, fs, exponential))] +pub fn pyfn_portamento<'py>(py: Python<'py>, from_hz: f64, to_hz: f64, n: usize, fs: f64, exponential: bool) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::envelope::portamento(from_hz, to_hz, n, fs, exponential))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_envelope_follower, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_peak_envelope, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rms_envelope, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_exponential_decay_envelope, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_apply_envelope, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fade_in, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fade_out, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_crossfade, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_portamento, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_audio__oscillators.rs b/bindings/python/src/generated/m_audio__oscillators.rs new file mode 100644 index 0000000..d420fe5 --- /dev/null +++ b/bindings/python/src/generated/m_audio__oscillators.rs @@ -0,0 +1,227 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Anti-aliased sawtooth (−1..1) at phase t with increment dt. +/// +/// Rust: `audio::oscillators::polyblep_saw` +#[pyfunction] +#[pyo3(name = "polyblep_saw", signature = (phase, dt))] +pub fn pyfn_polyblep_saw(phase: f64, dt: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::oscillators::polyblep_saw(phase, dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Anti-aliased pulse with the given duty cycle. +/// +/// Rust: `audio::oscillators::polyblep_square` +#[pyfunction] +#[pyo3(name = "polyblep_square", signature = (phase, dt, duty))] +pub fn pyfn_polyblep_square(phase: f64, dt: f64, duty: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::oscillators::polyblep_square(phase, dt, duty)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Anti-aliased triangle (corner smoothing by polyBLAMP; triangle +/// aliasing is already −12 dB/oct so the correction is mild). +/// +/// Rust: `audio::oscillators::polyblep_triangle` +#[pyfunction] +#[pyo3(name = "polyblep_triangle", signature = (phase, dt))] +pub fn pyfn_polyblep_triangle(phase: f64, dt: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::oscillators::polyblep_triangle(phase, dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Band-limited saw from its Fourier series (n harmonics). +/// +/// Rust: `audio::oscillators::additive_saw` +#[pyfunction] +#[pyo3(name = "additive_saw", signature = (phase, n_harmonics))] +pub fn pyfn_additive_saw(phase: f64, n_harmonics: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::oscillators::additive_saw(phase, n_harmonics)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Band-limited square from its Fourier series. +/// +/// Rust: `audio::oscillators::additive_square` +#[pyfunction] +#[pyo3(name = "additive_square", signature = (phase, n_harmonics))] +pub fn pyfn_additive_square(phase: f64, n_harmonics: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::oscillators::additive_square(phase, n_harmonics)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Band-limited triangle from its Fourier series. +/// +/// Rust: `audio::oscillators::additive_triangle` +#[pyfunction] +#[pyo3(name = "additive_triangle", signature = (phase, n_harmonics))] +pub fn pyfn_additive_triangle(phase: f64, n_harmonics: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::oscillators::additive_triangle(phase, n_harmonics)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Linear chirp from f0 to f1 over `duration` seconds. +/// +/// Rust: `audio::oscillators::chirp_linear` +#[pyfunction] +#[pyo3(name = "chirp_linear", signature = (f0, f1, duration, fs))] +pub fn pyfn_chirp_linear<'py>(py: Python<'py>, f0: f64, f1: f64, duration: f64, fs: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::oscillators::chirp_linear(f0, f1, duration, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Exponential (logarithmic-sweep) chirp. +/// +/// Rust: `audio::oscillators::chirp_exponential` +#[pyfunction] +#[pyo3(name = "chirp_exponential", signature = (f0, f1, duration, fs))] +pub fn pyfn_chirp_exponential<'py>(py: Python<'py>, f0: f64, f1: f64, duration: f64, fs: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::oscillators::chirp_exponential(f0, f1, duration, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Hyperbolic chirp (linear period sweep). +/// +/// Rust: `audio::oscillators::chirp_hyperbolic` +#[pyfunction] +#[pyo3(name = "chirp_hyperbolic", signature = (f0, f1, duration, fs))] +pub fn pyfn_chirp_hyperbolic<'py>(py: Python<'py>, f0: f64, f1: f64, duration: f64, fs: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::oscillators::chirp_hyperbolic(f0, f1, duration, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Farina exponential sweep and its inverse filter: convolving the two +/// yields (a delayed) impulse, the standard impulse-response +/// measurement pair. +/// +/// Rust: `audio::oscillators::sine_sweep_with_inverse` +#[pyfunction] +#[pyo3(name = "sine_sweep_with_inverse", signature = (f0, f1, duration, fs))] +pub fn pyfn_sine_sweep_with_inverse(f0: f64, f1: f64, duration: f64, fs: f64) -> PyResult<(Vec, Vec)> { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::oscillators::sine_sweep_with_inverse(f0, f1, duration, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Unit impulse at `pos` in an n-sample buffer. +/// +/// Rust: `audio::oscillators::impulse` +#[pyfunction] +#[pyo3(name = "impulse", signature = (n, pos))] +pub fn pyfn_impulse<'py>(py: Python<'py>, n: usize, pos: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::oscillators::impulse(n, pos))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Constant (DC) buffer. +/// +/// Rust: `audio::oscillators::dc` +#[pyfunction] +#[pyo3(name = "dc", signature = (n, level))] +pub fn pyfn_dc<'py>(py: Python<'py>, n: usize, level: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::oscillators::dc(n, level))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Sum of sinusoids with per-tone amplitude and phase. +/// +/// Panics: +/// Panics if the parameter arrays differ in length. +/// +/// Rust: `audio::oscillators::multisine` +#[pyfunction] +#[pyo3(name = "multisine", signature = (freqs, amps, phases, n, fs))] +pub fn pyfn_multisine<'py>(py: Python<'py>, freqs: Vec, amps: Vec, phases: Vec, n: usize, fs: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::oscillators::multisine(&freqs, &s, &phases, n, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Schroeder-phase multisine of `n_tones` bin-aligned harmonics of +/// fs/n: near-minimal crest factor for broadband excitation. +/// +/// Rust: `audio::oscillators::schroeder_phase_multisine` +#[pyfunction] +#[pyo3(name = "schroeder_phase_multisine", signature = (n_tones, n, fs))] +pub fn pyfn_schroeder_phase_multisine<'py>(py: Python<'py>, n_tones: usize, n: usize, fs: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::oscillators::schroeder_phase_multisine(n_tones, n, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Rectangular pulse train: `width` seconds high per period. +/// +/// Rust: `audio::oscillators::pulse_train` +#[pyfunction] +#[pyo3(name = "pulse_train", signature = (freq, width, n, fs))] +pub fn pyfn_pulse_train<'py>(py: Python<'py>, freq: f64, width: f64, n: usize, fs: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::oscillators::pulse_train(freq, width, n, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Band-limited impulse train (all cosine harmonics up to Nyquist, +/// unit DC component). +/// +/// Rust: `audio::oscillators::band_limited_impulse_train` +#[pyfunction] +#[pyo3(name = "band_limited_impulse_train", signature = (freq, n, fs))] +pub fn pyfn_band_limited_impulse_train<'py>(py: Python<'py>, freq: f64, n: usize, fs: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::oscillators::band_limited_impulse_train(freq, n, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_polyblep_saw, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_polyblep_square, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_polyblep_triangle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_additive_saw, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_additive_square, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_additive_triangle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_chirp_linear, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_chirp_exponential, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_chirp_hyperbolic, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sine_sweep_with_inverse, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_impulse, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dc, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_multisine, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_schroeder_phase_multisine, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pulse_train, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_band_limited_impulse_train, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_audio__physical.rs b/bindings/python/src/generated/m_audio__physical.rs new file mode 100644 index 0000000..afdc2ee --- /dev/null +++ b/bindings/python/src/generated/m_audio__physical.rs @@ -0,0 +1,180 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// One waveguide per band: (center frequency, T60 seconds) pairs, as used +/// in banded waveguide synthesis of stiff/inharmonic objects. +/// +/// Rust: `audio::physical::banded_waveguide` +#[pyfunction] +#[pyo3(name = "banded_waveguide", signature = (freq, bands, fs))] +pub fn pyfn_banded_waveguide(freq: f64, bands: Vec<(f64, f64)>, fs: f64) -> PyResult> { + let bands = bands.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::audio::physical::banded_waveguide(freq, &bands, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyWaveguideString { inner: __x }).collect::>()) +} + +/// Commuted synthesis: the body impulse response is convolved into the +/// excitation and fed through the string, avoiding a body filter at +/// synthesis time. +/// +/// Rust: `audio::physical::commuted_synthesis` +#[pyfunction] +#[pyo3(name = "commuted_synthesis", signature = (body_ir, excitation, string, n))] +pub fn pyfn_commuted_synthesis(body_ir: Vec, excitation: Vec, string: pyo3::PyRefMut<'_, crate::generated::types::PyWaveguideString>, n: usize) -> PyResult> { + let mut string = string; + let __r = crate::runtime::guard(|| rust_physics_engine::audio::physical::commuted_synthesis(&body_ir, &excitation, &mut string.inner, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Piano hammer-string contact: a hammer of mass `hammer_mass` (kg) with +/// initial velocity `hammer_vel` compresses a nonlinear felt spring +/// F = k ξ^p against the string. Returns the contact force history +/// (one sample per tick until separation); the string is excited in place. +/// +/// Rust: `audio::physical::hammer_string_interaction` +#[pyfunction] +#[pyo3(name = "hammer_string_interaction", signature = (string, hammer_mass, hammer_vel, stiffness_exp, k))] +pub fn pyfn_hammer_string_interaction(string: pyo3::PyRefMut<'_, crate::generated::types::PyWaveguideString>, hammer_mass: f64, hammer_vel: f64, stiffness_exp: f64, k: f64) -> PyResult> { + let mut string = string; + let __r = crate::runtime::guard(|| rust_physics_engine::audio::physical::hammer_string_interaction(&mut string.inner, hammer_mass, hammer_vel, stiffness_exp, k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Single-reed reflection coefficient as a function of the pressure +/// difference across the reed (STK-style linear table, clamped to ±1). +/// +/// Rust: `audio::physical::reed_nonlinearity` +#[pyfunction] +#[pyo3(name = "reed_nonlinearity", signature = (delta_p, stiffness, closing_p))] +pub fn pyfn_reed_nonlinearity(delta_p: f64, stiffness: f64, closing_p: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::physical::reed_nonlinearity(delta_p, stiffness, closing_p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Flute jet nonlinearity x - x³, clamped to ±1. +/// +/// Rust: `audio::physical::jet_nonlinearity` +#[pyfunction] +#[pyo3(name = "jet_nonlinearity", signature = (x))] +pub fn pyfn_jet_nonlinearity(x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::physical::jet_nonlinearity(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Brass lip valve: pressure-controlled transmission coefficient; the +/// lips open on positive mouth-bore pressure difference. +/// +/// Rust: `audio::physical::lip_model` +#[pyfunction] +#[pyo3(name = "lip_model", signature = (delta_p, lip_tension))] +pub fn pyfn_lip_model(delta_p: f64, lip_tension: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::physical::lip_model(delta_p, lip_tension)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Build a Kelly-Lochbaum lattice from a tract area function (cm² or any +/// consistent unit); each section is one sample of travel at `fs`. +/// +/// Rust: `audio::physical::vocal_tract` +#[pyfunction] +#[pyo3(name = "vocal_tract", signature = (area_function, fs))] +pub fn pyfn_vocal_tract(area_function: Vec, fs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::physical::vocal_tract(&area_function, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyKellyLochbaum { inner: __v }) +} + +/// Simplified Liljencrants-Fant glottal flow *derivative* pulse over one +/// period t ∈ [0, t0): exponentially growing sinusoid up to `te` (peak of +/// the sinusoid at `tp`), then an exponential return phase with time +/// constant `ta`. +/// +/// Rust: `audio::physical::glottal_pulse_lf` +#[pyfunction] +#[pyo3(name = "glottal_pulse_lf", signature = (t, t0, te, tp, ta))] +pub fn pyfn_glottal_pulse_lf(t: f64, t0: f64, te: f64, tp: f64, ta: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::physical::glottal_pulse_lf(t, t0, te, tp, ta)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Rosenberg glottal flow pulse: raised-cosine rise over the first 2/3 of +/// the open phase, cosine fall over the last 1/3, zero when closed. +/// `phase` in [0, 1), `open_quotient` in (0, 1]. +/// +/// Rust: `audio::physical::rosenberg_pulse` +#[pyfunction] +#[pyo3(name = "rosenberg_pulse", signature = (phase, open_quotient))] +pub fn pyfn_rosenberg_pulse(phase: f64, open_quotient: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::physical::rosenberg_pulse(phase, open_quotient)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Tension (N) needed for a string of `length` (m) and line density `mu` +/// (kg/m) to sound at `freq`: T = μ (2 L f)². +/// +/// Rust: `audio::physical::string_tension_from_freq` +#[pyfunction] +#[pyo3(name = "string_tension_from_freq", signature = (freq, length, mu))] +pub fn pyfn_string_tension_from_freq(freq: f64, length: f64, mu: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::physical::string_tension_from_freq(freq, length, mu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Piano-style stretched partials f_k = k f0 √(1 + B k²). +/// +/// Rust: `audio::physical::inharmonic_partials` +#[pyfunction] +#[pyo3(name = "inharmonic_partials", signature = (f0, b, n))] +pub fn pyfn_inharmonic_partials<'py>(py: Python<'py>, f0: f64, b: f64, n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::physical::inharmonic_partials(f0, b, n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_banded_waveguide, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_commuted_synthesis, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hammer_string_interaction, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_reed_nonlinearity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_jet_nonlinearity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lip_model, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_vocal_tract, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_glottal_pulse_lf, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rosenberg_pulse, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_string_tension_from_freq, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_inharmonic_partials, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_audio__spatial.rs b/bindings/python/src/generated/m_audio__spatial.rs new file mode 100644 index 0000000..540e0a0 --- /dev/null +++ b/bindings/python/src/generated/m_audio__spatial.rs @@ -0,0 +1,410 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Linear pan law; `pos` in [-1 (left), 1 (right)]. +/// +/// Rust: `audio::spatial::pan_linear` +#[pyfunction] +#[pyo3(name = "pan_linear", signature = (x, pos))] +pub fn pyfn_pan_linear(x: f64, pos: f64) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::spatial::pan_linear(x, pos)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Constant-power (-3 dB center) pan law. +/// +/// Rust: `audio::spatial::pan_constant_power` +#[pyfunction] +#[pyo3(name = "pan_constant_power", signature = (x, pos))] +pub fn pyfn_pan_constant_power(x: f64, pos: f64) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::spatial::pan_constant_power(x, pos)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// -4.5 dB-center compromise pan law (geometric mean of the linear and +/// constant-power laws). +/// +/// Rust: `audio::spatial::pan_minus_4_5_db` +#[pyfunction] +#[pyo3(name = "pan_minus_4_5_db", signature = (x, pos))] +pub fn pyfn_pan_minus_4_5_db(x: f64, pos: f64) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::spatial::pan_minus_4_5_db(x, pos)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// 2D VBAP: gains for `speaker_angles` (radians, unsorted) reproducing a +/// source at `angle`; only the flanking pair is nonzero. +/// +/// Rust: `audio::spatial::pan_vbap_2d` +#[pyfunction] +#[pyo3(name = "pan_vbap_2d", signature = (angle, speaker_angles))] +pub fn pyfn_pan_vbap_2d<'py>(py: Python<'py>, angle: f64, speaker_angles: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::spatial::pan_vbap_2d(angle, &speaker_angles))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// 3D VBAP over speaker triplets: picks the triplet giving all-positive +/// gains with the best conditioning, normalized to unit power. +/// +/// Rust: `audio::spatial::pan_vbap_3d` +#[pyfunction] +#[pyo3(name = "pan_vbap_3d", signature = (dir, speakers))] +pub fn pyfn_pan_vbap_3d<'py>(py: Python<'py>, dir: crate::generated::types::PyVec3Arg, speakers: Vec) -> PyResult> { + let dir = dir.0; + let speakers = speakers.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::spatial::pan_vbap_3d(dir, &speakers))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// First-order B-format (FuMa WXYZ) encoding of one sample. +/// +/// Rust: `audio::spatial::ambisonics_encode_1st` +#[pyfunction] +#[pyo3(name = "ambisonics_encode_1st", signature = (x, azimuth, elevation))] +pub fn pyfn_ambisonics_encode_1st<'py>(py: Python<'py>, x: f64, azimuth: f64, elevation: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::spatial::ambisonics_encode_1st(x, azimuth, elevation))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_vec()) +} + +/// Higher-order ambisonic encoding (ACN channel order, SN3D weights) of +/// one sample; (order+1)² channels. +/// +/// Rust: `audio::spatial::ambisonics_encode` +#[pyfunction] +#[pyo3(name = "ambisonics_encode", signature = (x, az, el, order))] +pub fn pyfn_ambisonics_encode<'py>(py: Python<'py>, x: f64, az: f64, el: f64, order: u32) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::spatial::ambisonics_encode(x, az, el, order))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Basic projection decode of an ACN/SN3D signal set to speakers at +/// (azimuth, elevation) pairs. +/// +/// Rust: `audio::spatial::ambisonics_decode` +#[pyfunction] +#[pyo3(name = "ambisonics_decode", signature = (b, speakers, order))] +pub fn pyfn_ambisonics_decode<'py>(py: Python<'py>, b: Vec, speakers: Vec<(f64, f64)>, order: u32) -> PyResult> { + let speakers = speakers.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::spatial::ambisonics_decode(&b, &speakers, order))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Rotate an ACN/SN3D ambisonic frame by yaw/pitch/roll, via projection +/// onto a Fibonacci sphere sampling (exact for band-limited fields as the +/// sampling is dense; 256 points). +/// +/// Rust: `audio::spatial::ambisonics_rotate` +#[pyfunction] +#[pyo3(name = "ambisonics_rotate", signature = (b, yaw, pitch, roll, order))] +pub fn pyfn_ambisonics_rotate<'py>(py: Python<'py>, b: Vec, yaw: f64, pitch: f64, roll: f64, order: u32) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::spatial::ambisonics_rotate(&b, yaw, pitch, roll, order))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Woodworth interaural time difference (s) for a spherical head of +/// radius `head_radius`; `azimuth` in radians from the median plane. +/// +/// Rust: `audio::spatial::itd_woodworth` +#[pyfunction] +#[pyo3(name = "itd_woodworth", signature = (azimuth, head_radius, c))] +pub fn pyfn_itd_woodworth(azimuth: f64, head_radius: f64, c: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::spatial::itd_woodworth(azimuth, head_radius, c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Duda-Martens (Brown-Duda) spherical-head shadowing filter response at +/// one ear; `azimuth` is measured from that ear's axis (0 = ipsilateral). +/// +/// Rust: `audio::spatial::spherical_head_hrtf` +#[pyfunction] +#[pyo3(name = "spherical_head_hrtf", signature = (azimuth, freq, head_radius, c))] +pub fn pyfn_spherical_head_hrtf<'py>(py: Python<'py>, azimuth: f64, freq: f64, head_radius: f64, c: f64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::spatial::spherical_head_hrtf(azimuth, freq, head_radius, c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::complex_out(py, __v)) +} + +/// Interaural level difference (dB, positive = louder in the near ear) +/// from the spherical-head model. +/// +/// Rust: `audio::spatial::ild_spherical_head` +#[pyfunction] +#[pyo3(name = "ild_spherical_head", signature = (azimuth, freq, head_radius))] +pub fn pyfn_ild_spherical_head(azimuth: f64, freq: f64, head_radius: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::spatial::ild_spherical_head(azimuth, freq, head_radius)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Simple binaural rendering: ITD (fractional delay) plus first-order +/// head-shadow filtering per ear. +/// +/// Rust: `audio::spatial::binaural_simple` +#[pyfunction] +#[pyo3(name = "binaural_simple", signature = (x, azimuth, elevation, fs))] +pub fn pyfn_binaural_simple<'py>(py: Python<'py>, x: Vec, azimuth: f64, elevation: f64, fs: f64) -> PyResult<(Vec, Vec)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::spatial::binaural_simple(&x, azimuth, elevation, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Doppler by retarded-time resampling: the source moves along +/// `source_path(t)`; each output sample reads the emission-time signal +/// value with 1/r distance attenuation. +/// +/// Rust: `audio::spatial::doppler_resample` +#[pyfunction] +#[pyo3(name = "doppler_resample", signature = (x, source_path, listener, c, fs))] +pub fn pyfn_doppler_resample(x: Vec, source_path: pyo3::Py, listener: crate::generated::types::PyVec3Arg, c: f64, fs: f64) -> PyResult> { + let __cb_source_path = std::rc::Rc::new(crate::runtime::Callback::new(source_path)); + let source_path = { let __cb = __cb_source_path.clone(); move |__a0: f64| -> rust_physics_engine::math::Vec3 { { let __r = __cb.call::<_, crate::generated::types::PyVec3Arg>((__a0,), crate::generated::types::PyVec3Arg(rust_physics_engine::math::Vec3 { x: f64::NAN, y: f64::NAN, z: f64::NAN })); __r.0 } } }; + let listener = listener.0; + let __r = crate::runtime::guard(|| rust_physics_engine::audio::spatial::doppler_resample(&x, &source_path, listener, c, fs)); + crate::runtime::callback::check(&[&__cb_source_path], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Inverse-distance gain with reference distance and rolloff exponent. +/// +/// Rust: `audio::spatial::distance_gain` +#[pyfunction] +#[pyo3(name = "distance_gain", signature = (d, ref_d, rolloff))] +pub fn pyfn_distance_gain(d: f64, ref_d: f64, rolloff: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::spatial::distance_gain(d, ref_d, rolloff)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Atmospheric absorption over distance `d` approximated as a 2nd-order +/// Butterworth lowpass whose cutoff gives 3 dB of ISO 9613-style +/// high-frequency loss at that range. +/// +/// Rust: `audio::spatial::air_absorption_filter` +#[pyfunction] +#[pyo3(name = "air_absorption_filter", signature = (d, humidity, temp, fs))] +pub fn pyfn_air_absorption_filter(d: f64, humidity: f64, temp: f64, fs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::spatial::air_absorption_filter(d, humidity, temp, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySos { inner: __v }) +} + +/// Shoebox image-source impulse response (Allen-Berkley). `absorption` +/// holds wall absorption coefficients in the order +/// [-x, +x, -y, +y, -z, +z]. +/// +/// Rust: `audio::spatial::image_source_ir` +#[pyfunction] +#[pyo3(name = "image_source_ir", signature = (room, source, listener, absorption, max_order, fs, c))] +pub fn pyfn_image_source_ir<'py>(py: Python<'py>, room: crate::generated::types::PyVec3Arg, source: crate::generated::types::PyVec3Arg, listener: crate::generated::types::PyVec3Arg, absorption: Vec, max_order: usize, fs: f64, c: f64) -> PyResult> { + let room = room.0; + let source = source.0; + let listener = listener.0; + let absorption = <[f64; 6]>::try_from(absorption).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 6 values, got {}", __v.len())))?; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::spatial::image_source_ir(room, source, listener, absorption, max_order, fs, c))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Stochastic ray-traced energy impulse response in an arbitrary closed +/// mesh; `absorption[i]` indexes by triangle material. Amplitude is the +/// square root of collected energy per sample bin. +/// +/// Rust: `audio::spatial::ray_tracing_ir` +#[pyfunction] +#[pyo3(name = "ray_tracing_ir", signature = (room_mesh, source, listener, absorption, n_rays, max_bounces, fs, c, rng))] +pub fn pyfn_ray_tracing_ir(room_mesh: crate::generated::types::PyGeometryMeshMesh, source: crate::generated::types::PyVec3Arg, listener: crate::generated::types::PyVec3Arg, absorption: Vec, n_rays: usize, max_bounces: usize, fs: f64, c: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let room_mesh = room_mesh.inner; + let source = source.0; + let listener = listener.0; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::audio::spatial::ray_tracing_ir(&room_mesh, source, listener, &absorption, n_rays, max_bounces, fs, c, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Direct sound plus the six first-order reflections of a shoebox room: +/// (arrival time s, 1/(4πd) gain, unit direction of arrival). +/// +/// Rust: `audio::spatial::early_reflections` +#[pyfunction] +#[pyo3(name = "early_reflections", signature = (room, source, listener, c))] +pub fn pyfn_early_reflections(room: crate::generated::types::PyVec3Arg, source: crate::generated::types::PyVec3Arg, listener: crate::generated::types::PyVec3Arg, c: f64) -> PyResult> { + let room = room.0; + let source = source.0; + let listener = listener.0; + let __r = crate::runtime::guard(|| rust_physics_engine::audio::spatial::early_reflections(room, source, listener, c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, crate::generated::types::PyVec3 { inner: __x.2 })).collect::>()) +} + +/// Delay-and-sum beamformer steered toward the unit direction `steer` +/// (plane-wave model): aligns and averages the mic signals. +/// +/// Rust: `audio::spatial::beamforming_delay_sum` +#[pyfunction] +#[pyo3(name = "beamforming_delay_sum", signature = (mics, signals, steer, fs, c))] +pub fn pyfn_beamforming_delay_sum<'py>(py: Python<'py>, mics: Vec, signals: Vec>, steer: crate::generated::types::PyVec3Arg, fs: f64, c: f64) -> PyResult> { + let mics = mics.into_iter().map(|__e| __e.0).collect::>(); + let steer = steer.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::spatial::beamforming_delay_sum(&mics, &signals, steer, fs, c))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Narrowband frequency-domain MVDR beamformer at `freq`: per-block +/// spatial covariance with diagonal loading, steering toward `steer`. +/// +/// Rust: `audio::spatial::beamforming_mvdr` +#[pyfunction] +#[pyo3(name = "beamforming_mvdr", signature = (mics, signals, steer, freq, fs, c, diagonal_loading))] +pub fn pyfn_beamforming_mvdr<'py>(py: Python<'py>, mics: Vec, signals: Vec>, steer: crate::generated::types::PyVec3Arg, freq: f64, fs: f64, c: f64, diagonal_loading: f64) -> PyResult> { + let mics = mics.into_iter().map(|__e| __e.0).collect::>(); + let steer = steer.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::spatial::beamforming_mvdr(&mics, &signals, steer, freq, fs, c, diagonal_loading))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// GCC-PHAT time difference of arrival: delay of `b` relative to `a` in +/// seconds (positive = b lags a). +/// +/// Rust: `audio::spatial::tdoa_gcc_phat` +#[pyfunction] +#[pyo3(name = "tdoa_gcc_phat", signature = (a, b, fs))] +pub fn pyfn_tdoa_gcc_phat<'py>(py: Python<'py>, a: Vec, b: Vec, fs: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::spatial::tdoa_gcc_phat(&a, &b, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Least-squares source localization from TDOAs relative to `mics[0]` +/// (`tdoas[i]` is the extra delay at mic i+1), by Gauss-Newton. +/// +/// Rust: `audio::spatial::localize_tdoa` +#[pyfunction] +#[pyo3(name = "localize_tdoa", signature = (mics, tdoas, c))] +pub fn pyfn_localize_tdoa(mics: Vec, tdoas: Vec, c: f64) -> PyResult { + let mics = mics.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::audio::spatial::localize_tdoa(&mics, &tdoas, c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Round-trip echo time to range. +/// +/// Rust: `audio::spatial::sonar_range` +#[pyfunction] +#[pyo3(name = "sonar_range", signature = (t_echo, c))] +pub fn pyfn_sonar_range(t_echo: f64, c: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::spatial::sonar_range(t_echo, c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Active sonar equation: echo excess = SL - 2 TL + TS - (NL - DI), dB. +/// +/// Rust: `audio::spatial::sonar_equation` +#[pyfunction] +#[pyo3(name = "sonar_equation", signature = (sl, tl, ts, nl, di))] +pub fn pyfn_sonar_equation(sl: f64, tl: f64, ts: f64, nl: f64, di: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::spatial::sonar_equation(sl, tl, ts, nl, di)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Linkwitz-Riley 4th-order crossover: (lowpass, highpass), each two +/// cascaded 2nd-order Butterworth sections; the pair sums to allpass. +/// +/// Rust: `audio::spatial::speaker_crossover_lr4` +#[pyfunction] +#[pyo3(name = "speaker_crossover_lr4", signature = (fc, fs))] +pub fn pyfn_speaker_crossover_lr4(fc: f64, fs: f64) -> PyResult<(crate::generated::types::PySos, crate::generated::types::PySos)> { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::spatial::speaker_crossover_lr4(fc, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((crate::generated::types::PySos { inner: __v.0 }, crate::generated::types::PySos { inner: __v.1 })) +} + +/// Baffle-step compensation target: the +6 dB diffraction step of a +/// baffle of width `width_m`, centered at f3 = 115/width, as a high +/// shelf. +/// +/// Rust: `audio::spatial::speaker_baffle_step` +#[pyfunction] +#[pyo3(name = "speaker_baffle_step", signature = (width_m, fs))] +pub fn pyfn_speaker_baffle_step(width_m: f64, fs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::spatial::speaker_baffle_step(width_m, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySos { inner: __v }) +} + +/// Sealed-box (2nd-order highpass) response magnitude in dB of a driver +/// with free-air resonance `fs_driver`, total Q `qts`, and compliance +/// volume `vas` in a box of `box_volume` (same units), at frequency `f`. +/// +/// Rust: `audio::spatial::thiele_small_response` +#[pyfunction] +#[pyo3(name = "thiele_small_response", signature = (fs_driver, qts, vas, box_volume, f))] +pub fn pyfn_thiele_small_response(fs_driver: f64, qts: f64, vas: f64, box_volume: f64, f: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::spatial::thiele_small_response(fs_driver, qts, vas, box_volume, f)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_pan_linear, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pan_constant_power, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pan_minus_4_5_db, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pan_vbap_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pan_vbap_3d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ambisonics_encode_1st, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ambisonics_encode, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ambisonics_decode, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ambisonics_rotate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_itd_woodworth, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spherical_head_hrtf, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ild_spherical_head, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_binaural_simple, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_doppler_resample, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_distance_gain, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_air_absorption_filter, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_image_source_ir, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ray_tracing_ir, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_early_reflections, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_beamforming_delay_sum, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_beamforming_mvdr, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tdoa_gcc_phat, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_localize_tdoa, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sonar_range, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sonar_equation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_speaker_crossover_lr4, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_speaker_baffle_step, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_thiele_small_response, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_audio__synthesis.rs b/bindings/python/src/generated/m_audio__synthesis.rs new file mode 100644 index 0000000..7adf793 --- /dev/null +++ b/bindings/python/src/generated/m_audio__synthesis.rs @@ -0,0 +1,367 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Additive synthesis from (ratio, amplitude, phase) partials of a +/// fundamental `freq`. +/// +/// Rust: `audio::synthesis::additive` +#[pyfunction] +#[pyo3(name = "additive", signature = (harmonics, freq, n, fs))] +pub fn pyfn_additive<'py>(py: Python<'py>, harmonics: Vec<(f64, f64, f64)>, freq: f64, n: usize, fs: f64) -> PyResult> { + let harmonics = harmonics.into_iter().map(|__e| (__e.0, __e.1, __e.2)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::synthesis::additive(&harmonics, freq, n, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Additive synthesis with a per-partial amplitude envelope +/// (each envelope is resampled to n output samples). +/// +/// Rust: `audio::synthesis::additive_evolving` +#[pyfunction] +#[pyo3(name = "additive_evolving", signature = (harmonics, freq, n, fs))] +pub fn pyfn_additive_evolving<'py>(py: Python<'py>, harmonics: Vec<(f64, Vec)>, freq: f64, n: usize, fs: f64) -> PyResult> { + let harmonics = harmonics.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::synthesis::additive_evolving(&harmonics, freq, n, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Two-operator FM: sin(2πf_c·t + I·sin(2πf_m·t)). +/// +/// Rust: `audio::synthesis::fm_simple` +#[pyfunction] +#[pyo3(name = "fm_simple", signature = (carrier, modulator, index, n, fs))] +pub fn pyfn_fm_simple<'py>(py: Python<'py>, carrier: f64, modulator: f64, index: f64, n: usize, fs: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::synthesis::fm_simple(carrier, modulator, index, n, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Bessel sideband amplitudes |J_k(I)| for k = 0..n_sidebands. +/// +/// Rust: `audio::synthesis::fm_bessel_sidebands` +#[pyfunction] +#[pyo3(name = "fm_bessel_sidebands", signature = (index, n_sidebands))] +pub fn pyfn_fm_bessel_sidebands<'py>(py: Python<'py>, index: f64, n_sidebands: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::synthesis::fm_bessel_sidebands(index, n_sidebands))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Phase modulation (identical spectrum to `fm_simple` for a sine +/// modulator). +/// +/// Rust: `audio::synthesis::pm_simple` +#[pyfunction] +#[pyo3(name = "pm_simple", signature = (carrier, modulator, index, n, fs))] +pub fn pyfn_pm_simple<'py>(py: Python<'py>, carrier: f64, modulator: f64, index: f64, n: usize, fs: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::synthesis::pm_simple(carrier, modulator, index, n, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Amplitude modulation (1 + depth·sin(2πf_m t))·sin(2πf_c t). +/// +/// Rust: `audio::synthesis::am` +#[pyfunction] +#[pyo3(name = "am", signature = (carrier, modulator, depth, n, fs))] +pub fn pyfn_am<'py>(py: Python<'py>, carrier: f64, modulator: f64, depth: f64, n: usize, fs: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::synthesis::am(carrier, modulator, depth, n, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Ring modulation a·b. +/// +/// Rust: `audio::synthesis::ring_mod` +#[pyfunction] +#[pyo3(name = "ring_mod", signature = (a, b))] +pub fn pyfn_ring_mod<'py>(py: Python<'py>, a: Vec, b: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::synthesis::ring_mod(&a, &b))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Karplus-Strong plucked string: noise burst through the averaging +/// loop. `decay` scales the loop gain, `blend` the averaging strength. +/// +/// Rust: `audio::synthesis::karplus_strong` +#[pyfunction] +#[pyo3(name = "karplus_strong", signature = (freq, duration, fs, decay, blend, rng))] +pub fn pyfn_karplus_strong(freq: f64, duration: f64, fs: f64, decay: f64, blend: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::audio::synthesis::karplus_strong(freq, duration, fs, decay, blend, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Extended Karplus-Strong: pick position comb, pick-direction width +/// low-pass, and a dynamics low-pass on the excitation. +/// +/// Rust: `audio::synthesis::karplus_strong_extended` +#[pyfunction] +#[pyo3(name = "karplus_strong_extended", signature = (freq, duration, fs, pick_pos, pick_width, decay, dynamics))] +pub fn pyfn_karplus_strong_extended<'py>(py: Python<'py>, freq: f64, duration: f64, fs: f64, pick_pos: f64, pick_width: f64, decay: f64, dynamics: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::synthesis::karplus_strong_extended(freq, duration, fs, pick_pos, pick_width, decay, dynamics))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Subtractive synthesis: raw oscillator through a filter with an +/// amplitude envelope (`filter_env_amount` scales a per-sample cutoff +/// bias applied as post-gain tilt on the filtered signal — a simple +/// stand-in for a modulated-cutoff filter). +/// +/// Rust: `audio::synthesis::subtractive` +#[pyfunction] +#[pyo3(name = "subtractive", signature = (source, freq, filter, env, filter_env_amount, n))] +pub fn pyfn_subtractive(source: crate::generated::types::PyWaveform, freq: f64, filter: pyo3::PyRefMut<'_, crate::generated::types::PySos>, env: pyo3::PyRefMut<'_, crate::generated::types::PyAdsr>, filter_env_amount: f64, n: usize) -> PyResult> { + let source = source.inner; + let mut filter = filter; + let mut env = env; + let __r = crate::runtime::guard(|| rust_physics_engine::audio::synthesis::subtractive(source, freq, &mut filter.inner, &mut env.inner, filter_env_amount, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Granular synthesis: Hann-windowed grains read from a source buffer +/// at `position` (0..1, with `spread` jitter), pitch shifted by +/// resampled playback, `density` grains per second. +/// +/// Rust: `audio::synthesis::granular` +#[pyfunction] +#[pyo3(name = "granular", signature = (grain_source, grain_size, density, pitch_shift, position, spread, n, fs, rng))] +pub fn pyfn_granular(grain_source: Vec, grain_size: f64, density: f64, pitch_shift: f64, position: f64, spread: f64, n: usize, fs: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::audio::synthesis::granular(&grain_source, grain_size, density, pitch_shift, position, spread, n, fs, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Classic (Peterson-Barney-style) formant tables for the vowels +/// a, e, i, o, u: (frequency, bandwidth, amplitude) triples. +/// +/// Rust: `audio::synthesis::vowel_formants` +#[pyfunction] +#[pyo3(name = "vowel_formants", signature = (vowel, voice))] +pub fn pyfn_vowel_formants<'py>(py: Python<'py>, vowel: char, voice: crate::generated::types::PyVoice) -> PyResult> { + let voice = voice.to_rust(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::synthesis::vowel_formants(vowel, voice))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, __x.2)).collect::>()) +} + +/// Formant synthesis: a pulse-train glottal source through parallel +/// resonators (freq, bandwidth, amp). +/// +/// Rust: `audio::synthesis::formant_synth` +#[pyfunction] +#[pyo3(name = "formant_synth", signature = (f0, formants, n, fs))] +pub fn pyfn_formant_synth<'py>(py: Python<'py>, f0: f64, formants: Vec<(f64, f64, f64)>, n: usize, fs: f64) -> PyResult> { + let formants = formants.into_iter().map(|__e| (__e.0, __e.1, __e.2)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::synthesis::formant_synth(f0, &formants, n, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Pulsar synthesis: a formant-frequency sinusoid burst repeated at f0 +/// with the given duty cycle. +/// +/// Rust: `audio::synthesis::pulsar_synthesis` +#[pyfunction] +#[pyo3(name = "pulsar_synthesis", signature = (f0, formant, duty, n, fs))] +pub fn pyfn_pulsar_synthesis<'py>(py: Python<'py>, f0: f64, formant: f64, duty: f64, n: usize, fs: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::synthesis::pulsar_synthesis(f0, formant, duty, n, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Casio CZ-style phase distortion: warp the phase ramp before the +/// cosine lookup. `kind` 0 = knee (saw-like), 1 = resonant sweep. +/// +/// Rust: `audio::synthesis::phase_distortion` +#[pyfunction] +#[pyo3(name = "phase_distortion", signature = (phase, amount, kind))] +pub fn pyfn_phase_distortion(phase: f64, amount: f64, kind: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::synthesis::phase_distortion(phase, amount, kind)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Apply an arbitrary waveshaper. +/// +/// Rust: `audio::synthesis::waveshaper` +#[pyfunction] +#[pyo3(name = "waveshaper", signature = (x, f))] +pub fn pyfn_waveshaper(x: f64, f: pyo3::Py) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::audio::synthesis::waveshaper(x, &f)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Chebyshev waveshaper: Σ a_k·T_k(x) turns a pure cosine at amplitude +/// 1 into exactly the requested harmonic mix. +/// +/// Rust: `audio::synthesis::chebyshev_waveshaper` +#[pyfunction] +#[pyo3(name = "chebyshev_waveshaper", signature = (x, harmonic_amps))] +pub fn pyfn_chebyshev_waveshaper<'py>(py: Python<'py>, x: f64, harmonic_amps: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::synthesis::chebyshev_waveshaper(x, &harmonic_amps))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Hard-synced sawtooth: a slave saw retriggered at the master rate. +/// +/// Rust: `audio::synthesis::hard_sync_osc` +#[pyfunction] +#[pyo3(name = "hard_sync_osc", signature = (master_freq, slave_freq, n, fs))] +pub fn pyfn_hard_sync_osc<'py>(py: Python<'py>, master_freq: f64, slave_freq: f64, n: usize, fs: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::synthesis::hard_sync_osc(master_freq, slave_freq, n, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Detuned saw stack (JP-8000 style supersaw): `detune` is the maximum +/// relative detune of the outer voices. +/// +/// Rust: `audio::synthesis::supersaw` +#[pyfunction] +#[pyo3(name = "supersaw", signature = (freq, detune, n_voices, n, fs))] +pub fn pyfn_supersaw<'py>(py: Python<'py>, freq: f64, detune: f64, n_voices: usize, n: usize, fs: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::synthesis::supersaw(freq, detune, n_voices, n, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Sample playback with loop points and linear-interpolated rate +/// conversion. +/// +/// Rust: `audio::synthesis::sample_playback` +#[pyfunction] +#[pyo3(name = "sample_playback", signature = (sample, rate_ratio, loop_start, loop_end, n))] +pub fn pyfn_sample_playback<'py>(py: Python<'py>, sample: Vec, rate_ratio: f64, loop_start: usize, loop_end: usize, n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::synthesis::sample_playback(&sample, rate_ratio, loop_start, loop_end, n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Kick drum: exponential pitch sweep with an exponential amplitude +/// decay. +/// +/// Rust: `audio::synthesis::drum_kick` +#[pyfunction] +#[pyo3(name = "drum_kick", signature = (fs, pitch_start, pitch_end, decay))] +pub fn pyfn_drum_kick<'py>(py: Python<'py>, fs: f64, pitch_start: f64, pitch_end: f64, decay: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::synthesis::drum_kick(fs, pitch_start, pitch_end, decay))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Snare: tone plus band-passed noise, both decaying. +/// +/// Rust: `audio::synthesis::drum_snare` +#[pyfunction] +#[pyo3(name = "drum_snare", signature = (fs))] +pub fn pyfn_drum_snare<'py>(py: Python<'py>, fs: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::synthesis::drum_snare(fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Hi-hat: short bright filtered noise burst. +/// +/// Rust: `audio::synthesis::drum_hihat` +#[pyfunction] +#[pyo3(name = "drum_hihat", signature = (fs))] +pub fn pyfn_drum_hihat<'py>(py: Python<'py>, fs: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::synthesis::drum_hihat(fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Clap: a few staggered noise bursts. +/// +/// Rust: `audio::synthesis::drum_clap` +#[pyfunction] +#[pyo3(name = "drum_clap", signature = (fs))] +pub fn pyfn_drum_clap<'py>(py: Python<'py>, fs: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::synthesis::drum_clap(fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Tom: pitch-swept sine, longer than a kick. +/// +/// Rust: `audio::synthesis::drum_tom` +#[pyfunction] +#[pyo3(name = "drum_tom", signature = (fs, pitch))] +pub fn pyfn_drum_tom<'py>(py: Python<'py>, fs: f64, pitch: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::synthesis::drum_tom(fs, pitch))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Mix tracks with per-track gains (output as long as the longest track). +/// +/// Rust: `audio::synthesis::mix` +#[pyfunction] +#[pyo3(name = "mix", signature = (tracks, gains))] +pub fn pyfn_mix<'py>(py: Python<'py>, tracks: Vec>, gains: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::synthesis::mix(&tracks, &gains))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_additive, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_additive_evolving, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fm_simple, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fm_bessel_sidebands, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pm_simple, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_am, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ring_mod, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_karplus_strong, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_karplus_strong_extended, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_subtractive, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_granular, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_vowel_formants, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_formant_synth, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pulsar_synthesis, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_phase_distortion, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_waveshaper, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_chebyshev_waveshaper, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hard_sync_osc, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_supersaw, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sample_playback, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_drum_kick, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_drum_snare, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_drum_hihat, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_drum_clap, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_drum_tom, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mix, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_audio__tuning.rs b/bindings/python/src/generated/m_audio__tuning.rs new file mode 100644 index 0000000..32601fb --- /dev/null +++ b/bindings/python/src/generated/m_audio__tuning.rs @@ -0,0 +1,342 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// MIDI frequencies (128 entries) for an equal temperament with +/// `n_divisions` steps per octave anchored at (`base_midi`, `base_hz`). +/// +/// Rust: `audio::tuning::equal_temperament` +#[pyfunction] +#[pyo3(name = "equal_temperament", signature = (n_divisions, base_hz, base_midi))] +pub fn pyfn_equal_temperament<'py>(py: Python<'py>, n_divisions: u32, base_hz: f64, base_midi: u8) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::tuning::equal_temperament(n_divisions, base_hz, base_midi))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// 5-limit just intonation ratios from the tonic. +/// +/// Rust: `audio::tuning::just_intonation_5limit` +#[pyfunction] +#[pyo3(name = "just_intonation_5limit", signature = ())] +pub fn pyfn_just_intonation_5limit<'py>(py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::tuning::just_intonation_5limit())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_vec()) +} + +/// Pythagorean (3-limit) chromatic scale ratios. +/// +/// Rust: `audio::tuning::pythagorean` +#[pyfunction] +#[pyo3(name = "pythagorean", signature = ())] +pub fn pyfn_pythagorean<'py>(py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::tuning::pythagorean())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_vec()) +} + +/// Quarter-comma meantone: fifths flattened so major thirds are pure 5/4. +/// +/// Rust: `audio::tuning::meantone_quarter_comma` +#[pyfunction] +#[pyo3(name = "meantone_quarter_comma", signature = ())] +pub fn pyfn_meantone_quarter_comma<'py>(py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::tuning::meantone_quarter_comma())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_vec()) +} + +/// Werckmeister III well temperament (1691), as ratios from C. +/// +/// Rust: `audio::tuning::werckmeister_iii` +#[pyfunction] +#[pyo3(name = "werckmeister_iii", signature = ())] +pub fn pyfn_werckmeister_iii<'py>(py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::tuning::werckmeister_iii())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_vec()) +} + +/// Kirnberger III well temperament, as ratios from C. +/// +/// Rust: `audio::tuning::kirnberger_iii` +#[pyfunction] +#[pyo3(name = "kirnberger_iii", signature = ())] +pub fn pyfn_kirnberger_iii<'py>(py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::tuning::kirnberger_iii())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_vec()) +} + +/// Thomas Young's 1799 well temperament (Young II), as ratios from C. +/// +/// Rust: `audio::tuning::young` +#[pyfunction] +#[pyo3(name = "young", signature = ())] +pub fn pyfn_young<'py>(py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::tuning::young())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_vec()) +} + +/// Bohlen-Pierce scale: 13 equal divisions of the tritave (3:1); returns +/// the 14 ratios including both endpoints. +/// +/// Rust: `audio::tuning::bohlen_pierce` +#[pyfunction] +#[pyo3(name = "bohlen_pierce", signature = ())] +pub fn pyfn_bohlen_pierce<'py>(py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::tuning::bohlen_pierce())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Harmonic-series scale: partials n..2n reduced to ratios from 1 to 2. +/// +/// Rust: `audio::tuning::harmonic_series_scale` +#[pyfunction] +#[pyo3(name = "harmonic_series_scale", signature = (n))] +pub fn pyfn_harmonic_series_scale<'py>(py: Python<'py>, n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::tuning::harmonic_series_scale(n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Parse a Scala `.scl` file body into cents values (one per scale +/// degree, ending with the octave entry). Ratios like `3/2` and cents +/// like `701.955` are both accepted. +/// +/// Rust: `audio::tuning::scala_parse` +#[pyfunction] +#[pyo3(name = "scala_parse", signature = (scl))] +pub fn pyfn_scala_parse<'py>(py: Python<'py>, scl: String) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::tuning::scala_parse(&scl))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// Signed interval from `f1` to `f2` in cents. +/// +/// Rust: `audio::tuning::cents_between` +#[pyfunction] +#[pyo3(name = "cents_between", signature = (f1, f2))] +pub fn pyfn_cents_between(f1: f64, f2: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::tuning::cents_between(f1, f2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Frequency ratio to cents. +/// +/// Rust: `audio::tuning::ratio_to_cents` +#[pyfunction] +#[pyo3(name = "ratio_to_cents", signature = (r))] +pub fn pyfn_ratio_to_cents(r: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::tuning::ratio_to_cents(r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Cents to frequency ratio. +/// +/// Rust: `audio::tuning::cents_to_ratio` +#[pyfunction] +#[pyo3(name = "cents_to_ratio", signature = (c))] +pub fn pyfn_cents_to_ratio(c: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::tuning::cents_to_ratio(c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Nearest 12-TET MIDI note to `freq` for the given A4: returns +/// (midi, cents deviation from that note). +/// +/// Rust: `audio::tuning::nearest_note` +#[pyfunction] +#[pyo3(name = "nearest_note", signature = (freq, a4))] +pub fn pyfn_nearest_note(freq: f64, a4: f64) -> PyResult<(u8, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::tuning::nearest_note(freq, a4)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Name of the just interval closest to `ratio` (within 6 cents), or +/// "unknown". +/// +/// Rust: `audio::tuning::interval_name` +#[pyfunction] +#[pyo3(name = "interval_name", signature = (ratio))] +pub fn pyfn_interval_name(ratio: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::tuning::interval_name(ratio)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_string()) +} + +/// Plomp-Levelt consonance of two pure tones: 1 at unison, minimum near +/// a quarter of a critical band apart. +/// +/// Rust: `audio::tuning::consonance_plomp_levelt` +#[pyfunction] +#[pyo3(name = "consonance_plomp_levelt", signature = (f1, f2))] +pub fn pyfn_consonance_plomp_levelt(f1: f64, f2: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::tuning::consonance_plomp_levelt(f1, f2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Sethares dissonance curve: total pairwise Plomp-Levelt dissonance of +/// two copies of a `partials` timbre (`(ratio, amplitude)` relative to +/// `base` Hz) as the second copy sweeps through `ratio_range`. Returns +/// `n` points of (interval ratio, dissonance). +/// +/// Rust: `audio::tuning::dissonance_curve` +#[pyfunction] +#[pyo3(name = "dissonance_curve", signature = (base, partials, ratio_range, n))] +pub fn pyfn_dissonance_curve<'py>(py: Python<'py>, base: f64, partials: Vec<(f64, f64)>, ratio_range: (f64, f64), n: usize) -> PyResult> { + let partials = partials.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let ratio_range = (ratio_range.0, ratio_range.1); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::tuning::dissonance_curve(base, &partials, ratio_range, n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Piano stretch tuning deviation (cents from 12-TET) for a constant +/// string inharmonicity coefficient `b`: octaves are widened so partial 2 +/// of the lower note matches the fundamental of its octave. +/// +/// Rust: `audio::tuning::stretch_tuning_railsback` +#[pyfunction] +#[pyo3(name = "stretch_tuning_railsback", signature = (midi, b))] +pub fn pyfn_stretch_tuning_railsback(midi: f64, b: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::tuning::stretch_tuning_railsback(midi, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The syntonic comma 81/80. +/// +/// Rust: `audio::tuning::syntonic_comma` +#[pyfunction] +#[pyo3(name = "syntonic_comma", signature = ())] +pub fn pyfn_syntonic_comma() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::tuning::syntonic_comma()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Pythagorean comma 3¹²/2¹⁹. +/// +/// Rust: `audio::tuning::pythagorean_comma` +#[pyfunction] +#[pyo3(name = "pythagorean_comma", signature = ())] +pub fn pyfn_pythagorean_comma() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::tuning::pythagorean_comma()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The schisma 32805/32768 (Pythagorean comma / syntonic comma). +/// +/// Rust: `audio::tuning::schisma` +#[pyfunction] +#[pyo3(name = "schisma", signature = ())] +pub fn pyfn_schisma() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::tuning::schisma()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Frequency of a MIDI note in a 12-tone `temperament` (ratios from the +/// tonic C), anchored so that A4 (MIDI 69) sounds at `a4`. +/// +/// Rust: `audio::tuning::midi_to_freq_tuned` +#[pyfunction] +#[pyo3(name = "midi_to_freq_tuned", signature = (midi, a4, temperament))] +pub fn pyfn_midi_to_freq_tuned<'py>(py: Python<'py>, midi: u8, a4: f64, temperament: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::tuning::midi_to_freq_tuned(midi, a4, &temperament))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Pitch classes reached by successive fifths from `start`. +/// +/// Rust: `audio::tuning::circle_of_fifths` +#[pyfunction] +#[pyo3(name = "circle_of_fifths", signature = (start, n))] +pub fn pyfn_circle_of_fifths<'py>(py: Python<'py>, start: u8, n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::tuning::circle_of_fifths(start, n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Pitch classes of a scale on `root` (semitones 0-11, ascending). +/// +/// Rust: `audio::tuning::scale_degrees` +#[pyfunction] +#[pyo3(name = "scale_degrees", signature = (root, mode))] +pub fn pyfn_scale_degrees<'py>(py: Python<'py>, root: u8, mode: crate::generated::types::PyMode) -> PyResult> { + let mode = mode.to_rust(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::tuning::scale_degrees(root, mode))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Pitch classes of a chord on `root` (semitones 0-11). +/// +/// Rust: `audio::tuning::chord_tones` +#[pyfunction] +#[pyo3(name = "chord_tones", signature = (root, quality))] +pub fn pyfn_chord_tones<'py>(py: Python<'py>, root: u8, quality: crate::generated::types::PyChordQuality) -> PyResult> { + let quality = quality.to_rust(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::tuning::chord_tones(root, quality))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_equal_temperament, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_just_intonation_5limit, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pythagorean, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_meantone_quarter_comma, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_werckmeister_iii, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kirnberger_iii, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_young, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bohlen_pierce, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_harmonic_series_scale, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_scala_parse, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cents_between, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ratio_to_cents, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cents_to_ratio, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_nearest_note, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_interval_name, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_consonance_plomp_levelt, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dissonance_curve, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_stretch_tuning_railsback, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_syntonic_comma, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pythagorean_comma, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_schisma, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_midi_to_freq_tuned, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_circle_of_fifths, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_scale_degrees, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_chord_tones, m)?)?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_audio__vocoder.rs b/bindings/python/src/generated/m_audio__vocoder.rs new file mode 100644 index 0000000..970b368 --- /dev/null +++ b/bindings/python/src/generated/m_audio__vocoder.rs @@ -0,0 +1,132 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Classic channel vocoder: the modulator's band envelopes are imposed on +/// the carrier through a log-spaced bandpass bank. +/// +/// Rust: `audio::vocoder::channel_vocoder` +#[pyfunction] +#[pyo3(name = "channel_vocoder", signature = (carrier, modulator, n_bands, fs))] +pub fn pyfn_channel_vocoder<'py>(py: Python<'py>, carrier: Vec, modulator: Vec, n_bands: usize, fs: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::vocoder::channel_vocoder(&carrier, &modulator, n_bands, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// LPC analysis/resynthesis vocoder: per-frame all-pole envelopes driven +/// by a synthetic excitation. +/// +/// Rust: `audio::vocoder::lpc_vocoder` +#[pyfunction] +#[pyo3(name = "lpc_vocoder", signature = (x, order, frame, hop, excitation, fs))] +pub fn pyfn_lpc_vocoder<'py>(py: Python<'py>, x: Vec, order: usize, frame: usize, hop: usize, excitation: crate::generated::types::PyExcitation, fs: f64) -> PyResult> { + let excitation = excitation.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::vocoder::lpc_vocoder(&x, order, frame, hop, excitation, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// WSOLA time stretching: overlap-add of ~30 ms segments aligned by a +/// local cross-correlation search, preserving pitch. +/// +/// Rust: `audio::vocoder::wsola_time_stretch` +#[pyfunction] +#[pyo3(name = "wsola_time_stretch", signature = (x, ratio, fs))] +pub fn pyfn_wsola_time_stretch<'py>(py: Python<'py>, x: Vec, ratio: f64, fs: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::vocoder::wsola_time_stretch(&x, ratio, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// TD-PSOLA pitch shifting driven by a pitch track (as produced by +/// `audio::analysis::pitch_track`). +/// +/// Rust: `audio::vocoder::psola_pitch_shift` +#[pyfunction] +#[pyo3(name = "psola_pitch_shift", signature = (x, fs, f0_track, ratio))] +pub fn pyfn_psola_pitch_shift<'py>(py: Python<'py>, x: Vec, fs: f64, f0_track: Vec<(f64, Option)>, ratio: f64) -> PyResult> { + let f0_track = f0_track.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::vocoder::psola_pitch_shift(&x, fs, &f0_track, ratio))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Interpolate magnitudes between two sounds (phases from `a`); +/// `t` in 0..1. +/// +/// Rust: `audio::vocoder::spectral_morph` +#[pyfunction] +#[pyo3(name = "spectral_morph", signature = (a, b, t, n_fft, hop))] +pub fn pyfn_spectral_morph<'py>(py: Python<'py>, a: Vec, b: Vec, t: f64, n_fft: usize, hop: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::vocoder::spectral_morph(&a, &b, t, n_fft, hop))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Cross synthesis: the source's phases (fine structure) with the +/// filter's smoothed magnitude envelope. +/// +/// Rust: `audio::vocoder::cross_synthesis` +#[pyfunction] +#[pyo3(name = "cross_synthesis", signature = (source, filter, n_fft, hop))] +pub fn pyfn_cross_synthesis<'py>(py: Python<'py>, source: Vec, filter: Vec, n_fft: usize, hop: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::vocoder::cross_synthesis(&source, &filter, n_fft, hop))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Mix the dry signal with pitch-shifted copies at the given semitone +/// intervals. +/// +/// Rust: `audio::vocoder::harmonizer` +#[pyfunction] +#[pyo3(name = "harmonizer", signature = (x, fs, intervals))] +pub fn pyfn_harmonizer<'py>(py: Python<'py>, x: Vec, fs: f64, intervals: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::vocoder::harmonizer(&x, fs, &intervals))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Pull detected pitch toward the nearest pitch class in `scale` +/// (semitones 0-11); `strength` 0..1 is full correction at 1. Voiced +/// regions are retuned with phase-coherent PSOLA grains; unvoiced +/// regions pass through. +/// +/// Rust: `audio::vocoder::autotune` +#[pyfunction] +#[pyo3(name = "autotune", signature = (x, fs, scale, strength))] +pub fn pyfn_autotune<'py>(py: Python<'py>, x: Vec, fs: f64, scale: Vec, strength: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::vocoder::autotune(&x, fs, &scale, strength))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_channel_vocoder, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lpc_vocoder, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wsola_time_stretch, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_psola_pitch_shift, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spectral_morph, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cross_synthesis, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_harmonizer, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_autotune, m)?)?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_audio__wav.rs b/bindings/python/src/generated/m_audio__wav.rs new file mode 100644 index 0000000..9568ca7 --- /dev/null +++ b/bindings/python/src/generated/m_audio__wav.rs @@ -0,0 +1,146 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Decode a WAV byte stream. +/// +/// Errors: +/// `InvalidArgument` for malformed containers or unsupported encodings. +/// +/// Rust: `audio::wav::wav_read` +#[pyfunction] +#[pyo3(name = "wav_read", signature = (bytes))] +pub fn pyfn_wav_read(bytes: Vec) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::wav::wav_read(&bytes)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(crate::generated::types::PyWavData { inner: __v }) +} + +/// Encode to WAV bytes: PCM at 8/16/24/32 bits, or IEEE float at 32/64. +/// +/// Panics: +/// Panics for unsupported bit depths or mismatched channel lengths. +/// +/// Rust: `audio::wav::wav_write` +#[pyfunction] +#[pyo3(name = "wav_write", signature = (data, bits, float))] +pub fn pyfn_wav_write<'py>(py: Python<'py>, data: crate::generated::types::PyWavData, bits: u16, float: bool) -> PyResult> { + let data = data.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::wav::wav_write(&data, bits, float))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Read a WAV file from disk. +/// +/// Errors: +/// I/O errors from the filesystem; decode failures become +/// `InvalidData`. +/// +/// Rust: `audio::wav::wav_read_file` +#[pyfunction] +#[pyo3(name = "wav_read_file", signature = (path))] +pub fn pyfn_wav_read_file(path: String) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::wav::wav_read_file(&path)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::errors::map_display)?; + Ok(crate::generated::types::PyWavData { inner: __v }) +} + +/// Write a WAV file to disk. +/// +/// Errors: +/// I/O errors from the filesystem. +/// +/// Rust: `audio::wav::wav_write_file` +#[pyfunction] +#[pyo3(name = "wav_write_file", signature = (path, data, bits, float))] +pub fn pyfn_wav_write_file(path: String, data: crate::generated::types::PyWavData, bits: u16, float: bool) -> PyResult<()> { + let data = data.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::audio::wav::wav_write_file(&path, &data, bits, float)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::errors::map_display)?; + Ok(()) +} + +/// Header summary (fs, channels, bits, frames) without decoding samples. +/// +/// Errors: +/// `InvalidArgument` for malformed containers. +/// +/// Rust: `audio::wav::wav_info` +#[pyfunction] +#[pyo3(name = "wav_info", signature = (bytes))] +pub fn pyfn_wav_info<'py>(py: Python<'py>, bytes: Vec) -> PyResult<(u32, u16, u16, usize)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::wav::wav_info(&bytes))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok((__v.0, __v.1, __v.2, __v.3)) +} + +/// Average all channels down to one. +/// +/// Rust: `audio::wav::to_mono` +#[pyfunction] +#[pyo3(name = "to_mono", signature = (d))] +pub fn pyfn_to_mono<'py>(py: Python<'py>, d: crate::generated::types::PyWavData) -> PyResult> { + let d = d.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::wav::to_mono(&d))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Interleave channels (frame-major). +/// +/// Rust: `audio::wav::to_interleaved` +#[pyfunction] +#[pyo3(name = "to_interleaved", signature = (d))] +pub fn pyfn_to_interleaved<'py>(py: Python<'py>, d: crate::generated::types::PyWavData) -> PyResult> { + let d = d.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::wav::to_interleaved(&d))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Split an interleaved stream into per-channel vectors. +/// +/// Panics: +/// Panics if `channels == 0`. +/// +/// Rust: `audio::wav::from_interleaved` +#[pyfunction] +#[pyo3(name = "from_interleaved", signature = (x, channels))] +pub fn pyfn_from_interleaved<'py>(py: Python<'py>, x: Vec, channels: u16) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::wav::from_interleaved(&x, channels))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_wav_read, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wav_write, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wav_read_file, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wav_write_file, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wav_info, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_to_mono, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_to_interleaved, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_from_interleaved, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_biophysics.rs b/bindings/python/src/generated/m_biophysics.rs new file mode 100644 index 0000000..e540784 --- /dev/null +++ b/bindings/python/src/generated/m_biophysics.rs @@ -0,0 +1,255 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Nernst equation: E = (RT/(zF)) × ln(c_out/c_in) +/// +/// Rust: `biophysics::nernst_potential` +#[pyfunction] +#[pyo3(name = "nernst_potential", signature = (temperature, z, c_out, c_in))] +pub fn pyfn_nernst_potential(temperature: f64, z: f64, c_out: f64, c_in: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::nernst_potential(temperature, z, c_out, c_in)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Goldman-Hodgkin-Katz voltage equation for K⁺, Na⁺, and Cl⁻. +/// `Vm = (RT/F) × ln((Pk[K]o + Pna[Na]o + Pcl[Cl]i) / (Pk[K]i + Pna[Na]i + Pcl[Cl]o))` +/// +/// Rust: `biophysics::goldman_potential` +#[pyfunction] +#[pyo3(name = "goldman_potential", signature = (temperature, pk, pna, pcl, k_out, k_in, na_out, na_in, cl_out, cl_in))] +pub fn pyfn_goldman_potential(temperature: f64, pk: f64, pna: f64, pcl: f64, k_out: f64, k_in: f64, na_out: f64, na_in: f64, cl_out: f64, cl_in: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::goldman_potential(temperature, pk, pna, pcl, k_out, k_in, na_out, na_in, cl_out, cl_in)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Typical resting membrane potential for a neuron: -70 mV +/// +/// Rust: `biophysics::resting_membrane_potential_typical` +#[pyfunction] +#[pyo3(name = "resting_membrane_potential_typical", signature = ())] +pub fn pyfn_resting_membrane_potential_typical() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::resting_membrane_potential_typical()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Michaelis-Menten kinetics: `v = Vmax × [S] / (Km + [S])` +/// +/// Rust: `biophysics::michaelis_menten` +#[pyfunction] +#[pyo3(name = "michaelis_menten", signature = (vmax, km, substrate))] +pub fn pyfn_michaelis_menten(vmax: f64, km: f64, substrate: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::michaelis_menten(vmax, km, substrate)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Competitive inhibition: `v = Vmax × [S] / (Km(1 + [I]/Ki) + [S])` +/// +/// Rust: `biophysics::michaelis_menten_inhibited` +#[pyfunction] +#[pyo3(name = "michaelis_menten_inhibited", signature = (vmax, km, substrate, inhibitor, ki))] +pub fn pyfn_michaelis_menten_inhibited(vmax: f64, km: f64, substrate: f64, inhibitor: f64, ki: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::michaelis_menten_inhibited(vmax, km, substrate, inhibitor, ki)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Lineweaver-Burk transform: returns `(1/[S], 1/v)` for double-reciprocal plot +/// +/// Rust: `biophysics::lineweaver_burk` +#[pyfunction] +#[pyo3(name = "lineweaver_burk", signature = (vmax, km, substrate))] +pub fn pyfn_lineweaver_burk(vmax: f64, km: f64, substrate: f64) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::lineweaver_burk(vmax, km, substrate)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Hill equation for cooperative binding: `v = Vmax × [S]^n / (K^n + [S]^n)` +/// +/// Rust: `biophysics::hill_equation` +#[pyfunction] +#[pyo3(name = "hill_equation", signature = (vmax, k, substrate, n))] +pub fn pyfn_hill_equation(vmax: f64, k: f64, substrate: f64, n: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::hill_equation(vmax, k, substrate, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Derive Hill coefficient from two (substrate, velocity) data points. +/// n = log((v1/(Vmax-v1)) / (v2/(Vmax-v2))) / log(s1/s2) +/// +/// Rust: `biophysics::hill_coefficient_from_data` +#[pyfunction] +#[pyo3(name = "hill_coefficient_from_data", signature = (s1, v1, s2, v2, vmax))] +pub fn pyfn_hill_coefficient_from_data(s1: f64, v1: f64, s2: f64, v2: f64, vmax: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::hill_coefficient_from_data(s1, v1, s2, v2, vmax)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Exponential growth: N = N₀ × e^(rt) +/// +/// Rust: `biophysics::exponential_growth` +#[pyfunction] +#[pyo3(name = "exponential_growth", signature = (n0, rate, time))] +pub fn pyfn_exponential_growth(n0: f64, rate: f64, time: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::exponential_growth(n0, rate, time)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Logistic growth: N = K / (1 + ((K - N₀)/N₀) × e^(-rt)) +/// +/// Rust: `biophysics::logistic_growth` +#[pyfunction] +#[pyo3(name = "logistic_growth", signature = (n0, k, r, time))] +pub fn pyfn_logistic_growth(n0: f64, k: f64, r: f64, time: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::logistic_growth(n0, k, r, time)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Doubling time: td = ln(2) / r +/// +/// Rust: `biophysics::doubling_time_population` +#[pyfunction] +#[pyo3(name = "doubling_time_population", signature = (rate))] +pub fn pyfn_doubling_time_population(rate: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::doubling_time_population(rate)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Lotka-Volterra prey rate: dx/dt = αx - βxy +/// +/// Rust: `biophysics::lotka_volterra_prey` +#[pyfunction] +#[pyo3(name = "lotka_volterra_prey", signature = (prey, predator, alpha, beta))] +pub fn pyfn_lotka_volterra_prey(prey: f64, predator: f64, alpha: f64, beta: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::lotka_volterra_prey(prey, predator, alpha, beta)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Lotka-Volterra predator rate: dy/dt = δxy - γy +/// +/// Rust: `biophysics::lotka_volterra_predator` +#[pyfunction] +#[pyo3(name = "lotka_volterra_predator", signature = (prey, predator, delta, gamma))] +pub fn pyfn_lotka_volterra_predator(prey: f64, predator: f64, delta: f64, gamma: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::lotka_volterra_predator(prey, predator, delta, gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Cardiac output: CO = SV × HR (L/min when SV in L/beat and HR in bpm) +/// +/// Rust: `biophysics::cardiac_output` +#[pyfunction] +#[pyo3(name = "cardiac_output", signature = (stroke_volume, heart_rate))] +pub fn pyfn_cardiac_output(stroke_volume: f64, heart_rate: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::cardiac_output(stroke_volume, heart_rate)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Mean arterial pressure: MAP = DBP + (SBP - DBP)/3 +/// +/// Rust: `biophysics::mean_arterial_pressure` +#[pyfunction] +#[pyo3(name = "mean_arterial_pressure", signature = (systolic, diastolic))] +pub fn pyfn_mean_arterial_pressure(systolic: f64, diastolic: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::mean_arterial_pressure(systolic, diastolic)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Vascular resistance: R = ΔP / Q +/// +/// Rust: `biophysics::vascular_resistance` +#[pyfunction] +#[pyo3(name = "vascular_resistance", signature = (pressure_drop, flow))] +pub fn pyfn_vascular_resistance(pressure_drop: f64, flow: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::vascular_resistance(pressure_drop, flow)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Poiseuille flow in a vessel: Q = πr⁴ΔP / (8μL) +/// +/// Rust: `biophysics::poiseuille_blood_flow` +#[pyfunction] +#[pyo3(name = "poiseuille_blood_flow", signature = (radius, pressure_drop, viscosity, length))] +pub fn pyfn_poiseuille_blood_flow(radius: f64, pressure_drop: f64, viscosity: f64, length: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::poiseuille_blood_flow(radius, pressure_drop, viscosity, length)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Sigmoid function: f = 1 / (1 + exp(-slope × (x - x50))) +/// +/// Rust: `biophysics::sigmoid` +#[pyfunction] +#[pyo3(name = "sigmoid", signature = (x, x50, slope))] +pub fn pyfn_sigmoid(x: f64, x50: f64, slope: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::sigmoid(x, x50, slope)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// LD50 probit model: probability = sigmoid(ln(dose), ln(ld50), slope) +/// +/// Rust: `biophysics::ld50_probit` +#[pyfunction] +#[pyo3(name = "ld50_probit", signature = (dose, ld50, slope))] +pub fn pyfn_ld50_probit(dose: f64, ld50: f64, slope: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::ld50_probit(dose, ld50, slope)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_nernst_potential, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_goldman_potential, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_resting_membrane_potential_typical, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_michaelis_menten, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_michaelis_menten_inhibited, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lineweaver_burk, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hill_equation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hill_coefficient_from_data, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_exponential_growth, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_logistic_growth, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_doubling_time_population, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lotka_volterra_prey, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lotka_volterra_predator, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cardiac_output, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mean_arterial_pressure, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_vascular_resistance, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_poiseuille_blood_flow, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sigmoid, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ld50_probit, m)?)?; + m.add("FARADAY_BIO", rust_physics_engine::biophysics::FARADAY_BIO)?; + m.add("GAS_CONSTANT", rust_physics_engine::biophysics::GAS_CONSTANT)?; + m.add("BODY_TEMP", rust_physics_engine::biophysics::BODY_TEMP)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_biophysics__epidemiology.rs b/bindings/python/src/generated/m_biophysics__epidemiology.rs new file mode 100644 index 0000000..10b9fa1 --- /dev/null +++ b/bindings/python/src/generated/m_biophysics__epidemiology.rs @@ -0,0 +1,531 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// The classical SIR model. +/// +/// Errors: +/// Returns an error for negative rates, a bad initial condition, or a +/// non-positive end time. +/// +/// Rust: `biophysics::epidemiology::sir` +#[pyfunction] +#[pyo3(name = "sir", signature = (beta, gamma, s0, i0, t_end))] +pub fn pyfn_sir(beta: f64, gamma: f64, s0: f64, i0: f64, t_end: f64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::epidemiology::sir(beta, gamma, s0, i0, t_end)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyEpidemicSample { inner: __x }).collect::>()) +} + +/// SIS: recovery returns an individual to the susceptible pool, so there is +/// no removed class and the disease can persist indefinitely. +/// +/// The distinction from SIR is not a detail. With no removed class the +/// epidemic has an *endemic equilibrium* at `1 - 1/R0` rather than burning +/// out, which is why the same pathogen parameters give a one-off wave in one +/// model and a permanent prevalence in the other. +/// +/// Errors: +/// Returns an error on the same conditions as `sir`. +/// +/// Rust: `biophysics::epidemiology::sis` +#[pyfunction] +#[pyo3(name = "sis", signature = (beta, gamma, s0, i0, t_end))] +pub fn pyfn_sis(beta: f64, gamma: f64, s0: f64, i0: f64, t_end: f64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::epidemiology::sis(beta, gamma, s0, i0, t_end)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyEpidemicSample { inner: __x }).collect::>()) +} + +/// SIRS: immunity wanes at rate `omega`, returning the removed to the +/// susceptible pool. +/// +/// Errors: +/// Returns an error on the same conditions as `sir`. +/// +/// Rust: `biophysics::epidemiology::sirs` +#[pyfunction] +#[pyo3(name = "sirs", signature = (beta, gamma, omega, s0, i0, t_end))] +pub fn pyfn_sirs(beta: f64, gamma: f64, omega: f64, s0: f64, i0: f64, t_end: f64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::epidemiology::sirs(beta, gamma, omega, s0, i0, t_end)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyEpidemicSample { inner: __x }).collect::>()) +} + +/// SEIR: an exposed class that is infected but not yet infectious, entered +/// at the infection rate and left at rate `sigma`. +/// +/// The latent period does not change the final size at all -- that depends +/// on `R0` alone -- but it slows the *growth rate*, which is what makes two +/// pathogens with the same `R0` and different incubation periods look so +/// different in the first month. +/// +/// Errors: +/// Returns an error on the same conditions as `sir`. +/// +/// Rust: `biophysics::epidemiology::seir` +#[pyfunction] +#[pyo3(name = "seir", signature = (beta, sigma, gamma, s0, e0, i0, t_end))] +pub fn pyfn_seir(beta: f64, sigma: f64, gamma: f64, s0: f64, e0: f64, i0: f64, t_end: f64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::epidemiology::seir(beta, sigma, gamma, s0, e0, i0, t_end)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyEpidemicSample { inner: __x }).collect::>()) +} + +/// SEIRS: SEIR with waning immunity. +/// +/// Errors: +/// Returns an error on the same conditions as `sir`. +/// +/// Rust: `biophysics::epidemiology::seirs` +#[pyfunction] +#[pyo3(name = "seirs", signature = (beta, sigma, gamma, omega, s0, e0, i0, t_end))] +pub fn pyfn_seirs(beta: f64, sigma: f64, gamma: f64, omega: f64, s0: f64, e0: f64, i0: f64, t_end: f64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::epidemiology::seirs(beta, sigma, gamma, omega, s0, e0, i0, t_end)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyEpidemicSample { inner: __x }).collect::>()) +} + +/// MSIR: an additional class of infants protected by maternal antibodies, +/// which are lost at rate `delta`. +/// +/// Returns `(time, M, S, I, R)`. The maternal class is why measles +/// vaccination is not given at birth: the antibodies that protect the infant +/// also neutralise the vaccine. +/// +/// Errors: +/// Returns an error on the same conditions as `sir`. +/// +/// Rust: `biophysics::epidemiology::msir` +#[pyfunction] +#[pyo3(name = "msir", signature = (beta, gamma, delta, m0, s0, i0, t_end))] +pub fn pyfn_msir<'py>(py: Python<'py>, beta: f64, gamma: f64, delta: f64, m0: f64, s0: f64, i0: f64, t_end: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::epidemiology::msir(beta, gamma, delta, m0, s0, i0, t_end))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, __x.2, __x.3, __x.4)).collect::>()) +} + +/// `R0 = beta / gamma` for the SIR model. +/// +/// Errors: +/// Returns an error for a non-positive recovery rate, for which the +/// infectious period is unbounded and `R0` is not defined. +/// +/// Rust: `biophysics::epidemiology::r0_sir` +#[pyfunction] +#[pyo3(name = "r0_sir", signature = (beta, gamma))] +pub fn pyfn_r0_sir(beta: f64, gamma: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::epidemiology::r0_sir(beta, gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The herd immunity threshold `1 - 1/R0`: the immune fraction at which the +/// effective reproduction number falls to one. +/// +/// This is the threshold for the epidemic to stop *growing*, not the +/// fraction that ends up infected. An epidemic that reaches the threshold +/// keeps going and overshoots it, because the people already infectious at +/// that moment go on to infect others; see `final_size_equation`, whose +/// answer is always larger. +/// +/// Errors: +/// Returns an error for `R0` below one, where no immunity is needed. +/// +/// Rust: `biophysics::epidemiology::herd_immunity_threshold` +#[pyfunction] +#[pyo3(name = "herd_immunity_threshold", signature = (r0))] +pub fn pyfn_herd_immunity_threshold(r0: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::epidemiology::herd_immunity_threshold(r0)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The final size of an epidemic: the fraction ever infected, from the +/// implicit relation `1 - z = exp(-R0 z)`. +/// +/// Solved by bisection, which is unconditionally safe here because +/// `f(z) = 1 - z - exp(-R0 z)` vanishes at zero, is *positive* just above it +/// for every `R0 > 1` -- its slope there is `R0 - 1` -- and is `-exp(-R0)` +/// at one. So the sought root is bracketed with `f` positive at the low end +/// and negative at the high end, which is the opposite of the usual +/// arrangement and the easy thing to get backwards. Newton's method on the +/// same equation converges too, but from a poor start it can step outside +/// `[0, 1]`, where the epidemic fraction has no meaning. +/// +/// Errors: +/// Returns an error for a negative `R0`. +/// +/// Rust: `biophysics::epidemiology::final_size_equation` +#[pyfunction] +#[pyo3(name = "final_size_equation", signature = (r0))] +pub fn pyfn_final_size_equation(r0: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::epidemiology::final_size_equation(r0)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The probability that an introduction of `i0` infectious individuals dies +/// out rather than becoming an epidemic. +/// +/// From the branching-process approximation, valid while susceptibles are +/// undepleted: each case's offspring are geometric with mean `R0`, the +/// extinction probability of one chain is `1/R0`, and independent chains +/// multiply. So even a pathogen with `R0 = 3` fails to establish about a +/// third of the time from a single case -- epidemics are rarer than their +/// reproduction numbers suggest, and the ones that happen are the survivors +/// of many that did not. +/// +/// Errors: +/// Returns an error for a negative `R0` or no introductions. +/// +/// Rust: `biophysics::epidemiology::extinction_probability_epidemic` +#[pyfunction] +#[pyo3(name = "extinction_probability_epidemic", signature = (r0, i0))] +pub fn pyfn_extinction_probability_epidemic(r0: f64, i0: u32) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::epidemiology::extinction_probability_epidemic(r0, i0)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The epidemic threshold of a contact network: the reciprocal of the +/// largest eigenvalue of its adjacency matrix. +/// +/// A disease spreads on the network when `beta / gamma` exceeds this. The +/// mean degree is *not* the right quantity: a network with a few very +/// highly connected nodes has a spectral radius far above its mean degree, +/// and its epidemic threshold is correspondingly lower. That is why a +/// scale-free contact structure sustains an epidemic that a homogeneous +/// network with the same average contact rate would not. +/// +/// Errors: +/// Returns an error for an empty graph or one with no edges, whose spectral +/// radius is zero and whose threshold is unbounded. +/// +/// Rust: `biophysics::epidemiology::epidemic_threshold_network` +#[pyfunction] +#[pyo3(name = "epidemic_threshold_network", signature = (g))] +pub fn pyfn_epidemic_threshold_network(g: crate::generated::types::PyGraph) -> PyResult { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::epidemiology::epidemic_threshold_network(&g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// SIR with a fraction `coverage` vaccinated before the epidemic begins. +/// +/// Vaccination moves people straight from susceptible to removed, so it acts +/// exactly like a reduced initial susceptible fraction -- which is why the +/// effect of a vaccination campaign on the final size is entirely captured +/// by `R0 (1 - coverage)`, and why the threshold coverage is the herd +/// immunity threshold. +/// +/// Errors: +/// Returns an error for a coverage outside zero to one, or on the same +/// conditions as `sir`. +/// +/// Rust: `biophysics::epidemiology::sir_with_vaccination` +#[pyfunction] +#[pyo3(name = "sir_with_vaccination", signature = (beta, gamma, coverage, i0, t_end))] +pub fn pyfn_sir_with_vaccination(beta: f64, gamma: f64, coverage: f64, i0: f64, t_end: f64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::epidemiology::sir_with_vaccination(beta, gamma, coverage, i0, t_end)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyEpidemicSample { inner: __x }).collect::>()) +} + +/// SIR with births and deaths at rate `mu`, both at the same rate so the +/// population is constant. +/// +/// Demography is what turns a one-off epidemic into an endemic disease: the +/// birth of new susceptibles replenishes the fuel, and the trajectory spirals +/// into an equilibrium at `S* = 1/R0` rather than burning out. The damped +/// oscillation on the way there is the source of the multi-year cycles seen +/// in measles before vaccination. +/// +/// Errors: +/// Returns an error for a negative rate, or on the same conditions as +/// `sir`. +/// +/// Rust: `biophysics::epidemiology::sir_with_demography` +#[pyfunction] +#[pyo3(name = "sir_with_demography", signature = (beta, gamma, mu, s0, i0, t_end))] +pub fn pyfn_sir_with_demography(beta: f64, gamma: f64, mu: f64, s0: f64, i0: f64, t_end: f64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::epidemiology::sir_with_demography(beta, gamma, mu, s0, i0, t_end)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyEpidemicSample { inner: __x }).collect::>()) +} + +/// Two strains competing for the same susceptible pool, with complete +/// cross-immunity. +/// +/// Returns `(time, S, I1, I2, R)`. Both strains end at zero: they compete +/// for one susceptible pool and this model does not replenish it, so the +/// epidemic ends when the susceptibles do. +/// +/// Which strain infects more is *not* settled by `R0` alone. Competitive +/// exclusion -- the fitter strain driving the other out however far behind +/// it starts -- is a statement about a system with susceptible +/// replenishment, where there is an indefinite future to be excluded from. +/// Here the race is finite, and a strain with a thousandfold head start can +/// out-infect a rival with nearly twice its reproduction number before the +/// susceptibles are gone. From equal starts the fitter strain does win. +/// +/// Errors: +/// Returns an error for negative rates or a bad initial condition. +/// +/// Rust: `biophysics::epidemiology::two_strain` +#[pyfunction] +#[pyo3(name = "two_strain", signature = (beta1, gamma1, beta2, gamma2, s0, i1, i2, t_end))] +pub fn pyfn_two_strain<'py>(py: Python<'py>, beta1: f64, gamma1: f64, beta2: f64, gamma2: f64, s0: f64, i1: f64, i2: f64, t_end: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::epidemiology::two_strain(beta1, gamma1, beta2, gamma2, s0, i1, i2, t_end))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, __x.2, __x.3, __x.4)).collect::>()) +} + +/// An age-structured SIR with a contact matrix. +/// +/// `contact[i][j]` is the rate at which a member of group `i` is contacted +/// by a member of group `j`, and `sizes` gives each group's share of the +/// population. Returns the trajectory as `(time, S, I, R)` with one entry +/// per group. +/// +/// Structure changes the threshold, not just the detail. `R0` is the largest +/// eigenvalue of the next-generation matrix, not the average contact rate +/// times the infectious period, and the two differ whenever contact is +/// assortative -- which it always is by age. +/// +/// Errors: +/// Returns an error for a non-square or negative contact matrix, group sizes +/// that do not sum to one, or a bad initial condition. +/// +/// Rust: `biophysics::epidemiology::age_structured` +#[pyfunction] +#[pyo3(name = "age_structured", signature = (contact, sizes, gamma, i0, t_end))] +pub fn pyfn_age_structured<'py>(py: Python<'py>, contact: Vec>, sizes: Vec, gamma: f64, i0: Vec, t_end: f64) -> PyResult, Vec, Vec)>> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::epidemiology::age_structured(&contact, &sizes, gamma, &i0, t_end))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, __x.2, __x.3)).collect::>()) +} + +/// `R0` for an age-structured model: the largest eigenvalue of the +/// next-generation matrix `K[a][b] = contact[a][b] * sizes[a] / (gamma * +/// sizes[b])`. +/// +/// Errors: +/// Returns an error on the same conditions as `age_structured`, or for a +/// non-positive recovery rate. +/// +/// Rust: `biophysics::epidemiology::r0_age_structured` +#[pyfunction] +#[pyo3(name = "r0_age_structured", signature = (contact, sizes, gamma))] +pub fn pyfn_r0_age_structured<'py>(py: Python<'py>, contact: Vec>, sizes: Vec, gamma: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::epidemiology::r0_age_structured(&contact, &sizes, gamma))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// An exact stochastic SIR by Gillespie's direct method, in whole +/// individuals. +/// +/// Returns `(time, S, I, R)` after each event. The deterministic model +/// cannot answer the question this one is for: with `R0 > 1` the +/// deterministic epidemic always takes off, while the stochastic one dies +/// out with probability `(1/R0)^i0` -- and that difference is not a +/// correction, it is the whole behaviour at small numbers. +/// +/// Errors: +/// Returns an error for negative rates, an empty population, or a +/// non-positive end time. +/// +/// Rust: `biophysics::epidemiology::sir_stochastic_gillespie` +#[pyfunction] +#[pyo3(name = "sir_stochastic_gillespie", signature = (beta, gamma, n, i0, t_end, rng))] +pub fn pyfn_sir_stochastic_gillespie(beta: f64, gamma: f64, n: u64, i0: u64, t_end: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::epidemiology::sir_stochastic_gillespie(beta, gamma, n, i0, t_end, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, __x.2, __x.3)).collect::>()) +} + +/// An SIR epidemic on a contact network. +/// +/// Each infectious node infects each susceptible neighbour at rate `beta` +/// and recovers at rate `gamma`. Returns the `(S, I, R)` counts after each +/// event. Unlike the well-mixed model the epidemic here is limited by the +/// *local* structure: a node cannot reinfect its own neighbourhood, so the +/// final size is smaller than the well-mixed prediction at the same `R0`. +/// +/// Errors: +/// Returns an error for negative rates, an empty graph, or a patient zero +/// outside it. +/// +/// Rust: `biophysics::epidemiology::network_sir` +#[pyfunction] +#[pyo3(name = "network_sir", signature = (g, beta, gamma, patient_zero, rng))] +pub fn pyfn_network_sir(g: crate::generated::types::PyGraph, beta: f64, gamma: f64, patient_zero: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let g = g.inner; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::epidemiology::network_sir(&g, beta, gamma, patient_zero, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, __x.2)).collect::>()) +} + +/// The effective reproduction number over time, by the Cori method. +/// +/// `R_t` is the ratio of today's incidence to the total infectiousness +/// present, where the latter is past incidence weighted by the serial +/// interval distribution. Returns one estimate per day from `window` +/// onward, and `NaN` before that -- there is no data yet, and reporting a +/// number there would be worse than reporting nothing. +/// +/// The distinction from a naive ratio of consecutive counts matters: that +/// ratio is a *growth rate*, and converting it to a reproduction number +/// requires knowing the generation time. Two epidemics doubling at the same +/// speed have very different `R_t` if one has a serial interval of three +/// days and the other of ten. +/// +/// Errors: +/// Returns an error for a negative incidence, a serial interval that is not +/// a distribution, or a window longer than the record. +/// +/// Rust: `biophysics::epidemiology::effective_r_estimate` +#[pyfunction] +#[pyo3(name = "effective_r_estimate", signature = (incidence, serial_interval, window))] +pub fn pyfn_effective_r_estimate<'py>(py: Python<'py>, incidence: Vec, serial_interval: Vec, window: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::epidemiology::effective_r_estimate(&incidence, &serial_interval, window))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Fits a gamma distribution to observed serial intervals by the method of +/// moments, returning `(shape, scale)`. +/// +/// Errors: +/// Returns an error for fewer than two observations, a non-positive +/// interval, or observations with no spread. +/// +/// Rust: `biophysics::epidemiology::serial_interval_fit` +#[pyfunction] +#[pyo3(name = "serial_interval_fit", signature = (intervals))] +pub fn pyfn_serial_interval_fit<'py>(py: Python<'py>, intervals: Vec) -> PyResult<(f64, f64)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::epidemiology::serial_interval_fit(&intervals))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// Fits `(beta, sigma, gamma)` of an SEIR model to an incidence series by +/// Nelder-Mead on the sum of squared errors. +/// +/// Fitting three rates to one incidence curve is close to the edge of what +/// the data supports: the growth rate constrains a *combination* of `beta` +/// and `sigma`, so the two trade off against each other along a valley in +/// the objective and are only weakly separated by the shape of the peak. +/// The returned fit reproduces the curve; it should not be read as three +/// independently identified parameters. +/// +/// The initial infectious fraction is taken from the first observation +/// rather than estimated, so if that first point is noisy or the epidemic +/// was already under way when reporting began, the resulting time offset +/// appears as a residual that no choice of rates can remove. Fitting it as a +/// fourth parameter would trade that bias for a worse identifiability +/// problem than the one already described. +/// +/// Errors: +/// Returns an error for fewer than five points, a negative incidence, or a +/// non-positive population. +/// +/// Rust: `biophysics::epidemiology::seir_fit_to_incidence` +#[pyfunction] +#[pyo3(name = "seir_fit_to_incidence", signature = (incidence, dt, population, guess))] +pub fn pyfn_seir_fit_to_incidence<'py>(py: Python<'py>, incidence: Vec, dt: f64, population: f64, guess: (f64, f64, f64)) -> PyResult<(f64, f64, f64)> { + let guess = (guess.0, guess.1, guess.2); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::epidemiology::seir_fit_to_incidence(&incidence, dt, population, guess))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// The Wallinga-Teunis case reproduction number. +/// +/// Where the Cori method asks "how many people is each *current* case +/// infecting", this asks "how many did each *past* case go on to infect", +/// by assigning each case's infector probabilistically among the earlier +/// cases in proportion to the serial interval. The two answer different +/// questions and disagree near the end of a record, where Wallinga-Teunis +/// is biased down because the infections have not happened yet. +/// +/// Errors: +/// Returns an error for a negative incidence or a serial interval that is +/// not a distribution. +/// +/// Rust: `biophysics::epidemiology::wallinga_teunis` +#[pyfunction] +#[pyo3(name = "wallinga_teunis", signature = (incidence, serial_interval))] +pub fn pyfn_wallinga_teunis<'py>(py: Python<'py>, incidence: Vec, serial_interval: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::epidemiology::wallinga_teunis(&incidence, &serial_interval))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_sir, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sis, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sirs, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_seir, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_seirs, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_msir, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_r0_sir, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_herd_immunity_threshold, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_final_size_equation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_extinction_probability_epidemic, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_epidemic_threshold_network, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sir_with_vaccination, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sir_with_demography, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_two_strain, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_age_structured, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_r0_age_structured, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sir_stochastic_gillespie, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_network_sir, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_effective_r_estimate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_serial_interval_fit, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_seir_fit_to_incidence, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wallinga_teunis, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_biophysics__neuro.rs b/bindings/python/src/generated/m_biophysics__neuro.rs new file mode 100644 index 0000000..1ace4a9 --- /dev/null +++ b/bindings/python/src/generated/m_biophysics__neuro.rs @@ -0,0 +1,872 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// The times at which a voltage trace crosses `level` upward, interpolated +/// between samples. +/// +/// The level is the caller's because the models here peak at very +/// different voltages: Hodgkin-Huxley and Izhikevich overshoot well past +/// zero, while `adex` tops out at `v_t + 10 * slope`, which is usually +/// still negative. A detector fixed at zero would report that an AdEx +/// neuron never fires. +/// +/// Rust: `biophysics::neuro::spike_times` +#[pyfunction] +#[pyo3(name = "spike_times", signature = (trace, level))] +pub fn pyfn_spike_times<'py>(py: Python<'py>, trace: Vec<(f64, f64)>, level: f64) -> PyResult> { + let trace = trace.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::neuro::spike_times(&trace, level))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The steady-state gating variables at a holding potential. +/// +/// A gate settles at `alpha / (alpha + beta)`; starting a run anywhere +/// else adds a transient that has nothing to do with the stimulus. +/// +/// Rust: `biophysics::neuro::hh_steady_state` +#[pyfunction] +#[pyo3(name = "hh_steady_state", signature = (v))] +pub fn pyfn_hh_steady_state(v: f64) -> PyResult<(f64, f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::neuro::hh_steady_state(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// The Hodgkin-Huxley membrane, integrated with fixed-step RK4. +/// +/// Returns `(t, V, m, h, n)` per step. The run starts from the gating +/// variables' steady state at `HH_V_REST`, so an unstimulated axon stays +/// where it is instead of relaxing through a spurious transient. +/// +/// The action potential is not built in. Sodium activation `m` is fast and +/// its cube makes the inward current explosive; inactivation `h` and +/// potassium activation `n` are ten times slower and end it. That +/// separation of timescales is the whole mechanism, and it is why the +/// threshold is a property of the trajectory rather than a parameter. +/// +/// A strongly *hyperpolarising* current is the one thing this integrator +/// cannot take. Below about -25 uA/cm^2 the voltage falls far enough that +/// `beta_m`, which grows exponentially as the membrane hyperpolarises, +/// reaches thousands per millisecond and a fixed step of 0.01 ms is no +/// longer stable. That is reported as a breakdown rather than returned as +/// a trace full of nonsense. Depolarising currents have no such limit: +/// hundreds of uA/cm^2 integrate cleanly, and simply drive the model into +/// depolarisation block. +/// +/// Errors: +/// Returns an error for a non-positive `t_end`, a `dt` outside `(0, 0.05]` +/// -- above which fixed-step RK4 loses the upstroke -- a `dt` that is not +/// smaller than `t_end`, or an integration that diverges. +/// +/// Rust: `biophysics::neuro::hodgkin_huxley` +#[pyfunction] +#[pyo3(name = "hodgkin_huxley", signature = (i_ext, t_end, dt))] +pub fn pyfn_hodgkin_huxley(i_ext: pyo3::Py, t_end: f64, dt: f64) -> PyResult> { + let __cb_i_ext = std::rc::Rc::new(crate::runtime::Callback::new(i_ext)); + let i_ext = { let __cb = __cb_i_ext.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::neuro::hodgkin_huxley(&i_ext, t_end, dt)); + crate::runtime::callback::check(&[&__cb_i_ext], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, __x.2, __x.3, __x.4)).collect::>()) +} + +/// The spike times in a Hodgkin-Huxley trace. +/// +/// Rust: `biophysics::neuro::hh_spike_times` +#[pyfunction] +#[pyo3(name = "hh_spike_times", signature = (trace))] +pub fn pyfn_hh_spike_times<'py>(py: Python<'py>, trace: Vec<(f64, f64, f64, f64, f64)>) -> PyResult> { + let trace = trace.into_iter().map(|__e| (__e.0, __e.1, __e.2, __e.3, __e.4)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::neuro::hh_spike_times(&trace))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The smallest sustained current, in uA/cm^2, that makes the model fire. +/// +/// Found by bisection on "does a 120 ms step produce a spike". This is the +/// rheobase, and it is not the same thing as a voltage threshold: a brief +/// pulse well above this current can fail to fire, and the model has no +/// single voltage at which firing becomes inevitable. +/// +/// Rust: `biophysics::neuro::hh_spike_threshold_estimate` +#[pyfunction] +#[pyo3(name = "hh_spike_threshold_estimate", signature = ())] +pub fn pyfn_hh_spike_threshold_estimate() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::neuro::hh_spike_threshold_estimate()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The firing rate in hertz against sustained current, for each current in +/// `currents`. +/// +/// Hodgkin-Huxley's F-I curve is discontinuous: at the rheobase the rate +/// jumps to about 50 Hz rather than rising from zero, because the +/// oscillation is born through a subcritical Hopf bifurcation with a +/// finite frequency. A neuron whose rate can be tuned smoothly to +/// arbitrarily low values -- a type I neuron -- needs a different +/// bifurcation, which `morris_lecar` can be parameterised to show. +/// +/// The first 30 ms of each run are discarded so the onset transient does +/// not enter the rate. +/// +/// Errors: +/// Returns an error if `currents` is empty or holds a value that is not +/// finite. +/// +/// Rust: `biophysics::neuro::hh_fi_curve` +#[pyfunction] +#[pyo3(name = "hh_fi_curve", signature = (currents))] +pub fn pyfn_hh_fi_curve<'py>(py: Python<'py>, currents: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::neuro::hh_fi_curve(¤ts))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// FitzHugh-Nagumo, the two-variable caricature of an excitable membrane. +/// +/// `dv/dt = v - v^3/3 - w + I`, `dw/dt = (v + a - b w) / tau`. Returns +/// `(t, v, w)` per step, dimensionless throughout. +/// +/// The point of the reduction is that two variables can be drawn: the +/// cubic `v` nullcline and the straight `w` nullcline cross at a fixed +/// point, and whether that crossing sits on the cubic's middle branch +/// decides whether the neuron rests or oscillates. Excitability -- a small +/// push decaying, a slightly larger one taking a long excursion -- is +/// visible in the phase plane in a way it is not in four dimensions. +/// +/// Errors: +/// Returns an error for a non-positive `tau`, or a run length or step size +/// out of range. +/// +/// Rust: `biophysics::neuro::fitzhugh_nagumo_neuron` +#[pyfunction] +#[pyo3(name = "fitzhugh_nagumo_neuron", signature = (a, b, tau, current, v0, w0, t_end, dt))] +pub fn pyfn_fitzhugh_nagumo_neuron<'py>(py: Python<'py>, a: f64, b: f64, tau: f64, current: f64, v0: f64, w0: f64, t_end: f64, dt: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::neuro::fitzhugh_nagumo_neuron(a, b, tau, current, v0, w0, t_end, dt))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, __x.2)).collect::>()) +} + +/// Morris-Lecar, a calcium-potassium membrane with one gating variable. +/// +/// Returns `(t, V, w)` per step. The calcium current is instantaneous, +/// which is what removes the second gate: only potassium activation `w` +/// has its own equation. +/// +/// Errors: +/// Returns an error for a non-positive capacitance or slope, or a run +/// length or step size out of range. +/// +/// Rust: `biophysics::neuro::morris_lecar` +#[pyfunction] +#[pyo3(name = "morris_lecar", signature = (params, current, v0, w0, t_end, dt))] +pub fn pyfn_morris_lecar<'py>(py: Python<'py>, params: crate::generated::types::PyMorrisLecar, current: f64, v0: f64, w0: f64, t_end: f64, dt: f64) -> PyResult> { + let params = params.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::neuro::morris_lecar(¶ms, current, v0, w0, t_end, dt))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, __x.2)).collect::>()) +} + +/// Izhikevich's two-variable spiking model. +/// +/// `v' = 0.04 v^2 + 5 v + 140 - u + I` and `u' = a (b v - u)`, with the +/// reset `v <- c`, `u <- u + d` once `v` reaches 30 mV. Returns `(t, v)` +/// per step, with the spike sample set to the 30 mV peak so a trace can be +/// plotted without the reset looking like a downstroke. +/// +/// The quadratic term is what makes it a spike generator rather than a +/// leaky integrator: above the unstable fixed point `v` runs away in finite +/// time, and the reset is what stops it. Two parameters then buy most of +/// the qualitative variety real neurons show -- see +/// `izhikevich_presets`. +/// +/// The published implementation advances `v` in two half-steps for +/// stability, and that is what is done here; `dt` is the reporting step. +/// +/// Errors: +/// Returns an error for a non-positive `a`, or a run length or step size +/// out of range. +/// +/// Rust: `biophysics::neuro::izhikevich` +#[pyfunction] +#[pyo3(name = "izhikevich", signature = (a, b, c, d, current, t_end, dt))] +pub fn pyfn_izhikevich<'py>(py: Python<'py>, a: f64, b: f64, c: f64, d: f64, current: f64, t_end: f64, dt: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::neuro::izhikevich(a, b, c, d, current, t_end, dt))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// The five firing patterns Izhikevich's paper names, as `(a, b, c, d)`. +/// +/// Regular spiking, intrinsically bursting, chattering, fast spiking and +/// low-threshold spiking. `c` and `d` set what happens after a spike, so +/// they are what separates a regular spiker from a burster; `a` and `b` +/// set the recovery variable's speed and its coupling to voltage. +/// +/// Rust: `biophysics::neuro::izhikevich_presets` +#[pyfunction] +#[pyo3(name = "izhikevich_presets", signature = ())] +pub fn pyfn_izhikevich_presets<'py>(py: Python<'py>) -> PyResult)>> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::neuro::izhikevich_presets())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0.to_string(), __x.1.to_vec())).collect::>()) +} + +/// The adaptive exponential integrate-and-fire neuron. +/// +/// `C dV/dt = -g_L (V - E_L) + g_L dt_slope exp((V - v_t)/dt_slope) - w + I` +/// with `tau_w dw/dt = a (V - E_L) - w`, and the reset `V <- v_reset`, +/// `w <- w + b` at the peak. Returns `(t, V, w)` per step. +/// +/// The exponential term is fitted to the sodium activation curve, so the +/// upstroke's *shape* near threshold is right even though the spike itself +/// is still stipulated. The adaptation current `w` is what the leaky +/// integrator lacks: it accumulates over a spike train and slows it, which +/// is the commonest firing pattern in cortex and cannot be produced by a +/// model with one variable. +/// +/// The recorded spike sample sits at the peak `v_t + 10 * slope`, which is +/// where `spike_times` should be pointed to count them. +/// +/// Errors: +/// Returns an error for a non-positive capacitance, conductance, slope or +/// adaptation time constant, or a run length or step size out of range. +/// +/// Rust: `biophysics::neuro::adex` +#[pyfunction] +#[pyo3(name = "adex", signature = (c_m, g_l, e_l, slope, v_t, tau_w, a, b, v_reset, current, t_end, dt))] +pub fn pyfn_adex<'py>(py: Python<'py>, c_m: f64, g_l: f64, e_l: f64, slope: f64, v_t: f64, tau_w: f64, a: f64, b: f64, v_reset: f64, current: f64, t_end: f64, dt: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::neuro::adex(c_m, g_l, e_l, slope, v_t, tau_w, a, b, v_reset, current, t_end, dt))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, __x.2)).collect::>()) +} + +/// The leaky integrate-and-fire neuron's spike times. +/// +/// `tau dV/dt = -(V - V_rest) + R I`, with a spike and a reset to +/// `v_reset` whenever `V` reaches `v_th`, and an absolute refractory +/// period during which the voltage is clamped. Gaussian current noise of +/// standard deviation `noise` is added per unit time, scaled so the +/// result does not depend on `dt`. +/// +/// The voltage between spikes carries no information the times do not, so +/// only the times are returned. +/// +/// Errors: +/// Returns an error for a non-positive `tau`, a negative refractory period +/// or noise, a threshold at or below the reset, or a run length or step +/// size out of range. +/// +/// Rust: `biophysics::neuro::lif_neuron` +#[pyfunction] +#[pyo3(name = "lif_neuron", signature = (current, tau, v_th, v_reset, refractory, noise, t_end, dt, rng))] +pub fn pyfn_lif_neuron(current: f64, tau: f64, v_th: f64, v_reset: f64, refractory: f64, noise: f64, t_end: f64, dt: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::neuro::lif_neuron(current, tau, v_th, v_reset, refractory, noise, t_end, dt, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The leaky integrate-and-fire firing rate in the noiseless case, exactly. +/// +/// `1 / (t_ref + tau ln((I - V_reset)/(I - V_th)))` for a current above +/// threshold, and zero otherwise. The rest potential is taken as zero, so +/// `I` is measured in the same units as the voltages. +/// +/// The logarithm is what makes the curve saturate: doubling a large +/// current barely changes the rate, because the refractory period comes to +/// dominate. Below `v_th` the neuron never fires however long you wait -- +/// the exact zero, not a very small number. +/// +/// Errors: +/// Returns an error for a non-positive `tau`, a negative refractory +/// period, or a threshold at or below the reset. +/// +/// Rust: `biophysics::neuro::lif_fi_exact` +#[pyfunction] +#[pyo3(name = "lif_fi_exact", signature = (current, tau, v_th, v_reset, refractory))] +pub fn pyfn_lif_fi_exact(current: f64, tau: f64, v_th: f64, v_reset: f64, refractory: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::neuro::lif_fi_exact(current, tau, v_th, v_reset, refractory)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The gaps between successive spikes. +/// +/// Errors: +/// Returns an error if the times are not sorted, since an unsorted train +/// would silently produce negative intervals. +/// +/// Rust: `biophysics::neuro::interspike_intervals` +#[pyfunction] +#[pyo3(name = "interspike_intervals", signature = (spikes))] +pub fn pyfn_interspike_intervals<'py>(py: Python<'py>, spikes: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::neuro::interspike_intervals(&spikes))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The coefficient of variation of the interspike intervals. +/// +/// One for a Poisson process, because an exponential distribution's +/// standard deviation equals its mean; near zero for a regular pacemaker; +/// and above one for a bursting cell, whose intervals come in two very +/// different sizes. It is a measure of *irregularity*, not of rate: it is +/// unchanged by running the clock faster. +/// +/// Errors: +/// Returns an error for fewer than three spikes, an unsorted train, or a +/// mean interval of zero. +/// +/// Rust: `biophysics::neuro::cv_isi` +#[pyfunction] +#[pyo3(name = "cv_isi", signature = (spikes))] +pub fn pyfn_cv_isi<'py>(py: Python<'py>, spikes: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::neuro::cv_isi(&spikes))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The Fano factor of a set of counts: variance over mean. +/// +/// One for a Poisson process. Unlike `cv_isi` this is measured over a +/// window, so the two can disagree: a train with regular intervals but a +/// drifting rate has a low CV and a high Fano factor, because the +/// irregularity is between windows rather than within them. +/// +/// Errors: +/// Returns an error for fewer than two counts or a mean of zero. +/// +/// Rust: `biophysics::neuro::fano_factor` +#[pyfunction] +#[pyo3(name = "fano_factor", signature = (counts))] +pub fn pyfn_fano_factor<'py>(py: Python<'py>, counts: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::neuro::fano_factor(&counts))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// A homogeneous Poisson spike train on `[0, t_end)`. +/// +/// Generated by accumulating exponential waiting times, which is exact -- +/// there is no time step and so no chance of two spikes in one bin. +/// +/// Errors: +/// Returns an error for a non-positive rate or run length, or an expected +/// count above ten million. +/// +/// Rust: `biophysics::neuro::poisson_spike_train` +#[pyfunction] +#[pyo3(name = "poisson_spike_train", signature = (rate, t_end, rng))] +pub fn pyfn_poisson_spike_train(rate: f64, t_end: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::neuro::poisson_spike_train(rate, t_end, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The peri-stimulus time histogram: the mean firing rate in each bin, +/// across trials. +/// +/// Dividing by the bin width and the trial count is what makes this a +/// rate rather than a count, and is what lets histograms with different +/// binnings be compared. The bin width is the whole choice in a PSTH: too +/// wide and a transient response is smeared into the background, too +/// narrow and every bin is zero or one. +/// +/// Errors: +/// Returns an error for no trials, a non-positive bin width or window, or +/// a spike time outside `[0, t_end)`. +/// +/// Rust: `biophysics::neuro::psth` +#[pyfunction] +#[pyo3(name = "psth", signature = (trains, bin, t_end))] +pub fn pyfn_psth<'py>(py: Python<'py>, trains: Vec>, bin: f64, t_end: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::neuro::psth(&trains, bin, t_end))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Every spike as a `(time, trial)` pair, sorted by time. +/// +/// The raster is the raw data a PSTH averages away, and the two answer +/// different questions: a response present on every trial and one present +/// on half the trials at twice the rate give the same histogram. +/// +/// Rust: `biophysics::neuro::raster_data` +#[pyfunction] +#[pyo3(name = "raster_data", signature = (trains))] +pub fn pyfn_raster_data<'py>(py: Python<'py>, trains: Vec>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::neuro::raster_data(&trains))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// The spike-triggered average: the mean stimulus in the `window` samples +/// before a spike. +/// +/// Returned oldest sample first, so the last entry is the stimulus at the +/// spike itself. Spikes too early for a full window are skipped, and the +/// count of those that contributed decides the divisor. +/// +/// This estimates the neuron's linear filter only if the stimulus is white: +/// any correlation in the stimulus appears in the average and will be +/// mistaken for structure in the neuron. The usual remedy is to whiten by +/// the stimulus autocorrelation, which is a different calculation from +/// this one. +/// +/// Errors: +/// Returns an error for an empty stimulus, a non-positive sampling step, a +/// zero window, a window longer than the stimulus, or no usable spike. +/// +/// Rust: `biophysics::neuro::spike_triggered_average` +#[pyfunction] +#[pyo3(name = "spike_triggered_average", signature = (stimulus, dt, spikes, window))] +pub fn pyfn_spike_triggered_average<'py>(py: Python<'py>, stimulus: Vec, dt: f64, spikes: Vec, window: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::neuro::spike_triggered_average(&stimulus, dt, &spikes, window))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Fits `r(theta) = amplitude * exp(kappa * cos(theta - preferred))` to a +/// set of angles and rates, returning `(preferred, kappa, amplitude)`. +/// +/// Taking logarithms turns the von Mises form into +/// `ln r = ln A + (kappa cos mu) cos theta + (kappa sin mu) sin theta`, +/// which is linear in three coefficients and so is solved exactly rather +/// than searched for. `preferred` comes back in `(-pi, pi]`. +/// +/// The price of the linearisation is that it fits the log rate, so it +/// weights a doubling at a low rate as heavily as a doubling at the peak. +/// With noiseless data that costs nothing and the fit is exact; with noisy +/// data it biases toward the flanks. +/// +/// Errors: +/// Returns an error for fewer than three points, mismatched lengths, a +/// non-positive rate, or angles that do not determine the fit -- all equal, +/// or spread over too little of the circle. +/// +/// Rust: `biophysics::neuro::tuning_curve_fit_von_mises` +#[pyfunction] +#[pyo3(name = "tuning_curve_fit_von_mises", signature = (angles, rates))] +pub fn pyfn_tuning_curve_fit_von_mises<'py>(py: Python<'py>, angles: Vec, rates: Vec) -> PyResult<(f64, f64, f64)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::neuro::tuning_curve_fit_von_mises(&angles, &rates))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// The conductance of an exponential synapse at time `t`, given the +/// presynaptic spike times. +/// +/// Each spike adds `g_max` instantaneously and it decays as +/// `exp(-(t - t_spike)/tau)`. Conductances sum, so a burst arriving within +/// a time constant produces more than one spike's worth -- which is what +/// makes a synapse a low-pass filter of its input rather than a repeater. +/// +/// Errors: +/// Returns an error for a non-positive `tau` or an unsorted spike train. +/// +/// Rust: `biophysics::neuro::synapse_exp` +#[pyfunction] +#[pyo3(name = "synapse_exp", signature = (g_max, tau, spikes, t))] +pub fn pyfn_synapse_exp<'py>(py: Python<'py>, g_max: f64, tau: f64, spikes: Vec, t: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::neuro::synapse_exp(g_max, tau, &spikes, t))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The conductance of an alpha synapse at time `t`. +/// +/// `g_max * x * exp(1 - x)` with `x = (t - t_spike)/tau`, which peaks at +/// exactly `g_max` one time constant after the spike. The rise is what +/// distinguishes it from `synapse_exp`: a real conductance cannot jump, +/// and the delay to peak matters when the question is whether two inputs +/// coincide. +/// +/// Errors: +/// Returns an error for a non-positive `tau` or an unsorted spike train. +/// +/// Rust: `biophysics::neuro::alpha_synapse` +#[pyfunction] +#[pyo3(name = "alpha_synapse", signature = (g_max, tau, spikes, t))] +pub fn pyfn_alpha_synapse<'py>(py: Python<'py>, g_max: f64, tau: f64, spikes: Vec, t: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::neuro::alpha_synapse(g_max, tau, &spikes, t))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The spike-timing-dependent plasticity window: the weight change for a +/// post-minus-pre interval of `delta`. +/// +/// Positive `delta` -- the postsynaptic spike came second -- potentiates by +/// `a_plus exp(-delta/tau_plus)`; negative depresses by +/// `-a_minus exp(delta/tau_minus)`. Exactly simultaneous spikes give zero, +/// which is the discontinuity at the origin the rule is known for: a +/// millisecond either way is the difference between strengthening and +/// weakening. +/// +/// Errors: +/// Returns an error for a non-positive time constant or a negative +/// amplitude. +/// +/// Rust: `biophysics::neuro::stdp_window` +#[pyfunction] +#[pyo3(name = "stdp_window", signature = (delta, a_plus, a_minus, tau_plus, tau_minus))] +pub fn pyfn_stdp_window(delta: f64, a_plus: f64, a_minus: f64, tau_plus: f64, tau_minus: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::neuro::stdp_window(delta, a_plus, a_minus, tau_plus, tau_minus)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The total weight change from every pre-post pair in two trains. +/// +/// This is the all-to-all rule: each presynaptic spike is paired with each +/// postsynaptic spike. It is the simplest interpretation and not the only +/// one -- nearest-neighbour pairing gives noticeably less potentiation at +/// high rates, because a burst's later spikes no longer each count against +/// every earlier one. +/// +/// Errors: +/// Returns an error for a bad window parameter, an unsorted train, or more +/// than ten million pairs. +/// +/// Rust: `biophysics::neuro::stdp_train` +#[pyfunction] +#[pyo3(name = "stdp_train", signature = (pre, post, a_plus, a_minus, tau_plus, tau_minus))] +pub fn pyfn_stdp_train<'py>(py: Python<'py>, pre: Vec, post: Vec, a_plus: f64, a_minus: f64, tau_plus: f64, tau_minus: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::neuro::stdp_train(&pre, &post, a_plus, a_minus, tau_plus, tau_minus))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Izhikevich's randomly connected network of excitatory and inhibitory +/// neurons, returning every spike as `(time in ms, neuron index)`. +/// +/// Excitatory neurons are regular spikers scattered toward chattering by a +/// squared random factor, inhibitory ones toward fast spiking, exactly as +/// in the published network; each neuron receives a random thalamic drive +/// each millisecond, with the excitatory population driven harder. All +/// weights are all-to-all with random excitatory strengths and stronger +/// fixed inhibitory ones. +/// +/// The behaviour worth looking for is that the population synchronises +/// into gamma-band rhythms without any oscillator being built in: the +/// rhythm is a property of the excitatory-inhibitory loop, not of the +/// cells. Inhibition being both stronger and faster than excitation is +/// what produces it. +/// +/// Errors: +/// Returns an error for no excitatory or no inhibitory neurons, more than +/// four thousand in total, or a non-positive run length. +/// +/// Rust: `biophysics::neuro::izhikevich_network` +#[pyfunction] +#[pyo3(name = "izhikevich_network", signature = (n_exc, n_inh, t_end, rng))] +pub fn pyfn_izhikevich_network(n_exc: usize, n_inh: usize, t_end: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::neuro::izhikevich_network(n_exc, n_inh, t_end, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// The Hebbian weight matrix storing a set of +-1 patterns. +/// +/// `w_ij = (1/n) sum_p x_i^p x_j^p` with a zero diagonal. The rule is +/// local and one-shot: each pattern is written by a single pass and never +/// revisited, which is why the network cannot unlearn and why capacity is +/// the limiting resource rather than training time. +/// +/// Errors: +/// Returns an error for no patterns, patterns of differing or zero length, +/// or an entry that is not exactly +1 or -1. +/// +/// Rust: `biophysics::neuro::hopfield_store` +#[pyfunction] +#[pyo3(name = "hopfield_store", signature = (patterns))] +pub fn pyfn_hopfield_store(patterns: Vec>) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::neuro::hopfield_store(&patterns)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) +} + +/// Recalls from a probe by sweeping the units in index order, stopping +/// early once a whole sweep changes nothing. `steps` counts sweeps. +/// +/// The updates are sequential rather than simultaneous, and the difference +/// is not cosmetic. Flipping one unit at a time against the current state +/// can only lower the energy `-1/2 x' W x` when the weights are symmetric +/// with a zero diagonal, so recall converges to a fixed point. Updating +/// every unit at once against the *old* state has no such guarantee: it +/// can raise the energy and settle into a two-cycle that oscillates +/// forever between two states, neither of them stored. What it converges *to* need not be a stored +/// pattern: mixtures of three stored patterns are also minima, and so are +/// the negatives of everything stored, since flipping every unit leaves +/// the energy unchanged. +/// +/// Errors: +/// Returns an error for a non-square matrix, a probe of the wrong length, +/// or a probe entry that is not exactly +1 or -1. +/// +/// Rust: `biophysics::neuro::hopfield_recall` +#[pyfunction] +#[pyo3(name = "hopfield_recall", signature = (w, probe, steps))] +pub fn pyfn_hopfield_recall<'py>(py: Python<'py>, w: crate::generated::types::PyMatrixArg, probe: Vec, steps: usize) -> PyResult> { + let w = w.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::neuro::hopfield_recall(&w, &probe, steps))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The energy of a state under a Hopfield weight matrix. +/// +/// Errors: +/// Returns an error for a non-square matrix or a state of the wrong length. +/// +/// Rust: `biophysics::neuro::hopfield_energy` +#[pyfunction] +#[pyo3(name = "hopfield_energy", signature = (w, state))] +pub fn pyfn_hopfield_energy<'py>(py: Python<'py>, w: crate::generated::types::PyMatrixArg, state: Vec) -> PyResult { + let w = w.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::neuro::hopfield_energy(&w, &state))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The fraction of stored patterns recalled exactly from themselves, over +/// `trials` random pattern sets of size `stored`. +/// +/// Recall from the pattern itself is the easiest possible test, so this +/// measures storage rather than error correction. It falls off sharply +/// near `0.138 n` patterns: below that the stored patterns are stable, and +/// above it the crosstalk between them overwhelms the signal and the +/// network forgets everything at once rather than degrading gracefully. +/// +/// Errors: +/// Returns an error for a network or trial count of zero, no patterns to +/// store, or a request above five hundred units. +/// +/// Rust: `biophysics::neuro::hopfield_capacity_check` +#[pyfunction] +#[pyo3(name = "hopfield_capacity_check", signature = (n, stored, trials, rng))] +pub fn pyfn_hopfield_capacity_check(n: usize, stored: usize, trials: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::neuro::hopfield_capacity_check(n, stored, trials, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The Wilson-Cowan equations for coupled excitatory and inhibitory +/// populations, returning `(t, E, I)`. +/// +/// `tau_e dE/dt = -E + S(c_ee E - c_ei I + p_e)` and the matching +/// equation for `I`, with `S` the logistic function. `E` and `I` are +/// fractions of each population active, so they stay in `[0, 1]`. +/// +/// This is a mean-field model: it describes what a population does on +/// average and says nothing about individual spikes or their timing. +/// Oscillations here are oscillations of the *rate*, which is a different +/// claim from the synchrony a spiking network shows, and the two need not +/// coincide. +/// +/// Errors: +/// Returns an error for a non-positive time constant or slope, initial +/// activity outside `[0, 1]`, or a run length or step size out of range. +/// +/// Rust: `biophysics::neuro::wilson_cowan` +#[pyfunction] +#[pyo3(name = "wilson_cowan", signature = (c_ee, c_ei, c_ie, c_ii, p_e, p_i, tau_e, tau_i, slope, threshold, e0, i0, t_end, dt))] +pub fn pyfn_wilson_cowan<'py>(py: Python<'py>, c_ee: f64, c_ei: f64, c_ie: f64, c_ii: f64, p_e: f64, p_i: f64, tau_e: f64, tau_i: f64, slope: f64, threshold: f64, e0: f64, i0: f64, t_end: f64, dt: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::neuro::wilson_cowan(c_ee, c_ei, c_ie, c_ii, p_e, p_i, tau_e, tau_i, slope, threshold, e0, i0, t_end, dt))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, __x.2)).collect::>()) +} + +/// The passive cable's length constant `sqrt(d R_m / (4 R_i))`. +/// +/// With `r_m` in ohm-cm^2, `r_i` in ohm-cm and the diameter in cm, the +/// answer is in cm. The square root is the reason thin processes are +/// electrically short: halving the diameter shortens the reach only by +/// `sqrt(2)`, but that is enough that a dendritic spine's neck is a +/// different electrical world from its parent branch. +/// +/// Errors: +/// Returns an error for a non-positive resistance or diameter. +/// +/// Rust: `biophysics::neuro::length_constant` +#[pyfunction] +#[pyo3(name = "length_constant", signature = (r_m, r_i, diameter))] +pub fn pyfn_length_constant(r_m: f64, r_i: f64, diameter: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::neuro::length_constant(r_m, r_i, diameter)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The steady-state voltage along a finite passive cable with current +/// injected at one end and the far end sealed. +/// +/// Returns `points` samples of `V(x)` over `[0, length]`, solved from the +/// discretised cable equation `lambda^2 V'' = V` rather than from the +/// closed form, so the boundary conditions are imposed rather than +/// assumed. The analytic answer for a sealed end is +/// `V(x) = V(0) cosh((L - x)/lambda) / cosh(L/lambda)`. +/// +/// A sealed end is not a neutral choice. Current that reaches it has +/// nowhere to go, so the voltage there is *higher* than an infinite cable +/// would give -- an end effect that grows as the cable shortens relative +/// to its length constant. +/// +/// Errors: +/// Returns an error for a non-positive length or length constant, fewer +/// than three points, or a singular system. +/// +/// Rust: `biophysics::neuro::cable_equation_1d` +#[pyfunction] +#[pyo3(name = "cable_equation_1d", signature = (length, lambda_, v_injected, points))] +pub fn pyfn_cable_equation_1d<'py>(py: Python<'py>, length: f64, lambda_: f64, v_injected: f64, points: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::neuro::cable_equation_1d(length, lambda_, v_injected, points))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Simulated reaction times from the drift-diffusion model, as +/// `(time, chose the positive bound)`. +/// +/// Evidence accumulates from zero with constant `drift` and Gaussian noise +/// until it reaches `+threshold` or `-threshold`. The model's appeal is +/// that one mechanism produces both the choice and its latency, and it +/// predicts the awkward fact that errors and correct responses have +/// nearly the same distribution of times when the starting point is +/// unbiased. +/// +/// Errors: +/// Returns an error for a non-positive threshold, noise or step, no +/// trials, or a run that exhausts the fifty-million-step budget shared +/// across all trials. +/// +/// Rust: `biophysics::neuro::reaction_time_ddm` +#[pyfunction] +#[pyo3(name = "reaction_time_ddm", signature = (drift, threshold, noise, dt, trials, rng))] +pub fn pyfn_reaction_time_ddm(drift: f64, threshold: f64, noise: f64, dt: f64, trials: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::neuro::reaction_time_ddm(drift, threshold, noise, dt, trials, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// The exact probability that unbiased evidence reaches the positive +/// bound: `1 / (1 + exp(-2 * drift * threshold / noise^2))`. +/// +/// This is the gambler's-ruin answer for Brownian motion with drift +/// between symmetric absorbing barriers, and it depends on the three +/// parameters only through `drift * threshold / noise^2`. Doubling the +/// drift and the noise variance together therefore changes the accuracy +/// not at all, only the time taken. +/// +/// Errors: +/// Returns an error for a non-positive threshold or noise. +/// +/// Rust: `biophysics::neuro::ddm_analytic_accuracy` +#[pyfunction] +#[pyo3(name = "ddm_analytic_accuracy", signature = (drift, threshold, noise))] +pub fn pyfn_ddm_analytic_accuracy(drift: f64, threshold: f64, noise: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::neuro::ddm_analytic_accuracy(drift, threshold, noise)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_spike_times, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hh_steady_state, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hodgkin_huxley, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hh_spike_times, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hh_spike_threshold_estimate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hh_fi_curve, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fitzhugh_nagumo_neuron, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_morris_lecar, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_izhikevich, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_izhikevich_presets, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_adex, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lif_neuron, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lif_fi_exact, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_interspike_intervals, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cv_isi, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fano_factor, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_poisson_spike_train, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_psth, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_raster_data, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spike_triggered_average, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tuning_curve_fit_von_mises, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_synapse_exp, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_alpha_synapse, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_stdp_window, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_stdp_train, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_izhikevich_network, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hopfield_store, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hopfield_recall, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hopfield_energy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hopfield_capacity_check, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wilson_cowan, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_length_constant, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cable_equation_1d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_reaction_time_ddm, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ddm_analytic_accuracy, m)?)?; + m.add("HH_C_M", rust_physics_engine::biophysics::neuro::HH_C_M)?; + m.add("HH_G_NA", rust_physics_engine::biophysics::neuro::HH_G_NA)?; + m.add("HH_G_K", rust_physics_engine::biophysics::neuro::HH_G_K)?; + m.add("HH_G_L", rust_physics_engine::biophysics::neuro::HH_G_L)?; + m.add("HH_E_NA", rust_physics_engine::biophysics::neuro::HH_E_NA)?; + m.add("HH_E_K", rust_physics_engine::biophysics::neuro::HH_E_K)?; + m.add("HH_E_L", rust_physics_engine::biophysics::neuro::HH_E_L)?; + m.add("HH_V_REST", rust_physics_engine::biophysics::neuro::HH_V_REST)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_biophysics__phylo.rs b/bindings/python/src/generated/m_biophysics__phylo.rs new file mode 100644 index 0000000..f4b195a --- /dev/null +++ b/bindings/python/src/generated/m_biophysics__phylo.rs @@ -0,0 +1,316 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// UPGMA: unweighted pair group method with arithmetic mean. +/// +/// Repeatedly joins the two closest clusters and places their common +/// ancestor at half their distance, so every leaf ends up the same distance +/// from the root. That ultrametricity is *assumed*, not measured: UPGMA +/// returns a clocklike tree whether or not the data are clocklike, and on +/// data where one lineage evolves faster it will place that lineage's +/// long branch too close to the root -- the classic long-branch artefact. +/// Use `neighbor_joining` unless a clock is justified. +/// +/// The distance between merged clusters is the mean over all pairs of +/// members, which is what makes the merge heights non-decreasing and the +/// result a valid ultrametric tree. +/// +/// Errors: +/// Returns an error for a non-square, asymmetric, negative or non-finite +/// matrix, a label count that disagrees with it, or fewer than two taxa. +/// +/// Rust: `biophysics::phylo::upgma` +#[pyfunction] +#[pyo3(name = "upgma", signature = (dist, labels))] +pub fn pyfn_upgma(dist: crate::generated::types::PyMatrixArg, labels: Vec) -> PyResult { + let dist = dist.0; + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::phylo::upgma(&dist, &labels)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyPhyloTree { inner: __v }) +} + +/// Saitou and Nei's neighbour joining. +/// +/// Joins the pair minimising `Q(i,j) = (n-2) d(i,j) - r_i - r_j`, where +/// `r_i` is the row sum, rather than the pair that is simply closest. The +/// correction is what makes the method consistent without a clock: two +/// taxa can be close together merely because both evolve slowly, and `Q` +/// discounts exactly that. Given an additive matrix the method recovers +/// the true tree exactly. +/// +/// The result is an **unrooted** tree returned in rooted form: the final +/// node has three children and is a placeholder, not an inferred ancestor. +/// Do not read `PhyloTree::height` or `PhyloTree::depth` off it as +/// times, and expect `PhyloTree::is_binary` to be false at that node. +/// +/// Non-additive data can imply a negative branch. Since a negative length +/// has no meaning as a number of substitutions, it is clamped to zero -- +/// the standard remedy, and a sign that the data do not fit a tree. +/// +/// Errors: +/// Returns an error for a malformed matrix (see `upgma`) or fewer than +/// three taxa. +/// +/// Rust: `biophysics::phylo::neighbor_joining` +#[pyfunction] +#[pyo3(name = "neighbor_joining", signature = (dist, labels))] +pub fn pyfn_neighbor_joining(dist: crate::generated::types::PyMatrixArg, labels: Vec) -> PyResult { + let dist = dist.0; + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::phylo::neighbor_joining(&dist, &labels)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyPhyloTree { inner: __v }) +} + +/// The matrix of Jukes-Cantor corrected pairwise distances. +/// +/// Sites where either sequence is not one of A, C, G, T are skipped for +/// that pair, so different pairs may rest on different numbers of sites. +/// +/// Errors: +/// Returns an error for fewer than two sequences, sequences of differing +/// or zero length, a pair with no comparable site, or a pair whose observed +/// difference has saturated at three quarters, where the correction gives +/// no finite answer. +/// +/// Rust: `biophysics::phylo::distance_matrix_jc69` +#[pyfunction] +#[pyo3(name = "distance_matrix_jc69", signature = (seqs))] +pub fn pyfn_distance_matrix_jc69(seqs: Vec>) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::phylo::distance_matrix_jc69(&seqs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) +} + +/// Fitch's parsimony score: the fewest character changes the tree needs. +/// +/// `characters` holds one state per leaf, in the order `PhyloTree::leaves` +/// returns them. Working from the tips down, each node takes the +/// intersection of its children's state sets, or -- when that is empty -- +/// their union at the cost of one change. +/// +/// The score counts changes, not their positions: a site can be explained +/// by several equally parsimonious assignments, and parsimony picks none of +/// them. It is also biased when rates vary a lot between branches, where it +/// can be positively misled (long-branch attraction) into preferring the +/// wrong topology however much data you add. +/// +/// Errors: +/// Returns an error if the character count differs from the leaf count or +/// more than 32 distinct states appear. +/// +/// Rust: `biophysics::phylo::parsimony_fitch` +#[pyfunction] +#[pyo3(name = "parsimony_fitch", signature = (tree, characters))] +pub fn pyfn_parsimony_fitch<'py>(py: Python<'py>, tree: crate::generated::types::PyPhyloTree, characters: Vec) -> PyResult { + let tree = tree.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::phylo::parsimony_fitch(&tree, &characters))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The log-likelihood of an alignment on a tree under Jukes-Cantor, by +/// Felsenstein's pruning algorithm. +/// +/// `seqs` holds one aligned sequence per leaf, in the order +/// `PhyloTree::leaves` returns them, and branch lengths are expected +/// substitutions per site. Under JC69 a branch of length `t` leaves a site +/// unchanged with probability `1/4 + 3/4 e^(-4t/3)` and sends it to each +/// other base with `1/4 - 1/4 e^(-4t/3)`; pruning sums over every ancestral +/// assignment in one pass up the tree rather than enumerating `4^nodes` of +/// them. +/// +/// The result is a *log* likelihood because the likelihood itself +/// underflows: a thousand sites each contributing a factor near `0.25` +/// gives a number around `1e-600`, which is not representable. +/// +/// Sites where a leaf carries an ambiguous or missing base contribute a +/// factor of one from that leaf -- the site still informs the others. +/// +/// Errors: +/// Returns an error if the sequence count differs from the leaf count, the +/// sequences are empty or of differing length. +/// +/// Rust: `biophysics::phylo::likelihood_jc69` +#[pyfunction] +#[pyo3(name = "likelihood_jc69", signature = (tree, seqs))] +pub fn pyfn_likelihood_jc69<'py>(py: Python<'py>, tree: crate::generated::types::PyPhyloTree, seqs: Vec>) -> PyResult { + let tree = tree.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::phylo::likelihood_jc69(&tree, &seqs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Bootstrap support for the splits of a distance tree. +/// +/// Builds a reference tree from the whole alignment, then resamples the +/// *columns* with replacement `replicates` times, rebuilds, and reports the +/// fraction of replicates recovering each branch of the reference. The +/// returned vector is aligned with `reference.bipartitions()`. +/// +/// Branches are compared as unrooted bipartitions rather than rooted +/// clades. Neighbour joining's root is an artefact, so two replicates that +/// found the same tree can report a clade and its complement; treating +/// those as different answers would understate support for no reason. +/// +/// Columns are the sampling unit because sites are what the model treats as +/// independent draws; resampling taxa instead would answer a different +/// question. High support means the signal is spread across the alignment +/// rather than resting on a handful of sites -- it is not a probability +/// that the split is true, and a consistently wrong method will support a +/// wrong split at 100%. +/// +/// Replicates whose resampled alignment yields no usable distance matrix +/// (a saturated pair, say) are skipped, and the divisor counts only those +/// that succeeded. +/// +/// Errors: +/// Returns an error for fewer than three sequences, unaligned or empty +/// sequences, a label count that disagrees, zero replicates, or a whole +/// alignment that yields no tree. +/// +/// Rust: `biophysics::phylo::bootstrap_trees` +#[pyfunction] +#[pyo3(name = "bootstrap_trees", signature = (seqs, labels, replicates, method, rng))] +pub fn pyfn_bootstrap_trees(seqs: Vec>, labels: Vec, replicates: usize, method: crate::generated::types::PyDistanceMethod, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(crate::generated::types::PyPhyloTree, Vec)> { + let method = method.to_rust(); + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::phylo::bootstrap_trees(&seqs, &labels, replicates, method, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((crate::generated::types::PyPhyloTree { inner: __v.0 }, __v.1)) +} + +/// A birth-death tree, pruned to the lineages that survive. +/// +/// Runs the forward process -- each lineage speciating at rate `lambda` and +/// dying at rate `mu` -- until `n_leaves` lineages are alive at once, then +/// removes the extinct ones and suppresses the resulting single-child +/// nodes. What comes back is the *reconstructed* tree, the only one a +/// phylogeny of living species could ever show. +/// +/// That pruning is why extinction leaves a signature rather than +/// disappearing. Near the present, lineages have not yet had time to die, +/// so the reconstructed tree grows at the full rate `lambda` there while +/// deeper down it grows at `lambda - mu`. The surviving tree therefore +/// looks as though speciation accelerated toward the present -- the "pull +/// of the present", which shows up as a positive `gamma_statistic` and an +/// upturn in the `lineage_through_time` curve. +/// +/// The tree is stopped at the first event *after* the target count is +/// reached, so the interval during which `n_leaves` lineages coexist has a +/// length rather than collapsing to zero. +/// +/// The tree is ultrametric by construction -- every tip sits at the same +/// stopping time. +/// +/// Errors: +/// Returns an error for a non-positive `lambda`, a negative or non-finite +/// `mu`, `mu >= lambda`, fewer than three leaves, or if every attempt died +/// out before reaching the target. +/// +/// Rust: `biophysics::phylo::birth_death_tree` +#[pyfunction] +#[pyo3(name = "birth_death_tree", signature = (lambda_, mu, n_leaves, rng))] +pub fn pyfn_birth_death_tree(lambda_: f64, mu: f64, n_leaves: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::phylo::birth_death_tree(lambda_, mu, n_leaves, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyPhyloTree { inner: __v }) +} + +/// Pybus and Harvey's gamma statistic. +/// +/// Standard normal under a constant-rate pure-birth process, so it is a +/// direct test of that null: negative gamma means the internal branching +/// events sit closer to the root than a constant rate predicts -- an early +/// burst, or a diversification rate that slowed -- and positive gamma means +/// they crowd toward the present. +/// +/// Extinction pushes gamma *positive* on a reconstructed tree even at a +/// constant rate: recent lineages have not yet had time to die, so nodes +/// crowd toward the present. A positive value is therefore not by itself +/// evidence of an accelerating rate. The bias runs the other way from the +/// slowdown test, which is why a significantly negative gamma is taken as +/// conservative evidence of a slowdown. +/// +/// The statistic reads times off the tree, so it is meaningful only for an +/// ultrametric one; a tree with unequal tip depths is rejected rather than +/// silently misread. +/// +/// Errors: +/// Returns an error for fewer than three tips, a tree that is not +/// ultrametric to `1e-8` relative, or one of zero height. +/// +/// Rust: `biophysics::phylo::gamma_statistic` +#[pyfunction] +#[pyo3(name = "gamma_statistic", signature = (tree))] +pub fn pyfn_gamma_statistic(tree: crate::generated::types::PyPhyloTree) -> PyResult { + let tree = tree.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::phylo::gamma_statistic(&tree)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The lineage-through-time curve: `(time, lineage count)` at the root, at +/// every branching, and at the present. +/// +/// Time is measured from the root. Plotted with a log count axis, a +/// constant-rate pure-birth tree gives a straight line of slope `lambda`, +/// which is what makes the curve's departures readable: a bend downward +/// toward the tips is a slowdown, and the upturn near the present on a tree +/// with extinction is the pull of the present rather than a real burst. +/// +/// For a tree whose tips are not all at the same depth, the final point +/// uses the deepest tip and the count there is the leaf total. +/// +/// Errors: +/// Returns an error for a tree with fewer than two tips. +/// +/// Rust: `biophysics::phylo::lineage_through_time` +#[pyfunction] +#[pyo3(name = "lineage_through_time", signature = (tree))] +pub fn pyfn_lineage_through_time<'py>(py: Python<'py>, tree: crate::generated::types::PyPhyloTree) -> PyResult> { + let tree = tree.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::phylo::lineage_through_time(&tree))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_upgma, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_neighbor_joining, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_distance_matrix_jc69, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_parsimony_fitch, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_likelihood_jc69, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bootstrap_trees, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_birth_death_tree, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gamma_statistic, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lineage_through_time, m)?)?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_biophysics__population.rs b/bindings/python/src/generated/m_biophysics__population.rs new file mode 100644 index 0000000..9f6ff93 --- /dev/null +++ b/bindings/python/src/generated/m_biophysics__population.rs @@ -0,0 +1,849 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Logistic growth in closed form: `N = K N0 e^(rt) / (K + N0 (e^(rt) - 1))`. +/// +/// Evaluated from the analytic solution rather than integrated, so it is +/// exact at every time and costs nothing at large `t`. +/// +/// Errors: +/// Returns an error for a non-positive carrying capacity or a negative +/// initial population. +/// +/// Rust: `biophysics::population::logistic_growth` +#[pyfunction] +#[pyo3(name = "logistic_growth", signature = (r, k, n0, t))] +pub fn pyfn_logistic_growth(r: f64, k: f64, n0: f64, t: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::population::logistic_growth(r, k, n0, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Gompertz growth: `N = K exp(ln(N0/K) e^(-rt))`. +/// +/// Differs from the logistic in where it turns: the inflection is at `K/e`, +/// about 37 per cent of capacity, rather than at half. That asymmetry is why +/// it fits tumour and organ growth better than the logistic does -- those +/// slow down earlier than a symmetric curve allows. +/// +/// Errors: +/// Returns an error for a non-positive capacity or initial population. +/// +/// Rust: `biophysics::population::gompertz` +#[pyfunction] +#[pyo3(name = "gompertz", signature = (r, k, n0, t))] +pub fn pyfn_gompertz(r: f64, k: f64, n0: f64, t: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::population::gompertz(r, k, n0, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Richards growth, which contains both: `nu = 1` is logistic and the limit +/// `nu -> 0` is Gompertz. +/// +/// `N = K (1 + q e^(-r t))^(-1/nu)` with `q = (K/N0)^nu - 1`, solving +/// `dN/dt = (r/nu) N (1 - (N/K)^nu)`. +/// +/// The `r/nu` in that equation is not decoration, and writing the solution +/// with `e^(-r nu t)` instead -- which solves the tidier-looking +/// `dN/dt = r N (1 - (N/K)^nu)` -- destroys the Gompertz limit. Under that +/// convention the effective rate is `r nu`, so letting `nu -> 0` at fixed +/// `r` freezes the curve at its initial value rather than approaching +/// anything. Here `r` is the intrinsic rate in both limits, which is what +/// makes the family a genuine interpolation rather than two special cases +/// with a gap between them. +/// +/// Errors: +/// Returns an error for a non-positive capacity, initial population or +/// shape. +/// +/// Rust: `biophysics::population::richards` +#[pyfunction] +#[pyo3(name = "richards", signature = (r, k, nu, n0, t))] +pub fn pyfn_richards(r: f64, k: f64, nu: f64, n0: f64, t: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::population::richards(r, k, nu, n0, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Growth with a strong Allee effect: +/// `dN/dt = r N (N/A - 1) (1 - N/K)`. +/// +/// Below the threshold `A` the growth rate is *negative* and the population +/// collapses however far it is from the capacity. That is the qualitative +/// difference from logistic growth, where any positive population recovers: +/// here there is a point of no return, which is why a species can be +/// committed to extinction while individuals are still alive. +/// +/// Errors: +/// Returns an error for a threshold not below the capacity, a negative +/// initial population, or a non-positive end time. +/// +/// Rust: `biophysics::population::allee_effect_ode` +#[pyfunction] +#[pyo3(name = "allee_effect_ode", signature = (r, a, k, n0, t_end))] +pub fn pyfn_allee_effect_ode<'py>(py: Python<'py>, r: f64, a: f64, k: f64, n0: f64, t_end: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::population::allee_effect_ode(r, a, k, n0, t_end))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// The Lotka-Volterra predator-prey system, with its conserved quantity. +/// +/// `dx/dt = alpha x - beta x y`, `dy/dt = delta x y - gamma y`. Returns +/// `(time, prey, predator)` together with +/// `V = delta x - gamma ln x + beta y - alpha ln y`, which is constant along +/// every orbit. +/// +/// That constant is the reason the orbits are closed curves rather than a +/// limit cycle: the system is conservative, so its amplitude is set by where +/// it started and never forgets. A model that damped onto a single cycle +/// would be a different system, and returning the invariant lets a caller +/// see the integrator's drift rather than take it on trust. +/// +/// Errors: +/// Returns an error for non-positive rates or a non-positive initial +/// population, for which the invariant is undefined. +/// +/// Rust: `biophysics::population::lotka_volterra` +#[pyfunction] +#[pyo3(name = "lotka_volterra", signature = (alpha, beta, delta, gamma, x0, y0, t_end))] +pub fn pyfn_lotka_volterra(alpha: f64, beta: f64, delta: f64, gamma: f64, x0: f64, y0: f64, t_end: f64) -> PyResult<(Vec<(f64, f64, f64)>, Vec)> { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::population::lotka_volterra(alpha, beta, delta, gamma, x0, y0, t_end)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0.into_iter().map(|__x| (__x.0, __x.1, __x.2)).collect::>(), __v.1)) +} + +/// The Rosenzweig-MacArthur predator-prey model: logistic prey with a +/// saturating (Holling type II) predator response. +/// +/// `dx/dt = r x (1 - x/K) - a x y / (1 + a h x)`, +/// `dy/dt = e a x y / (1 + a h x) - m y`. +/// +/// The saturating response is what produces the *paradox of enrichment*: +/// raising the prey's carrying capacity destabilises the coexistence +/// equilibrium into a limit cycle of growing amplitude, so enriching the +/// system makes extinction more likely rather than less. The plain +/// Lotka-Volterra model, whose response is linear, cannot show this. +/// +/// Errors: +/// Returns an error for non-positive parameters or a non-positive initial +/// population. +/// +/// Rust: `biophysics::population::rosenzweig_macarthur` +#[pyfunction] +#[pyo3(name = "rosenzweig_macarthur", signature = (r, k, attack, handling, efficiency, mortality, x0, y0, t_end))] +pub fn pyfn_rosenzweig_macarthur<'py>(py: Python<'py>, r: f64, k: f64, attack: f64, handling: f64, efficiency: f64, mortality: f64, x0: f64, y0: f64, t_end: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::population::rosenzweig_macarthur(r, k, attack, handling, efficiency, mortality, x0, y0, t_end))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, __x.2)).collect::>()) +} + +/// The prey density at which the Rosenzweig-MacArthur coexistence +/// equilibrium loses stability, `K = (1 + a h x*) / (a h - ...)`, expressed +/// as the critical carrying capacity. +/// +/// The equilibrium prey density is `x* = m / (a (e - m h))`, independent of +/// `K`, and the equilibrium is stable while `K < x* + 1/(a h)` and unstable +/// above -- the Hopf bifurcation of the paradox of enrichment. +/// +/// Errors: +/// Returns an error for parameters that admit no coexistence equilibrium: +/// the predator must gain more from a prey item than it spends handling it. +/// +/// Rust: `biophysics::population::enrichment_critical_capacity` +#[pyfunction] +#[pyo3(name = "enrichment_critical_capacity", signature = (attack, handling, efficiency, mortality))] +pub fn pyfn_enrichment_critical_capacity(attack: f64, handling: f64, efficiency: f64, mortality: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::population::enrichment_critical_capacity(attack, handling, efficiency, mortality)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The outcome of two-species competition, from the competition +/// coefficients and capacities alone. +/// +/// Coexistence requires each species to limit *itself* more than it limits +/// the other -- `alpha12 < K1/K2` and `alpha21 < K2/K1`. If both +/// inequalities reverse, both exclusion equilibria are stable and the winner +/// is decided by the starting densities rather than by the parameters. This +/// is the content of the competitive exclusion principle, and it is a +/// statement about niche overlap rather than about which species is +/// "stronger". +/// +/// Errors: +/// Returns an error for non-positive capacities or negative coefficients. +/// +/// Rust: `biophysics::population::coexistence_condition` +#[pyfunction] +#[pyo3(name = "coexistence_condition", signature = (k1, k2, alpha12, alpha21))] +pub fn pyfn_coexistence_condition(k1: f64, k2: f64, alpha12: f64, alpha21: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::population::coexistence_condition(k1, k2, alpha12, alpha21)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyCompetition::from_rust(&__v)) +} + +/// Two-species Lotka-Volterra competition, integrated. +/// +/// Errors: +/// Returns an error for non-positive capacities or rates, or negative +/// initial densities. +/// +/// Rust: `biophysics::population::competition_lv` +#[pyfunction] +#[pyo3(name = "competition_lv", signature = (r1, r2, k1, k2, alpha12, alpha21, n1, n2, t_end))] +pub fn pyfn_competition_lv<'py>(py: Python<'py>, r1: f64, r2: f64, k1: f64, k2: f64, alpha12: f64, alpha21: f64, n1: f64, n2: f64, t_end: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::population::competition_lv(r1, r2, k1, k2, alpha12, alpha21, n1, n2, t_end))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, __x.2)).collect::>()) +} + +/// The Levins metapopulation model: `dp/dt = c p (1 - p) - e p`. +/// +/// The equilibrium occupancy is `1 - e/c`, and the population persists only +/// while colonisation outpaces extinction. Note what it says about habitat +/// loss: destroying a fraction `D` of patches replaces the equilibrium with +/// `1 - D - e/c`, so a metapopulation goes extinct while a fraction `e/c` of +/// its habitat still remains -- the extinction debt. +/// +/// Errors: +/// Returns an error for negative rates or an occupancy outside zero to one. +/// +/// Rust: `biophysics::population::metapopulation_levins` +#[pyfunction] +#[pyo3(name = "metapopulation_levins", signature = (c, e, p0, t_end))] +pub fn pyfn_metapopulation_levins<'py>(py: Python<'py>, c: f64, e: f64, p0: f64, t_end: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::population::metapopulation_levins(c, e, p0, t_end))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// The Leslie projection matrix from age-specific fecundity and survival. +/// +/// `fecundity[i]` is the expected offspring of an individual in class `i` +/// over one time step, and `survival[i]` the probability of surviving from +/// class `i` to `i + 1`, so `survival` is one shorter than `fecundity`. +/// +/// Errors: +/// Returns an error for empty input, a mismatched length, a negative +/// fecundity, or a survival outside zero to one. +/// +/// Rust: `biophysics::population::leslie_matrix` +#[pyfunction] +#[pyo3(name = "leslie_matrix", signature = (fecundity, survival))] +pub fn pyfn_leslie_matrix(fecundity: Vec, survival: Vec) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::population::leslie_matrix(&fecundity, &survival)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) +} + +/// The asymptotic growth rate and stable age distribution of a Leslie +/// matrix, by power iteration. +/// +/// Returns `(lambda, distribution)` with the distribution normalised to sum +/// to one. Perron-Frobenius guarantees the dominant eigenvalue of a +/// primitive non-negative matrix is real, positive and simple, which is what +/// makes power iteration the right method here rather than a general +/// eigensolver. +/// +/// The strong ergodic theorem is the substance: *whatever* age distribution +/// a population starts with, it converges to this one and then grows by +/// `lambda` per step. The transient depends on the start; the asymptote does +/// not. +/// +/// Errors: +/// Returns an error for a non-square matrix or one whose iteration does not +/// converge -- which happens when the matrix is imprimitive, for instance a +/// species that reproduces at exactly one age, whose age classes then cycle +/// forever instead of settling. +/// +/// Rust: `biophysics::population::leslie_growth_rate` +#[pyfunction] +#[pyo3(name = "leslie_growth_rate", signature = (l))] +pub fn pyfn_leslie_growth_rate<'py>(py: Python<'py>, l: crate::generated::types::PyMatrixArg) -> PyResult<(f64, Vec)> { + let l = l.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::population::leslie_growth_rate(&l))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// The stable age distribution alone. +/// +/// Errors: +/// Returns an error on the same conditions as `leslie_growth_rate`. +/// +/// Rust: `biophysics::population::stable_age_distribution` +#[pyfunction] +#[pyo3(name = "stable_age_distribution", signature = (l))] +pub fn pyfn_stable_age_distribution<'py>(py: Python<'py>, l: crate::generated::types::PyMatrixArg) -> PyResult> { + let l = l.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::population::stable_age_distribution(&l))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Solves the Euler-Lotka equation `sum l_x m_x r^(-x) = 1` for the growth +/// rate `r` per time step. +/// +/// `lx[i]` is survivorship to age `i + 1` and `mx[i]` the fecundity there, +/// so the first entry describes age one. The left side is strictly +/// decreasing in `r`, so bisection cannot fail; it is the same growth rate +/// `leslie_growth_rate` finds, reached from the life table rather than +/// from the matrix. +/// +/// Errors: +/// Returns an error for mismatched lengths, a survivorship outside zero to +/// one, or a population with no reproduction at all. +/// +/// Rust: `biophysics::population::euler_lotka_solve` +#[pyfunction] +#[pyo3(name = "euler_lotka_solve", signature = (lx, mx))] +pub fn pyfn_euler_lotka_solve<'py>(py: Python<'py>, lx: Vec, mx: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::population::euler_lotka_solve(&lx, &mx))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The Ricker map `N -> N exp(r (1 - N/K))`, iterated. +/// +/// Overcompensating density dependence: a population far above capacity +/// crashes below it rather than settling, and as `r` grows the fixed point +/// period-doubles into chaos. That a deterministic single-species model with +/// no environmental variation produces apparently random fluctuations is the +/// point -- population data need not be noisy to look noisy. +/// +/// Errors: +/// Returns an error for a non-positive capacity or negative start. +/// +/// Rust: `biophysics::population::ricker_map` +#[pyfunction] +#[pyo3(name = "ricker_map", signature = (r, k, n0, steps))] +pub fn pyfn_ricker_map<'py>(py: Python<'py>, r: f64, k: f64, n0: f64, steps: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::population::ricker_map(r, k, n0, steps))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The Beverton-Holt map `N -> R N / (1 + (R - 1) N / K)`, iterated. +/// +/// Compensating rather than overcompensating: however far above capacity the +/// population starts it approaches `K` monotonically and never overshoots, +/// so unlike Ricker it has no route to chaos at any `R`. The two models +/// differ in nothing but the shape of the density dependence, and that +/// single difference is the whole distinction between a stable fishery model +/// and a chaotic one. +/// +/// It also has a closed-form solution, which is what the tests check against. +/// +/// Errors: +/// Returns an error for a growth ratio at or below one, a non-positive +/// capacity, or a negative start. +/// +/// Rust: `biophysics::population::beverton_holt` +#[pyfunction] +#[pyo3(name = "beverton_holt", signature = (ratio, k, n0, steps))] +pub fn pyfn_beverton_holt<'py>(py: Python<'py>, ratio: f64, k: f64, n0: f64, steps: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::population::beverton_holt(ratio, k, n0, steps))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The attractor of the Ricker map at each of a range of growth rates: the +/// bifurcation diagram. +/// +/// Returns `(r, attractor points)` per rate, with the transient discarded +/// and the remaining points deduplicated so a period-`p` cycle reports `p` +/// values. +/// +/// Errors: +/// Returns an error for an empty or descending range, or bad map parameters. +/// +/// Rust: `biophysics::population::bifurcation_ricker` +#[pyfunction] +#[pyo3(name = "bifurcation_ricker", signature = (r_lo, r_hi, samples, transient, keep))] +pub fn pyfn_bifurcation_ricker<'py>(py: Python<'py>, r_lo: f64, r_hi: f64, samples: usize, transient: usize, keep: usize) -> PyResult)>> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::population::bifurcation_ricker(r_lo, r_hi, samples, transient, keep))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// A Wright-Fisher allele-frequency trajectory: each generation resamples +/// `2n` gene copies binomially from the previous frequency. +/// +/// The expected frequency never changes -- drift is a martingale -- and yet +/// every trajectory eventually fixes at zero or one. That is the whole point +/// of the model, and the reason no deterministic account of it is possible: +/// the mean is constant while the outcome is certain to be extreme. +/// +/// Errors: +/// Returns an error for no individuals or a frequency outside zero to one. +/// +/// Rust: `biophysics::population::wright_fisher` +#[pyfunction] +#[pyo3(name = "wright_fisher", signature = (n, p0, generations, rng))] +pub fn pyfn_wright_fisher(n: u64, p0: f64, generations: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::population::wright_fisher(n, p0, generations, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// A Moran process: one birth and one death per step, with the mutant type +/// having relative fitness `r`. +/// +/// Returns `(fixed, steps)` -- whether the mutant fixed rather than being +/// lost, and how many steps it took. Unlike Wright-Fisher the population +/// overlaps generations, and the fixation probability has an exact closed +/// form; see `fixation_probability_moran`. +/// +/// Errors: +/// Returns an error for an empty population, a starting count above it, or a +/// non-positive fitness. +/// +/// Rust: `biophysics::population::moran_process` +#[pyfunction] +#[pyo3(name = "moran_process", signature = (n, i0, fitness, rng))] +pub fn pyfn_moran_process(n: u64, i0: u64, fitness: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(bool, u64)> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::population::moran_process(n, i0, fitness, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// The exact fixation probability of `i` mutants of relative fitness `r` in +/// a Moran population of `n`: `(1 - r^-i) / (1 - r^-n)`. +/// +/// At `r = 1` it degenerates to `i/n` -- a neutral mutant fixes with +/// probability equal to its initial frequency, which is the cleanest +/// statement of what drift alone does. A single advantageous mutant with +/// `r = 1.01` fixes with probability about `1/100` rather than the certainty +/// a deterministic model would predict: even a beneficial mutation is +/// usually lost. +/// +/// Errors: +/// Returns an error for an empty population, a count above it, or a +/// non-positive fitness. +/// +/// Rust: `biophysics::population::fixation_probability_moran` +#[pyfunction] +#[pyo3(name = "fixation_probability_moran", signature = (n, i, r))] +pub fn pyfn_fixation_probability_moran(n: u64, i: u64, r: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::population::fixation_probability_moran(n, i, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The expected heterozygosity after `t` generations of drift: +/// `H_t = H_0 (1 - 1/(2N))^t`. +/// +/// The `2N` rather than `N` is the diploid gene copy count, and getting it +/// wrong halves the predicted rate of decay. Variation is lost at a rate set +/// by the population size alone -- no selection is involved -- which is why +/// small populations lose diversity even when nothing is wrong with them. +/// +/// Errors: +/// Returns an error for an empty population or a heterozygosity outside zero +/// to one. +/// +/// Rust: `biophysics::population::genetic_drift_heterozygosity` +#[pyfunction] +#[pyo3(name = "genetic_drift_heterozygosity", signature = (n, h0, t))] +pub fn pyfn_genetic_drift_heterozygosity(n: u64, h0: f64, t: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::population::genetic_drift_heterozygosity(n, h0, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Hardy-Weinberg genotype frequencies `(p^2, 2pq, q^2)`. +/// +/// Errors: +/// Returns an error for an allele frequency outside zero to one. +/// +/// Rust: `biophysics::population::hardy_weinberg` +#[pyfunction] +#[pyo3(name = "hardy_weinberg", signature = (p))] +pub fn pyfn_hardy_weinberg(p: f64) -> PyResult<(f64, f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::population::hardy_weinberg(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// A chi-squared test of observed genotype counts against Hardy-Weinberg +/// proportions, with the allele frequency estimated from the same data. +/// +/// One degree of freedom, not two: estimating `p` from the counts costs one, +/// which is why the standard `k - 1` rule does not apply here. Reported +/// through `chi_squared_gof`, whose degrees of freedom are corrected +/// afterwards. +/// +/// Errors: +/// Returns an error for a negative count or an empty sample. +/// +/// Rust: `biophysics::population::hw_chi_square_test` +#[pyfunction] +#[pyo3(name = "hw_chi_square_test", signature = (observed))] +pub fn pyfn_hw_chi_square_test(observed: Vec) -> PyResult { + let observed = <[f64; 3]>::try_from(observed).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 3 values, got {}", __v.len())))?; + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::population::hw_chi_square_test(observed)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyTestResult { inner: __v }) +} + +/// One generation at a time of selection at a single diploid locus, with +/// genotype fitnesses `[w_AA, w_Aa, w_aa]`. +/// +/// Returns the allele frequency each generation. Which allele wins is not +/// decided by fitness alone: with heterozygote advantage neither fixes and +/// the population settles at a polymorphic equilibrium, while with +/// heterozygote *disadvantage* both fixations are stable and the outcome +/// depends on where it started. Directional selection is only one of three +/// possibilities. +/// +/// Errors: +/// Returns an error for a frequency outside zero to one, a negative fitness, +/// or a population with no viable genotype. +/// +/// Rust: `biophysics::population::selection_one_locus` +#[pyfunction] +#[pyo3(name = "selection_one_locus", signature = (p0, w, generations))] +pub fn pyfn_selection_one_locus<'py>(py: Python<'py>, p0: f64, w: Vec, generations: usize) -> PyResult> { + let w = <[f64; 3]>::try_from(w).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 3 values, got {}", __v.len())))?; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::population::selection_one_locus(p0, w, generations))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The polymorphic equilibrium of a locus with heterozygote advantage: +/// `p* = (w_Aa - w_aa) / (2 w_Aa - w_AA - w_aa)`. +/// +/// Errors: +/// Returns an error unless the heterozygote is strictly the fittest, in +/// which case there is no interior equilibrium to report. +/// +/// Rust: `biophysics::population::balanced_polymorphism` +#[pyfunction] +#[pyo3(name = "balanced_polymorphism", signature = (w))] +pub fn pyfn_balanced_polymorphism<'py>(py: Python<'py>, w: Vec) -> PyResult { + let w = <[f64; 3]>::try_from(w).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 3 values, got {}", __v.len())))?; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::population::balanced_polymorphism(w))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The equilibrium frequency of a deleterious allele maintained by +/// mutation. +/// +/// For a fully recessive allele the balance is `sqrt(mu/s)`; with any +/// dominance `h > 0` it is `mu/(h s)` instead. The difference is large: at +/// `mu = 1e-6` and `s = 0.1` a recessive allele sits at 0.32 per cent while +/// one with `h = 0.1` sits at 0.01 per cent, some thirty times rarer. +/// Selection acts on heterozygotes far more often than on the rare +/// homozygote, so even slight dominance dominates the balance. +/// +/// Errors: +/// Returns an error for a non-positive selection coefficient, a negative +/// mutation rate, or a dominance outside zero to one. +/// +/// Rust: `biophysics::population::mutation_selection_balance` +#[pyfunction] +#[pyo3(name = "mutation_selection_balance", signature = (mu, s, h))] +pub fn pyfn_mutation_selection_balance(mu: f64, s: f64, h: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::population::mutation_selection_balance(mu, s, h)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Hamilton's rule: an altruistic act spreads when `r b > c`. +/// +/// Errors: +/// Returns an error for a relatedness outside zero to one. +/// +/// Rust: `biophysics::population::kin_selection_hamilton` +#[pyfunction] +#[pyo3(name = "kin_selection_hamilton", signature = (r, b, c))] +pub fn pyfn_kin_selection_hamilton(r: f64, b: f64, c: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::population::kin_selection_hamilton(r, b, c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The Price equation, decomposing the change in a mean trait into +/// selection and transmission. +/// +/// Returns `(selection, transmission)` with +/// `selection = Cov(w, z) / w_bar` and +/// `transmission = E[w dz] / w_bar`, whose sum is exactly the change in the +/// mean trait. This is an *identity*, not a model -- it assumes nothing +/// about inheritance or fitness and holds for any population whatever, which +/// is what makes it useful for deciding whether an observed change was +/// selection at all. +/// +/// Errors: +/// Returns an error for mismatched lengths, an empty population, a negative +/// fitness, or a mean fitness of zero. +/// +/// Rust: `biophysics::population::price_equation_decompose` +#[pyfunction] +#[pyo3(name = "price_equation_decompose", signature = (trait_values, fitness, offspring_trait))] +pub fn pyfn_price_equation_decompose<'py>(py: Python<'py>, trait_values: Vec, fitness: Vec, offspring_trait: Vec) -> PyResult<(f64, f64)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::population::price_equation_decompose(&trait_values, &fitness, &offspring_trait))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// The expected time, in generations, during which a sample of `n` lineages +/// has exactly `k` ancestors: `E[T_k] = 4N / (k (k - 1))`. +/// +/// The `4N` is the diploid gene-copy convention: there are `2N` copies, and +/// the coalescence rate for `k` lineages is `C(k,2) / (2N)`. The +/// distribution's shape is the striking part -- `T_2` alone is `2N` +/// generations, longer than every other interval put together, so the +/// genealogy of a sample is dominated by its deepest branch and estimates of +/// ancient history rest on very little independent information. +/// +/// Errors: +/// Returns an error for fewer than two lineages or an empty population. +/// +/// Rust: `biophysics::population::coalescent_time_expected` +#[pyfunction] +#[pyo3(name = "coalescent_time_expected", signature = (n, k))] +pub fn pyfn_coalescent_time_expected(n: u64, k: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::population::coalescent_time_expected(n, k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The expected time to the most recent common ancestor of a sample of `k`: +/// `4N (1 - 1/k)` generations. +/// +/// Bounded above by `4N` however large the sample: adding sequences barely +/// deepens the tree, because new lineages coalesce almost immediately with +/// the ones already there. Sampling more individuals buys resolution near +/// the tips and almost nothing at the root. +/// +/// Errors: +/// Returns an error for fewer than two lineages or an empty population. +/// +/// Rust: `biophysics::population::coalescent_tmrca_expected` +#[pyfunction] +#[pyo3(name = "coalescent_tmrca_expected", signature = (n, k))] +pub fn pyfn_coalescent_tmrca_expected(n: u64, k: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::population::coalescent_tmrca_expected(n, k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// One realisation of the coalescent: the waiting times, in generations, +/// while the sample has `k, k-1, ..., 2` ancestors. +/// +/// Returns the intervals in that order, so the total tree height is their +/// sum. Each is exponential with rate `C(k,2)/(2N)`. +/// +/// The tree *topology* belongs with the phylogenetics module; this reports +/// the times, which is what the diversity statistics here need. +/// +/// Errors: +/// Returns an error for fewer than two lineages or an empty population. +/// +/// Rust: `biophysics::population::coalescent_simulate` +#[pyfunction] +#[pyo3(name = "coalescent_simulate", signature = (n, samples, rng))] +pub fn pyfn_coalescent_simulate(n: u64, samples: u64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::population::coalescent_simulate(n, samples, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Watterson's estimator of `theta = 4 N mu` from the number of segregating +/// sites: `theta_W = S / a_n`. +/// +/// The division by `a_n` rather than by `n` is the whole content: the number +/// of segregating sites grows only logarithmically with the sample, because +/// each additional sequence adds a shorter and shorter branch to the +/// genealogy. Dividing by the sample size would make the estimate fall +/// steadily as more data arrived. +/// +/// Errors: +/// Returns an error for fewer than two sequences or a negative site count. +/// +/// Rust: `biophysics::population::watterson_theta` +#[pyfunction] +#[pyo3(name = "watterson_theta", signature = (segregating, n))] +pub fn pyfn_watterson_theta(segregating: f64, n: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::population::watterson_theta(segregating, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Nucleotide diversity `pi`: the mean number of differences between a pair +/// of sequences. +/// +/// Errors: +/// Returns an error for fewer than two sequences or sequences of differing +/// length. +/// +/// Rust: `biophysics::population::nucleotide_diversity` +#[pyfunction] +#[pyo3(name = "nucleotide_diversity", signature = (sequences))] +pub fn pyfn_nucleotide_diversity<'py>(py: Python<'py>, sequences: Vec>) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::population::nucleotide_diversity(&sequences))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The number of segregating sites in an alignment. +/// +/// Errors: +/// Returns an error for fewer than two sequences or sequences of differing +/// length. +/// +/// Rust: `biophysics::population::segregating_sites` +#[pyfunction] +#[pyo3(name = "segregating_sites", signature = (sequences))] +pub fn pyfn_segregating_sites<'py>(py: Python<'py>, sequences: Vec>) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::population::segregating_sites(&sequences))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Tajima's D: the standardised difference between nucleotide diversity and +/// Watterson's estimator. +/// +/// Both estimate the same `theta` under neutrality and constant size, so +/// their difference is zero in expectation and any departure is evidence +/// that one of those assumptions fails. The sign carries the interpretation: +/// negative means an excess of rare variants -- a recent expansion or a +/// selective sweep -- and positive means an excess of intermediate ones, +/// as under balancing selection or population structure. It cannot +/// distinguish demography from selection, which is why a significant D is a +/// question rather than an answer. +/// +/// Errors: +/// Returns an error for fewer than four sequences, below which the variance +/// is not defined, or for an alignment with no variation. +/// +/// Rust: `biophysics::population::tajima_d` +#[pyfunction] +#[pyo3(name = "tajima_d", signature = (sequences))] +pub fn pyfn_tajima_d<'py>(py: Python<'py>, sequences: Vec>) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::population::tajima_d(&sequences))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Wright's `F_ST` from subpopulation allele frequencies: +/// `(H_T - H_S) / H_T`. +/// +/// Zero when the subpopulations have identical frequencies and one when each +/// is fixed for a different allele. It measures how much of the total +/// heterozygosity is *lost* by subdivision, so it is a statement about +/// variance in frequency rather than about how different the populations +/// look. +/// +/// Errors: +/// Returns an error for fewer than two subpopulations, a frequency outside +/// zero to one, or a set of populations all fixed for the same allele, for +/// which there is no heterozygosity to partition. +/// +/// Rust: `biophysics::population::fst` +#[pyfunction] +#[pyo3(name = "fst", signature = (subpop_freqs))] +pub fn pyfn_fst<'py>(py: Python<'py>, subpop_freqs: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::population::fst(&subpop_freqs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_logistic_growth, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gompertz, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_richards, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_allee_effect_ode, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lotka_volterra, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rosenzweig_macarthur, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_enrichment_critical_capacity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_coexistence_condition, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_competition_lv, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_metapopulation_levins, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_leslie_matrix, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_leslie_growth_rate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_stable_age_distribution, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_euler_lotka_solve, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ricker_map, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_beverton_holt, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bifurcation_ricker, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wright_fisher, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_moran_process, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fixation_probability_moran, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_genetic_drift_heterozygosity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hardy_weinberg, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hw_chi_square_test, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_selection_one_locus, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_balanced_polymorphism, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mutation_selection_balance, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kin_selection_hamilton, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_price_equation_decompose, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_coalescent_time_expected, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_coalescent_tmrca_expected, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_coalescent_simulate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_watterson_theta, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_nucleotide_diversity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_segregating_sites, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tajima_d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fst, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_biophysics__seq_align.rs b/bindings/python/src/generated/m_biophysics__seq_align.rs new file mode 100644 index 0000000..8bade20 --- /dev/null +++ b/bindings/python/src/generated/m_biophysics__seq_align.rs @@ -0,0 +1,634 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Global alignment by Needleman-Wunsch. +/// +/// Returns the optimal score and the two aligned strings, with `-` for gaps. +/// The alignment spans both sequences end to end, which is the right model +/// when the sequences are known to be homologous over their whole length and +/// the wrong one when only a domain is shared -- for that, see +/// `smith_waterman`. +/// +/// Errors: +/// Returns an error for a non-negative gap penalty, or sequences long enough +/// that the quadratic table would not fit; use `hirschberg` for those. +/// +/// Rust: `biophysics::seq_align::needleman_wunsch` +#[pyfunction] +#[pyo3(name = "needleman_wunsch", signature = (a, b, score))] +pub fn pyfn_needleman_wunsch<'py>(py: Python<'py>, a: Vec, b: Vec, score: crate::generated::types::PyScoring) -> PyResult<(i64, String, String)> { + let score = score.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::seq_align::needleman_wunsch(&a, &b, &score))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1.to_string(), __v.2.to_string())) +} + +/// Local alignment by Smith-Waterman. +/// +/// Returns `(score, start in a, start in b, aligned a, aligned b)`. +/// +/// The single change from Needleman-Wunsch -- clamping each cell at zero -- +/// is what makes it local: a prefix that aligns badly is discarded rather +/// than carried, so a strong internal match is found whatever surrounds it. +/// The score is therefore never negative, and an alignment of two unrelated +/// sequences reports a small positive score rather than a large negative +/// one, which is why local scores need a significance model and global ones +/// less so. +/// +/// Errors: +/// Returns an error on the same conditions as `needleman_wunsch`. +/// +/// Rust: `biophysics::seq_align::smith_waterman` +#[pyfunction] +#[pyo3(name = "smith_waterman", signature = (a, b, score))] +pub fn pyfn_smith_waterman<'py>(py: Python<'py>, a: Vec, b: Vec, score: crate::generated::types::PyScoring) -> PyResult<(i64, usize, usize, String, String)> { + let score = score.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::seq_align::smith_waterman(&a, &b, &score))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1, __v.2, __v.3.to_string(), __v.4.to_string())) +} + +/// Global alignment with affine gap penalties, by Gotoh's algorithm. +/// +/// A gap of length `k` costs `open + k * extend` rather than `k * gap`, so a +/// single long insertion is cheap relative to many short ones. That is the +/// biologically right shape -- one indel event of twenty residues is far +/// more likely than twenty separate ones -- and it is why affine gaps are +/// the default in practice despite costing three tables instead of one. +/// +/// With `open = 0` the model degenerates to linear gaps and the result must +/// agree with `needleman_wunsch` at `gap = extend`, which the tests check. +/// +/// Errors: +/// Returns an error for a positive gap penalty or an oversized table. +/// +/// Rust: `biophysics::seq_align::gotoh_affine` +#[pyfunction] +#[pyo3(name = "gotoh_affine", signature = (a, b, match_score, mismatch, gap_open, gap_extend))] +pub fn pyfn_gotoh_affine<'py>(py: Python<'py>, a: Vec, b: Vec, match_score: i64, mismatch: i64, gap_open: i64, gap_extend: i64) -> PyResult<(i64, String, String)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::seq_align::gotoh_affine(&a, &b, match_score, mismatch, gap_open, gap_extend))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1.to_string(), __v.2.to_string())) +} + +/// The global alignment score restricted to a diagonal band. +/// +/// Only cells with `|i - j| <= band` are computed, so the cost is +/// `O(n * band)` rather than `O(n * m)`. The result is the true optimum only +/// when the optimal alignment stays inside the band -- which is why this is +/// a heuristic for similar sequences rather than a general algorithm, and +/// why a band wide enough to contain the whole table must reproduce +/// `needleman_wunsch` exactly. +/// +/// Errors: +/// Returns an error for a non-negative gap penalty, or a band too narrow to +/// reach the far corner. +/// +/// Rust: `biophysics::seq_align::banded_alignment` +#[pyfunction] +#[pyo3(name = "banded_alignment", signature = (a, b, band, score))] +pub fn pyfn_banded_alignment<'py>(py: Python<'py>, a: Vec, b: Vec, band: usize, score: crate::generated::types::PyScoring) -> PyResult { + let score = score.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::seq_align::banded_alignment(&a, &b, band, &score))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Global alignment in linear space, by Hirschberg's divide and conquer. +/// +/// The score of a global alignment can be computed in `O(min(n, m))` space +/// by keeping two rows, but the *traceback* seems to need the whole table. +/// Hirschberg's observation is that the optimal alignment must cross the +/// middle row somewhere, that the crossing point can be found from two +/// linear-space score passes -- one forward, one backward -- and that the +/// problem then splits in two. The cost is a constant factor more time for +/// an asymptotic saving in space, which is the trade that makes whole-genome +/// alignment possible at all. +/// +/// The alignment it returns is optimal, so its score must equal +/// `needleman_wunsch`'s; the tests check exactly that. +/// +/// Errors: +/// Returns an error for a non-negative gap penalty. +/// +/// Rust: `biophysics::seq_align::hirschberg` +#[pyfunction] +#[pyo3(name = "hirschberg", signature = (a, b, score))] +pub fn pyfn_hirschberg<'py>(py: Python<'py>, a: Vec, b: Vec, score: crate::generated::types::PyScoring) -> PyResult<(String, String)> { + let score = score.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::seq_align::hirschberg(&a, &b, &score))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0.to_string(), __v.1.to_string())) +} + +/// The score of an alignment already made, under a scoring scheme. +/// +/// Used to check that a dynamic program achieved the score it reported -- +/// the commonest way for one of these to be wrong is to report a maximum it +/// did not actually reach. +/// +/// Gaps are charged linearly, so this agrees with `needleman_wunsch` and +/// with `gotoh_affine` only when the latter's open cost is zero. +/// +/// Errors: +/// Returns an error for alignments of differing length or a column of two +/// gaps, which no alignment should contain. +/// +/// Rust: `biophysics::seq_align::alignment_score` +#[pyfunction] +#[pyo3(name = "alignment_score", signature = (top, bottom, score))] +pub fn pyfn_alignment_score(top: String, bottom: String, score: crate::generated::types::PyScoring) -> PyResult { + let score = score.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::seq_align::alignment_score(&top, &bottom, &score)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The score of an alignment under affine gap penalties. +/// +/// Errors: +/// Returns an error on the same conditions as `alignment_score`. +/// +/// Rust: `biophysics::seq_align::alignment_score_affine` +#[pyfunction] +#[pyo3(name = "alignment_score_affine", signature = (top, bottom, match_score, mismatch, gap_open, gap_extend))] +pub fn pyfn_alignment_score_affine(top: String, bottom: String, match_score: i64, mismatch: i64, gap_open: i64, gap_extend: i64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::seq_align::alignment_score_affine(&top, &bottom, match_score, mismatch, gap_open, gap_extend)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The BLOSUM62 substitution matrix. +/// +/// Derived from blocks of aligned protein segments no more than 62 per cent +/// identical, which is what the number means -- a *higher* BLOSUM number is +/// built from more similar sequences and suits closer homologues, the +/// opposite of the intuition the name suggests. The diagonal is not +/// constant: a tryptophan match scores 11 and a leucine match 4, because +/// tryptophan is rare and its conservation is correspondingly more +/// informative. +/// +/// Rust: `biophysics::seq_align::blosum62` +#[pyfunction] +#[pyo3(name = "blosum62", signature = ())] +pub fn pyfn_blosum62() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::seq_align::blosum62()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySubstitutionMatrix { inner: __v }) +} + +/// The PAM250 substitution matrix. +/// +/// Extrapolated from one per cent accepted mutations by raising the +/// substitution probability matrix to the 250th power, so it describes very +/// distant relationships -- the opposite end of the range from BLOSUM62. The +/// extrapolation is its weakness: errors in the one-per-cent estimates +/// compound over 250 multiplications, which is the reason BLOSUM, built +/// directly from distant alignments, generally does better at finding remote +/// homologues. +/// +/// Rust: `biophysics::seq_align::pam250` +#[pyfunction] +#[pyo3(name = "pam250", signature = ())] +pub fn pyfn_pam250() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::seq_align::pam250()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySubstitutionMatrix { inner: __v }) +} + +/// The fraction of G and C bases. +/// +/// Errors: +/// Returns an error for an empty sequence. +/// +/// Rust: `biophysics::seq_align::gc_content` +#[pyfunction] +#[pyo3(name = "gc_content", signature = (seq))] +pub fn pyfn_gc_content<'py>(py: Python<'py>, seq: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::seq_align::gc_content(&seq))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The reverse complement of a DNA sequence. +/// +/// An involution: applying it twice returns the original, which is what +/// makes it a symmetry of double-stranded DNA rather than a transformation +/// of it. Unrecognised bases are passed through as `N`. +/// +/// Rust: `biophysics::seq_align::reverse_complement` +#[pyfunction] +#[pyo3(name = "reverse_complement", signature = (seq))] +pub fn pyfn_reverse_complement<'py>(py: Python<'py>, seq: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::seq_align::reverse_complement(&seq))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// DNA to RNA: thymine becomes uracil. +/// +/// Rust: `biophysics::seq_align::transcribe` +#[pyfunction] +#[pyo3(name = "transcribe", signature = (seq))] +pub fn pyfn_transcribe<'py>(py: Python<'py>, seq: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::seq_align::transcribe(&seq))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The amino acid a codon encodes, or `*` for a stop and `X` for anything +/// unrecognised. +/// +/// Rust: `biophysics::seq_align::codon_to_amino` +#[pyfunction] +#[pyo3(name = "codon_to_amino", signature = (codon))] +pub fn pyfn_codon_to_amino<'py>(py: Python<'py>, codon: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::seq_align::codon_to_amino(&codon))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Translates a nucleotide sequence in frame zero, stopping at the first +/// stop codon. +/// +/// Rust: `biophysics::seq_align::translate` +#[pyfunction] +#[pyo3(name = "translate", signature = (seq))] +pub fn pyfn_translate<'py>(py: Python<'py>, seq: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::seq_align::translate(&seq))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Open reading frames, as `(start, end, strand)` with the strand `+1` or +/// `-1` and positions on the forward strand. +/// +/// Searches all six frames. `min_len` is in amino acids, excluding the stop. +/// +/// Errors: +/// Returns an error for a zero minimum length, which would report every +/// start codon. +/// +/// Rust: `biophysics::seq_align::orf_find` +#[pyfunction] +#[pyo3(name = "orf_find", signature = (seq, min_len))] +pub fn pyfn_orf_find<'py>(py: Python<'py>, seq: Vec, min_len: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::seq_align::orf_find(&seq, min_len))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, __x.2)).collect::>()) +} + +/// Codon usage counts as fractions, for codons appearing in frame zero. +/// +/// Errors: +/// Returns an error for a sequence shorter than one codon. +/// +/// Rust: `biophysics::seq_align::codon_usage` +#[pyfunction] +#[pyo3(name = "codon_usage", signature = (seq))] +pub fn pyfn_codon_usage<'py>(py: Python<'py>, seq: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::seq_align::codon_usage(&seq))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0.to_string(), __x.1)).collect::>()) +} + +/// The Wallace rule melting temperature: `2 (A + T) + 4 (G + C)` degrees. +/// +/// Valid only for short oligonucleotides, roughly 14 to 20 bases. It ignores +/// concentration, salt and stacking entirely, which is why it disagrees with +/// `tm_nearest_neighbor` by ten degrees or more on anything longer -- the +/// stacking energy that the nearest-neighbour model accounts for is not a +/// correction at that length, it is most of the answer. +/// +/// Errors: +/// Returns an error for an empty sequence. +/// +/// Rust: `biophysics::seq_align::melting_temperature_wallace` +#[pyfunction] +#[pyo3(name = "melting_temperature_wallace", signature = (seq))] +pub fn pyfn_melting_temperature_wallace<'py>(py: Python<'py>, seq: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::seq_align::melting_temperature_wallace(&seq))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The nearest-neighbour melting temperature, in degrees Celsius. +/// +/// `Tm = dH / (dS + R ln(C/4)) - 273.15`, with the enthalpy and entropy +/// summed over adjacent base pairs from the SantaLucia unified parameters. +/// The concentration enters logarithmically, so a hundredfold change moves +/// the melting point by only a few degrees -- which is why primer design +/// tolerates approximate concentrations and not approximate sequences. +/// +/// Errors: +/// Returns an error for a sequence shorter than two bases, a non-positive +/// concentration, or a base outside A, C, G and T. +/// +/// Rust: `biophysics::seq_align::tm_nearest_neighbor` +#[pyfunction] +#[pyo3(name = "tm_nearest_neighbor", signature = (seq, concentration))] +pub fn pyfn_tm_nearest_neighbor<'py>(py: Python<'py>, seq: Vec, concentration: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::seq_align::tm_nearest_neighbor(&seq, concentration))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The Hamming distance, or `None` if the sequences differ in length. +/// +/// Rust: `biophysics::seq_align::hamming_seqs` +#[pyfunction] +#[pyo3(name = "hamming_seqs", signature = (a, b))] +pub fn pyfn_hamming_seqs<'py>(py: Python<'py>, a: Vec, b: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::seq_align::hamming_seqs(&a, &b))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// The proportion of differing sites. +/// +/// Errors: +/// Returns an error for empty or mismatched sequences. +/// +/// Rust: `biophysics::seq_align::p_distance` +#[pyfunction] +#[pyo3(name = "p_distance", signature = (a, b))] +pub fn pyfn_p_distance<'py>(py: Python<'py>, a: Vec, b: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::seq_align::p_distance(&a, &b))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The Jukes-Cantor corrected distance +/// `d = -3/4 ln(1 - 4p/3)`. +/// +/// The correction is for *multiple hits*: two sequences that have diverged +/// far enough will differ at three quarters of their sites by chance alone, +/// because a random base matches one time in four. So the observed +/// proportion saturates at 0.75 while the true number of substitutions grows +/// without bound, and the logarithm is what recovers the latter from the +/// former. Above the saturation point the distance is not merely large -- +/// it is undefined, and reporting a large finite number there would be +/// worse than refusing. +/// +/// Errors: +/// Returns an error for a proportion outside `[0, 3/4)`. +/// +/// Rust: `biophysics::seq_align::jukes_cantor_distance` +#[pyfunction] +#[pyo3(name = "jukes_cantor_distance", signature = (p))] +pub fn pyfn_jukes_cantor_distance(p: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::seq_align::jukes_cantor_distance(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Kimura's two-parameter distance from transition and transversion +/// proportions. +/// +/// Distinguishing the two matters because transitions -- purine to purine or +/// pyrimidine to pyrimidine -- happen several times more often than +/// transversions despite there being twice as many transversions available. +/// Treating all changes alike, as Jukes-Cantor does, therefore +/// underestimates the divergence of sequences that have accumulated mostly +/// transitions. +/// +/// Errors: +/// Returns an error for proportions outside the range where the formula's +/// logarithms are defined. +/// +/// Rust: `biophysics::seq_align::kimura_2p` +#[pyfunction] +#[pyo3(name = "kimura_2p", signature = (transitions, transversions))] +pub fn pyfn_kimura_2p(transitions: f64, transversions: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::seq_align::kimura_2p(transitions, transversions)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Every `k`-mer and the positions it occurs at, sorted by k-mer. +/// +/// Errors: +/// Returns an error for a zero `k` or one longer than the sequence. +/// +/// Rust: `biophysics::seq_align::kmer_index` +#[pyfunction] +#[pyo3(name = "kmer_index", signature = (seq, k))] +pub fn pyfn_kmer_index<'py>(py: Python<'py>, seq: Vec, k: usize) -> PyResult, Vec)>> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::seq_align::kmer_index(&seq, k))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// The minimizers of a sequence: the smallest-hashing k-mer in each window +/// of `w` consecutive k-mers, deduplicated by position. +/// +/// The property that makes minimizers useful is not that they are a sample +/// but that they are a *consistent* one: two sequences that share a +/// substring of length at least `w + k - 1` are guaranteed to select the +/// same minimizer from it, so a shared region is found without comparing +/// every k-mer. Random sampling has no such guarantee. +/// +/// Errors: +/// Returns an error for a zero `k` or `w`, or a sequence too short to hold a +/// window. +/// +/// Rust: `biophysics::seq_align::minimizers` +#[pyfunction] +#[pyo3(name = "minimizers", signature = (seq, k, w))] +pub fn pyfn_minimizers<'py>(py: Python<'py>, seq: Vec, k: usize, w: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::seq_align::minimizers(&seq, k, w))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Exact pattern search over the Burrows-Wheeler transform, by backward +/// search on an FM-index. +/// +/// Backward search narrows an interval of the suffix array one pattern +/// character at a time, so the cost depends on the *pattern* length and not +/// on the text's -- which is the whole point of the index. Returns the +/// matching positions in the original text, sorted. +/// +/// Errors: +/// Returns an error for an empty pattern or text. +/// +/// Rust: `biophysics::seq_align::burrows_wheeler_search` +#[pyfunction] +#[pyo3(name = "burrows_wheeler_search", signature = (text, pattern))] +pub fn pyfn_burrows_wheeler_search<'py>(py: Python<'py>, text: Vec, pattern: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::seq_align::burrows_wheeler_search(&text, &pattern))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// A centre-star multiple alignment. +/// +/// Picks the sequence with the best total pairwise score as the centre, +/// aligns every other to it, and merges the results by inserting gaps so +/// that all agree with the centre. The result is not optimal -- optimal +/// multiple alignment is NP-hard in the number of sequences -- and its +/// quality depends entirely on the centre being a reasonable +/// representative, which is why it degrades on a divergent family. +/// +/// Errors: +/// Returns an error for fewer than two sequences, an empty sequence, or a +/// bad scoring. +/// +/// Rust: `biophysics::seq_align::msa_center_star` +#[pyfunction] +#[pyo3(name = "msa_center_star", signature = (sequences, score))] +pub fn pyfn_msa_center_star<'py>(py: Python<'py>, sequences: Vec>, score: crate::generated::types::PyScoring) -> PyResult> { + let score = score.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::seq_align::msa_center_star(&sequences, &score))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| __x.to_string()).collect::>()) +} + +/// The residue frequency profile of an alignment, as `(residue, column +/// frequencies)` sorted by residue. +/// +/// Errors: +/// Returns an error for an empty alignment or rows of differing length. +/// +/// Rust: `biophysics::seq_align::profile_from_msa` +#[pyfunction] +#[pyo3(name = "profile_from_msa", signature = (msa))] +pub fn pyfn_profile_from_msa<'py>(py: Python<'py>, msa: Vec) -> PyResult)>> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::seq_align::profile_from_msa(&msa))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// The consensus sequence: the commonest residue in each column, with gaps +/// broken in favour of a residue. +/// +/// Errors: +/// Returns an error on the same conditions as `profile_from_msa`. +/// +/// Rust: `biophysics::seq_align::consensus` +#[pyfunction] +#[pyo3(name = "consensus", signature = (msa))] +pub fn pyfn_consensus<'py>(py: Python<'py>, msa: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::seq_align::consensus(&msa))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.to_string()) +} + +/// Scores a sequence against a position-specific scoring matrix, sliding it +/// along and reporting the log-odds score at each offset. +/// +/// The background is uniform over the profile's residues. A count of zero +/// would give a log-odds of negative infinity, so a pseudocount is added -- +/// without one, a single unobserved residue vetoes an otherwise perfect +/// match, which is an artefact of finite sampling rather than a fact about +/// the motif. +/// +/// Errors: +/// Returns an error for an empty profile or a sequence shorter than it. +/// +/// Rust: `biophysics::seq_align::pssm_score` +#[pyfunction] +#[pyo3(name = "pssm_score", signature = (profile, seq))] +pub fn pyfn_pssm_score<'py>(py: Python<'py>, profile: Vec<(u8, Vec)>, seq: Vec) -> PyResult> { + let profile = profile.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::seq_align::pssm_score(&profile, &seq))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// A de Bruijn assembly: the unambiguous paths through the k-mer graph of a +/// read set. +/// +/// Each read contributes its `k`-mers; nodes are `(k-1)`-mers and edges are +/// `k`-mers. Contigs are grown along vertices with exactly one way in and +/// one way out, and stop wherever the graph branches -- which is exactly +/// where a repeat longer than `k` sits. That is the fundamental limit of +/// short-read assembly, not a shortcoming of this implementation: a repeat +/// longer than the read length cannot be resolved by any amount of coverage. +/// +/// Errors: +/// Returns an error for a `k` below two, or no reads long enough. +/// +/// Rust: `biophysics::seq_align::de_bruijn_assembly_lite` +#[pyfunction] +#[pyo3(name = "de_bruijn_assembly_lite", signature = (reads, k))] +pub fn pyfn_de_bruijn_assembly_lite<'py>(py: Python<'py>, reads: Vec>, k: usize) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::biophysics::seq_align::de_bruijn_assembly_lite(&reads, k))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_needleman_wunsch, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_smith_waterman, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gotoh_affine, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_banded_alignment, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hirschberg, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_alignment_score, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_alignment_score_affine, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_blosum62, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pam250, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gc_content, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_reverse_complement, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_transcribe, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_codon_to_amino, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_translate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_orf_find, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_codon_usage, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_melting_temperature_wallace, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tm_nearest_neighbor, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hamming_seqs, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_p_distance, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_jukes_cantor_distance, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kimura_2p, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kmer_index, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_minimizers, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_burrows_wheeler_search, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_msa_center_star, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_profile_from_msa, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_consensus, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pssm_score, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_de_bruijn_assembly_lite, m)?)?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_cfd.rs b/bindings/python/src/generated/m_cfd.rs new file mode 100644 index 0000000..c02bbf8 --- /dev/null +++ b/bindings/python/src/generated/m_cfd.rs @@ -0,0 +1,22 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + Ok(()) +} diff --git a/bindings/python/src/generated/m_cfd__advection.rs b/bindings/python/src/generated/m_cfd__advection.rs new file mode 100644 index 0000000..3d90a73 --- /dev/null +++ b/bindings/python/src/generated/m_cfd__advection.rs @@ -0,0 +1,253 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// First-order upwind advection of a periodic 1D field by constant +/// velocity `u` for one step. +/// +/// Rust: `cfd::advection::advect_upwind_1d` +#[pyfunction] +#[pyo3(name = "advect_upwind_1d", signature = (q, u, dx, dt))] +pub fn pyfn_advect_upwind_1d<'py>(py: Python<'py>, q: Vec, u: f64, dx: f64, dt: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::cfd::advection::advect_upwind_1d(&q, u, dx, dt))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Second-order Lax-Wendroff advection (dispersive near discontinuities). +/// +/// Rust: `cfd::advection::advect_lax_wendroff_1d` +#[pyfunction] +#[pyo3(name = "advect_lax_wendroff_1d", signature = (q, u, dx, dt))] +pub fn pyfn_advect_lax_wendroff_1d<'py>(py: Python<'py>, q: Vec, u: f64, dx: f64, dt: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::cfd::advection::advect_lax_wendroff_1d(&q, u, dx, dt))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// MUSCL advection with a slope limiter (TVD for Minmod/VanLeer/…). +/// +/// Rust: `cfd::advection::advect_muscl_1d` +#[pyfunction] +#[pyo3(name = "advect_muscl_1d", signature = (q, u, dx, dt, limiter))] +pub fn pyfn_advect_muscl_1d<'py>(py: Python<'py>, q: Vec, u: f64, dx: f64, dt: f64, limiter: crate::generated::types::PyAdvectionLimiter) -> PyResult> { + let limiter = limiter.to_rust(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::cfd::advection::advect_muscl_1d(&q, u, dx, dt, limiter))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// WENO5 reconstruction of face states at every i+1/2: returns +/// (left-biased, right-biased) values, both length n (periodic). +/// +/// Rust: `cfd::advection::weno5_reconstruct` +#[pyfunction] +#[pyo3(name = "weno5_reconstruct", signature = (q))] +pub fn pyfn_weno5_reconstruct<'py>(py: Python<'py>, q: Vec) -> PyResult<(Vec, Vec)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::cfd::advection::weno5_reconstruct(&q))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// WENO5 upwind advection (Euler step in time). +/// +/// Rust: `cfd::advection::advect_weno5_1d` +#[pyfunction] +#[pyo3(name = "advect_weno5_1d", signature = (q, u, dx, dt))] +pub fn pyfn_advect_weno5_1d<'py>(py: Python<'py>, q: Vec, u: f64, dx: f64, dt: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::cfd::advection::advect_weno5_1d(&q, u, dx, dt))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Semi-Lagrangian advection of a cell field through a MAC velocity +/// field (RK2 backtrace, bilinear sampling). Unconditionally stable. +/// +/// Rust: `cfd::advection::advect_semi_lagrangian_2d` +#[pyfunction] +#[pyo3(name = "advect_semi_lagrangian_2d", signature = (q, grid, dt))] +pub fn pyfn_advect_semi_lagrangian_2d(q: crate::generated::types::PyCellField2, grid: pyo3::PyRef<'_, crate::generated::types::PyMacGrid2>, dt: f64) -> PyResult { + let q = q.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::advection::advect_semi_lagrangian_2d(&q, &grid.inner, dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCellField2 { inner: __v }) +} + +/// Back-and-forth error compensation and correction (BFECC): second +/// order, limited to the local min/max to avoid new extrema. +/// +/// Rust: `cfd::advection::advect_bfecc_2d` +#[pyfunction] +#[pyo3(name = "advect_bfecc_2d", signature = (q, grid, dt))] +pub fn pyfn_advect_bfecc_2d(q: crate::generated::types::PyCellField2, grid: pyo3::PyRef<'_, crate::generated::types::PyMacGrid2>, dt: f64) -> PyResult { + let q = q.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::advection::advect_bfecc_2d(&q, &grid.inner, dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCellField2 { inner: __v }) +} + +/// Unsplit MacCormack advection with min/max limiting. +/// +/// Rust: `cfd::advection::advect_maccormack_2d` +#[pyfunction] +#[pyo3(name = "advect_maccormack_2d", signature = (q, grid, dt))] +pub fn pyfn_advect_maccormack_2d(q: crate::generated::types::PyCellField2, grid: pyo3::PyRef<'_, crate::generated::types::PyMacGrid2>, dt: f64) -> PyResult { + let q = q.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::advection::advect_maccormack_2d(&q, &grid.inner, dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCellField2 { inner: __v }) +} + +/// First-order upwind advection on the 2D grid using face velocities. +/// +/// Rust: `cfd::advection::advect_upwind_2d` +#[pyfunction] +#[pyo3(name = "advect_upwind_2d", signature = (q, grid, dt))] +pub fn pyfn_advect_upwind_2d(q: crate::generated::types::PyCellField2, grid: pyo3::PyRef<'_, crate::generated::types::PyMacGrid2>, dt: f64) -> PyResult { + let q = q.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::advection::advect_upwind_2d(&q, &grid.inner, dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCellField2 { inner: __v }) +} + +/// Advect the MAC velocity field itself semi-Lagrangianly (each face +/// component backtraced from its own staggered position). +/// +/// Rust: `cfd::advection::advect_velocity_semi_lagrangian` +#[pyfunction] +#[pyo3(name = "advect_velocity_semi_lagrangian", signature = (grid, dt))] +pub fn pyfn_advect_velocity_semi_lagrangian(grid: pyo3::PyRefMut<'_, crate::generated::types::PyMacGrid2>, dt: f64) -> PyResult<()> { + let mut grid = grid; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::advection::advect_velocity_semi_lagrangian(&mut grid.inner, dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) +} + +/// Dimensionally split flux-limited (MUSCL) advection on the 2D grid. +/// +/// Rust: `cfd::advection::advect_flux_limited_2d` +#[pyfunction] +#[pyo3(name = "advect_flux_limited_2d", signature = (q, grid, dt, limiter))] +pub fn pyfn_advect_flux_limited_2d(q: crate::generated::types::PyCellField2, grid: pyo3::PyRef<'_, crate::generated::types::PyMacGrid2>, dt: f64, limiter: crate::generated::types::PyAdvectionLimiter) -> PyResult { + let q = q.inner; + let limiter = limiter.to_rust(); + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::advection::advect_flux_limited_2d(&q, &grid.inner, dt, limiter)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCellField2 { inner: __v }) +} + +/// Strong-stability-preserving third-order Runge-Kutta step for +/// dq/dt = rhs(q). +/// +/// Rust: `cfd::advection::rk3_ssp` +#[pyfunction] +#[pyo3(name = "rk3_ssp", signature = (q, rhs, dt))] +pub fn pyfn_rk3_ssp(q: Vec, rhs: pyo3::Py, dt: f64) -> PyResult> { + let __cb_rhs = std::rc::Rc::new(crate::runtime::Callback::new(rhs)); + let rhs = { let __cb = __cb_rhs.clone(); move |__a0: &[f64]| -> Vec { __cb.call::<_, Vec>((__a0.to_vec(),), Vec::new()) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::advection::rk3_ssp(&q, &rhs, dt)); + crate::runtime::callback::check(&[&__cb_rhs], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// One explicit step of viscous Burgers u_t + (u²/2)_x = ν u_xx on a +/// periodic domain. +/// +/// Rust: `cfd::advection::burgers_step` +#[pyfunction] +#[pyo3(name = "burgers_step", signature = (u, dx, dt, nu, scheme))] +pub fn pyfn_burgers_step<'py>(py: Python<'py>, u: Vec, dx: f64, dt: f64, nu: f64, scheme: crate::generated::types::PyScheme) -> PyResult> { + let scheme = scheme.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::cfd::advection::burgers_step(&u, dx, dt, nu, scheme))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Exact viscous Burgers solution by the Cole-Hopf transform: +/// u(x,t) = ∫ ((x−y)/t) e^{−G/2ν} dy / ∫ e^{−G/2ν} dy with +/// G(y) = (x−y)²/(2t) + ∫₀^y u₀. +/// +/// Rust: `cfd::advection::burgers_exact_cole_hopf` +#[pyfunction] +#[pyo3(name = "burgers_exact_cole_hopf", signature = (x, t, nu, u0))] +pub fn pyfn_burgers_exact_cole_hopf(x: f64, t: f64, nu: f64, u0: pyo3::Py) -> PyResult { + let __cb_u0 = std::rc::Rc::new(crate::runtime::Callback::new(u0)); + let u0 = { let __cb = __cb_u0.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::advection::burgers_exact_cole_hopf(x, t, nu, &u0)); + crate::runtime::callback::check(&[&__cb_u0], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// One explicit step of 1D advection-diffusion q_t + u q_x = D q_xx. +/// +/// Rust: `cfd::advection::advection_diffusion_1d` +#[pyfunction] +#[pyo3(name = "advection_diffusion_1d", signature = (q, u, d, dx, dt, scheme))] +pub fn pyfn_advection_diffusion_1d<'py>(py: Python<'py>, q: Vec, u: f64, d: f64, dx: f64, dt: f64, scheme: crate::generated::types::PyScheme) -> PyResult> { + let scheme = scheme.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::cfd::advection::advection_diffusion_1d(&q, u, d, dx, dt, scheme))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Cell Péclet number u dx / D. +/// +/// Rust: `cfd::advection::peclet_cell` +#[pyfunction] +#[pyo3(name = "peclet_cell", signature = (u, dx, d))] +pub fn pyfn_peclet_cell(u: f64, dx: f64, d: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::advection::peclet_cell(u, dx, d)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Total variation Σ |q_{i+1} − q_i| (periodic). +/// +/// Rust: `cfd::advection::total_variation` +#[pyfunction] +#[pyo3(name = "total_variation", signature = (q))] +pub fn pyfn_total_variation<'py>(py: Python<'py>, q: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::cfd::advection::total_variation(&q))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_advect_upwind_1d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_advect_lax_wendroff_1d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_advect_muscl_1d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_weno5_reconstruct, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_advect_weno5_1d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_advect_semi_lagrangian_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_advect_bfecc_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_advect_maccormack_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_advect_upwind_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_advect_velocity_semi_lagrangian, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_advect_flux_limited_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rk3_ssp, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_burgers_step, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_burgers_exact_cole_hopf, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_advection_diffusion_1d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_peclet_cell, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_total_variation, m)?)?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_cfd__boundary_layer.rs b/bindings/python/src/generated/m_cfd__boundary_layer.rs new file mode 100644 index 0000000..5c180af --- /dev/null +++ b/bindings/python/src/generated/m_cfd__boundary_layer.rs @@ -0,0 +1,413 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Blasius flat-plate similarity solution: rows (η, f, f′, f″) with +/// f‴ + ½ f f″ = 0, solved by RK4 shooting on f″(0). +/// +/// Rust: `cfd::boundary_layer::blasius_solve` +#[pyfunction] +#[pyo3(name = "blasius_solve", signature = (eta_max, n))] +pub fn pyfn_blasius_solve<'py>(py: Python<'py>, eta_max: f64, n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::cfd::boundary_layer::blasius_solve(eta_max, n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, __x.2, __x.3)).collect::>()) +} + +/// Blasius streamwise velocity u(y) at station x. +/// +/// Rust: `cfd::boundary_layer::blasius_profile` +#[pyfunction] +#[pyo3(name = "blasius_profile", signature = (y, x, u_inf, nu))] +pub fn pyfn_blasius_profile(y: f64, x: f64, u_inf: f64, nu: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::boundary_layer::blasius_profile(y, x, u_inf, nu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Blasius thicknesses (δ99, δ*, θ) at station x. +/// +/// Rust: `cfd::boundary_layer::blasius_thickness` +#[pyfunction] +#[pyo3(name = "blasius_thickness", signature = (x, u_inf, nu))] +pub fn pyfn_blasius_thickness(x: f64, u_inf: f64, nu: f64) -> PyResult<(f64, f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::boundary_layer::blasius_thickness(x, u_inf, nu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// Local Blasius skin friction 0.664/√Re_x. +/// +/// Rust: `cfd::boundary_layer::blasius_cf` +#[pyfunction] +#[pyo3(name = "blasius_cf", signature = (x, u_inf, nu))] +pub fn pyfn_blasius_cf(x: f64, u_inf: f64, nu: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::boundary_layer::blasius_cf(x, u_inf, nu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Total laminar drag of one side of a flat plate. +/// +/// Rust: `cfd::boundary_layer::blasius_drag_plate` +#[pyfunction] +#[pyo3(name = "blasius_drag_plate", signature = (l, width, u_inf, nu, rho))] +pub fn pyfn_blasius_drag_plate(l: f64, width: f64, u_inf: f64, nu: f64, rho: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::boundary_layer::blasius_drag_plate(l, width, u_inf, nu, rho)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Falkner-Skan wedge-flow similarity: rows (η, f, f′) for +/// f‴ + f f″ + β(1 − f′²) = 0. +/// +/// Rust: `cfd::boundary_layer::falkner_skan_solve` +#[pyfunction] +#[pyo3(name = "falkner_skan_solve", signature = (beta, eta_max, n))] +pub fn pyfn_falkner_skan_solve<'py>(py: Python<'py>, beta: f64, eta_max: f64, n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::cfd::boundary_layer::falkner_skan_solve(beta, eta_max, n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, __x.2)).collect::>()) +} + +/// Falkner-Skan separation parameter β = −0.1988. +/// +/// Rust: `cfd::boundary_layer::falkner_skan_separation_beta` +#[pyfunction] +#[pyo3(name = "falkner_skan_separation_beta", signature = ())] +pub fn pyfn_falkner_skan_separation_beta() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::boundary_layer::falkner_skan_separation_beta()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Thwaites integral method along stations `x` with edge velocity +/// `u_e(x)`: rows (θ, λ, H, cf). +/// +/// Rust: `cfd::boundary_layer::thwaites_method` +#[pyfunction] +#[pyo3(name = "thwaites_method", signature = (u_e, x, nu))] +pub fn pyfn_thwaites_method(u_e: pyo3::Py, x: Vec, nu: f64) -> PyResult> { + let __cb_u_e = std::rc::Rc::new(crate::runtime::Callback::new(u_e)); + let u_e = { let __cb = __cb_u_e.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::boundary_layer::thwaites_method(&u_e, &x, nu)); + crate::runtime::callback::check(&[&__cb_u_e], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, __x.2, __x.3)).collect::>()) +} + +/// First station where Thwaites' λ drops below −0.09 (separation). +/// +/// Rust: `cfd::boundary_layer::thwaites_separation_point` +#[pyfunction] +#[pyo3(name = "thwaites_separation_point", signature = (u_e, x, nu))] +pub fn pyfn_thwaites_separation_point(u_e: pyo3::Py, x: Vec, nu: f64) -> PyResult> { + let __cb_u_e = std::rc::Rc::new(crate::runtime::Callback::new(u_e)); + let u_e = { let __cb = __cb_u_e.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::boundary_layer::thwaites_separation_point(&u_e, &x, nu)); + crate::runtime::callback::check(&[&__cb_u_e], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// Pohlhausen quartic velocity profile u/U at η = y/δ with shape +/// parameter λ. +/// +/// Rust: `cfd::boundary_layer::pohlhausen_profile` +#[pyfunction] +#[pyo3(name = "pohlhausen_profile", signature = (eta, lambda_))] +pub fn pyfn_pohlhausen_profile(eta: f64, lambda_: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::boundary_layer::pohlhausen_profile(eta, lambda_)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Turbulent 1/n power-law profile. +/// +/// Rust: `cfd::boundary_layer::turbulent_bl_power_law` +#[pyfunction] +#[pyo3(name = "turbulent_bl_power_law", signature = (y, delta, n))] +pub fn pyfn_turbulent_bl_power_law(y: f64, delta: f64, n: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::boundary_layer::turbulent_bl_power_law(y, delta, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Local turbulent skin friction (Prandtl 1/5-power law) 0.0592 Re⁻⅕. +/// +/// Rust: `cfd::boundary_layer::turbulent_cf_prandtl` +#[pyfunction] +#[pyo3(name = "turbulent_cf_prandtl", signature = (re_x))] +pub fn pyfn_turbulent_cf_prandtl(re_x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::boundary_layer::turbulent_cf_prandtl(re_x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Schlichting's local turbulent skin friction (2 log₁₀Re − 0.65)⁻²·³. +/// +/// Rust: `cfd::boundary_layer::turbulent_cf_schlichting` +#[pyfunction] +#[pyo3(name = "turbulent_cf_schlichting", signature = (re_x))] +pub fn pyfn_turbulent_cf_schlichting(re_x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::boundary_layer::turbulent_cf_schlichting(re_x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Turbulent boundary-layer thickness δ = 0.37 x / Re_x^{1/5}. +/// +/// Rust: `cfd::boundary_layer::turbulent_thickness_1_7` +#[pyfunction] +#[pyo3(name = "turbulent_thickness_1_7", signature = (x, re_x))] +pub fn pyfn_turbulent_thickness_1_7(x: f64, re_x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::boundary_layer::turbulent_thickness_1_7(x, re_x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Logarithmic law of the wall u⁺ = ln(y⁺)/κ + B. +/// +/// Rust: `cfd::boundary_layer::law_of_the_wall` +#[pyfunction] +#[pyo3(name = "law_of_the_wall", signature = (y_plus, kappa, b))] +pub fn pyfn_law_of_the_wall(y_plus: f64, kappa: f64, b: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::boundary_layer::law_of_the_wall(y_plus, kappa, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Spalding's composite wall profile: u⁺(y⁺) by inverting +/// y⁺ = u⁺ + e^{−κB}(e^{κu⁺} − 1 − κu⁺ − (κu⁺)²/2 − (κu⁺)³/6). +/// +/// Rust: `cfd::boundary_layer::spalding` +#[pyfunction] +#[pyo3(name = "spalding", signature = (y_plus))] +pub fn pyfn_spalding(y_plus: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::boundary_layer::spalding(y_plus)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Van Driest near-wall damping 1 − e^{−y⁺/A}. +/// +/// Rust: `cfd::boundary_layer::van_driest_damping` +#[pyfunction] +#[pyo3(name = "van_driest_damping", signature = (y_plus, a))] +pub fn pyfn_van_driest_damping(y_plus: f64, a: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::boundary_layer::van_driest_damping(y_plus, a)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Wall coordinate y⁺ = y u_τ/ν. +/// +/// Rust: `cfd::boundary_layer::y_plus` +#[pyfunction] +#[pyo3(name = "y_plus", signature = (y, u_tau, nu))] +pub fn pyfn_y_plus(y: f64, u_tau: f64, nu: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::boundary_layer::y_plus(y, u_tau, nu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Friction velocity √(τ_w/ρ). +/// +/// Rust: `cfd::boundary_layer::u_tau` +#[pyfunction] +#[pyo3(name = "u_tau", signature = (tau_w, rho))] +pub fn pyfn_u_tau(tau_w: f64, rho: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::boundary_layer::u_tau(tau_w, rho)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// First-cell height for a target y⁺ on a plate of length `l` +/// (turbulent flat-plate friction estimate). +/// +/// Rust: `cfd::boundary_layer::first_cell_height` +#[pyfunction] +#[pyo3(name = "first_cell_height", signature = (y_plus_target, u_inf, nu, re_l, l))] +pub fn pyfn_first_cell_height(y_plus_target: f64, u_inf: f64, nu: f64, re_l: f64, l: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::boundary_layer::first_cell_height(y_plus_target, u_inf, nu, re_l, l)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Mayle-style transition estimate: Re_θt ≈ 400 Ti^{−5/8} (Ti in +/// percent), converted to Re_x with the Blasius relation θ = +/// 0.664 x/√Re_x. +/// +/// Rust: `cfd::boundary_layer::transition_re_x_estimate` +#[pyfunction] +#[pyo3(name = "transition_re_x_estimate", signature = (turbulence_intensity))] +pub fn pyfn_transition_re_x_estimate(turbulence_intensity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::boundary_layer::transition_re_x_estimate(turbulence_intensity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Michel's transition criterion: Re_θ > 1.174 (1 + 22400/Re_x) Re_x^0.46. +/// +/// Rust: `cfd::boundary_layer::michel_transition_criterion` +#[pyfunction] +#[pyo3(name = "michel_transition_criterion", signature = (re_theta, re_x))] +pub fn pyfn_michel_transition_criterion(re_theta: f64, re_x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::boundary_layer::michel_transition_criterion(re_theta, re_x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Head's entrainment integral method for turbulent boundary layers: +/// rows (θ, H, cf) marched along `x`. +/// +/// Rust: `cfd::boundary_layer::head_entrainment_method` +#[pyfunction] +#[pyo3(name = "head_entrainment_method", signature = (u_e, x, nu, theta0, h0))] +pub fn pyfn_head_entrainment_method(u_e: pyo3::Py, x: Vec, nu: f64, theta0: f64, h0: f64) -> PyResult> { + let __cb_u_e = std::rc::Rc::new(crate::runtime::Callback::new(u_e)); + let u_e = { let __cb = __cb_u_e.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::boundary_layer::head_entrainment_method(&u_e, &x, nu, theta0, h0)); + crate::runtime::callback::check(&[&__cb_u_e], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, __x.2)).collect::>()) +} + +/// Stratford's turbulent separation criterion: +/// Cp √(x dCp/dx) ≥ 0.39 (10⁻⁶ Re_x)^{0.1}. +/// +/// Rust: `cfd::boundary_layer::stratford_separation_criterion` +#[pyfunction] +#[pyo3(name = "stratford_separation_criterion", signature = (cp, x, dcp_dx, re_x))] +pub fn pyfn_stratford_separation_criterion(cp: f64, x: f64, dcp_dx: f64, re_x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::boundary_layer::stratford_separation_criterion(cp, x, dcp_dx, re_x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Ekman spiral velocity (u, v) at height z for geostrophic wind `u_g`. +/// +/// Rust: `cfd::boundary_layer::ekman_spiral` +#[pyfunction] +#[pyo3(name = "ekman_spiral", signature = (z, u_g, f, nu))] +pub fn pyfn_ekman_spiral(z: f64, u_g: f64, f: f64, nu: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::boundary_layer::ekman_spiral(z, u_g, f, nu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) +} + +/// Ekman layer depth π√(2ν/f). +/// +/// Rust: `cfd::boundary_layer::ekman_depth` +#[pyfunction] +#[pyo3(name = "ekman_depth", signature = (nu, f))] +pub fn pyfn_ekman_depth(nu: f64, f: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::boundary_layer::ekman_depth(nu, f)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Stokes' second problem (oscillating plate): u(y, t). +/// +/// Rust: `cfd::boundary_layer::stokes_second_problem` +#[pyfunction] +#[pyo3(name = "stokes_second_problem", signature = (y, t, u0, omega, nu))] +pub fn pyfn_stokes_second_problem(y: f64, t: f64, u0: f64, omega: f64, nu: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::boundary_layer::stokes_second_problem(y, t, u0, omega, nu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Stokes' first problem (impulsively started plate): u = u0 erfc(η). +/// +/// Rust: `cfd::boundary_layer::stokes_first_problem` +#[pyfunction] +#[pyo3(name = "stokes_first_problem", signature = (y, t, u0, nu))] +pub fn pyfn_stokes_first_problem(y: f64, t: f64, u0: f64, nu: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::boundary_layer::stokes_first_problem(y, t, u0, nu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Plane Couette-Poiseuille flow u(y) between plates 0 and h. +/// +/// Rust: `cfd::boundary_layer::couette_flow` +#[pyfunction] +#[pyo3(name = "couette_flow", signature = (y, h, u_wall, dp_dx, mu))] +pub fn pyfn_couette_flow(y: f64, h: f64, u_wall: f64, dp_dx: f64, mu: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::boundary_layer::couette_flow(y, h, u_wall, dp_dx, mu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Laminar flat-plate local Nusselt number 0.332 Re_x^½ Pr^⅓. +/// +/// Rust: `cfd::boundary_layer::flat_plate_heat_transfer_laminar` +#[pyfunction] +#[pyo3(name = "flat_plate_heat_transfer_laminar", signature = (re_x, pr))] +pub fn pyfn_flat_plate_heat_transfer_laminar(re_x: f64, pr: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::boundary_layer::flat_plate_heat_transfer_laminar(re_x, pr)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Thermal to velocity boundary-layer thickness ratio ≈ Pr^{−1/3}. +/// +/// Rust: `cfd::boundary_layer::thermal_bl_ratio` +#[pyfunction] +#[pyo3(name = "thermal_bl_ratio", signature = (pr))] +pub fn pyfn_thermal_bl_ratio(pr: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::boundary_layer::thermal_bl_ratio(pr)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_blasius_solve, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_blasius_profile, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_blasius_thickness, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_blasius_cf, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_blasius_drag_plate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_falkner_skan_solve, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_falkner_skan_separation_beta, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_thwaites_method, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_thwaites_separation_point, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pohlhausen_profile, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_turbulent_bl_power_law, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_turbulent_cf_prandtl, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_turbulent_cf_schlichting, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_turbulent_thickness_1_7, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_law_of_the_wall, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spalding, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_van_driest_damping, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_y_plus, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_u_tau, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_first_cell_height, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_transition_re_x_estimate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_michel_transition_criterion, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_head_entrainment_method, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_stratford_separation_criterion, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ekman_spiral, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ekman_depth, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_stokes_second_problem, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_stokes_first_problem, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_couette_flow, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_flat_plate_heat_transfer_laminar, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_thermal_bl_ratio, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_cfd__grid.rs b/bindings/python/src/generated/m_cfd__grid.rs new file mode 100644 index 0000000..18d520b --- /dev/null +++ b/bindings/python/src/generated/m_cfd__grid.rs @@ -0,0 +1,26 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_cfd__lbm.rs b/bindings/python/src/generated/m_cfd__lbm.rs new file mode 100644 index 0000000..7f31888 --- /dev/null +++ b/bindings/python/src/generated/m_cfd__lbm.rs @@ -0,0 +1,132 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Body-force-driven Poiseuille channel: solid walls at j = 0 and +/// j = ny−1, periodic in x. +/// +/// Rust: `cfd::lbm::lbm_poiseuille_2d` +#[pyfunction] +#[pyo3(name = "lbm_poiseuille_2d", signature = (nx, ny, tau, force))] +pub fn pyfn_lbm_poiseuille_2d(nx: usize, ny: usize, tau: f64, force: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::lbm::lbm_poiseuille_2d(nx, ny, tau, force)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLbmD2Q9 { inner: __v }) +} + +/// Exact Poiseuille profile u(y) for channel half-width walls at y = 0 +/// and y = h. +/// +/// Rust: `cfd::lbm::poiseuille_exact` +#[pyfunction] +#[pyo3(name = "poiseuille_exact", signature = (y, h, force, nu))] +pub fn pyfn_poiseuille_exact(y: f64, h: f64, force: f64, nu: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::lbm::poiseuille_exact(y, h, force, nu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Flow past a cylinder at Reynolds number `re` (Zou-He inlet/outlet). +/// +/// Rust: `cfd::lbm::lbm_cylinder` +#[pyfunction] +#[pyo3(name = "lbm_cylinder", signature = (nx, ny, re))] +pub fn pyfn_lbm_cylinder(nx: usize, ny: usize, re: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::lbm::lbm_cylinder(nx, ny, re)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLbmD2Q9 { inner: __v }) +} + +/// Lid-driven cavity at Reynolds number `re` (moving top wall via +/// bounce-back with wall velocity). +/// +/// Rust: `cfd::lbm::lbm_lid_cavity` +#[pyfunction] +#[pyo3(name = "lbm_lid_cavity", signature = (n, re))] +pub fn pyfn_lbm_lid_cavity(n: usize, re: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::lbm::lbm_lid_cavity(n, re)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLbmD2Q9 { inner: __v }) +} + +/// Apply the moving-lid boundary to a cavity solver for one step: after +/// the regular step, impose the lid velocity on the top row (Zou-He). +/// +/// Rust: `cfd::lbm::lbm_cavity_step` +#[pyfunction] +#[pyo3(name = "lbm_cavity_step", signature = (lbm, u_lid))] +pub fn pyfn_lbm_cavity_step(lbm: pyo3::PyRefMut<'_, crate::generated::types::PyLbmD2Q9>, u_lid: f64) -> PyResult<()> { + let mut lbm = lbm; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::lbm::lbm_cavity_step(&mut lbm.inner, u_lid)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) +} + +/// Double-distribution thermal LBM: returns (flow lattice, temperature +/// lattice); the temperature field advects with the flow and feeds back +/// as a Boussinesq force. Step both manually with `thermal_step`. +/// +/// Rust: `cfd::lbm::lbm_thermal` +#[pyfunction] +#[pyo3(name = "lbm_thermal", signature = (nx, ny, tau_f, tau_g))] +pub fn pyfn_lbm_thermal(nx: usize, ny: usize, tau_f: f64, tau_g: f64) -> PyResult<(crate::generated::types::PyLbmD2Q9, crate::generated::types::PyLbmD2Q9)> { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::lbm::lbm_thermal(nx, ny, tau_f, tau_g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((crate::generated::types::PyLbmD2Q9 { inner: __v.0 }, crate::generated::types::PyLbmD2Q9 { inner: __v.1 })) +} + +/// One coupled Boussinesq step of the double-distribution system. +/// +/// Rust: `cfd::lbm::thermal_step` +#[pyfunction] +#[pyo3(name = "thermal_step", signature = (flow, temp, buoyancy))] +pub fn pyfn_thermal_step(flow: pyo3::PyRefMut<'_, crate::generated::types::PyLbmD2Q9>, temp: pyo3::PyRefMut<'_, crate::generated::types::PyLbmD2Q9>, buoyancy: f64) -> PyResult<()> { + let mut flow = flow; + let mut temp = temp; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::lbm::thermal_step(&mut flow.inner, &mut temp.inner, buoyancy)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) +} + +/// Convert a lattice velocity to physical units given the lattice +/// spacing and time step. +/// +/// Rust: `cfd::lbm::lbm_to_physical` +#[pyfunction] +#[pyo3(name = "lbm_to_physical", signature = (u_lattice, dx, dt))] +pub fn pyfn_lbm_to_physical(u_lattice: f64, dx: f64, dt: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::lbm::lbm_to_physical(u_lattice, dx, dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_lbm_poiseuille_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_poiseuille_exact, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lbm_cylinder, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lbm_lid_cavity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lbm_cavity_step, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lbm_thermal, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_thermal_step, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lbm_to_physical, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_cfd__level_set.rs b/bindings/python/src/generated/m_cfd__level_set.rs new file mode 100644 index 0000000..fa892b4 --- /dev/null +++ b/bindings/python/src/generated/m_cfd__level_set.rs @@ -0,0 +1,182 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Zalesak's slotted disk on an n × n unit grid. +/// +/// Rust: `cfd::level_set::zalesak_disk` +#[pyfunction] +#[pyo3(name = "zalesak_disk", signature = (n))] +pub fn pyfn_zalesak_disk(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::level_set::zalesak_disk(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLevelSet2 { inner: __v }) +} + +/// Rigidly rotate a level set about the domain center for the given +/// revolutions; returns the relative area error. +/// +/// Rust: `cfd::level_set::zalesak_rotate` +#[pyfunction] +#[pyo3(name = "zalesak_rotate", signature = (ls, revolutions))] +pub fn pyfn_zalesak_rotate(ls: pyo3::PyRefMut<'_, crate::generated::types::PyLevelSet2>, revolutions: f64) -> PyResult { + let mut ls = ls; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::level_set::zalesak_rotate(&mut ls.inner, revolutions)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Single-vortex deformation test (LeVeque): stretch for t_period/2, +/// reverse, and return the relative area error at the end. +/// +/// Rust: `cfd::level_set::single_vortex_deformation_test` +#[pyfunction] +#[pyo3(name = "single_vortex_deformation_test", signature = (n, t_period))] +pub fn pyfn_single_vortex_deformation_test(n: usize, t_period: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::level_set::single_vortex_deformation_test(n, t_period)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Rayleigh-Plesset bubble dynamics: returns (t, R, Ṙ) samples +/// (RK4, incompressible liquid). +/// +/// Rust: `cfd::level_set::rayleigh_plesset` +#[pyfunction] +#[pyo3(name = "rayleigh_plesset", signature = (r0, p_inf, p_v, sigma, mu, rho, t_end, dt))] +pub fn pyfn_rayleigh_plesset(r0: f64, p_inf: pyo3::Py, p_v: f64, sigma: f64, mu: f64, rho: f64, t_end: f64, dt: f64) -> PyResult> { + let __cb_p_inf = std::rc::Rc::new(crate::runtime::Callback::new(p_inf)); + let p_inf = { let __cb = __cb_p_inf.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::level_set::rayleigh_plesset(r0, &p_inf, p_v, sigma, mu, rho, t_end, dt)); + crate::runtime::callback::check(&[&__cb_p_inf], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, __x.2)).collect::>()) +} + +/// Minnaert resonance frequency of a gas bubble. +/// +/// Rust: `cfd::level_set::minnaert_frequency` +#[pyfunction] +#[pyo3(name = "minnaert_frequency", signature = (r, p, rho, gamma))] +pub fn pyfn_minnaert_frequency(r: f64, p: f64, rho: f64, gamma: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::level_set::minnaert_frequency(r, p, rho, gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Droplet breakup regime by Weber number. +/// +/// Rust: `cfd::level_set::weber_breakup_regime` +#[pyfunction] +#[pyo3(name = "weber_breakup_regime", signature = (we))] +pub fn pyfn_weber_breakup_regime(we: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::level_set::weber_breakup_regime(we)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_string()) +} + +/// Ohnesorge number μ/√(ρσL). +/// +/// Rust: `cfd::level_set::ohnesorge` +#[pyfunction] +#[pyo3(name = "ohnesorge", signature = (mu, rho, sigma, l))] +pub fn pyfn_ohnesorge(mu: f64, rho: f64, sigma: f64, l: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::level_set::ohnesorge(mu, rho, sigma, l)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Deep-water gravity-capillary dispersion ω = √(gk + σk³/ρ). +/// +/// Rust: `cfd::level_set::capillary_wave_dispersion` +#[pyfunction] +#[pyo3(name = "capillary_wave_dispersion", signature = (k, sigma, rho, g))] +pub fn pyfn_capillary_wave_dispersion(k: f64, sigma: f64, rho: f64, g: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::level_set::capillary_wave_dispersion(k, sigma, rho, g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Young-Laplace pressure jump σ(1/R₁ + 1/R₂). +/// +/// Rust: `cfd::level_set::young_laplace_pressure` +#[pyfunction] +#[pyo3(name = "young_laplace_pressure", signature = (sigma, r1, r2))] +pub fn pyfn_young_laplace_pressure(sigma: f64, r1: f64, r2: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::level_set::young_laplace_pressure(sigma, r1, r2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Young's contact angle from the interfacial tensions. +/// +/// Rust: `cfd::level_set::contact_angle_young` +#[pyfunction] +#[pyo3(name = "contact_angle_young", signature = (sigma_sv, sigma_sl, sigma_lv))] +pub fn pyfn_contact_angle_young(sigma_sv: f64, sigma_sl: f64, sigma_lv: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::level_set::contact_angle_young(sigma_sv, sigma_sl, sigma_lv)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Pendant droplet profile from the axisymmetric Young-Laplace +/// equations (Bashforth-Adams): apex radius of curvature `b`, capillary +/// shape factor `beta` = Δρ g b²/σ; returns (r, z) points hanging below +/// the apex. +/// +/// Rust: `cfd::level_set::droplet_shape_pendant` +#[pyfunction] +#[pyo3(name = "droplet_shape_pendant", signature = (b, beta, n))] +pub fn pyfn_droplet_shape_pendant(b: f64, beta: f64, n: usize) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::level_set::droplet_shape_pendant(b, beta, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// Taylor bubble (slug) rise velocity 0.35 √(g D). +/// +/// Rust: `cfd::level_set::taylor_bubble_velocity` +#[pyfunction] +#[pyo3(name = "taylor_bubble_velocity", signature = (d, g))] +pub fn pyfn_taylor_bubble_velocity(d: f64, g: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::level_set::taylor_bubble_velocity(d, g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_zalesak_disk, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_zalesak_rotate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_single_vortex_deformation_test, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rayleigh_plesset, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_minnaert_frequency, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_weber_breakup_regime, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ohnesorge, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_capillary_wave_dispersion, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_young_laplace_pressure, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_contact_angle_young, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_droplet_shape_pendant, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_taylor_bubble_velocity, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_cfd__multiphase.rs b/bindings/python/src/generated/m_cfd__multiphase.rs new file mode 100644 index 0000000..a89c035 --- /dev/null +++ b/bindings/python/src/generated/m_cfd__multiphase.rs @@ -0,0 +1,479 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Mixture density rho_m = alpha rho_g + (1 - alpha) rho_l for void +/// fraction `alpha`. +/// +/// Rust: `cfd::multiphase::mixture_density` +#[pyfunction] +#[pyo3(name = "mixture_density", signature = (alpha, rho_g, rho_l))] +pub fn pyfn_mixture_density(alpha: f64, rho_g: f64, rho_l: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::multiphase::mixture_density(alpha, rho_g, rho_l)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// McAdams homogeneous mixture viscosity from quality `x`: +/// 1/mu_m = x/mu_g + (1-x)/mu_l. +/// +/// Rust: `cfd::multiphase::mixture_viscosity_mcadams` +#[pyfunction] +#[pyo3(name = "mixture_viscosity_mcadams", signature = (x, mu_g, mu_l))] +pub fn pyfn_mixture_viscosity_mcadams(x: f64, mu_g: f64, mu_l: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::multiphase::mixture_viscosity_mcadams(x, mu_g, mu_l)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Dukler mixture viscosity: mu_m = rho_m (x mu_g/rho_g + (1-x) mu_l/rho_l). +/// +/// Rust: `cfd::multiphase::mixture_viscosity_dukler` +#[pyfunction] +#[pyo3(name = "mixture_viscosity_dukler", signature = (x, rho_g, rho_l, mu_g, mu_l))] +pub fn pyfn_mixture_viscosity_dukler(x: f64, rho_g: f64, rho_l: f64, mu_g: f64, mu_l: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::multiphase::mixture_viscosity_dukler(x, rho_g, rho_l, mu_g, mu_l)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Drift-flux gas velocity v_g = C0 j + v_gj with total superficial +/// velocity j = j_g + j_l. +/// +/// Rust: `cfd::multiphase::drift_flux_velocity` +#[pyfunction] +#[pyo3(name = "drift_flux_velocity", signature = (j_g, j_l, c0, v_gj))] +pub fn pyfn_drift_flux_velocity(j_g: f64, j_l: f64, c0: f64, v_gj: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::multiphase::drift_flux_velocity(j_g, j_l, c0, v_gj)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Homogeneous (no-slip) void fraction from quality `x`. +/// +/// Rust: `cfd::multiphase::void_fraction_homogeneous` +#[pyfunction] +#[pyo3(name = "void_fraction_homogeneous", signature = (x, rho_g, rho_l))] +pub fn pyfn_void_fraction_homogeneous(x: f64, rho_g: f64, rho_l: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::multiphase::void_fraction_homogeneous(x, rho_g, rho_l)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Drift-flux void fraction alpha = j_g / (C0 j + v_gj). +/// +/// Rust: `cfd::multiphase::void_fraction_drift_flux` +#[pyfunction] +#[pyo3(name = "void_fraction_drift_flux", signature = (j_g, j_l, c0, v_gj))] +pub fn pyfn_void_fraction_drift_flux(j_g: f64, j_l: f64, c0: f64, v_gj: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::multiphase::void_fraction_drift_flux(j_g, j_l, c0, v_gj)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Lockhart-Martinelli void fraction (turbulent-turbulent): +/// alpha = 1 - 1/sqrt(1 + 20/X + 1/X^2) with the Martinelli parameter from +/// quality and fluid properties. +/// +/// Rust: `cfd::multiphase::void_fraction_lockhart_martinelli` +#[pyfunction] +#[pyo3(name = "void_fraction_lockhart_martinelli", signature = (x, rho_g, rho_l, mu_g, mu_l))] +pub fn pyfn_void_fraction_lockhart_martinelli(x: f64, rho_g: f64, rho_l: f64, mu_g: f64, mu_l: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::multiphase::void_fraction_lockhart_martinelli(x, rho_g, rho_l, mu_g, mu_l)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Turbulent-turbulent Martinelli parameter +/// X_tt = ((1-x)/x)^0.9 (rho_g/rho_l)^0.5 (mu_l/mu_g)^0.1. +/// +/// Rust: `cfd::multiphase::martinelli_parameter` +#[pyfunction] +#[pyo3(name = "martinelli_parameter", signature = (x, rho_g, rho_l, mu_g, mu_l))] +pub fn pyfn_martinelli_parameter(x: f64, rho_g: f64, rho_l: f64, mu_g: f64, mu_l: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::multiphase::martinelli_parameter(x, rho_g, rho_l, mu_g, mu_l)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Lockhart-Martinelli two-phase pressure gradient from the single-phase +/// liquid and gas gradients: dp_tp = dp_l phi_l^2 with +/// phi_l^2 = 1 + C/X + 1/X^2, X^2 = dp_l/dp_g. +/// +/// Rust: `cfd::multiphase::two_phase_pressure_drop_lockhart_martinelli` +#[pyfunction] +#[pyo3(name = "two_phase_pressure_drop_lockhart_martinelli", signature = (dp_l, dp_g, c))] +pub fn pyfn_two_phase_pressure_drop_lockhart_martinelli(dp_l: f64, dp_g: f64, c: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::multiphase::two_phase_pressure_drop_lockhart_martinelli(dp_l, dp_g, c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Chisholm C coefficient from the flow regimes of each phase +/// (turbulent-turbulent 20, viscous-turbulent 12, turbulent-viscous 10, +/// viscous-viscous 5). +/// +/// Rust: `cfd::multiphase::chisholm` +#[pyfunction] +#[pyo3(name = "chisholm", signature = (re_l, re_g))] +pub fn pyfn_chisholm(re_l: f64, re_g: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::multiphase::chisholm(re_l, re_g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Simplified Friedel two-phase multiplier phi_lo^2 for the +/// liquid-only pressure gradient, using the homogeneous density, Froude and +/// Weber corrections. +/// +/// Rust: `cfd::multiphase::friedel_correlation` +#[pyfunction] +#[pyo3(name = "friedel_correlation", signature = (x, rho_g, rho_l, mu_g, mu_l, sigma, d, mass_flux))] +pub fn pyfn_friedel_correlation(x: f64, rho_g: f64, rho_l: f64, mu_g: f64, mu_l: f64, sigma: f64, d: f64, mass_flux: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::multiphase::friedel_correlation(x, rho_g, rho_l, mu_g, mu_l, sigma, d, mass_flux)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Simplified Taitel-Dukler flow-pattern map for a pipe of diameter `d` at +/// inclination `inclination` (radians from horizontal), from superficial +/// velocities `j_g`, `j_l`. +/// +/// Rust: `cfd::multiphase::flow_pattern_taitel_dukler` +#[pyfunction] +#[pyo3(name = "flow_pattern_taitel_dukler", signature = (j_g, j_l, d, rho_g, rho_l, mu_g, mu_l, inclination))] +pub fn pyfn_flow_pattern_taitel_dukler(j_g: f64, j_l: f64, d: f64, rho_g: f64, rho_l: f64, mu_g: f64, mu_l: f64, inclination: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::multiphase::flow_pattern_taitel_dukler(j_g, j_l, d, rho_g, rho_l, mu_g, mu_l, inclination)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyFlowPattern::from_rust(&__v)) +} + +/// Eotvos (Bond) number Eo = delta_rho g d^2 / sigma. +/// +/// Rust: `cfd::multiphase::eotvos` +#[pyfunction] +#[pyo3(name = "eotvos", signature = (delta_rho, g, d, sigma))] +pub fn pyfn_eotvos(delta_rho: f64, g: f64, d: f64, sigma: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::multiphase::eotvos(delta_rho, g, d, sigma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Morton number Mo = g mu^4 / (rho sigma^3) (continuous-phase properties, +/// density difference folded into g for near-unit density ratios). +/// +/// Rust: `cfd::multiphase::morton_number` +#[pyfunction] +#[pyo3(name = "morton_number", signature = (g, mu, rho, sigma))] +pub fn pyfn_morton_number(g: f64, mu: f64, rho: f64, sigma: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::multiphase::morton_number(g, mu, rho, sigma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Tomiyama drag coefficient for a contaminated bubble: +/// Cd = max(24/Re (1 + 0.15 Re^0.687), 8 Eo / (3 (Eo + 4))). +/// +/// Rust: `cfd::multiphase::bubble_drag_coefficient` +#[pyfunction] +#[pyo3(name = "bubble_drag_coefficient", signature = (re, eo, mo))] +pub fn pyfn_bubble_drag_coefficient(re: f64, eo: f64, mo: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::multiphase::bubble_drag_coefficient(re, eo, mo)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Terminal rise velocity of a bubble of diameter `d`: force balance with +/// the Tomiyama contaminated drag law, solved by bisection. Reduces to the +/// Stokes settling formula for tiny bubbles and to the Eotvos-limited cap +/// regime for large ones. +/// +/// Rust: `cfd::multiphase::bubble_rise_velocity` +#[pyfunction] +#[pyo3(name = "bubble_rise_velocity", signature = (d, rho_l, rho_g, mu_l, sigma))] +pub fn pyfn_bubble_rise_velocity(d: f64, rho_l: f64, rho_g: f64, mu_l: f64, sigma: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::multiphase::bubble_rise_velocity(d, rho_l, rho_g, mu_l, sigma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Stokes terminal velocity of a small droplet in a continuous phase: +/// u = g d^2 (rho_d - rho_c) / (18 mu_c). +/// +/// Rust: `cfd::multiphase::droplet_terminal_velocity` +#[pyfunction] +#[pyo3(name = "droplet_terminal_velocity", signature = (d, rho_d, rho_c, mu_c))] +pub fn pyfn_droplet_terminal_velocity(d: f64, rho_d: f64, rho_c: f64, mu_c: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::multiphase::droplet_terminal_velocity(d, rho_d, rho_c, mu_c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Sauter mean diameter d32 = sum d^3 / sum d^2. +/// +/// Rust: `cfd::multiphase::sauter_mean_diameter` +#[pyfunction] +#[pyo3(name = "sauter_mean_diameter", signature = (diameters))] +pub fn pyfn_sauter_mean_diameter<'py>(py: Python<'py>, diameters: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::cfd::multiphase::sauter_mean_diameter(&diameters))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Rosin-Rammler cumulative mass fraction below diameter `d`: +/// F = 1 - exp(-(d/d_mean)^n). +/// +/// Rust: `cfd::multiphase::rosin_rammler` +#[pyfunction] +#[pyo3(name = "rosin_rammler", signature = (d, d_mean, n))] +pub fn pyfn_rosin_rammler(d: f64, d_mean: f64, n: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::multiphase::rosin_rammler(d, d_mean, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Simplified Luo-Svendsen breakup rate for a bubble/droplet of diameter +/// `d` in turbulence of dissipation `eps` at dispersed-phase fraction +/// `alpha`: rate ~ 0.923 (1-alpha) (eps/d^2)^{1/3} +/// exp(-12 sigma / (2.05 rho_c eps^{2/3} d^{5/3})). +/// +/// Rust: `cfd::multiphase::breakup_rate_luo_svendsen` +#[pyfunction] +#[pyo3(name = "breakup_rate_luo_svendsen", signature = (alpha, eps, d, sigma, rho_c))] +pub fn pyfn_breakup_rate_luo_svendsen(alpha: f64, eps: f64, d: f64, sigma: f64, rho_c: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::multiphase::breakup_rate_luo_svendsen(alpha, eps, d, sigma, rho_c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Simplified Prince-Blanch coalescence kernel for bubbles of diameters +/// `d1`, `d2` (turbulent collision frequency times a film-drainage +/// efficiency). +/// +/// Rust: `cfd::multiphase::coalescence_rate_prince_blanch` +#[pyfunction] +#[pyo3(name = "coalescence_rate_prince_blanch", signature = (d1, d2, eps, rho_c, sigma))] +pub fn pyfn_coalescence_rate_prince_blanch(d1: f64, d2: f64, eps: f64, rho_c: f64, sigma: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::multiphase::coalescence_rate_prince_blanch(d1, d2, eps, rho_c, sigma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// One explicit step of a discrete population balance on size classes +/// `sizes` (diameters) with number densities `n`: binary breakage into two +/// equal-volume daughters and pairwise coalescence into the nearest class by +/// volume. Number densities update; volume moves between resolved classes. +/// +/// Rust: `cfd::multiphase::population_balance_1d` +#[pyfunction] +#[pyo3(name = "population_balance_1d", signature = (n, sizes, breakup, coalescence, dt))] +pub fn pyfn_population_balance_1d(n: Vec, sizes: Vec, breakup: pyo3::Py, coalescence: pyo3::Py, dt: f64) -> PyResult> { + let __cb_breakup = std::rc::Rc::new(crate::runtime::Callback::new(breakup)); + let breakup = { let __cb = __cb_breakup.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __cb_coalescence = std::rc::Rc::new(crate::runtime::Callback::new(coalescence)); + let coalescence = { let __cb = __cb_coalescence.clone(); move |__a0: f64, __a1: f64| -> f64 { __cb.call::<_, f64>((__a0, __a1), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::multiphase::population_balance_1d(&n, &sizes, &breakup, &coalescence, dt)); + crate::runtime::callback::check(&[&__cb_breakup, &__cb_coalescence], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Cavitation number sigma_c = (p - p_v) / (rho u^2 / 2). +/// +/// Rust: `cfd::multiphase::cavitation_number` +#[pyfunction] +#[pyo3(name = "cavitation_number", signature = (p, p_v, rho, u))] +pub fn pyfn_cavitation_number(p: f64, p_v: f64, rho: f64, u: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::multiphase::cavitation_number(p, p_v, rho, u)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Rohsenow nucleate-boiling heat flux for wall superheat `delta_t` (K) +/// with surface constant `c_sf` (0.013 for water on polished surfaces). +/// +/// Rust: `cfd::multiphase::boiling_heat_flux_rohsenow` +#[pyfunction] +#[pyo3(name = "boiling_heat_flux_rohsenow", signature = (delta_t, fluid, c_sf))] +pub fn pyfn_boiling_heat_flux_rohsenow(delta_t: f64, fluid: crate::generated::types::PySaturatedFluid, c_sf: f64) -> PyResult { + let fluid = fluid.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::multiphase::boiling_heat_flux_rohsenow(delta_t, &fluid, c_sf)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Zuber critical heat flux: +/// q_chf = 0.131 h_fg rho_g^{1/2} (sigma g (rho_l - rho_g))^{1/4}. +/// +/// Rust: `cfd::multiphase::critical_heat_flux_zuber` +#[pyfunction] +#[pyo3(name = "critical_heat_flux_zuber", signature = (fluid))] +pub fn pyfn_critical_heat_flux_zuber(fluid: crate::generated::types::PySaturatedFluid) -> PyResult { + let fluid = fluid.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::multiphase::critical_heat_flux_zuber(&fluid)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Nusselt laminar film condensation coefficient on a vertical plate of +/// height `height` with wall subcooling `delta_t` and liquid conductivity +/// `k_l`: h = 0.943 [rho_l (rho_l - rho_g) g h_fg k^3 / (mu dT L)]^{1/4}. +/// +/// Rust: `cfd::multiphase::condensation_nusselt_film` +#[pyfunction] +#[pyo3(name = "condensation_nusselt_film", signature = (fluid, k_l, delta_t, height))] +pub fn pyfn_condensation_nusselt_film(fluid: crate::generated::types::PySaturatedFluid, k_l: f64, delta_t: f64, height: f64) -> PyResult { + let fluid = fluid.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::multiphase::condensation_nusselt_film(&fluid, k_l, delta_t, height)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Hertz-Knudsen maximum evaporation mass flux (kg/m^2/s) for molar mass +/// `m` (kg/mol) at temperature `t`: J = (p_sat - p) sqrt(m / (2 pi R T)). +/// +/// Rust: `cfd::multiphase::evaporation_rate_hertz_knudsen` +#[pyfunction] +#[pyo3(name = "evaporation_rate_hertz_knudsen", signature = (p_sat, p, t, m))] +pub fn pyfn_evaporation_rate_hertz_knudsen(p_sat: f64, p: f64, t: f64, m: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::multiphase::evaporation_rate_hertz_knudsen(p_sat, p, t, m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Hiroyasu spray tip penetration for injection pressure drop `delta_p` +/// into gas of density `rho_a` through a nozzle of diameter `d_nozzle`, +/// at time `t` after start of injection. +/// +/// Rust: `cfd::multiphase::spray_penetration_hiroyasu` +#[pyfunction] +#[pyo3(name = "spray_penetration_hiroyasu", signature = (delta_p, rho_l, rho_a, d_nozzle, t))] +pub fn pyfn_spray_penetration_hiroyasu(delta_p: f64, rho_l: f64, rho_a: f64, d_nozzle: f64, t: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::multiphase::spray_penetration_hiroyasu(delta_p, rho_l, rho_a, d_nozzle, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Particle response time tau_p = rho_p d^2 / (18 mu). +/// +/// Rust: `cfd::multiphase::particle_response_time` +#[pyfunction] +#[pyo3(name = "particle_response_time", signature = (rho_p, d_p, mu))] +pub fn pyfn_particle_response_time(rho_p: f64, d_p: f64, mu: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::multiphase::particle_response_time(rho_p, d_p, mu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Stokes number St = tau_p u / l. +/// +/// Rust: `cfd::multiphase::stokes_number` +#[pyfunction] +#[pyo3(name = "stokes_number", signature = (rho_p, d_p, u, mu, l))] +pub fn pyfn_stokes_number(rho_p: f64, d_p: f64, u: f64, mu: f64, l: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::multiphase::stokes_number(rho_p, d_p, u, mu, l)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Terminal settling velocity of a sphere with the Schiller-Naumann drag +/// Cd = 24/Re (1 + 0.15 Re^0.687), solved by bisection. +/// +/// Rust: `cfd::multiphase::settling_velocity` +#[pyfunction] +#[pyo3(name = "settling_velocity", signature = (d, rho_p, rho_f, mu, g))] +pub fn pyfn_settling_velocity(d: f64, rho_p: f64, rho_f: f64, mu: f64, g: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::multiphase::settling_velocity(d, rho_p, rho_f, mu, g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Minimum fluidization velocity from the Ergun equation balanced against +/// the bed weight at voidage `porosity`, solved by bisection. +/// +/// Rust: `cfd::multiphase::fluidization_minimum_velocity` +#[pyfunction] +#[pyo3(name = "fluidization_minimum_velocity", signature = (d, rho_p, rho_f, mu, porosity))] +pub fn pyfn_fluidization_minimum_velocity(d: f64, rho_p: f64, rho_f: f64, mu: f64, porosity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::multiphase::fluidization_minimum_velocity(d, rho_p, rho_f, mu, porosity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Richardson-Zaki hindered settling velocity u = u_t phi^n where `phi` is +/// the fluid voidage. +/// +/// Rust: `cfd::multiphase::sedimentation_richardson_zaki` +#[pyfunction] +#[pyo3(name = "sedimentation_richardson_zaki", signature = (u_t, porosity, n))] +pub fn pyfn_sedimentation_richardson_zaki(u_t: f64, porosity: f64, n: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::multiphase::sedimentation_richardson_zaki(u_t, porosity, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Richardson-Zaki exponent as a function of particle Reynolds number. +/// +/// Rust: `cfd::multiphase::hindered_settling_exponent` +#[pyfunction] +#[pyo3(name = "hindered_settling_exponent", signature = (re))] +pub fn pyfn_hindered_settling_exponent(re: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::multiphase::hindered_settling_exponent(re)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_mixture_density, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mixture_viscosity_mcadams, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mixture_viscosity_dukler, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_drift_flux_velocity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_void_fraction_homogeneous, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_void_fraction_drift_flux, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_void_fraction_lockhart_martinelli, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_martinelli_parameter, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_two_phase_pressure_drop_lockhart_martinelli, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_chisholm, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_friedel_correlation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_flow_pattern_taitel_dukler, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_eotvos, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_morton_number, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bubble_drag_coefficient, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bubble_rise_velocity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_droplet_terminal_velocity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sauter_mean_diameter, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rosin_rammler, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_breakup_rate_luo_svendsen, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_coalescence_rate_prince_blanch, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_population_balance_1d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cavitation_number, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_boiling_heat_flux_rohsenow, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_critical_heat_flux_zuber, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_condensation_nusselt_film, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_evaporation_rate_hertz_knudsen, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spray_penetration_hiroyasu, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_particle_response_time, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_stokes_number, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_settling_velocity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fluidization_minimum_velocity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sedimentation_richardson_zaki, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hindered_settling_exponent, m)?)?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_cfd__porous.rs b/bindings/python/src/generated/m_cfd__porous.rs new file mode 100644 index 0000000..3c68e85 --- /dev/null +++ b/bindings/python/src/generated/m_cfd__porous.rs @@ -0,0 +1,345 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Darcy velocity (specific discharge) q = -(k/mu) grad p; returns the +/// magnitude for a pressure gradient `grad_p` (Pa/m). +/// +/// Rust: `cfd::porous::darcy_velocity` +#[pyfunction] +#[pyo3(name = "darcy_velocity", signature = (k, mu, grad_p))] +pub fn pyfn_darcy_velocity(k: f64, mu: f64, grad_p: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::porous::darcy_velocity(k, mu, grad_p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Volumetric flow rate through area `a` over length `l` under pressure +/// difference `dp`: Q = k A dp / (mu L). +/// +/// Rust: `cfd::porous::darcy_flow_rate` +#[pyfunction] +#[pyo3(name = "darcy_flow_rate", signature = (k, a, mu, dp, l))] +pub fn pyfn_darcy_flow_rate(k: f64, a: f64, mu: f64, dp: f64, l: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::porous::darcy_flow_rate(k, a, mu, dp, l)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Kozeny-Carman permeability of a packed bed of spheres of diameter +/// `d_particle` (Ergun-consistent constant 150): +/// k = phi^3 d^2 / (150 (1-phi)^2). +/// +/// Rust: `cfd::porous::permeability_kozeny_carman` +#[pyfunction] +#[pyo3(name = "permeability_kozeny_carman", signature = (porosity, d_particle))] +pub fn pyfn_permeability_kozeny_carman(porosity: f64, d_particle: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::porous::permeability_kozeny_carman(porosity, d_particle)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Approximate Kozeny-type permeability of a random fiber mat with fiber +/// diameter `d_fiber` (Kozeny constant ~ 5 with the fiber specific surface +/// 4/d): k = phi^3 d^2 / (80 (1-phi)^2). +/// +/// Rust: `cfd::porous::carman_kozeny_fibers` +#[pyfunction] +#[pyo3(name = "carman_kozeny_fibers", signature = (porosity, d_fiber))] +pub fn pyfn_carman_kozeny_fibers(porosity: f64, d_fiber: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::porous::carman_kozeny_fibers(porosity, d_fiber)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Ergun pressure drop over bed length `l` for superficial velocity `u`: +/// dP/L = 150 mu u (1-phi)^2/(phi^3 d^2) + 1.75 rho u^2 (1-phi)/(phi^3 d). +/// +/// Rust: `cfd::porous::ergun_pressure_drop` +#[pyfunction] +#[pyo3(name = "ergun_pressure_drop", signature = (u, d, porosity, mu, rho, l))] +pub fn pyfn_ergun_pressure_drop(u: f64, d: f64, porosity: f64, mu: f64, rho: f64, l: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::porous::ergun_pressure_drop(u, d, porosity, mu, rho, l)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Forchheimer pressure gradient magnitude: dp/dx = mu u / k + beta rho u^2. +/// +/// Rust: `cfd::porous::forchheimer` +#[pyfunction] +#[pyo3(name = "forchheimer", signature = (k, beta, mu, rho, u))] +pub fn pyfn_forchheimer(k: f64, beta: f64, mu: f64, rho: f64, u: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::porous::forchheimer(k, beta, mu, rho, u)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Brinkman flow in a porous channel of height `h` driven by `dp_dx`: +/// u(y) = -(k/mu) dp/dx [1 - cosh((y - h/2)/sqrt(k)) / cosh(h/(2 sqrt(k)))]. +/// +/// Rust: `cfd::porous::brinkman_velocity_profile` +#[pyfunction] +#[pyo3(name = "brinkman_velocity_profile", signature = (y, h, k, mu, dp_dx))] +pub fn pyfn_brinkman_velocity_profile(y: f64, h: f64, k: f64, mu: f64, dp_dx: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::porous::brinkman_velocity_profile(y, h, k, mu, dp_dx)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Hydraulic conductivity K = k rho g / mu (m/s). +/// +/// Rust: `cfd::porous::hydraulic_conductivity` +#[pyfunction] +#[pyo3(name = "hydraulic_conductivity", signature = (k, rho, g, mu))] +pub fn pyfn_hydraulic_conductivity(k: f64, rho: f64, g: f64, mu: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::porous::hydraulic_conductivity(k, rho, g, mu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Brooks-Corey effective saturation for entry pressure head `h_b` (m) and +/// pore-size index `lambda`. +/// +/// Rust: `cfd::porous::brooks_corey` +#[pyfunction] +#[pyo3(name = "brooks_corey", signature = (h, h_b, lambda_))] +pub fn pyfn_brooks_corey(h: f64, h_b: f64, lambda_: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::porous::brooks_corey(h, h_b, lambda_)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Explicit finite-volume Richards equation in 1D (z positive downward), +/// theta-form so mass is conserved exactly. Boundary conditions are water +/// fluxes (m/s, positive downward): `bc_top` enters the first cell, +/// `bc_bottom` leaves the last. Returns the water-content profile after each +/// step (`steps + 1` rows including the initial state). +/// +/// Rust: `cfd::porous::richards_equation_1d` +#[pyfunction] +#[pyo3(name = "richards_equation_1d", signature = (theta0, soil, dz, dt, steps, bc_top, bc_bottom))] +pub fn pyfn_richards_equation_1d<'py>(py: Python<'py>, theta0: Vec, soil: crate::generated::types::PyVanGenuchtenArg, dz: f64, dt: f64, steps: usize, bc_top: f64, bc_bottom: f64) -> PyResult>> { + let soil = soil.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::cfd::porous::richards_equation_1d(&theta0, &soil, dz, dt, steps, bc_top, bc_bottom))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Theis transient drawdown at radius `r` and time `t` for pumping rate `q`, +/// transmissivity `t_coeff` and storativity `s`: +/// s_d = Q/(4 pi T) W(u), u = r^2 S/(4 T t), W = E1. +/// +/// Rust: `cfd::porous::theis_drawdown` +#[pyfunction] +#[pyo3(name = "theis_drawdown", signature = (q, t_coeff, s, r, t))] +pub fn pyfn_theis_drawdown(q: f64, t_coeff: f64, s: f64, r: f64, t: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::porous::theis_drawdown(q, t_coeff, s, r, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Thiem steady-state head at radius `r2` given head `h1` at `r1`: +/// h2 = h1 + Q/(2 pi T) ln(r2/r1). +/// +/// Rust: `cfd::porous::thiem_steady` +#[pyfunction] +#[pyo3(name = "thiem_steady", signature = (q, t_coeff, r1, r2, h1))] +pub fn pyfn_thiem_steady(q: f64, t_coeff: f64, r1: f64, r2: f64, h1: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::porous::thiem_steady(q, t_coeff, r1, r2, h1)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Dupuit unconfined flow between heads `h1` and `h2` over length `l`: +/// the water-table height at distance `x`. +/// +/// Rust: `cfd::porous::dupuit_unconfined` +#[pyfunction] +#[pyo3(name = "dupuit_unconfined", signature = (h1, h2, l, x))] +pub fn pyfn_dupuit_unconfined(h1: f64, h2: f64, l: f64, x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::porous::dupuit_unconfined(h1, h2, l, x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Steady 2D groundwater flow: solve div(K grad h) = -recharge with +/// Dirichlet head fixed at the listed `(i, j, head)` cells and no-flow +/// elsewhere on the boundary. Gauss-Seidel with harmonic-mean face +/// conductivities. +/// +/// Rust: `cfd::porous::groundwater_flow_2d` +#[pyfunction] +#[pyo3(name = "groundwater_flow_2d", signature = (k_field, bc, recharge))] +pub fn pyfn_groundwater_flow_2d(k_field: crate::generated::types::PyCellField2, bc: Vec<(usize, usize, f64)>, recharge: crate::generated::types::PyCellField2) -> PyResult { + let k_field = k_field.inner; + let bc = bc.into_iter().map(|__e| (__e.0, __e.1, __e.2)).collect::>(); + let recharge = recharge.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::porous::groundwater_flow_2d(&k_field, &bc, &recharge)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCellField2 { inner: __v }) +} + +/// Peclet number for porous transport: Pe = u l / D. +/// +/// Rust: `cfd::porous::peclet_porous` +#[pyfunction] +#[pyo3(name = "peclet_porous", signature = (u, l, d))] +pub fn pyfn_peclet_porous(u: f64, l: f64, d: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::porous::peclet_porous(u, l, d)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Hydrodynamic dispersion coefficient D = alpha_L u + D_m. +/// +/// Rust: `cfd::porous::dispersion_coefficient` +#[pyfunction] +#[pyo3(name = "dispersion_coefficient", signature = (alpha_l, u, d_m))] +pub fn pyfn_dispersion_coefficient(alpha_l: f64, u: f64, d_m: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::porous::dispersion_coefficient(alpha_l, u, d_m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// One explicit step of the 1D advection-dispersion-reaction equation with +/// retardation factor R and first-order decay: +/// R dc/dt + u dc/dx = D d2c/dx2 - R lambda c (upwind advection). +/// +/// Rust: `cfd::porous::advection_dispersion_1d` +#[pyfunction] +#[pyo3(name = "advection_dispersion_1d", signature = (c, u, d, dx, dt, retardation, decay))] +pub fn pyfn_advection_dispersion_1d<'py>(py: Python<'py>, c: Vec, u: f64, d: f64, dx: f64, dt: f64, retardation: f64, decay: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::cfd::porous::advection_dispersion_1d(&c, u, d, dx, dt, retardation, decay))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Ogata-Banks solution for continuous injection at x = 0 into an initially +/// clean semi-infinite column: c/c0 at (x, t). +/// +/// Rust: `cfd::porous::ogata_banks` +#[pyfunction] +#[pyo3(name = "ogata_banks", signature = (x, t, u, d))] +pub fn pyfn_ogata_banks(x: f64, t: f64, u: f64, d: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::porous::ogata_banks(x, t, u, d)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Leverett J-function scaling of capillary pressure: +/// Pc = sigma cos(theta) sqrt(phi/k) J(Sw), with J = 0.5 Sw^{-1/2}. +/// +/// Rust: `cfd::porous::capillary_pressure_leverett` +#[pyfunction] +#[pyo3(name = "capillary_pressure_leverett", signature = (sw, porosity, k, sigma, theta))] +pub fn pyfn_capillary_pressure_leverett(sw: f64, porosity: f64, k: f64, sigma: f64, theta: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::porous::capillary_pressure_leverett(sw, porosity, k, sigma, theta)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Corey relative permeabilities `(k_rw, k_ro)` with residual saturations +/// and exponent `n`. +/// +/// Rust: `cfd::porous::relative_permeability_corey` +#[pyfunction] +#[pyo3(name = "relative_permeability_corey", signature = (sw, sw_r, so_r, n))] +pub fn pyfn_relative_permeability_corey(sw: f64, sw_r: f64, so_r: f64, n: f64) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::porous::relative_permeability_corey(sw, sw_r, so_r, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Buckley-Leverett water saturation at position `x` and time `t` for total +/// (Darcy) velocity `u_total` injected into a column at connate water +/// saturation, using quadratic Corey curves. Returns Sw(x, t) including the +/// Welge shock front. +/// +/// Rust: `cfd::porous::buckley_leverett` +#[pyfunction] +#[pyo3(name = "buckley_leverett", signature = (x, t, u_total, porosity, mu_w, mu_o, sw_r, so_r))] +pub fn pyfn_buckley_leverett(x: f64, t: f64, u_total: f64, porosity: f64, mu_w: f64, mu_o: f64, sw_r: f64, so_r: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::porous::buckley_leverett(x, t, u_total, porosity, mu_w, mu_o, sw_r, so_r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Porosity reduction by biofilm growth: phi = phi0 - biomass/rho_biofilm. +/// +/// Rust: `cfd::porous::bioclogging_porosity_change` +#[pyfunction] +#[pyo3(name = "bioclogging_porosity_change", signature = (phi0, biomass, rho_biofilm))] +pub fn pyfn_bioclogging_porosity_change(phi0: f64, biomass: f64, rho_biofilm: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::porous::bioclogging_porosity_change(phi0, biomass, rho_biofilm)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Effective thermal conductivity of a saturated porous medium (geometric +/// mean mixing): k_eff = k_s^(1-phi) k_f^phi. +/// +/// Rust: `cfd::porous::effective_thermal_conductivity_porous` +#[pyfunction] +#[pyo3(name = "effective_thermal_conductivity_porous", signature = (k_s, k_f, porosity))] +pub fn pyfn_effective_thermal_conductivity_porous(k_s: f64, k_f: f64, porosity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::porous::effective_thermal_conductivity_porous(k_s, k_f, porosity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Gravity number: ratio of gravity to viscous forces in porous flow +/// (used in the tests for scaling sanity). +/// +/// Rust: `cfd::porous::gravity_number` +#[pyfunction] +#[pyo3(name = "gravity_number", signature = (k, rho, mu, u))] +pub fn pyfn_gravity_number(k: f64, rho: f64, mu: f64, u: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::porous::gravity_number(k, rho, mu, u)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_darcy_velocity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_darcy_flow_rate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_permeability_kozeny_carman, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_carman_kozeny_fibers, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ergun_pressure_drop, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_forchheimer, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_brinkman_velocity_profile, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hydraulic_conductivity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_brooks_corey, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_richards_equation_1d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_theis_drawdown, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_thiem_steady, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dupuit_unconfined, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_groundwater_flow_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_peclet_porous, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dispersion_coefficient, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_advection_dispersion_1d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ogata_banks, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_capillary_pressure_leverett, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_relative_permeability_corey, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_buckley_leverett, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bioclogging_porosity_change, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_effective_thermal_conductivity_porous, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gravity_number, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_cfd__potential_flow.rs b/bindings/python/src/generated/m_cfd__potential_flow.rs new file mode 100644 index 0000000..943e774 --- /dev/null +++ b/bindings/python/src/generated/m_cfd__potential_flow.rs @@ -0,0 +1,322 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Flow past a circular cylinder with circulation Γ. +/// +/// Rust: `cfd::potential_flow::cylinder_flow` +#[pyfunction] +#[pyo3(name = "cylinder_flow", signature = (u_inf, r, gamma))] +pub fn pyfn_cylinder_flow(u_inf: f64, r: f64, gamma: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::potential_flow::cylinder_flow(u_inf, r, gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPotentialFlow2 { inner: __v }) +} + +/// Rankine oval: source and sink of strength m at (±a, 0) in a stream. +/// +/// Rust: `cfd::potential_flow::rankine_oval` +#[pyfunction] +#[pyo3(name = "rankine_oval", signature = (u, m, a))] +pub fn pyfn_rankine_oval(u: f64, m: f64, a: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::potential_flow::rankine_oval(u, m, a)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPotentialFlow2 { inner: __v }) +} + +/// Exact surface pressure coefficient of the rotating cylinder +/// (Γ counterclockwise-positive, matching `Element::Vortex`). +/// +/// Rust: `cfd::potential_flow::cylinder_cp_exact` +#[pyfunction] +#[pyo3(name = "cylinder_cp_exact", signature = (theta, gamma, u, r))] +pub fn pyfn_cylinder_cp_exact(theta: f64, gamma: f64, u: f64, r: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::potential_flow::cylinder_cp_exact(theta, gamma, u, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Joukowski map ζ = z + c²/z. +/// +/// Rust: `cfd::potential_flow::joukowski_transform` +#[pyfunction] +#[pyo3(name = "joukowski_transform", signature = (z, c))] +pub fn pyfn_joukowski_transform<'py>(py: Python<'py>, z: crate::runtime::coerce::ComplexArg, c: f64) -> PyResult> { + let z = z.0; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::potential_flow::joukowski_transform(z, c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::complex_out(py, __v)) +} + +/// Inverse Joukowski map (branch with |z| ≥ c). +/// +/// Rust: `cfd::potential_flow::inverse_joukowski` +#[pyfunction] +#[pyo3(name = "inverse_joukowski", signature = (zeta, c))] +pub fn pyfn_inverse_joukowski<'py>(py: Python<'py>, zeta: crate::runtime::coerce::ComplexArg, c: f64) -> PyResult> { + let zeta = zeta.0; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::potential_flow::inverse_joukowski(zeta, c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::complex_out(py, __v)) +} + +/// Joukowski airfoil: image of the circle through (c, 0) centered at +/// `center`. +/// +/// Rust: `cfd::potential_flow::joukowski_airfoil` +#[pyfunction] +#[pyo3(name = "joukowski_airfoil", signature = (center, c, n_points))] +pub fn pyfn_joukowski_airfoil(center: crate::runtime::coerce::ComplexArg, c: f64, n_points: usize) -> PyResult> { + let center = center.0; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::potential_flow::joukowski_airfoil(center, c, n_points)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// Joukowski airfoil with the Kutta condition: returns (surface points, +/// surface cp, lift coefficient). +/// +/// Rust: `cfd::potential_flow::joukowski_airfoil_flow` +#[pyfunction] +#[pyo3(name = "joukowski_airfoil_flow", signature = (center, c, alpha, u_inf))] +pub fn pyfn_joukowski_airfoil_flow(center: crate::runtime::coerce::ComplexArg, c: f64, alpha: f64, u_inf: f64) -> PyResult<(Vec, Vec, f64)> { + let center = center.0; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::potential_flow::joukowski_airfoil_flow(center, c, alpha, u_inf)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>(), __v.1, __v.2)) +} + +/// Karman-Trefftz airfoil (finite trailing-edge angle set by `n_exp` +/// slightly below 2). +/// +/// Rust: `cfd::potential_flow::karman_trefftz_airfoil` +#[pyfunction] +#[pyo3(name = "karman_trefftz_airfoil", signature = (center, c, n_exp, n_points))] +pub fn pyfn_karman_trefftz_airfoil(center: crate::runtime::coerce::ComplexArg, c: f64, n_exp: f64, n_points: usize) -> PyResult> { + let center = center.0; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::potential_flow::karman_trefftz_airfoil(center, c, n_exp, n_points)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// NACA 4-digit airfoil ("2412" etc.), chord 1, from the trailing edge +/// over the top and back along the bottom. +/// +/// Rust: `cfd::potential_flow::naca4` +#[pyfunction] +#[pyo3(name = "naca4", signature = (code, n_points, closed_te))] +pub fn pyfn_naca4(code: String, n_points: usize, closed_te: bool) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::potential_flow::naca4(&code, n_points, closed_te)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// NACA 5-digit airfoil ("23012" etc.). +/// +/// Rust: `cfd::potential_flow::naca5` +#[pyfunction] +#[pyo3(name = "naca5", signature = (code, n_points))] +pub fn pyfn_naca5(code: String, n_points: usize) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::potential_flow::naca5(&code, n_points)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// Thin-airfoil lift coefficient for a camber-line slope dz/dx given on +/// x ∈ [0, 1]: cl = 2π(α − α_L0). +/// +/// Rust: `cfd::potential_flow::thin_airfoil_cl` +#[pyfunction] +#[pyo3(name = "thin_airfoil_cl", signature = (alpha, camber_slope))] +pub fn pyfn_thin_airfoil_cl(alpha: f64, camber_slope: pyo3::Py) -> PyResult { + let __cb_camber_slope = std::rc::Rc::new(crate::runtime::Callback::new(camber_slope)); + let camber_slope = { let __cb = __cb_camber_slope.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::potential_flow::thin_airfoil_cl(alpha, &camber_slope)); + crate::runtime::callback::check(&[&__cb_camber_slope], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Flat-plate thin-airfoil lift 2πα. +/// +/// Rust: `cfd::potential_flow::thin_airfoil_cl_flat` +#[pyfunction] +#[pyo3(name = "thin_airfoil_cl_flat", signature = (alpha))] +pub fn pyfn_thin_airfoil_cl_flat(alpha: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::potential_flow::thin_airfoil_cl_flat(alpha)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Prandtl lifting line with a Fourier sine series: returns +/// (CL, CDi, circulation at the collocation stations). +/// +/// Rust: `cfd::potential_flow::lifting_line` +#[pyfunction] +#[pyo3(name = "lifting_line", signature = (span, chord, alpha, n_terms, u_inf))] +pub fn pyfn_lifting_line(span: f64, chord: pyo3::Py, alpha: pyo3::Py, n_terms: usize, u_inf: f64) -> PyResult<(f64, f64, Vec)> { + let __cb_chord = std::rc::Rc::new(crate::runtime::Callback::new(chord)); + let chord = { let __cb = __cb_chord.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __cb_alpha = std::rc::Rc::new(crate::runtime::Callback::new(alpha)); + let alpha = { let __cb = __cb_alpha.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::potential_flow::lifting_line(span, &chord, &alpha, n_terms, u_inf)); + crate::runtime::callback::check(&[&__cb_chord, &__cb_alpha], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// Elliptic-wing lift slope: CL = 2πα/(1 + 2/AR). +/// +/// Rust: `cfd::potential_flow::elliptic_wing_cl` +#[pyfunction] +#[pyo3(name = "elliptic_wing_cl", signature = (ar, alpha))] +pub fn pyfn_elliptic_wing_cl(ar: f64, alpha: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::potential_flow::elliptic_wing_cl(ar, alpha)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Induced drag CL²/(π e AR). +/// +/// Rust: `cfd::potential_flow::induced_drag` +#[pyfunction] +#[pyo3(name = "induced_drag", signature = (cl, ar, e))] +pub fn pyfn_induced_drag(cl: f64, ar: f64, e: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::potential_flow::induced_drag(cl, ar, e)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Raymer's straight-wing Oswald efficiency estimate with a sweep +/// correction. +/// +/// Rust: `cfd::potential_flow::oswald_efficiency_estimate` +#[pyfunction] +#[pyo3(name = "oswald_efficiency_estimate", signature = (ar, sweep))] +pub fn pyfn_oswald_efficiency_estimate(ar: f64, sweep: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::potential_flow::oswald_efficiency_estimate(ar, sweep)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Single-lattice-row vortex lattice method (horseshoe vortices at the +/// quarter chord, collocation at 3/4 chord): returns (CL, CDi, +/// circulation per strip). +/// +/// Rust: `cfd::potential_flow::vortex_lattice` +#[pyfunction] +#[pyo3(name = "vortex_lattice", signature = (wing, alpha, n_span, n_chord, u_inf))] +pub fn pyfn_vortex_lattice(wing: crate::generated::types::PyWingGeometryArg, alpha: f64, n_span: usize, n_chord: usize, u_inf: f64) -> PyResult<(f64, f64, Vec)> { + let wing = wing.0; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::potential_flow::vortex_lattice(&wing, alpha, n_span, n_chord, u_inf)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// McCormick ground-effect induced-drag factor (16 h/b)²/(1 + (16 h/b)²). +/// +/// Rust: `cfd::potential_flow::ground_effect_factor` +#[pyfunction] +#[pyo3(name = "ground_effect_factor", signature = (h_over_b))] +pub fn pyfn_ground_effect_factor(h_over_b: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::potential_flow::ground_effect_factor(h_over_b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Velocity of a base flow seen through a conformal map at the physical +/// point z (numerical dW/dζ via the chain rule). +/// +/// Rust: `cfd::potential_flow::conformal_map_flow` +#[pyfunction] +#[pyo3(name = "conformal_map_flow", signature = (map, base, z))] +pub fn pyfn_conformal_map_flow(map: pyo3::Py, base: pyo3::PyRef<'_, crate::generated::types::PyPotentialFlow2>, z: crate::runtime::coerce::ComplexArg) -> PyResult { + let __cb_map = std::rc::Rc::new(crate::runtime::Callback::new(map)); + let map = { let __cb = __cb_map.clone(); move |__a0: rust_physics_engine::fractals::Complex| -> rust_physics_engine::fractals::Complex { { let __r = __cb.call::<_, crate::runtime::coerce::ComplexArg>((crate::runtime::coerce::Cx(__a0),), crate::runtime::coerce::ComplexArg(rust_physics_engine::fractals::Complex::new(f64::NAN, f64::NAN))); __r.0 } } }; + let z = z.0; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::potential_flow::conformal_map_flow(&map, &base.inner, z)); + crate::runtime::callback::check(&[&__cb_map], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) +} + +/// Mirror every element across a wall (method of images). +/// +/// Rust: `cfd::potential_flow::method_of_images_wall` +#[pyfunction] +#[pyo3(name = "method_of_images_wall", signature = (elements, wall))] +pub fn pyfn_method_of_images_wall(elements: Vec, wall: crate::generated::types::PyPlane2) -> PyResult { + let elements = elements.into_iter().map(|__e| __e.inner).collect::>(); + let wall = wall.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::potential_flow::method_of_images_wall(&elements, wall)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPotentialFlow2 { inner: __v }) +} + +/// Added mass of an accelerating cylinder per unit length ρπr². +/// +/// Rust: `cfd::potential_flow::added_mass_cylinder` +#[pyfunction] +#[pyo3(name = "added_mass_cylinder", signature = (rho, r))] +pub fn pyfn_added_mass_cylinder(rho: f64, r: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::potential_flow::added_mass_cylinder(rho, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Added mass of a sphere (2/3)ρπr³. +/// +/// Rust: `cfd::potential_flow::added_mass_sphere` +#[pyfunction] +#[pyo3(name = "added_mass_sphere", signature = (rho, r))] +pub fn pyfn_added_mass_sphere(rho: f64, r: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::potential_flow::added_mass_sphere(rho, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_cylinder_flow, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rankine_oval, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cylinder_cp_exact, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_joukowski_transform, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_inverse_joukowski, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_joukowski_airfoil, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_joukowski_airfoil_flow, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_karman_trefftz_airfoil, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_naca4, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_naca5, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_thin_airfoil_cl, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_thin_airfoil_cl_flat, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lifting_line, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_elliptic_wing_cl, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_induced_drag, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_oswald_efficiency_estimate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_vortex_lattice, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ground_effect_factor, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_conformal_map_flow, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_method_of_images_wall, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_added_mass_cylinder, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_added_mass_sphere, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_cfd__riemann.rs b/bindings/python/src/generated/m_cfd__riemann.rs new file mode 100644 index 0000000..782aa3c --- /dev/null +++ b/bindings/python/src/generated/m_cfd__riemann.rs @@ -0,0 +1,371 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Primitive → conserved. +/// +/// Rust: `cfd::riemann::prim_to_cons` +#[pyfunction] +#[pyo3(name = "prim_to_cons", signature = (p, gamma))] +pub fn pyfn_prim_to_cons(p: crate::generated::types::PyPrimArg, gamma: f64) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::riemann::prim_to_cons(p, gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCons { inner: __v }) +} + +/// Conserved → primitive. +/// +/// Rust: `cfd::riemann::cons_to_prim` +#[pyfunction] +#[pyo3(name = "cons_to_prim", signature = (c, gamma))] +pub fn pyfn_cons_to_prim(c: crate::generated::types::PyConsArg, gamma: f64) -> PyResult { + let c = c.0; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::riemann::cons_to_prim(c, gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPrim { inner: __v }) +} + +/// Physical flux F(U). +/// +/// Rust: `cfd::riemann::flux` +#[pyfunction] +#[pyo3(name = "flux", signature = (c, gamma))] +pub fn pyfn_flux(c: crate::generated::types::PyConsArg, gamma: f64) -> PyResult { + let c = c.0; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::riemann::flux(c, gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCons { inner: __v }) +} + +/// Speed of sound √(γp/ρ). +/// +/// Rust: `cfd::riemann::sound_speed` +#[pyfunction] +#[pyo3(name = "sound_speed", signature = (p, gamma))] +pub fn pyfn_sound_speed(p: crate::generated::types::PyPrimArg, gamma: f64) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::riemann::sound_speed(p, gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Star-region pressure and velocity of the exact Riemann problem +/// (Newton iteration, Toro ch. 4). +/// +/// Rust: `cfd::riemann::riemann_exact_star` +#[pyfunction] +#[pyo3(name = "riemann_exact_star", signature = (l, r, gamma))] +pub fn pyfn_riemann_exact_star(l: crate::generated::types::PyPrimArg, r: crate::generated::types::PyPrimArg, gamma: f64) -> PyResult<(f64, f64)> { + let l = l.0; + let r = r.0; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::riemann::riemann_exact_star(l, r, gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Sample the exact Riemann solution at similarity coordinate ξ = x/t. +/// +/// Rust: `cfd::riemann::riemann_exact` +#[pyfunction] +#[pyo3(name = "riemann_exact", signature = (l, r, gamma, x_over_t))] +pub fn pyfn_riemann_exact(l: crate::generated::types::PyPrimArg, r: crate::generated::types::PyPrimArg, gamma: f64, x_over_t: f64) -> PyResult { + let l = l.0; + let r = r.0; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::riemann::riemann_exact(l, r, gamma, x_over_t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPrim { inner: __v }) +} + +/// Einfeldt (Roe-averaged) wave speed bounds (S_L, S_R). +/// +/// Rust: `cfd::riemann::wave_speeds_einfeldt` +#[pyfunction] +#[pyo3(name = "wave_speeds_einfeldt", signature = (l, r, gamma))] +pub fn pyfn_wave_speeds_einfeldt(l: crate::generated::types::PyConsArg, r: crate::generated::types::PyConsArg, gamma: f64) -> PyResult<(f64, f64)> { + let l = l.0; + let r = r.0; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::riemann::wave_speeds_einfeldt(l, r, gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// HLL flux. +/// +/// Rust: `cfd::riemann::flux_hll` +#[pyfunction] +#[pyo3(name = "flux_hll", signature = (l, r, gamma))] +pub fn pyfn_flux_hll(l: crate::generated::types::PyConsArg, r: crate::generated::types::PyConsArg, gamma: f64) -> PyResult { + let l = l.0; + let r = r.0; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::riemann::flux_hll(l, r, gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCons { inner: __v }) +} + +/// HLLC flux (restores the contact wave). +/// +/// Rust: `cfd::riemann::flux_hllc` +#[pyfunction] +#[pyo3(name = "flux_hllc", signature = (l, r, gamma))] +pub fn pyfn_flux_hllc(l: crate::generated::types::PyConsArg, r: crate::generated::types::PyConsArg, gamma: f64) -> PyResult { + let l = l.0; + let r = r.0; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::riemann::flux_hllc(l, r, gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCons { inner: __v }) +} + +/// Roe flux with a Harten entropy fix. +/// +/// Rust: `cfd::riemann::flux_roe` +#[pyfunction] +#[pyo3(name = "flux_roe", signature = (l, r, gamma))] +pub fn pyfn_flux_roe(l: crate::generated::types::PyConsArg, r: crate::generated::types::PyConsArg, gamma: f64) -> PyResult { + let l = l.0; + let r = r.0; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::riemann::flux_roe(l, r, gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCons { inner: __v }) +} + +/// Rusanov (local Lax-Friedrichs) flux. +/// +/// Rust: `cfd::riemann::flux_rusanov` +#[pyfunction] +#[pyo3(name = "flux_rusanov", signature = (l, r, gamma))] +pub fn pyfn_flux_rusanov(l: crate::generated::types::PyConsArg, r: crate::generated::types::PyConsArg, gamma: f64) -> PyResult { + let l = l.0; + let r = r.0; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::riemann::flux_rusanov(l, r, gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCons { inner: __v }) +} + +/// AUSM+ flux (Liou 1996). +/// +/// Rust: `cfd::riemann::flux_ausm_plus` +#[pyfunction] +#[pyo3(name = "flux_ausm_plus", signature = (l, r, gamma))] +pub fn pyfn_flux_ausm_plus(l: crate::generated::types::PyConsArg, r: crate::generated::types::PyConsArg, gamma: f64) -> PyResult { + let l = l.0; + let r = r.0; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::riemann::flux_ausm_plus(l, r, gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCons { inner: __v }) +} + +/// Sod's shock tube on n cells (γ = 1.4, unit domain). +/// +/// Rust: `cfd::riemann::sod_shock_tube` +#[pyfunction] +#[pyo3(name = "sod_shock_tube", signature = (n))] +pub fn pyfn_sod_shock_tube(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::riemann::sod_shock_tube(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyEuler1D { inner: __v }) +} + +/// Lax's problem. +/// +/// Rust: `cfd::riemann::lax_problem` +#[pyfunction] +#[pyo3(name = "lax_problem", signature = (n))] +pub fn pyfn_lax_problem(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::riemann::lax_problem(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyEuler1D { inner: __v }) +} + +/// Shu-Osher shock/entropy-wave interaction. +/// +/// Rust: `cfd::riemann::shu_osher` +#[pyfunction] +#[pyo3(name = "shu_osher", signature = (n))] +pub fn pyfn_shu_osher(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::riemann::shu_osher(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyEuler1D { inner: __v }) +} + +/// Woodward-Colella interacting blast waves (reflective walls). +/// +/// Rust: `cfd::riemann::blast_wave_woodward_colella` +#[pyfunction] +#[pyo3(name = "blast_wave_woodward_colella", signature = (n))] +pub fn pyfn_blast_wave_woodward_colella(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::riemann::blast_wave_woodward_colella(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyEuler1D { inner: __v }) +} + +/// Sedov point blast in 1D planar symmetry. +/// +/// Rust: `cfd::riemann::sedov_1d` +#[pyfunction] +#[pyo3(name = "sedov_1d", signature = (n))] +pub fn pyfn_sedov_1d(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::riemann::sedov_1d(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyEuler1D { inner: __v }) +} + +/// Exact Sod solution at (x, t) on the unit domain split at 0.5. +/// +/// Rust: `cfd::riemann::sod_exact` +#[pyfunction] +#[pyo3(name = "sod_exact", signature = (x, t))] +pub fn pyfn_sod_exact(x: f64, t: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::riemann::sod_exact(x, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPrim { inner: __v }) +} + +/// Post-shock primitive state behind a normal shock of Mach `mach` +/// moving into gas at (p1, rho1) at rest (lab frame). +/// +/// Rust: `cfd::riemann::rankine_hugoniot` +#[pyfunction] +#[pyo3(name = "rankine_hugoniot", signature = (p1, rho1, mach, gamma))] +pub fn pyfn_rankine_hugoniot(p1: f64, rho1: f64, mach: f64, gamma: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::riemann::rankine_hugoniot(p1, rho1, mach, gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPrim { inner: __v }) +} + +/// Normal shock relations: (p2/p1, ρ2/ρ1, T2/T1, M2). +/// +/// Rust: `cfd::riemann::normal_shock_relations` +#[pyfunction] +#[pyo3(name = "normal_shock_relations", signature = (mach, gamma))] +pub fn pyfn_normal_shock_relations(mach: f64, gamma: f64) -> PyResult<(f64, f64, f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::riemann::normal_shock_relations(mach, gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2, __v.3)) +} + +/// Oblique shock wave angles (weak, strong) in radians for a flow +/// deflection; `None` if the deflection exceeds the maximum attached +/// angle. +/// +/// Rust: `cfd::riemann::oblique_shock_angle` +#[pyfunction] +#[pyo3(name = "oblique_shock_angle", signature = (mach, deflection, gamma))] +pub fn pyfn_oblique_shock_angle(mach: f64, deflection: f64, gamma: f64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::riemann::oblique_shock_angle(mach, deflection, gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| (__x.0, __x.1))) +} + +/// Prandtl-Meyer function ν(M) in radians. +/// +/// Rust: `cfd::riemann::prandtl_meyer` +#[pyfunction] +#[pyo3(name = "prandtl_meyer", signature = (mach, gamma))] +pub fn pyfn_prandtl_meyer(mach: f64, gamma: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::riemann::prandtl_meyer(mach, gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Isentropic area ratio A/A* for a given Mach number. +/// +/// Rust: `cfd::riemann::nozzle_area_ratio` +#[pyfunction] +#[pyo3(name = "nozzle_area_ratio", signature = (mach, gamma))] +pub fn pyfn_nozzle_area_ratio(mach: f64, gamma: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::riemann::nozzle_area_ratio(mach, gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Invert the area ratio for the subsonic or supersonic branch. +/// +/// Rust: `cfd::riemann::nozzle_mach_from_area` +#[pyfunction] +#[pyo3(name = "nozzle_mach_from_area", signature = (ratio, gamma, supersonic))] +pub fn pyfn_nozzle_mach_from_area(ratio: f64, gamma: f64, supersonic: bool) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::riemann::nozzle_mach_from_area(ratio, gamma, supersonic)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Quasi-1D isentropic nozzle solution: `area(x)` on x ∈ [0, 1] with a +/// single interior throat; chooses the supersonic branch downstream when +/// `p_exit` is below the critical exit pressure. Returns primitives at +/// `n` stations (R = 287 J/kg·K). +/// +/// Rust: `cfd::riemann::quasi_1d_nozzle` +#[pyfunction] +#[pyo3(name = "quasi_1d_nozzle", signature = (area, n, p0, t0, p_exit, gamma))] +pub fn pyfn_quasi_1d_nozzle(area: pyo3::Py, n: usize, p0: f64, t0: f64, p_exit: f64, gamma: f64) -> PyResult> { + let __cb_area = std::rc::Rc::new(crate::runtime::Callback::new(area)); + let area = { let __cb = __cb_area.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::riemann::quasi_1d_nozzle(&area, n, p0, t0, p_exit, gamma)); + crate::runtime::callback::check(&[&__cb_area], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyPrim { inner: __x }).collect::>()) +} + +/// Isentropic vortex (strength 5) advected by a uniform (1, 1) +/// background on a 10 × 10 periodic domain: returns (ρ, u, v, p). +/// +/// Rust: `cfd::riemann::isentropic_vortex_exact` +#[pyfunction] +#[pyo3(name = "isentropic_vortex_exact", signature = (x, y, t, gamma))] +pub fn pyfn_isentropic_vortex_exact(x: f64, y: f64, t: f64, gamma: f64) -> PyResult<(f64, f64, f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::riemann::isentropic_vortex_exact(x, y, t, gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2, __v.3)) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_prim_to_cons, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cons_to_prim, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_flux, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sound_speed, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_riemann_exact_star, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_riemann_exact, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wave_speeds_einfeldt, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_flux_hll, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_flux_hllc, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_flux_roe, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_flux_rusanov, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_flux_ausm_plus, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sod_shock_tube, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lax_problem, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_shu_osher, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_blast_wave_woodward_colella, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sedov_1d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sod_exact, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rankine_hugoniot, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_normal_shock_relations, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_oblique_shock_angle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_prandtl_meyer, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_nozzle_area_ratio, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_nozzle_mach_from_area, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_quasi_1d_nozzle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_isentropic_vortex_exact, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_cfd__shallow_water.rs b/bindings/python/src/generated/m_cfd__shallow_water.rs new file mode 100644 index 0000000..a73327e --- /dev/null +++ b/bindings/python/src/generated/m_cfd__shallow_water.rs @@ -0,0 +1,212 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Stoker's exact wet-bed dam-break solution: (h, u) at position x +/// (dam at x = 0) and time t. +/// +/// Rust: `cfd::shallow_water::swe_1d_exact_dam_break` +#[pyfunction] +#[pyo3(name = "swe_1d_exact_dam_break", signature = (x, t, h_l, h_r, g))] +pub fn pyfn_swe_1d_exact_dam_break(x: f64, t: f64, h_l: f64, h_r: f64, g: f64) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::shallow_water::swe_1d_exact_dam_break(x, t, h_l, h_r, g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// 1D well-balanced SWE step on columns with bathymetry (helper used by +/// the tsunami run-up model and `ShallowWater1D::step_hll`). +/// +/// Rust: `cfd::shallow_water::swe_1d_step_hll` +#[pyfunction] +#[pyo3(name = "swe_1d_step_hll", signature = (h, hu, b, dx, g, dt, dry, reflective))] +pub fn pyfn_swe_1d_step_hll<'py>(h: pyo3::Bound<'py, pyo3::PyAny>, hu: pyo3::Bound<'py, pyo3::PyAny>, b: Vec, dx: f64, g: f64, dt: f64, dry: f64, reflective: bool) -> PyResult<()> { + let mut h__v: Vec = h.extract()?; + let mut hu__v: Vec = hu.extract()?; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::shallow_water::swe_1d_step_hll(&mut h__v, &mut hu__v, &b, dx, g, dt, dry, reflective)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + crate::runtime::coerce::write_back(&h, &h__v)?; + crate::runtime::coerce::write_back(&hu, &hu__v)?; + Ok(()) +} + +/// Tsunami run-up on a plane beach of slope `slope`: an offshore +/// Gaussian wave of amplitude `a` (width `sigma`, centered at world +/// x = x0) propagates onto the beach. Returns the shoreline position +/// sampled at each of `n_samples` uniform times up to `t_end`. +/// +/// Rust: `cfd::shallow_water::tsunami_runup_1d` +#[pyfunction] +#[pyo3(name = "tsunami_runup_1d", signature = (slope, a, sigma, x0, n, domain, t_end, n_samples))] +pub fn pyfn_tsunami_runup_1d<'py>(py: Python<'py>, slope: f64, a: f64, sigma: f64, x0: f64, n: usize, domain: f64, t_end: f64, n_samples: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::cfd::shallow_water::tsunami_runup_1d(slope, a, sigma, x0, n, domain, t_end, n_samples))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Shallow-water wave speed √(gh). +/// +/// Rust: `cfd::shallow_water::wave_speed_shallow` +#[pyfunction] +#[pyo3(name = "wave_speed_shallow", signature = (h, g))] +pub fn pyfn_wave_speed_shallow(h: f64, g: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::shallow_water::wave_speed_shallow(h, g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Shallow-water dispersion ω = k√(gh). +/// +/// Rust: `cfd::shallow_water::dispersion_shallow` +#[pyfunction] +#[pyo3(name = "dispersion_shallow", signature = (k, h, g))] +pub fn pyfn_dispersion_shallow(k: f64, h: f64, g: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::shallow_water::dispersion_shallow(k, h, g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Deep-water dispersion ω = √(gk). +/// +/// Rust: `cfd::shallow_water::dispersion_deep` +#[pyfunction] +#[pyo3(name = "dispersion_deep", signature = (k, g))] +pub fn pyfn_dispersion_deep(k: f64, g: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::shallow_water::dispersion_deep(k, g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Full linear dispersion ω = √(gk tanh(kh)). +/// +/// Rust: `cfd::shallow_water::dispersion_full` +#[pyfunction] +#[pyo3(name = "dispersion_full", signature = (k, h, g))] +pub fn pyfn_dispersion_full(k: f64, h: f64, g: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::shallow_water::dispersion_full(k, h, g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Stokes drift at the surface: U = a²ωk cosh(2kh)/(2 sinh²(kh)). +/// +/// Rust: `cfd::shallow_water::stokes_drift` +#[pyfunction] +#[pyo3(name = "stokes_drift", signature = (a, k, h, g))] +pub fn pyfn_stokes_drift(a: f64, k: f64, h: f64, g: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::shallow_water::stokes_drift(a, k, h, g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Miche/steepness breaking criterion: waves break when H/λ exceeds +/// 1/7. +/// +/// Rust: `cfd::shallow_water::wave_breaking_criterion` +#[pyfunction] +#[pyo3(name = "wave_breaking_criterion", signature = (h_over_lambda))] +pub fn pyfn_wave_breaking_criterion(h_over_lambda: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::shallow_water::wave_breaking_criterion(h_over_lambda)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// JONSWAP spectrum S(f) (m²/Hz) for significant wave height `hs`, peak +/// period `tp`, and peak-enhancement `gamma` (≈3.3), normalized so that +/// ∫S df = hs²/16. +/// +/// Rust: `cfd::shallow_water::jonswap_spectrum` +#[pyfunction] +#[pyo3(name = "jonswap_spectrum", signature = (f, hs, tp, gamma))] +pub fn pyfn_jonswap_spectrum(f: f64, hs: f64, tp: f64, gamma: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::shallow_water::jonswap_spectrum(f, hs, tp, gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Pierson-Moskowitz spectrum for wind speed `u10` (m/s) at 10 m. +/// +/// Rust: `cfd::shallow_water::pierson_moskowitz` +#[pyfunction] +#[pyo3(name = "pierson_moskowitz", signature = (f, u10))] +pub fn pyfn_pierson_moskowitz(f: f64, u10: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::shallow_water::pierson_moskowitz(f, u10)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Synthesize a sea-surface elevation time series from a one-sided +/// spectrum (random phases): `n` samples at rate `fs`. +/// +/// Rust: `cfd::shallow_water::wave_field_from_spectrum` +#[pyfunction] +#[pyo3(name = "wave_field_from_spectrum", signature = (spectrum, n, fs, rng))] +pub fn pyfn_wave_field_from_spectrum(spectrum: pyo3::Py, n: usize, fs: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let __cb_spectrum = std::rc::Rc::new(crate::runtime::Callback::new(spectrum)); + let spectrum = { let __cb = __cb_spectrum.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::shallow_water::wave_field_from_spectrum(&spectrum, n, fs, &mut rng.inner)); + crate::runtime::callback::check(&[&__cb_spectrum], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Gerstner (trochoidal) wave displacement of the surface point whose +/// rest position is `p`, summing waves given as (amplitude, wavelength, +/// speed, direction). +/// +/// Rust: `cfd::shallow_water::gerstner_wave` +#[pyfunction] +#[pyo3(name = "gerstner_wave", signature = (p, t, waves))] +pub fn pyfn_gerstner_wave(p: crate::generated::types::PyVec2Arg, t: f64, waves: Vec<(f64, f64, f64, crate::generated::types::PyVec2Arg)>) -> PyResult { + let p = p.0; + let waves = waves.into_iter().map(|__e| (__e.0, __e.1, __e.2, __e.3.0)).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::shallow_water::gerstner_wave(p, t, &waves)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Kelvin ship-wake half angle arcsin(1/3) ≈ 19.47°. +/// +/// Rust: `cfd::shallow_water::kelvin_wake_angle` +#[pyfunction] +#[pyo3(name = "kelvin_wake_angle", signature = ())] +pub fn pyfn_kelvin_wake_angle() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::shallow_water::kelvin_wake_angle()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_swe_1d_exact_dam_break, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_swe_1d_step_hll, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tsunami_runup_1d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wave_speed_shallow, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dispersion_shallow, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dispersion_deep, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dispersion_full, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_stokes_drift, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wave_breaking_criterion, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_jonswap_spectrum, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pierson_moskowitz, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wave_field_from_spectrum, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gerstner_wave, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kelvin_wake_angle, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_cfd__sph.rs b/bindings/python/src/generated/m_cfd__sph.rs new file mode 100644 index 0000000..f71dbd0 --- /dev/null +++ b/bindings/python/src/generated/m_cfd__sph.rs @@ -0,0 +1,146 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Support radius (as a multiple of h) of each kernel. +/// +/// Rust: `cfd::sph::kernel_support` +#[pyfunction] +#[pyo3(name = "kernel_support", signature = (k))] +pub fn pyfn_kernel_support(k: crate::generated::types::PyKernel) -> PyResult { + let k = k.to_rust(); + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::sph::kernel_support(k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Kernel value W(r, h). +/// +/// Rust: `cfd::sph::kernel_w` +#[pyfunction] +#[pyo3(name = "kernel_w", signature = (k, r, h, dim))] +pub fn pyfn_kernel_w(k: crate::generated::types::PyKernel, r: f64, h: f64, dim: usize) -> PyResult { + let k = k.to_rust(); + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::sph::kernel_w(k, r, h, dim)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Kernel gradient ∇W (points from the neighbor toward decreasing W). +/// +/// Rust: `cfd::sph::kernel_grad` +#[pyfunction] +#[pyo3(name = "kernel_grad", signature = (k, r_vec, h, dim))] +pub fn pyfn_kernel_grad(k: crate::generated::types::PyKernel, r_vec: crate::generated::types::PyVec3Arg, h: f64, dim: usize) -> PyResult { + let k = k.to_rust(); + let r_vec = r_vec.0; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::sph::kernel_grad(k, r_vec, h, dim)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Radial Laplacian ∇²W = W'' + (dim−1)/r · W′ (the Müller viscosity +/// kernel returns its purpose-built positive Laplacian). +/// +/// Rust: `cfd::sph::kernel_laplacian` +#[pyfunction] +#[pyo3(name = "kernel_laplacian", signature = (k, r, h, dim))] +pub fn pyfn_kernel_laplacian(k: crate::generated::types::PyKernel, r: f64, h: f64, dim: usize) -> PyResult { + let k = k.to_rust(); + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::sph::kernel_laplacian(k, r, h, dim)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// 2D dam break: a water column of width `width` and height `height` in +/// a tank 4×width long. +/// +/// Rust: `cfd::sph::dam_break_2d` +#[pyfunction] +#[pyo3(name = "dam_break_2d", signature = (h, width, height))] +pub fn pyfn_dam_break_2d(h: f64, width: f64, height: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::sph::dam_break_2d(h, width, height)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySph { inner: __v }) +} + +/// Ritter's dry-bed dam-break front position x = 2 t √(g h0) (measured +/// from the dam). +/// +/// Rust: `cfd::sph::dam_break_exact_front` +#[pyfunction] +#[pyo3(name = "dam_break_exact_front", signature = (t, h0, g))] +pub fn pyfn_dam_break_exact_front(t: f64, h0: f64, g: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::sph::dam_break_exact_front(t, h0, g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Still tank of the given depth (hydrostatic pressure benchmark). +/// +/// Rust: `cfd::sph::hydrostatic_tank` +#[pyfunction] +#[pyo3(name = "hydrostatic_tank", signature = (h, width, depth))] +pub fn pyfn_hydrostatic_tank(h: f64, width: f64, depth: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::sph::hydrostatic_tank(h, width, depth)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySph { inner: __v }) +} + +/// Zero-gravity droplet with surface tension (Rayleigh oscillation +/// benchmark). +/// +/// Rust: `cfd::sph::droplet_oscillation` +#[pyfunction] +#[pyo3(name = "droplet_oscillation", signature = (h, radius, tension))] +pub fn pyfn_droplet_oscillation(h: f64, radius: f64, tension: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::sph::droplet_oscillation(h, radius, tension)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySph { inner: __v }) +} + +/// Body-force-driven planar channel (Poiseuille) flow between two walls. +/// +/// Rust: `cfd::sph::poiseuille_sph` +#[pyfunction] +#[pyo3(name = "poiseuille_sph", signature = (h, gap, force))] +pub fn pyfn_poiseuille_sph(h: f64, gap: f64, force: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::sph::poiseuille_sph(h, gap, force)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySph { inner: __v }) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_kernel_support, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kernel_w, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kernel_grad, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kernel_laplacian, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dam_break_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dam_break_exact_front, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hydrostatic_tank, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_droplet_oscillation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_poiseuille_sph, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_cfd__stable_fluids.rs b/bindings/python/src/generated/m_cfd__stable_fluids.rs new file mode 100644 index 0000000..0f39a6c --- /dev/null +++ b/bindings/python/src/generated/m_cfd__stable_fluids.rs @@ -0,0 +1,134 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Solve ∇²p = rhs with Neumann boundaries (and solid cells) by +/// conjugate gradients; the mean of `rhs` over fluid cells is removed +/// for compatibility and the solution has zero mean. +/// +/// Rust: `cfd::stable_fluids::pressure_poisson_cg` +#[pyfunction] +#[pyo3(name = "pressure_poisson_cg", signature = (div, solid, nx, ny, dx, tol, max_iter))] +pub fn pyfn_pressure_poisson_cg<'py>(py: Python<'py>, div: Vec, solid: Vec, nx: usize, ny: usize, dx: f64, tol: f64, max_iter: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::cfd::stable_fluids::pressure_poisson_cg(&div, &solid, nx, ny, dx, tol, max_iter))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// CG solve of the node-centered Neumann pressure system used by +/// `sim::fluid_sim::EulerFluid2D` (column-major layout +/// `i*ny + j`, mirror boundary nodes, anisotropic spacing). This is the +/// Part 3 rewire target for that solver's Poisson step. +/// +/// Rust: `cfd::stable_fluids::poisson_neumann_cg_rect` +#[pyfunction] +#[pyo3(name = "poisson_neumann_cg_rect", signature = (rhs, nx, ny, dx, dy, tol, max_iter))] +pub fn pyfn_poisson_neumann_cg_rect<'py>(py: Python<'py>, rhs: Vec, nx: usize, ny: usize, dx: f64, dy: f64, tol: f64, max_iter: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::cfd::stable_fluids::poisson_neumann_cg_rect(&rhs, nx, ny, dx, dy, tol, max_iter))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// One geometric multigrid V-cycle for the Dirichlet Poisson problem +/// ∇²u = rhs on the unit square (rhs is an n × n interior-node grid with +/// n = √len, spacing h = 1/(n+1)); returns the V-cycle approximation to +/// the solution from a zero initial guess. Iterate on the residual for a +/// full solve. +/// +/// Rust: `cfd::stable_fluids::multigrid_vcycle` +#[pyfunction] +#[pyo3(name = "multigrid_vcycle", signature = (rhs, levels, pre, post))] +pub fn pyfn_multigrid_vcycle<'py>(py: Python<'py>, rhs: Vec, levels: usize, pre: usize, post: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::cfd::stable_fluids::multigrid_vcycle(&rhs, levels, pre, post))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Lid-driven cavity at Reynolds number `re` on an n × n unit box, +/// stepped to `t_end` (lid speed 1). +/// +/// Rust: `cfd::stable_fluids::lid_driven_cavity` +#[pyfunction] +#[pyo3(name = "lid_driven_cavity", signature = (n, re, t_end))] +pub fn pyfn_lid_driven_cavity(n: usize, re: f64, t_end: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::stable_fluids::lid_driven_cavity(n, re, t_end)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyStableFluid2 { inner: __v }) +} + +/// Uniform inflow past a circular cylinder (diameter ~ny/5 cells) at +/// Reynolds number `re` (inflow speed 1). +/// +/// Rust: `cfd::stable_fluids::flow_past_cylinder` +#[pyfunction] +#[pyo3(name = "flow_past_cylinder", signature = (nx, ny, re))] +pub fn pyfn_flow_past_cylinder(nx: usize, ny: usize, re: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::stable_fluids::flow_past_cylinder(nx, ny, re)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyStableFluid2 { inner: __v }) +} + +/// Rayleigh-Bénard convection cell: Rayleigh number `ra`, Prandtl `pr`, +/// hot floor and cold ceiling encoded in the initial temperature. +/// +/// Rust: `cfd::stable_fluids::rayleigh_benard` +#[pyfunction] +#[pyo3(name = "rayleigh_benard", signature = (nx, ny, ra, pr))] +pub fn pyfn_rayleigh_benard(nx: usize, ny: usize, ra: f64, pr: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::stable_fluids::rayleigh_benard(nx, ny, ra, pr)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyStableFluid2 { inner: __v }) +} + +/// Taylor-Green vortex in a free-slip unit box: +/// u = sin(πx) cos(πy), v = −cos(πx) sin(πy), decaying as e^{−2π²νt}. +/// +/// Rust: `cfd::stable_fluids::taylor_green_vortex` +#[pyfunction] +#[pyo3(name = "taylor_green_vortex", signature = (n, nu))] +pub fn pyfn_taylor_green_vortex(n: usize, nu: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::stable_fluids::taylor_green_vortex(n, nu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyStableFluid2 { inner: __v }) +} + +/// Exact Taylor-Green velocity at (x, y, t). +/// +/// Rust: `cfd::stable_fluids::taylor_green_exact` +#[pyfunction] +#[pyo3(name = "taylor_green_exact", signature = (x, y, t, nu))] +pub fn pyfn_taylor_green_exact(x: f64, y: f64, t: f64, nu: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::stable_fluids::taylor_green_exact(x, y, t, nu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_pressure_poisson_cg, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_poisson_neumann_cg_rect, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_multigrid_vcycle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lid_driven_cavity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_flow_past_cylinder, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rayleigh_benard, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_taylor_green_vortex, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_taylor_green_exact, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_cfd__turbulence.rs b/bindings/python/src/generated/m_cfd__turbulence.rs new file mode 100644 index 0000000..a1d5629 --- /dev/null +++ b/bindings/python/src/generated/m_cfd__turbulence.rs @@ -0,0 +1,466 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Kolmogorov -5/3 inertial-range energy spectrum E(k) = C eps^{2/3} k^{-5/3}. +/// +/// Rust: `cfd::turbulence::kolmogorov_spectrum` +#[pyfunction] +#[pyo3(name = "kolmogorov_spectrum", signature = (k, dissipation))] +pub fn pyfn_kolmogorov_spectrum(k: f64, dissipation: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::turbulence::kolmogorov_spectrum(k, dissipation)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Kolmogorov length, time and velocity scales `(eta, tau, u)` from +/// kinematic viscosity and dissipation rate. +/// +/// Rust: `cfd::turbulence::kolmogorov_scales` +#[pyfunction] +#[pyo3(name = "kolmogorov_scales", signature = (nu, dissipation))] +pub fn pyfn_kolmogorov_scales(nu: f64, dissipation: f64) -> PyResult<(f64, f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::turbulence::kolmogorov_scales(nu, dissipation)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// Taylor microscale lambda = sqrt(15 nu u'^2 / eps). +/// +/// Rust: `cfd::turbulence::taylor_microscale` +#[pyfunction] +#[pyo3(name = "taylor_microscale", signature = (u_rms, nu, dissipation))] +pub fn pyfn_taylor_microscale(u_rms: f64, nu: f64, dissipation: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::turbulence::taylor_microscale(u_rms, nu, dissipation)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Integral length scale estimate L = u'^3 / eps. +/// +/// Rust: `cfd::turbulence::integral_scale` +#[pyfunction] +#[pyo3(name = "integral_scale", signature = (u_rms, dissipation))] +pub fn pyfn_integral_scale(u_rms: f64, dissipation: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::turbulence::integral_scale(u_rms, dissipation)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Taylor-microscale Reynolds number Re_lambda = u' lambda / nu. +/// +/// Rust: `cfd::turbulence::re_lambda` +#[pyfunction] +#[pyo3(name = "re_lambda", signature = (u_rms, nu, dissipation))] +pub fn pyfn_re_lambda(u_rms: f64, nu: f64, dissipation: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::turbulence::re_lambda(u_rms, nu, dissipation)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// One-dimensional energy spectrum of a periodic velocity signal sampled at +/// spacing `dx`. Returns `(k, e_k)` with k in rad per unit length. The sum of +/// `e_k` times dk equals half the mean-square fluctuation. +/// +/// Rust: `cfd::turbulence::energy_spectrum_1d` +#[pyfunction] +#[pyo3(name = "energy_spectrum_1d", signature = (u, dx))] +pub fn pyfn_energy_spectrum_1d<'py>(py: Python<'py>, u: Vec, dx: f64) -> PyResult<(Vec, Vec)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::cfd::turbulence::energy_spectrum_1d(&u, dx))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Shell-averaged 2D energy spectrum of a periodic (u, v) field on an +/// `n` x `n` grid with spacing `dx`. Returns `(k, e_k)` binned on integer +/// wavenumber shells. +/// +/// Rust: `cfd::turbulence::energy_spectrum_2d` +#[pyfunction] +#[pyo3(name = "energy_spectrum_2d", signature = (u, v, n, dx))] +pub fn pyfn_energy_spectrum_2d<'py>(py: Python<'py>, u: Vec, v: Vec, n: usize, dx: f64) -> PyResult<(Vec, Vec)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::cfd::turbulence::energy_spectrum_2d(&u, &v, n, dx))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Shell-averaged 3D energy spectrum of periodic (u, v, w) on an n^3 grid +/// (index `(k*n + j)*n + i`) with spacing `dx`. +/// +/// Rust: `cfd::turbulence::energy_spectrum_3d` +#[pyfunction] +#[pyo3(name = "energy_spectrum_3d", signature = (u, v, w, n, dx))] +pub fn pyfn_energy_spectrum_3d<'py>(py: Python<'py>, u: Vec, v: Vec, w: Vec, n: usize, dx: f64) -> PyResult<(Vec, Vec)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::cfd::turbulence::energy_spectrum_3d(&u, &v, &w, n, dx))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Dissipation rate from a spectrum: eps = 2 nu integral k^2 E(k) dk +/// (trapezoidal). +/// +/// Rust: `cfd::turbulence::dissipation_rate_from_spectrum` +#[pyfunction] +#[pyo3(name = "dissipation_rate_from_spectrum", signature = (k, e, nu))] +pub fn pyfn_dissipation_rate_from_spectrum<'py>(py: Python<'py>, k: Vec, e: Vec, nu: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::cfd::turbulence::dissipation_rate_from_spectrum(&k, &e, nu))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Longitudinal structure function of order `p` at separation `r` (in +/// samples times dx) for a periodic 1D signal: <|u(x+r) - u(x)|^p>. +/// +/// Rust: `cfd::turbulence::structure_function` +#[pyfunction] +#[pyo3(name = "structure_function", signature = (u, sep, order))] +pub fn pyfn_structure_function<'py>(py: Python<'py>, u: Vec, sep: usize, order: i32) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::cfd::turbulence::structure_function(&u, sep, order))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Two-point autocorrelation of a periodic 1D signal at separation `sep` +/// (samples), normalized to R(0) = 1. +/// +/// Rust: `cfd::turbulence::two_point_correlation` +#[pyfunction] +#[pyo3(name = "two_point_correlation", signature = (u, sep))] +pub fn pyfn_two_point_correlation<'py>(py: Python<'py>, u: Vec, sep: usize) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::cfd::turbulence::two_point_correlation(&u, sep))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Symmetric strain-rate tensor S = (grad u + grad u^T)/2 from a velocity +/// gradient tensor g where `g.data[i][j] = du_i/dx_j`. +/// +/// Rust: `cfd::turbulence::strain_tensor` +#[pyfunction] +#[pyo3(name = "strain_tensor", signature = (g))] +pub fn pyfn_strain_tensor(g: crate::generated::types::PyMat3) -> PyResult { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::turbulence::strain_tensor(&g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMat3 { inner: __v }) +} + +/// Antisymmetric rotation-rate tensor W = (grad u - grad u^T)/2. +/// +/// Rust: `cfd::turbulence::rotation_tensor` +#[pyfunction] +#[pyo3(name = "rotation_tensor", signature = (g))] +pub fn pyfn_rotation_tensor(g: crate::generated::types::PyMat3) -> PyResult { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::turbulence::rotation_tensor(&g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMat3 { inner: __v }) +} + +/// Smagorinsky eddy viscosity nu_t = (Cs Delta)^2 |S|, |S| = sqrt(2 S:S). +/// +/// Rust: `cfd::turbulence::smagorinsky_nu_t` +#[pyfunction] +#[pyo3(name = "smagorinsky_nu_t", signature = (g, delta, cs))] +pub fn pyfn_smagorinsky_nu_t(g: crate::generated::types::PyMat3, delta: f64, cs: f64) -> PyResult { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::turbulence::smagorinsky_nu_t(&g, delta, cs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Germano-Lilly dynamic Smagorinsky coefficient from resolved and +/// test-filtered fields. `l` is the Leonard stress tensor and `m` the model +/// difference tensor; returns Cs^2 = / clipped at zero. +/// +/// Rust: `cfd::turbulence::dynamic_smagorinsky_cs` +#[pyfunction] +#[pyo3(name = "dynamic_smagorinsky_cs", signature = (l, m))] +pub fn pyfn_dynamic_smagorinsky_cs(l: crate::generated::types::PyMat3, m: crate::generated::types::PyMat3) -> PyResult { + let l = l.inner; + let m = m.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::turbulence::dynamic_smagorinsky_cs(&l, &m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// WALE subgrid eddy viscosity (Nicoud & Ducros 1999) with constant `cw` +/// (typically 0.325 to 0.5). +/// +/// Rust: `cfd::turbulence::wale_nu_t` +#[pyfunction] +#[pyo3(name = "wale_nu_t", signature = (g, delta, cw))] +pub fn pyfn_wale_nu_t(g: crate::generated::types::PyMat3, delta: f64, cw: f64) -> PyResult { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::turbulence::wale_nu_t(&g, delta, cw)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Vreman subgrid eddy viscosity (Vreman 2004) with constant `c` +/// (approximately 2.5 Cs^2, so about 0.07). +/// +/// Rust: `cfd::turbulence::vreman_nu_t` +#[pyfunction] +#[pyo3(name = "vreman_nu_t", signature = (g, delta, c))] +pub fn pyfn_vreman_nu_t(g: crate::generated::types::PyMat3, delta: f64, c: f64) -> PyResult { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::turbulence::vreman_nu_t(&g, delta, c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Q-criterion: Q = (|W|^2 - |S|^2)/2. Positive Q marks vortical regions. +/// +/// Rust: `cfd::turbulence::q_criterion` +#[pyfunction] +#[pyo3(name = "q_criterion", signature = (g))] +pub fn pyfn_q_criterion(g: crate::generated::types::PyMat3) -> PyResult { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::turbulence::q_criterion(&g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Lambda-2 criterion: middle eigenvalue of S^2 + W^2. Negative values mark +/// vortex cores. +/// +/// Rust: `cfd::turbulence::lambda2_criterion` +#[pyfunction] +#[pyo3(name = "lambda2_criterion", signature = (g))] +pub fn pyfn_lambda2_criterion(g: crate::generated::types::PyMat3) -> PyResult { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::turbulence::lambda2_criterion(&g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Delta criterion: Delta = (Q/3)^3 + (det(g)/2)^2 > 0 marks complex +/// eigenvalues of the velocity gradient (swirling motion). +/// +/// Rust: `cfd::turbulence::delta_criterion` +#[pyfunction] +#[pyo3(name = "delta_criterion", signature = (g))] +pub fn pyfn_delta_criterion(g: crate::generated::types::PyMat3) -> PyResult { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::turbulence::delta_criterion(&g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Identify vortical cells in a 2D periodic velocity field on an n x n grid +/// (spacing `dx`) by the 2D Q-criterion; returns flags where Q > `threshold`. +/// +/// Rust: `cfd::turbulence::vortex_identify_q` +#[pyfunction] +#[pyo3(name = "vortex_identify_q", signature = (u, v, n, dx, threshold))] +pub fn pyfn_vortex_identify_q<'py>(py: Python<'py>, u: Vec, v: Vec, n: usize, dx: f64, threshold: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::cfd::turbulence::vortex_identify_q(&u, &v, n, dx, threshold))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Turbulence intensity: rms fluctuation over mean speed. +/// +/// Rust: `cfd::turbulence::turbulence_intensity` +#[pyfunction] +#[pyo3(name = "turbulence_intensity", signature = (u))] +pub fn pyfn_turbulence_intensity<'py>(py: Python<'py>, u: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::cfd::turbulence::turbulence_intensity(&u))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Reynolds stress component from paired samples. +/// +/// Rust: `cfd::turbulence::reynolds_stress` +#[pyfunction] +#[pyo3(name = "reynolds_stress", signature = (u, v))] +pub fn pyfn_reynolds_stress<'py>(py: Python<'py>, u: Vec, v: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::cfd::turbulence::reynolds_stress(&u, &v))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Divergence-free synthetic 2D turbulence (Kraichnan-style random Fourier +/// modes) on an n x n periodic grid of size `l`. Each of `n_modes` modes has a +/// random wavevector with magnitude near `k_peak` and an amplitude direction +/// perpendicular to it. Returns `(u, v)`. +/// +/// Rust: `cfd::turbulence::synthetic_turbulence_kraichnan` +#[pyfunction] +#[pyo3(name = "synthetic_turbulence_kraichnan", signature = (n, l, n_modes, k_peak, u_rms, seed))] +pub fn pyfn_synthetic_turbulence_kraichnan(n: usize, l: f64, n_modes: usize, k_peak: f64, u_rms: f64, seed: u64) -> PyResult<(Vec, Vec)> { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::turbulence::synthetic_turbulence_kraichnan(n, l, n_modes, k_peak, u_rms, seed)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Synthetic eddy method: superpose `n_eddies` compact Gaussian eddies with +/// random centers and signs in a periodic box of size `l`, sampled at `n_pts` +/// points along a line. Returns a fluctuation signal scaled to `u_rms`. +/// +/// Rust: `cfd::turbulence::synthetic_eddy_method` +#[pyfunction] +#[pyo3(name = "synthetic_eddy_method", signature = (n_pts, l, n_eddies, eddy_size, u_rms, seed))] +pub fn pyfn_synthetic_eddy_method<'py>(py: Python<'py>, n_pts: usize, l: f64, n_eddies: usize, eddy_size: f64, u_rms: f64, seed: u64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::cfd::turbulence::synthetic_eddy_method(n_pts, l, n_eddies, eddy_size, u_rms, seed))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Von Karman model spectrum with integral-scale wavenumber `ke` and +/// Kolmogorov cutoff `k_eta`: +/// E(k) ~ (k/ke)^4 / (1 + (k/ke)^2)^{17/6} * exp(-2 (k/k_eta)^2) scaled so +/// the peak region carries energy `k_energy` overall (approximate). +/// +/// Rust: `cfd::turbulence::von_karman_spectrum` +#[pyfunction] +#[pyo3(name = "von_karman_spectrum", signature = (k, k_energy, ke, k_eta))] +pub fn pyfn_von_karman_spectrum(k: f64, k_energy: f64, ke: f64, k_eta: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::turbulence::von_karman_spectrum(k, k_energy, ke, k_eta)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Pao dissipation-range spectrum: +/// E(k) = C eps^{2/3} k^{-5/3} exp(-1.5 C (k eta)^{4/3}). +/// +/// Rust: `cfd::turbulence::pao_spectrum` +#[pyfunction] +#[pyo3(name = "pao_spectrum", signature = (k, dissipation, nu))] +pub fn pyfn_pao_spectrum(k: f64, dissipation: f64, nu: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::turbulence::pao_spectrum(k, dissipation, nu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Fit the log law u+ = (1/kappa) ln y+ + B to a profile `(y, u)` for a fluid +/// of viscosity `nu`, returning `(u_tau, b)`. The friction velocity comes from +/// the slope of u versus ln y: u_tau = kappa * slope. +/// +/// Rust: `cfd::turbulence::log_law_fit` +#[pyfunction] +#[pyo3(name = "log_law_fit", signature = (y, u, nu))] +pub fn pyfn_log_law_fit<'py>(py: Python<'py>, y: Vec, u: Vec, nu: f64) -> PyResult<(f64, f64)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::cfd::turbulence::log_law_fit(&y, &u, nu))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Reference mean-velocity profile for turbulent channel flow at friction +/// Reynolds number `re_tau`: Reichardt's composite profile evaluated at +/// `n` points from the wall to the centerline. Returns `(y_plus, u_plus)`. +/// +/// Rust: `cfd::turbulence::channel_flow_dns_reference` +#[pyfunction] +#[pyo3(name = "channel_flow_dns_reference", signature = (re_tau, n))] +pub fn pyfn_channel_flow_dns_reference(re_tau: f64, n: usize) -> PyResult<(Vec, Vec)> { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::turbulence::channel_flow_dns_reference(re_tau, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Log-log slope of a spectrum over the wavenumber band `[k_lo, k_hi]` +/// (least squares). For an inertial range this returns about -5/3. +/// +/// Rust: `cfd::turbulence::inertial_range_exponent` +#[pyfunction] +#[pyo3(name = "inertial_range_exponent", signature = (k, e, k_lo, k_hi))] +pub fn pyfn_inertial_range_exponent<'py>(py: Python<'py>, k: Vec, e: Vec, k_lo: f64, k_hi: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::cfd::turbulence::inertial_range_exponent(&k, &e, k_lo, k_hi))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Eddy-turnover (cascade) time at scale `l` with velocity `u_l`: +/// tau = l / u_l. +/// +/// Rust: `cfd::turbulence::richardson_cascade_time` +#[pyfunction] +#[pyo3(name = "richardson_cascade_time", signature = (l, u_l))] +pub fn pyfn_richardson_cascade_time(l: f64, u_l: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::turbulence::richardson_cascade_time(l, u_l)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Turbulent diffusivity nu_t / Pr_t. +/// +/// Rust: `cfd::turbulence::turbulent_diffusivity` +#[pyfunction] +#[pyo3(name = "turbulent_diffusivity", signature = (nu_t, pr_t))] +pub fn pyfn_turbulent_diffusivity(nu_t: f64, pr_t: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::turbulence::turbulent_diffusivity(nu_t, pr_t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Set up a decaying isotropic turbulence run: a StableFluid3 on an n^3 +/// periodic box seeded with divergence-free random modes, advanced to +/// `t_end`. Returns the fluid for inspection. Keep `n` small (16 or so). +/// +/// Rust: `cfd::turbulence::decaying_isotropic_turbulence` +#[pyfunction] +#[pyo3(name = "decaying_isotropic_turbulence", signature = (n, nu, t_end))] +pub fn pyfn_decaying_isotropic_turbulence(n: usize, nu: f64, t_end: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::turbulence::decaying_isotropic_turbulence(n, nu, t_end)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyStableFluid3 { inner: __v }) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_kolmogorov_spectrum, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kolmogorov_scales, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_taylor_microscale, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_integral_scale, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_re_lambda, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_energy_spectrum_1d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_energy_spectrum_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_energy_spectrum_3d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dissipation_rate_from_spectrum, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_structure_function, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_two_point_correlation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_strain_tensor, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rotation_tensor, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_smagorinsky_nu_t, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dynamic_smagorinsky_cs, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wale_nu_t, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_vreman_nu_t, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_q_criterion, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lambda2_criterion, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_delta_criterion, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_vortex_identify_q, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_turbulence_intensity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_reynolds_stress, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_synthetic_turbulence_kraichnan, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_synthetic_eddy_method, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_von_karman_spectrum, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pao_spectrum, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_log_law_fit, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_channel_flow_dns_reference, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_inertial_range_exponent, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_richardson_cascade_time, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_turbulent_diffusivity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_decaying_isotropic_turbulence, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_cfd__vortex.rs b/bindings/python/src/generated/m_cfd__vortex.rs new file mode 100644 index 0000000..ba81c57 --- /dev/null +++ b/bindings/python/src/generated/m_cfd__vortex.rs @@ -0,0 +1,259 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Velocity induced at `p` by a straight vortex segment from `a` to `b` +/// carrying circulation `gamma`. +/// +/// Rust: `cfd::vortex::biot_savart_segment` +#[pyfunction] +#[pyo3(name = "biot_savart_segment", signature = (p, a, b, gamma))] +pub fn pyfn_biot_savart_segment(p: crate::generated::types::PyVec3Arg, a: crate::generated::types::PyVec3Arg, b: crate::generated::types::PyVec3Arg, gamma: f64) -> PyResult { + let p = p.0; + let a = a.0; + let b = b.0; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::vortex::biot_savart_segment(p, a, b, gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Velocity induced at `p` by a circular vortex ring discretized into +/// `n_seg` straight segments. +/// +/// Rust: `cfd::vortex::biot_savart_ring` +#[pyfunction] +#[pyo3(name = "biot_savart_ring", signature = (p, center, radius, gamma, normal, n_seg))] +pub fn pyfn_biot_savart_ring(p: crate::generated::types::PyVec3Arg, center: crate::generated::types::PyVec3Arg, radius: f64, gamma: f64, normal: crate::generated::types::PyVec3Arg, n_seg: usize) -> PyResult { + let p = p.0; + let center = center.0; + let normal = normal.0; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::vortex::biot_savart_ring(p, center, radius, gamma, normal, n_seg)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Kelvin's formula for the self-induced translation speed of a thin vortex +/// ring: U = Gamma/(4 pi R) (ln(8R/a) - 1/4). +/// +/// Rust: `cfd::vortex::vortex_ring_self_velocity` +#[pyfunction] +#[pyo3(name = "vortex_ring_self_velocity", signature = (gamma, r, core))] +pub fn pyfn_vortex_ring_self_velocity(gamma: f64, r: f64, core: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::vortex::vortex_ring_self_velocity(gamma, r, core)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Lamb-Oseen azimuthal velocity at radius `r` and time `t`: +/// v = Gamma/(2 pi r) (1 - exp(-r^2/(4 nu t))). +/// +/// Rust: `cfd::vortex::lamb_oseen_velocity` +#[pyfunction] +#[pyo3(name = "lamb_oseen_velocity", signature = (r, t, gamma, nu))] +pub fn pyfn_lamb_oseen_velocity(r: f64, t: f64, gamma: f64, nu: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::vortex::lamb_oseen_velocity(r, t, gamma, nu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Rankine vortex: solid-body rotation inside `r_core`, potential outside. +/// +/// Rust: `cfd::vortex::rankine_vortex` +#[pyfunction] +#[pyo3(name = "rankine_vortex", signature = (r, r_core, gamma))] +pub fn pyfn_rankine_vortex(r: f64, r_core: f64, gamma: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::vortex::rankine_vortex(r, r_core, gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Burgers vortex azimuthal velocity: the steady balance of diffusion +/// against axial strain `strain` (units 1/s): +/// v = Gamma/(2 pi r) (1 - exp(-strain r^2/(4 nu))). +/// +/// Rust: `cfd::vortex::burgers_vortex` +#[pyfunction] +#[pyo3(name = "burgers_vortex", signature = (r, gamma, nu, strain))] +pub fn pyfn_burgers_vortex(r: f64, gamma: f64, nu: f64, strain: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::vortex::burgers_vortex(r, gamma, nu, strain)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Hill's spherical vortex of radius `a` in the co-moving frame: far-field +/// velocity is -u along z, the sphere surface is a stream surface, and the +/// poles are stagnation points. +/// +/// Rust: `cfd::vortex::hill_spherical_vortex` +#[pyfunction] +#[pyo3(name = "hill_spherical_vortex", signature = (p, u, a))] +pub fn pyfn_hill_spherical_vortex(p: crate::generated::types::PyVec3Arg, u: f64, a: f64) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::vortex::hill_spherical_vortex(p, u, a)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Translation speed of a counter-rotating vortex pair: Gamma/(2 pi d). +/// +/// Rust: `cfd::vortex::vortex_pair_velocity` +#[pyfunction] +#[pyo3(name = "vortex_pair_velocity", signature = (gamma, d))] +pub fn pyfn_vortex_pair_velocity(gamma: f64, d: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::vortex::vortex_pair_velocity(gamma, d)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Point-vortex Hamiltonian H = -(1/4 pi) sum_{i(py: Python<'py>, pos: Vec, gammas: Vec) -> PyResult { + let pos = pos.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::cfd::vortex::point_vortex_hamiltonian(&pos, &gammas))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// One implicit-midpoint step of the point-vortex system (symplectic; the +/// Hamiltonian error stays bounded over long integrations). +/// +/// Rust: `cfd::vortex::point_vortex_step` +#[pyfunction] +#[pyo3(name = "point_vortex_step", signature = (pos, gammas, dt))] +pub fn pyfn_point_vortex_step<'py>(pos: pyo3::Bound<'py, pyo3::PyAny>, gammas: Vec, dt: f64) -> PyResult<()> { + let mut pos__v: Vec = pos.extract::>()?.into_iter().map(|__e| __e.0).collect(); + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::vortex::point_vortex_step(&mut pos__v, &gammas, dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + crate::runtime::coerce::write_back_objects(&pos, pos__v.into_iter().map(|__e| crate::generated::types::PyVec2 { inner: __e }).collect::>())?; + Ok(()) +} + +/// Inviscid Kelvin-Helmholtz growth rate for a velocity jump `delta_u`: +/// sigma = k delta_u / 2. +/// +/// Rust: `cfd::vortex::kelvin_helmholtz_growth_exact` +#[pyfunction] +#[pyo3(name = "kelvin_helmholtz_growth_exact", signature = (k, delta_u))] +pub fn pyfn_kelvin_helmholtz_growth_exact(k: f64, delta_u: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::vortex::kelvin_helmholtz_growth_exact(k, delta_u)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Shedding frequency f = St U / D. +/// +/// Rust: `cfd::vortex::vortex_shedding_frequency` +#[pyfunction] +#[pyo3(name = "vortex_shedding_frequency", signature = (strouhal, u, d))] +pub fn pyfn_vortex_shedding_frequency(strouhal: f64, u: f64, d: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::vortex::vortex_shedding_frequency(strouhal, u, d)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Roshko-style Strouhal-Reynolds correlation for a circular cylinder. +/// +/// Rust: `cfd::vortex::strouhal_from_re` +#[pyfunction] +#[pyo3(name = "strouhal_from_re", signature = (re))] +pub fn pyfn_strouhal_from_re(re: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::vortex::strouhal_from_re(re)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Crow instability growth rate for a trailing-vortex pair of spacing `b` +/// (approximate peak rate ~0.83 Gamma/(2 pi b^2), weakly dependent on core). +/// +/// Rust: `cfd::vortex::crow_instability_growth` +#[pyfunction] +#[pyo3(name = "crow_instability_growth", signature = (b, gamma, core))] +pub fn pyfn_crow_instability_growth(b: f64, gamma: f64, core: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::vortex::crow_instability_growth(b, gamma, core)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Peak swirl velocity of a decaying tip vortex at time `t` (Lamb-Oseen core +/// growth from initial core radius `r_core0`). +/// +/// Rust: `cfd::vortex::tip_vortex_decay` +#[pyfunction] +#[pyo3(name = "tip_vortex_decay", signature = (gamma, r_core0, nu, t))] +pub fn pyfn_tip_vortex_decay(gamma: f64, r_core0: f64, nu: f64, t: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::vortex::tip_vortex_decay(gamma, r_core0, nu, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Helicity density u . omega. +/// +/// Rust: `cfd::vortex::helicity_density` +#[pyfunction] +#[pyo3(name = "helicity_density", signature = (u, omega))] +pub fn pyfn_helicity_density(u: crate::generated::types::PyVec3Arg, omega: crate::generated::types::PyVec3Arg) -> PyResult { + let u = u.0; + let omega = omega.0; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::vortex::helicity_density(u, omega)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Trace a vortex line through a vorticity field by RK4 along the normalized +/// field direction, with arc-length step `ds`. +/// +/// Rust: `cfd::vortex::vortex_line_trace` +#[pyfunction] +#[pyo3(name = "vortex_line_trace", signature = (omega_field, seed, steps, ds))] +pub fn pyfn_vortex_line_trace(omega_field: pyo3::Py, seed: crate::generated::types::PyVec3Arg, steps: usize, ds: f64) -> PyResult> { + let __cb_omega_field = std::rc::Rc::new(crate::runtime::Callback::new(omega_field)); + let omega_field = { let __cb = __cb_omega_field.clone(); move |__a0: rust_physics_engine::math::Vec3| -> rust_physics_engine::math::Vec3 { { let __r = __cb.call::<_, crate::generated::types::PyVec3Arg>((crate::generated::types::PyVec3 { inner: __a0 },), crate::generated::types::PyVec3Arg(rust_physics_engine::math::Vec3 { x: f64::NAN, y: f64::NAN, z: f64::NAN })); __r.0 } } }; + let seed = seed.0; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::vortex::vortex_line_trace(&omega_field, seed, steps, ds)); + crate::runtime::callback::check(&[&__cb_omega_field], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_biot_savart_segment, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_biot_savart_ring, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_vortex_ring_self_velocity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lamb_oseen_velocity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rankine_vortex, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_burgers_vortex, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hill_spherical_vortex, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_vortex_pair_velocity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_point_vortex_hamiltonian, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_point_vortex_step, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kelvin_helmholtz_growth_exact, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_vortex_shedding_frequency, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_strouhal_from_re, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_crow_instability_growth, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tip_vortex_decay, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_helicity_density, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_vortex_line_trace, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_chemistry.rs b/bindings/python/src/generated/m_chemistry.rs new file mode 100644 index 0000000..8c6f193 --- /dev/null +++ b/bindings/python/src/generated/m_chemistry.rs @@ -0,0 +1,239 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Arrhenius equation: k = A × exp(-Ea / (RT)) +/// +/// Rust: `chemistry::arrhenius_rate` +#[pyfunction] +#[pyo3(name = "arrhenius_rate", signature = (pre_exponential, activation_energy, temperature))] +pub fn pyfn_arrhenius_rate(pre_exponential: f64, activation_energy: f64, temperature: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::chemistry::arrhenius_rate(pre_exponential, activation_energy, temperature)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// First-order half-life: t½ = ln(2) / k +/// +/// Rust: `chemistry::half_life_first_order` +#[pyfunction] +#[pyo3(name = "half_life_first_order", signature = (rate_constant))] +pub fn pyfn_half_life_first_order(rate_constant: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::chemistry::half_life_first_order(rate_constant)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// First-order concentration decay: `[A] = [A]₀ × e^(-kt)` +/// +/// Rust: `chemistry::concentration_first_order` +#[pyfunction] +#[pyo3(name = "concentration_first_order", signature = (c0, rate_constant, time))] +pub fn pyfn_concentration_first_order(c0: f64, rate_constant: f64, time: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::chemistry::concentration_first_order(c0, rate_constant, time)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Second-order integrated rate law: `1/[A] = 1/[A]₀ + kt`, returns `[A]` +/// +/// Rust: `chemistry::concentration_second_order` +#[pyfunction] +#[pyo3(name = "concentration_second_order", signature = (c0, rate_constant, time))] +pub fn pyfn_concentration_second_order(c0: f64, rate_constant: f64, time: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::chemistry::concentration_second_order(c0, rate_constant, time)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// General rate law: `r = k × Π([Ci]^ni)` +/// +/// Rust: `chemistry::reaction_rate` +#[pyfunction] +#[pyo3(name = "reaction_rate", signature = (k, concentrations, orders))] +pub fn pyfn_reaction_rate<'py>(py: Python<'py>, k: f64, concentrations: Vec, orders: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::chemistry::reaction_rate(k, &concentrations, &orders))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Gibbs free energy: ΔG = ΔH - TΔS +/// +/// Rust: `chemistry::gibbs_free_energy` +#[pyfunction] +#[pyo3(name = "gibbs_free_energy", signature = (enthalpy, temperature, entropy))] +pub fn pyfn_gibbs_free_energy(enthalpy: f64, temperature: f64, entropy: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::chemistry::gibbs_free_energy(enthalpy, temperature, entropy)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Equilibrium constant from Gibbs energy: K = exp(-ΔG / (RT)) +/// +/// Rust: `chemistry::equilibrium_constant_from_gibbs` +#[pyfunction] +#[pyo3(name = "equilibrium_constant_from_gibbs", signature = (delta_g, temperature))] +pub fn pyfn_equilibrium_constant_from_gibbs(delta_g: f64, temperature: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::chemistry::equilibrium_constant_from_gibbs(delta_g, temperature)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Van't Hoff equation: ln(K2/K1) = -ΔH/R × (1/T2 - 1/T1), returns K2 +/// +/// Rust: `chemistry::vant_hoff` +#[pyfunction] +#[pyo3(name = "vant_hoff", signature = (k1, delta_h, t1, t2))] +pub fn pyfn_vant_hoff(k1: f64, delta_h: f64, t1: f64, t2: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::chemistry::vant_hoff(k1, delta_h, t1, t2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Hess's law: ΔH_rxn = Σ(ci × ΔHi) +/// +/// Rust: `chemistry::hess_law` +#[pyfunction] +#[pyo3(name = "hess_law", signature = (enthalpies, coefficients))] +pub fn pyfn_hess_law<'py>(py: Python<'py>, enthalpies: Vec, coefficients: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::chemistry::hess_law(&enthalpies, &coefficients))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Nernst equation: E = E° - (RT / (nF)) × ln(Q) +/// +/// Rust: `chemistry::nernst_potential` +#[pyfunction] +#[pyo3(name = "nernst_potential", signature = (e_standard, temperature, n_electrons, reaction_quotient))] +pub fn pyfn_nernst_potential(e_standard: f64, temperature: f64, n_electrons: f64, reaction_quotient: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::chemistry::nernst_potential(e_standard, temperature, n_electrons, reaction_quotient)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Cell potential: E_cell = E_cathode - E_anode +/// +/// Rust: `chemistry::cell_potential` +#[pyfunction] +#[pyo3(name = "cell_potential", signature = (e_cathode, e_anode))] +pub fn pyfn_cell_potential(e_cathode: f64, e_anode: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::chemistry::cell_potential(e_cathode, e_anode)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Faraday's law of electrolysis: m = (I × t × M) / (n × F) +/// +/// Rust: `chemistry::faraday_electrolysis` +#[pyfunction] +#[pyo3(name = "faraday_electrolysis", signature = (current, time, molar_mass, n_electrons))] +pub fn pyfn_faraday_electrolysis(current: f64, time: f64, molar_mass: f64, n_electrons: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::chemistry::faraday_electrolysis(current, time, molar_mass, n_electrons)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// pH = -log₁₀([H⁺]) +/// +/// Rust: `chemistry::ph` +#[pyfunction] +#[pyo3(name = "ph", signature = (h_concentration))] +pub fn pyfn_ph(h_concentration: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::chemistry::ph(h_concentration)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// pOH = -log₁₀([OH⁻]) +/// +/// Rust: `chemistry::poh` +#[pyfunction] +#[pyo3(name = "poh", signature = (oh_concentration))] +pub fn pyfn_poh(oh_concentration: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::chemistry::poh(oh_concentration)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// [H⁺] = 10^(-pH) +/// +/// Rust: `chemistry::h_from_ph` +#[pyfunction] +#[pyo3(name = "h_from_ph", signature = (ph))] +pub fn pyfn_h_from_ph(ph: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::chemistry::h_from_ph(ph)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Osmotic pressure: Π = iMRT +/// +/// Rust: `chemistry::osmotic_pressure` +#[pyfunction] +#[pyo3(name = "osmotic_pressure", signature = (molarity, temperature, i_factor))] +pub fn pyfn_osmotic_pressure(molarity: f64, temperature: f64, i_factor: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::chemistry::osmotic_pressure(molarity, temperature, i_factor)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Molarity: M = n / V +/// +/// Rust: `chemistry::molarity` +#[pyfunction] +#[pyo3(name = "molarity", signature = (moles, volume_liters))] +pub fn pyfn_molarity(moles: f64, volume_liters: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::chemistry::molarity(moles, volume_liters)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Dilution: C2 = C1 × V1 / V2 +/// +/// Rust: `chemistry::dilution` +#[pyfunction] +#[pyo3(name = "dilution", signature = (c1, v1, v2))] +pub fn pyfn_dilution(c1: f64, v1: f64, v2: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::chemistry::dilution(c1, v1, v2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_arrhenius_rate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_half_life_first_order, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_concentration_first_order, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_concentration_second_order, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_reaction_rate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gibbs_free_energy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_equilibrium_constant_from_gibbs, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_vant_hoff, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hess_law, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_nernst_potential, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cell_potential, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_faraday_electrolysis, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ph, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_poh, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_h_from_ph, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_osmotic_pressure, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_molarity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dilution, m)?)?; + m.add("FARADAY", rust_physics_engine::chemistry::FARADAY)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_classical.rs b/bindings/python/src/generated/m_classical.rs new file mode 100644 index 0000000..c519c05 --- /dev/null +++ b/bindings/python/src/generated/m_classical.rs @@ -0,0 +1,682 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Position after uniform acceleration: x = x0 + v0*t + 0.5*a*t^2 +/// +/// Rust: `classical::displacement` +#[pyfunction] +#[pyo3(name = "displacement", signature = (initial_velocity, acceleration, time))] +pub fn pyfn_displacement(initial_velocity: f64, acceleration: f64, time: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::displacement(initial_velocity, acceleration, time)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Velocity after uniform acceleration: v = v0 + a*t +/// +/// Rust: `classical::velocity` +#[pyfunction] +#[pyo3(name = "velocity", signature = (initial_velocity, acceleration, time))] +pub fn pyfn_velocity(initial_velocity: f64, acceleration: f64, time: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::velocity(initial_velocity, acceleration, time)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Velocity squared: v^2 = v0^2 + 2*a*d +/// +/// Rust: `classical::velocity_squared` +#[pyfunction] +#[pyo3(name = "velocity_squared", signature = (initial_velocity, acceleration, displacement))] +pub fn pyfn_velocity_squared(initial_velocity: f64, acceleration: f64, displacement: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::velocity_squared(initial_velocity, acceleration, displacement)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// 3D position under constant acceleration. +/// +/// Rust: `classical::position_3d` +#[pyfunction] +#[pyo3(name = "position_3d", signature = (pos, vel, acc, t))] +pub fn pyfn_position_3d(pos: crate::generated::types::PyVec3Arg, vel: crate::generated::types::PyVec3Arg, acc: crate::generated::types::PyVec3Arg, t: f64) -> PyResult { + let pos = pos.0; + let vel = vel.0; + let acc = acc.0; + let __r = crate::runtime::guard(|| rust_physics_engine::classical::position_3d(pos, vel, acc, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// 3D velocity under constant acceleration. +/// +/// Rust: `classical::velocity_3d` +#[pyfunction] +#[pyo3(name = "velocity_3d", signature = (vel, acc, t))] +pub fn pyfn_velocity_3d(vel: crate::generated::types::PyVec3Arg, acc: crate::generated::types::PyVec3Arg, t: f64) -> PyResult { + let vel = vel.0; + let acc = acc.0; + let __r = crate::runtime::guard(|| rust_physics_engine::classical::velocity_3d(vel, acc, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Range of a projectile on flat ground: R = v^2 * sin(2θ) / g +/// +/// Rust: `classical::projectile_range` +#[pyfunction] +#[pyo3(name = "projectile_range", signature = (speed, angle_rad, g))] +pub fn pyfn_projectile_range(speed: f64, angle_rad: f64, g: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::projectile_range(speed, angle_rad, g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Maximum height of a projectile: H = v^2 * sin^2(θ) / (2g) +/// +/// Rust: `classical::projectile_max_height` +#[pyfunction] +#[pyo3(name = "projectile_max_height", signature = (speed, angle_rad, g))] +pub fn pyfn_projectile_max_height(speed: f64, angle_rad: f64, g: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::projectile_max_height(speed, angle_rad, g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Time of flight for a projectile on flat ground: T = 2v*sin(θ) / g +/// +/// Rust: `classical::projectile_time_of_flight` +#[pyfunction] +#[pyo3(name = "projectile_time_of_flight", signature = (speed, angle_rad, g))] +pub fn pyfn_projectile_time_of_flight(speed: f64, angle_rad: f64, g: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::projectile_time_of_flight(speed, angle_rad, g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Force = mass * acceleration (Newton's second law) +/// +/// Rust: `classical::force` +#[pyfunction] +#[pyo3(name = "force", signature = (mass, acceleration))] +pub fn pyfn_force(mass: f64, acceleration: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::force(mass, acceleration)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// F = ma as vectors +/// +/// Rust: `classical::force_3d` +#[pyfunction] +#[pyo3(name = "force_3d", signature = (mass, acceleration))] +pub fn pyfn_force_3d(mass: f64, acceleration: crate::generated::types::PyVec3Arg) -> PyResult { + let acceleration = acceleration.0; + let __r = crate::runtime::guard(|| rust_physics_engine::classical::force_3d(mass, acceleration)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Acceleration from force: a = F / m +/// +/// Rust: `classical::acceleration` +#[pyfunction] +#[pyo3(name = "acceleration", signature = (force, mass))] +pub fn pyfn_acceleration(force: f64, mass: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::acceleration(force, mass)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Weight: W = m * g +/// +/// Rust: `classical::weight` +#[pyfunction] +#[pyo3(name = "weight", signature = (mass, g))] +pub fn pyfn_weight(mass: f64, g: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::weight(mass, g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Linear momentum: p = m * v +/// +/// Rust: `classical::momentum` +#[pyfunction] +#[pyo3(name = "momentum", signature = (mass, velocity))] +pub fn pyfn_momentum(mass: f64, velocity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::momentum(mass, velocity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// 3D momentum. +/// +/// Rust: `classical::momentum_3d` +#[pyfunction] +#[pyo3(name = "momentum_3d", signature = (mass, velocity))] +pub fn pyfn_momentum_3d(mass: f64, velocity: crate::generated::types::PyVec3Arg) -> PyResult { + let velocity = velocity.0; + let __r = crate::runtime::guard(|| rust_physics_engine::classical::momentum_3d(mass, velocity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Impulse: J = F * Δt +/// +/// Rust: `classical::impulse` +#[pyfunction] +#[pyo3(name = "impulse", signature = (force, delta_t))] +pub fn pyfn_impulse(force: f64, delta_t: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::impulse(force, delta_t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Final velocities after a 1D elastic collision between two masses. +/// Returns (v1_final, v2_final). +/// +/// Rust: `classical::elastic_collision_1d` +#[pyfunction] +#[pyo3(name = "elastic_collision_1d", signature = (m1, v1, m2, v2))] +pub fn pyfn_elastic_collision_1d(m1: f64, v1: f64, m2: f64, v2: f64) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::elastic_collision_1d(m1, v1, m2, v2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Final velocity after a perfectly inelastic collision (objects stick together). +/// +/// Rust: `classical::inelastic_collision_1d` +#[pyfunction] +#[pyo3(name = "inelastic_collision_1d", signature = (m1, v1, m2, v2))] +pub fn pyfn_inelastic_collision_1d(m1: f64, v1: f64, m2: f64, v2: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::inelastic_collision_1d(m1, v1, m2, v2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Coefficient of restitution: e = -(v1f - v2f) / (v1i - v2i) +/// +/// Rust: `classical::coefficient_of_restitution` +#[pyfunction] +#[pyo3(name = "coefficient_of_restitution", signature = (v1i, v2i, v1f, v2f))] +pub fn pyfn_coefficient_of_restitution(v1i: f64, v2i: f64, v1f: f64, v2f: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::coefficient_of_restitution(v1i, v2i, v1f, v2f)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Kinetic energy: KE = 0.5 * m * v^2 +/// +/// Rust: `classical::kinetic_energy` +#[pyfunction] +#[pyo3(name = "kinetic_energy", signature = (mass, speed))] +pub fn pyfn_kinetic_energy(mass: f64, speed: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::kinetic_energy(mass, speed)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Gravitational potential energy: PE = m * g * h +/// +/// Rust: `classical::potential_energy_gravity` +#[pyfunction] +#[pyo3(name = "potential_energy_gravity", signature = (mass, g, height))] +pub fn pyfn_potential_energy_gravity(mass: f64, g: f64, height: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::potential_energy_gravity(mass, g, height)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Elastic potential energy: PE = 0.5 * k * x^2 +/// +/// Rust: `classical::potential_energy_spring` +#[pyfunction] +#[pyo3(name = "potential_energy_spring", signature = (spring_constant, displacement))] +pub fn pyfn_potential_energy_spring(spring_constant: f64, displacement: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::potential_energy_spring(spring_constant, displacement)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Work: W = F * d * cos(θ) +/// +/// Rust: `classical::work` +#[pyfunction] +#[pyo3(name = "work", signature = (force, displacement, angle_rad))] +pub fn pyfn_work(force: f64, displacement: f64, angle_rad: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::work(force, displacement, angle_rad)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Power: P = W / t +/// +/// Rust: `classical::power` +#[pyfunction] +#[pyo3(name = "power", signature = (work, time))] +pub fn pyfn_power(work: f64, time: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::power(work, time)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Power (instantaneous): P = F * v +/// +/// Rust: `classical::power_instantaneous` +#[pyfunction] +#[pyo3(name = "power_instantaneous", signature = (force, velocity))] +pub fn pyfn_power_instantaneous(force: f64, velocity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::power_instantaneous(force, velocity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Angular velocity: ω = Δθ / Δt +/// +/// Rust: `classical::angular_velocity` +#[pyfunction] +#[pyo3(name = "angular_velocity", signature = (delta_theta, delta_t))] +pub fn pyfn_angular_velocity(delta_theta: f64, delta_t: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::angular_velocity(delta_theta, delta_t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Angular acceleration: α = Δω / Δt +/// +/// Rust: `classical::angular_acceleration` +#[pyfunction] +#[pyo3(name = "angular_acceleration", signature = (delta_omega, delta_t))] +pub fn pyfn_angular_acceleration(delta_omega: f64, delta_t: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::angular_acceleration(delta_omega, delta_t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Torque: τ = r * F * sin(θ) +/// +/// Rust: `classical::torque` +#[pyfunction] +#[pyo3(name = "torque", signature = (radius, force, angle_rad))] +pub fn pyfn_torque(radius: f64, force: f64, angle_rad: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::torque(radius, force, angle_rad)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Torque as cross product: τ = r × F +/// +/// Rust: `classical::torque_3d` +#[pyfunction] +#[pyo3(name = "torque_3d", signature = (r, f))] +pub fn pyfn_torque_3d(r: crate::generated::types::PyVec3Arg, f: crate::generated::types::PyVec3Arg) -> PyResult { + let r = r.0; + let f = f.0; + let __r = crate::runtime::guard(|| rust_physics_engine::classical::torque_3d(r, f)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Moment of inertia of a point mass: I = m * r^2 +/// +/// Rust: `classical::moment_of_inertia_point` +#[pyfunction] +#[pyo3(name = "moment_of_inertia_point", signature = (mass, radius))] +pub fn pyfn_moment_of_inertia_point(mass: f64, radius: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::moment_of_inertia_point(mass, radius)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Moment of inertia of a solid sphere: I = (2/5) * m * r^2 +/// +/// Rust: `classical::moment_of_inertia_solid_sphere` +#[pyfunction] +#[pyo3(name = "moment_of_inertia_solid_sphere", signature = (mass, radius))] +pub fn pyfn_moment_of_inertia_solid_sphere(mass: f64, radius: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::moment_of_inertia_solid_sphere(mass, radius)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Moment of inertia of a solid cylinder about its axis: I = (1/2) * m * r^2 +/// +/// Rust: `classical::moment_of_inertia_solid_cylinder` +#[pyfunction] +#[pyo3(name = "moment_of_inertia_solid_cylinder", signature = (mass, radius))] +pub fn pyfn_moment_of_inertia_solid_cylinder(mass: f64, radius: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::moment_of_inertia_solid_cylinder(mass, radius)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Rotational kinetic energy: KE = 0.5 * I * ω^2 +/// +/// Rust: `classical::rotational_kinetic_energy` +#[pyfunction] +#[pyo3(name = "rotational_kinetic_energy", signature = (moment_of_inertia, angular_velocity))] +pub fn pyfn_rotational_kinetic_energy(moment_of_inertia: f64, angular_velocity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::rotational_kinetic_energy(moment_of_inertia, angular_velocity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Angular momentum: L = I * ω +/// +/// Rust: `classical::angular_momentum` +#[pyfunction] +#[pyo3(name = "angular_momentum", signature = (moment_of_inertia, angular_velocity))] +pub fn pyfn_angular_momentum(moment_of_inertia: f64, angular_velocity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::angular_momentum(moment_of_inertia, angular_velocity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Centripetal acceleration: a = v^2 / r +/// +/// Rust: `classical::centripetal_acceleration` +#[pyfunction] +#[pyo3(name = "centripetal_acceleration", signature = (speed, radius))] +pub fn pyfn_centripetal_acceleration(speed: f64, radius: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::centripetal_acceleration(speed, radius)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Centripetal force: F = m * v^2 / r +/// +/// Rust: `classical::centripetal_force` +#[pyfunction] +#[pyo3(name = "centripetal_force", signature = (mass, speed, radius))] +pub fn pyfn_centripetal_force(mass: f64, speed: f64, radius: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::centripetal_force(mass, speed, radius)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Friction force: f = μ * N +/// +/// Rust: `classical::friction_force` +#[pyfunction] +#[pyo3(name = "friction_force", signature = (coefficient, normal_force))] +pub fn pyfn_friction_force(coefficient: f64, normal_force: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::friction_force(coefficient, normal_force)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Period of a mass-spring system: T = 2π * sqrt(m / k) +/// +/// Rust: `classical::shm_period_spring` +#[pyfunction] +#[pyo3(name = "shm_period_spring", signature = (mass, spring_constant))] +pub fn pyfn_shm_period_spring(mass: f64, spring_constant: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::shm_period_spring(mass, spring_constant)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Period of a simple pendulum: T = 2π * sqrt(L / g) +/// +/// Rust: `classical::shm_period_pendulum` +#[pyfunction] +#[pyo3(name = "shm_period_pendulum", signature = (length, g))] +pub fn pyfn_shm_period_pendulum(length: f64, g: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::shm_period_pendulum(length, g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Position of SHM: x(t) = A * cos(ωt + φ) +/// +/// Rust: `classical::shm_position` +#[pyfunction] +#[pyo3(name = "shm_position", signature = (amplitude, angular_freq, time, phase))] +pub fn pyfn_shm_position(amplitude: f64, angular_freq: f64, time: f64, phase: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::shm_position(amplitude, angular_freq, time, phase)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Velocity of SHM: v(t) = -A * ω * sin(ωt + φ) +/// +/// Rust: `classical::shm_velocity` +#[pyfunction] +#[pyo3(name = "shm_velocity", signature = (amplitude, angular_freq, time, phase))] +pub fn pyfn_shm_velocity(amplitude: f64, angular_freq: f64, time: f64, phase: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::shm_velocity(amplitude, angular_freq, time, phase)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Damped angular frequency: ωd = ω₀√(1 - ζ²), returns 0 if overdamped (ζ ≥ 1) +/// +/// Rust: `classical::damped_frequency` +#[pyfunction] +#[pyo3(name = "damped_frequency", signature = (natural_freq, damping_ratio))] +pub fn pyfn_damped_frequency(natural_freq: f64, damping_ratio: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::damped_frequency(natural_freq, damping_ratio)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Damped amplitude: A(t) = A₀ × e^(-γt) +/// +/// Rust: `classical::damped_amplitude` +#[pyfunction] +#[pyo3(name = "damped_amplitude", signature = (initial_amplitude, damping_coeff, time))] +pub fn pyfn_damped_amplitude(initial_amplitude: f64, damping_coeff: f64, time: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::damped_amplitude(initial_amplitude, damping_coeff, time)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Damped oscillation position: x(t) = A₀e^(-γt)cos(ωdt + φ) +/// +/// Rust: `classical::damped_position` +#[pyfunction] +#[pyo3(name = "damped_position", signature = (amplitude, damping_coeff, angular_freq, time, phase))] +pub fn pyfn_damped_position(amplitude: f64, damping_coeff: f64, angular_freq: f64, time: f64, phase: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::damped_position(amplitude, damping_coeff, angular_freq, time, phase)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Damping ratio: ζ = c / (2√(mk)) +/// +/// Rust: `classical::damping_ratio` +#[pyfunction] +#[pyo3(name = "damping_ratio", signature = (damping_coeff, mass, spring_constant))] +pub fn pyfn_damping_ratio(damping_coeff: f64, mass: f64, spring_constant: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::damping_ratio(damping_coeff, mass, spring_constant)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Critical damping coefficient: c_crit = 2√(mk) +/// +/// Rust: `classical::critical_damping` +#[pyfunction] +#[pyo3(name = "critical_damping", signature = (mass, spring_constant))] +pub fn pyfn_critical_damping(mass: f64, spring_constant: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::critical_damping(mass, spring_constant)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Logarithmic decrement: δ = 2πζ / √(1 - ζ²) +/// +/// Rust: `classical::logarithmic_decrement` +#[pyfunction] +#[pyo3(name = "logarithmic_decrement", signature = (damping_ratio))] +pub fn pyfn_logarithmic_decrement(damping_ratio: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::logarithmic_decrement(damping_ratio)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Quality factor: Q = 1 / (2ζ) +/// +/// Rust: `classical::quality_factor` +#[pyfunction] +#[pyo3(name = "quality_factor", signature = (damping_ratio))] +pub fn pyfn_quality_factor(damping_ratio: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::quality_factor(damping_ratio)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Decay time constant: τ = 1/γ (time for amplitude to drop to 1/e) +/// +/// Rust: `classical::decay_time` +#[pyfunction] +#[pyo3(name = "decay_time", signature = (damping_coeff))] +pub fn pyfn_decay_time(damping_coeff: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::decay_time(damping_coeff)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Driven oscillation amplitude: A = f₀ / √((ω₀²-ω²)² + (2γω)²) +/// where f₀ = F₀/m (driving force per unit mass) +/// +/// Rust: `classical::driven_amplitude` +#[pyfunction] +#[pyo3(name = "driven_amplitude", signature = (f0, omega, omega0, gamma))] +pub fn pyfn_driven_amplitude(f0: f64, omega: f64, omega0: f64, gamma: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::driven_amplitude(f0, omega, omega0, gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Phase lag of driven oscillation: φ = atan2(2γω, ω₀²-ω²) +/// +/// Rust: `classical::driven_phase` +#[pyfunction] +#[pyo3(name = "driven_phase", signature = (omega, omega0, gamma))] +pub fn pyfn_driven_phase(omega: f64, omega0: f64, gamma: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::driven_phase(omega, omega0, gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Resonance frequency: ωr = √(ω₀² - 2γ²), returns 0 if overdamped +/// +/// Rust: `classical::resonance_frequency` +#[pyfunction] +#[pyo3(name = "resonance_frequency", signature = (natural_freq, damping_coeff))] +pub fn pyfn_resonance_frequency(natural_freq: f64, damping_coeff: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::resonance_frequency(natural_freq, damping_coeff)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Peak amplitude at resonance: A_max = f₀ / (2γ√(ω₀² - γ²)) +/// +/// Rust: `classical::resonance_amplitude` +#[pyfunction] +#[pyo3(name = "resonance_amplitude", signature = (f0, omega0, gamma))] +pub fn pyfn_resonance_amplitude(f0: f64, omega0: f64, gamma: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::resonance_amplitude(f0, omega0, gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Normal-mode frequencies of two identical masses coupled by a spring: +/// ω₁ = √(k/m), ω₂ = √((k + 2k_c)/m) +/// +/// Rust: `classical::coupled_normal_frequencies` +#[pyfunction] +#[pyo3(name = "coupled_normal_frequencies", signature = (k, k_coupling, m))] +pub fn pyfn_coupled_normal_frequencies(k: f64, k_coupling: f64, m: f64) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::coupled_normal_frequencies(k, k_coupling, m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Beat frequency of coupled oscillators: f_beat = |f1 - f2| +/// +/// Rust: `classical::beat_frequency_coupled` +#[pyfunction] +#[pyo3(name = "beat_frequency_coupled", signature = (freq1, freq2))] +pub fn pyfn_beat_frequency_coupled(freq1: f64, freq2: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::classical::beat_frequency_coupled(freq1, freq2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_displacement, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_velocity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_velocity_squared, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_position_3d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_velocity_3d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_projectile_range, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_projectile_max_height, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_projectile_time_of_flight, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_force, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_force_3d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_acceleration, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_weight, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_momentum, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_momentum_3d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_impulse, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_elastic_collision_1d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_inelastic_collision_1d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_coefficient_of_restitution, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kinetic_energy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_potential_energy_gravity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_potential_energy_spring, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_work, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_power, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_power_instantaneous, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_angular_velocity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_angular_acceleration, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_torque, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_torque_3d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_moment_of_inertia_point, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_moment_of_inertia_solid_sphere, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_moment_of_inertia_solid_cylinder, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rotational_kinetic_energy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_angular_momentum, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_centripetal_acceleration, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_centripetal_force, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_friction_force, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_shm_period_spring, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_shm_period_pendulum, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_shm_position, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_shm_velocity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_damped_frequency, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_damped_amplitude, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_damped_position, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_damping_ratio, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_critical_damping, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_logarithmic_decrement, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_quality_factor, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_decay_time, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_driven_amplitude, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_driven_phase, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_resonance_frequency, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_resonance_amplitude, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_coupled_normal_frequencies, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_beat_frequency_coupled, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_codes.rs b/bindings/python/src/generated/m_codes.rs new file mode 100644 index 0000000..c02bbf8 --- /dev/null +++ b/bindings/python/src/generated/m_codes.rs @@ -0,0 +1,22 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + Ok(()) +} diff --git a/bindings/python/src/generated/m_codes__block.rs b/bindings/python/src/generated/m_codes__block.rs new file mode 100644 index 0000000..603d750 --- /dev/null +++ b/bindings/python/src/generated/m_codes__block.rs @@ -0,0 +1,234 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// The number of ones in a bit vector: its Hamming weight. +/// +/// Rust: `codes::block::weight` +#[pyfunction] +#[pyo3(name = "weight", signature = (v))] +pub fn pyfn_weight<'py>(py: Python<'py>, v: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::block::weight(&v))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The bitwise difference of two equal-length vectors. +/// +/// Panics: +/// Panics unless the lengths agree. +/// +/// Rust: `codes::block::xor` +#[pyfunction] +#[pyo3(name = "xor", signature = (a, b))] +pub fn pyfn_xor<'py>(py: Python<'py>, a: Vec, b: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::block::xor(&a, &b))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Hamming(7, 4) encoding: four data bits in, seven out. +/// +/// The classical layout, with the parity bits at the powers of two: position +/// one, two and four, counting from one at the least significant bit of the +/// result. Parity bit `b` covers exactly the positions whose index has bit +/// `b` set, so the three parity checks of a corrupted word spell out the +/// binary numeral of the corrupted position. +/// +/// Panics: +/// Panics if `nibble` has anything above its low four bits. +/// +/// Rust: `codes::block::hamming_74_encode` +#[pyfunction] +#[pyo3(name = "hamming_74_encode", signature = (nibble))] +pub fn pyfn_hamming_74_encode(nibble: u8) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::block::hamming_74_encode(nibble)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Hamming(7, 4) decoding: correct any single error and return the four data +/// bits, with a flag saying whether a correction was made. +/// +/// Panics: +/// Panics if `byte` has its top bit set, which is outside the seven-bit code. +/// +/// Rust: `codes::block::hamming_74_decode` +#[pyfunction] +#[pyo3(name = "hamming_74_decode", signature = (byte))] +pub fn pyfn_hamming_74_decode(byte: u8) -> PyResult<(u8, bool)> { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::block::hamming_74_decode(byte)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// The Singleton bound: `d <= n - k + 1`. +/// +/// Deleting `d - 1` positions must leave the codewords distinct, since they +/// differ in at least `d`, so the code embeds in `GF(2)^(n - d + 1)` and +/// `k <= n - d + 1`. Returns the largest distance the parameters allow. +/// +/// Panics: +/// Panics unless `k <= n`. +/// +/// Rust: `codes::block::singleton_bound` +#[pyfunction] +#[pyo3(name = "singleton_bound", signature = (n, k))] +pub fn pyfn_singleton_bound(n: usize, k: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::block::singleton_bound(n, k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Hamming, or sphere-packing, bound on how many codewords a binary code +/// of length `n` and distance `d` can have. +/// +/// Spheres of radius `t = (d - 1) / 2` around distinct codewords are +/// disjoint, so their total volume fits inside `2^n`. A code meeting it with +/// equality is *perfect* -- the spheres tile the space -- which the Hamming +/// and Golay codes do and almost nothing else does. +/// +/// Rust: `codes::block::hamming_bound` +#[pyfunction] +#[pyo3(name = "hamming_bound", signature = (n, d))] +pub fn pyfn_hamming_bound(n: usize, d: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::block::hamming_bound(n, d)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Gilbert-Varshamov bound: a code of length `n` and distance `d` with at +/// least this many codewords exists. +/// +/// A lower bound, and a constructive one: keep adding any word at distance +/// `d` or more from everything chosen so far, and you can only be stuck once +/// the balls of radius `d - 1` cover the space. Where the Hamming bound says +/// what is impossible, this says what is unavoidable, and the best known +/// binary codes sit between them. +/// +/// Rust: `codes::block::gilbert_varshamov` +#[pyfunction] +#[pyo3(name = "gilbert_varshamov", signature = (n, d))] +pub fn pyfn_gilbert_varshamov(n: usize, d: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::block::gilbert_varshamov(n, d)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Plotkin bound, for codes whose distance is more than half their +/// length. +/// +/// When `2d > n` the average distance between codewords cannot reach `d` +/// unless there are very few of them, and the count is capped at +/// `2 * floor(d / (2d - n))`. Outside that regime the bound says nothing and +/// this returns infinity. +/// +/// Rust: `codes::block::plotkin_bound` +#[pyfunction] +#[pyo3(name = "plotkin_bound", signature = (n, d))] +pub fn pyfn_plotkin_bound(n: usize, d: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::block::plotkin_bound(n, d)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// A regular low-density parity check matrix by Gallager's construction: +/// `wc` ones in every column and `wr` in every row. +/// +/// The first band of rows partitions the columns into consecutive runs of +/// `wr`; each later band is a column permutation of that one. The result is +/// sparse by construction, which is the whole point -- belief propagation +/// costs one message per one in the matrix, and its accuracy depends on the +/// Tanner graph having few short cycles, which a sparse random matrix +/// mostly does. +/// +/// Panics: +/// Panics unless `wr` divides `n` and `wc` is between one and `n / wr`. +/// +/// Rust: `codes::block::ldpc_regular` +#[pyfunction] +#[pyo3(name = "ldpc_regular", signature = (n, wc, wr, rng))] +pub fn pyfn_ldpc_regular(n: usize, wc: usize, wr: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::codes::block::ldpc_regular(n, wc, wr, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGf2Matrix { inner: __v }) +} + +/// Belief propagation decoding of an LDPC code, in the log-likelihood domain. +/// +/// `llr[i]` is the log of the ratio of the probability that bit `i` is zero +/// to the probability that it is one, so a positive value leans towards zero. +/// Each round every check tells each of its bits what the other bits imply, +/// and every bit tells each of its checks what the other checks imply; the +/// exclusions are what keep a message from being fed its own output back. +/// +/// Returns the hard decisions and whether every parity check is satisfied. +/// A `true` is strong evidence of a correct decode but not proof: the +/// algorithm can settle on a different codeword. +/// +/// Panics: +/// Panics unless `llr` has one entry per column. +/// +/// Rust: `codes::block::ldpc_decode_bp` +#[pyfunction] +#[pyo3(name = "ldpc_decode_bp", signature = (h, llr, iters))] +pub fn pyfn_ldpc_decode_bp<'py>(py: Python<'py>, h: crate::generated::types::PyGf2Matrix, llr: Vec, iters: usize) -> PyResult<(Vec, bool)> { + let h = h.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::block::ldpc_decode_bp(&h, &llr, iters))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Gallager's bit-flipping decoder: repeatedly flip whichever bits sit in the +/// most unsatisfied checks. +/// +/// Hard decisions only, so it throws away the channel's confidence and pays +/// for it -- roughly two decibels against belief propagation on the same +/// code. What it buys is that a round is a handful of parity computations +/// with no transcendental functions anywhere. +/// +/// Panics: +/// Panics unless `recv` has one entry per column. +/// +/// Rust: `codes::block::ldpc_decode_bitflip` +#[pyfunction] +#[pyo3(name = "ldpc_decode_bitflip", signature = (h, recv, iters))] +pub fn pyfn_ldpc_decode_bitflip<'py>(py: Python<'py>, h: crate::generated::types::PyGf2Matrix, recv: Vec, iters: usize) -> PyResult> { + let h = h.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::block::ldpc_decode_bitflip(&h, &recv, iters))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_weight, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_xor, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hamming_74_encode, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hamming_74_decode, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_singleton_bound, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hamming_bound, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gilbert_varshamov, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_plotkin_bound, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ldpc_regular, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ldpc_decode_bp, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ldpc_decode_bitflip, m)?)?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_codes__checksum.rs b/bindings/python/src/generated/m_codes__checksum.rs new file mode 100644 index 0000000..f342a9b --- /dev/null +++ b/bindings/python/src/generated/m_codes__checksum.rs @@ -0,0 +1,415 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Checksums and check digits: cheap ways to notice that data changed. +/// +/// None of these corrects anything, and none of them resists an adversary. +/// What they do is turn a class of likely accidents into a mismatch, and the +/// useful question about each is which class. A single parity bit catches any +/// odd number of flipped bits and nothing else. A Fletcher or Adler sum +/// catches reordering, which a plain sum does not, because the second +/// accumulator weights each byte by its position. A CRC of width `w` catches +/// every burst of `w` bits or fewer, every odd number of bit errors when the +/// polynomial has `x + 1` as a factor, and all but `2^-w` of everything else. +/// The decimal check digits catch every single-digit error and, except for +/// Luhn, every transposition of adjacent digits. +/// +/// For an adversary, none of this is relevant: all of it is linear or nearly +/// so, and a forger can adjust the data to hit any checksum they like. +/// Even parity: `true` when an odd number of bits are set, so that appending +/// it makes the total even. +/// +/// Detects any odd number of bit errors and no even number, which is the +/// whole of what a single bit can promise. +/// +/// Rust: `codes::checksum::parity` +#[pyfunction] +#[pyo3(name = "parity", signature = (bits))] +pub fn pyfn_parity<'py>(py: Python<'py>, bits: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::checksum::parity(&bits))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Parity of the set bits of a word. +/// +/// Rust: `codes::checksum::parity_u64` +#[pyfunction] +#[pyo3(name = "parity_u64", signature = (x))] +pub fn pyfn_parity_u64(x: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::checksum::parity_u64(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Fletcher-16 checksum: a running byte sum and a running sum of that +/// sum, both modulo 255, packed into sixteen bits. +/// +/// The second accumulator is what makes it more than a sum: it weights each +/// byte by how many bytes follow it, so swapping two bytes changes the +/// result, which a plain sum cannot notice. Modulo 255 rather than 256 +/// because a modulus with a factor of two lets the high bits of a byte fall +/// out of the low accumulator entirely. +/// +/// Rust: `codes::checksum::checksum_fletcher16` +#[pyfunction] +#[pyo3(name = "checksum_fletcher16", signature = (data))] +pub fn pyfn_checksum_fletcher16<'py>(py: Python<'py>, data: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::checksum::checksum_fletcher16(&data))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Fletcher-32 checksum, over sixteen-bit words modulo 65535. +/// +/// Odd-length input is padded with a zero byte, which is the usual +/// convention and the reason Fletcher-32 cannot distinguish `"ab"` from +/// `"ab\0"`. +/// +/// Rust: `codes::checksum::checksum_fletcher32` +#[pyfunction] +#[pyo3(name = "checksum_fletcher32", signature = (data))] +pub fn pyfn_checksum_fletcher32<'py>(py: Python<'py>, data: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::checksum::checksum_fletcher32(&data))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Adler-32, as used by zlib: Fletcher's idea with a prime modulus. +/// +/// The accumulators start at one and zero and run modulo 65521, the largest +/// prime below `2^16`. The prime modulus spreads the values more evenly than +/// Fletcher's 65535, and the leading one makes the checksum of an empty +/// input distinguishable from the checksum of a run of zero bytes. +/// +/// Rust: `codes::checksum::adler32` +#[pyfunction] +#[pyo3(name = "adler32", signature = (data))] +pub fn pyfn_adler32<'py>(py: Python<'py>, data: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::checksum::adler32(&data))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// A cyclic redundancy check, in the parametric form every named CRC is an +/// instance of. +/// +/// The message is treated as a polynomial over `GF(2)`, shifted left by +/// `width` and divided by `poly`; the remainder is the check value. Because +/// the code is linear, the difference between a message and a corrupted one +/// has its own remainder, so a corruption goes unnoticed exactly when its +/// error pattern is itself a multiple of `poly` -- which no burst shorter +/// than `width + 1` can be, since `poly` has degree `width`. +/// +/// `init` seeds the register, so a run of leading zero bytes changes the +/// result; `xor_out` is applied at the end; `reflect` reverses the bits of +/// each input byte and of the final register, which is what the +/// bit-at-a-time hardware of a serial line does naturally. The named CRCs in +/// wide use all reflect input and output together or neither, so one flag +/// covers them. +/// +/// Panics: +/// Panics unless `width` is between 8 and 64. +/// +/// Rust: `codes::checksum::crc` +#[pyfunction] +#[pyo3(name = "crc", signature = (data, poly, width, init, xor_out, reflect_io))] +pub fn pyfn_crc<'py>(py: Python<'py>, data: Vec, poly: u64, width: u32, init: u64, xor_out: u64, reflect_io: bool) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::checksum::crc(&data, poly, width, init, xor_out, reflect_io))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// CRC-32 as used by Ethernet, zip, PNG and gzip. +/// +/// Polynomial `0x04C11DB7`, register seeded to all ones, reflected in and +/// out, complemented at the end. The check value of `"123456789"` is +/// `0xCBF43926`. +/// +/// Rust: `codes::checksum::crc32_ieee` +#[pyfunction] +#[pyo3(name = "crc32_ieee", signature = (data))] +pub fn pyfn_crc32_ieee<'py>(py: Python<'py>, data: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::checksum::crc32_ieee(&data))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// CRC-16/CCITT-FALSE: polynomial `0x1021`, seeded to all ones, unreflected, +/// no final xor. The check value of `"123456789"` is `0x29B1`. +/// +/// The name records a long-standing confusion: the true CCITT parameters +/// seed the register to zero, and this variant -- which is the one actually +/// deployed, in XMODEM's successors and in many microcontroller libraries -- +/// does not. +/// +/// Rust: `codes::checksum::crc16_ccitt` +#[pyfunction] +#[pyo3(name = "crc16_ccitt", signature = (data))] +pub fn pyfn_crc16_ccitt<'py>(py: Python<'py>, data: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::checksum::crc16_ccitt(&data))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// CRC-8/SMBUS: polynomial `0x07`, zero seed, unreflected. The check value +/// of `"123456789"` is `0xF4`. +/// +/// Rust: `codes::checksum::crc8` +#[pyfunction] +#[pyo3(name = "crc8", signature = (data))] +pub fn pyfn_crc8<'py>(py: Python<'py>, data: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::checksum::crc8(&data))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The 256-entry lookup table for a reflected 32-bit CRC. +/// +/// `poly` is the *reversed* polynomial -- `0xEDB88320` for CRC-32 -- because +/// a reflected CRC shifts right, and the table holds the remainder of each +/// possible byte. Processing a byte becomes one table lookup instead of +/// eight conditional shifts; the table is the loop unrolled once and cached. +/// +/// Rust: `codes::checksum::crc_table` +#[pyfunction] +#[pyo3(name = "crc_table", signature = (poly))] +pub fn pyfn_crc_table<'py>(py: Python<'py>, poly: u32) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::checksum::crc_table(poly))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_vec()) +} + +/// CRC-32 driven by a precomputed table rather than bit by bit. +/// +/// The same value as `crc32_ieee`, computed eight bits at a time. Pass the +/// table from `crc_table` with the reversed polynomial. +/// +/// Rust: `codes::checksum::crc32_with_table` +#[pyfunction] +#[pyo3(name = "crc32_with_table", signature = (data, table))] +pub fn pyfn_crc32_with_table<'py>(py: Python<'py>, data: Vec, table: Vec) -> PyResult { + let table = <[u32; 256]>::try_from(table).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 256 values, got {}", __v.len())))?; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::checksum::crc32_with_table(&data, &table))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Luhn checksum test, as used on payment card numbers. +/// +/// Doubling every second digit from the right and casting out nines catches +/// every single-digit error and every transposition of adjacent digits +/// except `09` against `90`, which it maps to the same sum. That one blind +/// spot is why Verhoeff and Damm exist. +/// +/// The check digit is the last element of `digits`. +/// +/// Panics: +/// Panics if any entry is above nine. +/// +/// Rust: `codes::checksum::luhn_check` +#[pyfunction] +#[pyo3(name = "luhn_check", signature = (digits))] +pub fn pyfn_luhn_check<'py>(py: Python<'py>, digits: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::checksum::luhn_check(&digits))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Luhn check digit that completes `payload`. +/// +/// Panics: +/// Panics if any entry is above nine. +/// +/// Rust: `codes::checksum::luhn_generate` +#[pyfunction] +#[pyo3(name = "luhn_generate", signature = (payload))] +pub fn pyfn_luhn_generate<'py>(py: Python<'py>, payload: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::checksum::luhn_generate(&payload))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// ISBN-10, whose check digit is a weighted sum modulo eleven. +/// +/// Weights ten down to one, and the modulus is prime, which is what lets it +/// catch every transposition -- swapping two digits changes the sum by a +/// non-zero multiple of their difference, and a prime modulus has no zero +/// divisors to hide that. The price is that the check digit sometimes has to +/// be ten, written `X`; pass it as the value `10`. +/// +/// Panics: +/// Panics unless there are ten entries, each at most nine, except the last +/// which may be ten. +/// +/// Rust: `codes::checksum::isbn10_check` +#[pyfunction] +#[pyo3(name = "isbn10_check", signature = (digits))] +pub fn pyfn_isbn10_check<'py>(py: Python<'py>, digits: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::checksum::isbn10_check(&digits))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// ISBN-13, the same numbering embedded in the EAN-13 scheme: alternating +/// weights of one and three modulo ten. +/// +/// The modulus is composite, so unlike ISBN-10 it misses transpositions of +/// adjacent digits differing by five -- but it never needs an `X`, which is +/// what the change bought. +/// +/// Panics: +/// Panics unless there are thirteen digits, each at most nine. +/// +/// Rust: `codes::checksum::isbn13_check` +#[pyfunction] +#[pyo3(name = "isbn13_check", signature = (digits))] +pub fn pyfn_isbn13_check<'py>(py: Python<'py>, digits: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::checksum::isbn13_check(&digits))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Verhoeff check, which catches every single-digit error and every +/// transposition of adjacent digits. +/// +/// It works by giving up on arithmetic modulo ten and using the dihedral +/// group of order ten instead, which is not commutative -- so swapping two +/// digits genuinely changes the product, with no cases left over. A +/// position-dependent permutation of order eight is applied first, which is +/// what extends the guarantee past the two digits nearest the check digit. +/// +/// The check digit is the last element of `digits`. +/// +/// Panics: +/// Panics if any entry is above nine. +/// +/// Rust: `codes::checksum::verhoeff_check` +#[pyfunction] +#[pyo3(name = "verhoeff_check", signature = (digits))] +pub fn pyfn_verhoeff_check<'py>(py: Python<'py>, digits: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::checksum::verhoeff_check(&digits))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Verhoeff check digit that completes `payload`. +/// +/// Panics: +/// Panics if any entry is above nine. +/// +/// Rust: `codes::checksum::verhoeff_generate` +#[pyfunction] +#[pyo3(name = "verhoeff_generate", signature = (payload))] +pub fn pyfn_verhoeff_generate<'py>(py: Python<'py>, payload: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::checksum::verhoeff_generate(&payload))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Damm check, with the same guarantees as Verhoeff and none of its +/// tables. +/// +/// One quasigroup operation folded across the digits, with no permutation +/// and no inverse: the check digit is simply the interim value, because the +/// table's diagonal is zero. Total anti-symmetry -- that `(a * b) * c` and +/// `(a * c) * b` differ whenever `b` and `c` do -- is exactly the property +/// that catches transpositions, and it is built into the table rather than +/// arranged around it. +/// +/// The check digit is the last element of `digits`. +/// +/// Panics: +/// Panics if any entry is above nine. +/// +/// Rust: `codes::checksum::damm_check` +#[pyfunction] +#[pyo3(name = "damm_check", signature = (digits))] +pub fn pyfn_damm_check<'py>(py: Python<'py>, digits: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::checksum::damm_check(&digits))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Damm check digit that completes `payload`. +/// +/// Panics: +/// Panics if any entry is above nine. +/// +/// Rust: `codes::checksum::damm_generate` +#[pyfunction] +#[pyo3(name = "damm_generate", signature = (payload))] +pub fn pyfn_damm_generate<'py>(py: Python<'py>, payload: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::checksum::damm_generate(&payload))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The number of bit positions in which two words differ. +/// +/// The distance a code needs to survive: a code whose words are all at least +/// `d` apart detects `d - 1` errors and corrects `(d - 1) / 2`, because a +/// received word within that radius of a codeword is within that radius of +/// no other. +/// +/// Rust: `codes::checksum::hamming_distance_bits` +#[pyfunction] +#[pyo3(name = "hamming_distance_bits", signature = (a, b))] +pub fn pyfn_hamming_distance_bits(a: u64, b: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::checksum::hamming_distance_bits(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The bitwise Hamming distance between two byte strings, or `None` if they +/// are different lengths. +/// +/// Rust: `codes::checksum::hamming_distance_bytes` +#[pyfunction] +#[pyo3(name = "hamming_distance_bytes", signature = (a, b))] +pub fn pyfn_hamming_distance_bytes<'py>(py: Python<'py>, a: Vec, b: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::checksum::hamming_distance_bytes(&a, &b))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_parity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_parity_u64, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_checksum_fletcher16, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_checksum_fletcher32, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_adler32, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_crc, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_crc32_ieee, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_crc16_ccitt, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_crc8, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_crc_table, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_crc32_with_table, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_luhn_check, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_luhn_generate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_isbn10_check, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_isbn13_check, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_verhoeff_check, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_verhoeff_generate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_damm_check, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_damm_generate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hamming_distance_bits, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hamming_distance_bytes, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_codes__compression.rs b/bindings/python/src/generated/m_codes__compression.rs new file mode 100644 index 0000000..3dd779f --- /dev/null +++ b/bindings/python/src/generated/m_codes__compression.rs @@ -0,0 +1,517 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Optimal prefix code lengths and codewords for the given symbol +/// frequencies, one entry per symbol. +/// +/// Returns `(codeword, length)` pairs; a symbol of zero frequency gets +/// `(0, 0)` and must not be encoded. The codes are canonical, so a decoder +/// needs only the lengths. +/// +/// Huffman's construction repeatedly merges the two least frequent symbols. +/// It is optimal, and the proof is short: in some optimal code the two rarest +/// symbols are siblings at the greatest depth, so merging them and solving +/// the smaller problem loses nothing. Optimal means no prefix code has a +/// smaller expected length -- not that it reaches the entropy, which it +/// cannot when the probabilities are not powers of two. +/// +/// Panics: +/// Panics on an empty frequency table. +/// +/// Rust: `codes::compression::huffman_build` +#[pyfunction] +#[pyo3(name = "huffman_build", signature = (freqs))] +pub fn pyfn_huffman_build<'py>(py: Python<'py>, freqs: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::compression::huffman_build(&freqs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Canonical codewords for the given code lengths. +/// +/// Symbols are ordered by length and then by index, and codewords are +/// assigned in increasing numeric order, doubling at each length increase. +/// Any two prefix codes with the same length multiset compress identically, +/// so a decoder can be handed the lengths alone -- which is why every real +/// format transmits lengths rather than a tree. +/// +/// Panics: +/// Panics if the lengths do not satisfy Kraft's inequality, since no prefix +/// code has them. +/// +/// Rust: `codes::compression::canonical_huffman` +#[pyfunction] +#[pyo3(name = "canonical_huffman", signature = (lengths))] +pub fn pyfn_canonical_huffman<'py>(py: Python<'py>, lengths: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::compression::canonical_huffman(&lengths))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Kraft sum of a set of code lengths: `sum 2^-l`. +/// +/// At most one for any prefix code, and exactly one when the code wastes +/// nothing -- which Huffman's always does, since a tree with an only child +/// could shorten that child by a bit. +/// +/// Rust: `codes::compression::kraft_sum` +#[pyfunction] +#[pyo3(name = "kraft_sum", signature = (lengths))] +pub fn pyfn_kraft_sum<'py>(py: Python<'py>, lengths: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::compression::kraft_sum(&lengths))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Huffman-codes a byte string, returning the packed bits, the code table, +/// and the number of bits that matter. +/// +/// Rust: `codes::compression::huffman_encode` +#[pyfunction] +#[pyo3(name = "huffman_encode", signature = (data))] +pub fn pyfn_huffman_encode<'py>(py: Python<'py>, data: Vec) -> PyResult<(Vec, Vec<(u64, u8)>, usize)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::compression::huffman_encode(&data))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1.into_iter().map(|__x| (__x.0, __x.1)).collect::>(), __v.2)) +} + +/// Decodes `n` symbols from a Huffman-coded bit string. +/// +/// Panics: +/// Panics if the bits do not spell out `n` valid codewords. +/// +/// Rust: `codes::compression::huffman_decode` +#[pyfunction] +#[pyo3(name = "huffman_decode", signature = (bits, table, n))] +pub fn pyfn_huffman_decode<'py>(py: Python<'py>, bits: Vec, table: Vec<(u64, u8)>, n: usize) -> PyResult> { + let table = table.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::compression::huffman_decode(&bits, &table, n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Shannon-Fano coding: split the frequency-sorted symbols into two halves of +/// as nearly equal weight as possible, and recurse. +/// +/// The older construction, and never better than Huffman: it decides the top +/// of the tree first and cannot revise, while Huffman builds from the leaves +/// and so is optimal. The gap is usually small and occasionally a whole bit +/// per symbol. +/// +/// Panics: +/// Panics on an empty frequency table. +/// +/// Rust: `codes::compression::shannon_fano` +#[pyfunction] +#[pyo3(name = "shannon_fano", signature = (freqs))] +pub fn pyfn_shannon_fano<'py>(py: Python<'py>, freqs: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::compression::shannon_fano(&freqs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// The average code length of a prefix code against the given frequencies, in +/// bits per symbol. +/// +/// Rust: `codes::compression::average_code_length` +#[pyfunction] +#[pyo3(name = "average_code_length", signature = (table, freqs))] +pub fn pyfn_average_code_length<'py>(py: Python<'py>, table: Vec<(u64, u8)>, freqs: Vec) -> PyResult { + let table = table.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::compression::average_code_length(&table, &freqs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Arithmetic coding against a fixed model of symbol frequencies. +/// +/// Where a prefix code must spend a whole number of bits on every symbol, +/// arithmetic coding narrows a single interval by a factor of each symbol's +/// probability and writes out one number identifying it. The cost of a +/// message is therefore `-log2` of its probability to within two bits *in +/// total*, not per symbol, which is what makes it beat Huffman whenever some +/// symbol is much more likely than a half. +/// +/// Panics: +/// Panics unless the model has one non-negative count per symbol value, the +/// total is between one and 65536, and every byte that occurs has a positive +/// count. +/// +/// Rust: `codes::compression::arithmetic_encode` +#[pyfunction] +#[pyo3(name = "arithmetic_encode", signature = (data, model))] +pub fn pyfn_arithmetic_encode<'py>(py: Python<'py>, data: Vec, model: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::compression::arithmetic_encode(&data, &model))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Decodes `n` symbols from an arithmetic-coded stream. +/// +/// Panics: +/// Panics under the same conditions as `arithmetic_encode`. +/// +/// Rust: `codes::compression::arithmetic_decode` +#[pyfunction] +#[pyo3(name = "arithmetic_decode", signature = (bits, model, n))] +pub fn pyfn_arithmetic_decode<'py>(py: Python<'py>, bits: Vec, model: Vec, n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::compression::arithmetic_decode(&bits, &model, n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// LZ77: replace repeats with references to earlier text. +/// +/// The window bounds how far back a reference may point and the lookahead how +/// long a match may be. A match is allowed to run past its own start -- an +/// offset of one with length twenty is a run of twenty identical bytes -- and +/// the decompressor copying one byte at a time handles that for free, which +/// is why run-length encoding falls out of LZ77 rather than needing to be +/// added to it. +/// +/// Panics: +/// Panics if the window or lookahead is zero. +/// +/// Rust: `codes::compression::lz77_compress` +#[pyfunction] +#[pyo3(name = "lz77_compress", signature = (data, window, lookahead))] +pub fn pyfn_lz77_compress(data: Vec, window: usize, lookahead: usize) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::compression::lz77_compress(&data, window, lookahead)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyLz77Token { inner: __x }).collect::>()) +} + +/// Rebuilds the original from LZ77 tokens. +/// +/// Panics: +/// Panics if a token points further back than the output so far. +/// +/// Rust: `codes::compression::lz77_decompress` +#[pyfunction] +#[pyo3(name = "lz77_decompress", signature = (tokens))] +pub fn pyfn_lz77_decompress<'py>(py: Python<'py>, tokens: Vec) -> PyResult> { + let tokens = tokens.into_iter().map(|__e| __e.inner).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::compression::lz77_decompress(&tokens))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// LZW: build a dictionary of every phrase seen plus one byte, and emit +/// dictionary indices. +/// +/// The decoder rebuilds the same dictionary from the same output, so nothing +/// has to be transmitted with the data -- which is what made it practical for +/// modems and printers with no memory to spare. +/// +/// Rust: `codes::compression::lzw_compress` +#[pyfunction] +#[pyo3(name = "lzw_compress", signature = (data))] +pub fn pyfn_lzw_compress<'py>(py: Python<'py>, data: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::compression::lzw_compress(&data))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Rebuilds the original from LZW codes. +/// +/// Panics: +/// Panics on a code the dictionary cannot yet contain. +/// +/// Rust: `codes::compression::lzw_decompress` +#[pyfunction] +#[pyo3(name = "lzw_decompress", signature = (codes))] +pub fn pyfn_lzw_decompress<'py>(py: Python<'py>, codes: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::compression::lzw_decompress(&codes))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Run-length encoding in the PackBits scheme. +/// +/// A control byte below 128 means "the next `n + 1` bytes are literal"; one +/// at or above means "repeat the next byte `257 - n` times". Incompressible +/// data grows by one byte in every 128, which is the price of never needing +/// an escape character. +/// +/// Rust: `codes::compression::rle_compress` +#[pyfunction] +#[pyo3(name = "rle_compress", signature = (data))] +pub fn pyfn_rle_compress<'py>(py: Python<'py>, data: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::compression::rle_compress(&data))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Rebuilds the original from PackBits run-length encoding. +/// +/// Panics: +/// Panics if the stream is truncated part way through a run or literal. +/// +/// Rust: `codes::compression::rle_decompress` +#[pyfunction] +#[pyo3(name = "rle_decompress", signature = (data))] +pub fn pyfn_rle_decompress<'py>(py: Python<'py>, data: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::compression::rle_decompress(&data))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The suffix array: the starting positions of the suffixes, in the order +/// those suffixes sort. +/// +/// Built by prefix doubling. After round `k` the suffixes are sorted by their +/// first `2^k` characters, and the next round sorts by pairs of the ranks +/// already computed -- so each round doubles the prefix length examined and +/// `log n` rounds settle it. Not the linear-time construction, but the +/// simplest one whose correctness is visible. +/// +/// Rust: `codes::compression::suffix_array` +#[pyfunction] +#[pyo3(name = "suffix_array", signature = (data))] +pub fn pyfn_suffix_array<'py>(py: Python<'py>, data: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::compression::suffix_array(&data))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The longest common prefix of each adjacent pair in the suffix array, by +/// Kasai's algorithm. +/// +/// `lcp[i]` is the overlap between the suffixes at `sa[i - 1]` and `sa[i]`, +/// with `lcp[0]` zero. Kasai's insight is that walking the suffixes in +/// *text* order lets the previous answer be reused: dropping the first +/// character of a suffix shortens its overlap with its neighbour by at most +/// one, so the total work is linear rather than quadratic. +/// +/// Panics: +/// Panics unless the suffix array matches the data's length. +/// +/// Rust: `codes::compression::lcp_array` +#[pyfunction] +#[pyo3(name = "lcp_array", signature = (data, sa))] +pub fn pyfn_lcp_array<'py>(py: Python<'py>, data: Vec, sa: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::compression::lcp_array(&data, &sa))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The longest substring that occurs at least twice, as `(start, length)`. +/// +/// The largest entry of the longest-common-prefix array, because two +/// occurrences of the same substring are two suffixes sharing that prefix, +/// and suffixes sharing a long prefix are adjacent in the suffix array. +/// Length zero when nothing repeats. +/// +/// Rust: `codes::compression::longest_repeated_substring` +#[pyfunction] +#[pyo3(name = "longest_repeated_substring", signature = (data))] +pub fn pyfn_longest_repeated_substring<'py>(py: Python<'py>, data: Vec) -> PyResult<(usize, usize)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::compression::longest_repeated_substring(&data))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// The Burrows-Wheeler transform: the last column of the sorted rotations, +/// and which row the original occupies. +/// +/// The transform is reversible and sorts nothing about the data itself -- it +/// is a permutation of the bytes. What it does is bring together the bytes +/// that precede similar contexts, so English text comes out in long runs of +/// the same letter, and a run-length or move-to-front stage that could do +/// nothing with the original then has plenty to work with. +/// +/// Rust: `codes::compression::bwt` +#[pyfunction] +#[pyo3(name = "bwt", signature = (data))] +pub fn pyfn_bwt<'py>(py: Python<'py>, data: Vec) -> PyResult<(Vec, usize)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::compression::bwt(&data))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Inverts the Burrows-Wheeler transform. +/// +/// The last column plus the row index is enough, because sorting the last +/// column gives the first, and the `i`-th occurrence of a byte in the last +/// column is the `i`-th in the first -- rotations sharing a first byte stay +/// in the same relative order. That correspondence is the whole inverse. +/// +/// Panics: +/// Panics if the index is outside the data. +/// +/// Rust: `codes::compression::ibwt` +#[pyfunction] +#[pyo3(name = "ibwt", signature = (data, idx))] +pub fn pyfn_ibwt<'py>(py: Python<'py>, data: Vec, idx: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::compression::ibwt(&data, idx))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Move-to-front coding: emit each byte's position in a list, then move it to +/// the front. +/// +/// It turns locality into small numbers. A stretch using only a few distinct +/// bytes -- which is what the Burrows-Wheeler transform produces -- becomes a +/// stretch of values near zero, and a stretch of one repeated byte becomes a +/// run of zeros, which an entropy coder or a run-length stage can then +/// exploit. +/// +/// Rust: `codes::compression::mtf_encode` +#[pyfunction] +#[pyo3(name = "mtf_encode", signature = (data))] +pub fn pyfn_mtf_encode<'py>(py: Python<'py>, data: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::compression::mtf_encode(&data))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Inverts move-to-front coding. +/// +/// Rust: `codes::compression::mtf_decode` +#[pyfunction] +#[pyo3(name = "mtf_decode", signature = (data))] +pub fn pyfn_mtf_decode<'py>(py: Python<'py>, data: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::compression::mtf_decode(&data))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Differences between consecutive bytes, modulo 256, with the first byte +/// kept as it is. +/// +/// Worth doing when the data is a slowly varying signal: a smooth ramp has +/// high byte entropy and near-zero difference entropy. +/// +/// Rust: `codes::compression::delta_encode` +#[pyfunction] +#[pyo3(name = "delta_encode", signature = (data))] +pub fn pyfn_delta_encode<'py>(py: Python<'py>, data: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::compression::delta_encode(&data))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Inverts delta coding. +/// +/// Rust: `codes::compression::delta_decode` +#[pyfunction] +#[pyo3(name = "delta_decode", signature = (data))] +pub fn pyfn_delta_decode<'py>(py: Python<'py>, data: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::compression::delta_decode(&data))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Shannon entropy of the byte histogram, in bits per byte. +/// +/// The floor for any coder that treats the bytes as independent draws. +/// Between zero, for a constant stream, and eight, for a uniform one. It is +/// not a floor for compression in general: a stream of a million alternating +/// bytes has an entropy of one bit per byte and compresses to nothing, since +/// the bytes are not independent. +/// +/// Rust: `codes::compression::entropy_bytes` +#[pyfunction] +#[pyo3(name = "entropy_bytes", signature = (data))] +pub fn pyfn_entropy_bytes<'py>(py: Python<'py>, data: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::compression::entropy_bytes(&data))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The size in bytes that the byte entropy allows, which no memoryless coder +/// can beat. +/// +/// Rust: `codes::compression::compression_bound` +#[pyfunction] +#[pyo3(name = "compression_bound", signature = (data))] +pub fn pyfn_compression_bound<'py>(py: Python<'py>, data: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::compression::compression_bound(&data))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The size the module's own best pipeline achieves, as a stand-in for the +/// incompressible content of the data. +/// +/// Kolmogorov complexity is not computable, and this is not an approximation +/// to it in any rigorous sense -- it is an upper bound that happens to behave +/// sensibly, which is what the practical literature uses it for. The pipeline +/// is Burrows-Wheeler, then move-to-front, then run lengths, then Huffman: +/// each stage exposes structure the next can spend. +/// +/// Rust: `codes::compression::kolmogorov_estimate_by_compressors` +#[pyfunction] +#[pyo3(name = "kolmogorov_estimate_by_compressors", signature = (data))] +pub fn pyfn_kolmogorov_estimate_by_compressors<'py>(py: Python<'py>, data: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::compression::kolmogorov_estimate_by_compressors(&data))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The normalized compression distance between two byte strings. +/// +/// `(C(ab) - min(C(a), C(b))) / max(C(a), C(b))`: if knowing `a` makes `b` +/// cheap to describe, they are close. Near zero for identical inputs and near +/// one for unrelated ones, and it needs no notion of what the data means, +/// which is why it gets used on genomes and on music alike. +/// +/// Rust: `codes::compression::normalized_compression_distance` +#[pyfunction] +#[pyo3(name = "normalized_compression_distance", signature = (a, b))] +pub fn pyfn_normalized_compression_distance<'py>(py: Python<'py>, a: Vec, b: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::compression::normalized_compression_distance(&a, &b))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_huffman_build, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_canonical_huffman, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kraft_sum, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_huffman_encode, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_huffman_decode, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_shannon_fano, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_average_code_length, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_arithmetic_encode, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_arithmetic_decode, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lz77_compress, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lz77_decompress, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lzw_compress, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lzw_decompress, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rle_compress, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rle_decompress, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_suffix_array, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lcp_array, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_longest_repeated_substring, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bwt, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ibwt, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mtf_encode, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mtf_decode, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_delta_encode, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_delta_decode, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_entropy_bytes, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_compression_bound, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kolmogorov_estimate_by_compressors, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_normalized_compression_distance, m)?)?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_codes__convolutional.rs b/bindings/python/src/generated/m_codes__convolutional.rs new file mode 100644 index 0000000..bc8bd15 --- /dev/null +++ b/bindings/python/src/generated/m_codes__convolutional.rs @@ -0,0 +1,293 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// A block interleaver: write the sequence into a rectangle row by row, read +/// it out column by column. +/// +/// Returns the permutation `pi` with `pi[i]` the source index of output `i`. +/// It spreads any run of `rows` consecutive positions to distance `rows` +/// apart, which is what turns a burst into scattered single errors that a +/// random-error code can handle. +/// +/// Panics: +/// Panics unless `rows` divides `n` and both are positive. +/// +/// Rust: `codes::convolutional::interleaver_block` +#[pyfunction] +#[pyo3(name = "interleaver_block", signature = (n, rows))] +pub fn pyfn_interleaver_block<'py>(py: Python<'py>, n: usize, rows: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::convolutional::interleaver_block(n, rows))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// A uniformly random interleaver. +/// +/// Rust: `codes::convolutional::interleaver_random` +#[pyfunction] +#[pyo3(name = "interleaver_random", signature = (n, rng))] +pub fn pyfn_interleaver_random(n: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::codes::convolutional::interleaver_random(n, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// A quadratic permutation polynomial interleaver: `pi(i) = f1 i + f2 i^2` +/// modulo `n`, the family LTE uses. +/// +/// It is a permutation exactly when `f1` is coprime to `n` and every prime +/// dividing `n` also divides `f2` -- conditions cheap enough to check, which +/// is the point: an LTE receiver reconstructs the interleaver from two +/// integers instead of storing a table of six thousand entries. +/// +/// Panics: +/// Panics unless the parameters give a permutation. +/// +/// Rust: `codes::convolutional::qpp_interleaver` +#[pyfunction] +#[pyo3(name = "qpp_interleaver", signature = (n, f1, f2))] +pub fn pyfn_qpp_interleaver<'py>(py: Python<'py>, n: usize, f1: usize, f2: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::convolutional::qpp_interleaver(n, f1, f2))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Transmits bits over an additive white Gaussian noise channel with binary +/// phase shift keying, returning the received samples. +/// +/// A zero bit is sent as `+1` and a one as `-1`, so the received value is +/// `±1` plus a Gaussian of variance `1 / (2 * 10^(snr_db/10))`. That variance +/// is the one that makes `snr_db` the symbol energy to noise density ratio +/// `Es/N0` in decibels. +/// +/// Rust: `codes::convolutional::awgn_channel` +#[pyfunction] +#[pyo3(name = "awgn_channel", signature = (bits, snr_db, rng))] +pub fn pyfn_awgn_channel(bits: Vec, snr_db: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::codes::convolutional::awgn_channel(&bits, snr_db, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The noise standard deviation for a given `Es/N0` in decibels, with unit +/// symbol energy. +/// +/// Rust: `codes::convolutional::awgn_sigma` +#[pyfunction] +#[pyo3(name = "awgn_sigma", signature = (snr_db))] +pub fn pyfn_awgn_sigma(snr_db: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::convolutional::awgn_sigma(snr_db)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The log-likelihood ratios a Gaussian channel implies, positive for a zero +/// bit. +/// +/// Rust: `codes::convolutional::llr_from_awgn` +#[pyfunction] +#[pyo3(name = "llr_from_awgn", signature = (samples, sigma))] +pub fn pyfn_llr_from_awgn<'py>(py: Python<'py>, samples: Vec, sigma: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::convolutional::llr_from_awgn(&samples, sigma))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Transmits bits over a binary symmetric channel that flips each with +/// probability `p`. +/// +/// Panics: +/// Panics unless `p` is in `[0, 1]`. +/// +/// Rust: `codes::convolutional::bsc_channel` +#[pyfunction] +#[pyo3(name = "bsc_channel", signature = (bits, p, rng))] +pub fn pyfn_bsc_channel(bits: Vec, p: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::codes::convolutional::bsc_channel(&bits, p, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Bit error rates against signal to noise ratio, for a convolutional code +/// decoded softly. +/// +/// `snr_db_range` is `Eb/N0` in decibels -- energy per *information* bit, +/// which is the only fair way to compare codes of different rates, since a +/// stronger code spends more channel symbols on each message bit and must be +/// charged for them. +/// +/// Panics: +/// Panics if `n_bits` is zero. +/// +/// Rust: `codes::convolutional::ber_simulation` +#[pyfunction] +#[pyo3(name = "ber_simulation", signature = (code, snr_db_range, n_bits, rng))] +pub fn pyfn_ber_simulation(code: crate::generated::types::PyConvolutionalCode, snr_db_range: Vec, n_bits: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let code = code.inner; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::codes::convolutional::ber_simulation(&code, &snr_db_range, n_bits, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Binary entropy in bits. +/// +/// Rust: `codes::convolutional::binary_entropy` +#[pyfunction] +#[pyo3(name = "binary_entropy", signature = (p))] +pub fn pyfn_binary_entropy(p: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::convolutional::binary_entropy(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The capacity of a binary symmetric channel: `1 - H(p)` bits per use. +/// +/// Panics: +/// Panics unless `p` is in `[0, 1]`. +/// +/// Rust: `codes::convolutional::capacity_bsc` +#[pyfunction] +#[pyo3(name = "capacity_bsc", signature = (p))] +pub fn pyfn_capacity_bsc(p: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::convolutional::capacity_bsc(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The capacity of a binary erasure channel: `1 - e` bits per use. +/// +/// The one channel whose capacity needs no argument: a fraction `e` of the +/// symbols never arrive, and the rest arrive perfectly. +/// +/// Panics: +/// Panics unless `e` is in `[0, 1]`. +/// +/// Rust: `codes::convolutional::capacity_bec` +#[pyfunction] +#[pyo3(name = "capacity_bec", signature = (e))] +pub fn pyfn_capacity_bec(e: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::convolutional::capacity_bec(e)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The capacity of a real additive white Gaussian noise channel with the +/// given signal to noise ratio: `0.5 log2(1 + snr)` bits per use. +/// +/// `snr` here is the ratio of signal power to noise *variance*. That is not +/// `Es/N0`: a real channel has variance `N0/2`, so the ratio to pass is +/// twice `Es/N0`. Comparing this against `channel_capacity_bpsk`, which +/// takes `Es/N0`, without that factor is the easy way to conclude that +/// restricting the input alphabet raises capacity. +/// +/// Panics: +/// Panics if the ratio is negative. +/// +/// Rust: `codes::convolutional::channel_capacity_awgn` +#[pyfunction] +#[pyo3(name = "channel_capacity_awgn", signature = (snr))] +pub fn pyfn_channel_capacity_awgn(snr: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::convolutional::channel_capacity_awgn(snr)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The capacity of a Gaussian channel whose input is restricted to `+/-1`. +/// +/// Restricting the input costs something: at high signal to noise the +/// unrestricted channel's capacity grows without bound while this saturates +/// at one bit per use, because one bit is all a binary symbol can carry. The +/// expectation has no closed form and is integrated numerically. +/// +/// Panics: +/// Panics if the ratio is negative. +/// +/// Rust: `codes::convolutional::channel_capacity_bpsk` +#[pyfunction] +#[pyo3(name = "channel_capacity_bpsk", signature = (snr))] +pub fn pyfn_channel_capacity_bpsk(snr: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::convolutional::channel_capacity_bpsk(snr)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The lowest `Eb/N0`, in decibels, at which a binary code of the given rate +/// can work. +/// +/// Found by bisecting `channel_capacity_bpsk` for the point where capacity +/// equals the rate, then converting from `Es/N0` to `Eb/N0` by dividing out +/// the rate. At rate one half the answer is about `0.187` decibels; as the +/// rate falls towards zero it approaches `-1.59`, which is `10 log10(ln 2)` +/// and is the limit for any code at any rate. +/// +/// Panics: +/// Panics unless the rate is in `(0, 1)`. +/// +/// Rust: `codes::convolutional::shannon_limit_bpsk` +#[pyfunction] +#[pyo3(name = "shannon_limit_bpsk", signature = (rate))] +pub fn pyfn_shannon_limit_bpsk(rate: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::convolutional::shannon_limit_bpsk(rate)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The same limit for a channel with no restriction on the input alphabet: +/// `(2^(2R) - 1) / (2R)`, in decibels. +/// +/// Always at or below `shannon_limit_bpsk`, since removing a restriction +/// cannot make a channel worse, and equal to it in the limit of low rate. +/// +/// Panics: +/// Panics unless the rate is positive. +/// +/// Rust: `codes::convolutional::shannon_limit_unconstrained` +#[pyfunction] +#[pyo3(name = "shannon_limit_unconstrained", signature = (rate))] +pub fn pyfn_shannon_limit_unconstrained(rate: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::convolutional::shannon_limit_unconstrained(rate)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_interleaver_block, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_interleaver_random, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_qpp_interleaver, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_awgn_channel, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_awgn_sigma, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_llr_from_awgn, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bsc_channel, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ber_simulation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_binary_entropy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_capacity_bsc, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_capacity_bec, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_channel_capacity_awgn, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_channel_capacity_bpsk, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_shannon_limit_bpsk, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_shannon_limit_unconstrained, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_codes__crypto_math.rs b/bindings/python/src/generated/m_codes__crypto_math.rs new file mode 100644 index 0000000..c39da03 --- /dev/null +++ b/bindings/python/src/generated/m_codes__crypto_math.rs @@ -0,0 +1,503 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Generates an RSA modulus and exponent pair: `(n, e, d)`. +/// +/// Two primes of about `bits / 2` each are drawn, `n` is their product, and +/// `d` inverts `e` modulo the Carmichael function of `n` -- the exponent of +/// the multiplicative group, which is the least value that works and so gives +/// the smallest `d`. The public exponent is 65537, whose binary form has two +/// set bits and therefore encrypts in seventeen squarings. +/// +/// Panics: +/// Panics unless `bits` is between 16 and 2048. Anything in that range is far +/// too small to protect anything. +/// +/// Rust: `codes::crypto_math::rsa_keygen` +#[pyfunction] +#[pyo3(name = "rsa_keygen", signature = (bits, rng))] +pub fn pyfn_rsa_keygen<'py>(py: Python<'py>, bits: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(pyo3::Bound<'py, pyo3::PyAny>, pyo3::Bound<'py, pyo3::PyAny>, pyo3::Bound<'py, pyo3::PyAny>)> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::codes::crypto_math::rsa_keygen(bits, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((crate::runtime::coerce::bigint_out(py, &__v.0)?, crate::runtime::coerce::bigint_out(py, &__v.1)?, crate::runtime::coerce::bigint_out(py, &__v.2)?)) +} + +/// Generates a key and keeps the primes, which the Chinese remainder form of +/// decryption needs. +/// +/// Panics: +/// Panics unless `bits` is between 16 and 2048. +/// +/// Rust: `codes::crypto_math::rsa_keygen_with_primes` +#[pyfunction] +#[pyo3(name = "rsa_keygen_with_primes", signature = (bits, rng))] +pub fn pyfn_rsa_keygen_with_primes<'py>(py: Python<'py>, bits: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(pyo3::Bound<'py, pyo3::PyAny>, pyo3::Bound<'py, pyo3::PyAny>, pyo3::Bound<'py, pyo3::PyAny>, pyo3::Bound<'py, pyo3::PyAny>, pyo3::Bound<'py, pyo3::PyAny>)> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::codes::crypto_math::rsa_keygen_with_primes(bits, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((crate::runtime::coerce::bigint_out(py, &__v.0)?, crate::runtime::coerce::bigint_out(py, &__v.1)?, crate::runtime::coerce::bigint_out(py, &__v.2)?, crate::runtime::coerce::bigint_out(py, &__v.3)?, crate::runtime::coerce::bigint_out(py, &__v.4)?)) +} + +/// Textbook RSA encryption: `m^e` modulo `n`. +/// +/// Deterministic, and therefore not a secure encryption scheme on its own -- +/// the same message always gives the same ciphertext, so an attacker who can +/// guess the plaintext can confirm the guess. Real use pads the message with +/// randomness first. +/// +/// Rust: `codes::crypto_math::rsa_encrypt` +#[pyfunction] +#[pyo3(name = "rsa_encrypt", signature = (m, e, n))] +pub fn pyfn_rsa_encrypt<'py>(py: Python<'py>, m: crate::runtime::coerce::BigIntArg, e: crate::runtime::coerce::BigIntArg, n: crate::runtime::coerce::BigIntArg) -> PyResult> { + let m = m.0; + let e = e.0; + let n = n.0; + let __r = crate::runtime::guard(|| rust_physics_engine::codes::crypto_math::rsa_encrypt(&m, &e, &n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// Textbook RSA decryption: `c^d` modulo `n`. +/// +/// Rust: `codes::crypto_math::rsa_decrypt` +#[pyfunction] +#[pyo3(name = "rsa_decrypt", signature = (c, d, n))] +pub fn pyfn_rsa_decrypt<'py>(py: Python<'py>, c: crate::runtime::coerce::BigIntArg, d: crate::runtime::coerce::BigIntArg, n: crate::runtime::coerce::BigIntArg) -> PyResult> { + let c = c.0; + let d = d.0; + let n = n.0; + let __r = crate::runtime::guard(|| rust_physics_engine::codes::crypto_math::rsa_decrypt(&c, &d, &n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// Decryption through the Chinese remainder theorem, given the two primes. +/// +/// Working modulo `p` and `q` separately and recombining costs about a +/// quarter of the work, since modular exponentiation is cubic in the operand +/// size and the operands are half as long. Every real implementation does +/// this, which is also why a fault during one of the two halves famously +/// reveals the factorisation. +/// +/// Panics: +/// Panics if `p` and `q` are not coprime, so that the recombination has no +/// inverse. +/// +/// Rust: `codes::crypto_math::rsa_crt_decrypt` +#[pyfunction] +#[pyo3(name = "rsa_crt_decrypt", signature = (c, d, p, q))] +pub fn pyfn_rsa_crt_decrypt<'py>(py: Python<'py>, c: crate::runtime::coerce::BigIntArg, d: crate::runtime::coerce::BigIntArg, p: crate::runtime::coerce::BigIntArg, q: crate::runtime::coerce::BigIntArg) -> PyResult> { + let c = c.0; + let d = d.0; + let p = p.0; + let q = q.0; + let __r = crate::runtime::guard(|| rust_physics_engine::codes::crypto_math::rsa_crt_decrypt(&c, &d, &p, &q)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// A Diffie-Hellman exchange in full: both parties' key pairs and the shared +/// secret they arrive at. +/// +/// Returns `((a, A), (b, B), s)` where `A = g^a`, `B = g^b` and +/// `s = B^a = A^b`, all modulo `p`. The exchange works because +/// exponentiation commutes; it is secure only if recovering `a` from `g^a` is +/// hard, which needs `p` to be a large safe prime and `g` to generate a large +/// subgroup. Neither is checked here. +/// +/// Panics: +/// Panics unless `p` is at least three. +/// +/// Rust: `codes::crypto_math::diffie_hellman_demo` +#[pyfunction] +#[pyo3(name = "diffie_hellman_demo", signature = (p, g, rng))] +pub fn pyfn_diffie_hellman_demo<'py>(py: Python<'py>, p: crate::runtime::coerce::BigIntArg, g: crate::runtime::coerce::BigIntArg, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<((pyo3::Bound<'py, pyo3::PyAny>, pyo3::Bound<'py, pyo3::PyAny>), (pyo3::Bound<'py, pyo3::PyAny>, pyo3::Bound<'py, pyo3::PyAny>), pyo3::Bound<'py, pyo3::PyAny>)> { + let p = p.0; + let g = g.0; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::codes::crypto_math::diffie_hellman_demo(&p, &g, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(((crate::runtime::coerce::bigint_out(py, &__v.0.0)?, crate::runtime::coerce::bigint_out(py, &__v.0.1)?), (crate::runtime::coerce::bigint_out(py, &__v.1.0)?, crate::runtime::coerce::bigint_out(py, &__v.1.1)?), crate::runtime::coerce::bigint_out(py, &__v.2)?)) +} + +/// An elliptic curve Diffie-Hellman exchange in full. +/// +/// Returns `((a, aG), (b, bG), s)`. The same construction as the +/// multiplicative version, in a group where the best known attack is +/// square-root time rather than sub-exponential -- which is why a 256-bit +/// curve stands against a 3072-bit modulus. +/// +/// Panics: +/// Panics if the base point is not on the curve. +/// +/// Rust: `codes::crypto_math::ecdh_demo` +#[pyfunction] +#[pyo3(name = "ecdh_demo", signature = (curve, g, order, rng))] +pub fn pyfn_ecdh_demo<'py>(py: Python<'py>, curve: crate::generated::types::PyEcCurve, g: crate::generated::types::PyEcPoint, order: crate::runtime::coerce::BigIntArg, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<((pyo3::Bound<'py, pyo3::PyAny>, crate::generated::types::PyEcPoint), (pyo3::Bound<'py, pyo3::PyAny>, crate::generated::types::PyEcPoint), crate::generated::types::PyEcPoint)> { + let curve = curve.inner; + let g = g.inner; + let order = order.0; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::codes::crypto_math::ecdh_demo(&curve, &g, &order, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(((crate::runtime::coerce::bigint_out(py, &__v.0.0)?, crate::generated::types::PyEcPoint { inner: __v.0.1 }), (crate::runtime::coerce::bigint_out(py, &__v.1.0)?, crate::generated::types::PyEcPoint { inner: __v.1.1 }), crate::generated::types::PyEcPoint { inner: __v.2 })) +} + +/// The number of points on a small curve, including infinity. +/// +/// Panics: +/// Panics if the field has more than a million elements. +/// +/// Rust: `codes::crypto_math::ec_count_points_small` +#[pyfunction] +#[pyo3(name = "ec_count_points_small", signature = (curve))] +pub fn pyfn_ec_count_points_small(curve: crate::generated::types::PyEcCurve) -> PyResult { + let curve = curve.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::codes::crypto_math::ec_count_points_small(&curve)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Whether a point count satisfies Hasse's theorem. +/// +/// The count lies within `2 sqrt(p)` of `p + 1`. That is a remarkably tight +/// bound -- the group is always about as large as the field, never a constant +/// factor away -- and it is what makes a curve's security predictable from +/// its field size alone. +/// +/// Rust: `codes::crypto_math::hasse_bound_check` +#[pyfunction] +#[pyo3(name = "hasse_bound_check", signature = (count, p))] +pub fn pyfn_hasse_bound_check(count: u64, p: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::crypto_math::hasse_bound_check(count, p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Splits a secret into `n` shares of which any `k` suffice. +/// +/// The secret is the constant term of a random polynomial of degree `k - 1` +/// over `F_prime`, and a share is that polynomial's value at a non-zero +/// point. Any `k` points determine the polynomial by interpolation, and any +/// `k - 1` leave the constant term uniformly distributed -- so fewer than `k` +/// shares give not merely a hard problem but no information at all. That is +/// what makes the scheme *perfect*, and it is rare. +/// +/// Panics: +/// Panics unless `1 <= k <= n`, `n` is below the prime, and the secret is a +/// non-negative residue below it. +/// +/// Rust: `codes::crypto_math::shamir_split` +#[pyfunction] +#[pyo3(name = "shamir_split", signature = (secret, k, n, prime, rng))] +pub fn pyfn_shamir_split<'py>(py: Python<'py>, secret: crate::runtime::coerce::BigIntArg, k: usize, n: usize, prime: crate::runtime::coerce::BigIntArg, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult)>> { + let secret = secret.0; + let prime = prime.0; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::codes::crypto_math::shamir_split(&secret, k, n, &prime, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| -> PyResult<(u64, pyo3::Bound<'py, pyo3::PyAny>)> { Ok((__x.0, crate::runtime::coerce::bigint_out(py, &__x.1)?)) }).collect::>>()?) +} + +/// Recovers the secret from any `k` shares by Lagrange interpolation at zero. +/// +/// Panics: +/// Panics on an empty share list, on a repeated abscissa, or if the modulus +/// is not prime enough for the required inverses to exist. +/// +/// Rust: `codes::crypto_math::shamir_reconstruct` +#[pyfunction] +#[pyo3(name = "shamir_reconstruct", signature = (shares, prime))] +pub fn pyfn_shamir_reconstruct<'py>(py: Python<'py>, shares: Vec<(u64, crate::runtime::coerce::BigIntArg)>, prime: crate::runtime::coerce::BigIntArg) -> PyResult> { + let shares = shares.into_iter().map(|__e| (__e.0, __e.1.0)).collect::>(); + let prime = prime.0; + let __r = crate::runtime::guard(|| rust_physics_engine::codes::crypto_math::shamir_reconstruct(&shares, &prime)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// Exclusive-or of the data with a repeating key. +/// +/// With a key as long as the message, drawn uniformly and never reused, this +/// is the one cipher with a proof of perfect secrecy: the ciphertext is +/// independent of the plaintext, so an adversary with unlimited computation +/// learns nothing. With a short key repeated, it is a Vigenere cipher and +/// `vigenere_break` undoes it. The gap between those two is entirely the +/// key. +/// +/// Panics: +/// Panics on an empty key. +/// +/// Rust: `codes::crypto_math::one_time_pad` +#[pyfunction] +#[pyo3(name = "one_time_pad", signature = (data, key))] +pub fn pyfn_one_time_pad<'py>(py: Python<'py>, data: Vec, key: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::crypto_math::one_time_pad(&data, &key))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// A Fibonacci linear feedback shift register: `n` output bits from a state +/// and a tap mask. +/// +/// The new bit is the parity of the tapped positions, and the register shifts +/// right. The output is a linear recurrence over `GF(2)`, which is what makes +/// it fast, and also what makes it hopeless as a cipher on its own: +/// `berlekamp_massey_attack` recovers the whole register from twice its +/// length in output. +/// +/// Tap bit zero, or the step map is not reversible and the register cannot +/// reach every state -- see `lfsr_period`. +/// +/// Panics: +/// Panics on a zero tap mask. +/// +/// Rust: `codes::crypto_math::lfsr` +#[pyfunction] +#[pyo3(name = "lfsr", signature = (taps, state, n))] +pub fn pyfn_lfsr<'py>(py: Python<'py>, taps: u64, state: u64, n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::crypto_math::lfsr(taps, state, n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The period of a shift register of the given width, by running it until it +/// repeats. +/// +/// A width-`w` register has at most `2^w - 1` states before it must repeat, +/// and reaches that only for a *primitive* tap polynomial. The all-zero state +/// is absorbing, which is why the maximum is one short of the state count. +/// +/// The step map is a bijection only when bit zero is tapped: without it, the +/// outgoing bit does not influence the feedback, two states share an image, +/// and the register runs into a cycle it can never leave and never started +/// on. Returns zero in that case, meaning the register never comes back. +/// +/// Rust: `codes::crypto_math::lfsr_period` +#[pyfunction] +#[pyo3(name = "lfsr_period", signature = (taps, width))] +pub fn pyfn_lfsr_period(taps: u64, width: u32) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::crypto_math::lfsr_period(taps, width)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Recovers the shortest linear recurrence a bit stream satisfies, as +/// `(length, taps)`. +/// +/// The Berlekamp-Massey algorithm, over `GF(2)`. Given `2L` bits of output +/// from a register of length `L` it returns that register, which is why a +/// bare shift register is not a cipher: the keystream reveals the key +/// generator in time linear in its size. +/// +/// Rust: `codes::crypto_math::berlekamp_massey_attack` +#[pyfunction] +#[pyo3(name = "berlekamp_massey_attack", signature = (stream))] +pub fn pyfn_berlekamp_massey_attack<'py>(py: Python<'py>, stream: Vec) -> PyResult<(u64, u64)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::crypto_math::berlekamp_massey_attack(&stream))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// How close a hash comes to flipping half its output bits when one input bit +/// changes. +/// +/// Returns the mean fraction of output bits that flip. A good hash sits at a +/// half: every output bit should be an unbiased, independent-looking function +/// of every input bit, so that no partial information about the input +/// survives. A value far from a half is a structural weakness a distinguisher +/// can be built from. +/// +/// Panics: +/// Panics if `trials` is zero. +/// +/// Rust: `codes::crypto_math::hash_avalanche_test` +#[pyfunction] +#[pyo3(name = "hash_avalanche_test", signature = (h, trials, rng))] +pub fn pyfn_hash_avalanche_test(h: pyo3::Py, trials: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let __cb_h = std::rc::Rc::new(crate::runtime::Callback::new(h)); + let h = { let __cb = __cb_h.clone(); move |__a0: &[u8]| -> u64 { __cb.call::<_, u64>((__a0.to_vec(),), 0) } }; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::codes::crypto_math::hash_avalanche_test(&h, trials, &mut rng.inner)); + crate::runtime::callback::check(&[&__cb_h], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The number of samples at which a collision becomes likely for an output of +/// `n_bits`. +/// +/// About `2^(n/2)`, up to a constant: with `k` samples there are about +/// `k^2 / 2` pairs and each collides with probability `2^-n`, so the count of +/// collisions reaches one near the square root. It is why a 128-bit hash +/// offers 64 bits of collision resistance, not 128. +/// +/// Rust: `codes::crypto_math::birthday_bound` +#[pyfunction] +#[pyo3(name = "birthday_bound", signature = (n_bits))] +pub fn pyfn_birthday_bound(n_bits: u32) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::crypto_math::birthday_bound(n_bits)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The frequency of each letter, ignoring everything else, as fractions +/// summing to one. +/// +/// Rust: `codes::crypto_math::frequency_analysis` +#[pyfunction] +#[pyo3(name = "frequency_analysis", signature = (text))] +pub fn pyfn_frequency_analysis<'py>(py: Python<'py>, text: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::crypto_math::frequency_analysis(&text))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_vec()) +} + +/// The index of coincidence: the chance that two letters drawn at random from +/// the text are the same. +/// +/// About `0.066` for English and `0.038` for a uniform jumble. Because it is +/// unchanged by a substitution -- relabelling the letters does not change how +/// often two match -- it tells a monoalphabetic cipher from a polyalphabetic +/// one without any guess about the key, which is what makes it the first +/// measurement to take. +/// +/// Rust: `codes::crypto_math::index_of_coincidence` +#[pyfunction] +#[pyo3(name = "index_of_coincidence", signature = (text))] +pub fn pyfn_index_of_coincidence<'py>(py: Python<'py>, text: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::crypto_math::index_of_coincidence(&text))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Candidate key lengths from repeated trigrams, as Kasiski proposed. +/// +/// A trigram repeating in the ciphertext usually means the same plaintext +/// trigram met the same stretch of key, so the gap between the two is a +/// multiple of the key length. Returns the lengths that divide the most gaps, +/// best first. +/// +/// Rust: `codes::crypto_math::kasiski_examination` +#[pyfunction] +#[pyo3(name = "kasiski_examination", signature = (text))] +pub fn pyfn_kasiski_examination<'py>(py: Python<'py>, text: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::crypto_math::kasiski_examination(&text))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Caesar shift that best matches English letter frequencies. +/// +/// Scored by the dot product of the observed and expected distributions, +/// which is largest when the two line up -- the same statistic as chi-squared +/// scoring, with the arithmetic the other way up. +/// +/// Rust: `codes::crypto_math::caesar_break` +#[pyfunction] +#[pyo3(name = "caesar_break", signature = (text))] +pub fn pyfn_caesar_break<'py>(py: Python<'py>, text: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::crypto_math::caesar_break(&text))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The most likely Vigenere key, searching lengths up to `max_key`. +/// +/// The key length is chosen by the average index of coincidence of the +/// columns -- at the true length each column is a Caesar shift of English and +/// so looks like English, and at any other length the columns are jumbled -- +/// and each column is then solved as its own Caesar shift. +/// +/// Panics: +/// Panics if `max_key` is zero. +/// +/// Rust: `codes::crypto_math::vigenere_break` +#[pyfunction] +#[pyo3(name = "vigenere_break", signature = (text, max_key))] +pub fn pyfn_vigenere_break<'py>(py: Python<'py>, text: Vec, max_key: usize) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::crypto_math::vigenere_break(&text, max_key))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_string()) +} + +/// The permutation a perfect riffle shuffle applies to `n` cards. +/// +/// An *out* shuffle keeps the top card on top; an *in* shuffle pushes it to +/// second. Eight out-shuffles restore a 52-card deck and 52 in-shuffles do, +/// which is the standard demonstration that a deterministic shuffle is no +/// shuffle at all. +/// +/// Panics: +/// Panics unless `n` is positive and even. +/// +/// Rust: `codes::crypto_math::perfect_shuffle_permutation` +#[pyfunction] +#[pyo3(name = "perfect_shuffle_permutation", signature = (n, out))] +pub fn pyfn_perfect_shuffle_permutation<'py>(py: Python<'py>, n: usize, out: bool) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::crypto_math::perfect_shuffle_permutation(n, out))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// How many times a permutation must be applied before everything returns +/// home: the least common multiple of its cycle lengths. +/// +/// Panics: +/// Panics unless the input is a permutation of `0..n`. +/// +/// Rust: `codes::crypto_math::permutation_cipher_period` +#[pyfunction] +#[pyo3(name = "permutation_cipher_period", signature = (perm))] +pub fn pyfn_permutation_cipher_period<'py>(py: Python<'py>, perm: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::crypto_math::permutation_cipher_period(&perm))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_rsa_keygen, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rsa_keygen_with_primes, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rsa_encrypt, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rsa_decrypt, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rsa_crt_decrypt, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_diffie_hellman_demo, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ecdh_demo, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ec_count_points_small, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hasse_bound_check, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_shamir_split, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_shamir_reconstruct, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_one_time_pad, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lfsr, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lfsr_period, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_berlekamp_massey_attack, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hash_avalanche_test, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_birthday_bound, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_frequency_analysis, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_index_of_coincidence, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kasiski_examination, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_caesar_break, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_vigenere_break, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_perfect_shuffle_permutation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_permutation_cipher_period, m)?)?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_codes__reed_solomon.rs b/bindings/python/src/generated/m_codes__reed_solomon.rs new file mode 100644 index 0000000..fe8cdaf --- /dev/null +++ b/bindings/python/src/generated/m_codes__reed_solomon.rs @@ -0,0 +1,93 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// `RS(255, 223)`, the CCSDS telemetry standard: sixteen correctable symbol +/// errors in a 255-byte frame, used on essentially every deep space mission +/// since Voyager. +/// +/// Rust: `codes::reed_solomon::rs_ccsds` +#[pyfunction] +#[pyo3(name = "rs_ccsds", signature = ())] +pub fn pyfn_rs_ccsds() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::reed_solomon::rs_ccsds()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyReedSolomon { inner: __v }) +} + +/// The Reed-Solomon block a QR code of the given version uses at its lowest +/// error correction level. +/// +/// Panics: +/// Panics unless the version is between one and four, the range tabulated +/// here. +/// +/// Rust: `codes::reed_solomon::rs_qr_code` +#[pyfunction] +#[pyo3(name = "rs_qr_code", signature = (version))] +pub fn pyfn_rs_qr_code(version: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::reed_solomon::rs_qr_code(version)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyReedSolomon { inner: __v }) +} + +/// `RS(32, 28)`, the outer code of the cross-interleaved scheme on a compact +/// disc and its descendants. +/// +/// Rust: `codes::reed_solomon::rs_dvd` +#[pyfunction] +#[pyo3(name = "rs_dvd", signature = ())] +pub fn pyfn_rs_dvd() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::reed_solomon::rs_dvd()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyReedSolomon { inner: __v }) +} + +/// The generator polynomials of every binary cyclic code of length `n`, as +/// the divisors of `x^n - 1` over `GF(2)`. +/// +/// A cyclic code of length `n` is exactly an ideal in `GF(2)[x] / (x^n - 1)`, +/// and every such ideal is generated by a divisor of `x^n - 1`. So the +/// cyclic codes of a given length are in bijection with those divisors, and +/// listing them lists the codes. Returned constant term first. +/// +/// Panics: +/// Panics unless `n` is odd and at most 31 -- an even `n` makes `x^n - 1` +/// non-squarefree in characteristic two, and the enumeration is exponential. +/// +/// Rust: `codes::reed_solomon::cyclic_code_generators` +#[pyfunction] +#[pyo3(name = "cyclic_code_generators", signature = (n))] +pub fn pyfn_cyclic_code_generators<'py>(py: Python<'py>, n: usize) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::codes::reed_solomon::cyclic_code_generators(n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_rs_ccsds, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rs_qr_code, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rs_dvd, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cyclic_code_generators, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_color_science.rs b/bindings/python/src/generated/m_color_science.rs new file mode 100644 index 0000000..956e544 --- /dev/null +++ b/bindings/python/src/generated/m_color_science.rs @@ -0,0 +1,196 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Convert a visible light wavelength (380-780 nm) to linear RGB in [0, 1]. +/// +/// Uses a standard piecewise approximation with intensity falloff at the +/// edges of the visible spectrum. +/// +/// Rust: `color_science::wavelength_to_rgb` +#[pyfunction] +#[pyo3(name = "wavelength_to_rgb", signature = (wavelength_nm))] +pub fn pyfn_wavelength_to_rgb(wavelength_nm: f64) -> PyResult<(f64, f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::color_science::wavelength_to_rgb(wavelength_nm)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// Convert a blackbody temperature (1000-40000 K) to linear RGB in [0, 1]. +/// +/// Uses the Tanner Helland approximation. +/// +/// Rust: `color_science::blackbody_to_rgb` +#[pyfunction] +#[pyo3(name = "blackbody_to_rgb", signature = (temperature_k))] +pub fn pyfn_blackbody_to_rgb(temperature_k: f64) -> PyResult<(f64, f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::color_science::blackbody_to_rgb(temperature_k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// Convert linear RGB `[0,1]` to HSV. H in `[0,360)`, S and V in `[0,1]`. +/// +/// Rust: `color_science::rgb_to_hsv` +#[pyfunction] +#[pyo3(name = "rgb_to_hsv", signature = (r, g, b))] +pub fn pyfn_rgb_to_hsv(r: f64, g: f64, b: f64) -> PyResult<(f64, f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::color_science::rgb_to_hsv(r, g, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// Convert HSV to linear RGB. H in `[0,360)`, S and V in `[0,1]`. +/// +/// Rust: `color_science::hsv_to_rgb` +#[pyfunction] +#[pyo3(name = "hsv_to_rgb", signature = (h, s, v))] +pub fn pyfn_hsv_to_rgb(h: f64, s: f64, v: f64) -> PyResult<(f64, f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::color_science::hsv_to_rgb(h, s, v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// Convert linear RGB `[0,1]` to HSL. H in `[0,360)`, S and L in `[0,1]`. +/// +/// Rust: `color_science::rgb_to_hsl` +#[pyfunction] +#[pyo3(name = "rgb_to_hsl", signature = (r, g, b))] +pub fn pyfn_rgb_to_hsl(r: f64, g: f64, b: f64) -> PyResult<(f64, f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::color_science::rgb_to_hsl(r, g, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// Convert HSL to linear RGB. H in `[0,360)`, S and L in `[0,1]`. +/// +/// Rust: `color_science::hsl_to_rgb` +#[pyfunction] +#[pyo3(name = "hsl_to_rgb", signature = (h, s, l))] +pub fn pyfn_hsl_to_rgb(h: f64, s: f64, l: f64) -> PyResult<(f64, f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::color_science::hsl_to_rgb(h, s, l)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// Apply sRGB gamma correction to a linear channel value. +/// +/// Rust: `color_science::linear_to_srgb` +#[pyfunction] +#[pyo3(name = "linear_to_srgb", signature = (c))] +pub fn pyfn_linear_to_srgb(c: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::color_science::linear_to_srgb(c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert an sRGB gamma-encoded channel value back to linear. +/// +/// Rust: `color_science::srgb_to_linear` +#[pyfunction] +#[pyo3(name = "srgb_to_linear", signature = (c))] +pub fn pyfn_srgb_to_linear(c: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::color_science::srgb_to_linear(c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert linear sRGB to CIE XYZ (D65 illuminant). +/// +/// Rust: `color_science::rgb_to_xyz` +#[pyfunction] +#[pyo3(name = "rgb_to_xyz", signature = (r, g, b))] +pub fn pyfn_rgb_to_xyz(r: f64, g: f64, b: f64) -> PyResult<(f64, f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::color_science::rgb_to_xyz(r, g, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// Convert CIE XYZ (D65 illuminant) to linear sRGB. +/// +/// Rust: `color_science::xyz_to_rgb` +#[pyfunction] +#[pyo3(name = "xyz_to_rgb", signature = (x, y, z))] +pub fn pyfn_xyz_to_rgb(x: f64, y: f64, z: f64) -> PyResult<(f64, f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::color_science::xyz_to_rgb(x, y, z)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// Compute correlated color temperature from CIE xy chromaticity using +/// McCamy's approximation. +/// +/// Rust: `color_science::correlated_color_temperature` +#[pyfunction] +#[pyo3(name = "correlated_color_temperature", signature = (x, y))] +pub fn pyfn_correlated_color_temperature(x: f64, y: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::color_science::correlated_color_temperature(x, y)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Euclidean color difference in RGB space. +/// +/// Rust: `color_science::color_difference_euclidean` +#[pyfunction] +#[pyo3(name = "color_difference_euclidean", signature = (r1, g1, b1, r2, g2, b2))] +pub fn pyfn_color_difference_euclidean(r1: f64, g1: f64, b1: f64, r2: f64, g2: f64, b2: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::color_science::color_difference_euclidean(r1, g1, b1, r2, g2, b2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Relative luminance per ITU-R BT.709. +/// +/// Rust: `color_science::luminance` +#[pyfunction] +#[pyo3(name = "luminance", signature = (r, g, b))] +pub fn pyfn_luminance(r: f64, g: f64, b: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::color_science::luminance(r, g, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// WCAG contrast ratio between two relative luminance values. +/// +/// Rust: `color_science::contrast_ratio` +#[pyfunction] +#[pyo3(name = "contrast_ratio", signature = (l1, l2))] +pub fn pyfn_contrast_ratio(l1: f64, l2: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::color_science::contrast_ratio(l1, l2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_wavelength_to_rgb, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_blackbody_to_rgb, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rgb_to_hsv, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hsv_to_rgb, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rgb_to_hsl, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hsl_to_rgb, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_linear_to_srgb, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_srgb_to_linear, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rgb_to_xyz, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_xyz_to_rgb, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_correlated_color_temperature, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_color_difference_euclidean, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_luminance, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_contrast_ratio, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_continuum_mechanics.rs b/bindings/python/src/generated/m_continuum_mechanics.rs new file mode 100644 index 0000000..14f980e --- /dev/null +++ b/bindings/python/src/generated/m_continuum_mechanics.rs @@ -0,0 +1,278 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Constructs a symmetric 3x3 Cauchy stress tensor from six independent components. +/// +/// Rust: `continuum_mechanics::stress_tensor` +#[pyfunction] +#[pyo3(name = "stress_tensor", signature = (sxx, syy, szz, sxy, sxz, syz))] +pub fn pyfn_stress_tensor(sxx: f64, syy: f64, szz: f64, sxy: f64, sxz: f64, syz: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::continuum_mechanics::stress_tensor(sxx, syy, szz, sxy, sxz, syz)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMat3 { inner: __v }) +} + +/// Computes the three stress invariants (I1, I2, I3) of a symmetric stress tensor. +/// +/// I1 = tr(sigma), I2 = (tr^2 - tr(sigma^2))/2, I3 = det(sigma). +/// +/// Rust: `continuum_mechanics::stress_invariants` +#[pyfunction] +#[pyo3(name = "stress_invariants", signature = (stress))] +pub fn pyfn_stress_invariants(stress: crate::generated::types::PyMat3) -> PyResult<(f64, f64, f64)> { + let stress = stress.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::continuum_mechanics::stress_invariants(&stress)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// Computes the three principal stresses (eigenvalues) of a symmetric 3x3 tensor, +/// returned in descending order: sigma1 >= sigma2 >= sigma3. +/// +/// Uses the analytical cubic solution via the characteristic equation det(sigma - lambda*I) = 0. +/// +/// Rust: `continuum_mechanics::principal_stresses` +#[pyfunction] +#[pyo3(name = "principal_stresses", signature = (stress))] +pub fn pyfn_principal_stresses<'py>(py: Python<'py>, stress: crate::generated::types::PyMat3) -> PyResult> { + let stress = stress.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::continuum_mechanics::principal_stresses(&stress))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_vec()) +} + +/// Hydrostatic (mean) stress: sigma_h = tr(sigma) / 3. +/// +/// Rust: `continuum_mechanics::hydrostatic_stress` +#[pyfunction] +#[pyo3(name = "hydrostatic_stress", signature = (stress))] +pub fn pyfn_hydrostatic_stress(stress: crate::generated::types::PyMat3) -> PyResult { + let stress = stress.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::continuum_mechanics::hydrostatic_stress(&stress)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Deviatoric stress tensor: s = sigma - sigma_h * I. +/// +/// Rust: `continuum_mechanics::deviatoric_stress` +#[pyfunction] +#[pyo3(name = "deviatoric_stress", signature = (stress))] +pub fn pyfn_deviatoric_stress(stress: crate::generated::types::PyMat3) -> PyResult { + let stress = stress.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::continuum_mechanics::deviatoric_stress(&stress)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMat3 { inner: __v }) +} + +/// Von Mises equivalent stress from a full stress tensor: sigma_vm = sqrt(3/2 * s:s). +/// +/// Rust: `continuum_mechanics::von_mises_from_tensor` +#[pyfunction] +#[pyo3(name = "von_mises_from_tensor", signature = (stress))] +pub fn pyfn_von_mises_from_tensor(stress: crate::generated::types::PyMat3) -> PyResult { + let stress = stress.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::continuum_mechanics::von_mises_from_tensor(&stress)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Maximum shear stress: tau_max = (sigma1 - sigma3) / 2. +/// +/// Rust: `continuum_mechanics::max_shear_stress` +#[pyfunction] +#[pyo3(name = "max_shear_stress", signature = (stress))] +pub fn pyfn_max_shear_stress(stress: crate::generated::types::PyMat3) -> PyResult { + let stress = stress.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::continuum_mechanics::max_shear_stress(&stress)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Constructs a symmetric 3x3 strain tensor from six independent components. +/// +/// Rust: `continuum_mechanics::strain_tensor` +#[pyfunction] +#[pyo3(name = "strain_tensor", signature = (exx, eyy, ezz, exy, exz, eyz))] +pub fn pyfn_strain_tensor(exx: f64, eyy: f64, ezz: f64, exy: f64, exz: f64, eyz: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::continuum_mechanics::strain_tensor(exx, eyy, ezz, exy, exz, eyz)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMat3 { inner: __v }) +} + +/// Volumetric strain: epsilon_v = tr(epsilon). +/// +/// Rust: `continuum_mechanics::volumetric_strain` +#[pyfunction] +#[pyo3(name = "volumetric_strain", signature = (strain))] +pub fn pyfn_volumetric_strain(strain: crate::generated::types::PyMat3) -> PyResult { + let strain = strain.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::continuum_mechanics::volumetric_strain(&strain)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Deviatoric strain tensor: e = epsilon - (epsilon_v / 3) * I. +/// +/// Rust: `continuum_mechanics::deviatoric_strain` +#[pyfunction] +#[pyo3(name = "deviatoric_strain", signature = (strain))] +pub fn pyfn_deviatoric_strain(strain: crate::generated::types::PyMat3) -> PyResult { + let strain = strain.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::continuum_mechanics::deviatoric_strain(&strain)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMat3 { inner: __v }) +} + +/// Small (infinitesimal) strain from the displacement gradient: epsilon = (grad_u + grad_u^T) / 2. +/// +/// Rust: `continuum_mechanics::strain_from_displacement_gradient` +#[pyfunction] +#[pyo3(name = "strain_from_displacement_gradient", signature = (grad_u))] +pub fn pyfn_strain_from_displacement_gradient(grad_u: crate::generated::types::PyMat3) -> PyResult { + let grad_u = grad_u.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::continuum_mechanics::strain_from_displacement_gradient(&grad_u)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMat3 { inner: __v }) +} + +/// Green-Lagrange finite strain tensor: E = (F^T F - I) / 2. +/// +/// Rust: `continuum_mechanics::green_lagrange_strain` +#[pyfunction] +#[pyo3(name = "green_lagrange_strain", signature = (deformation_gradient))] +pub fn pyfn_green_lagrange_strain(deformation_gradient: crate::generated::types::PyMat3) -> PyResult { + let deformation_gradient = deformation_gradient.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::continuum_mechanics::green_lagrange_strain(&deformation_gradient)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMat3 { inner: __v }) +} + +/// 3D isotropic linear elastic (Hooke's law): +/// sigma_ij = lambda * tr(epsilon) * delta_ij + 2 * mu * epsilon_ij. +/// +/// Rust: `continuum_mechanics::hooke_3d` +#[pyfunction] +#[pyo3(name = "hooke_3d", signature = (strain, youngs, poisson))] +pub fn pyfn_hooke_3d(strain: crate::generated::types::PyMat3, youngs: f64, poisson: f64) -> PyResult { + let strain = strain.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::continuum_mechanics::hooke_3d(&strain, youngs, poisson)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMat3 { inner: __v }) +} + +/// Returns the six key elastic constants for an isotropic material: +/// [C11, C12, C44, lambda, mu (shear modulus), K (bulk modulus)]. +/// +/// Rust: `continuum_mechanics::compliance_matrix_isotropic` +#[pyfunction] +#[pyo3(name = "compliance_matrix_isotropic", signature = (youngs, poisson))] +pub fn pyfn_compliance_matrix_isotropic<'py>(py: Python<'py>, youngs: f64, poisson: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::continuum_mechanics::compliance_matrix_isotropic(youngs, poisson))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_vec()) +} + +/// Plane stress: returns (sigma_xx, sigma_yy, tau_xy) given in-plane strains. +/// Uses the constitutive relation sigma = E/(1-nu^2) * [1,nu; nu,1] * epsilon for normal, +/// and tau_xy = G * gamma_xy where G = E/(2(1+nu)). +/// +/// Rust: `continuum_mechanics::plane_stress` +#[pyfunction] +#[pyo3(name = "plane_stress", signature = (strain_xx, strain_yy, strain_xy, youngs, poisson))] +pub fn pyfn_plane_stress(strain_xx: f64, strain_yy: f64, strain_xy: f64, youngs: f64, poisson: f64) -> PyResult<(f64, f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::continuum_mechanics::plane_stress(strain_xx, strain_yy, strain_xy, youngs, poisson)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// Plane strain: returns (sigma_xx, sigma_yy, tau_xy) given in-plane strains. +/// This is the 3D Hooke's law with epsilon_zz = 0 and the z-normal stress is nonzero but not returned. +/// +/// Rust: `continuum_mechanics::plane_strain` +#[pyfunction] +#[pyo3(name = "plane_strain", signature = (strain_xx, strain_yy, strain_xy, youngs, poisson))] +pub fn pyfn_plane_strain(strain_xx: f64, strain_yy: f64, strain_xy: f64, youngs: f64, poisson: f64) -> PyResult<(f64, f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::continuum_mechanics::plane_strain(strain_xx, strain_yy, strain_xy, youngs, poisson)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// Tresca equivalent stress: max(|s1-s2|, |s2-s3|, |s3-s1|). +/// +/// Rust: `continuum_mechanics::tresca_stress` +#[pyfunction] +#[pyo3(name = "tresca_stress", signature = (stress))] +pub fn pyfn_tresca_stress(stress: crate::generated::types::PyMat3) -> PyResult { + let stress = stress.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::continuum_mechanics::tresca_stress(&stress)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Mohr-Coulomb failure criterion: tau - c - sigma * tan(phi). +/// Returns positive if the stress state violates the criterion (failure). +/// `friction_angle` is in radians. +/// +/// Rust: `continuum_mechanics::mohr_coulomb` +#[pyfunction] +#[pyo3(name = "mohr_coulomb", signature = (normal_stress, shear_stress, cohesion, friction_angle))] +pub fn pyfn_mohr_coulomb(normal_stress: f64, shear_stress: f64, cohesion: f64, friction_angle: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::continuum_mechanics::mohr_coulomb(normal_stress, shear_stress, cohesion, friction_angle)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Drucker-Prager failure criterion: sqrt(J2) + alpha * I1 - k. +/// Returns positive if the stress state violates the criterion (failure). +/// Alpha and k are derived from cohesion c and friction angle phi (radians) +/// using the inscribed-cone approximation (matching Mohr-Coulomb for compression). +/// +/// Rust: `continuum_mechanics::drucker_prager` +#[pyfunction] +#[pyo3(name = "drucker_prager", signature = (stress, cohesion, friction_angle))] +pub fn pyfn_drucker_prager(stress: crate::generated::types::PyMat3, cohesion: f64, friction_angle: f64) -> PyResult { + let stress = stress.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::continuum_mechanics::drucker_prager(&stress, cohesion, friction_angle)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_stress_tensor, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_stress_invariants, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_principal_stresses, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hydrostatic_stress, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_deviatoric_stress, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_von_mises_from_tensor, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_max_shear_stress, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_strain_tensor, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_volumetric_strain, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_deviatoric_strain, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_strain_from_displacement_gradient, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_green_lagrange_strain, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hooke_3d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_compliance_matrix_isotropic, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_plane_stress, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_plane_strain, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tresca_stress, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mohr_coulomb, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_drucker_prager, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_control_systems.rs b/bindings/python/src/generated/m_control_systems.rs new file mode 100644 index 0000000..a14b087 --- /dev/null +++ b/bindings/python/src/generated/m_control_systems.rs @@ -0,0 +1,243 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// First-order step response: y(t) = K(1 - e^(-t/τ)) +/// +/// Rust: `control_systems::first_order_step_response` +#[pyfunction] +#[pyo3(name = "first_order_step_response", signature = (gain, tau, t))] +pub fn pyfn_first_order_step_response(gain: f64, tau: f64, t: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::control_systems::first_order_step_response(gain, tau, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// First-order impulse response: h(t) = (K/τ)·e^(-t/τ) +/// +/// Rust: `control_systems::first_order_impulse_response` +#[pyfunction] +#[pyo3(name = "first_order_impulse_response", signature = (gain, tau, t))] +pub fn pyfn_first_order_impulse_response(gain: f64, tau: f64, t: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::control_systems::first_order_impulse_response(gain, tau, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Second-order step response for underdamped, critically damped, and overdamped systems. +/// +/// Rust: `control_systems::second_order_step_response` +#[pyfunction] +#[pyo3(name = "second_order_step_response", signature = (gain, omega_n, zeta, t))] +pub fn pyfn_second_order_step_response(gain: f64, omega_n: f64, zeta: f64, t: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::control_systems::second_order_step_response(gain, omega_n, zeta, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Natural frequency of a second-order system: ωn = √(k/m) +/// +/// Rust: `control_systems::second_order_natural_frequency` +#[pyfunction] +#[pyo3(name = "second_order_natural_frequency", signature = (k, m))] +pub fn pyfn_second_order_natural_frequency(k: f64, m: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::control_systems::second_order_natural_frequency(k, m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Damping ratio of a second-order system: ζ = c/(2√(km)) +/// +/// Rust: `control_systems::second_order_damping_ratio` +#[pyfunction] +#[pyo3(name = "second_order_damping_ratio", signature = (c, k, m))] +pub fn pyfn_second_order_damping_ratio(c: f64, k: f64, m: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::control_systems::second_order_damping_ratio(c, k, m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Rise time of a first-order system (10% to 90%): tr = 2.2τ +/// +/// Rust: `control_systems::rise_time_first_order` +#[pyfunction] +#[pyo3(name = "rise_time_first_order", signature = (tau))] +pub fn pyfn_rise_time_first_order(tau: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::control_systems::rise_time_first_order(tau)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Settling time of a first-order system (2% criterion): ts = 4τ +/// +/// Rust: `control_systems::settling_time_first_order` +#[pyfunction] +#[pyo3(name = "settling_time_first_order", signature = (tau))] +pub fn pyfn_settling_time_first_order(tau: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::control_systems::settling_time_first_order(tau)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Settling time of a second-order system (2% criterion): ts = 4/(ζωn) +/// +/// Rust: `control_systems::settling_time_second_order` +#[pyfunction] +#[pyo3(name = "settling_time_second_order", signature = (zeta, omega_n))] +pub fn pyfn_settling_time_second_order(zeta: f64, omega_n: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::control_systems::settling_time_second_order(zeta, omega_n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Peak overshoot percentage: Mp = 100·exp(-πζ/√(1-ζ²)) +/// +/// Rust: `control_systems::overshoot_percent` +#[pyfunction] +#[pyo3(name = "overshoot_percent", signature = (zeta))] +pub fn pyfn_overshoot_percent(zeta: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::control_systems::overshoot_percent(zeta)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Bandwidth of a first-order system: ωb = 1/τ +/// +/// Rust: `control_systems::bandwidth_first_order` +#[pyfunction] +#[pyo3(name = "bandwidth_first_order", signature = (tau))] +pub fn pyfn_bandwidth_first_order(tau: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::control_systems::bandwidth_first_order(tau)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Gain margin in dB: GM = -20·log₁₀(|G(jω)|) at the phase crossover frequency +/// +/// Rust: `control_systems::gain_margin_db` +#[pyfunction] +#[pyo3(name = "gain_margin_db", signature = (open_loop_gain_at_phase_crossover))] +pub fn pyfn_gain_margin_db(open_loop_gain_at_phase_crossover: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::control_systems::gain_margin_db(open_loop_gain_at_phase_crossover)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Phase margin in degrees: PM = 180° + φ(ωgc) +/// +/// Rust: `control_systems::phase_margin` +#[pyfunction] +#[pyo3(name = "phase_margin", signature = (phase_at_gain_crossover))] +pub fn pyfn_phase_margin(phase_at_gain_crossover: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::control_systems::phase_margin(phase_at_gain_crossover)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Steady-state error for a type-0 system with step input: ess = 1/(1 + K) +/// +/// Rust: `control_systems::steady_state_error_type0` +#[pyfunction] +#[pyo3(name = "steady_state_error_type0", signature = (gain))] +pub fn pyfn_steady_state_error_type0(gain: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::control_systems::steady_state_error_type0(gain)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Steady-state error for a type-1 system with ramp input: ess = 1/K +/// +/// Rust: `control_systems::steady_state_error_type1` +#[pyfunction] +#[pyo3(name = "steady_state_error_type1", signature = (gain))] +pub fn pyfn_steady_state_error_type1(gain: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::control_systems::steady_state_error_type1(gain)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Check stability of a first-order system: stable when τ > 0. +/// +/// Rust: `control_systems::is_stable_first_order` +#[pyfunction] +#[pyo3(name = "is_stable_first_order", signature = (tau))] +pub fn pyfn_is_stable_first_order(tau: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::control_systems::is_stable_first_order(tau)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Check stability of a second-order system: stable when ζ > 0 and ωn > 0. +/// +/// Rust: `control_systems::is_stable_second_order` +#[pyfunction] +#[pyo3(name = "is_stable_second_order", signature = (zeta, omega_n))] +pub fn pyfn_is_stable_second_order(zeta: f64, omega_n: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::control_systems::is_stable_second_order(zeta, omega_n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Routh stability criterion for a 2nd-order polynomial: stable when all coefficients > 0. +/// +/// Rust: `control_systems::routh_criterion_2nd` +#[pyfunction] +#[pyo3(name = "routh_criterion_2nd", signature = (a0, a1, a2))] +pub fn pyfn_routh_criterion_2nd(a0: f64, a1: f64, a2: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::control_systems::routh_criterion_2nd(a0, a1, a2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Poles of a transfer function: the complex roots of the denominator +/// polynomial (coefficients highest degree first), via +/// `numerical::roots::polynomial_roots`. A system is BIBO-stable when +/// every pole has a negative real part. +/// +/// Rust: `control_systems::transfer_function_poles` +#[pyfunction] +#[pyo3(name = "transfer_function_poles", signature = (denominator))] +pub fn pyfn_transfer_function_poles<'py>(py: Python<'py>, denominator: Vec) -> PyResult>> { + let __r = crate::runtime::guard(|| rust_physics_engine::control_systems::transfer_function_poles(&denominator)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_first_order_step_response, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_first_order_impulse_response, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_second_order_step_response, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_second_order_natural_frequency, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_second_order_damping_ratio, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rise_time_first_order, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_settling_time_first_order, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_settling_time_second_order, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_overshoot_percent, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bandwidth_first_order, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gain_margin_db, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_phase_margin, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_steady_state_error_type0, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_steady_state_error_type1, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_is_stable_first_order, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_is_stable_second_order, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_routh_criterion_2nd, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_transfer_function_poles, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_control_systems__kalman.rs b/bindings/python/src/generated/m_control_systems__kalman.rs new file mode 100644 index 0000000..765f8bd --- /dev/null +++ b/bindings/python/src/generated/m_control_systems__kalman.rs @@ -0,0 +1,24 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_core.rs b/bindings/python/src/generated/m_core.rs new file mode 100644 index 0000000..c02bbf8 --- /dev/null +++ b/bindings/python/src/generated/m_core.rs @@ -0,0 +1,22 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + Ok(()) +} diff --git a/bindings/python/src/generated/m_core__compensated.rs b/bindings/python/src/generated/m_core__compensated.rs new file mode 100644 index 0000000..85859db --- /dev/null +++ b/bindings/python/src/generated/m_core__compensated.rs @@ -0,0 +1,75 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Compensated (error-free-transformation) summation. +/// +/// Formulas: Neumaier's improved Kahan-Babuska summation +/// (A. Neumaier, "Rundungsfehleranalyse einiger Verfahren zur Summation +/// endlicher Summen", ZAMM 54, 1974) and recursive pairwise summation +/// (Higham, *Accuracy and Stability of Numerical Algorithms*, ch. 4). +/// Neumaier compensated sum of a slice. +/// +/// Computes `Σ xᵢ` with a running compensation term that captures the +/// low-order bits lost in each addition, giving results accurate to +/// O(1) ulp independent of length for well-scaled data. +/// +/// Rust: `core::compensated::sum_neumaier` +#[pyfunction] +#[pyo3(name = "sum_neumaier", signature = (xs))] +pub fn pyfn_sum_neumaier<'py>(py: Python<'py>, xs: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::core::compensated::sum_neumaier(&xs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Recursive pairwise sum of a slice: `Σ xᵢ` with O(log n) error growth. +/// +/// Splits the slice in half and sums each half recursively; runs of up +/// to 32 elements are summed naively as the base case. +/// +/// Rust: `core::compensated::sum_pairwise` +#[pyfunction] +#[pyo3(name = "sum_pairwise", signature = (xs))] +pub fn pyfn_sum_pairwise<'py>(py: Python<'py>, xs: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::core::compensated::sum_pairwise(&xs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Compensated dot product `Σ aᵢ·bᵢ` via Neumaier accumulation of the +/// individual products. +/// +/// Panics: +/// Panics if `a` and `b` have different lengths. +/// +/// Rust: `core::compensated::dot_compensated` +#[pyfunction] +#[pyo3(name = "dot_compensated", signature = (a, b))] +pub fn pyfn_dot_compensated<'py>(py: Python<'py>, a: Vec, b: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::core::compensated::dot_compensated(&a, &b))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_sum_neumaier, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sum_pairwise, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dot_compensated, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_core__dual.rs b/bindings/python/src/generated/m_core__dual.rs new file mode 100644 index 0000000..8bdd4e0 --- /dev/null +++ b/bindings/python/src/generated/m_core__dual.rs @@ -0,0 +1,38 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Exact derivative f'(x) of a scalar function via forward-mode AD. +/// +/// Rust: `core::dual::derivative` +#[pyfunction] +#[pyo3(name = "derivative", signature = (f, x))] +pub fn pyfn_derivative(f: pyo3::Py, x: f64) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: rust_physics_engine::core::dual::Dual| -> rust_physics_engine::core::dual::Dual { { let __r = __cb.call::<_, crate::generated::types::PyDualArg>((crate::generated::types::PyDual { inner: __a0 },), crate::generated::types::PyDualArg(rust_physics_engine::core::dual::Dual { re: f64::NAN, eps: f64::NAN })); __r.0 } } }; + let __r = crate::runtime::guard(|| rust_physics_engine::core::dual::derivative(f, x)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_derivative, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_core__interval.rs b/bindings/python/src/generated/m_core__interval.rs new file mode 100644 index 0000000..8242a9c --- /dev/null +++ b/bindings/python/src/generated/m_core__interval.rs @@ -0,0 +1,52 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Rigorous interval Newton method: encloses every root of f in `x0`. +/// +/// `f` and `df` must be interval extensions of the function and its +/// derivative. Boxes where 0 ∉ f(X) are discarded; where f'(X) +/// excludes 0 the Newton contraction N(X) = m − f(m)/f'(X) ∩ X is +/// applied; otherwise the box is bisected. Boxes narrower than `tol` +/// that still satisfy 0 ∈ f(X) are reported (overlapping neighbors +/// merged). Every real root in `x0` is contained in some returned +/// interval; spurious near-root boxes may also appear at width ~tol. +/// +/// Panics: +/// Panics unless tol > 0. +/// +/// Rust: `core::interval::interval_newton` +#[pyfunction] +#[pyo3(name = "interval_newton", signature = (f, df, x0, tol, max_iter))] +pub fn pyfn_interval_newton(f: pyo3::Py, df: pyo3::Py, x0: crate::generated::types::PyIntervalArg, tol: f64, max_iter: usize) -> PyResult> { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: rust_physics_engine::core::interval::Interval| -> rust_physics_engine::core::interval::Interval { { let __r = __cb.call::<_, crate::generated::types::PyIntervalArg>((crate::generated::types::PyInterval { inner: __a0 },), crate::generated::types::PyIntervalArg(rust_physics_engine::core::interval::Interval { lo: f64::NAN, hi: f64::NAN })); __r.0 } } }; + let __cb_df = std::rc::Rc::new(crate::runtime::Callback::new(df)); + let df = { let __cb = __cb_df.clone(); move |__a0: rust_physics_engine::core::interval::Interval| -> rust_physics_engine::core::interval::Interval { { let __r = __cb.call::<_, crate::generated::types::PyIntervalArg>((crate::generated::types::PyInterval { inner: __a0 },), crate::generated::types::PyIntervalArg(rust_physics_engine::core::interval::Interval { lo: f64::NAN, hi: f64::NAN })); __r.0 } } }; + let x0 = x0.0; + let __r = crate::runtime::guard(|| rust_physics_engine::core::interval::interval_newton(&f, &df, x0, tol, max_iter)); + crate::runtime::callback::check(&[&__cb_f, &__cb_df], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyInterval { inner: __x }).collect::>()) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_interval_newton, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_curves.rs b/bindings/python/src/generated/m_curves.rs new file mode 100644 index 0000000..7035f6c --- /dev/null +++ b/bindings/python/src/generated/m_curves.rs @@ -0,0 +1,346 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Area of a circle: A = πr² +/// +/// Rust: `curves::circle_area` +#[pyfunction] +#[pyo3(name = "circle_area", signature = (radius))] +pub fn pyfn_circle_area(radius: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::curves::circle_area(radius)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Circle circumference: C = 2πr +/// +/// Rust: `curves::circle_circumference` +#[pyfunction] +#[pyo3(name = "circle_circumference", signature = (radius))] +pub fn pyfn_circle_circumference(radius: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::curves::circle_circumference(radius)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Returns (x-cx)^2 + (y-cy)^2 - r^2. Zero means the point lies on the circle. +/// +/// Rust: `curves::circle_equation` +#[pyfunction] +#[pyo3(name = "circle_equation", signature = (x, y, cx, cy, r))] +pub fn pyfn_circle_equation(x: f64, y: f64, cx: f64, cy: f64, r: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::curves::circle_equation(x, y, cx, cy, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Ramanujan approximation: pi * (3(a+b) - sqrt((3a+b)(a+3b))) +/// +/// Rust: `curves::ellipse_circumference_approx` +#[pyfunction] +#[pyo3(name = "ellipse_circumference_approx", signature = (a, b))] +pub fn pyfn_ellipse_circumference_approx(a: f64, b: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::curves::ellipse_circumference_approx(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Returns x^2/a^2 + y^2/b^2 - 1. Zero means the point lies on the ellipse. +/// +/// Rust: `curves::ellipse_equation` +#[pyfunction] +#[pyo3(name = "ellipse_equation", signature = (x, y, a, b))] +pub fn pyfn_ellipse_equation(x: f64, y: f64, a: f64, b: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::curves::ellipse_equation(x, y, a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Eccentricity e = sqrt(1 - b^2/a^2) for a > b. +/// +/// Rust: `curves::ellipse_eccentricity` +#[pyfunction] +#[pyo3(name = "ellipse_eccentricity", signature = (a, b))] +pub fn pyfn_ellipse_eccentricity(a: f64, b: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::curves::ellipse_eccentricity(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Focus distance f = 1/(4a) for parabola y = ax^2. +/// +/// Rust: `curves::parabola_focus` +#[pyfunction] +#[pyo3(name = "parabola_focus", signature = (a))] +pub fn pyfn_parabola_focus(a: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::curves::parabola_focus(a)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Parabola equation: y = ax² +/// +/// Rust: `curves::parabola_equation` +#[pyfunction] +#[pyo3(name = "parabola_equation", signature = (x, a))] +pub fn pyfn_parabola_equation(x: f64, a: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::curves::parabola_equation(x, a)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Eccentricity e = sqrt(1 + b^2/a^2). +/// +/// Rust: `curves::hyperbola_eccentricity` +#[pyfunction] +#[pyo3(name = "hyperbola_eccentricity", signature = (a, b))] +pub fn pyfn_hyperbola_eccentricity(a: f64, b: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::curves::hyperbola_eccentricity(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Asymptote slope of a hyperbola: m = b/a +/// +/// Rust: `curves::hyperbola_asymptote_slope` +#[pyfunction] +#[pyo3(name = "hyperbola_asymptote_slope", signature = (a, b))] +pub fn pyfn_hyperbola_asymptote_slope(a: f64, b: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::curves::hyperbola_asymptote_slope(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Discriminant B^2 - 4AC for general conic Ax^2 + Bxy + Cy^2 + ... +/// Negative => ellipse, zero => parabola, positive => hyperbola. +/// +/// Rust: `curves::conic_discriminant` +#[pyfunction] +#[pyo3(name = "conic_discriminant", signature = (a, b, c))] +pub fn pyfn_conic_discriminant(a: f64, b: f64, c: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::curves::conic_discriminant(a, b, c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Quadratic Bezier curve point: B(t) = (1-t)²P₀ + 2(1-t)tP₁ + t²P₂ +/// +/// Rust: `curves::bezier_quadratic` +#[pyfunction] +#[pyo3(name = "bezier_quadratic", signature = (t, p0, p1, p2))] +pub fn pyfn_bezier_quadratic(t: f64, p0: (f64, f64), p1: (f64, f64), p2: (f64, f64)) -> PyResult<(f64, f64)> { + let p0 = (p0.0, p0.1); + let p1 = (p1.0, p1.1); + let p2 = (p2.0, p2.1); + let __r = crate::runtime::guard(|| rust_physics_engine::curves::bezier_quadratic(t, p0, p1, p2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Cubic Bezier curve point: B(t) = (1-t)³P₀ + 3(1-t)²tP₁ + 3(1-t)t²P₂ + t³P₃ +/// +/// Rust: `curves::bezier_cubic` +#[pyfunction] +#[pyo3(name = "bezier_cubic", signature = (t, p0, p1, p2, p3))] +pub fn pyfn_bezier_cubic(t: f64, p0: (f64, f64), p1: (f64, f64), p2: (f64, f64), p3: (f64, f64)) -> PyResult<(f64, f64)> { + let p0 = (p0.0, p0.1); + let p1 = (p1.0, p1.1); + let p2 = (p2.0, p2.1); + let p3 = (p3.0, p3.1); + let __r = crate::runtime::guard(|| rust_physics_engine::curves::bezier_cubic(t, p0, p1, p2, p3)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Quadratic Bezier curve in 3D: B(t) = (1-t)²P₀ + 2(1-t)tP₁ + t²P₂ +/// +/// Rust: `curves::bezier_quadratic_3d` +#[pyfunction] +#[pyo3(name = "bezier_quadratic_3d", signature = (t, p0, p1, p2))] +pub fn pyfn_bezier_quadratic_3d(t: f64, p0: crate::generated::types::PyVec3Arg, p1: crate::generated::types::PyVec3Arg, p2: crate::generated::types::PyVec3Arg) -> PyResult { + let p0 = p0.0; + let p1 = p1.0; + let p2 = p2.0; + let __r = crate::runtime::guard(|| rust_physics_engine::curves::bezier_quadratic_3d(t, p0, p1, p2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Cubic Bezier curve in 3D: B(t) = (1-t)³P₀ + 3(1-t)²tP₁ + 3(1-t)t²P₂ + t³P₃ +/// +/// Rust: `curves::bezier_cubic_3d` +#[pyfunction] +#[pyo3(name = "bezier_cubic_3d", signature = (t, p0, p1, p2, p3))] +pub fn pyfn_bezier_cubic_3d(t: f64, p0: crate::generated::types::PyVec3Arg, p1: crate::generated::types::PyVec3Arg, p2: crate::generated::types::PyVec3Arg, p3: crate::generated::types::PyVec3Arg) -> PyResult { + let p0 = p0.0; + let p1 = p1.0; + let p2 = p2.0; + let p3 = p3.0; + let __r = crate::runtime::guard(|| rust_physics_engine::curves::bezier_cubic_3d(t, p0, p1, p2, p3)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Sample n+1 evenly spaced points along a cubic Bezier curve (t from 0 to 1). +/// +/// Rust: `curves::bezier_sample` +#[pyfunction] +#[pyo3(name = "bezier_sample", signature = (p0, p1, p2, p3, n))] +pub fn pyfn_bezier_sample<'py>(py: Python<'py>, p0: (f64, f64), p1: (f64, f64), p2: (f64, f64), p3: (f64, f64), n: usize) -> PyResult> { + let p0 = (p0.0, p0.1); + let p1 = (p1.0, p1.1); + let p2 = (p2.0, p2.1); + let p3 = (p3.0, p3.1); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::curves::bezier_sample(p0, p1, p2, p3, n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Parametric circle: (x, y) = (r·cos(t), r·sin(t)) +/// +/// Rust: `curves::parametric_circle` +#[pyfunction] +#[pyo3(name = "parametric_circle", signature = (t, r))] +pub fn pyfn_parametric_circle(t: f64, r: f64) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::curves::parametric_circle(t, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Parametric ellipse: (x, y) = (a·cos(t), b·sin(t)) +/// +/// Rust: `curves::parametric_ellipse` +#[pyfunction] +#[pyo3(name = "parametric_ellipse", signature = (t, a, b))] +pub fn pyfn_parametric_ellipse(t: f64, a: f64, b: f64) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::curves::parametric_ellipse(t, a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Archimedean spiral: r = a + b*t. +/// +/// Rust: `curves::parametric_spiral` +#[pyfunction] +#[pyo3(name = "parametric_spiral", signature = (t, a, b))] +pub fn pyfn_parametric_spiral(t: f64, a: f64, b: f64) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::curves::parametric_spiral(t, a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Lissajous figure: (sin(a*t + delta), sin(b*t)). +/// +/// Rust: `curves::parametric_lissajous` +#[pyfunction] +#[pyo3(name = "parametric_lissajous", signature = (t, a, b, delta))] +pub fn pyfn_parametric_lissajous(t: f64, a: f64, b: f64, delta: f64) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::curves::parametric_lissajous(t, a, b, delta)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Cycloid: (r(t - sin(t)), r(1 - cos(t))). +/// +/// Rust: `curves::parametric_cycloid` +#[pyfunction] +#[pyo3(name = "parametric_cycloid", signature = (t, r))] +pub fn pyfn_parametric_cycloid(t: f64, r: f64) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::curves::parametric_cycloid(t, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Helix: (r cos(t), r sin(t), pitch * t / (2*pi)). +/// +/// Rust: `curves::parametric_helix` +#[pyfunction] +#[pyo3(name = "parametric_helix", signature = (t, radius, pitch))] +pub fn pyfn_parametric_helix(t: f64, radius: f64, pitch: f64) -> PyResult<(f64, f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::curves::parametric_helix(t, radius, pitch)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// Numerical arc length via piecewise linear approximation with n segments. +/// +/// Rust: `curves::arc_length_parametric` +#[pyfunction] +#[pyo3(name = "arc_length_parametric", signature = (fx, fy, t0, t1, n))] +pub fn pyfn_arc_length_parametric(fx: pyo3::Py, fy: pyo3::Py, t0: f64, t1: f64, n: usize) -> PyResult { + let __cb_fx = std::rc::Rc::new(crate::runtime::Callback::new(fx)); + let fx = { let __cb = __cb_fx.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __cb_fy = std::rc::Rc::new(crate::runtime::Callback::new(fy)); + let fy = { let __cb = __cb_fy.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::curves::arc_length_parametric(&fx, &fy, t0, t1, n)); + crate::runtime::callback::check(&[&__cb_fx, &__cb_fy], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Arc length of a circular arc: s = rθ +/// +/// Rust: `curves::arc_length_circle` +#[pyfunction] +#[pyo3(name = "arc_length_circle", signature = (radius, angle))] +pub fn pyfn_arc_length_circle(radius: f64, angle: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::curves::arc_length_circle(radius, angle)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Curvature kappa = |x'y'' - y'x''| / (x'^2 + y'^2)^(3/2). +/// +/// Rust: `curves::curvature_2d` +#[pyfunction] +#[pyo3(name = "curvature_2d", signature = (dxdt, dydt, d2xdt2, d2ydt2))] +pub fn pyfn_curvature_2d(dxdt: f64, dydt: f64, d2xdt2: f64, d2ydt2: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::curves::curvature_2d(dxdt, dydt, d2xdt2, d2ydt2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_circle_area, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_circle_circumference, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_circle_equation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ellipse_circumference_approx, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ellipse_equation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ellipse_eccentricity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_parabola_focus, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_parabola_equation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hyperbola_eccentricity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hyperbola_asymptote_slope, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_conic_discriminant, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bezier_quadratic, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bezier_cubic, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bezier_quadratic_3d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bezier_cubic_3d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bezier_sample, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_parametric_circle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_parametric_ellipse, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_parametric_spiral, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_parametric_lissajous, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_parametric_cycloid, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_parametric_helix, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_arc_length_parametric, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_arc_length_circle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_curvature_2d, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_discrete.rs b/bindings/python/src/generated/m_discrete.rs new file mode 100644 index 0000000..c02bbf8 --- /dev/null +++ b/bindings/python/src/generated/m_discrete.rs @@ -0,0 +1,22 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + Ok(()) +} diff --git a/bindings/python/src/generated/m_discrete__combinatorics.rs b/bindings/python/src/generated/m_discrete__combinatorics.rs new file mode 100644 index 0000000..24dd9c8 --- /dev/null +++ b/bindings/python/src/generated/m_discrete__combinatorics.rs @@ -0,0 +1,973 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// `C(n, k)` in `u64`, exactly when the result fits, otherwise `None`. +/// +/// Multiplies and divides alternately so the running value is always an exact +/// binomial coefficient and therefore an integer: after step `i` the value is +/// `C(n, i + 1)`. +/// +/// The running product before the division is `C(n, i+1) * (i+1)`, which is up +/// to `k` times the answer, so doing this in `u64` would report overflow for +/// results that fit. It runs in `u128` instead and tests the *coefficient* +/// against `u64::MAX`. Since `k` is folded to `min(k, n-k)`, the coefficient +/// only increases along the loop, so passing the bound once is final. +/// +/// Rust: `discrete::combinatorics::binomial_u64` +#[pyfunction] +#[pyo3(name = "binomial_u64", signature = (n, k))] +pub fn pyfn_binomial_u64(n: u64, k: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::binomial_u64(n, k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// `C(n, k) mod p` for prime `p`, by Lucas's theorem. +/// +/// Lucas reduces the coefficient to a product of coefficients of the base-`p` +/// digits, each of which is below `p` and so computable directly. A digit of +/// `k` exceeding the matching digit of `n` makes the whole product zero. +/// +/// Panics: +/// Panics if `p` is zero or one. The result is only correct for prime `p`. +/// +/// Rust: `discrete::combinatorics::binomial_mod_p` +#[pyfunction] +#[pyo3(name = "binomial_mod_p", signature = (n, k, p))] +pub fn pyfn_binomial_mod_p(n: u64, k: u64, p: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::binomial_mod_p(n, k, p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The multinomial `(sum ks)! / prod(ks!)`. +/// +/// Built as a product of binomials rather than a ratio of factorials, so +/// every intermediate is itself an integer count. +/// +/// Rust: `discrete::combinatorics::multinomial` +#[pyfunction] +#[pyo3(name = "multinomial", signature = (ks))] +pub fn pyfn_multinomial<'py>(py: Python<'py>, ks: Vec) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::multinomial(&ks)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// The falling factorial `n * (n-1) * ... * (n-k+1)`, or `None` on overflow. +/// +/// Rust: `discrete::combinatorics::permutations_count` +#[pyfunction] +#[pyo3(name = "permutations_count", signature = (n, k))] +pub fn pyfn_permutations_count(n: u64, k: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::permutations_count(n, k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// All permutations of `items`, by Heap's algorithm. +/// +/// Heap's algorithm reaches each of the `n!` arrangements with a single +/// transposition per step, so generating the whole family costs `O(n!)` swaps +/// rather than `O(n * n!)` copies -- the copies here are only to hand out +/// owned results. The order is Heap's, not lexicographic. +/// +/// Rust: `discrete::combinatorics::permutations_iter` +#[pyfunction] +#[pyo3(name = "permutations_iter", signature = (items))] +pub fn pyfn_permutations_iter(items: Vec) -> PyResult>> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::permutations_iter(&items)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.collect::>()) +} + +/// Advances `p` to the next permutation in lexicographic order in place. +/// +/// Returns `false` when `p` is already the last (descending) arrangement, in +/// which case `p` is left untouched. This is the standard pivot-and-reverse +/// step: find the rightmost ascent, swap its left element with the smallest +/// larger element to its right, then reverse the now-descending suffix. +/// +/// Rust: `discrete::combinatorics::permutations_lex_next` +#[pyfunction] +#[pyo3(name = "permutations_lex_next", signature = (p))] +pub fn pyfn_permutations_lex_next<'py>(p: pyo3::Bound<'py, pyo3::PyAny>) -> PyResult { + let mut p__v: Vec = p.extract()?; + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::permutations_lex_next(&mut p__v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + crate::runtime::coerce::write_back(&p, &p__v)?; + Ok(__v) +} + +/// The permutation of `0..n_items` at the given lexicographic `index`, by the +/// factorial number system. +/// +/// Digit `i` of the factoradic expansion says how many of the still-unused +/// symbols to skip, which is exactly what selecting the `index`-th +/// lexicographic arrangement does. +/// +/// Panics: +/// Panics if `index` is negative or at least `n_items!`. +/// +/// Rust: `discrete::combinatorics::nth_permutation` +#[pyfunction] +#[pyo3(name = "nth_permutation", signature = (n_items, index))] +pub fn pyfn_nth_permutation<'py>(py: Python<'py>, n_items: usize, index: crate::runtime::coerce::BigIntArg) -> PyResult> { + let index = index.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::combinatorics::nth_permutation(n_items, &index))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The lexicographic index of `p` among the permutations of its own symbols. +/// +/// Inverse of `nth_permutation`: counts, at each position, how many unused +/// symbols are smaller than the one chosen, and weights that by the factorial +/// of the remaining length. +/// +/// Rust: `discrete::combinatorics::permutation_index` +#[pyfunction] +#[pyo3(name = "permutation_index", signature = (p))] +pub fn pyfn_permutation_index<'py>(py: Python<'py>, p: Vec) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::permutation_index(&p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// The `k`-subsets of `0..n`, each sorted ascending, in lexicographic order. +/// +/// Rust: `discrete::combinatorics::combinations_iter` +#[pyfunction] +#[pyo3(name = "combinations_iter", signature = (n, k))] +pub fn pyfn_combinations_iter(n: usize, k: usize) -> PyResult>> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::combinations_iter(n, k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.collect::>()) +} + +/// The `k`-multisets over `0..n`, each non-decreasing, in lexicographic order. +/// +/// Same shape as `combinations_iter` with the strict ceiling relaxed: +/// entries may repeat, so position `j` is capped at `n - 1` rather than at +/// `j + n - k`. +/// +/// Rust: `discrete::combinatorics::combinations_with_replacement_iter` +#[pyfunction] +#[pyo3(name = "combinations_with_replacement_iter", signature = (n, k))] +pub fn pyfn_combinations_with_replacement_iter(n: usize, k: usize) -> PyResult>> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::combinations_with_replacement_iter(n, k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.collect::>()) +} + +/// The `2^n_bits` reflected binary Gray codes in order. +/// +/// `g(i) = i XOR (i >> 1)`, whose consecutive values differ in exactly one +/// bit. +/// +/// Panics: +/// Panics if `n_bits` exceeds 63. +/// +/// Rust: `discrete::combinatorics::gray_code_iter` +#[pyfunction] +#[pyo3(name = "gray_code_iter", signature = (n_bits))] +pub fn pyfn_gray_code_iter(n_bits: u32) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::gray_code_iter(n_bits)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.collect::>()) +} + +/// The `2^n` subsets of `0..n` as bitmasks, in increasing numeric order. +/// +/// Panics: +/// Panics if `n` exceeds 63. +/// +/// Rust: `discrete::combinatorics::subsets_iter` +#[pyfunction] +#[pyo3(name = "subsets_iter", signature = (n))] +pub fn pyfn_subsets_iter(n: u32) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::subsets_iter(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.collect::>()) +} + +/// The number of permutations of `n` symbols with no fixed point. +/// +/// Uses the recurrence `D(n) = (n-1) (D(n-1) + D(n-2))`, which is exact in +/// integers, rather than the alternating factorial sum, which alternates in +/// sign and would need cancellation. +/// +/// Rust: `discrete::combinatorics::derangements_count` +#[pyfunction] +#[pyo3(name = "derangements_count", signature = (n))] +pub fn pyfn_derangements_count<'py>(py: Python<'py>, n: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::derangements_count(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// True when `p` is a permutation with no fixed point. +/// +/// Rust: `discrete::combinatorics::is_derangement` +#[pyfunction] +#[pyo3(name = "is_derangement", signature = (p))] +pub fn pyfn_is_derangement<'py>(py: Python<'py>, p: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::combinatorics::is_derangement(&p))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// True when `p` is a bijection on `0..p.len()`. +/// +/// Rust: `discrete::combinatorics::is_permutation` +#[pyfunction] +#[pyo3(name = "is_permutation", signature = (p))] +pub fn pyfn_is_permutation<'py>(py: Python<'py>, p: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::combinatorics::is_permutation(&p))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// A uniformly random permutation of `0..n`, by Fisher-Yates. +/// +/// Each step picks uniformly from the untouched suffix, which gives every one +/// of the `n!` arrangements the same probability. +/// +/// Rust: `discrete::combinatorics::random_permutation` +#[pyfunction] +#[pyo3(name = "random_permutation", signature = (n, rng))] +pub fn pyfn_random_permutation(n: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::random_permutation(n, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// A uniformly random derangement of `0..n`, by rejection. +/// +/// The density of derangements tends to `1/e`, so the expected number of +/// draws is about 2.72 regardless of `n` -- rejection is the cheap method +/// here, not a fallback. Returns the empty permutation for `n = 0` and panics +/// for `n = 1`, which has no derangement. +/// +/// Panics: +/// Panics if `n` is 1. +/// +/// Rust: `discrete::combinatorics::random_derangement` +#[pyfunction] +#[pyo3(name = "random_derangement", signature = (n, rng))] +pub fn pyfn_random_derangement(n: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::random_derangement(n, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The composition `a` after `b`: `(a . b)(i) = a[b[i]]`. +/// +/// Panics: +/// Panics if the two permutations have different lengths. +/// +/// Rust: `discrete::combinatorics::permutation_compose` +#[pyfunction] +#[pyo3(name = "permutation_compose", signature = (a, b))] +pub fn pyfn_permutation_compose<'py>(py: Python<'py>, a: Vec, b: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::combinatorics::permutation_compose(&a, &b))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The inverse permutation. +/// +/// Rust: `discrete::combinatorics::permutation_inverse` +#[pyfunction] +#[pyo3(name = "permutation_inverse", signature = (p))] +pub fn pyfn_permutation_inverse<'py>(py: Python<'py>, p: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::combinatorics::permutation_inverse(&p))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The cycle lengths of `p`, sorted descending. +/// +/// This is the conjugacy class invariant: two permutations are conjugate in +/// the symmetric group exactly when their cycle types agree. Fixed points +/// count as cycles of length one, so the entries sum to `p.len()`. +/// +/// Rust: `discrete::combinatorics::permutation_cycle_type` +#[pyfunction] +#[pyo3(name = "permutation_cycle_type", signature = (p))] +pub fn pyfn_permutation_cycle_type<'py>(py: Python<'py>, p: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::combinatorics::permutation_cycle_type(&p))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The order of `p` in the symmetric group: the lcm of its cycle lengths. +/// +/// Returns a `BigInt` because the maximum order over `S_n` (Landau's +/// function) passes `u64::MAX` well before `n = 130`. +/// +/// Rust: `discrete::combinatorics::permutation_order` +#[pyfunction] +#[pyo3(name = "permutation_order", signature = (p))] +pub fn pyfn_permutation_order<'py>(py: Python<'py>, p: Vec) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::permutation_order(&p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// The sign of `p`: `+1` for an even permutation, `-1` for an odd one. +/// +/// A cycle of length `L` is a product of `L - 1` transpositions, so the sign +/// is `(-1)^(n - number of cycles)`. +/// +/// Rust: `discrete::combinatorics::permutation_sign` +#[pyfunction] +#[pyo3(name = "permutation_sign", signature = (p))] +pub fn pyfn_permutation_sign<'py>(py: Python<'py>, p: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::combinatorics::permutation_sign(&p))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The disjoint cycles of `p`, each starting at its smallest element, ordered +/// by that element. Fixed points appear as one-element cycles. +/// +/// Rust: `discrete::combinatorics::permutation_to_cycles` +#[pyfunction] +#[pyo3(name = "permutation_to_cycles", signature = (p))] +pub fn pyfn_permutation_to_cycles<'py>(py: Python<'py>, p: Vec) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::combinatorics::permutation_to_cycles(&p))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The permutation of `0..n` with the given disjoint cycles. +/// +/// Symbols not mentioned are fixed. Each cycle maps every element to the next +/// one listed and the last back to the first. +/// +/// Panics: +/// Panics if a symbol is at least `n` or appears in two cycles. +/// +/// Rust: `discrete::combinatorics::permutation_from_cycles` +#[pyfunction] +#[pyo3(name = "permutation_from_cycles", signature = (n, cycles))] +pub fn pyfn_permutation_from_cycles<'py>(py: Python<'py>, n: usize, cycles: Vec>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::combinatorics::permutation_from_cycles(n, &cycles))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The permutation matrix `P` with `P[p[j], j] = 1`. +/// +/// With this convention `P` applied to a coordinate vector moves the entry at +/// `j` to `p[j]`, so `permutation_matrix(compose(a, b))` is the product of the +/// two matrices in the same order. +/// +/// Rust: `discrete::combinatorics::permutation_matrix` +#[pyfunction] +#[pyo3(name = "permutation_matrix", signature = (p))] +pub fn pyfn_permutation_matrix(p: Vec) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::permutation_matrix(&p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) +} + +/// Unsigned Stirling numbers of the first kind: the number of permutations of +/// `n` symbols with exactly `k` cycles. +/// +/// Recurrence `c(n, k) = c(n-1, k-1) + (n-1) c(n-1, k)`: the new symbol is +/// either its own cycle or inserted after one of the `n-1` existing symbols. +/// +/// Rust: `discrete::combinatorics::stirling_first` +#[pyfunction] +#[pyo3(name = "stirling_first", signature = (n, k))] +pub fn pyfn_stirling_first<'py>(py: Python<'py>, n: u64, k: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::stirling_first(n, k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// Stirling numbers of the second kind: the number of ways to partition `n` +/// labelled objects into exactly `k` non-empty unlabelled blocks. +/// +/// Recurrence `S(n, k) = S(n-1, k-1) + k S(n-1, k)`: the new object either +/// opens a block of its own or joins one of the `k` existing ones. +/// +/// Rust: `discrete::combinatorics::stirling_second` +#[pyfunction] +#[pyo3(name = "stirling_second", signature = (n, k))] +pub fn pyfn_stirling_second<'py>(py: Python<'py>, n: u64, k: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::stirling_second(n, k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// The `n`-th Bell number: the number of partitions of an `n`-element set. +/// +/// Rust: `discrete::combinatorics::bell_number` +#[pyfunction] +#[pyo3(name = "bell_number", signature = (n))] +pub fn pyfn_bell_number<'py>(py: Python<'py>, n: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::bell_number(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// The first `n + 1` rows of the Bell (Peirce) triangle. +/// +/// Row 0 is `[1]`; each later row starts with the last entry of the previous +/// row and each subsequent entry is the sum of its left neighbour and the +/// entry above that neighbour. Row `i` begins with the `i`-th Bell number. +/// +/// Rust: `discrete::combinatorics::bell_triangle` +#[pyfunction] +#[pyo3(name = "bell_triangle", signature = (n))] +pub fn pyfn_bell_triangle<'py>(py: Python<'py>, n: u64) -> PyResult>>> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::bell_triangle(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| -> PyResult>> { Ok(__x.into_iter().map(|__x| -> PyResult> { Ok(crate::runtime::coerce::bigint_out(py, &__x)?) }).collect::>>()?) }).collect::>>()?) +} + +/// The `n`-th Catalan number, `C(2n, n) / (n + 1)`. +/// +/// Rust: `discrete::combinatorics::catalan` +#[pyfunction] +#[pyo3(name = "catalan", signature = (n))] +pub fn pyfn_catalan<'py>(py: Python<'py>, n: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::catalan(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// The `n`-th Catalan number modulo `m`, for any `m`. +/// +/// Uses the convolution recurrence `C(n+1) = sum_i C(i) C(n-i)` rather than +/// the closed form. The closed form needs a division by `n + 1`, which has no +/// modular meaning when `n + 1` shares a factor with `m`; the convolution is +/// pure addition and multiplication and so is valid for every modulus. +/// Costs `O(n^2)`. +/// +/// Panics: +/// Panics if `m` is zero. +/// +/// Rust: `discrete::combinatorics::catalan_mod` +#[pyfunction] +#[pyo3(name = "catalan_mod", signature = (n, m))] +pub fn pyfn_catalan_mod(n: u64, m: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::catalan_mod(n, m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Eulerian number `A(n, k)`: permutations of `n` symbols with exactly `k` +/// ascents. +/// +/// Recurrence `A(n, k) = (k+1) A(n-1, k) + (n-k) A(n-1, k-1)`. +/// +/// Rust: `discrete::combinatorics::eulerian_number` +#[pyfunction] +#[pyo3(name = "eulerian_number", signature = (n, k))] +pub fn pyfn_eulerian_number<'py>(py: Python<'py>, n: u64, k: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::eulerian_number(n, k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// Narayana number `N(n, k) = C(n, k) C(n, k-1) / n`, the number of Dyck paths +/// of semilength `n` with exactly `k` peaks. Defined for `1 <= k <= n`. +/// +/// Rust: `discrete::combinatorics::narayana` +#[pyfunction] +#[pyo3(name = "narayana", signature = (n, k))] +pub fn pyfn_narayana<'py>(py: Python<'py>, n: u64, k: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::narayana(n, k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// The `n`-th Motzkin number: lattice paths from `(0,0)` to `(n,0)` with steps +/// up, down and level that never dip below the axis. +/// +/// Recurrence `M(n+1) = M(n) + sum_i M(i) M(n-1-i)`. +/// +/// Rust: `discrete::combinatorics::motzkin` +#[pyfunction] +#[pyo3(name = "motzkin", signature = (n))] +pub fn pyfn_motzkin<'py>(py: Python<'py>, n: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::motzkin(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// The `n`-th large Schroeder number: lattice paths from `(0,0)` to `(n,n)` +/// with steps east, north and diagonal that stay weakly below the diagonal. +/// +/// Recurrence `3(2n-1) S(n-1) = (n+1) S(n) + (n-2) S(n-2)`, rearranged; done +/// here by the equivalent convolution `S(n) = S(n-1) + sum_i S(i) S(n-1-i)`. +/// +/// Rust: `discrete::combinatorics::schroeder` +#[pyfunction] +#[pyo3(name = "schroeder", signature = (n))] +pub fn pyfn_schroeder<'py>(py: Python<'py>, n: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::schroeder(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// The Delannoy number `D(m, n)`: lattice paths from `(0,0)` to `(m,n)` with +/// east, north and diagonal steps. +/// +/// Rust: `discrete::combinatorics::delannoy` +#[pyfunction] +#[pyo3(name = "delannoy", signature = (m, n))] +pub fn pyfn_delannoy<'py>(py: Python<'py>, m: u64, n: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::delannoy(m, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// The unsigned Lah number `L(n, k) = C(n-1, k-1) n! / k!`: the number of ways +/// to partition `n` labelled objects into `k` non-empty ordered lists. +/// +/// Rust: `discrete::combinatorics::lah_number` +#[pyfunction] +#[pyo3(name = "lah_number", signature = (n, k))] +pub fn pyfn_lah_number<'py>(py: Python<'py>, n: u64, k: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::lah_number(n, k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// The ballot number: the number of ways to count `p` votes for A and `q` for +/// B so that A is never behind. +/// +/// Equal to `C(p+q, q) (p - q + 1) / (p + 1)`; zero when `q > p`. +/// +/// Rust: `discrete::combinatorics::ballot_number` +#[pyfunction] +#[pyo3(name = "ballot_number", signature = (p, q))] +pub fn pyfn_ballot_number<'py>(py: Python<'py>, p: u64, q: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::ballot_number(p, q)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// The Dyck paths of semilength `n`, as step vectors of `2n` booleans where +/// `true` is an up step. +/// +/// Every prefix has at least as many up steps as down steps and the whole path +/// balances, so there are `catalan(n)` of them. Generated in lexicographic +/// order with `false < true`. +/// +/// Rust: `discrete::combinatorics::dyck_paths_iter` +#[pyfunction] +#[pyo3(name = "dyck_paths_iter", signature = (n))] +pub fn pyfn_dyck_paths_iter(n: usize) -> PyResult>> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::dyck_paths_iter(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.collect::>()) +} + +/// The set partitions of `0..n`, as restricted growth strings. +/// +/// Entry `i` of the string is the index of the block containing `i`. The +/// restriction is that a string starts at 0 and never jumps by more than one +/// above the running maximum, which makes the correspondence with partitions +/// exactly one-to-one -- block indices are forced to appear in order of their +/// smallest element, so relabelling the blocks cannot produce a duplicate. +/// There are `bell_number(n)` of them. +/// +/// Rust: `discrete::combinatorics::set_partitions_iter` +#[pyfunction] +#[pyo3(name = "set_partitions_iter", signature = (n))] +pub fn pyfn_set_partitions_iter(n: usize) -> PyResult>> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::set_partitions_iter(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.collect::>()) +} + +/// The compositions of `n`: the ordered tuples of positive integers summing to +/// `n`. There are `2^(n-1)` for `n >= 1`, and one (the empty tuple) for `n = 0`. +/// +/// Generated from the `n - 1` gap positions: a composition is exactly a choice +/// of which of the `n - 1` gaps between `n` units to cut. +/// +/// Rust: `discrete::combinatorics::compositions_iter` +#[pyfunction] +#[pyo3(name = "compositions_iter", signature = (n))] +pub fn pyfn_compositions_iter(n: u64) -> PyResult>> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::compositions_iter(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.collect::>()) +} + +/// The number of necklaces: `k`-colourings of `n` beads in a cycle, counted up +/// to rotation. +/// +/// Burnside over the cyclic group: the rotation by `j` fixes a colouring +/// exactly when the colouring is constant on the `gcd(j, n)` orbits, so the +/// count is `(1/n) sum_{d | n} phi(d) k^(n/d)`. +/// +/// Rust: `discrete::combinatorics::necklaces_count` +#[pyfunction] +#[pyo3(name = "necklaces_count", signature = (n, k))] +pub fn pyfn_necklaces_count<'py>(py: Python<'py>, n: u64, k: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::necklaces_count(n, k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// The number of bracelets: `k`-colourings of `n` beads in a cycle, counted up +/// to rotation *and* reflection. +/// +/// Burnside over the dihedral group. The reflections contribute +/// `k^((n+1)/2)` each for odd `n`, and for even `n` split into `n/2` axes +/// through two beads (`k^(n/2 + 1)`) and `n/2` axes through two gaps +/// (`k^(n/2)`). +/// +/// Rust: `discrete::combinatorics::bracelets_count` +#[pyfunction] +#[pyo3(name = "bracelets_count", signature = (n, k))] +pub fn pyfn_bracelets_count<'py>(py: Python<'py>, n: u64, k: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::bracelets_count(n, k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// Burnside's lemma: the number of orbits is the average number of points +/// fixed by a group element. +/// +/// Takes one fixed-point count per group element, so the slice length is the +/// group order. +/// +/// Panics: +/// Panics on an empty slice, and if the average is not an integer -- which +/// cannot happen for a genuine group action, so a non-zero remainder means the +/// caller's counts are not a group's. +/// +/// Rust: `discrete::combinatorics::burnside_orbit_count` +#[pyfunction] +#[pyo3(name = "burnside_orbit_count", signature = (group_element_fixed_counts))] +pub fn pyfn_burnside_orbit_count<'py>(py: Python<'py>, group_element_fixed_counts: Vec) -> PyResult> { + let group_element_fixed_counts = group_element_fixed_counts.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::burnside_orbit_count(&group_element_fixed_counts)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// Polya enumeration: the number of colourings with `colors` colours, given a +/// cycle index. +/// +/// The cycle index of a group acting on `n` points is a polynomial in `n` +/// variables `a_1..a_n`. Polya's theorem with unweighted colours substitutes +/// the same value -- the number of colours -- for every variable, and the +/// result of that substitution is a polynomial in one variable. That single +/// variable form is what `cycle_index_cyclic`, `cycle_index_dihedral` and +/// `cycle_index_symmetric` return and what this function evaluates, so the +/// specialisation happens once at construction rather than at every call. +/// +/// Panics: +/// Panics if the value at `colors` is not an integer, which cannot happen for +/// a cycle index of a genuine group. +/// +/// Rust: `discrete::combinatorics::polya_enumeration` +#[pyfunction] +#[pyo3(name = "polya_enumeration", signature = (cycle_index, colors))] +pub fn pyfn_polya_enumeration<'py>(py: Python<'py>, cycle_index: crate::generated::types::PyPolyQ, colors: u64) -> PyResult> { + let cycle_index = cycle_index.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::polya_enumeration(&cycle_index, colors)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// The cycle index of the cyclic group `C_n` acting on `n` points, with every +/// variable already set to the colour count: `(1/n) sum_{d | n} phi(d) x^(n/d)`. +/// +/// Rust: `discrete::combinatorics::cycle_index_cyclic` +#[pyfunction] +#[pyo3(name = "cycle_index_cyclic", signature = (n))] +pub fn pyfn_cycle_index_cyclic(n: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::cycle_index_cyclic(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyQ { inner: __v }) +} + +/// The cycle index of the dihedral group `D_n` acting on `n` points, with +/// every variable set to the colour count. +/// +/// Half the cyclic index plus the reflection average. +/// +/// Rust: `discrete::combinatorics::cycle_index_dihedral` +#[pyfunction] +#[pyo3(name = "cycle_index_dihedral", signature = (n))] +pub fn pyfn_cycle_index_dihedral(n: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::cycle_index_dihedral(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyQ { inner: __v }) +} + +/// The cycle index of the symmetric group `S_n` acting on `n` points, with +/// every variable set to the colour count. +/// +/// Averaging over all of `S_n` collapses to the rising factorial +/// `x (x+1) ... (x+n-1) / n!`, which is `C(x + n - 1, n)` -- the count of +/// `n`-multisets, exactly what "colourings up to any relabelling of the +/// points" means. +/// +/// Rust: `discrete::combinatorics::cycle_index_symmetric` +#[pyfunction] +#[pyo3(name = "cycle_index_symmetric", signature = (n))] +pub fn pyfn_cycle_index_symmetric(n: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::cycle_index_symmetric(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyQ { inner: __v }) +} + +/// The guaranteed occupancy of the fullest box: `ceil(items / boxes)`. +/// +/// The pigeonhole principle in its quantitative form -- some box holds at +/// least this many, and a balanced distribution shows the bound is attained. +/// +/// Panics: +/// Panics if `boxes` is zero. +/// +/// Rust: `discrete::combinatorics::pigeonhole_min_overlap` +#[pyfunction] +#[pyo3(name = "pigeonhole_min_overlap", signature = (items, boxes))] +pub fn pyfn_pigeonhole_min_overlap(items: u64, boxes: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::pigeonhole_min_overlap(items, boxes)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Ramsey number `R(s, t)` when it is known exactly, otherwise `None`. +/// +/// Only nine non-trivial values are known; everything beyond `R(4,5) = 25` +/// and the `R(3, t)` ladder is open, so this returns `None` rather than a +/// bound. +/// +/// Rust: `discrete::combinatorics::ramsey_known` +#[pyfunction] +#[pyo3(name = "ramsey_known", signature = (s, t))] +pub fn pyfn_ramsey_known(s: u64, t: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::ramsey_known(s, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// True when every row and every column of `sq` is a permutation of `0..n`. +/// +/// Rust: `discrete::combinatorics::is_latin_square` +#[pyfunction] +#[pyo3(name = "is_latin_square", signature = (sq))] +pub fn pyfn_is_latin_square<'py>(py: Python<'py>, sq: Vec>) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::combinatorics::is_latin_square(&sq))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// A random Latin square of order `n`. +/// +/// Built from the cyclic square `(i + j) mod n` by applying an independent +/// random permutation to the rows, to the columns, and to the symbols. Each +/// of those three operations preserves the Latin property, so the result is +/// always valid. It samples the isotopy class of the cyclic square rather +/// than all Latin squares uniformly, which the caller should not assume +/// otherwise. +/// +/// Rust: `discrete::combinatorics::latin_square_random` +#[pyfunction] +#[pyo3(name = "latin_square_random", signature = (n, rng))] +pub fn pyfn_latin_square_random(n: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult>> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::latin_square_random(n, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// A magic square of order `n`, or `None` for `n = 2`, which has none. +/// +/// Three constructions by residue: the Siamese method for odd `n`, the +/// complement pattern for `n` divisible by four, and Strachey's LUX method for +/// `n` congruent to 2 mod 4. Entries are `1..=n^2` and every row, column and +/// both diagonals sum to `n(n^2+1)/2`. +/// +/// Rust: `discrete::combinatorics::magic_square` +#[pyfunction] +#[pyo3(name = "magic_square", signature = (n))] +pub fn pyfn_magic_square(n: usize) -> PyResult>>> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::magic_square(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// A de Bruijn sequence `B(k, n)`: a cyclic sequence of length `k^n` over the +/// alphabet `0..k` in which every `n`-tuple appears exactly once. +/// +/// Built by the Frank-Kessler-Maiorana algorithm, which concatenates the +/// Lyndon words over the alphabet whose length divides `n`, in lexicographic +/// order. +/// +/// Panics: +/// Panics if `k` is zero or `n` is zero. +/// +/// Rust: `discrete::combinatorics::de_bruijn_sequence` +#[pyfunction] +#[pyo3(name = "de_bruijn_sequence", signature = (k, n))] +pub fn pyfn_de_bruijn_sequence<'py>(py: Python<'py>, k: usize, n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::combinatorics::de_bruijn_sequence(k, n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The number of perfect shuffles that restore a deck of `n_cards`. +/// +/// An out-shuffle keeps the top and bottom cards fixed and permutes the rest +/// by doubling their position modulo `n_cards - 1`, so its order is the +/// multiplicative order of 2 there. An in-shuffle moves every card, doubling +/// position modulo `n_cards + 1`. +/// +/// Panics: +/// Panics if `n_cards` is odd or below two: a perfect shuffle needs two equal +/// halves. +/// +/// Rust: `discrete::combinatorics::perfect_shuffles_order` +#[pyfunction] +#[pyo3(name = "perfect_shuffles_order", signature = (n_cards, out))] +pub fn pyfn_perfect_shuffles_order(n_cards: u64, out: bool) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::perfect_shuffles_order(n_cards, out)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The survivor of the Josephus problem: `n` people in a circle, every `k`-th +/// eliminated, returned as a zero-based position. +/// +/// Recurrence `J(1) = 0`, `J(i) = (J(i-1) + k) mod i`: after the first +/// elimination the problem is the same one on `i - 1` people with the origin +/// shifted by `k`. +/// +/// Panics: +/// Panics if `n` or `k` is zero. +/// +/// Rust: `discrete::combinatorics::josephus` +#[pyfunction] +#[pyo3(name = "josephus", signature = (n, k))] +pub fn pyfn_josephus(n: usize, k: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::josephus(n, k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The moves solving the Tower of Hanoi for `n` discs, as `(from, to)` pegs. +/// +/// Exactly `2^n - 1` moves, the known minimum. +/// +/// Panics: +/// Panics if `from` and `to` are equal or either is outside `0..3`. +/// +/// Rust: `discrete::combinatorics::tower_of_hanoi_moves` +#[pyfunction] +#[pyo3(name = "tower_of_hanoi_moves", signature = (n, from_, to))] +pub fn pyfn_tower_of_hanoi_moves<'py>(py: Python<'py>, n: u32, from_: u8, to: u8) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::combinatorics::tower_of_hanoi_moves(n, from_, to))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// The twelvefold way: `n` balls into `k` boxes under the six combinations of +/// distinguishability and the three restrictions. +/// +/// A restriction applies when its argument is `Some(true)`; `Some(false)` and +/// `None` both mean "no restriction", so `Some(false)` does not ask for a +/// map that fails to be injective. +/// +/// Rust: `discrete::combinatorics::twelvefold_way` +#[pyfunction] +#[pyo3(name = "twelvefold_way", signature = (n, k, injective, surjective, distinguishable_balls, distinguishable_boxes))] +pub fn pyfn_twelvefold_way<'py>(py: Python<'py>, n: u64, k: u64, injective: Option, surjective: Option, distinguishable_balls: bool, distinguishable_boxes: bool) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::combinatorics::twelvefold_way(n, k, injective, surjective, distinguishable_balls, distinguishable_boxes)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_binomial_u64, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_binomial_mod_p, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_multinomial, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_permutations_count, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_permutations_iter, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_permutations_lex_next, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_nth_permutation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_permutation_index, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_combinations_iter, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_combinations_with_replacement_iter, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gray_code_iter, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_subsets_iter, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_derangements_count, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_is_derangement, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_is_permutation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_random_permutation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_random_derangement, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_permutation_compose, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_permutation_inverse, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_permutation_cycle_type, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_permutation_order, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_permutation_sign, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_permutation_to_cycles, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_permutation_from_cycles, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_permutation_matrix, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_stirling_first, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_stirling_second, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bell_number, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bell_triangle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_catalan, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_catalan_mod, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_eulerian_number, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_narayana, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_motzkin, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_schroeder, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_delannoy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lah_number, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ballot_number, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dyck_paths_iter, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_set_partitions_iter, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_compositions_iter, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_necklaces_count, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bracelets_count, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_burnside_orbit_count, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_polya_enumeration, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cycle_index_cyclic, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cycle_index_dihedral, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cycle_index_symmetric, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pigeonhole_min_overlap, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ramsey_known, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_is_latin_square, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_latin_square_random, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_magic_square, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_de_bruijn_sequence, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_perfect_shuffles_order, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_josephus, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tower_of_hanoi_moves, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_twelvefold_way, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_discrete__disjoint_set.rs b/bindings/python/src/generated/m_discrete__disjoint_set.rs new file mode 100644 index 0000000..1b26df5 --- /dev/null +++ b/bindings/python/src/generated/m_discrete__disjoint_set.rs @@ -0,0 +1,23 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_discrete__number_theory.rs b/bindings/python/src/generated/m_discrete__number_theory.rs new file mode 100644 index 0000000..28c5918 --- /dev/null +++ b/bindings/python/src/generated/m_discrete__number_theory.rs @@ -0,0 +1,829 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Greatest common divisor, by the binary (Stein) algorithm. +/// +/// `gcd(0, n) == n`, so `gcd(0, 0) == 0`. +/// +/// Rust: `discrete::number_theory::gcd_u64` +#[pyfunction] +#[pyo3(name = "gcd_u64", signature = (a, b))] +pub fn pyfn_gcd_u64(a: u64, b: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::number_theory::gcd_u64(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Least common multiple; zero whenever either argument is zero. +/// +/// Panics: +/// Panics if the least common multiple does not fit in a `u64`. +/// +/// Rust: `discrete::number_theory::lcm_u64` +#[pyfunction] +#[pyo3(name = "lcm_u64", signature = (a, b))] +pub fn pyfn_lcm_u64(a: u64, b: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::number_theory::lcm_u64(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Extended Euclidean algorithm: `(g, x, y)` with `a*x + b*y == g` and +/// `g == gcd(|a|, |b|) >= 0`. +/// +/// Panics: +/// Panics on `a == i64::MIN` or `b == i64::MIN`, whose negation is not +/// representable. +/// +/// Rust: `discrete::number_theory::extended_gcd_i64` +#[pyfunction] +#[pyo3(name = "extended_gcd_i64", signature = (a, b))] +pub fn pyfn_extended_gcd_i64(a: i64, b: i64) -> PyResult<(i64, i64, i64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::number_theory::extended_gcd_i64(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// Modular exponentiation `base^exp mod m`. +/// +/// Shares the implementation in `discrete::primes::mod_pow_u64`. +/// +/// Rust: `discrete::number_theory::mod_pow_u64` +#[pyfunction] +#[pyo3(name = "mod_pow_u64", signature = (base, exp, m))] +pub fn pyfn_mod_pow_u64(base: u64, exp: u64, m: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::number_theory::mod_pow_u64(base, exp, m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The inverse of `a` modulo `m`, or `None` when `gcd(a, m) != 1`. +/// +/// The residue is returned in `[0, m)`; the modulus `0` has no residues +/// and yields `None`, while modulus `1` yields `0`. +/// +/// Rust: `discrete::number_theory::mod_inverse_u64` +#[pyfunction] +#[pyo3(name = "mod_inverse_u64", signature = (a, m))] +pub fn pyfn_mod_inverse_u64(a: u64, m: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::number_theory::mod_inverse_u64(a, m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// Chinese remainder theorem for general (not necessarily coprime) +/// moduli. +/// +/// Takes `(remainder, modulus)` pairs and returns the unique class +/// `(r, m)` with `m == lcm` of the moduli and `r` in `[0, m)` satisfying +/// every congruence. Returns `None` when the system is inconsistent, +/// when any modulus is zero, or when the combined modulus overflows a +/// `u64`. An empty system is solved by `(0, 1)`. +/// +/// Rust: `discrete::number_theory::crt` +#[pyfunction] +#[pyo3(name = "crt", signature = (residues))] +pub fn pyfn_crt<'py>(py: Python<'py>, residues: Vec<(u64, u64)>) -> PyResult> { + let residues = residues.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::number_theory::crt(&residues))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| (__x.0, __x.1))) +} + +/// Euler's totient: the count of integers in `[1, n]` coprime to `n`. +/// +/// `euler_phi(0)` is defined as `0`. +/// +/// Rust: `discrete::number_theory::euler_phi` +#[pyfunction] +#[pyo3(name = "euler_phi", signature = (n))] +pub fn pyfn_euler_phi(n: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::number_theory::euler_phi(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// `euler_phi` for every index up to `n`, by a sieve. +/// +/// Entry `i` of the returned vector is `euler_phi(i)`, so its length is +/// `n + 1`. +/// +/// Rust: `discrete::number_theory::phi_sieve` +#[pyfunction] +#[pyo3(name = "phi_sieve", signature = (n))] +pub fn pyfn_phi_sieve<'py>(py: Python<'py>, n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::number_theory::phi_sieve(n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Moebius function: `0` when `n` is not squarefree, otherwise +/// `(-1)^k` for `k` distinct prime factors. +/// +/// `mobius(0)` is defined as `0` and `mobius(1) == 1`. +/// +/// Rust: `discrete::number_theory::mobius` +#[pyfunction] +#[pyo3(name = "mobius", signature = (n))] +pub fn pyfn_mobius(n: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::number_theory::mobius(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// `mobius` for every index up to `n`, by a linear sieve. +/// +/// Entry `i` of the returned vector is `mobius(i)`, so its length is +/// `n + 1`. +/// +/// Rust: `discrete::number_theory::mobius_sieve` +#[pyfunction] +#[pyo3(name = "mobius_sieve", signature = (n))] +pub fn pyfn_mobius_sieve<'py>(py: Python<'py>, n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::number_theory::mobius_sieve(n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Every divisor of `n`, ascending. Empty for `n == 0`. +/// +/// Rust: `discrete::number_theory::divisors` +#[pyfunction] +#[pyo3(name = "divisors", signature = (n))] +pub fn pyfn_divisors<'py>(py: Python<'py>, n: u64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::number_theory::divisors(n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The number of divisors, `sigma_0(n)`. Zero for `n == 0`. +/// +/// Rust: `discrete::number_theory::divisor_count` +#[pyfunction] +#[pyo3(name = "divisor_count", signature = (n))] +pub fn pyfn_divisor_count(n: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::number_theory::divisor_count(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The sum of divisors, `sigma_1(n)`. Zero for `n == 0`. +/// +/// Panics: +/// Panics if the sum does not fit in a `u64`. +/// +/// Rust: `discrete::number_theory::divisor_sum` +#[pyfunction] +#[pyo3(name = "divisor_sum", signature = (n))] +pub fn pyfn_divisor_sum(n: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::number_theory::divisor_sum(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The divisor power sum `sigma_k(n) = sum_{d | n} d^k`. +/// +/// `k == 0` counts divisors. Zero for `n == 0`. +/// +/// Panics: +/// Panics if the sum does not fit in a `u64`. +/// +/// Rust: `discrete::number_theory::sigma_k` +#[pyfunction] +#[pyo3(name = "sigma_k", signature = (n, k))] +pub fn pyfn_sigma_k(n: u64, k: u32) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::number_theory::sigma_k(n, k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Whether `n` equals the sum of its proper divisors. +/// +/// Panics: +/// Panics if the divisor sum does not fit in a `u64`. +/// +/// Rust: `discrete::number_theory::is_perfect` +#[pyfunction] +#[pyo3(name = "is_perfect", signature = (n))] +pub fn pyfn_is_perfect(n: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::number_theory::is_perfect(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Whether the proper divisors of `n` sum to more than `n`. +/// +/// Panics: +/// Panics if the divisor sum does not fit in a `u64`. +/// +/// Rust: `discrete::number_theory::is_abundant` +#[pyfunction] +#[pyo3(name = "is_abundant", signature = (n))] +pub fn pyfn_is_abundant(n: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::number_theory::is_abundant(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Whether the proper divisors of `n` sum to less than `n`. +/// +/// Panics: +/// Panics if the divisor sum does not fit in a `u64`. +/// +/// Rust: `discrete::number_theory::is_deficient` +#[pyfunction] +#[pyo3(name = "is_deficient", signature = (n))] +pub fn pyfn_is_deficient(n: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::number_theory::is_deficient(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// All amicable pairs `(a, b)` with `a < b <= limit`. +/// +/// A pair is amicable when each number is the sum of the other's proper +/// divisors. Aliquot sums are built by one `O(limit log limit)` sieve. +/// +/// Rust: `discrete::number_theory::amicable_pairs` +#[pyfunction] +#[pyo3(name = "amicable_pairs", signature = (limit))] +pub fn pyfn_amicable_pairs<'py>(py: Python<'py>, limit: u64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::number_theory::amicable_pairs(limit))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// The least `k > 0` with `a^k == 1 (mod n)`, or `None` when `a` and `n` +/// are not coprime. +/// +/// The trivial group modulo `1` gives `Some(1)`. +/// +/// Rust: `discrete::number_theory::multiplicative_order` +#[pyfunction] +#[pyo3(name = "multiplicative_order", signature = (a, n))] +pub fn pyfn_multiplicative_order(a: u64, n: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::number_theory::multiplicative_order(a, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// The least primitive root modulo the prime `p`, or `None` when `p` is +/// not prime. +/// +/// A primitive root generates the whole multiplicative group, so its +/// order is `p - 1`. +/// +/// Rust: `discrete::number_theory::primitive_root` +#[pyfunction] +#[pyo3(name = "primitive_root", signature = (p))] +pub fn pyfn_primitive_root(p: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::number_theory::primitive_root(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// Every primitive root modulo the prime `p`, ascending. +/// +/// There are `euler_phi(p - 1)` of them; the list is empty when `p` is +/// not prime. +/// +/// Rust: `discrete::number_theory::all_primitive_roots` +#[pyfunction] +#[pyo3(name = "all_primitive_roots", signature = (p))] +pub fn pyfn_all_primitive_roots<'py>(py: Python<'py>, p: u64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::number_theory::all_primitive_roots(p))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Discrete logarithm by baby-step giant-step: the least `x >= 0` with +/// `base^x == target (mod modulus)`, or `None` when none exists. +/// +/// The modulus is arbitrary — a leading reduction strips the common +/// factors of `base` and `modulus` before the classical coprime search, +/// so `base` need not be invertible. Time and memory are both +/// `O(sqrt(modulus))`. +/// +/// Rust: `discrete::number_theory::discrete_log_bsgs` +#[pyfunction] +#[pyo3(name = "discrete_log_bsgs", signature = (base, target, modulus))] +pub fn pyfn_discrete_log_bsgs(base: u64, target: u64, modulus: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::number_theory::discrete_log_bsgs(base, target, modulus)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// Discrete logarithm modulo a prime by the Pohlig-Hellman reduction. +/// +/// `factorization` is the factorization of the order of `base` — for a +/// primitive root, that of `p - 1`, as produced by +/// `discrete::primes::factorize`. The logarithm is recovered in +/// each prime-power subgroup and glued by the CRT, which costs +/// `O(sum e_i (log n + sqrt(q_i)))` instead of `O(sqrt(p))`. +/// +/// Returns `None` when `p` is not an odd prime, when the factorization +/// does not describe the order of `base`, or when no logarithm exists. +/// +/// Rust: `discrete::number_theory::discrete_log_pohlig_hellman` +#[pyfunction] +#[pyo3(name = "discrete_log_pohlig_hellman", signature = (base, target, p, factorization))] +pub fn pyfn_discrete_log_pohlig_hellman<'py>(py: Python<'py>, base: u64, target: u64, p: u64, factorization: Vec<(u64, u32)>) -> PyResult> { + let factorization = factorization.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::number_theory::discrete_log_pohlig_hellman(base, target, p, &factorization))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// The Legendre symbol `(a/p)`: `0` when `p` divides `a`, `1` when `a` +/// is a nonzero quadratic residue, `-1` otherwise. +/// +/// Panics: +/// Panics unless `p` is an odd prime. +/// +/// Rust: `discrete::number_theory::legendre_symbol` +#[pyfunction] +#[pyo3(name = "legendre_symbol", signature = (a, p))] +pub fn pyfn_legendre_symbol(a: i64, p: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::number_theory::legendre_symbol(a, p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Jacobi symbol `(a/n)` for odd `n > 0`, by reciprocity. +/// +/// Equal to the Legendre symbol when `n` is prime. A value of `1` for +/// composite `n` does not imply that `a` is a residue. +/// +/// Panics: +/// Panics if `n` is even or zero. +/// +/// Rust: `discrete::number_theory::jacobi_symbol` +#[pyfunction] +#[pyo3(name = "jacobi_symbol", signature = (a, n))] +pub fn pyfn_jacobi_symbol(a: i64, n: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::number_theory::jacobi_symbol(a, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// A square root of `a` modulo the prime `p` by Tonelli-Shanks, or +/// `None` when `a` is a non-residue. +/// +/// The smaller of the two roots is returned, so the result is always in +/// `[0, p/2]`. +/// +/// Panics: +/// Panics unless `p` is prime. +/// +/// Rust: `discrete::number_theory::tonelli_shanks` +#[pyfunction] +#[pyo3(name = "tonelli_shanks", signature = (a, p))] +pub fn pyfn_tonelli_shanks(a: u64, p: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::number_theory::tonelli_shanks(a, p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// The nonzero quadratic residues modulo the odd prime `p`, ascending. +/// +/// There are exactly `(p - 1) / 2` of them. The list is empty when `p` +/// is not an odd prime. +/// +/// Rust: `discrete::number_theory::quadratic_residues` +#[pyfunction] +#[pyo3(name = "quadratic_residues", signature = (p))] +pub fn pyfn_quadratic_residues<'py>(py: Python<'py>, p: u64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::number_theory::quadratic_residues(p))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Carmichael's `lambda(n)`: the exponent of the group of units modulo +/// `n`, that is the least `k` with `a^k == 1 (mod n)` for every `a` +/// coprime to `n`. +/// +/// Always a divisor of `euler_phi(n)`. `lambda(0)` is defined as `0`. +/// +/// Rust: `discrete::number_theory::carmichael_lambda` +#[pyfunction] +#[pyo3(name = "carmichael_lambda", signature = (n))] +pub fn pyfn_carmichael_lambda(n: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::number_theory::carmichael_lambda(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Whether `n` is a Carmichael number: composite, yet `a^(n-1) == 1 +/// (mod n)` for every `a` coprime to `n`. +/// +/// Decided by Korselt's criterion — `n` odd, squarefree, and `p - 1` +/// divides `n - 1` for every prime `p` dividing `n`. +/// +/// Rust: `discrete::number_theory::is_carmichael` +#[pyfunction] +#[pyo3(name = "is_carmichael", signature = (n))] +pub fn pyfn_is_carmichael(n: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::number_theory::is_carmichael(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The sum of the digits of `n` written in `base`. +/// +/// Panics: +/// Panics if `base < 2`. +/// +/// Rust: `discrete::number_theory::digit_sum` +#[pyfunction] +#[pyo3(name = "digit_sum", signature = (n, base))] +pub fn pyfn_digit_sum(n: u64, base: u32) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::number_theory::digit_sum(n, base)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The digital root: repeated digit sums until a single digit remains. +/// +/// Equal to `1 + (n - 1) mod (base - 1)` for positive `n`, which is the +/// closed form used here. +/// +/// Panics: +/// Panics if `base < 2`. +/// +/// Rust: `discrete::number_theory::digital_root` +#[pyfunction] +#[pyo3(name = "digital_root", signature = (n, base))] +pub fn pyfn_digital_root(n: u64, base: u32) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::number_theory::digital_root(n, base)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Whether the digits of `n` in `base` read the same both ways. +/// +/// Panics: +/// Panics if `base < 2`. +/// +/// Rust: `discrete::number_theory::is_palindrome` +#[pyfunction] +#[pyo3(name = "is_palindrome", signature = (n, base))] +pub fn pyfn_is_palindrome(n: u64, base: u32) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::number_theory::is_palindrome(n, base)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// `n` with its digits in `base` reversed. +/// +/// Panics: +/// Panics if `base < 2`, or if the reversed value overflows a `u64`. +/// +/// Rust: `discrete::number_theory::reverse_digits` +#[pyfunction] +#[pyo3(name = "reverse_digits", signature = (n, base))] +pub fn pyfn_reverse_digits(n: u64, base: u32) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::number_theory::reverse_digits(n, base)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Whether iterating the sum of squared decimal digits reaches `1`. +/// +/// Cycle detection is by Floyd's algorithm; `0` is not happy. +/// +/// Rust: `discrete::number_theory::happy_number` +#[pyfunction] +#[pyo3(name = "happy_number", signature = (n))] +pub fn pyfn_happy_number(n: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::number_theory::happy_number(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Collatz trajectory of `n`, from `n` down to the terminal `1`. +/// +/// Empty for `n == 0`. +/// +/// Panics: +/// Panics if some `3x + 1` step overflows a `u64`. +/// +/// Rust: `discrete::number_theory::collatz_trajectory` +#[pyfunction] +#[pyo3(name = "collatz_trajectory", signature = (n))] +pub fn pyfn_collatz_trajectory<'py>(py: Python<'py>, n: u64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::number_theory::collatz_trajectory(n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The total stopping time: the number of Collatz steps from `n` to `1`. +/// +/// Zero for `n == 0` and `n == 1`. +/// +/// Panics: +/// Panics if some `3x + 1` step overflows a `u64`. +/// +/// Rust: `discrete::number_theory::collatz_stopping_time` +#[pyfunction] +#[pyo3(name = "collatz_stopping_time", signature = (n))] +pub fn pyfn_collatz_stopping_time(n: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::number_theory::collatz_stopping_time(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// A representation `n = a^2 + b^2` with `a <= b`, or `None` when none +/// exists. +/// +/// By Fermat's two-square theorem a representation exists exactly when +/// every prime `p == 3 (mod 4)` divides `n` to an even power; that test +/// runs first, so non-representable inputs cost only a factorization. +/// +/// Rust: `discrete::number_theory::sum_of_two_squares` +#[pyfunction] +#[pyo3(name = "sum_of_two_squares", signature = (n))] +pub fn pyfn_sum_of_two_squares(n: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::number_theory::sum_of_two_squares(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| (__x.0, __x.1))) +} + +/// A representation `n = a^2 + b^2 + c^2 + d^2` with the parts +/// ascending. +/// +/// Lagrange's four-square theorem guarantees one exists for every `n`. +/// The search fixes the largest part first, which leaves a small +/// remainder for the inner two-square search. +/// +/// Panics: +/// Panics if no representation is found, which would contradict +/// Lagrange's theorem. +/// +/// Rust: `discrete::number_theory::sum_of_four_squares` +#[pyfunction] +#[pyo3(name = "sum_of_four_squares", signature = (n))] +pub fn pyfn_sum_of_four_squares(n: u64) -> PyResult<(u64, u64, u64, u64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::number_theory::sum_of_four_squares(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2, __v.3)) +} + +/// Every primitive Pythagorean triple `(a, b, c)` with `a < b < c` and +/// hypotenuse `c <= limit`, ascending. +/// +/// Generated by the Berggren ternary tree rooted at `(3, 4, 5)`: every +/// primitive triple is reached exactly once, so no gcd filtering or +/// deduplication is needed. +/// +/// Rust: `discrete::number_theory::pythagorean_triples_primitive` +#[pyfunction] +#[pyo3(name = "pythagorean_triples_primitive", signature = (limit))] +pub fn pyfn_pythagorean_triples_primitive<'py>(py: Python<'py>, limit: u64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::number_theory::pythagorean_triples_primitive(limit))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, __x.2)).collect::>()) +} + +/// Factor a Gaussian integer into Gaussian primes. +/// +/// The product of the returned list reproduces the input exactly: a +/// leading unit (`-1`, `i` or `-i`) is included whenever one is needed, +/// and the empty list is returned for the input `1` and for `0`. Rational +/// primes `p == 3 (mod 4)` stay inert and appear as `(p, 0)`; `2` splits +/// as powers of `1 + i`; primes `p == 1 (mod 4)` split into the conjugate +/// pair coming from `p = a^2 + b^2`. +/// +/// Panics: +/// Panics if the norm `re^2 + im^2` does not fit in a `u64`. +/// +/// Rust: `discrete::number_theory::gaussian_integer_factor` +#[pyfunction] +#[pyo3(name = "gaussian_integer_factor", signature = (re, im))] +pub fn pyfn_gaussian_integer_factor<'py>(py: Python<'py>, re: i64, im: i64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::number_theory::gaussian_integer_factor(re, im))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// The Frobenius number of a coin system: the largest amount that cannot +/// be paid exactly. +/// +/// `None` when the coins share a common factor (infinitely many amounts +/// are then unreachable) or when the list holds no positive coin. A coin +/// of value `1` makes every non-negative amount payable and reports `0`. +/// Two coprime coins use the closed form `ab - a - b`; more coins use a +/// Dijkstra search over the residues of the smallest coin, so memory is +/// `O(min(coins))`. +/// +/// Rust: `discrete::number_theory::frobenius_number` +#[pyfunction] +#[pyo3(name = "frobenius_number", signature = (coins))] +pub fn pyfn_frobenius_number<'py>(py: Python<'py>, coins: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::number_theory::frobenius_number(&coins))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// The greedy (Fibonacci-Sylvester) Egyptian-fraction expansion of a +/// positive rational: denominators `d` with `sum 1/d == r`. +/// +/// Each step subtracts the largest unit fraction not exceeding the +/// remainder, which strictly reduces the numerator and therefore +/// terminates. An empty list is returned for `r <= 0`. +/// +/// Rust: `discrete::number_theory::egyptian_fractions_greedy` +#[pyfunction] +#[pyo3(name = "egyptian_fractions_greedy", signature = (r))] +pub fn pyfn_egyptian_fractions_greedy<'py>(py: Python<'py>, r: crate::runtime::coerce::RationalArg) -> PyResult>> { + let r = r.0; + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::number_theory::egyptian_fractions_greedy(&r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| -> PyResult> { Ok(crate::runtime::coerce::bigint_out(py, &__x)?) }).collect::>>()?) +} + +/// The Zeckendorf representation of `n`: the unique set of +/// non-consecutive Fibonacci numbers summing to `n`, ascending. +/// +/// Uses the Fibonacci numbers `1, 2, 3, 5, 8, ...`, each at most once. +/// Empty for `n == 0`. +/// +/// Rust: `discrete::number_theory::zeckendorf` +#[pyfunction] +#[pyo3(name = "zeckendorf", signature = (n))] +pub fn pyfn_zeckendorf<'py>(py: Python<'py>, n: u64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::number_theory::zeckendorf(n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Lucas sequence `U_n(P, Q) mod m`, where `U_0 = 0`, `U_1 = 1` and +/// `U_n = P*U_{n-1} - Q*U_{n-2}`. +/// +/// `U_n(1, -1)` is the Fibonacci sequence. Evaluated by the recurrence, +/// so the cost is linear in `n`. Returns `0` for `m <= 1`. +/// +/// Rust: `discrete::number_theory::lucas_sequence_u` +#[pyfunction] +#[pyo3(name = "lucas_sequence_u", signature = (p, q, n, m))] +pub fn pyfn_lucas_sequence_u(p: i64, q: i64, n: u64, m: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::number_theory::lucas_sequence_u(p, q, n, m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Every integer solution of `a*x^2 + b*y^2 == c`, ascending. +/// +/// Only the definite case is enumerable: with `a > 0`, `b > 0` and +/// `c >= 0` the solution set is finite and is returned in full. An +/// indefinite form (a Pell-type equation) has infinitely many solutions, +/// so an empty list is returned there instead. +/// +/// Rust: `discrete::number_theory::quadratic_diophantine_solve` +#[pyfunction] +#[pyo3(name = "quadratic_diophantine_solve", signature = (a, b, c))] +pub fn pyfn_quadratic_diophantine_solve<'py>(py: Python<'py>, a: i64, b: i64, c: i64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::number_theory::quadratic_diophantine_solve(a, b, c))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Solve `a*x + b*y == c` over the integers. +/// +/// Returns `(x0, y0, dx, dy)`: a particular solution together with the +/// homogeneous step, so that `(x0 + t*dx, y0 + t*dy)` is a solution for +/// every integer `t` and every solution has this form. `None` when +/// `gcd(a, b)` does not divide `c`, when both coefficients are zero, or +/// when the particular solution overflows an `i64`. +/// +/// Rust: `discrete::number_theory::linear_diophantine` +#[pyfunction] +#[pyo3(name = "linear_diophantine", signature = (a, b, c))] +pub fn pyfn_linear_diophantine(a: i64, b: i64, c: i64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::number_theory::linear_diophantine(a, b, c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| (__x.0, __x.1, __x.2, __x.3))) +} + +/// The `n`-th positive rational in breadth-first order on the +/// Stern-Brocot tree, counting the root `1/1` as `n == 1`. +/// +/// The bits of `n` below its leading bit spell the descent: `0` goes +/// left, `1` goes right, and each node is the mediant of its bounding +/// ancestors. Every positive rational appears exactly once, already in +/// lowest terms. +/// +/// Panics: +/// Panics if `n == 0`. +/// +/// Rust: `discrete::number_theory::stern_brocot_nth` +#[pyfunction] +#[pyo3(name = "stern_brocot_nth", signature = (n))] +pub fn pyfn_stern_brocot_nth<'py>(py: Python<'py>, n: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::number_theory::stern_brocot_nth(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::rational_out(py, &__v)?) +} + +/// The next fraction after `a` in the Farey sequence of order `n`. +/// +/// The successor `r/s` is the unique fraction with `s <= n` and +/// `r*q - p*s == 1` for `a = p/q`, found by solving `p*s == -1 (mod q)` +/// and taking the largest admissible `s`. +/// +/// Panics: +/// Panics if `n == 0`, if `a` does not fit in `i64`, or if the +/// denominator of `a` exceeds `n`. +/// +/// Rust: `discrete::number_theory::farey_next` +#[pyfunction] +#[pyo3(name = "farey_next", signature = (a, n))] +pub fn pyfn_farey_next<'py>(py: Python<'py>, a: crate::runtime::coerce::RationalArg, n: u64) -> PyResult> { + let a = a.0; + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::number_theory::farey_next(&a, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::rational_out(py, &__v)?) +} + +/// The Dirichlet convolution `(f * g)(n) = sum_{d | n} f(d) g(n/d)`. +/// +/// Both slices are indexed by the argument, so element `i` holds the +/// value at `i` and element `0` is unused (it is zero on output). The +/// result has the length of the shorter input. +/// +/// Rust: `discrete::number_theory::dirichlet_convolution` +#[pyfunction] +#[pyo3(name = "dirichlet_convolution", signature = (f, g))] +pub fn pyfn_dirichlet_convolution<'py>(py: Python<'py>, f: Vec, g: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::number_theory::dirichlet_convolution(&f, &g))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_gcd_u64, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lcm_u64, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_extended_gcd_i64, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mod_pow_u64, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mod_inverse_u64, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_crt, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_euler_phi, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_phi_sieve, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mobius, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mobius_sieve, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_divisors, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_divisor_count, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_divisor_sum, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sigma_k, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_is_perfect, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_is_abundant, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_is_deficient, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_amicable_pairs, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_multiplicative_order, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_primitive_root, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_all_primitive_roots, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_discrete_log_bsgs, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_discrete_log_pohlig_hellman, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_legendre_symbol, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_jacobi_symbol, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tonelli_shanks, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_quadratic_residues, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_carmichael_lambda, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_is_carmichael, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_digit_sum, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_digital_root, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_is_palindrome, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_reverse_digits, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_happy_number, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_collatz_trajectory, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_collatz_stopping_time, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sum_of_two_squares, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sum_of_four_squares, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pythagorean_triples_primitive, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gaussian_integer_factor, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_frobenius_number, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_egyptian_fractions_greedy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_zeckendorf, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lucas_sequence_u, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_quadratic_diophantine_solve, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_linear_diophantine, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_stern_brocot_nth, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_farey_next, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dirichlet_convolution, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_discrete__partitions.rs b/bindings/python/src/generated/m_discrete__partitions.rs new file mode 100644 index 0000000..01a57b7 --- /dev/null +++ b/bindings/python/src/generated/m_discrete__partitions.rs @@ -0,0 +1,253 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// The number of partitions of `n`, by Euler's pentagonal number theorem. +/// +/// The theorem gives `p(n) = sum_k (-1)^(k+1) [p(n - g_k) + p(n - g'_k)]` over +/// the generalised pentagonal numbers `g_k = k(3k-1)/2`. There are only +/// `O(sqrt n)` of those below `n`, so each value costs `O(sqrt n)` additions +/// and the whole table costs `O(n^1.5)` -- far less than the `O(n^2)` of the +/// naive "partitions of n into parts at most m" table. +/// +/// Rust: `discrete::partitions::partition_count` +#[pyfunction] +#[pyo3(name = "partition_count", signature = (n))] +pub fn pyfn_partition_count<'py>(py: Python<'py>, n: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::partitions::partition_count(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// `p(0)` through `p(n)`. +/// +/// Rust: `discrete::partitions::partition_count_table` +#[pyfunction] +#[pyo3(name = "partition_count_table", signature = (n))] +pub fn pyfn_partition_count_table<'py>(py: Python<'py>, n: u64) -> PyResult>> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::partitions::partition_count_table(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| -> PyResult> { Ok(crate::runtime::coerce::bigint_out(py, &__x)?) }).collect::>>()?) +} + +/// The partitions of `n`, each weakly decreasing, in reverse lexicographic +/// order (starting at `[n]` and ending at all ones). +/// +/// Rust: `discrete::partitions::partitions_iter` +#[pyfunction] +#[pyo3(name = "partitions_iter", signature = (n))] +pub fn pyfn_partitions_iter(n: u64) -> PyResult>> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::partitions::partitions_iter(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.collect::>()) +} + +/// The number of partitions of `n` into exactly `k` positive parts. +/// +/// Recurrence `P(n, k) = P(n-1, k-1) + P(n-k, k)`: either the smallest part is +/// a one, which removes it, or every part is at least two, which subtracts one +/// from each. +/// +/// Rust: `discrete::partitions::partitions_into_k` +#[pyfunction] +#[pyo3(name = "partitions_into_k", signature = (n, k))] +pub fn pyfn_partitions_into_k<'py>(py: Python<'py>, n: u64, k: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::partitions::partitions_into_k(n, k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// The number of partitions of `n` into at most `k` parts. +/// +/// By conjugation this also counts the partitions of `n` whose largest part is +/// at most `k`. +/// +/// Rust: `discrete::partitions::partition_count_into_at_most_k` +#[pyfunction] +#[pyo3(name = "partition_count_into_at_most_k", signature = (n, k))] +pub fn pyfn_partition_count_into_at_most_k<'py>(py: Python<'py>, n: u64, k: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::partitions::partition_count_into_at_most_k(n, k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// The number of partitions of `n` into distinct parts. +/// +/// Product `prod_{i=1..n} (1 + x^i)` accumulated as a coefficient table. +/// +/// Rust: `discrete::partitions::partitions_distinct` +#[pyfunction] +#[pyo3(name = "partitions_distinct", signature = (n))] +pub fn pyfn_partitions_distinct<'py>(py: Python<'py>, n: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::partitions::partitions_distinct(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// The number of partitions of `n` into odd parts. +/// +/// Euler's theorem says this equals `partitions_distinct`; the two are +/// computed independently here so that agreement is evidence rather than a +/// tautology. +/// +/// Rust: `discrete::partitions::partitions_odd` +#[pyfunction] +#[pyo3(name = "partitions_odd", signature = (n))] +pub fn pyfn_partitions_odd<'py>(py: Python<'py>, n: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::partitions::partitions_odd(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// The conjugate partition: the column lengths of the Young diagram. +/// +/// `conjugate(p)[j]` counts the parts of `p` exceeding `j`. Conjugation is an +/// involution and preserves the sum. +/// +/// Rust: `discrete::partitions::partition_conjugate` +#[pyfunction] +#[pyo3(name = "partition_conjugate", signature = (p))] +pub fn pyfn_partition_conjugate<'py>(py: Python<'py>, p: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::partitions::partition_conjugate(&p))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Young diagram of `p` in English notation: row `i` has `p[i]` true +/// cells, padded with false to the width of the first row. +/// +/// Rust: `discrete::partitions::young_diagram` +#[pyfunction] +#[pyo3(name = "young_diagram", signature = (p))] +pub fn pyfn_young_diagram<'py>(py: Python<'py>, p: Vec) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::partitions::young_diagram(&p))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The hook length of every cell of the Young diagram, in the same ragged +/// shape as `p`. +/// +/// The hook of a cell is the cell itself, the cells to its right in the row +/// (the arm), and the cells below it in the column (the leg). +/// +/// Rust: `discrete::partitions::hook_lengths` +#[pyfunction] +#[pyo3(name = "hook_lengths", signature = (p))] +pub fn pyfn_hook_lengths<'py>(py: Python<'py>, p: Vec) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::partitions::hook_lengths(&p))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The number of standard Young tableaux of shape `p`, by the hook length +/// formula `n! / prod(hooks)`. +/// +/// Panics: +/// Panics if `p` is not weakly decreasing, since the hook lengths would then +/// be meaningless. +/// +/// Rust: `discrete::partitions::standard_tableaux_count` +#[pyfunction] +#[pyo3(name = "standard_tableaux_count", signature = (p))] +pub fn pyfn_standard_tableaux_count<'py>(py: Python<'py>, p: Vec) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::partitions::standard_tableaux_count(&p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// The Robinson-Schensted correspondence: a permutation of `0..n` maps to a +/// pair of standard Young tableaux of the same shape. +/// +/// `P` is built by row insertion (each value bumps the leftmost strictly +/// larger entry down a row) and `Q` records which cell was created at each +/// step, so `Q` is standard by construction. The map is a bijection between +/// `S_n` and such pairs, which is the combinatorial content of the identity +/// `sum_shapes f(shape)^2 = n!`. +/// +/// Entries of `P` are the permutation's own values; entries of `Q` are the +/// step indices `0..n`. +/// +/// Rust: `discrete::partitions::rsk_correspondence` +#[pyfunction] +#[pyo3(name = "rsk_correspondence", signature = (perm))] +pub fn pyfn_rsk_correspondence<'py>(py: Python<'py>, perm: Vec) -> PyResult<(Vec>, Vec>)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::partitions::rsk_correspondence(&perm))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// The side of the Durfee square: the largest `s` with `p[s-1] >= s`, that is, +/// the largest square that fits in the top-left of the Young diagram. +/// +/// Rust: `discrete::partitions::durfee_square` +#[pyfunction] +#[pyo3(name = "durfee_square", signature = (p))] +pub fn pyfn_durfee_square<'py>(py: Python<'py>, p: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::partitions::durfee_square(&p))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Hardy-Ramanujan asymptotic for the partition count, +/// `exp(pi sqrt(2n/3)) / (4 n sqrt 3)`. +/// +/// The relative error decays like `1/sqrt(n)`, so this is an order-of-magnitude +/// estimate rather than a value to round. +/// +/// Rust: `discrete::partitions::hardy_ramanujan_estimate` +#[pyfunction] +#[pyo3(name = "hardy_ramanujan_estimate", signature = (n))] +pub fn pyfn_hardy_ramanujan_estimate(n: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::partitions::hardy_ramanujan_estimate(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// True when every even number from 4 to `up_to` is a sum of two primes. +/// +/// Verification, not proof: the conjecture is open. Returns `true` vacuously +/// for `up_to < 4`. +/// +/// Rust: `discrete::partitions::goldbach_conjecture_verify` +#[pyfunction] +#[pyo3(name = "goldbach_conjecture_verify", signature = (up_to))] +pub fn pyfn_goldbach_conjecture_verify(up_to: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::partitions::goldbach_conjecture_verify(up_to)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_partition_count, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_partition_count_table, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_partitions_iter, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_partitions_into_k, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_partition_count_into_at_most_k, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_partitions_distinct, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_partitions_odd, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_partition_conjugate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_young_diagram, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hook_lengths, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_standard_tableaux_count, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rsk_correspondence, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_durfee_square, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hardy_ramanujan_estimate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_goldbach_conjecture_verify, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_discrete__primes.rs b/bindings/python/src/generated/m_discrete__primes.rs new file mode 100644 index 0000000..64ace84 --- /dev/null +++ b/bindings/python/src/generated/m_discrete__primes.rs @@ -0,0 +1,404 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// All primes up to and including `n`, by the sieve of Eratosthenes. +/// +/// Rust: `discrete::primes::sieve_eratosthenes` +#[pyfunction] +#[pyo3(name = "sieve_eratosthenes", signature = (n))] +pub fn pyfn_sieve_eratosthenes<'py>(py: Python<'py>, n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::primes::sieve_eratosthenes(n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Primes in `[lo, hi)`, sieving only that window. +/// +/// The window is marked using the primes up to `sqrt(hi)`, so memory scales +/// with the window rather than with `hi`. +/// +/// Rust: `discrete::primes::sieve_segmented` +#[pyfunction] +#[pyo3(name = "sieve_segmented", signature = (lo, hi))] +pub fn pyfn_sieve_segmented<'py>(py: Python<'py>, lo: u64, hi: u64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::primes::sieve_segmented(lo, hi))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Primes up to `n` together with the smallest prime factor of every +/// integer up to `n`, by the linear (Gries-Misra) sieve. +/// +/// Each composite is struck exactly once, by its smallest prime factor. +/// +/// Rust: `discrete::primes::sieve_linear` +#[pyfunction] +#[pyo3(name = "sieve_linear", signature = (n))] +pub fn pyfn_sieve_linear(n: usize) -> PyResult<(Vec, Vec)> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::primes::sieve_linear(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Modular exponentiation on `u64`. +/// +/// Rust: `discrete::primes::mod_pow_u64` +#[pyfunction] +#[pyo3(name = "mod_pow_u64", signature = (base, exp, m))] +pub fn pyfn_mod_pow_u64(base: u64, exp: u64, m: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::primes::mod_pow_u64(base, exp, m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Deterministic primality for every `u64`. +/// +/// Miller-Rabin over the first twelve prime bases is proven correct for +/// all 64-bit inputs, so this is a decision procedure rather than a +/// probabilistic test. +/// +/// Rust: `discrete::primes::is_prime_u64` +#[pyfunction] +#[pyo3(name = "is_prime_u64", signature = (n))] +pub fn pyfn_is_prime_u64(n: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::primes::is_prime_u64(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Probabilistic primality for a `BigInt`: `rounds` Miller-Rabin bases +/// followed by a strong Lucas test, which together form BPSW. +/// +/// No composite is known to pass BPSW, though none is proven not to; a +/// composite passing `rounds` independent Miller-Rabin bases alone has +/// probability at most `4^-rounds`. +/// +/// Panics: +/// Panics if `n` is negative. +/// +/// Rust: `discrete::primes::is_prime_bigint` +#[pyfunction] +#[pyo3(name = "is_prime_bigint", signature = (n, rounds, rng))] +pub fn pyfn_is_prime_bigint(n: crate::runtime::coerce::BigIntArg, rounds: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let n = n.0; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::primes::is_prime_bigint(&n, rounds, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The smallest prime strictly greater than `n`. +/// +/// Panics: +/// Panics if the search would overflow `u64`. +/// +/// Rust: `discrete::primes::next_prime` +#[pyfunction] +#[pyo3(name = "next_prime", signature = (n))] +pub fn pyfn_next_prime(n: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::primes::next_prime(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The largest prime strictly less than `n`, or `None` below 3. +/// +/// Rust: `discrete::primes::prev_prime` +#[pyfunction] +#[pyo3(name = "prev_prime", signature = (n))] +pub fn pyfn_prev_prime(n: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::primes::prev_prime(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// A random prime with exactly `bits` bits. +/// +/// Panics: +/// Panics if `bits` is below 2. +/// +/// Rust: `discrete::primes::random_prime` +#[pyfunction] +#[pyo3(name = "random_prime", signature = (bits, rng))] +pub fn pyfn_random_prime<'py>(py: Python<'py>, bits: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::primes::random_prime(bits, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// A non-trivial factor of a composite `n` by Pollard's rho with +/// Brent's cycle detection, or `None` if the attempt fails. +/// +/// Rust: `discrete::primes::pollard_rho` +#[pyfunction] +#[pyo3(name = "pollard_rho", signature = (n))] +pub fn pyfn_pollard_rho(n: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::primes::pollard_rho(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// Pollard's rho over `BigInt`, for factors beyond `u64`. +/// +/// Rust: `discrete::primes::pollard_rho_bigint` +#[pyfunction] +#[pyo3(name = "pollard_rho_bigint", signature = (n, rng))] +pub fn pyfn_pollard_rho_bigint<'py>(py: Python<'py>, n: crate::runtime::coerce::BigIntArg, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult>> { + let n = n.0; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::primes::pollard_rho_bigint(&n, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(match __v { Some(__x) => Some(crate::runtime::coerce::bigint_out(py, &__x)?), None => None }) +} + +/// Pollard's p-1 method: finds a factor `p` of `n` when `p - 1` is +/// `bound`-smooth. Returns `None` when no such factor separates. +/// +/// Rust: `discrete::primes::pollard_p_minus_1` +#[pyfunction] +#[pyo3(name = "pollard_p_minus_1", signature = (n, bound))] +pub fn pyfn_pollard_p_minus_1(n: u64, bound: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::primes::pollard_p_minus_1(n, bound)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// Trial division up to `limit`: the factors found and the unfactored +/// remainder. +/// +/// Rust: `discrete::primes::trial_division` +#[pyfunction] +#[pyo3(name = "trial_division", signature = (n, limit))] +pub fn pyfn_trial_division(n: u64, limit: u64) -> PyResult<(Vec<(u64, u32)>, u64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::primes::trial_division(n, limit)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0.into_iter().map(|__x| (__x.0, __x.1)).collect::>(), __v.1)) +} + +/// Fermat's method: write an odd `n` as a difference of squares. +/// +/// Effective only when `n` has two factors close to its square root; +/// returns `None` once the search passes a generous bound. +/// +/// Rust: `discrete::primes::fermat_factor` +#[pyfunction] +#[pyo3(name = "fermat_factor", signature = (n))] +pub fn pyfn_fermat_factor(n: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::primes::fermat_factor(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| (__x.0, __x.1))) +} + +/// The complete prime factorization of `n`, ascending by prime. +/// +/// Small factors go by trial division, the rest by Pollard's rho. +/// +/// Rust: `discrete::primes::factorize` +#[pyfunction] +#[pyo3(name = "factorize", signature = (n))] +pub fn pyfn_factorize<'py>(py: Python<'py>, n: u64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::primes::factorize(n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// The factorization of a `BigInt` into primes. +/// +/// Complete in every case that terminates, which is every case observed. +/// Unlike `factorize` this cannot promise it: splitting a large composite +/// has no guaranteed-terminating fallback the way trial division is one below +/// `2^64`, so a cofactor that survives Pollard rho and Pollard p-1 is returned +/// as a single entry even though it is known composite. A caller that needs +/// certainty should test each returned base with `is_prime_bigint`. Rho is +/// tried three times and each call draws sixteen fresh random polynomials, so +/// reaching that state means forty-eight independent attempts all failed. +/// +/// Panics: +/// Panics if `n` is not positive. +/// +/// Rust: `discrete::primes::factorize_bigint` +#[pyfunction] +#[pyo3(name = "factorize_bigint", signature = (n, rng))] +pub fn pyfn_factorize_bigint<'py>(py: Python<'py>, n: crate::runtime::coerce::BigIntArg, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult, u32)>> { + let n = n.0; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::primes::factorize_bigint(&n, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| -> PyResult<(pyo3::Bound<'py, pyo3::PyAny>, u32)> { Ok((crate::runtime::coerce::bigint_out(py, &__x.0)?, __x.1)) }).collect::>>()?) +} + +/// The exact count of primes up to `n`, without sieving to `n`. +/// +/// Uses the Lucy_Hedgehog recurrence over the distinct values of +/// `n / i`: starting from a count of all integers, each prime up to +/// `sqrt(n)` sieves its multiples out of every partial count at once. The +/// state has `O(sqrt n)` entries and the whole computation is +/// `O(n^(3/4))`, so `pi(10^9)` is reachable without a `10^9`-bit sieve. +/// +/// Rust: `discrete::primes::prime_count_meissel` +#[pyfunction] +#[pyo3(name = "prime_count_meissel", signature = (n))] +pub fn pyfn_prime_count_meissel(n: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::primes::prime_count_meissel(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The logarithmic integral estimate of `pi(x)`, by series. +/// +/// Rust: `discrete::primes::prime_count_li_approx` +#[pyfunction] +#[pyo3(name = "prime_count_li_approx", signature = (x))] +pub fn pyfn_prime_count_li_approx(x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::primes::prime_count_li_approx(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Riemann's refinement `R(x) = sum_{k>=1} mu(k)/k * li(x^(1/k))`. +/// +/// Rust: `discrete::primes::riemann_r` +#[pyfunction] +#[pyo3(name = "riemann_r", signature = (x))] +pub fn pyfn_riemann_r(x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::primes::riemann_r(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The `n`th prime, one-based: `nth_prime(1) == 2`. +/// +/// Panics: +/// Panics if `n` is zero. +/// +/// Rust: `discrete::primes::nth_prime` +#[pyfunction] +#[pyo3(name = "nth_prime", signature = (n))] +pub fn pyfn_nth_prime(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::primes::nth_prime(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The gaps between consecutive primes up to `n`. +/// +/// Rust: `discrete::primes::prime_gaps` +#[pyfunction] +#[pyo3(name = "prime_gaps", signature = (n))] +pub fn pyfn_prime_gaps<'py>(py: Python<'py>, n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::primes::prime_gaps(n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Twin prime pairs `(p, p+2)` with `p + 2 <= n`. +/// +/// Rust: `discrete::primes::twin_primes` +#[pyfunction] +#[pyo3(name = "twin_primes", signature = (n))] +pub fn pyfn_twin_primes<'py>(py: Python<'py>, n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::primes::twin_primes(n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Every way to write an even `n` as an ordered sum of two primes with +/// `p <= q`. +/// +/// Rust: `discrete::primes::goldbach_partitions` +#[pyfunction] +#[pyo3(name = "goldbach_partitions", signature = (n))] +pub fn pyfn_goldbach_partitions<'py>(py: Python<'py>, n: u64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::primes::goldbach_partitions(n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// The first `count` primes in the arithmetic progression `a, a+d, ...`. +/// +/// Panics: +/// Panics if `d` is zero. +/// +/// Rust: `discrete::primes::primes_in_arithmetic_progression` +#[pyfunction] +#[pyo3(name = "primes_in_arithmetic_progression", signature = (a, d, count))] +pub fn pyfn_primes_in_arithmetic_progression<'py>(py: Python<'py>, a: u64, d: u64, count: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::primes::primes_in_arithmetic_progression(a, d, count))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Lucas-Lehmer test: is the Mersenne number `2^p - 1` prime? +/// +/// `p` must itself be prime for the test to be meaningful; composite `p` +/// gives a composite Mersenne number and the function returns false. +/// +/// Rust: `discrete::primes::mersenne_lucas_lehmer` +#[pyfunction] +#[pyo3(name = "mersenne_lucas_lehmer", signature = (p))] +pub fn pyfn_mersenne_lucas_lehmer(p: u32) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::primes::mersenne_lucas_lehmer(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Wilson's theorem: `p` is prime exactly when `(p-1)! = -1 (mod p)`. +/// +/// Correct but exponentially slower than `is_prime_u64`; included for +/// the identity rather than for use. +/// +/// Rust: `discrete::primes::wilson_check` +#[pyfunction] +#[pyo3(name = "wilson_check", signature = (p))] +pub fn pyfn_wilson_check(p: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::primes::wilson_check(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_sieve_eratosthenes, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sieve_segmented, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sieve_linear, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mod_pow_u64, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_is_prime_u64, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_is_prime_bigint, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_next_prime, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_prev_prime, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_random_prime, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pollard_rho, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pollard_rho_bigint, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pollard_p_minus_1, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_trial_division, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fermat_factor, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_factorize, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_factorize_bigint, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_prime_count_meissel, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_prime_count_li_approx, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_riemann_r, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_nth_prime, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_prime_gaps, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_twin_primes, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_goldbach_partitions, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_primes_in_arithmetic_progression, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mersenne_lucas_lehmer, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wilson_check, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_discrete__sequences.rs b/bindings/python/src/generated/m_discrete__sequences.rs new file mode 100644 index 0000000..45094ce --- /dev/null +++ b/bindings/python/src/generated/m_discrete__sequences.rs @@ -0,0 +1,401 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// The first `n` Taylor coefficients of `f` about the origin, by Cauchy's +/// integral evaluated on a circle of the given radius. +/// +/// `a_k = (1 / 2 pi i) * contour integral of f(z) / z^(k+1)`. Sampling the +/// circle at `N` equally spaced points turns that into a discrete Fourier +/// transform, so all `N` coefficients come out of one FFT rather than `n` +/// separate quadratures. +/// +/// The radius is the accuracy knob and the caller owns it: it must be inside +/// the disc of convergence, and the error in `a_k` scales like +/// `(radius / R)^N` for the true radius of convergence `R`. A radius near `R` +/// resolves high-order coefficients but amplifies the low-order ones by +/// `radius^-k`; a small radius does the reverse. +/// +/// Returns the real parts, so this is for series with real coefficients. +/// +/// Panics: +/// Panics if `n` is zero or `radius` is not positive. +/// +/// Rust: `discrete::sequences::ogf_coefficients` +#[pyfunction] +#[pyo3(name = "ogf_coefficients", signature = (f, n, radius))] +pub fn pyfn_ogf_coefficients(f: pyo3::Py, n: usize, radius: f64) -> PyResult> { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: rust_physics_engine::fractals::Complex| -> rust_physics_engine::fractals::Complex { { let __r = __cb.call::<_, crate::runtime::coerce::ComplexArg>((crate::runtime::coerce::Cx(__a0),), crate::runtime::coerce::ComplexArg(rust_physics_engine::fractals::Complex::new(f64::NAN, f64::NAN))); __r.0 } } }; + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::sequences::ogf_coefficients(&f, n, radius)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Converts exponential generating function coefficients to ordinary ones by +/// multiplying term `k` by `k!`. +/// +/// The factorial overflows `f64` past `k = 170`, so the tail beyond that is +/// infinite rather than silently wrong. +/// +/// Rust: `discrete::sequences::egf_to_ogf` +#[pyfunction] +#[pyo3(name = "egf_to_ogf", signature = (coeffs))] +pub fn pyfn_egf_to_ogf<'py>(py: Python<'py>, coeffs: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::sequences::egf_to_ogf(&coeffs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The `n`-th term of the linear recurrence +/// `a_k = coeffs[0] a_{k-1} + coeffs[1] a_{k-2} + ...`, with `init` giving +/// `a_0 .. a_{order-1}`. +/// +/// Panics: +/// Panics unless `init` and `coeffs` have the same non-zero length. +/// +/// Rust: `discrete::sequences::linear_recurrence` +#[pyfunction] +#[pyo3(name = "linear_recurrence", signature = (init, coeffs, n))] +pub fn pyfn_linear_recurrence<'py>(py: Python<'py>, init: Vec, coeffs: Vec, n: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::sequences::linear_recurrence(&init, &coeffs, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// The `n`-th term of the same recurrence, modulo `m`, by matrix +/// exponentiation. +/// +/// Costs `O(order^3 log n)` rather than `O(order * n)`, which is what makes an +/// index like `10^18` reachable. +/// +/// Panics: +/// Panics unless `init` and `coeffs` have the same non-zero length, or if `m` +/// is zero. +/// +/// Rust: `discrete::sequences::linear_recurrence_mod` +#[pyfunction] +#[pyo3(name = "linear_recurrence_mod", signature = (init, coeffs, n, m))] +pub fn pyfn_linear_recurrence_mod<'py>(py: Python<'py>, init: Vec, coeffs: Vec, n: u64, m: u64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::sequences::linear_recurrence_mod(&init, &coeffs, n, m))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The shortest linear recurrence generating `seq`, by Berlekamp-Massey over +/// the rationals. +/// +/// Returns `c` with `a_n = c[0] a_{n-1} + c[1] a_{n-2} + ...`, or `None` when +/// the sequence is too short to determine one. A recurrence of order `L` is +/// only pinned down by `2L` terms, so a candidate found from fewer is a guess; +/// this reports `None` in that case rather than returning it. The empty vector +/// is returned for the all-zero sequence, whose recurrence has order zero. +/// +/// Rust: `discrete::sequences::find_linear_recurrence` +#[pyfunction] +#[pyo3(name = "find_linear_recurrence", signature = (seq))] +pub fn pyfn_find_linear_recurrence<'py>(py: Python<'py>, seq: Vec) -> PyResult>>> { + let seq = seq.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::sequences::find_linear_recurrence(&seq)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(match __v { Some(__x) => Some(__x.into_iter().map(|__x| -> PyResult> { Ok(crate::runtime::coerce::rational_out(py, &__x)?) }).collect::>>()?), None => None }) +} + +/// The connection polynomial of the shortest linear feedback shift register +/// generating `seq` over GF(2), returned as taps `t` with +/// `a_n = t[0] a_{n-1} XOR t[1] a_{n-2} XOR ...`. +/// +/// Same algorithm as `find_linear_recurrence` with the field replaced by +/// GF(2), where every non-zero discrepancy is one and subtraction is XOR, so +/// there is no division to do. +/// +/// Rust: `discrete::sequences::berlekamp_massey_gf2` +#[pyfunction] +#[pyo3(name = "berlekamp_massey_gf2", signature = (seq))] +pub fn pyfn_berlekamp_massey_gf2<'py>(py: Python<'py>, seq: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::sequences::berlekamp_massey_gf2(&seq))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// `F(n) mod m`, by fast doubling. +/// +/// The identities `F(2k) = F(k) (2 F(k+1) - F(k))` and +/// `F(2k+1) = F(k)^2 + F(k+1)^2` halve the index each step, so this is +/// `O(log n)` multiplications rather than `O(n)` additions. +/// +/// Panics: +/// Panics if `m` is zero. +/// +/// Rust: `discrete::sequences::fibonacci_mod` +#[pyfunction] +#[pyo3(name = "fibonacci_mod", signature = (n, m))] +pub fn pyfn_fibonacci_mod(n: u64, m: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::sequences::fibonacci_mod(n, m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Pisano period: the period of the Fibonacci sequence modulo `m`. +/// +/// Found by advancing until the pair `(0, 1)` recurs, which is the state that +/// starts the sequence, so the first recurrence is the full period. +/// +/// Panics: +/// Panics if `m` is zero. +/// +/// Rust: `discrete::sequences::pisano_period` +#[pyfunction] +#[pyo3(name = "pisano_period", signature = (m))] +pub fn pyfn_pisano_period(m: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::sequences::pisano_period(m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The `n`-th Lucas number: `L(0) = 2`, `L(1) = 1`, `L(n) = L(n-1) + L(n-2)`. +/// +/// Rust: `discrete::sequences::lucas` +#[pyfunction] +#[pyo3(name = "lucas", signature = (n))] +pub fn pyfn_lucas<'py>(py: Python<'py>, n: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::sequences::lucas(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// The `n`-th Pell number: `P(0) = 0`, `P(1) = 1`, `P(n) = 2 P(n-1) + P(n-2)`. +/// +/// Rust: `discrete::sequences::pell_number` +#[pyfunction] +#[pyo3(name = "pell_number", signature = (n))] +pub fn pyfn_pell_number<'py>(py: Python<'py>, n: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::sequences::pell_number(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// The `n`-th Jacobsthal number: `J(0) = 0`, `J(1) = 1`, +/// `J(n) = J(n-1) + 2 J(n-2)`. +/// +/// Rust: `discrete::sequences::jacobsthal` +#[pyfunction] +#[pyo3(name = "jacobsthal", signature = (n))] +pub fn pyfn_jacobsthal<'py>(py: Python<'py>, n: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::sequences::jacobsthal(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// The `n`-th tribonacci number: `0, 0, 1, 1, 2, 4, 7, 13, ...`. +/// +/// Rust: `discrete::sequences::tribonacci` +#[pyfunction] +#[pyo3(name = "tribonacci", signature = (n))] +pub fn pyfn_tribonacci<'py>(py: Python<'py>, n: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::sequences::tribonacci(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// The look-and-say sequence: each step reads the previous term aloud. +/// +/// `"1"` becomes `"11"` (one 1), which becomes `"21"` (two 1s), and so on. +/// +/// Panics: +/// Panics if `seed` is empty or contains a non-digit. +/// +/// Rust: `discrete::sequences::look_and_say` +#[pyfunction] +#[pyo3(name = "look_and_say", signature = (seed, iterations))] +pub fn pyfn_look_and_say(seed: String, iterations: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::sequences::look_and_say(&seed, iterations)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_string()) +} + +/// Conway's constant, estimated from the growth of look-and-say lengths. +/// +/// The true value 1.303577... is the unique real root above one of Conway's +/// degree-71 polynomial. Lengths grow at that rate asymptotically, but the +/// single-step ratio does not settle onto it smoothly: it is still swinging +/// between 1.3137 and 1.3510 at twenty iterations, so reading off one ratio +/// would be worse at twenty steps than at sixteen. The swing has period four, +/// so this takes the geometric mean across a four-step window instead, which +/// cancels most of it and reaches four digits by thirty iterations. +/// +/// Fewer than four iterations are run as four, since the window needs them. +/// +/// Rust: `discrete::sequences::conway_constant_estimate` +#[pyfunction] +#[pyo3(name = "conway_constant_estimate", signature = (iters))] +pub fn pyfn_conway_constant_estimate(iters: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::sequences::conway_constant_estimate(iters)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The `n`-th Thue-Morse bit: the parity of the number of ones in `n`. +/// +/// Rust: `discrete::sequences::thue_morse` +#[pyfunction] +#[pyo3(name = "thue_morse", signature = (n))] +pub fn pyfn_thue_morse(n: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::sequences::thue_morse(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The first `n` bits of the Thue-Morse sequence. +/// +/// Rust: `discrete::sequences::thue_morse_sequence` +#[pyfunction] +#[pyo3(name = "thue_morse_sequence", signature = (n))] +pub fn pyfn_thue_morse_sequence<'py>(py: Python<'py>, n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::sequences::thue_morse_sequence(n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The first `n` terms of the Kolakoski sequence over `{1, 2}`. +/// +/// The sequence is its own run-length encoding: it starts `1, 2, 2, 1, 1, 2`, +/// whose run lengths are `1, 2, 2, 1, 1, 2` again. Generated by reading the +/// sequence back as it is written -- term `k` says how long run `k` is. +/// +/// Rust: `discrete::sequences::kolakoski` +#[pyfunction] +#[pyo3(name = "kolakoski", signature = (n))] +pub fn pyfn_kolakoski<'py>(py: Python<'py>, n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::sequences::kolakoski(n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The first `n` terms of Recaman's sequence. +/// +/// `a(0) = 0`; each step subtracts the index if the result is positive and +/// has not appeared before, and otherwise adds it. +/// +/// Rust: `discrete::sequences::recaman` +#[pyfunction] +#[pyo3(name = "recaman", signature = (n))] +pub fn pyfn_recaman<'py>(py: Python<'py>, n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::sequences::recaman(n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The first `n` terms of the Ulam sequence starting `a, b`. +/// +/// After the seeds, each term is the smallest integer larger than the last +/// that is the sum of two distinct earlier terms in exactly one way. +/// +/// Panics: +/// Panics unless `0 < a < b`. +/// +/// Rust: `discrete::sequences::ulam_sequence` +#[pyfunction] +#[pyo3(name = "ulam_sequence", signature = (a, b, n))] +pub fn pyfn_ulam_sequence<'py>(py: Python<'py>, a: u64, b: u64, n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::sequences::ulam_sequence(a, b, n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The aliquot sequence from `n`: repeatedly replace a number by the sum of +/// its proper divisors. +/// +/// Stops early at zero, which is terminal, and at a repeat, which means the +/// sequence has entered a cycle (a perfect number, an amicable pair, or a +/// longer sociable chain). The returned vector includes `n` itself and the +/// repeated value, so a cycle is visible in the output. +/// +/// Rust: `discrete::sequences::aliquot_sequence` +#[pyfunction] +#[pyo3(name = "aliquot_sequence", signature = (n, max_steps))] +pub fn pyfn_aliquot_sequence<'py>(py: Python<'py>, n: u64, max_steps: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::sequences::aliquot_sequence(n, max_steps))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Ackermann function, for arguments whose value is representable. +/// +/// `A(m, n)` is computed by the closed forms rather than the recursion, which +/// would not terminate in practice: `A(0,n) = n+1`, `A(1,n) = n+2`, +/// `A(2,n) = 2n+3`, `A(3,n) = 2^(n+3) - 3`, and `A(4,n)` is a tower of twos. +/// Returns `None` when the value cannot be built -- `A(4, 2)` already has +/// 19729 digits and `A(5, 0) = A(4, 1)` is the largest value below it that +/// this returns. +/// +/// Rust: `discrete::sequences::ackermann_small` +#[pyfunction] +#[pyo3(name = "ackermann_small", signature = (m, n))] +pub fn pyfn_ackermann_small<'py>(py: Python<'py>, m: u64, n: u64) -> PyResult>> { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::sequences::ackermann_small(m, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(match __v { Some(__x) => Some(crate::runtime::coerce::bigint_out(py, &__x)?), None => None }) +} + +/// Names of the known sequences whose opening terms match `terms`. +/// +/// Every candidate family is generated and compared term by term, so a name is +/// returned only on an exact match of the whole input. A linear recurrence +/// found by `find_linear_recurrence` is reported as well, which covers the +/// families not listed by name. +/// +/// The result is a list because short prefixes are genuinely ambiguous: +/// `1, 1, 2` opens the Fibonacci numbers, the Catalan numbers, and the +/// partition counts alike. +/// +/// Rust: `discrete::sequences::sequence_identify` +#[pyfunction] +#[pyo3(name = "sequence_identify", signature = (terms))] +pub fn pyfn_sequence_identify<'py>(py: Python<'py>, terms: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::discrete::sequences::sequence_identify(&terms))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.to_string()).collect::>()) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_ogf_coefficients, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_egf_to_ogf, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_linear_recurrence, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_linear_recurrence_mod, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_find_linear_recurrence, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_berlekamp_massey_gf2, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fibonacci_mod, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pisano_period, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lucas, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pell_number, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_jacobsthal, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tribonacci, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_look_and_say, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_conway_constant_estimate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_thue_morse, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_thue_morse_sequence, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kolakoski, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_recaman, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ulam_sequence, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_aliquot_sequence, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ackermann_small, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sequence_identify, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_dsp.rs b/bindings/python/src/generated/m_dsp.rs new file mode 100644 index 0000000..c02bbf8 --- /dev/null +++ b/bindings/python/src/generated/m_dsp.rs @@ -0,0 +1,22 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + Ok(()) +} diff --git a/bindings/python/src/generated/m_dsp__fir.rs b/bindings/python/src/generated/m_dsp__fir.rs new file mode 100644 index 0000000..6086948 --- /dev/null +++ b/bindings/python/src/generated/m_dsp__fir.rs @@ -0,0 +1,316 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Windowed-sinc low-pass FIR; unit DC gain. `cutoff` in (0, 0.5). +/// +/// Panics: +/// Panics if `n_taps == 0` or the cutoff is out of range. +/// +/// Rust: `dsp::fir::fir_lowpass` +#[pyfunction] +#[pyo3(name = "fir_lowpass", signature = (n_taps, cutoff, w))] +pub fn pyfn_fir_lowpass<'py>(py: Python<'py>, n_taps: usize, cutoff: f64, w: crate::generated::types::PyWindowKind) -> PyResult> { + let w = w.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::fir::fir_lowpass(n_taps, cutoff, w))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Windowed-sinc high-pass FIR via spectral inversion; unit Nyquist gain. +/// +/// Panics: +/// Panics unless `n_taps` is odd (type I linear phase is required for a +/// high-pass) and the cutoff is in range. +/// +/// Rust: `dsp::fir::fir_highpass` +#[pyfunction] +#[pyo3(name = "fir_highpass", signature = (n_taps, cutoff, w))] +pub fn pyfn_fir_highpass<'py>(py: Python<'py>, n_taps: usize, cutoff: f64, w: crate::generated::types::PyWindowKind) -> PyResult> { + let w = w.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::fir::fir_highpass(n_taps, cutoff, w))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Windowed-sinc band-pass FIR (difference of two low-passes); unit gain +/// at the band center (lo + hi)/2. +/// +/// Panics: +/// Panics unless `0 < lo < hi < 0.5`. +/// +/// Rust: `dsp::fir::fir_bandpass` +#[pyfunction] +#[pyo3(name = "fir_bandpass", signature = (n_taps, lo, hi, w))] +pub fn pyfn_fir_bandpass<'py>(py: Python<'py>, n_taps: usize, lo: f64, hi: f64, w: crate::generated::types::PyWindowKind) -> PyResult> { + let w = w.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::fir::fir_bandpass(n_taps, lo, hi, w))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Windowed-sinc band-stop FIR; unit DC gain. +/// +/// Panics: +/// Panics unless `n_taps` is odd and `0 < lo < hi < 0.5`. +/// +/// Rust: `dsp::fir::fir_bandstop` +#[pyfunction] +#[pyo3(name = "fir_bandstop", signature = (n_taps, lo, hi, w))] +pub fn pyfn_fir_bandstop<'py>(py: Python<'py>, n_taps: usize, lo: f64, hi: f64, w: crate::generated::types::PyWindowKind) -> PyResult> { + let w = w.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::fir::fir_bandstop(n_taps, lo, hi, w))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Kaiser-window low-pass design from a passband/stopband spec: +/// passband edge, stopband edge (normalized), maximum passband ripple +/// and minimum stopband attenuation in dB. Chooses the tap count and β +/// by Kaiser's formulas. +/// +/// Panics: +/// Panics unless `0 < pass < stop < 0.5`. +/// +/// Rust: `dsp::fir::fir_kaiser_design` +#[pyfunction] +#[pyo3(name = "fir_kaiser_design", signature = (pass_, stop, ripple_db, atten_db))] +pub fn pyfn_fir_kaiser_design<'py>(py: Python<'py>, pass_: f64, stop: f64, ripple_db: f64, atten_db: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::fir::fir_kaiser_design(pass_, stop, ripple_db, atten_db))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Equiripple (Parks-McClellan / Remez exchange) linear-phase type I +/// design. `bands` are disjoint ascending (lo, hi) pairs in [0, 0.5]; +/// `desired` and `weights` give one amplitude and weight per band. +/// +/// Errors: +/// Returns `SolveError::InvalidArgument` for a malformed spec and +/// `SolveError::NoConvergence` if the exchange fails to settle. +/// +/// Panics: +/// Panics unless `n_taps` is odd and ≥ 3. +/// +/// Rust: `dsp::fir::fir_parks_mcclellan` +#[pyfunction] +#[pyo3(name = "fir_parks_mcclellan", signature = (n_taps, bands, desired, weights))] +pub fn pyfn_fir_parks_mcclellan<'py>(py: Python<'py>, n_taps: usize, bands: Vec<(f64, f64)>, desired: Vec, weights: Vec) -> PyResult> { + let bands = bands.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::fir::fir_parks_mcclellan(n_taps, &bands, &desired, &weights))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// Least-squares linear-phase type I design over the given bands +/// (transition regions are "don't care"). +/// +/// Panics: +/// Panics unless `n_taps` is odd and the spec lengths match. +/// +/// Rust: `dsp::fir::fir_least_squares` +#[pyfunction] +#[pyo3(name = "fir_least_squares", signature = (n_taps, bands, desired))] +pub fn pyfn_fir_least_squares<'py>(py: Python<'py>, n_taps: usize, bands: Vec<(f64, f64)>, desired: Vec) -> PyResult> { + let bands = bands.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::fir::fir_least_squares(n_taps, &bands, &desired))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Windowed ideal differentiator (antisymmetric, Blackman window). The +/// output of `fir_apply` approximates dx/dn (per-sample derivative) +/// delayed by (n_taps−1)/2. +/// +/// Panics: +/// Panics unless `n_taps` is odd. +/// +/// Rust: `dsp::fir::fir_differentiator` +#[pyfunction] +#[pyo3(name = "fir_differentiator", signature = (n_taps))] +pub fn pyfn_fir_differentiator<'py>(py: Python<'py>, n_taps: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::fir::fir_differentiator(n_taps))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Windowed ideal Hilbert transformer (antisymmetric, Blackman window): +/// shifts every positive-frequency component by −90°. +/// +/// Panics: +/// Panics unless `n_taps` is odd. +/// +/// Rust: `dsp::fir::fir_hilbert` +#[pyfunction] +#[pyo3(name = "fir_hilbert", signature = (n_taps))] +pub fn pyfn_fir_hilbert<'py>(py: Python<'py>, n_taps: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::fir::fir_hilbert(n_taps))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Raised-cosine (Nyquist) pulse: `span` symbols long at `sps` samples +/// per symbol with roll-off `beta` ∈ [0, 1]. Length span·sps + 1, peak 1, +/// zero ISI at symbol spacing. +/// +/// Panics: +/// Panics if `span` or `sps` is zero, or beta is outside [0, 1]. +/// +/// Rust: `dsp::fir::fir_raised_cosine` +#[pyfunction] +#[pyo3(name = "fir_raised_cosine", signature = (span, sps, beta))] +pub fn pyfn_fir_raised_cosine<'py>(py: Python<'py>, span: usize, sps: usize, beta: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::fir::fir_raised_cosine(span, sps, beta))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Root-raised-cosine pulse (same span/sps/beta conventions as +/// `fir_raised_cosine`); convolving it with itself gives a raised +/// cosine. Normalized to unit energy. +/// +/// Panics: +/// Panics if `span` or `sps` is zero, or beta is outside [0, 1]. +/// +/// Rust: `dsp::fir::fir_root_raised_cosine` +#[pyfunction] +#[pyo3(name = "fir_root_raised_cosine", signature = (span, sps, beta))] +pub fn pyfn_fir_root_raised_cosine<'py>(py: Python<'py>, span: usize, sps: usize, beta: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::fir::fir_root_raised_cosine(span, sps, beta))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Gaussian pulse-shaping filter with bandwidth-time product `bt` +/// (bandwidth normalized to the sample rate). Unit DC gain. +/// +/// Panics: +/// Panics if `n_taps == 0` or `bt <= 0`. +/// +/// Rust: `dsp::fir::fir_gaussian` +#[pyfunction] +#[pyo3(name = "fir_gaussian", signature = (n_taps, bt))] +pub fn pyfn_fir_gaussian<'py>(py: Python<'py>, n_taps: usize, bt: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::fir::fir_gaussian(n_taps, bt))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Savitzky-Golay convolution kernel: fits a polynomial of `order` over +/// a centered odd `window` and evaluates its `deriv`-th derivative (unit +/// sample spacing). Feeding it to `fir_apply` estimates the derivative +/// delayed by (window−1)/2 samples. +/// +/// Panics: +/// Panics unless `window` is odd and `deriv <= order < window`. +/// +/// Rust: `dsp::fir::fir_savitzky_golay` +#[pyfunction] +#[pyo3(name = "fir_savitzky_golay", signature = (window, order, deriv))] +pub fn pyfn_fir_savitzky_golay<'py>(py: Python<'py>, window: usize, order: usize, deriv: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::fir::fir_savitzky_golay(window, order, deriv))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Causal FIR filtering by direct convolution; output has the same +/// length as the input (group delay is not compensated). +/// +/// Rust: `dsp::fir::fir_apply` +#[pyfunction] +#[pyo3(name = "fir_apply", signature = (h, x))] +pub fn pyfn_fir_apply<'py>(py: Python<'py>, h: Vec, x: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::fir::fir_apply(&h, &x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Causal FIR filtering via overlap-save FFT blocks; identical output to +/// `fir_apply` but O(n log n) for long kernels. +/// +/// Rust: `dsp::fir::fir_apply_fft` +#[pyfunction] +#[pyo3(name = "fir_apply_fft", signature = (h, x))] +pub fn pyfn_fir_apply_fft<'py>(py: Python<'py>, h: Vec, x: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::fir::fir_apply_fft(&h, &x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Zero-phase filtering: filter forward, reverse, filter again, reverse. +/// The effective magnitude response is |H|². +/// +/// Rust: `dsp::fir::filtfilt_fir` +#[pyfunction] +#[pyo3(name = "filtfilt_fir", signature = (h, x))] +pub fn pyfn_filtfilt_fir<'py>(py: Python<'py>, h: Vec, x: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::fir::filtfilt_fir(&h, &x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Frequency response of an FIR at n points spanning [0, 0.5] (normalized +/// frequency); returns (frequencies, complex response). +/// +/// Panics: +/// Panics if `n < 2`. +/// +/// Rust: `dsp::fir::fir_freq_response` +#[pyfunction] +#[pyo3(name = "fir_freq_response", signature = (h, n))] +pub fn pyfn_fir_freq_response<'py>(py: Python<'py>, h: Vec, n: usize) -> PyResult<(Vec, Vec>)> { + let __r = crate::runtime::guard(|| rust_physics_engine::dsp::fir::fir_freq_response(&h, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>())) +} + +/// Group delay in samples: (n−1)/2 for (anti)symmetric linear-phase +/// kernels, otherwise the energy-weighted center of the impulse response. +/// +/// Rust: `dsp::fir::fir_group_delay` +#[pyfunction] +#[pyo3(name = "fir_group_delay", signature = (h))] +pub fn pyfn_fir_group_delay<'py>(py: Python<'py>, h: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::fir::fir_group_delay(&h))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_fir_lowpass, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fir_highpass, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fir_bandpass, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fir_bandstop, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fir_kaiser_design, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fir_parks_mcclellan, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fir_least_squares, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fir_differentiator, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fir_hilbert, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fir_raised_cosine, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fir_root_raised_cosine, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fir_gaussian, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fir_savitzky_golay, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fir_apply, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fir_apply_fft, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_filtfilt_fir, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fir_freq_response, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fir_group_delay, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_dsp__iir.rs b/bindings/python/src/generated/m_dsp__iir.rs new file mode 100644 index 0000000..32af208 --- /dev/null +++ b/bindings/python/src/generated/m_dsp__iir.rs @@ -0,0 +1,352 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Bilinear transform of an analog (z, p, k) description to a digital +/// `Sos` at sample rate fs. `prewarp` optionally pins one analog +/// frequency (Hz) to its digital location. +/// +/// Rust: `dsp::iir::bilinear_transform` +#[pyfunction] +#[pyo3(name = "bilinear_transform", signature = (s_zeros, s_poles, gain, fs, prewarp_hz=None))] +pub fn pyfn_bilinear_transform(s_zeros: Vec, s_poles: Vec, gain: f64, fs: f64, prewarp_hz: Option) -> PyResult { + let s_zeros = s_zeros.into_iter().map(|__e| __e.0).collect::>(); + let s_poles = s_poles.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::dsp::iir::bilinear_transform(&s_zeros, &s_poles, gain, fs, prewarp_hz)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySos { inner: __v }) +} + +/// Group a digital (z, p, k) set into second-order sections. Complex +/// values must come in conjugate pairs. +/// +/// Rust: `dsp::iir::zpk_to_sos` +#[pyfunction] +#[pyo3(name = "zpk_to_sos", signature = (zeros, poles, gain))] +pub fn pyfn_zpk_to_sos(zeros: Vec, poles: Vec, gain: f64) -> PyResult { + let zeros = zeros.into_iter().map(|__e| __e.0).collect::>(); + let poles = poles.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::dsp::iir::zpk_to_sos(&zeros, &poles, gain)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySos { inner: __v }) +} + +/// Digital (zeros, poles, gain) from transfer-function coefficient +/// arrays in z⁻¹ order (b\[0\] + b\[1\]z⁻¹ + …), using +/// `numerical::polynomial_roots`. +/// +/// Panics: +/// Panics if either polynomial is degenerate (all zero). +/// +/// Rust: `dsp::iir::tf_to_zpk` +#[pyfunction] +#[pyo3(name = "tf_to_zpk", signature = (b, a))] +pub fn pyfn_tf_to_zpk<'py>(py: Python<'py>, b: Vec, a: Vec) -> PyResult<(Vec>, Vec>, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::dsp::iir::tf_to_zpk(&b, &a)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>(), __v.1.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>(), __v.2)) +} + +/// Butterworth digital filter (maximally flat magnitude). +/// +/// Panics: +/// Panics if `order == 0` or the band edges are invalid for fs. +/// +/// Rust: `dsp::iir::butterworth` +#[pyfunction] +#[pyo3(name = "butterworth", signature = (order, kind, fs))] +pub fn pyfn_butterworth(order: usize, kind: crate::generated::types::PyIirKind, fs: f64) -> PyResult { + let kind = kind.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::dsp::iir::butterworth(order, kind, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySos { inner: __v }) +} + +/// Chebyshev type I (equiripple passband, `ripple_db` peak-to-peak). +/// +/// Panics: +/// Panics if `order == 0`. +/// +/// Rust: `dsp::iir::chebyshev1` +#[pyfunction] +#[pyo3(name = "chebyshev1", signature = (order, ripple_db, kind, fs))] +pub fn pyfn_chebyshev1(order: usize, ripple_db: f64, kind: crate::generated::types::PyIirKind, fs: f64) -> PyResult { + let kind = kind.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::dsp::iir::chebyshev1(order, ripple_db, kind, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySos { inner: __v }) +} + +/// Chebyshev type II (monotone passband, equiripple stopband at +/// −`atten_db`). The cutoff marks the stopband edge. +/// +/// Panics: +/// Panics if `order == 0`. +/// +/// Rust: `dsp::iir::chebyshev2` +#[pyfunction] +#[pyo3(name = "chebyshev2", signature = (order, atten_db, kind, fs))] +pub fn pyfn_chebyshev2(order: usize, atten_db: f64, kind: crate::generated::types::PyIirKind, fs: f64) -> PyResult { + let kind = kind.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::dsp::iir::chebyshev2(order, atten_db, kind, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySos { inner: __v }) +} + +/// Elliptic (Cauer) filter: `ripple_db` passband ripple and `atten_db` +/// stopband attenuation. +/// +/// Panics: +/// Panics if `order == 0`. +/// +/// Rust: `dsp::iir::elliptic` +#[pyfunction] +#[pyo3(name = "elliptic", signature = (order, ripple_db, atten_db, kind, fs))] +pub fn pyfn_elliptic(order: usize, ripple_db: f64, atten_db: f64, kind: crate::generated::types::PyIirKind, fs: f64) -> PyResult { + let kind = kind.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::dsp::iir::elliptic(order, ripple_db, atten_db, kind, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySos { inner: __v }) +} + +/// Bessel-Thomson filter (maximally flat group delay), −3 dB at the +/// cutoff. +/// +/// Panics: +/// Panics if `order == 0`. +/// +/// Rust: `dsp::iir::bessel` +#[pyfunction] +#[pyo3(name = "bessel", signature = (order, kind, fs))] +pub fn pyfn_bessel(order: usize, kind: crate::generated::types::PyIirKind, fs: f64) -> PyResult { + let kind = kind.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::dsp::iir::bessel(order, kind, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySos { inner: __v }) +} + +/// Minimum Butterworth order meeting a low-pass spec: passband edge, +/// stopband edge (Hz), maximum passband ripple and minimum stopband +/// attenuation (dB). +/// +/// Panics: +/// Panics unless `0 < pass < stop < fs/2`. +/// +/// Rust: `dsp::iir::butterworth_order` +#[pyfunction] +#[pyo3(name = "butterworth_order", signature = (pass_, stop, ripple_db, atten_db, fs))] +pub fn pyfn_butterworth_order(pass_: f64, stop: f64, ripple_db: f64, atten_db: f64, fs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::dsp::iir::butterworth_order(pass_, stop, ripple_db, atten_db, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Zero-phase filtering: odd-reflection padding, steady-state priming +/// of every section, forward pass, backward pass (the same edge- +/// transient suppression goal as Gustafsson's method). +/// +/// Rust: `dsp::iir::filtfilt` +#[pyfunction] +#[pyo3(name = "filtfilt", signature = (sos, x))] +pub fn pyfn_filtfilt<'py>(py: Python<'py>, sos: crate::generated::types::PySos, x: Vec) -> PyResult> { + let sos = sos.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::iir::filtfilt(&sos, &x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Direct-form II transposed filtering with arbitrary-order (b, a) +/// coefficient arrays in z⁻¹ order. +/// +/// Panics: +/// Panics if `a` is empty or `a[0] == 0`. +/// +/// Rust: `dsp::iir::iir_apply` +#[pyfunction] +#[pyo3(name = "iir_apply", signature = (b, a, x))] +pub fn pyfn_iir_apply<'py>(py: Python<'py>, b: Vec, a: Vec, x: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::iir::iir_apply(&b, &a, &x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Impulse response of a cascade (n samples). +/// +/// Rust: `dsp::iir::impulse_response` +#[pyfunction] +#[pyo3(name = "impulse_response", signature = (sos, n))] +pub fn pyfn_impulse_response<'py>(py: Python<'py>, sos: crate::generated::types::PySos, n: usize) -> PyResult> { + let sos = sos.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::iir::impulse_response(&sos, n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Step response of a cascade (n samples). +/// +/// Rust: `dsp::iir::step_response` +#[pyfunction] +#[pyo3(name = "step_response", signature = (sos, n))] +pub fn pyfn_step_response<'py>(py: Python<'py>, sos: crate::generated::types::PySos, n: usize) -> PyResult> { + let sos = sos.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::iir::step_response(&sos, n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Group delay in samples over `n_points` frequencies spanning +/// (0, fs/2): τ(ω) = −dφ/dω from the unwrapped phase. +/// +/// Rust: `dsp::iir::group_delay` +#[pyfunction] +#[pyo3(name = "group_delay", signature = (sos, n_points, fs))] +pub fn pyfn_group_delay(sos: crate::generated::types::PySos, n_points: usize, fs: f64) -> PyResult<(Vec, Vec)> { + let sos = sos.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::dsp::iir::group_delay(&sos, n_points, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// One-pole low-pass coefficients (b0, a1) for +/// y\[n\] = b0·x\[n\] + a1·y\[n−1\], with a1 = e^(−2π·fc/fs). +/// The pre-Part-3 `first_order_lowpass` is this filter with +/// α = dt/(RC + dt). +/// +/// Rust: `dsp::iir::one_pole_lowpass` +#[pyfunction] +#[pyo3(name = "one_pole_lowpass", signature = (fc, fs))] +pub fn pyfn_one_pole_lowpass(fc: f64, fs: f64) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::dsp::iir::one_pole_lowpass(fc, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// DC-blocking filter: H(z) = (1 − z⁻¹)/(1 − r·z⁻¹), r slightly below 1. +/// +/// Rust: `dsp::iir::dc_blocker` +#[pyfunction] +#[pyo3(name = "dc_blocker", signature = (r))] +pub fn pyfn_dc_blocker(r: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::dsp::iir::dc_blocker(r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBiquad { inner: __v }) +} + +/// Build a Chamberlin SVF at cutoff fc with resonance q. +/// +/// Rust: `dsp::iir::state_variable_filter` +#[pyfunction] +#[pyo3(name = "state_variable_filter", signature = (fc, fs, q))] +pub fn pyfn_state_variable_filter(fc: f64, fs: f64, q: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::dsp::iir::state_variable_filter(fc, fs, q)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySvf { inner: __v }) +} + +/// IEC 61672 A-weighting as a digital cascade (bilinear transform of the +/// standard analog poles), normalized to exactly 0 dB at 1 kHz. +/// +/// Rust: `dsp::iir::a_weighting_filter` +#[pyfunction] +#[pyo3(name = "a_weighting_filter", signature = (fs))] +pub fn pyfn_a_weighting_filter(fs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::dsp::iir::a_weighting_filter(fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySos { inner: __v }) +} + +/// IEC 61672 C-weighting, normalized to 0 dB at 1 kHz. +/// +/// Rust: `dsp::iir::c_weighting_filter` +#[pyfunction] +#[pyo3(name = "c_weighting_filter", signature = (fs))] +pub fn pyfn_c_weighting_filter(fs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::dsp::iir::c_weighting_filter(fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySos { inner: __v }) +} + +/// RBJ Q for a given bandwidth in octaves at center fc: +/// 1/Q = 2·sinh(ln2/2 · BW · ω/sin ω). +/// +/// Rust: `dsp::iir::rbj_q_from_bandwidth` +#[pyfunction] +#[pyo3(name = "rbj_q_from_bandwidth", signature = (bw_octaves, fc, fs))] +pub fn pyfn_rbj_q_from_bandwidth(bw_octaves: f64, fc: f64, fs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::dsp::iir::rbj_q_from_bandwidth(bw_octaves, fc, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// First-order RC low-pass filter: α = dt / (RC + dt) +/// +/// Panics: +/// Panics if `dt <= 0` or `rc < 0`. +/// +/// Rust: `dsp::iir::first_order_lowpass` +#[pyfunction] +#[pyo3(name = "first_order_lowpass", signature = (signal, dt, rc))] +pub fn pyfn_first_order_lowpass<'py>(py: Python<'py>, signal: Vec, dt: f64, rc: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::iir::first_order_lowpass(&signal, dt, rc))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// First-order RC high-pass filter: α = RC / (RC + dt) +/// +/// Panics: +/// Panics if `dt <= 0` or `rc < 0`. +/// +/// Rust: `dsp::iir::first_order_highpass` +#[pyfunction] +#[pyo3(name = "first_order_highpass", signature = (signal, dt, rc))] +pub fn pyfn_first_order_highpass<'py>(py: Python<'py>, signal: Vec, dt: f64, rc: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::iir::first_order_highpass(&signal, dt, rc))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_bilinear_transform, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_zpk_to_sos, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tf_to_zpk, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_butterworth, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_chebyshev1, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_chebyshev2, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_elliptic, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bessel, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_butterworth_order, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_filtfilt, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_iir_apply, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_impulse_response, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_step_response, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_group_delay, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_one_pole_lowpass, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dc_blocker, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_state_variable_filter, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_a_weighting_filter, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_c_weighting_filter, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rbj_q_from_bandwidth, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_first_order_lowpass, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_first_order_highpass, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_dsp__phase.rs b/bindings/python/src/generated/m_dsp__phase.rs new file mode 100644 index 0000000..28fbb90 --- /dev/null +++ b/bindings/python/src/generated/m_dsp__phase.rs @@ -0,0 +1,136 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Wrap an angle into (−π, π]. +/// +/// Rust: `dsp::phase::wrap_phase` +#[pyfunction] +#[pyo3(name = "wrap_phase", signature = (p))] +pub fn pyfn_wrap_phase(p: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::dsp::phase::wrap_phase(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// 1D phase unwrapping: remove 2π jumps between consecutive samples. +/// +/// Rust: `dsp::phase::unwrap_phase` +#[pyfunction] +#[pyo3(name = "unwrap_phase", signature = (p))] +pub fn pyfn_unwrap_phase<'py>(py: Python<'py>, p: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::phase::unwrap_phase(&p))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// 2D phase unwrapping by Itoh's method: unwrap each row, then unwrap +/// the columns of the row-unwrapped field. Exact for residue-free +/// (consistent) phase maps. +/// +/// Panics: +/// Panics unless `p.len() == w * h`. +/// +/// Rust: `dsp::phase::unwrap_phase_2d` +#[pyfunction] +#[pyo3(name = "unwrap_phase_2d", signature = (p, w, h))] +pub fn pyfn_unwrap_phase_2d<'py>(py: Python<'py>, p: Vec, w: usize, h: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::phase::unwrap_phase_2d(&p, w, h))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Wrapped per-sample phase difference a − b. +/// +/// Panics: +/// Panics if the lengths differ. +/// +/// Rust: `dsp::phase::phase_difference` +#[pyfunction] +#[pyo3(name = "phase_difference", signature = (a, b))] +pub fn pyfn_phase_difference<'py>(py: Python<'py>, a: Vec, b: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::phase::phase_difference(&a, &b))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Group delay −dφ/dω from unwrapped phase samples on an angular +/// frequency grid (central differences; one-sided at the ends). +/// +/// Panics: +/// Panics if the lengths differ or fewer than 2 points. +/// +/// Rust: `dsp::phase::group_delay_from_phase` +#[pyfunction] +#[pyo3(name = "group_delay_from_phase", signature = (phase, freqs))] +pub fn pyfn_group_delay_from_phase<'py>(py: Python<'py>, phase: Vec, freqs: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::phase::group_delay_from_phase(&phase, &freqs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Second-order phase-locked loop tracking a real tone near f0: +/// returns the NCO phase track and the instantaneous frequency estimate +/// (Hz) per sample. `bandwidth` is the loop bandwidth in Hz. +/// +/// Panics: +/// Panics unless the rates are positive. +/// +/// Rust: `dsp::phase::phase_locked_loop` +#[pyfunction] +#[pyo3(name = "phase_locked_loop", signature = (x, fs, f0, bandwidth))] +pub fn pyfn_phase_locked_loop<'py>(py: Python<'py>, x: Vec, fs: f64, f0: f64, bandwidth: f64) -> PyResult<(Vec, Vec)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::phase::phase_locked_loop(&x, fs, f0, bandwidth))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Linearly interpolated zero-crossing times (seconds), both directions. +/// +/// Rust: `dsp::phase::zero_crossing_times` +#[pyfunction] +#[pyo3(name = "zero_crossing_times", signature = (x, fs))] +pub fn pyfn_zero_crossing_times<'py>(py: Python<'py>, x: Vec, fs: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::phase::zero_crossing_times(&x, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Phase (radians) of the signal's component at `ref_freq` relative to +/// cos(2π·f·t) starting at the first sample, via single-bin correlation. +/// +/// Rust: `dsp::phase::phase_vs_reference` +#[pyfunction] +#[pyo3(name = "phase_vs_reference", signature = (x, ref_freq, fs))] +pub fn pyfn_phase_vs_reference<'py>(py: Python<'py>, x: Vec, ref_freq: f64, fs: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::phase::phase_vs_reference(&x, ref_freq, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_wrap_phase, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_unwrap_phase, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_unwrap_phase_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_phase_difference, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_group_delay_from_phase, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_phase_locked_loop, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_zero_crossing_times, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_phase_vs_reference, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_dsp__resample.rs b/bindings/python/src/generated/m_dsp__resample.rs new file mode 100644 index 0000000..d4f9a74 --- /dev/null +++ b/bindings/python/src/generated/m_dsp__resample.rs @@ -0,0 +1,180 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Integer upsampling: zero-stuff by `factor`, then interpolate with a +/// Kaiser-windowed sinc low-pass at the original Nyquist. Output length +/// is `x.len() * factor`. +/// +/// Panics: +/// Panics if `factor == 0`. +/// +/// Rust: `dsp::resample::upsample` +#[pyfunction] +#[pyo3(name = "upsample", signature = (x, factor))] +pub fn pyfn_upsample<'py>(py: Python<'py>, x: Vec, factor: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::resample::upsample(&x, factor))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Integer decimation: anti-alias low-pass at the new Nyquist, then +/// keep every `factor`-th sample. Output length ⌈n/factor⌉. +/// +/// Panics: +/// Panics if `factor == 0`. +/// +/// Rust: `dsp::resample::decimate` +#[pyfunction] +#[pyo3(name = "decimate", signature = (x, factor))] +pub fn pyfn_decimate<'py>(py: Python<'py>, x: Vec, factor: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::resample::decimate(&x, factor))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Rational resampling by up/down with a single polyphase Kaiser-sinc +/// kernel. Output length ⌈n·up/down⌉. +/// +/// Panics: +/// Panics if `up == 0` or `down == 0`. +/// +/// Rust: `dsp::resample::resample_rational` +#[pyfunction] +#[pyo3(name = "resample_rational", signature = (x, up, down))] +pub fn pyfn_resample_rational<'py>(py: Python<'py>, x: Vec, up: usize, down: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::resample::resample_rational(&x, up, down))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Resample from `fs_in` to `fs_out`, approximating the ratio with a +/// rational up/down (denominator ≤ 1000) and delegating to +/// `resample_rational`. +/// +/// Panics: +/// Panics unless both rates are positive. +/// +/// Rust: `dsp::resample::resample_to_rate` +#[pyfunction] +#[pyo3(name = "resample_to_rate", signature = (x, fs_in, fs_out))] +pub fn pyfn_resample_to_rate<'py>(py: Python<'py>, x: Vec, fs_in: f64, fs_out: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::resample::resample_to_rate(&x, fs_in, fs_out))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Windowed-sinc (Hann) interpolation of the sample stream at fractional +/// index t (samples), using `half_width` taps on each side. +/// +/// Rust: `dsp::resample::sinc_interpolate` +#[pyfunction] +#[pyo3(name = "sinc_interpolate", signature = (x, t, half_width))] +pub fn pyfn_sinc_interpolate<'py>(py: Python<'py>, x: Vec, t: f64, half_width: usize) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::resample::sinc_interpolate(&x, t, half_width))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Arbitrary-ratio resampling by windowed-sinc interpolation; output +/// length ⌈n·ratio⌉. +/// +/// Panics: +/// Panics if `ratio <= 0`. +/// +/// Rust: `dsp::resample::resample_sinc` +#[pyfunction] +#[pyo3(name = "resample_sinc", signature = (x, ratio, half_width))] +pub fn pyfn_resample_sinc<'py>(py: Python<'py>, x: Vec, ratio: f64, half_width: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::resample::resample_sinc(&x, ratio, half_width))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Linear-interpolation resampling (cheap, −12 dB/oct images). +/// +/// Panics: +/// Panics if `ratio <= 0`. +/// +/// Rust: `dsp::resample::resample_linear` +#[pyfunction] +#[pyo3(name = "resample_linear", signature = (x, ratio))] +pub fn pyfn_resample_linear<'py>(py: Python<'py>, x: Vec, ratio: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::resample::resample_linear(&x, ratio))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Catmull-Rom cubic resampling. +/// +/// Panics: +/// Panics if `ratio <= 0`. +/// +/// Rust: `dsp::resample::resample_cubic` +#[pyfunction] +#[pyo3(name = "resample_cubic", signature = (x, ratio))] +pub fn pyfn_resample_cubic<'py>(py: Python<'py>, x: Vec, ratio: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::resample::resample_cubic(&x, ratio))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Cascaded integrator-comb decimation: `stages` integrators, decimate +/// by `factor`, `stages` combs; output scaled by factor^stages so DC +/// gain is one. +/// +/// Panics: +/// Panics if `factor == 0` or `stages == 0`. +/// +/// Rust: `dsp::resample::cic_decimate` +#[pyfunction] +#[pyo3(name = "cic_decimate", signature = (x, factor, stages))] +pub fn pyfn_cic_decimate<'py>(py: Python<'py>, x: Vec, factor: usize, stages: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::resample::cic_decimate(&x, factor, stages))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Half-band FIR: odd length, every second tap zero (except the 0.5 +/// center), cutoff 0.25 — the workhorse for factor-2 stages. +/// +/// Panics: +/// Panics unless `n_taps` is odd and ≥ 7. +/// +/// Rust: `dsp::resample::half_band_filter` +#[pyfunction] +#[pyo3(name = "half_band_filter", signature = (n_taps))] +pub fn pyfn_half_band_filter<'py>(py: Python<'py>, n_taps: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::resample::half_band_filter(n_taps))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_upsample, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_decimate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_resample_rational, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_resample_to_rate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sinc_interpolate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_resample_sinc, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_resample_linear, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_resample_cubic, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cic_decimate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_half_band_filter, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_dsp__windows.rs b/bindings/python/src/generated/m_dsp__windows.rs new file mode 100644 index 0000000..a39549a --- /dev/null +++ b/bindings/python/src/generated/m_dsp__windows.rs @@ -0,0 +1,114 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Generate a window of length n. `periodic` selects the DFT-even form +/// (denominator n, for spectral analysis); symmetric windows use +/// denominator n−1 (for FIR design). +/// +/// Rust: `dsp::windows::window` +#[pyfunction] +#[pyo3(name = "window", signature = (kind, n, periodic))] +pub fn pyfn_window<'py>(py: Python<'py>, kind: crate::generated::types::PyWindowKind, n: usize, periodic: bool) -> PyResult> { + let kind = kind.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::windows::window(kind, n, periodic))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Measure a window's figures of merit by direct evaluation of its DTFT +/// on a fine frequency grid (64 points per bin). +/// +/// Rust: `dsp::windows::window_metrics` +#[pyfunction] +#[pyo3(name = "window_metrics", signature = (w))] +pub fn pyfn_window_metrics(w: Vec) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::dsp::windows::window_metrics(&w)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyWindowMetrics { inner: __v }) +} + +/// Kaiser window β for a target stopband attenuation in dB +/// (Kaiser's empirical formula). +/// +/// Rust: `dsp::windows::kaiser_beta_for_attenuation` +#[pyfunction] +#[pyo3(name = "kaiser_beta_for_attenuation", signature = (db))] +pub fn pyfn_kaiser_beta_for_attenuation(db: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::dsp::windows::kaiser_beta_for_attenuation(db)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Generate a Hann window of length n: `w[k] = 0.5·(1 - cos(2πk/(n-1)))` +/// +/// Rust: `dsp::windows::hann_window` +#[pyfunction] +#[pyo3(name = "hann_window", signature = (n))] +pub fn pyfn_hann_window<'py>(py: Python<'py>, n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::windows::hann_window(n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Generate a Hamming window of length n: `w[k] = 0.54 - 0.46·cos(2πk/(n-1))` +/// +/// Rust: `dsp::windows::hamming_window` +#[pyfunction] +#[pyo3(name = "hamming_window", signature = (n))] +pub fn pyfn_hamming_window<'py>(py: Python<'py>, n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::windows::hamming_window(n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Generate a Blackman window of length n: +/// `w[k] = 0.42 - 0.5·cos(2πk/(n-1)) + 0.08·cos(4πk/(n-1))` +/// +/// Rust: `dsp::windows::blackman_window` +#[pyfunction] +#[pyo3(name = "blackman_window", signature = (n))] +pub fn pyfn_blackman_window<'py>(py: Python<'py>, n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::windows::blackman_window(n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Generate a rectangular (uniform) window of length n: `w[k] = 1` for all k +/// +/// Rust: `dsp::windows::rectangular_window` +#[pyfunction] +#[pyo3(name = "rectangular_window", signature = (n))] +pub fn pyfn_rectangular_window<'py>(py: Python<'py>, n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::dsp::windows::rectangular_window(n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_window, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_window_metrics, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kaiser_beta_for_attenuation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hann_window, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hamming_window, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_blackman_window, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rectangular_window, m)?)?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_electromagnetism.rs b/bindings/python/src/generated/m_electromagnetism.rs new file mode 100644 index 0000000..ac41252 --- /dev/null +++ b/bindings/python/src/generated/m_electromagnetism.rs @@ -0,0 +1,749 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Coulomb's law: F = k_e * |q1 * q2| / r^2 +/// +/// Rust: `electromagnetism::coulomb_force` +#[pyfunction] +#[pyo3(name = "coulomb_force", signature = (q1, q2, distance))] +pub fn pyfn_coulomb_force(q1: f64, q2: f64, distance: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::coulomb_force(q1, q2, distance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Coulomb force (signed, 1D): positive = repulsive, negative = attractive +/// +/// Rust: `electromagnetism::coulomb_force_signed` +#[pyfunction] +#[pyo3(name = "coulomb_force_signed", signature = (q1, q2, distance))] +pub fn pyfn_coulomb_force_signed(q1: f64, q2: f64, distance: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::coulomb_force_signed(q1, q2, distance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Coulomb force vector from charge at pos1 to charge at pos2. +/// +/// Rust: `electromagnetism::coulomb_force_vec` +#[pyfunction] +#[pyo3(name = "coulomb_force_vec", signature = (q1, pos1, q2, pos2))] +pub fn pyfn_coulomb_force_vec(q1: f64, pos1: crate::generated::types::PyVec3Arg, q2: f64, pos2: crate::generated::types::PyVec3Arg) -> PyResult { + let pos1 = pos1.0; + let pos2 = pos2.0; + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::coulomb_force_vec(q1, pos1, q2, pos2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Electric field due to a point charge: E = k_e * q / r^2 +/// +/// Rust: `electromagnetism::electric_field_point_charge` +#[pyfunction] +#[pyo3(name = "electric_field_point_charge", signature = (charge, distance))] +pub fn pyfn_electric_field_point_charge(charge: f64, distance: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::electric_field_point_charge(charge, distance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Electric field vector at a point due to a charge at a given position. +/// +/// Rust: `electromagnetism::electric_field_vec` +#[pyfunction] +#[pyo3(name = "electric_field_vec", signature = (charge, charge_pos, field_point))] +pub fn pyfn_electric_field_vec(charge: f64, charge_pos: crate::generated::types::PyVec3Arg, field_point: crate::generated::types::PyVec3Arg) -> PyResult { + let charge_pos = charge_pos.0; + let field_point = field_point.0; + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::electric_field_vec(charge, charge_pos, field_point)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Electric potential due to a point charge: V = k_e * q / r +/// +/// Rust: `electromagnetism::electric_potential` +#[pyfunction] +#[pyo3(name = "electric_potential", signature = (charge, distance))] +pub fn pyfn_electric_potential(charge: f64, distance: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::electric_potential(charge, distance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Electric potential energy: U = k_e * q1 * q2 / r +/// +/// Rust: `electromagnetism::electric_potential_energy` +#[pyfunction] +#[pyo3(name = "electric_potential_energy", signature = (q1, q2, distance))] +pub fn pyfn_electric_potential_energy(q1: f64, q2: f64, distance: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::electric_potential_energy(q1, q2, distance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Electric flux through a surface (Gauss's law): Φ = q_enclosed / ε_0 +/// +/// Rust: `electromagnetism::electric_flux_gauss` +#[pyfunction] +#[pyo3(name = "electric_flux_gauss", signature = (enclosed_charge))] +pub fn pyfn_electric_flux_gauss(enclosed_charge: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::electric_flux_gauss(enclosed_charge)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Capacitance of a parallel plate capacitor: C = ε_0 * A / d +/// +/// Rust: `electromagnetism::capacitance_parallel_plate` +#[pyfunction] +#[pyo3(name = "capacitance_parallel_plate", signature = (area, separation))] +pub fn pyfn_capacitance_parallel_plate(area: f64, separation: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::capacitance_parallel_plate(area, separation)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Energy stored in a capacitor: U = 0.5 * C * V^2 +/// +/// Rust: `electromagnetism::capacitor_energy` +#[pyfunction] +#[pyo3(name = "capacitor_energy", signature = (capacitance, voltage))] +pub fn pyfn_capacitor_energy(capacitance: f64, voltage: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::capacitor_energy(capacitance, voltage)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Ohm's law: V = I * R +/// +/// Rust: `electromagnetism::ohms_law_voltage` +#[pyfunction] +#[pyo3(name = "ohms_law_voltage", signature = (current, resistance))] +pub fn pyfn_ohms_law_voltage(current: f64, resistance: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::ohms_law_voltage(current, resistance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Ohm's law: I = V / R +/// +/// Rust: `electromagnetism::ohms_law_current` +#[pyfunction] +#[pyo3(name = "ohms_law_current", signature = (voltage, resistance))] +pub fn pyfn_ohms_law_current(voltage: f64, resistance: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::ohms_law_current(voltage, resistance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Ohm's law: R = V / I +/// +/// Rust: `electromagnetism::ohms_law_resistance` +#[pyfunction] +#[pyo3(name = "ohms_law_resistance", signature = (voltage, current))] +pub fn pyfn_ohms_law_resistance(voltage: f64, current: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::ohms_law_resistance(voltage, current)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Electrical power: P = V * I +/// +/// Rust: `electromagnetism::electrical_power` +#[pyfunction] +#[pyo3(name = "electrical_power", signature = (voltage, current))] +pub fn pyfn_electrical_power(voltage: f64, current: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::electrical_power(voltage, current)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Electrical power: P = I^2 * R +/// +/// Rust: `electromagnetism::electrical_power_from_current` +#[pyfunction] +#[pyo3(name = "electrical_power_from_current", signature = (current, resistance))] +pub fn pyfn_electrical_power_from_current(current: f64, resistance: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::electrical_power_from_current(current, resistance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Resistors in series: R_total = R1 + R2 + ... +/// +/// Rust: `electromagnetism::resistors_series` +#[pyfunction] +#[pyo3(name = "resistors_series", signature = (resistances))] +pub fn pyfn_resistors_series<'py>(py: Python<'py>, resistances: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::electromagnetism::resistors_series(&resistances))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Resistors in parallel: 1/R_total = 1/R1 + 1/R2 + ... +/// +/// Rust: `electromagnetism::resistors_parallel` +#[pyfunction] +#[pyo3(name = "resistors_parallel", signature = (resistances))] +pub fn pyfn_resistors_parallel<'py>(py: Python<'py>, resistances: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::electromagnetism::resistors_parallel(&resistances))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Capacitors in series: 1/C_total = 1/C1 + 1/C2 + ... +/// +/// Rust: `electromagnetism::capacitors_series` +#[pyfunction] +#[pyo3(name = "capacitors_series", signature = (capacitances))] +pub fn pyfn_capacitors_series<'py>(py: Python<'py>, capacitances: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::electromagnetism::capacitors_series(&capacitances))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Capacitors in parallel: C_total = C1 + C2 + ... +/// +/// Rust: `electromagnetism::capacitors_parallel` +#[pyfunction] +#[pyo3(name = "capacitors_parallel", signature = (capacitances))] +pub fn pyfn_capacitors_parallel<'py>(py: Python<'py>, capacitances: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::electromagnetism::capacitors_parallel(&capacitances))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// RC time constant: τ = R * C +/// +/// Rust: `electromagnetism::rc_time_constant` +#[pyfunction] +#[pyo3(name = "rc_time_constant", signature = (resistance, capacitance))] +pub fn pyfn_rc_time_constant(resistance: f64, capacitance: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::rc_time_constant(resistance, capacitance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Voltage across charging capacitor: V(t) = V0 * (1 - e^(-t/RC)) +/// +/// Rust: `electromagnetism::rc_charging_voltage` +#[pyfunction] +#[pyo3(name = "rc_charging_voltage", signature = (v0, resistance, capacitance, time))] +pub fn pyfn_rc_charging_voltage(v0: f64, resistance: f64, capacitance: f64, time: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::rc_charging_voltage(v0, resistance, capacitance, time)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Magnetic force on a moving charge: F = q * v * B * sin(θ) +/// +/// Rust: `electromagnetism::magnetic_force_on_charge` +#[pyfunction] +#[pyo3(name = "magnetic_force_on_charge", signature = (charge, velocity, b_field, angle_rad))] +pub fn pyfn_magnetic_force_on_charge(charge: f64, velocity: f64, b_field: f64, angle_rad: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::magnetic_force_on_charge(charge, velocity, b_field, angle_rad)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Lorentz force: F = q * (E + v × B) +/// +/// Rust: `electromagnetism::lorentz_force` +#[pyfunction] +#[pyo3(name = "lorentz_force", signature = (charge, e_field, velocity, b_field))] +pub fn pyfn_lorentz_force(charge: f64, e_field: crate::generated::types::PyVec3Arg, velocity: crate::generated::types::PyVec3Arg, b_field: crate::generated::types::PyVec3Arg) -> PyResult { + let e_field = e_field.0; + let velocity = velocity.0; + let b_field = b_field.0; + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::lorentz_force(charge, e_field, velocity, b_field)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Magnetic field from a long straight wire: B = μ_0 * I / (2π * r) +/// +/// Rust: `electromagnetism::magnetic_field_wire` +#[pyfunction] +#[pyo3(name = "magnetic_field_wire", signature = (current, distance))] +pub fn pyfn_magnetic_field_wire(current: f64, distance: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::magnetic_field_wire(current, distance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Magnetic force between two parallel wires per unit length: F/L = μ_0 * I1 * I2 / (2π * d) +/// +/// Rust: `electromagnetism::force_between_wires` +#[pyfunction] +#[pyo3(name = "force_between_wires", signature = (i1, i2, distance))] +pub fn pyfn_force_between_wires(i1: f64, i2: f64, distance: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::force_between_wires(i1, i2, distance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Cyclotron radius: r = m*v / (|q|*B) +/// +/// Rust: `electromagnetism::cyclotron_radius` +#[pyfunction] +#[pyo3(name = "cyclotron_radius", signature = (mass, velocity, charge, b_field))] +pub fn pyfn_cyclotron_radius(mass: f64, velocity: f64, charge: f64, b_field: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::cyclotron_radius(mass, velocity, charge, b_field)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Cyclotron frequency: f = |q|*B / (2π*m) +/// +/// Rust: `electromagnetism::cyclotron_frequency` +#[pyfunction] +#[pyo3(name = "cyclotron_frequency", signature = (charge, b_field, mass))] +pub fn pyfn_cyclotron_frequency(charge: f64, b_field: f64, mass: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::cyclotron_frequency(charge, b_field, mass)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Faraday's law (magnitude): EMF = -N * dΦ/dt +/// +/// Rust: `electromagnetism::faraday_emf` +#[pyfunction] +#[pyo3(name = "faraday_emf", signature = (num_turns, delta_flux, delta_time))] +pub fn pyfn_faraday_emf(num_turns: f64, delta_flux: f64, delta_time: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::faraday_emf(num_turns, delta_flux, delta_time)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Motional EMF: ε = B * L * v +/// +/// Rust: `electromagnetism::motional_emf` +#[pyfunction] +#[pyo3(name = "motional_emf", signature = (b_field, length, velocity))] +pub fn pyfn_motional_emf(b_field: f64, length: f64, velocity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::motional_emf(b_field, length, velocity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Inductance energy: U = 0.5 * L * I^2 +/// +/// Rust: `electromagnetism::inductor_energy` +#[pyfunction] +#[pyo3(name = "inductor_energy", signature = (inductance, current))] +pub fn pyfn_inductor_energy(inductance: f64, current: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::inductor_energy(inductance, current)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Relationship between wavelength and frequency: c = λ * f +/// +/// Rust: `electromagnetism::wavelength_from_frequency` +#[pyfunction] +#[pyo3(name = "wavelength_from_frequency", signature = (frequency))] +pub fn pyfn_wavelength_from_frequency(frequency: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::wavelength_from_frequency(frequency)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Frequency from wavelength: f = c / λ +/// +/// Rust: `electromagnetism::frequency_from_wavelength` +#[pyfunction] +#[pyo3(name = "frequency_from_wavelength", signature = (wavelength))] +pub fn pyfn_frequency_from_wavelength(wavelength: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::frequency_from_wavelength(wavelength)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Poynting vector magnitude (EM wave intensity): S = E * B / μ_0 +/// +/// Rust: `electromagnetism::poynting_magnitude` +#[pyfunction] +#[pyo3(name = "poynting_magnitude", signature = (e_field, b_field))] +pub fn pyfn_poynting_magnitude(e_field: f64, b_field: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::poynting_magnitude(e_field, b_field)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Solenoid magnetic field: B = μ₀nI +/// +/// Rust: `electromagnetism::solenoid_field` +#[pyfunction] +#[pyo3(name = "solenoid_field", signature = (mu0, turns_per_length, current))] +pub fn pyfn_solenoid_field(mu0: f64, turns_per_length: f64, current: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::solenoid_field(mu0, turns_per_length, current)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Toroid magnetic field: B = μ₀NI/(2πr) +/// +/// Rust: `electromagnetism::toroid_field` +#[pyfunction] +#[pyo3(name = "toroid_field", signature = (mu0, total_turns, current, radius))] +pub fn pyfn_toroid_field(mu0: f64, total_turns: f64, current: f64, radius: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::toroid_field(mu0, total_turns, current, radius)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Magnetic flux: Φ = BA cos(θ) +/// +/// Rust: `electromagnetism::magnetic_flux` +#[pyfunction] +#[pyo3(name = "magnetic_flux", signature = (b_field, area, angle))] +pub fn pyfn_magnetic_flux(b_field: f64, area: f64, angle: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::magnetic_flux(b_field, area, angle)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Magnetic energy density: u = B²/(2μ₀) +/// +/// Rust: `electromagnetism::magnetic_energy_density` +#[pyfunction] +#[pyo3(name = "magnetic_energy_density", signature = (b_field))] +pub fn pyfn_magnetic_energy_density(b_field: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::magnetic_energy_density(b_field)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Mutual inductance of coaxial solenoids: M = μ₀n₁n₂AL +/// +/// Rust: `electromagnetism::mutual_inductance_coaxial` +#[pyfunction] +#[pyo3(name = "mutual_inductance_coaxial", signature = (mu0, n1, n2, area, length))] +pub fn pyfn_mutual_inductance_coaxial(mu0: f64, n1: f64, n2: f64, area: f64, length: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::mutual_inductance_coaxial(mu0, n1, n2, area, length)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Self-inductance of a solenoid: L = μ₀N²A/l +/// +/// Rust: `electromagnetism::self_inductance_solenoid` +#[pyfunction] +#[pyo3(name = "self_inductance_solenoid", signature = (mu0, turns, area, length))] +pub fn pyfn_self_inductance_solenoid(mu0: f64, turns: f64, area: f64, length: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::self_inductance_solenoid(mu0, turns, area, length)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Magnetic dipole moment: m = IA +/// +/// Rust: `electromagnetism::magnetic_dipole_moment` +#[pyfunction] +#[pyo3(name = "magnetic_dipole_moment", signature = (current, area))] +pub fn pyfn_magnetic_dipole_moment(current: f64, area: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::magnetic_dipole_moment(current, area)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Torque on a magnetic dipole: τ = mB sin(θ) +/// +/// Rust: `electromagnetism::torque_on_dipole` +#[pyfunction] +#[pyo3(name = "torque_on_dipole", signature = (moment, b_field, angle))] +pub fn pyfn_torque_on_dipole(moment: f64, b_field: f64, angle: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::torque_on_dipole(moment, b_field, angle)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Capacitive reactance: Xc = 1/(2πfC) +/// +/// Rust: `electromagnetism::capacitive_reactance` +#[pyfunction] +#[pyo3(name = "capacitive_reactance", signature = (frequency, capacitance))] +pub fn pyfn_capacitive_reactance(frequency: f64, capacitance: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::capacitive_reactance(frequency, capacitance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Inductive reactance: XL = 2πfL +/// +/// Rust: `electromagnetism::inductive_reactance` +#[pyfunction] +#[pyo3(name = "inductive_reactance", signature = (frequency, inductance))] +pub fn pyfn_inductive_reactance(frequency: f64, inductance: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::inductive_reactance(frequency, inductance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Impedance of a series RLC circuit: Z = √(R² + (XL - XC)²) +/// +/// Rust: `electromagnetism::impedance_rlc_series` +#[pyfunction] +#[pyo3(name = "impedance_rlc_series", signature = (resistance, inductive_reactance, capacitive_reactance))] +pub fn pyfn_impedance_rlc_series(resistance: f64, inductive_reactance: f64, capacitive_reactance: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::impedance_rlc_series(resistance, inductive_reactance, capacitive_reactance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Resonant frequency of an LC circuit: f₀ = 1/(2π√(LC)) +/// +/// Rust: `electromagnetism::resonant_frequency_lc` +#[pyfunction] +#[pyo3(name = "resonant_frequency_lc", signature = (inductance, capacitance))] +pub fn pyfn_resonant_frequency_lc(inductance: f64, capacitance: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::resonant_frequency_lc(inductance, capacitance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Power factor: cos(φ) = R/Z +/// +/// Rust: `electromagnetism::power_factor` +#[pyfunction] +#[pyo3(name = "power_factor", signature = (resistance, impedance))] +pub fn pyfn_power_factor(resistance: f64, impedance: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::power_factor(resistance, impedance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// RMS voltage: V_rms = V_peak/√2 +/// +/// Rust: `electromagnetism::rms_voltage` +#[pyfunction] +#[pyo3(name = "rms_voltage", signature = (peak))] +pub fn pyfn_rms_voltage(peak: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::rms_voltage(peak)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// RMS current: I_rms = I_peak/√2 +/// +/// Rust: `electromagnetism::rms_current` +#[pyfunction] +#[pyo3(name = "rms_current", signature = (peak))] +pub fn pyfn_rms_current(peak: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::rms_current(peak)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Average AC power: P = V_rms × I_rms × cos(φ) +/// +/// Rust: `electromagnetism::ac_power_average` +#[pyfunction] +#[pyo3(name = "ac_power_average", signature = (vrms, irms, power_factor))] +pub fn pyfn_ac_power_average(vrms: f64, irms: f64, power_factor: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::ac_power_average(vrms, irms, power_factor)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Quality factor of an RLC circuit: Q = (1/R)√(L/C) +/// +/// Rust: `electromagnetism::quality_factor_rlc` +#[pyfunction] +#[pyo3(name = "quality_factor_rlc", signature = (inductance, capacitance, resistance))] +pub fn pyfn_quality_factor_rlc(inductance: f64, capacitance: f64, resistance: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::quality_factor_rlc(inductance, capacitance, resistance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Bandwidth of an RLC circuit: BW = f₀/Q +/// +/// Rust: `electromagnetism::bandwidth_rlc` +#[pyfunction] +#[pyo3(name = "bandwidth_rlc", signature = (resonant_freq, quality))] +pub fn pyfn_bandwidth_rlc(resonant_freq: f64, quality: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::bandwidth_rlc(resonant_freq, quality)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// EM wave speed in a medium: v = 1/√(εμ) +/// +/// Rust: `electromagnetism::em_wave_speed` +#[pyfunction] +#[pyo3(name = "em_wave_speed", signature = (permittivity, permeability))] +pub fn pyfn_em_wave_speed(permittivity: f64, permeability: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::em_wave_speed(permittivity, permeability)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Refractive index from relative permittivity and permeability: n = √(ε_r × μ_r) +/// +/// Rust: `electromagnetism::refractive_index_from_em` +#[pyfunction] +#[pyo3(name = "refractive_index_from_em", signature = (permittivity_rel, permeability_rel))] +pub fn pyfn_refractive_index_from_em(permittivity_rel: f64, permeability_rel: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::refractive_index_from_em(permittivity_rel, permeability_rel)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Characteristic impedance of a medium: η = √(μ/ε) +/// +/// Rust: `electromagnetism::characteristic_impedance` +#[pyfunction] +#[pyo3(name = "characteristic_impedance", signature = (permeability, permittivity))] +pub fn pyfn_characteristic_impedance(permeability: f64, permittivity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::characteristic_impedance(permeability, permittivity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Free-space impedance: η₀ = √(μ₀/ε₀) ≈ 377 Ω +/// +/// Rust: `electromagnetism::free_space_impedance` +#[pyfunction] +#[pyo3(name = "free_space_impedance", signature = ())] +pub fn pyfn_free_space_impedance() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::free_space_impedance()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Total EM energy density: u = ε₀E²/2 + B²/(2μ₀) +/// +/// Rust: `electromagnetism::energy_density_em` +#[pyfunction] +#[pyo3(name = "energy_density_em", signature = (e_field, b_field))] +pub fn pyfn_energy_density_em(e_field: f64, b_field: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::energy_density_em(e_field, b_field)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Radiation intensity of a Hertzian dipole: I(θ) = (3P/(8π)) × sin²(θ) +/// +/// Rust: `electromagnetism::radiation_intensity_dipole` +#[pyfunction] +#[pyo3(name = "radiation_intensity_dipole", signature = (power, angle))] +pub fn pyfn_radiation_intensity_dipole(power: f64, angle: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::radiation_intensity_dipole(power, angle)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Larmor radiated power: P = q²a²/(6πε₀c³) +/// +/// Rust: `electromagnetism::larmor_power` +#[pyfunction] +#[pyo3(name = "larmor_power", signature = (charge, acceleration))] +pub fn pyfn_larmor_power(charge: f64, acceleration: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::larmor_power(charge, acceleration)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Transformer secondary voltage: V₂ = V₁ × N₂/N₁ +/// +/// Rust: `electromagnetism::transformer_voltage` +#[pyfunction] +#[pyo3(name = "transformer_voltage", signature = (v_primary, n_primary, n_secondary))] +pub fn pyfn_transformer_voltage(v_primary: f64, n_primary: f64, n_secondary: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::transformer_voltage(v_primary, n_primary, n_secondary)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Transformer secondary current: I₂ = I₁ × N₁/N₂ +/// +/// Rust: `electromagnetism::transformer_current` +#[pyfunction] +#[pyo3(name = "transformer_current", signature = (i_primary, n_primary, n_secondary))] +pub fn pyfn_transformer_current(i_primary: f64, n_primary: f64, n_secondary: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electromagnetism::transformer_current(i_primary, n_primary, n_secondary)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_coulomb_force, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_coulomb_force_signed, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_coulomb_force_vec, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_electric_field_point_charge, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_electric_field_vec, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_electric_potential, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_electric_potential_energy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_electric_flux_gauss, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_capacitance_parallel_plate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_capacitor_energy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ohms_law_voltage, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ohms_law_current, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ohms_law_resistance, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_electrical_power, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_electrical_power_from_current, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_resistors_series, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_resistors_parallel, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_capacitors_series, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_capacitors_parallel, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rc_time_constant, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rc_charging_voltage, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_magnetic_force_on_charge, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lorentz_force, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_magnetic_field_wire, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_force_between_wires, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cyclotron_radius, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cyclotron_frequency, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_faraday_emf, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_motional_emf, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_inductor_energy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wavelength_from_frequency, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_frequency_from_wavelength, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_poynting_magnitude, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_solenoid_field, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_toroid_field, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_magnetic_flux, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_magnetic_energy_density, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mutual_inductance_coaxial, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_self_inductance_solenoid, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_magnetic_dipole_moment, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_torque_on_dipole, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_capacitive_reactance, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_inductive_reactance, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_impedance_rlc_series, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_resonant_frequency_lc, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_power_factor, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rms_voltage, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rms_current, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ac_power_average, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_quality_factor_rlc, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bandwidth_rlc, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_em_wave_speed, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_refractive_index_from_em, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_characteristic_impedance, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_free_space_impedance, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_energy_density_em, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_radiation_intensity_dipole, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_larmor_power, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_transformer_voltage, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_transformer_current, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_electronics.rs b/bindings/python/src/generated/m_electronics.rs new file mode 100644 index 0000000..f51177b --- /dev/null +++ b/bindings/python/src/generated/m_electronics.rs @@ -0,0 +1,229 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Intrinsic carrier concentration: ni = sqrt(Nc * Nv) * exp(-Eg / (2kT)) +/// +/// Rust: `electronics::intrinsic_carrier_concentration` +#[pyfunction] +#[pyo3(name = "intrinsic_carrier_concentration", signature = (nc, nv, band_gap, temperature))] +pub fn pyfn_intrinsic_carrier_concentration(nc: f64, nv: f64, band_gap: f64, temperature: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electronics::intrinsic_carrier_concentration(nc, nv, band_gap, temperature)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Fermi-Dirac distribution: f(E) = 1 / (1 + exp((E - Ef) / (kT))) +/// +/// Rust: `electronics::fermi_dirac` +#[pyfunction] +#[pyo3(name = "fermi_dirac", signature = (energy, fermi_level, temperature))] +pub fn pyfn_fermi_dirac(energy: f64, fermi_level: f64, temperature: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electronics::fermi_dirac(energy, fermi_level, temperature)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Thermal voltage: Vt = kT / q +/// +/// Rust: `electronics::thermal_voltage` +#[pyfunction] +#[pyo3(name = "thermal_voltage", signature = (temperature))] +pub fn pyfn_thermal_voltage(temperature: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electronics::thermal_voltage(temperature)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Electrical conductivity: sigma = n * q * mu +/// +/// Rust: `electronics::conductivity` +#[pyfunction] +#[pyo3(name = "conductivity", signature = (carrier_density, mobility, charge))] +pub fn pyfn_conductivity(carrier_density: f64, mobility: f64, charge: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electronics::conductivity(carrier_density, mobility, charge)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Resistivity: rho = 1 / sigma +/// +/// Rust: `electronics::resistivity` +#[pyfunction] +#[pyo3(name = "resistivity", signature = (conductivity))] +pub fn pyfn_resistivity(conductivity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electronics::resistivity(conductivity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Drift velocity: vd = mu * E +/// +/// Rust: `electronics::drift_velocity` +#[pyfunction] +#[pyo3(name = "drift_velocity", signature = (mobility, electric_field))] +pub fn pyfn_drift_velocity(mobility: f64, electric_field: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electronics::drift_velocity(mobility, electric_field)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Einstein relation for diffusion coefficient: D = mu * kT / q +/// +/// Rust: `electronics::diffusion_coefficient_einstein` +#[pyfunction] +#[pyo3(name = "diffusion_coefficient_einstein", signature = (mobility, temperature))] +pub fn pyfn_diffusion_coefficient_einstein(mobility: f64, temperature: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electronics::diffusion_coefficient_einstein(mobility, temperature)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Built-in potential of a p-n junction: Vbi = (kT/q) * ln(Na * Nd / ni^2) +/// +/// Rust: `electronics::built_in_potential` +#[pyfunction] +#[pyo3(name = "built_in_potential", signature = (na, nd, ni, temperature))] +pub fn pyfn_built_in_potential(na: f64, nd: f64, ni: f64, temperature: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electronics::built_in_potential(na, nd, ni, temperature)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Depletion region width: W = sqrt(2 * epsilon * Vbi * (1/Na + 1/Nd) / q) +/// +/// Rust: `electronics::depletion_width` +#[pyfunction] +#[pyo3(name = "depletion_width", signature = (epsilon, vbi, na, nd, charge))] +pub fn pyfn_depletion_width(epsilon: f64, vbi: f64, na: f64, nd: f64, charge: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electronics::depletion_width(epsilon, vbi, na, nd, charge)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Shockley diode equation: I = Is * (exp(V / (n * Vt)) - 1) +/// +/// Rust: `electronics::diode_current` +#[pyfunction] +#[pyo3(name = "diode_current", signature = (is_, voltage, temperature, n))] +pub fn pyfn_diode_current(is_: f64, voltage: f64, temperature: f64, n: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electronics::diode_current(is_, voltage, temperature, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Reverse saturation current (symmetric approximation): +/// Is = q * A * ni^2 * (Dn/Ln + Dp/Lp) +/// +/// Rust: `electronics::diode_reverse_saturation` +#[pyfunction] +#[pyo3(name = "diode_reverse_saturation", signature = (area, ni, dn, dp, ln, lp, charge))] +pub fn pyfn_diode_reverse_saturation(area: f64, ni: f64, dn: f64, dp: f64, ln: f64, lp: f64, charge: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electronics::diode_reverse_saturation(area, ni, dn, dp, ln, lp, charge)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// MOSFET drain current in the linear region: +/// Id = mu * Cox * (W/L) * ((Vgs - Vth) * Vds - Vds^2 / 2) +/// +/// Rust: `electronics::mosfet_drain_current_linear` +#[pyfunction] +#[pyo3(name = "mosfet_drain_current_linear", signature = (mu, cox, w, l, vgs, vth, vds))] +pub fn pyfn_mosfet_drain_current_linear(mu: f64, cox: f64, w: f64, l: f64, vgs: f64, vth: f64, vds: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electronics::mosfet_drain_current_linear(mu, cox, w, l, vgs, vth, vds)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// MOSFET drain current in the saturation region: +/// Id = (mu * Cox / 2) * (W/L) * (Vgs - Vth)^2 +/// +/// Rust: `electronics::mosfet_drain_current_saturation` +#[pyfunction] +#[pyo3(name = "mosfet_drain_current_saturation", signature = (mu, cox, w, l, vgs, vth))] +pub fn pyfn_mosfet_drain_current_saturation(mu: f64, cox: f64, w: f64, l: f64, vgs: f64, vth: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electronics::mosfet_drain_current_saturation(mu, cox, w, l, vgs, vth)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Solar cell current: I = Iph - I0 * (exp(V / Vt) - 1) +/// +/// Rust: `electronics::solar_cell_current` +#[pyfunction] +#[pyo3(name = "solar_cell_current", signature = (photocurrent, dark_current, voltage, temperature))] +pub fn pyfn_solar_cell_current(photocurrent: f64, dark_current: f64, voltage: f64, temperature: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electronics::solar_cell_current(photocurrent, dark_current, voltage, temperature)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Open-circuit voltage: Voc = Vt * ln(Iph / I0 + 1) +/// +/// Rust: `electronics::open_circuit_voltage` +#[pyfunction] +#[pyo3(name = "open_circuit_voltage", signature = (photocurrent, dark_current, temperature))] +pub fn pyfn_open_circuit_voltage(photocurrent: f64, dark_current: f64, temperature: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electronics::open_circuit_voltage(photocurrent, dark_current, temperature)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Fill factor: FF = Pmax / (Voc * Isc) +/// +/// Rust: `electronics::fill_factor` +#[pyfunction] +#[pyo3(name = "fill_factor", signature = (voc, isc, pmax))] +pub fn pyfn_fill_factor(voc: f64, isc: f64, pmax: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electronics::fill_factor(voc, isc, pmax)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Solar cell efficiency: eta = Pmax / Pin +/// +/// Rust: `electronics::solar_cell_efficiency` +#[pyfunction] +#[pyo3(name = "solar_cell_efficiency", signature = (pmax, incident_power))] +pub fn pyfn_solar_cell_efficiency(pmax: f64, incident_power: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::electronics::solar_cell_efficiency(pmax, incident_power)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_intrinsic_carrier_concentration, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fermi_dirac, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_thermal_voltage, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_conductivity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_resistivity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_drift_velocity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_diffusion_coefficient_einstein, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_built_in_potential, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_depletion_width, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_diode_current, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_diode_reverse_saturation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mosfet_drain_current_linear, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mosfet_drain_current_saturation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_solar_cell_current, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_open_circuit_voltage, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fill_factor, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_solar_cell_efficiency, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_exact.rs b/bindings/python/src/generated/m_exact.rs new file mode 100644 index 0000000..c02bbf8 --- /dev/null +++ b/bindings/python/src/generated/m_exact.rs @@ -0,0 +1,22 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + Ok(()) +} diff --git a/bindings/python/src/generated/m_exact__bigfloat.rs b/bindings/python/src/generated/m_exact__bigfloat.rs new file mode 100644 index 0000000..7f8c5dc --- /dev/null +++ b/bindings/python/src/generated/m_exact__bigfloat.rs @@ -0,0 +1,101 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// The decimal expansion of π truncated to `n_decimal` places, e.g. +/// `"3.14159"` for `n_decimal == 5`. +/// +/// Rust: `exact::bigfloat::pi_digits` +#[pyfunction] +#[pyo3(name = "pi_digits", signature = (n_decimal))] +pub fn pyfn_pi_digits(n_decimal: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::bigfloat::pi_digits(n_decimal)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_string()) +} + +/// The decimal expansion of `e` truncated to `n` places. +/// +/// Rust: `exact::bigfloat::e_digits` +#[pyfunction] +#[pyo3(name = "e_digits", signature = (n))] +pub fn pyfn_e_digits(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::bigfloat::e_digits(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_string()) +} + +/// The decimal expansion of `√2` truncated to `n` places. +/// +/// Rust: `exact::bigfloat::sqrt2_digits` +#[pyfunction] +#[pyo3(name = "sqrt2_digits", signature = (n))] +pub fn pyfn_sqrt2_digits(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::bigfloat::sqrt2_digits(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_string()) +} + +/// π to `precision` bits from Machin's formula, +/// `π = 16·atan(1/5) − 4·atan(1/239)`. +/// +/// This is deliberately a different algorithm from `BigFloat::pi` (a +/// linearly convergent arctangent series against a quadratically +/// convergent AGM iteration) so that the two can cross-check each other. +/// +/// Panics: +/// Panics if `precision < 2`. +/// +/// Rust: `exact::bigfloat::machin_pi` +#[pyfunction] +#[pyo3(name = "machin_pi", signature = (precision))] +pub fn pyfn_machin_pi(precision: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::bigfloat::machin_pi(precision)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBigFloat { inner: __v }) +} + +/// The exact error of `core::compensated::sum_neumaier` on `xs`. +/// +/// Every `f64` is a dyadic rational, so the true sum `Σ xᵢ` is computed +/// exactly in `BigFloat` at a precision wide enough to hold every bit of +/// every operand. The return value is `sum_neumaier(xs) − Σ xᵢ`, +/// evaluated exactly and then rounded once to `f64`; it is exactly `0.0` +/// whenever the compensated sum is perfect. +/// +/// Panics: +/// Panics if any element is infinite or NaN. +/// +/// Rust: `exact::bigfloat::compensated_to_bigfloat_check` +#[pyfunction] +#[pyo3(name = "compensated_to_bigfloat_check", signature = (xs))] +pub fn pyfn_compensated_to_bigfloat_check<'py>(py: Python<'py>, xs: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::exact::bigfloat::compensated_to_bigfloat_check(&xs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_pi_digits, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_e_digits, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sqrt2_digits, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_machin_pi, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_compensated_to_bigfloat_check, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_exact__bigint.rs b/bindings/python/src/generated/m_exact__bigint.rs new file mode 100644 index 0000000..72bd89e --- /dev/null +++ b/bindings/python/src/generated/m_exact__bigint.rs @@ -0,0 +1,560 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// +/// Rust: `exact::bigint::BigInt::zero` +#[pyfunction] +#[pyo3(name = "zero", signature = ())] +pub fn pyfn_bigint_zero<'py>(py: Python<'py>) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::bigint::BigInt::zero()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// +/// Rust: `exact::bigint::BigInt::one` +#[pyfunction] +#[pyo3(name = "one", signature = ())] +pub fn pyfn_bigint_one<'py>(py: Python<'py>) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::bigint::BigInt::one()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// +/// Rust: `exact::bigint::BigInt::from_u64` +#[pyfunction] +#[pyo3(name = "from_u64", signature = (n))] +pub fn pyfn_bigint_from_u64<'py>(py: Python<'py>, n: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::bigint::BigInt::from_u64(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// +/// Rust: `exact::bigint::BigInt::from_i64` +#[pyfunction] +#[pyo3(name = "from_i64", signature = (n))] +pub fn pyfn_bigint_from_i64<'py>(py: Python<'py>, n: i64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::bigint::BigInt::from_i64(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// Parse in `radix` (2..=36), accepting a leading `+` or `-` and +/// either case of letter digit. +/// +/// Errors: +/// Returns `GeomError::InvalidArgument` for an unsupported radix, an +/// empty digit string, or an out-of-range character. +/// +/// Rust: `exact::bigint::BigInt::from_str_radix` +#[pyfunction] +#[pyo3(name = "from_str_radix", signature = (s, radix))] +pub fn pyfn_bigint_from_str_radix<'py>(py: Python<'py>, s: String, radix: u32) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::bigint::BigInt::from_str_radix(&s, radix)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// Render in `radix` (2..=36) using lower-case letter digits. +/// +/// Panics: +/// Panics if `radix` is outside 2..=36. +/// +/// Rust: `exact::bigint::BigInt::to_string_radix` +#[pyfunction] +#[pyo3(name = "to_string_radix", signature = (n, radix))] +pub fn pyfn_bigint_to_string_radix(n: crate::runtime::coerce::BigIntArg, radix: u32) -> PyResult { + let n = n.0; + let __r = crate::runtime::guard(|| n.to_string_radix(radix)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_string()) +} + +/// Nearest `f64`, saturating to infinity beyond the exponent range. +/// +/// Rust: `exact::bigint::BigInt::to_f64` +#[pyfunction] +#[pyo3(name = "to_f64", signature = (n))] +pub fn pyfn_bigint_to_f64(n: crate::runtime::coerce::BigIntArg) -> PyResult { + let n = n.0; + let __r = crate::runtime::guard(|| n.to_f64()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The value as an `i64`, or `None` if it does not fit. +/// +/// Rust: `exact::bigint::BigInt::to_i64` +#[pyfunction] +#[pyo3(name = "to_i64", signature = (n))] +pub fn pyfn_bigint_to_i64(n: crate::runtime::coerce::BigIntArg) -> PyResult> { + let n = n.0; + let __r = crate::runtime::guard(|| n.to_i64()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// Number of bits in the magnitude; zero has zero bits. +/// +/// Rust: `exact::bigint::BigInt::bits` +#[pyfunction] +#[pyo3(name = "bits", signature = (n))] +pub fn pyfn_bigint_bits(n: crate::runtime::coerce::BigIntArg) -> PyResult { + let n = n.0; + let __r = crate::runtime::guard(|| n.bits()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// +/// Rust: `exact::bigint::BigInt::is_zero` +#[pyfunction] +#[pyo3(name = "is_zero", signature = (n))] +pub fn pyfn_bigint_is_zero(n: crate::runtime::coerce::BigIntArg) -> PyResult { + let n = n.0; + let __r = crate::runtime::guard(|| n.is_zero()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// +/// Rust: `exact::bigint::BigInt::is_negative` +#[pyfunction] +#[pyo3(name = "is_negative", signature = (n))] +pub fn pyfn_bigint_is_negative(n: crate::runtime::coerce::BigIntArg) -> PyResult { + let n = n.0; + let __r = crate::runtime::guard(|| n.is_negative()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// +/// Rust: `exact::bigint::BigInt::is_even` +#[pyfunction] +#[pyo3(name = "is_even", signature = (n))] +pub fn pyfn_bigint_is_even(n: crate::runtime::coerce::BigIntArg) -> PyResult { + let n = n.0; + let __r = crate::runtime::guard(|| n.is_even()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// +/// Rust: `exact::bigint::BigInt::abs` +#[pyfunction] +#[pyo3(name = "abs", signature = (n))] +pub fn pyfn_bigint_abs<'py>(py: Python<'py>, n: crate::runtime::coerce::BigIntArg) -> PyResult> { + let n = n.0; + let __r = crate::runtime::guard(|| n.abs()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// +/// Rust: `exact::bigint::BigInt::neg` +#[pyfunction] +#[pyo3(name = "neg", signature = (n))] +pub fn pyfn_bigint_neg<'py>(py: Python<'py>, n: crate::runtime::coerce::BigIntArg) -> PyResult> { + let n = n.0; + let __r = crate::runtime::guard(|| n.neg()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// +/// Rust: `exact::bigint::BigInt::add` +#[pyfunction] +#[pyo3(name = "add", signature = (n, other))] +pub fn pyfn_bigint_add<'py>(py: Python<'py>, n: crate::runtime::coerce::BigIntArg, other: crate::runtime::coerce::BigIntArg) -> PyResult> { + let n = n.0; + let other = other.0; + let __r = crate::runtime::guard(|| n.add(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// +/// Rust: `exact::bigint::BigInt::sub` +#[pyfunction] +#[pyo3(name = "sub", signature = (n, other))] +pub fn pyfn_bigint_sub<'py>(py: Python<'py>, n: crate::runtime::coerce::BigIntArg, other: crate::runtime::coerce::BigIntArg) -> PyResult> { + let n = n.0; + let other = other.0; + let __r = crate::runtime::guard(|| n.sub(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// +/// Rust: `exact::bigint::BigInt::mul` +#[pyfunction] +#[pyo3(name = "mul", signature = (n, other))] +pub fn pyfn_bigint_mul<'py>(py: Python<'py>, n: crate::runtime::coerce::BigIntArg, other: crate::runtime::coerce::BigIntArg) -> PyResult> { + let n = n.0; + let other = other.0; + let __r = crate::runtime::guard(|| n.mul(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// Truncated division: the quotient rounds toward zero and the +/// remainder takes the sign of the dividend, matching Rust's `/` and +/// `%` on primitive integers. +/// +/// Panics: +/// Panics if `other` is zero. +/// +/// Rust: `exact::bigint::BigInt::div_rem` +#[pyfunction] +#[pyo3(name = "div_rem", signature = (n, other))] +pub fn pyfn_bigint_div_rem<'py>(py: Python<'py>, n: crate::runtime::coerce::BigIntArg, other: crate::runtime::coerce::BigIntArg) -> PyResult<(pyo3::Bound<'py, pyo3::PyAny>, pyo3::Bound<'py, pyo3::PyAny>)> { + let n = n.0; + let other = other.0; + let __r = crate::runtime::guard(|| n.div_rem(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((crate::runtime::coerce::bigint_out(py, &__v.0)?, crate::runtime::coerce::bigint_out(py, &__v.1)?)) +} + +/// Euclidean remainder: always in `0..|m|`. +/// +/// Panics: +/// Panics if `m` is zero. +/// +/// Rust: `exact::bigint::BigInt::rem_euclid` +#[pyfunction] +#[pyo3(name = "rem_euclid", signature = (n, m))] +pub fn pyfn_bigint_rem_euclid<'py>(py: Python<'py>, n: crate::runtime::coerce::BigIntArg, m: crate::runtime::coerce::BigIntArg) -> PyResult> { + let n = n.0; + let m = m.0; + let __r = crate::runtime::guard(|| n.rem_euclid(&m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// `self` raised to `e` by binary exponentiation. +/// +/// Rust: `exact::bigint::BigInt::pow` +#[pyfunction] +#[pyo3(name = "pow", signature = (n, e))] +pub fn pyfn_bigint_pow<'py>(py: Python<'py>, n: crate::runtime::coerce::BigIntArg, e: u64) -> PyResult> { + let n = n.0; + let __r = crate::runtime::guard(|| n.pow(e)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// Modular exponentiation by a 4-bit sliding window, reducing after +/// every multiply. The result is the least non-negative residue. +/// +/// Panics: +/// Panics if `m` is zero or `e` is negative. +/// +/// Rust: `exact::bigint::BigInt::mod_pow` +#[pyfunction] +#[pyo3(name = "mod_pow", signature = (n, e, m))] +pub fn pyfn_bigint_mod_pow<'py>(py: Python<'py>, n: crate::runtime::coerce::BigIntArg, e: crate::runtime::coerce::BigIntArg, m: crate::runtime::coerce::BigIntArg) -> PyResult> { + let n = n.0; + let e = e.0; + let m = m.0; + let __r = crate::runtime::guard(|| n.mod_pow(&e, &m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// Greatest common divisor, always non-negative. `gcd(0, 0)` is 0. +/// +/// Rust: `exact::bigint::BigInt::gcd` +#[pyfunction] +#[pyo3(name = "gcd", signature = (n, other))] +pub fn pyfn_bigint_gcd<'py>(py: Python<'py>, n: crate::runtime::coerce::BigIntArg, other: crate::runtime::coerce::BigIntArg) -> PyResult> { + let n = n.0; + let other = other.0; + let __r = crate::runtime::guard(|| n.gcd(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// Least common multiple, always non-negative. Zero if either side is +/// zero. +/// +/// Rust: `exact::bigint::BigInt::lcm` +#[pyfunction] +#[pyo3(name = "lcm", signature = (n, other))] +pub fn pyfn_bigint_lcm<'py>(py: Python<'py>, n: crate::runtime::coerce::BigIntArg, other: crate::runtime::coerce::BigIntArg) -> PyResult> { + let n = n.0; + let other = other.0; + let __r = crate::runtime::guard(|| n.lcm(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// Extended Euclid: returns `(g, x, y)` with `self*x + other*y == g` +/// and `g == gcd(self, other) >= 0`. +/// +/// Rust: `exact::bigint::BigInt::extended_gcd` +#[pyfunction] +#[pyo3(name = "extended_gcd", signature = (n, other))] +pub fn pyfn_bigint_extended_gcd<'py>(py: Python<'py>, n: crate::runtime::coerce::BigIntArg, other: crate::runtime::coerce::BigIntArg) -> PyResult<(pyo3::Bound<'py, pyo3::PyAny>, pyo3::Bound<'py, pyo3::PyAny>, pyo3::Bound<'py, pyo3::PyAny>)> { + let n = n.0; + let other = other.0; + let __r = crate::runtime::guard(|| n.extended_gcd(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((crate::runtime::coerce::bigint_out(py, &__v.0)?, crate::runtime::coerce::bigint_out(py, &__v.1)?, crate::runtime::coerce::bigint_out(py, &__v.2)?)) +} + +/// Modular inverse, or `None` when `gcd(self, m) != 1`. +/// +/// Rust: `exact::bigint::BigInt::mod_inverse` +#[pyfunction] +#[pyo3(name = "mod_inverse", signature = (n, m))] +pub fn pyfn_bigint_mod_inverse<'py>(py: Python<'py>, n: crate::runtime::coerce::BigIntArg, m: crate::runtime::coerce::BigIntArg) -> PyResult>> { + let n = n.0; + let m = m.0; + let __r = crate::runtime::guard(|| n.mod_inverse(&m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(match __v { Some(__x) => Some(crate::runtime::coerce::bigint_out(py, &__x)?), None => None }) +} + +/// Shift left by `bits`, preserving sign. +/// +/// Rust: `exact::bigint::BigInt::shl` +#[pyfunction] +#[pyo3(name = "shl", signature = (n, bits))] +pub fn pyfn_bigint_shl<'py>(py: Python<'py>, n: crate::runtime::coerce::BigIntArg, bits: usize) -> PyResult> { + let n = n.0; + let __r = crate::runtime::guard(|| n.shl(bits)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// Shift the magnitude right by `bits`, preserving sign. This +/// truncates toward zero rather than flooring, so it matches +/// `div_rem` by a power of two rather than an arithmetic shift. +/// +/// Rust: `exact::bigint::BigInt::shr` +#[pyfunction] +#[pyo3(name = "shr", signature = (n, bits))] +pub fn pyfn_bigint_shr<'py>(py: Python<'py>, n: crate::runtime::coerce::BigIntArg, bits: usize) -> PyResult> { + let n = n.0; + let __r = crate::runtime::guard(|| n.shr(bits)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// Bit `i` of the magnitude, counting from the least significant. +/// +/// Rust: `exact::bigint::BigInt::bit` +#[pyfunction] +#[pyo3(name = "bit", signature = (n, i))] +pub fn pyfn_bigint_bit(n: crate::runtime::coerce::BigIntArg, i: usize) -> PyResult { + let n = n.0; + let __r = crate::runtime::guard(|| n.bit(i)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Bitwise AND of the magnitudes; the result takes `self`'s sign. +/// +/// Rust: `exact::bigint::BigInt::and` +#[pyfunction] +#[pyo3(name = "and_", signature = (n, other))] +pub fn pyfn_bigint_and<'py>(py: Python<'py>, n: crate::runtime::coerce::BigIntArg, other: crate::runtime::coerce::BigIntArg) -> PyResult> { + let n = n.0; + let other = other.0; + let __r = crate::runtime::guard(|| n.and(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// Bitwise OR of the magnitudes; the result takes `self`'s sign, or +/// `other`'s when `self` is zero. +/// +/// Rust: `exact::bigint::BigInt::or` +#[pyfunction] +#[pyo3(name = "or_", signature = (n, other))] +pub fn pyfn_bigint_or<'py>(py: Python<'py>, n: crate::runtime::coerce::BigIntArg, other: crate::runtime::coerce::BigIntArg) -> PyResult> { + let n = n.0; + let other = other.0; + let __r = crate::runtime::guard(|| n.or(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// Bitwise XOR of the magnitudes; the result takes `self`'s sign, or +/// `other`'s when `self` is zero. +/// +/// Rust: `exact::bigint::BigInt::xor` +#[pyfunction] +#[pyo3(name = "xor", signature = (n, other))] +pub fn pyfn_bigint_xor<'py>(py: Python<'py>, n: crate::runtime::coerce::BigIntArg, other: crate::runtime::coerce::BigIntArg) -> PyResult> { + let n = n.0; + let other = other.0; + let __r = crate::runtime::guard(|| n.xor(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// Integer square root: the largest `r` with `r*r <= self`. +/// +/// Panics: +/// Panics if `self` is negative. +/// +/// Rust: `exact::bigint::BigInt::sqrt` +#[pyfunction] +#[pyo3(name = "sqrt", signature = (n))] +pub fn pyfn_bigint_sqrt<'py>(py: Python<'py>, n: crate::runtime::coerce::BigIntArg) -> PyResult> { + let n = n.0; + let __r = crate::runtime::guard(|| n.sqrt()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// Integer `n`th root: the largest `r` with `r^n <= self`. +/// +/// Panics: +/// Panics if `n` is zero, or if `self` is negative with even `n`. +/// +/// Rust: `exact::bigint::BigInt::nth_root` +#[pyfunction] +#[pyo3(name = "nth_root", signature = (bigint, n))] +pub fn pyfn_bigint_nth_root<'py>(py: Python<'py>, bigint: crate::runtime::coerce::BigIntArg, n: u32) -> PyResult> { + let bigint = bigint.0; + let __r = crate::runtime::guard(|| bigint.nth_root(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// +/// Rust: `exact::bigint::BigInt::is_perfect_square` +#[pyfunction] +#[pyo3(name = "is_perfect_square", signature = (n))] +pub fn pyfn_bigint_is_perfect_square(n: crate::runtime::coerce::BigIntArg) -> PyResult { + let n = n.0; + let __r = crate::runtime::guard(|| n.is_perfect_square()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// A uniformly random non-negative integer with exactly `bits` bits of +/// magnitude (the top bit is set), or zero when `bits` is zero. +/// +/// Rust: `exact::bigint::BigInt::random_bits` +#[pyfunction] +#[pyo3(name = "random_bits", signature = (bits, rng))] +pub fn pyfn_bigint_random_bits<'py>(py: Python<'py>, bits: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::exact::bigint::BigInt::random_bits(bits, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// A uniformly random integer in `0..bound` by rejection sampling. +/// +/// Panics: +/// Panics if `bound` is not positive. +/// +/// Rust: `exact::bigint::BigInt::random_below` +#[pyfunction] +#[pyo3(name = "random_below", signature = (bound, rng))] +pub fn pyfn_bigint_random_below<'py>(py: Python<'py>, bound: crate::runtime::coerce::BigIntArg, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let bound = bound.0; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::exact::bigint::BigInt::random_below(&bound, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// `n!`. +/// +/// Rust: `exact::bigint::BigInt::factorial` +#[pyfunction] +#[pyo3(name = "factorial", signature = (n))] +pub fn pyfn_bigint_factorial<'py>(py: Python<'py>, n: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::bigint::BigInt::factorial(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// The binomial coefficient `n choose k`, zero when `k > n`. +/// +/// Rust: `exact::bigint::BigInt::binomial` +#[pyfunction] +#[pyo3(name = "binomial", signature = (n, k))] +pub fn pyfn_bigint_binomial<'py>(py: Python<'py>, n: u64, k: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::bigint::BigInt::binomial(n, k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// The `n`th Fibonacci number by fast doubling, with `F(0) = 0`. +/// +/// Rust: `exact::bigint::BigInt::fibonacci` +#[pyfunction] +#[pyo3(name = "fibonacci", signature = (n))] +pub fn pyfn_bigint_fibonacci<'py>(py: Python<'py>, n: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::bigint::BigInt::fibonacci(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_bigint_zero, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bigint_one, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bigint_from_u64, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bigint_from_i64, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bigint_from_str_radix, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bigint_to_string_radix, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bigint_to_f64, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bigint_to_i64, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bigint_bits, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bigint_is_zero, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bigint_is_negative, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bigint_is_even, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bigint_abs, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bigint_neg, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bigint_add, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bigint_sub, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bigint_mul, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bigint_div_rem, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bigint_rem_euclid, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bigint_pow, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bigint_mod_pow, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bigint_gcd, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bigint_lcm, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bigint_extended_gcd, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bigint_mod_inverse, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bigint_shl, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bigint_shr, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bigint_bit, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bigint_and, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bigint_or, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bigint_xor, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bigint_sqrt, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bigint_nth_root, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bigint_is_perfect_square, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bigint_random_bits, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bigint_random_below, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bigint_factorial, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bigint_binomial, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bigint_fibonacci, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_exact__contfrac.rs b/bindings/python/src/generated/m_exact__contfrac.rs new file mode 100644 index 0000000..a6b2443 --- /dev/null +++ b/bindings/python/src/generated/m_exact__contfrac.rs @@ -0,0 +1,211 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// The simple continued-fraction expansion of a float, `[a0; a1, a2, ...]`. +/// +/// Stops after `max_terms`, or earlier once the remaining fractional part +/// is too small to yield a meaningful term. Only the leading terms of the +/// result describe the intended real number: an `f64` carries about 53 +/// bits, so terms beyond roughly the twentieth describe the rounding of +/// the input rather than the number itself. +/// +/// Panics: +/// Panics if `x` is not finite. +/// +/// Rust: `exact::contfrac::continued_fraction_f64` +#[pyfunction] +#[pyo3(name = "continued_fraction_f64", signature = (x, max_terms))] +pub fn pyfn_continued_fraction_f64<'py>(py: Python<'py>, x: f64, max_terms: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::exact::contfrac::continued_fraction_f64(x, max_terms))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The convergents `h_k / k_k` of a simple continued fraction. +/// +/// Uses the standard recurrence `h_k = a_k h_{k-1} + h_{k-2}`, and the +/// same for the denominators. +/// +/// Rust: `exact::contfrac::convergents` +#[pyfunction] +#[pyo3(name = "convergents", signature = (cf))] +pub fn pyfn_convergents<'py>(py: Python<'py>, cf: Vec) -> PyResult>> { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::contfrac::convergents(&cf)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| -> PyResult> { Ok(crate::runtime::coerce::rational_out(py, &__x)?) }).collect::>>()?) +} + +/// The periodic continued fraction of `sqrt(n)`, as `(head, period)` with +/// `sqrt(n) = [head; period repeated]`. +/// +/// For a perfect square the period is empty. Otherwise the expansion is +/// purely periodic after the first term and the period always ends with +/// `2*a0`, which is the termination test used here. +/// +/// Panics: +/// Panics if `n` is zero. +/// +/// Rust: `exact::contfrac::periodic_cf_sqrt` +#[pyfunction] +#[pyo3(name = "periodic_cf_sqrt", signature = (n))] +pub fn pyfn_periodic_cf_sqrt(n: u64) -> PyResult<(Vec, Vec)> { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::contfrac::periodic_cf_sqrt(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// The fundamental solution of Pell's equation `x^2 - d y^2 = 1`. +/// +/// Returns `None` when `d` is a perfect square, where the equation has +/// only the trivial solution. Otherwise the smallest solution with +/// `y > 0` is a convergent of the continued fraction of `sqrt(d)`. +/// +/// Panics: +/// Panics if `d` is zero. +/// +/// Rust: `exact::contfrac::pell_fundamental_solution` +#[pyfunction] +#[pyo3(name = "pell_fundamental_solution", signature = (d))] +pub fn pyfn_pell_fundamental_solution<'py>(py: Python<'py>, d: u64) -> PyResult, pyo3::Bound<'py, pyo3::PyAny>)>> { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::contfrac::pell_fundamental_solution(d)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(match __v { Some(__x) => Some((crate::runtime::coerce::bigint_out(py, &__x.0)?, crate::runtime::coerce::bigint_out(py, &__x.1)?)), None => None }) +} + +/// Evaluate a generalized continued fraction +/// `b(0) + a(1)/(b(1) + a(2)/(b(2) + ...))` to `n` levels by the modified +/// Lentz algorithm. +/// +/// Lentz builds the value from the top down with multiplicative updates, +/// so it never forms the deep nested quotient directly and cannot lose the +/// tail to cancellation. Zero intermediates are nudged to a tiny value, +/// which is the "modified" part. +/// +/// Panics: +/// Panics if `n` is zero. +/// +/// Rust: `exact::contfrac::generalized_cf_eval` +#[pyfunction] +#[pyo3(name = "generalized_cf_eval", signature = (a, b, n))] +pub fn pyfn_generalized_cf_eval(a: pyo3::Py, b: pyo3::Py, n: usize) -> PyResult { + let __cb_a = std::rc::Rc::new(crate::runtime::Callback::new(a)); + let a = { let __cb = __cb_a.clone(); move |__a0: usize| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __cb_b = std::rc::Rc::new(crate::runtime::Callback::new(b)); + let b = { let __cb = __cb_b.clone(); move |__a0: usize| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::exact::contfrac::generalized_cf_eval(&a, &b, n)); + crate::runtime::callback::check(&[&__cb_a, &__cb_b], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The first `n` terms of the continued fraction of `e`. +/// +/// `e = [2; 1, 2, 1, 1, 4, 1, 1, 6, 1, ...]`: after the leading 2 the +/// terms run in blocks of `1, 2k, 1`. +/// +/// Rust: `exact::contfrac::cf_e` +#[pyfunction] +#[pyo3(name = "cf_e", signature = (n))] +pub fn pyfn_cf_e<'py>(py: Python<'py>, n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::exact::contfrac::cf_e(n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The first `n` terms of the continued fraction of `pi`. +/// +/// `pi` has no known pattern, so the terms are read off a high-precision +/// value computed here in fixed point rather than from an `f64`, which +/// would only support about twenty correct terms. The working precision +/// is chosen generously against the number of terms requested. +/// +/// Rust: `exact::contfrac::cf_pi_terms` +#[pyfunction] +#[pyo3(name = "cf_pi_terms", signature = (n))] +pub fn pyfn_cf_pi_terms<'py>(py: Python<'py>, n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::exact::contfrac::cf_pi_terms(n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The orbit of `x` under the Gauss map `G(x) = frac(1/x)`, `n` steps. +/// +/// The Gauss map is the shift on continued-fraction expansions: the +/// integer parts of the reciprocals along the orbit are exactly the +/// partial quotients. +/// +/// Panics: +/// Panics if `x` is not finite. +/// +/// Rust: `exact::contfrac::gauss_map_orbit` +#[pyfunction] +#[pyo3(name = "gauss_map_orbit", signature = (x, n))] +pub fn pyfn_gauss_map_orbit<'py>(py: Python<'py>, x: f64, n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::exact::contfrac::gauss_map_orbit(x, n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The geometric mean of the first `n` continued-fraction terms of `x`. +/// +/// For almost every irrational this tends to Khinchin's constant, +/// about 2.685452001. Convergence is very slow, so a short orbit only +/// lands in the neighbourhood. +/// +/// Panics: +/// Panics if `x` is not finite. +/// +/// Rust: `exact::contfrac::khinchin_estimate` +#[pyfunction] +#[pyo3(name = "khinchin_estimate", signature = (x, n))] +pub fn pyfn_khinchin_estimate(x: f64, n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::contfrac::khinchin_estimate(x, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Estimate Levy's constant from the growth of the convergent +/// denominators of `x`: `q_n^(1/n)` tends to `exp(pi^2 / (12 ln 2))`, +/// about 3.275822918. +/// +/// Panics: +/// Panics if `x` is not finite. +/// +/// Rust: `exact::contfrac::levy_constant_estimate` +#[pyfunction] +#[pyo3(name = "levy_constant_estimate", signature = (x, n))] +pub fn pyfn_levy_constant_estimate(x: f64, n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::contfrac::levy_constant_estimate(x, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_continued_fraction_f64, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_convergents, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_periodic_cf_sqrt, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pell_fundamental_solution, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_generalized_cf_eval, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cf_e, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cf_pi_terms, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gauss_map_orbit, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_khinchin_estimate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_levy_constant_estimate, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_exact__polynomial.rs b/bindings/python/src/generated/m_exact__polynomial.rs new file mode 100644 index 0000000..93f7fda --- /dev/null +++ b/bindings/python/src/generated/m_exact__polynomial.rs @@ -0,0 +1,109 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Product of two polynomials through the FFT: transform, multiply +/// pointwise, transform back. +/// +/// Mathematically identical to `Poly::mul`, and asymptotically faster, +/// at the cost of rounding on the order of `eps * n * max|a| * max|b|`. +/// +/// Rust: `exact::polynomial::polynomial_multiply_fft` +#[pyfunction] +#[pyo3(name = "polynomial_multiply_fft", signature = (a, b))] +pub fn pyfn_polynomial_multiply_fft(a: crate::generated::types::PyPolyArg, b: crate::generated::types::PyPolyArg) -> PyResult { + let a = a.0; + let b = b.0; + let __r = crate::runtime::guard(|| rust_physics_engine::exact::polynomial::polynomial_multiply_fft(&a, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPoly { inner: __v }) +} + +/// The Bernstein basis polynomial `B_{i,n}(t) = C(n, i) t^i (1 - t)^(n - i)`. +/// +/// Returns `0.0` when `i > n`. +/// +/// Rust: `exact::polynomial::bernstein_basis` +#[pyfunction] +#[pyo3(name = "bernstein_basis", signature = (n, i, t))] +pub fn pyfn_bernstein_basis(n: usize, i: usize, t: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::polynomial::bernstein_basis(n, i, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Bernstein coefficients of `p` on `[a, b]`. +/// +/// The returned `w` of length `deg(p) + 1` satisfies +/// `p(a + (b - a) t) = sum_i w[i] * bernstein_basis(n, i, t)` for all `t`, +/// which is the control polygon of `p` viewed as a Bezier curve. +/// +/// Panics: +/// Panics unless `a < b`. +/// +/// Rust: `exact::polynomial::to_bernstein` +#[pyfunction] +#[pyo3(name = "to_bernstein", signature = (p, a, b))] +pub fn pyfn_to_bernstein<'py>(py: Python<'py>, p: crate::generated::types::PyPolyArg, a: f64, b: f64) -> PyResult> { + let p = p.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::exact::polynomial::to_bernstein(&p, a, b))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Elementary symmetric functions from power sums, by Newton's identities. +/// +/// Given `p_1 .. p_n` in `power_sums`, returns `e_0 .. e_n` (so the result +/// is one longer, and starts at `e_0 = 1`), using +/// `k e_k = sum_{i=1}^{k} (-1)^(i-1) e_{k-i} p_i`. +/// +/// Rust: `exact::polynomial::newton_identities` +#[pyfunction] +#[pyo3(name = "newton_identities", signature = (power_sums))] +pub fn pyfn_newton_identities<'py>(py: Python<'py>, power_sums: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::exact::polynomial::newton_identities(&power_sums))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Coefficients of the monic polynomial with the given complex roots, low +/// degree first (Vieta's formulas). +/// +/// Entry `n - k` is `(-1)^k e_k`, the signed `k`-th elementary symmetric +/// function of the roots; the leading entry is `1`. +/// +/// Rust: `exact::polynomial::vieta` +#[pyfunction] +#[pyo3(name = "vieta", signature = (roots))] +pub fn pyfn_vieta<'py>(py: Python<'py>, roots: Vec) -> PyResult>> { + let roots = roots.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::exact::polynomial::vieta(&roots)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_polynomial_multiply_fft, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bernstein_basis, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_to_bernstein, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_newton_identities, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_vieta, m)?)?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_exact__rational.rs b/bindings/python/src/generated/m_exact__rational.rs new file mode 100644 index 0000000..7438ee9 --- /dev/null +++ b/bindings/python/src/generated/m_exact__rational.rs @@ -0,0 +1,532 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// The Farey sequence F_n: every reduced fraction in `[0, 1]` with +/// denominator at most `n`, in ascending order. +/// +/// Panics: +/// Panics if `n` is zero. +/// +/// Rust: `exact::rational::farey_sequence` +#[pyfunction] +#[pyo3(name = "farey_sequence", signature = (n))] +pub fn pyfn_farey_sequence<'py>(py: Python<'py>, n: u64) -> PyResult>> { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::rational::farey_sequence(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| -> PyResult> { Ok(crate::runtime::coerce::rational_out(py, &__x)?) }).collect::>>()?) +} + +/// The path from the Stern-Brocot root `1/1` down to `r`, as a sequence of +/// branch choices: `true` for the right (larger) child, `false` for the +/// left. +/// +/// The root itself has an empty path. Only positive rationals have one. +/// +/// Panics: +/// Panics unless `r` is strictly positive. +/// +/// Rust: `exact::rational::stern_brocot_path` +#[pyfunction] +#[pyo3(name = "stern_brocot_path", signature = (r))] +pub fn pyfn_stern_brocot_path<'py>(py: Python<'py>, r: crate::runtime::coerce::RationalArg) -> PyResult> { + let r = r.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::exact::rational::stern_brocot_path(&r))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Every continued-fraction convergent of `x` with denominator at most +/// `max_den`, in increasing order of denominator. +/// +/// The last element is the best rational approximation to `x` under that +/// bound, in the strong sense that no fraction with a smaller denominator +/// is closer. +/// +/// Panics: +/// Panics if `max_den` is zero or `x` is not finite. +/// +/// Rust: `exact::rational::best_rational_approximations` +#[pyfunction] +#[pyo3(name = "best_rational_approximations", signature = (x, max_den))] +pub fn pyfn_best_rational_approximations<'py>(py: Python<'py>, x: f64, max_den: u64) -> PyResult>> { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::rational::best_rational_approximations(x, max_den)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| -> PyResult> { Ok(crate::runtime::coerce::rational_out(py, &__x)?) }).collect::>>()?) +} + +/// Solve `A x = b` exactly for rational data, by clearing denominators and +/// running Bareiss fraction-free elimination. +/// +/// Returns `None` if the matrix is not square, the shapes disagree, or the +/// system is singular. +/// +/// Rust: `exact::rational::solve_exact_rational` +#[pyfunction] +#[pyo3(name = "solve_exact_rational", signature = (a, b))] +pub fn pyfn_solve_exact_rational<'py>(py: Python<'py>, a: Vec>, b: Vec) -> PyResult>>> { + let a = a.into_iter().map(|__e| __e.into_iter().map(|__e| __e.0).collect::>()).collect::>(); + let b = b.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::exact::rational::solve_exact_rational(&a, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(match __v { Some(__x) => Some(__x.into_iter().map(|__x| -> PyResult> { Ok(crate::runtime::coerce::rational_out(py, &__x)?) }).collect::>>()?), None => None }) +} + +/// Solve `A x = b` exactly for an `f64` system. +/// +/// Each coefficient is converted to the rational it exactly equals — every +/// finite `f64` is dyadic — so the result is the exact solution of the +/// system as stored. Where the `f64` inputs are themselves roundings of +/// intended values, use `solve_exact_rational` to keep those values +/// exact instead. +/// +/// Returns `None` for a non-square or singular system, mismatched shapes, +/// or any non-finite entry. +/// +/// Rust: `exact::rational::solve_exact` +#[pyfunction] +#[pyo3(name = "solve_exact", signature = (a, b))] +pub fn pyfn_solve_exact<'py>(py: Python<'py>, a: crate::generated::types::PyMatrixArg, b: Vec) -> PyResult>>> { + let a = a.0; + let __r = crate::runtime::guard(|| rust_physics_engine::exact::rational::solve_exact(&a, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(match __v { Some(__x) => Some(__x.into_iter().map(|__x| -> PyResult> { Ok(crate::runtime::coerce::rational_out(py, &__x)?) }).collect::>>()?), None => None }) +} + +/// The exact determinant of a rational matrix, via Bareiss on the +/// denominator-cleared integer matrix. +/// +/// Panics: +/// Panics if the matrix is not square. +/// +/// Rust: `exact::rational::determinant_exact` +#[pyfunction] +#[pyo3(name = "determinant_exact", signature = (a))] +pub fn pyfn_determinant_exact<'py>(py: Python<'py>, a: Vec>) -> PyResult> { + let a = a.into_iter().map(|__e| __e.into_iter().map(|__e| __e.0).collect::>()).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::exact::rational::determinant_exact(&a)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::rational_out(py, &__v)?) +} + +/// The exact inverse of the `n x n` Hilbert matrix `H_ij = 1/(i+j+1)`. +/// +/// Uses the closed form +/// `(-1)^(i+j) (i+j+1) C(n+i, n-j-1) C(n+j, n-i-1) C(i+j, i)^2`, +/// whose entries are all integers. +/// +/// Panics: +/// Panics if `n` is zero. +/// +/// Rust: `exact::rational::hilbert_matrix_inverse_exact` +#[pyfunction] +#[pyo3(name = "hilbert_matrix_inverse_exact", signature = (n))] +pub fn pyfn_hilbert_matrix_inverse_exact<'py>(py: Python<'py>, n: usize) -> PyResult>>> { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::rational::hilbert_matrix_inverse_exact(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| -> PyResult>> { Ok(__x.into_iter().map(|__x| -> PyResult> { Ok(crate::runtime::coerce::rational_out(py, &__x)?) }).collect::>>()?) }).collect::>>()?) +} + +/// The `n x n` Hilbert matrix as exact rationals. +/// +/// Panics: +/// Panics if `n` is zero. +/// +/// Rust: `exact::rational::hilbert_matrix_exact` +#[pyfunction] +#[pyo3(name = "hilbert_matrix_exact", signature = (n))] +pub fn pyfn_hilbert_matrix_exact<'py>(py: Python<'py>, n: usize) -> PyResult>>> { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::rational::hilbert_matrix_exact(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| -> PyResult>> { Ok(__x.into_iter().map(|__x| -> PyResult> { Ok(crate::runtime::coerce::rational_out(py, &__x)?) }).collect::>>()?) }).collect::>>()?) +} + +/// The rational `n/d`, or `None` when `d` is zero. +/// +/// Rust: `exact::rational::Rational::new` +#[pyfunction] +#[pyo3(name = "new", signature = (n, d))] +pub fn pyfn_rational_new<'py>(py: Python<'py>, n: crate::runtime::coerce::BigIntArg, d: crate::runtime::coerce::BigIntArg) -> PyResult>> { + let n = n.0; + let d = d.0; + let __r = crate::runtime::guard(|| rust_physics_engine::exact::rational::Rational::new(n, d)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(match __v { Some(__x) => Some(crate::runtime::coerce::rational_out(py, &__x)?), None => None }) +} + +/// The rational `n/d` from machine integers. +/// +/// Panics: +/// Panics if `d` is zero. +/// +/// Rust: `exact::rational::Rational::from_i64` +#[pyfunction] +#[pyo3(name = "from_i64", signature = (n, d))] +pub fn pyfn_rational_from_i64<'py>(py: Python<'py>, n: i64, d: i64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::rational::Rational::from_i64(n, d)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::rational_out(py, &__v)?) +} + +/// The integer `n` as a rational. +/// +/// Rust: `exact::rational::Rational::from_int` +#[pyfunction] +#[pyo3(name = "from_int", signature = (n))] +pub fn pyfn_rational_from_int<'py>(py: Python<'py>, n: crate::runtime::coerce::BigIntArg) -> PyResult> { + let n = n.0; + let __r = crate::runtime::guard(|| rust_physics_engine::exact::rational::Rational::from_int(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::rational_out(py, &__v)?) +} + +/// +/// Rust: `exact::rational::Rational::zero` +#[pyfunction] +#[pyo3(name = "zero", signature = ())] +pub fn pyfn_rational_zero<'py>(py: Python<'py>) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::rational::Rational::zero()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::rational_out(py, &__v)?) +} + +/// +/// Rust: `exact::rational::Rational::one` +#[pyfunction] +#[pyo3(name = "one", signature = ())] +pub fn pyfn_rational_one<'py>(py: Python<'py>) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::rational::Rational::one()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::rational_out(py, &__v)?) +} + +/// The exact value of an IEEE-754 double, or `None` for NaN and the +/// infinities. +/// +/// Every finite `f64` is a dyadic rational `m * 2^e`, so the reduced +/// denominator is always a power of two. +/// +/// Rust: `exact::rational::Rational::from_f64_exact` +#[pyfunction] +#[pyo3(name = "from_f64_exact", signature = (x))] +pub fn pyfn_rational_from_f64_exact<'py>(py: Python<'py>, x: f64) -> PyResult>> { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::rational::Rational::from_f64_exact(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(match __v { Some(__x) => Some(crate::runtime::coerce::rational_out(py, &__x)?), None => None }) +} + +/// The best rational approximation to `x` with denominator at most +/// `max_den`, found by walking the continued fraction (equivalently, +/// descending the Stern-Brocot tree). +/// +/// Panics: +/// Panics if `max_den` is zero or `x` is not finite. +/// +/// Rust: `exact::rational::Rational::from_f64_approx` +#[pyfunction] +#[pyo3(name = "from_f64_approx", signature = (x, max_den))] +pub fn pyfn_rational_from_f64_approx<'py>(py: Python<'py>, x: f64, max_den: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::rational::Rational::from_f64_approx(x, max_den)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::rational_out(py, &__v)?) +} + +/// +/// Rust: `exact::rational::Rational::is_zero` +#[pyfunction] +#[pyo3(name = "is_zero", signature = (q))] +pub fn pyfn_rational_is_zero(q: crate::runtime::coerce::RationalArg) -> PyResult { + let q = q.0; + let __r = crate::runtime::guard(|| q.is_zero()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// +/// Rust: `exact::rational::Rational::is_negative` +#[pyfunction] +#[pyo3(name = "is_negative", signature = (q))] +pub fn pyfn_rational_is_negative(q: crate::runtime::coerce::RationalArg) -> PyResult { + let q = q.0; + let __r = crate::runtime::guard(|| q.is_negative()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// +/// Rust: `exact::rational::Rational::is_integer` +#[pyfunction] +#[pyo3(name = "is_integer", signature = (q))] +pub fn pyfn_rational_is_integer(q: crate::runtime::coerce::RationalArg) -> PyResult { + let q = q.0; + let __r = crate::runtime::guard(|| q.is_integer()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// +/// Rust: `exact::rational::Rational::abs` +#[pyfunction] +#[pyo3(name = "abs", signature = (q))] +pub fn pyfn_rational_abs<'py>(py: Python<'py>, q: crate::runtime::coerce::RationalArg) -> PyResult> { + let q = q.0; + let __r = crate::runtime::guard(|| q.abs()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::rational_out(py, &__v)?) +} + +/// +/// Rust: `exact::rational::Rational::neg` +#[pyfunction] +#[pyo3(name = "neg", signature = (q))] +pub fn pyfn_rational_neg<'py>(py: Python<'py>, q: crate::runtime::coerce::RationalArg) -> PyResult> { + let q = q.0; + let __r = crate::runtime::guard(|| q.neg()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::rational_out(py, &__v)?) +} + +/// The reciprocal, or `None` for zero. +/// +/// Rust: `exact::rational::Rational::recip` +#[pyfunction] +#[pyo3(name = "recip", signature = (q))] +pub fn pyfn_rational_recip<'py>(py: Python<'py>, q: crate::runtime::coerce::RationalArg) -> PyResult>> { + let q = q.0; + let __r = crate::runtime::guard(|| q.recip()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(match __v { Some(__x) => Some(crate::runtime::coerce::rational_out(py, &__x)?), None => None }) +} + +/// +/// Rust: `exact::rational::Rational::add` +#[pyfunction] +#[pyo3(name = "add", signature = (q, other))] +pub fn pyfn_rational_add<'py>(py: Python<'py>, q: crate::runtime::coerce::RationalArg, other: crate::runtime::coerce::RationalArg) -> PyResult> { + let q = q.0; + let other = other.0; + let __r = crate::runtime::guard(|| q.add(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::rational_out(py, &__v)?) +} + +/// +/// Rust: `exact::rational::Rational::sub` +#[pyfunction] +#[pyo3(name = "sub", signature = (q, other))] +pub fn pyfn_rational_sub<'py>(py: Python<'py>, q: crate::runtime::coerce::RationalArg, other: crate::runtime::coerce::RationalArg) -> PyResult> { + let q = q.0; + let other = other.0; + let __r = crate::runtime::guard(|| q.sub(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::rational_out(py, &__v)?) +} + +/// +/// Rust: `exact::rational::Rational::mul` +#[pyfunction] +#[pyo3(name = "mul", signature = (q, other))] +pub fn pyfn_rational_mul<'py>(py: Python<'py>, q: crate::runtime::coerce::RationalArg, other: crate::runtime::coerce::RationalArg) -> PyResult> { + let q = q.0; + let other = other.0; + let __r = crate::runtime::guard(|| q.mul(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::rational_out(py, &__v)?) +} + +/// Quotient, or `None` when `other` is zero. +/// +/// Rust: `exact::rational::Rational::div` +#[pyfunction] +#[pyo3(name = "div", signature = (q, other))] +pub fn pyfn_rational_div<'py>(py: Python<'py>, q: crate::runtime::coerce::RationalArg, other: crate::runtime::coerce::RationalArg) -> PyResult>> { + let q = q.0; + let other = other.0; + let __r = crate::runtime::guard(|| q.div(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(match __v { Some(__x) => Some(crate::runtime::coerce::rational_out(py, &__x)?), None => None }) +} + +/// `self` raised to a signed integer power. +/// +/// Panics: +/// Panics when raising zero to a negative power. +/// +/// Rust: `exact::rational::Rational::pow` +#[pyfunction] +#[pyo3(name = "pow", signature = (q, e))] +pub fn pyfn_rational_pow<'py>(py: Python<'py>, q: crate::runtime::coerce::RationalArg, e: i64) -> PyResult> { + let q = q.0; + let __r = crate::runtime::guard(|| q.pow(e)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::rational_out(py, &__v)?) +} + +/// Nearest `f64`. +/// +/// When both parts are individually representable the quotient is a +/// single correctly-rounded division. Otherwise -- a tiny value like +/// `1e-300` has a denominator of about `2^1049`, far past the `f64` +/// range even though the quotient is fine -- the numerator is scaled +/// by a power of two first so the quotient itself lands in range, and +/// the scale is undone afterwards. +/// +/// Rust: `exact::rational::Rational::to_f64` +#[pyfunction] +#[pyo3(name = "to_f64", signature = (q))] +pub fn pyfn_rational_to_f64(q: crate::runtime::coerce::RationalArg) -> PyResult { + let q = q.0; + let __r = crate::runtime::guard(|| q.to_f64()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Greatest integer not exceeding the value. +/// +/// Rust: `exact::rational::Rational::floor` +#[pyfunction] +#[pyo3(name = "floor", signature = (q))] +pub fn pyfn_rational_floor<'py>(py: Python<'py>, q: crate::runtime::coerce::RationalArg) -> PyResult> { + let q = q.0; + let __r = crate::runtime::guard(|| q.floor()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// Least integer not below the value. +/// +/// Rust: `exact::rational::Rational::ceil` +#[pyfunction] +#[pyo3(name = "ceil", signature = (q))] +pub fn pyfn_rational_ceil<'py>(py: Python<'py>, q: crate::runtime::coerce::RationalArg) -> PyResult> { + let q = q.0; + let __r = crate::runtime::guard(|| q.ceil()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// Nearest integer, with halves rounded away from zero. +/// +/// Rust: `exact::rational::Rational::round` +#[pyfunction] +#[pyo3(name = "round", signature = (q))] +pub fn pyfn_rational_round<'py>(py: Python<'py>, q: crate::runtime::coerce::RationalArg) -> PyResult> { + let q = q.0; + let __r = crate::runtime::guard(|| q.round()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// The fractional part `self - floor(self)`, always in `[0, 1)`. +/// +/// Rust: `exact::rational::Rational::fract` +#[pyfunction] +#[pyo3(name = "fract", signature = (q))] +pub fn pyfn_rational_fract<'py>(py: Python<'py>, q: crate::runtime::coerce::RationalArg) -> PyResult> { + let q = q.0; + let __r = crate::runtime::guard(|| q.fract()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::rational_out(py, &__v)?) +} + +/// The continued-fraction expansion `[a0; a1, a2, ...]`. +/// +/// The expansion is finite for every rational and, apart from the +/// integer case, never ends in a 1, which makes it canonical. +/// +/// Rust: `exact::rational::Rational::to_continued_fraction` +#[pyfunction] +#[pyo3(name = "to_continued_fraction", signature = (q))] +pub fn pyfn_rational_to_continued_fraction<'py>(py: Python<'py>, q: crate::runtime::coerce::RationalArg) -> PyResult>> { + let q = q.0; + let __r = crate::runtime::guard(|| q.to_continued_fraction()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| -> PyResult> { Ok(crate::runtime::coerce::bigint_out(py, &__x)?) }).collect::>>()?) +} + +/// Rebuild a rational from a continued fraction. +/// +/// Errors: +/// Returns `GeomError::Empty` for an empty expansion, and +/// `GeomError::InvalidArgument` if a non-leading term is not +/// positive, which cannot arise from `Self::to_continued_fraction`. +/// +/// Rust: `exact::rational::Rational::from_continued_fraction` +#[pyfunction] +#[pyo3(name = "from_continued_fraction", signature = (cf))] +pub fn pyfn_rational_from_continued_fraction<'py>(py: Python<'py>, cf: Vec) -> PyResult> { + let cf = cf.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::exact::rational::Rational::from_continued_fraction(&cf)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::runtime::coerce::rational_out(py, &__v)?) +} + +/// The mediant `(a.num + b.num) / (a.den + b.den)`. +/// +/// The mediant of two fractions always lies strictly between them, the +/// property the Stern-Brocot tree and Farey sequences are built on. +/// +/// Rust: `exact::rational::Rational::mediant` +#[pyfunction] +#[pyo3(name = "mediant", signature = (a, b))] +pub fn pyfn_rational_mediant<'py>(py: Python<'py>, a: crate::runtime::coerce::RationalArg, b: crate::runtime::coerce::RationalArg) -> PyResult> { + let a = a.0; + let b = b.0; + let __r = crate::runtime::guard(|| rust_physics_engine::exact::rational::Rational::mediant(&a, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::rational_out(py, &__v)?) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_farey_sequence, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_stern_brocot_path, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_best_rational_approximations, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_solve_exact_rational, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_solve_exact, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_determinant_exact, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hilbert_matrix_inverse_exact, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hilbert_matrix_exact, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rational_new, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rational_from_i64, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rational_from_int, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rational_zero, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rational_one, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rational_from_f64_exact, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rational_from_f64_approx, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rational_is_zero, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rational_is_negative, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rational_is_integer, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rational_abs, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rational_neg, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rational_recip, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rational_add, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rational_sub, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rational_mul, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rational_div, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rational_pow, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rational_to_f64, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rational_floor, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rational_ceil, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rational_round, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rational_fract, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rational_to_continued_fraction, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rational_from_continued_fraction, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rational_mediant, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_exact__symbolic.rs b/bindings/python/src/generated/m_exact__symbolic.rs new file mode 100644 index 0000000..e27e603 --- /dev/null +++ b/bindings/python/src/generated/m_exact__symbolic.rs @@ -0,0 +1,82 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// The Hessian matrix of second partial derivatives, simplified. +/// +/// Rust: `exact::symbolic::hessian` +#[pyfunction] +#[pyo3(name = "hessian", signature = (e, vars))] +pub fn pyfn_hessian(e: crate::generated::types::PyExpr, vars: Vec) -> PyResult>> { + let e = e.inner; + let vars__b: Vec<&str> = vars.iter().map(|__b| (*__b).as_str()).collect(); + let __r = crate::runtime::guard(|| rust_physics_engine::exact::symbolic::hessian(&e, &vars__b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.into_iter().map(|__x| crate::generated::types::PyExpr { inner: __x }).collect::>()).collect::>()) +} + +/// Real roots of `e` in a bracket, by scanning for sign changes and +/// bisecting each one. +/// +/// Only sign-changing roots are found; a root of even multiplicity, where +/// the curve touches the axis without crossing, is invisible to this +/// method. +/// +/// Panics: +/// Panics if the bracket is empty or reversed. +/// +/// Rust: `exact::symbolic::solve_univariate_numeric` +#[pyfunction] +#[pyo3(name = "solve_univariate_numeric", signature = (e, var, bracket))] +pub fn pyfn_solve_univariate_numeric<'py>(py: Python<'py>, e: crate::generated::types::PyExpr, var: String, bracket: (f64, f64)) -> PyResult> { + let e = e.inner; + let bracket = (bracket.0, bracket.1); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::exact::symbolic::solve_univariate_numeric(&e, &var, bracket))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Critical points of `e` in a range: the points where the derivative +/// changes sign, paired with the value of `e` there. +/// +/// `n` is unused beyond selecting the search resolution and is kept for +/// signature compatibility. +/// +/// Panics: +/// Panics if the range is empty or reversed. +/// +/// Rust: `exact::symbolic::critical_points` +#[pyfunction] +#[pyo3(name = "critical_points", signature = (e, var, range, n))] +pub fn pyfn_critical_points<'py>(py: Python<'py>, e: crate::generated::types::PyExpr, var: String, range: (f64, f64), n: usize) -> PyResult> { + let e = e.inner; + let range = (range.0, range.1); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::exact::symbolic::critical_points(&e, &var, range, n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_hessian, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_solve_univariate_numeric, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_critical_points, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_fem.rs b/bindings/python/src/generated/m_fem.rs new file mode 100644 index 0000000..c02bbf8 --- /dev/null +++ b/bindings/python/src/generated/m_fem.rs @@ -0,0 +1,22 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + Ok(()) +} diff --git a/bindings/python/src/generated/m_fem__fdtd.rs b/bindings/python/src/generated/m_fem__fdtd.rs new file mode 100644 index 0000000..40f4b52 --- /dev/null +++ b/bindings/python/src/generated/m_fem__fdtd.rs @@ -0,0 +1,265 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Whether a set of parameters satisfies the one-dimensional Courant +/// condition `c dt <= dx`. +/// +/// Equality is admissible and is in fact the best possible choice in one +/// dimension: see the module note on the magic time step. +/// +/// Rust: `fem::fdtd::fdtd_courant_check` +#[pyfunction] +#[pyo3(name = "fdtd_courant_check", signature = (dx, dt, c))] +pub fn pyfn_fdtd_courant_check(dx: f64, dt: f64, c: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fem::fdtd::fdtd_courant_check(dx, dt, c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The two-dimensional Courant condition, `c dt <= 1 / sqrt(1/dx^2 + +/// 1/dy^2)`. +/// +/// On a square grid that is `dx / (c sqrt(2))`, and unlike the +/// one-dimensional case the bound is not a good place to sit: the +/// dispersion error at the limit vanishes along the diagonals and is +/// worst along the axes, so no single Courant number is exact for every +/// direction. +/// +/// Rust: `fem::fdtd::fdtd_courant_check_2d` +#[pyfunction] +#[pyo3(name = "fdtd_courant_check_2d", signature = (dx, dy, dt, c))] +pub fn pyfn_fdtd_courant_check_2d(dx: f64, dy: f64, dt: f64, c: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fem::fdtd::fdtd_courant_check_2d(dx, dy, dt, c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Marches the one-dimensional Yee scheme. +/// +/// `eps_r` gives the relative permittivity of each cell, `courant` is +/// `c dt / dx` in vacuum, and `source` is added to `E` at `source_cell` +/// at every step -- a soft source, which a wave passes through rather +/// than reflecting off, unlike overwriting the cell. +/// +/// Errors: +/// +/// `SolveError::InvalidArgument` for a grid shorter than three cells, +/// a non-positive or non-finite permittivity, a source cell outside the +/// grid, a Courant number outside `(0, 1]`, or a Courant number above +/// the limit the grid's *fastest* medium sets -- past the limit the +/// scheme is unconditionally unstable and running it would produce +/// numbers rather than an answer. +/// +/// Rust: `fem::fdtd::fdtd_1d` +#[pyfunction] +#[pyo3(name = "fdtd_1d", signature = (eps_r, source, source_cell, courant, steps, boundary))] +pub fn pyfn_fdtd_1d(eps_r: Vec, source: pyo3::Py, source_cell: usize, courant: f64, steps: usize, boundary: crate::generated::types::PyBoundary1d) -> PyResult { + let __cb_source = std::rc::Rc::new(crate::runtime::Callback::new(source)); + let source = { let __cb = __cb_source.clone(); move |__a0: usize| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let boundary = boundary.to_rust(); + let __r = crate::runtime::guard(|| rust_physics_engine::fem::fdtd::fdtd_1d(&eps_r, &source, source_cell, courant, steps, boundary)); + crate::runtime::callback::check(&[&__cb_source], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(crate::generated::types::PyFdtd1d { inner: __v }) +} + +/// The photonic band gaps of an infinite `a`/`b` bilayer stack, found +/// from the Bloch dispersion relation. +/// +/// A period of the stack has a transfer matrix, and Bloch's theorem says +/// the propagating states are those whose transfer matrix has unit +/// modulus eigenvalues. For a two-layer period that reduces to +/// +/// +/// with `k_i = omega n_i / c`. A frequency for which the right-hand side +/// exceeds one in magnitude has no real `K`: nothing propagates, and +/// that is a gap. The prefactor `(n_a/n_b + n_b/n_a)/2` is at least one +/// with equality only when the two indices agree, which is the whole +/// reason a gap exists at all -- a homogeneous "stack" has none. +/// +/// Frequencies are angular and the speed of light is taken as one, so a +/// frequency is really `omega L / c` in disguise; scaling every +/// thickness by a factor scales every gap edge by its reciprocal. +/// +/// Returns the gaps below `omega_max` as `(low, high)` pairs, ascending, +/// with the edges refined by bisection rather than left at the sampling +/// resolution. +/// +/// Errors: +/// +/// `SolveError::InvalidArgument` for non-positive permittivities or +/// thicknesses, a non-positive frequency ceiling, or fewer than two +/// samples. +/// +/// Rust: `fem::fdtd::photonic_crystal_bandgap_1d` +#[pyfunction] +#[pyo3(name = "photonic_crystal_bandgap_1d", signature = (eps_a, eps_b, d_a, d_b, omega_max, samples))] +pub fn pyfn_photonic_crystal_bandgap_1d<'py>(py: Python<'py>, eps_a: f64, eps_b: f64, d_a: f64, d_b: f64, omega_max: f64, samples: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fem::fdtd::photonic_crystal_bandgap_1d(eps_a, eps_b, d_a, d_b, omega_max, samples))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Marches the two-dimensional transverse-magnetic Yee scheme with a +/// Berenger split-field perfectly matched layer. +/// +/// `eps_r` is row-major over `nx * ny` cells. `source` gives the value +/// added softly at `source_pos` on each step, exactly as in +/// `fdtd_1d` -- a continuous sinusoid at `f` cycles per step is +/// `|s| (TAU * f * s as f64).sin()`, and a pulse is anything with +/// compact support. Taking the waveform rather than a frequency is what +/// lets a caller switch the drive off, which is the only way to measure +/// what a boundary reflects: with a source still running, the field near +/// it is the source's own and says nothing about the layer. +/// +/// Ramp a continuous drive on rather than switching it: a step +/// broadcasts across the whole band the grid can carry, and none of it +/// is what was asked for. +/// +/// Why the field is split: +/// +/// A lossy layer absorbs, but an ordinary lossy layer also *reflects*, +/// because its impedance differs from the vacuum it adjoins. Berenger's +/// construction splits `E_z` into the two parts that the two spatial +/// derivatives feed, and damps each with the loss belonging to its own +/// axis. The resulting medium is matched at every angle and every +/// frequency, which no single isotropic conductivity can be: what is +/// left is only the reflection from grading the profile over a finite +/// depth, and that is what the `reflection` target controls. +/// +/// The layer is backed by a conductor. That is not a flaw -- anything +/// that reaches the backing has crossed the graded layer twice and comes +/// back attenuated by the round-trip factor the grading was designed +/// for. +/// +/// `pml` gives the depth on each axis separately, `(x, y)`. A depth of +/// zero on an axis leaves plain conducting walls there, which is what a +/// waveguide wants: absorbing its side walls would stop it being a +/// waveguide, while absorbing its ends stops the switch-on transient +/// rattling around forever and swamping the field being measured. +/// +/// Errors: +/// +/// `SolveError::InvalidArgument` for a grid smaller than the layers +/// need, a permittivity array of the wrong length or with a non-positive +/// entry, a source outside the grid, a non-finite frequency, a +/// reflection target outside `(0, 1)`, or a Courant number above the +/// two-dimensional limit for the fastest medium present. +/// +/// Rust: `fem::fdtd::fdtd_2d_tm` +#[pyfunction] +#[pyo3(name = "fdtd_2d_tm", signature = (eps_r, source_pos, source, nx, ny, steps, pml, courant, reflection))] +pub fn pyfn_fdtd_2d_tm(eps_r: Vec, source_pos: (usize, usize), source: pyo3::Py, nx: usize, ny: usize, steps: usize, pml: (usize, usize), courant: f64, reflection: f64) -> PyResult { + let source_pos = (source_pos.0, source_pos.1); + let __cb_source = std::rc::Rc::new(crate::runtime::Callback::new(source)); + let source = { let __cb = __cb_source.clone(); move |__a0: usize| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let pml = (pml.0, pml.1); + let __r = crate::runtime::guard(|| rust_physics_engine::fem::fdtd::fdtd_2d_tm(&eps_r, source_pos, &source, nx, ny, steps, pml, courant, reflection)); + crate::runtime::callback::check(&[&__cb_source], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(crate::generated::types::PyFdtd2d { inner: __v }) +} + +/// Infers a parallel-plate waveguide's cutoff frequency from the +/// evanescent decay it shows when driven below that cutoff. +/// +/// The guide is `width` cells between conducting plates, driven in its +/// `mode`-th transverse pattern at angular frequency `omega` in radians +/// per unit *time*, with the cell size and the speed of light both one +/// -- so a step advances the phase by `omega * S`, not by `omega`. +/// Below cutoff nothing propagates: the field falls off as +/// `exp(-alpha x)`, and measuring `alpha` down the guide gives the +/// cutoff back. +/// +/// Which cutoff comes back: +/// +/// Not the textbook `m pi c / a`. The grid has its own dispersion +/// relation, +/// +/// +/// and an evanescent `k_x = i alpha` turns the first term on the right +/// into `-sinh^2(alpha / 2)`. Solving for where `alpha` vanishes gives +/// the *numerical* cutoff +/// +/// +/// which is what this returns and what the simulation actually has. It +/// approaches the continuum value as the guide is resolved more finely, +/// from below -- the grid is always a little slow -- and the difference +/// is second order in the cell size. Reporting the continuum figure +/// would be reporting what the answer ought to be rather than what it +/// is. +/// +/// Errors: +/// +/// `SolveError::InvalidArgument` for a mode outside `1..width`, a +/// guide too short to measure a decay in, or a frequency at or above the +/// numerical cutoff, where there is no decay to measure; +/// `SolveError::NoConvergence` if the measured profile is not a clean +/// exponential, which is the honest answer when a mode is close to the +/// grid's resolution limit: three half-waves across ten cells decays +/// within a couple of cells, leaving too little of the profile above the +/// numerical floor to fit a slope to. Widening the guide fixes it. +/// +/// Rust: `fem::fdtd::waveguide_cutoff_check_fdtd` +#[pyfunction] +#[pyo3(name = "waveguide_cutoff_check_fdtd", signature = (width, length, mode, omega, courant, steps))] +pub fn pyfn_waveguide_cutoff_check_fdtd(width: usize, length: usize, mode: usize, omega: f64, courant: f64, steps: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fem::fdtd::waveguide_cutoff_check_fdtd(width, length, mode, omega, courant, steps)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// The numerical cutoff a parallel-plate guide of this width has on a +/// grid at this Courant number, `(2/S) arcsin(S sin(k_y/2))`. +/// +/// The continuum answer is `m pi / a`; this is what the grid actually +/// gives, and it is always the smaller of the two. +/// +/// Errors: +/// +/// `SolveError::InvalidArgument` for a mode outside `1..width` or a +/// Courant number outside the plane limit. +/// +/// Rust: `fem::fdtd::waveguide_cutoff_numerical` +#[pyfunction] +#[pyo3(name = "waveguide_cutoff_numerical", signature = (width, mode, courant))] +pub fn pyfn_waveguide_cutoff_numerical(width: usize, mode: usize, courant: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fem::fdtd::waveguide_cutoff_numerical(width, mode, courant)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_fdtd_courant_check, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fdtd_courant_check_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fdtd_1d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_photonic_crystal_bandgap_1d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fdtd_2d_tm, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_waveguide_cutoff_check_fdtd, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_waveguide_cutoff_numerical, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_fem__fem1d.rs b/bindings/python/src/generated/m_fem__fem1d.rs new file mode 100644 index 0000000..729cf0b --- /dev/null +++ b/bindings/python/src/generated/m_fem__fem1d.rs @@ -0,0 +1,189 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Solves `-u'' = f` with linear elements on a uniform mesh of `n` +/// elements, returning the `n + 1` nodal values. +/// +/// With exact load integration this is nodally exact -- see the module +/// documentation for why that is a property of the Laplacian rather than +/// of the discretisation. +/// +/// Errors: +/// +/// `SolveError::InvalidArgument` for an empty mesh, a degenerate +/// interval, or non-finite data; `SolveError::Singular` when both ends +/// carry a pure flux condition, which leaves the solution undetermined up +/// to an additive constant. +/// +/// Rust: `fem::fem1d::fem_1d_poisson` +#[pyfunction] +#[pyo3(name = "fem_1d_poisson", signature = (f, a, b, bc, n))] +pub fn pyfn_fem_1d_poisson(f: pyo3::Py, a: f64, b: f64, bc: (crate::generated::types::PyFem1dBc, crate::generated::types::PyFem1dBc), n: usize) -> PyResult> { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let bc = (bc.0.inner, bc.1.inner); + let __r = crate::runtime::guard(|| rust_physics_engine::fem::fem1d::fem_1d_poisson(&f, a, b, bc, n)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// Solves `-(p u')' + q u = f` with linear elements, returning the +/// `n + 1` nodal values. +/// +/// Errors: +/// +/// As `fem_1d_poisson`, and additionally +/// `SolveError::InvalidArgument` if `p` is not positive at a quadrature +/// point. A negative `q` large enough to make the operator indefinite is +/// reported as `SolveError::Singular`. +/// +/// Rust: `fem::fem1d::fem_1d_general` +#[pyfunction] +#[pyo3(name = "fem_1d_general", signature = (p, q, f, a, b, bc, n))] +pub fn pyfn_fem_1d_general(p: pyo3::Py, q: pyo3::Py, f: pyo3::Py, a: f64, b: f64, bc: (crate::generated::types::PyFem1dBc, crate::generated::types::PyFem1dBc), n: usize) -> PyResult> { + let __cb_p = std::rc::Rc::new(crate::runtime::Callback::new(p)); + let p = { let __cb = __cb_p.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __cb_q = std::rc::Rc::new(crate::runtime::Callback::new(q)); + let q = { let __cb = __cb_q.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let bc = (bc.0.inner, bc.1.inner); + let __r = crate::runtime::guard(|| rust_physics_engine::fem::fem1d::fem_1d_general(&p, &q, &f, a, b, bc, n)); + crate::runtime::callback::check(&[&__cb_p, &__cb_q, &__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// Solves `-(p u')' + q u = f` with quadratic elements, returning the +/// `2n + 1` nodal values: element vertices at the even indices and +/// midsides at the odd ones. +/// +/// Errors: +/// +/// As `fem_1d_general`. +/// +/// Rust: `fem::fem1d::fem_1d_quadratic` +#[pyfunction] +#[pyo3(name = "fem_1d_quadratic", signature = (p, q, f, a, b, bc, n))] +pub fn pyfn_fem_1d_quadratic(p: pyo3::Py, q: pyo3::Py, f: pyo3::Py, a: f64, b: f64, bc: (crate::generated::types::PyFem1dBc, crate::generated::types::PyFem1dBc), n: usize) -> PyResult> { + let __cb_p = std::rc::Rc::new(crate::runtime::Callback::new(p)); + let p = { let __cb = __cb_p.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __cb_q = std::rc::Rc::new(crate::runtime::Callback::new(q)); + let q = { let __cb = __cb_q.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let bc = (bc.0.inner, bc.1.inner); + let __r = crate::runtime::guard(|| rust_physics_engine::fem::fem1d::fem_1d_quadratic(&p, &q, &f, a, b, bc, n)); + crate::runtime::callback::check(&[&__cb_p, &__cb_q, &__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// The `L2` norm of the error against an exact solution. +/// +/// Rust: `fem::fem1d::fem_1d_error_l2` +#[pyfunction] +#[pyo3(name = "fem_1d_error_l2", signature = (u_h, u_exact))] +pub fn pyfn_fem_1d_error_l2(u_h: crate::generated::types::PyFem1dSolution, u_exact: pyo3::Py) -> PyResult { + let u_h = u_h.inner; + let __cb_u_exact = std::rc::Rc::new(crate::runtime::Callback::new(u_exact)); + let u_exact = { let __cb = __cb_u_exact.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::fem::fem1d::fem_1d_error_l2(&u_h, &u_exact)); + crate::runtime::callback::check(&[&__cb_u_exact], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The `H1` seminorm of the error: the `L2` norm of the derivative +/// difference alone. +/// +/// For the Poisson problem this is the energy norm, up to the factor the +/// coefficient `p` contributes, and so it is the norm in which the finite +/// element solution is the best approximation available. +/// +/// Rust: `fem::fem1d::fem_1d_error_h1_seminorm` +#[pyfunction] +#[pyo3(name = "fem_1d_error_h1_seminorm", signature = (u_h, du_exact))] +pub fn pyfn_fem_1d_error_h1_seminorm(u_h: crate::generated::types::PyFem1dSolution, du_exact: pyo3::Py) -> PyResult { + let u_h = u_h.inner; + let __cb_du_exact = std::rc::Rc::new(crate::runtime::Callback::new(du_exact)); + let du_exact = { let __cb = __cb_du_exact.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::fem::fem1d::fem_1d_error_h1_seminorm(&u_h, &du_exact)); + crate::runtime::callback::check(&[&__cb_du_exact], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The full `H1` norm of the error, `sqrt(L2^2 + seminorm^2)`. +/// +/// Rust: `fem::fem1d::fem_1d_error_h1` +#[pyfunction] +#[pyo3(name = "fem_1d_error_h1", signature = (u_h, u_exact, du_exact))] +pub fn pyfn_fem_1d_error_h1(u_h: crate::generated::types::PyFem1dSolution, u_exact: pyo3::Py, du_exact: pyo3::Py) -> PyResult { + let u_h = u_h.inner; + let __cb_u_exact = std::rc::Rc::new(crate::runtime::Callback::new(u_exact)); + let u_exact = { let __cb = __cb_u_exact.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __cb_du_exact = std::rc::Rc::new(crate::runtime::Callback::new(du_exact)); + let du_exact = { let __cb = __cb_du_exact.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::fem::fem1d::fem_1d_error_h1(&u_h, &u_exact, &du_exact)); + crate::runtime::callback::check(&[&__cb_u_exact, &__cb_du_exact], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The observed order of convergence: the least-squares slope of +/// `ln(error)` against `ln(h)`. +/// +/// A method converging as `C h^k` returns `k`. Fitting all the points +/// rather than taking the ratio of the last two is deliberate -- a single +/// ratio is a difference of two noisy logarithms and inherits the noise +/// of both. +/// +/// Errors: +/// +/// `SolveError::InvalidArgument` unless there are at least two pairs of +/// matching length, all strictly positive and finite, with at least two +/// distinct spacings. +/// +/// Rust: `fem::fem1d::convergence_rate` +#[pyfunction] +#[pyo3(name = "convergence_rate", signature = (errors, hs))] +pub fn pyfn_convergence_rate<'py>(py: Python<'py>, errors: Vec, hs: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fem::fem1d::convergence_rate(&errors, &hs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_fem_1d_poisson, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fem_1d_general, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fem_1d_quadratic, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fem_1d_error_l2, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fem_1d_error_h1_seminorm, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fem_1d_error_h1, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_convergence_rate, m)?)?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_fem__fem2d.rs b/bindings/python/src/generated/m_fem__fem2d.rs new file mode 100644 index 0000000..eb679d3 --- /dev/null +++ b/bindings/python/src/generated/m_fem__fem2d.rs @@ -0,0 +1,482 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Solves `-div(grad u) = f` on the mesh with the given Dirichlet data. +/// +/// `dirichlet` is consulted at every boundary node; returning `None` +/// leaves that node free, which imposes the natural zero-flux condition +/// there. Returning `None` everywhere leaves the constant in the kernel +/// and is reported as `SolveError::Singular`. +/// +/// The system is symmetric positive definite once the data is applied, so +/// it is solved by Jacobi-preconditioned conjugate gradients. +/// +/// Errors: +/// +/// `SolveError::InvalidArgument` for non-finite data, +/// `SolveError::Singular` if nothing pins the solution, and +/// `SolveError::NoConvergence` if the iteration stalls. +/// +/// Rust: `fem::fem2d::fem_2d_poisson` +#[pyfunction] +#[pyo3(name = "fem_2d_poisson", signature = (mesh, f, dirichlet))] +pub fn pyfn_fem_2d_poisson(mesh: crate::generated::types::PyFemMesh2, f: pyo3::Py, dirichlet: pyo3::Py) -> PyResult> { + let mesh = mesh.inner; + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: rust_physics_engine::math::Vec2| -> f64 { __cb.call::<_, f64>((crate::generated::types::PyVec2 { inner: __a0 },), f64::NAN) } }; + let __cb_dirichlet = std::rc::Rc::new(crate::runtime::Callback::new(dirichlet)); + let dirichlet = { let __cb = __cb_dirichlet.clone(); move |__a0: rust_physics_engine::math::Vec2| -> Option { __cb.call::<_, Option>((crate::generated::types::PyVec2 { inner: __a0 },), None) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::fem::fem2d::fem_2d_poisson(&mesh, &f, &dirichlet)); + crate::runtime::callback::check(&[&__cb_f, &__cb_dirichlet], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// Solves `-div(grad u) + c u = f` on the mesh with Dirichlet data. +/// +/// A positive `c` is a reaction term and keeps the problem coercive; a +/// negative one is the Helmholtz operator `-lap - k^2`, which loses +/// positive definiteness once `k^2` passes the first eigenvalue of the +/// domain. See `fem_2d_helmholtz` for that case, which needs a +/// different solver. +/// +/// Errors: +/// +/// As `fem_2d_poisson`, and `SolveError::NotPositiveDefinite` if the +/// reaction term makes the system indefinite. +/// +/// Rust: `fem::fem2d::fem_2d_reaction_diffusion` +#[pyfunction] +#[pyo3(name = "fem_2d_reaction_diffusion", signature = (mesh, c, f, dirichlet))] +pub fn pyfn_fem_2d_reaction_diffusion(mesh: crate::generated::types::PyFemMesh2, c: pyo3::Py, f: pyo3::Py, dirichlet: pyo3::Py) -> PyResult> { + let mesh = mesh.inner; + let __cb_c = std::rc::Rc::new(crate::runtime::Callback::new(c)); + let c = { let __cb = __cb_c.clone(); move |__a0: rust_physics_engine::math::Vec2| -> f64 { __cb.call::<_, f64>((crate::generated::types::PyVec2 { inner: __a0 },), f64::NAN) } }; + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: rust_physics_engine::math::Vec2| -> f64 { __cb.call::<_, f64>((crate::generated::types::PyVec2 { inner: __a0 },), f64::NAN) } }; + let __cb_dirichlet = std::rc::Rc::new(crate::runtime::Callback::new(dirichlet)); + let dirichlet = { let __cb = __cb_dirichlet.clone(); move |__a0: rust_physics_engine::math::Vec2| -> Option { __cb.call::<_, Option>((crate::generated::types::PyVec2 { inner: __a0 },), None) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::fem::fem2d::fem_2d_reaction_diffusion(&mesh, &c, &f, &dirichlet)); + crate::runtime::callback::check(&[&__cb_c, &__cb_f, &__cb_dirichlet], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// The assembled stiffness matrix of the Laplacian, with no boundary +/// conditions applied. +/// +/// The off-diagonal entry for an edge is minus half the sum of the +/// cotangents of the two angles opposite it -- the identity that ties the +/// M-matrix property to the Delaunay condition, since a cotangent turns +/// negative exactly when its angle turns obtuse. Every row sums to zero, +/// because the three shape functions of a triangle sum to the constant +/// one and so their gradients sum to zero. +/// +/// Rust: `fem::fem2d::stiffness_matrix` +#[pyfunction] +#[pyo3(name = "stiffness_matrix", signature = (mesh))] +pub fn pyfn_stiffness_matrix(mesh: crate::generated::types::PyFemMesh2) -> PyResult { + let mesh = mesh.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::fem::fem2d::stiffness_matrix(&mesh)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCsrMatrix { inner: __v }) +} + +/// The assembled consistent mass matrix. +/// +/// `A/6` on the diagonal and `A/12` off it, per triangle. Its entries sum +/// to the area of the mesh, since the shape functions form a partition of +/// unity; the *lumped* alternative, which puts each row's total on its +/// diagonal, is what an explicit time integrator wants and is a different +/// matrix with the same total. +/// +/// Rust: `fem::fem2d::mass_matrix` +#[pyfunction] +#[pyo3(name = "mass_matrix", signature = (mesh))] +pub fn pyfn_mass_matrix(mesh: crate::generated::types::PyFemMesh2) -> PyResult { + let mesh = mesh.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::fem::fem2d::mass_matrix(&mesh)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCsrMatrix { inner: __v }) +} + +/// The gradient of a nodal field on one triangle, which is constant +/// there because the field is linear. +/// +/// Returns `None` for an out-of-range triangle index or a mismatched +/// value count. +/// +/// Rust: `fem::fem2d::element_gradient` +#[pyfunction] +#[pyo3(name = "element_gradient", signature = (mesh, values, tri))] +pub fn pyfn_element_gradient(mesh: crate::generated::types::PyFemMesh2, values: Vec, tri: usize) -> PyResult> { + let mesh = mesh.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::fem::fem2d::element_gradient(&mesh, &values, tri)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyVec2 { inner: __x })) +} + +/// The Dirichlet energy `integral |grad u|^2` of a nodal field, computed +/// exactly. +/// +/// It is exact rather than quadrature-limited because the gradient is +/// constant on each triangle, so the integral is a sum of area times a +/// squared length. This is the energy norm the finite element solution +/// minimises, and the quantity that must fall when the mesh is refined. +/// +/// Errors: +/// +/// `SolveError::DimensionMismatch` if the value count does not match +/// the node count. +/// +/// Rust: `fem::fem2d::dirichlet_energy` +#[pyfunction] +#[pyo3(name = "dirichlet_energy", signature = (mesh, values))] +pub fn pyfn_dirichlet_energy<'py>(py: Python<'py>, mesh: crate::generated::types::PyFemMesh2, values: Vec) -> PyResult { + let mesh = mesh.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fem::fem2d::dirichlet_energy(&mesh, &values))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// Evaluates a nodal field at an arbitrary point by locating the +/// containing triangle and interpolating barycentrically. +/// +/// Returns `None` if the point lies outside every triangle, or if the +/// value count does not match the mesh. The search is linear in the +/// triangle count -- there is no spatial index here, so this is for +/// sampling an answer rather than for an inner loop. +/// +/// Rust: `fem::fem2d::interpolate` +#[pyfunction] +#[pyo3(name = "interpolate", signature = (mesh, values, p))] +pub fn pyfn_interpolate<'py>(py: Python<'py>, mesh: crate::generated::types::PyFemMesh2, values: Vec, p: crate::generated::types::PyVec2Arg) -> PyResult> { + let mesh = mesh.inner; + let p = p.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fem::fem2d::interpolate(&mesh, &values, p))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// Solves the Helmholtz problem `-lap u - k^2 u = f` with Dirichlet data. +/// +/// This is the same assembly as `fem_2d_reaction_diffusion` with a +/// negative reaction term, but it needs a different solver and the reason +/// is structural rather than numerical. Once `k^2` passes the first +/// Dirichlet eigenvalue of the domain the operator stops being positive +/// definite, and conjugate gradients -- which is a minimisation method -- +/// has nothing left to minimise. A dense LU factorisation is used +/// instead, which costs `O(n^3)` in the node count and confines this +/// function to modest meshes. +/// +/// At `k^2` exactly equal to an eigenvalue the operator is singular: the +/// homogeneous problem has a nonzero solution, so the inhomogeneous one +/// has either none or a whole line of them. That is resonance, not a +/// numerical accident, and it is reported as `SolveError::Singular`. +/// Approaching an eigenvalue the response grows like the reciprocal of +/// the distance to it. +/// +/// Errors: +/// +/// `SolveError::InvalidArgument` for non-finite data, +/// `SolveError::Singular` at or extremely close to a resonance. +/// +/// Rust: `fem::fem2d::fem_2d_helmholtz` +#[pyfunction] +#[pyo3(name = "fem_2d_helmholtz", signature = (mesh, k, f, dirichlet))] +pub fn pyfn_fem_2d_helmholtz(mesh: crate::generated::types::PyFemMesh2, k: f64, f: pyo3::Py, dirichlet: pyo3::Py) -> PyResult> { + let mesh = mesh.inner; + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: rust_physics_engine::math::Vec2| -> f64 { __cb.call::<_, f64>((crate::generated::types::PyVec2 { inner: __a0 },), f64::NAN) } }; + let __cb_dirichlet = std::rc::Rc::new(crate::runtime::Callback::new(dirichlet)); + let dirichlet = { let __cb = __cb_dirichlet.clone(); move |__a0: rust_physics_engine::math::Vec2| -> Option { __cb.call::<_, Option>((crate::generated::types::PyVec2 { inner: __a0 },), None) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::fem::fem2d::fem_2d_helmholtz(&mesh, k, &f, &dirichlet)); + crate::runtime::callback::check(&[&__cb_f, &__cb_dirichlet], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// The `count` smallest eigenvalues of the Dirichlet Laplacian on the +/// mesh -- the squared frequencies of a drum clamped at its rim. +/// +/// The discrete problem is the generalised one `K phi = lambda M phi` +/// over the interior nodes, solved by transforming it to a standard +/// symmetric problem through the Cholesky factor of the mass matrix. +/// Using the consistent mass matrix rather than a lumped one matters +/// here: lumping shifts the eigenvalues downwards, and it is precisely +/// their being *upper* bounds that makes them useful. +/// +/// That bound is the property worth knowing. The discrete eigenvalues +/// come from the Rayleigh quotient minimised over a subspace of the true +/// admissible space, and a minimum over less is never smaller, so every +/// computed eigenvalue is an upper bound on the true one and refining the +/// mesh can only lower it. A method whose eigenvalues approach the answer +/// from below has a defect, however good its error looks. +/// +/// The dense eigensolver is `O(n^3)` in the interior node count, so this +/// is for meshes of hundreds of nodes rather than thousands. +/// +/// Errors: +/// +/// `SolveError::InvalidArgument` if `count` is zero or exceeds the +/// number of interior nodes; `SolveError::NotPositiveDefinite` if the +/// mass matrix fails to factor, and whatever the eigensolver reports. +/// +/// Rust: `fem::fem2d::fem_eigenvalues_drum` +#[pyfunction] +#[pyo3(name = "fem_eigenvalues_drum", signature = (mesh, count))] +pub fn pyfn_fem_eigenvalues_drum<'py>(py: Python<'py>, mesh: crate::generated::types::PyFemMesh2, count: usize) -> PyResult> { + let mesh = mesh.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fem::fem2d::fem_eigenvalues_drum(&mesh, count))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// The `count` lowest drum modes: eigenvalues and the matching nodal +/// eigenvectors, the latter given over all nodes with zeros on the +/// clamped boundary. +/// +/// Eigenvectors are normalised so that the mass-weighted norm +/// `phi^T M phi` is one, which is the discrete form of normalising the +/// mode shape in `L2`. +/// +/// Errors: +/// +/// As `fem_eigenvalues_drum`. +/// +/// Rust: `fem::fem2d::fem_eigenmodes_drum` +#[pyfunction] +#[pyo3(name = "fem_eigenmodes_drum", signature = (mesh, count))] +pub fn pyfn_fem_eigenmodes_drum(mesh: crate::generated::types::PyFemMesh2, count: usize) -> PyResult<(Vec, Vec>)> { + let mesh = mesh.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::fem::fem2d::fem_eigenmodes_drum(&mesh, count)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok((__v.0, __v.1)) +} + +/// Solves the plane-stress elasticity problem on the mesh. +/// +/// `loads` are point forces applied at nodes and `fixed` prescribes +/// displacements at nodes, both components at once. Unit thickness is +/// assumed throughout, so a force is a force per unit thickness. +/// +/// The constant-strain triangle: +/// +/// Displacement is linear on each triangle, so strain -- its gradient -- +/// is constant there, and so is stress. That makes the element matrix +/// `A B^T D B` with no quadrature, exactly as for the Laplacian, and it +/// makes the stress field piecewise constant and discontinuous across +/// every edge. The discontinuity is not a bug to be smoothed away +/// silently: its size is an error estimate, and averaging it to the +/// nodes before showing it to anyone is how a coarse mesh comes to look +/// convincing. +/// +/// What has to be pinned: +/// +/// The stiffness matrix has a three-dimensional kernel: two translations +/// and one infinitesimal rotation. Prescribing fewer than three +/// independent degrees of freedom leaves the body free to move without +/// straining, and the system is singular no matter how many loads are +/// applied. This is checked directly. +/// +/// Errors: +/// +/// `SolveError::InvalidArgument` for a non-positive modulus, a +/// Poisson's ratio outside `(-1, 0.5)`, an out-of-range node index, or +/// non-finite data; `SolveError::Singular` if the constraints leave a +/// rigid body motion free. +/// +/// Rust: `fem::fem2d::fem_2d_elasticity_plane_stress` +#[pyfunction] +#[pyo3(name = "fem_2d_elasticity_plane_stress", signature = (mesh, e, nu, loads, fixed))] +pub fn pyfn_fem_2d_elasticity_plane_stress(mesh: crate::generated::types::PyFemMesh2, e: f64, nu: f64, loads: Vec<(usize, crate::generated::types::PyVec2Arg)>, fixed: Vec<(usize, crate::generated::types::PyVec2Arg)>) -> PyResult> { + let mesh = mesh.inner; + let loads = loads.into_iter().map(|__e| (__e.0, __e.1.0)).collect::>(); + let fixed = fixed.into_iter().map(|__e| (__e.0, __e.1.0)).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::fem::fem2d::fem_2d_elasticity_plane_stress(&mesh, e, nu, &loads, &fixed)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// The constant strain `(eps_x, eps_y, gamma)` of one triangle, given a +/// nodal displacement field. +/// +/// `gamma` is the engineering shear strain, twice the tensor component. +/// Returns `None` for an out-of-range index or a mismatched field. +/// +/// Rust: `fem::fem2d::element_strain` +#[pyfunction] +#[pyo3(name = "element_strain", signature = (mesh, u, tri))] +pub fn pyfn_element_strain<'py>(py: Python<'py>, mesh: crate::generated::types::PyFemMesh2, u: Vec, tri: usize) -> PyResult>> { + let mesh = mesh.inner; + let u = u.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fem::fem2d::element_strain(&mesh, &u, tri))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x.to_vec())) +} + +/// The constant stress `(sigma_x, sigma_y, tau)` of one triangle. +/// +/// Returns `None` for an out-of-range index, a mismatched field, or +/// material constants outside their admissible ranges. +/// +/// Rust: `fem::fem2d::element_stress` +#[pyfunction] +#[pyo3(name = "element_stress", signature = (mesh, u, e, nu, tri))] +pub fn pyfn_element_stress<'py>(py: Python<'py>, mesh: crate::generated::types::PyFemMesh2, u: Vec, e: f64, nu: f64, tri: usize) -> PyResult>> { + let mesh = mesh.inner; + let u = u.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fem::fem2d::element_stress(&mesh, &u, e, nu, tri))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x.to_vec())) +} + +/// The total strain energy `(1/2) integral sigma : eps`. +/// +/// At equilibrium this is half the work the applied loads do, which is +/// Clapeyron's theorem and follows from nothing more than the stiffness +/// matrix being symmetric. +/// +/// Errors: +/// +/// `SolveError::DimensionMismatch` for a mismatched field and +/// `SolveError::InvalidArgument` for invalid material constants. +/// +/// Rust: `fem::fem2d::strain_energy` +#[pyfunction] +#[pyo3(name = "strain_energy", signature = (mesh, u, e, nu))] +pub fn pyfn_strain_energy<'py>(py: Python<'py>, mesh: crate::generated::types::PyFemMesh2, u: Vec, e: f64, nu: f64) -> PyResult { + let mesh = mesh.inner; + let u = u.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fem::fem2d::strain_energy(&mesh, &u, e, nu))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// The von Mises equivalent stress of each triangle, given a nodal +/// displacement field. +/// +/// One value per triangle, not per node: the strain of a linear +/// displacement field is constant on an element and discontinuous across +/// its edges. That discontinuity is not a bug to be smoothed away +/// silently -- its size is an error estimate, and averaging it to the +/// nodes before showing it to anyone is how a coarse mesh comes to look +/// convincing. +/// +/// In plane stress the out-of-plane stress is zero rather than free, so +/// the equivalent stress is +/// `sqrt(sx^2 - sx sy + sy^2 + 3 tau^2)`. A consequence worth noticing: +/// equal biaxial tension `sx = sy = s` gives `|s|`, not zero. The +/// three-dimensional intuition that hydrostatic stress cannot yield a +/// material does not survive into plane stress, because a state that is +/// hydrostatic *in plane* has a free surface out of it and so is not +/// hydrostatic at all. +/// +/// Errors: +/// +/// `SolveError::DimensionMismatch` if the displacement count does not +/// match the node count, and `SolveError::InvalidArgument` for invalid +/// material constants. +/// +/// Rust: `fem::fem2d::von_mises_stress` +#[pyfunction] +#[pyo3(name = "von_mises_stress", signature = (mesh, u, e, nu))] +pub fn pyfn_von_mises_stress<'py>(py: Python<'py>, mesh: crate::generated::types::PyFemMesh2, u: Vec, e: f64, nu: f64) -> PyResult> { + let mesh = mesh.inner; + let u = u.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fem::fem2d::von_mises_stress(&mesh, &u, e, nu))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// Marches the heat equation `u_t = alpha lap u + f` with the +/// `theta` scheme, returning `steps + 1` snapshots starting from the +/// initial field. +/// +/// The step solves +/// `(M + theta alpha dt K) u_next = (M - (1-theta) alpha dt K) u + dt F`. +/// `theta = 0` is forward Euler, `1` backward Euler, `1/2` +/// Crank-Nicolson. +/// +/// Stability, and the difference between A-stable and L-stable: +/// +/// Applied to a discrete eigenmode the scheme multiplies its amplitude +/// by `(1 - (1-theta) a) / (1 + theta a)` each step, with +/// `a = alpha lambda dt`. For `theta >= 1/2` that factor has magnitude +/// below one for every positive `a`, which is A-stability, and forward +/// Euler instead needs `a < 2`. +/// +/// Crank-Nicolson is A-stable but *not* L-stable: as `a` grows its factor +/// tends to `-1`, not to zero. A mode too stiff to resolve therefore +/// survives while flipping sign every step, which is why a discontinuous +/// initial condition rings under Crank-Nicolson and why the usual remedy +/// is to take the first couple of steps with backward Euler, whose +/// factor does tend to zero. That contrast is asserted in the tests. +/// +/// Errors: +/// +/// `SolveError::InvalidArgument` for a mismatched initial field, a +/// non-positive step, a `theta` outside `[0, 1]`, a negative diffusivity +/// or non-finite data; whatever the linear solver reports otherwise. +/// +/// Rust: `fem::fem2d::fem_2d_heat_transient` +#[pyfunction] +#[pyo3(name = "fem_2d_heat_transient", signature = (mesh, initial, alpha, dt, steps, theta, source, dirichlet))] +pub fn pyfn_fem_2d_heat_transient(mesh: crate::generated::types::PyFemMesh2, initial: Vec, alpha: f64, dt: f64, steps: usize, theta: f64, source: pyo3::Py, dirichlet: pyo3::Py) -> PyResult>> { + let mesh = mesh.inner; + let __cb_source = std::rc::Rc::new(crate::runtime::Callback::new(source)); + let source = { let __cb = __cb_source.clone(); move |__a0: rust_physics_engine::math::Vec2| -> f64 { __cb.call::<_, f64>((crate::generated::types::PyVec2 { inner: __a0 },), f64::NAN) } }; + let __cb_dirichlet = std::rc::Rc::new(crate::runtime::Callback::new(dirichlet)); + let dirichlet = { let __cb = __cb_dirichlet.clone(); move |__a0: rust_physics_engine::math::Vec2| -> Option { __cb.call::<_, Option>((crate::generated::types::PyVec2 { inner: __a0 },), None) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::fem::fem2d::fem_2d_heat_transient(&mesh, &initial, alpha, dt, steps, theta, &source, &dirichlet)); + crate::runtime::callback::check(&[&__cb_source, &__cb_dirichlet], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_fem_2d_poisson, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fem_2d_reaction_diffusion, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_stiffness_matrix, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mass_matrix, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_element_gradient, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dirichlet_energy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_interpolate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fem_2d_helmholtz, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fem_eigenvalues_drum, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fem_eigenmodes_drum, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fem_2d_elasticity_plane_stress, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_element_strain, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_element_stress, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_strain_energy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_von_mises_stress, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fem_2d_heat_transient, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_fem__spectral_pde.rs b/bindings/python/src/generated/m_fem__spectral_pde.rs new file mode 100644 index 0000000..9cc8acf --- /dev/null +++ b/bindings/python/src/generated/m_fem__spectral_pde.rs @@ -0,0 +1,225 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// The `n + 1` Chebyshev-Gauss-Lobatto points on `[a, b]`. +/// +/// Ordered descending on `[-1, 1]` -- `x_j = cos(j pi / n)` runs from `1` +/// to `-1` -- which is the convention Trefethen's differentiation matrix +/// assumes, and mapped affinely onto `[a, b]`. Getting the order +/// backwards flips the sign of every derivative, silently. +/// +/// Errors: +/// +/// `SolveError::InvalidArgument` for `n == 0` or a degenerate +/// interval. +/// +/// Rust: `fem::spectral_pde::chebyshev_points` +#[pyfunction] +#[pyo3(name = "chebyshev_points", signature = (n, a, b))] +pub fn pyfn_chebyshev_points<'py>(py: Python<'py>, n: usize, a: f64, b: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fem::spectral_pde::chebyshev_points(n, a, b))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// The Chebyshev differentiation matrix on `[a, b]`, `(n+1)` square. +/// +/// Multiplying a vector of values at `chebyshev_points` by this matrix +/// gives the derivative of the degree-`n` polynomial through those +/// values, at the same points. For data that *is* a polynomial of degree +/// at most `n` the result is the exact derivative, to rounding, however +/// large `n` is. +/// +/// The off-diagonal entries are Trefethen's +/// `(c_i / c_j) (-1)^{i+j} / (x_i - x_j)`, with `c` equal to two at the +/// ends and one inside. The diagonal is *not* set from its closed form +/// but as minus the sum of the rest of its row -- the negative sum trick. +/// The two agree analytically, and differ in floating point by +/// cancellation that grows with `n`; taking the sum makes the matrix +/// annihilate constants exactly instead of nearly, which matters because +/// the constant is the one thing every derivative operator must kill and +/// the error in it pollutes everything else. +/// +/// Errors: +/// +/// `SolveError::InvalidArgument` for `n == 0` or a degenerate +/// interval. +/// +/// Rust: `fem::spectral_pde::cheb_diff_matrix` +#[pyfunction] +#[pyo3(name = "cheb_diff_matrix", signature = (n, a, b))] +pub fn pyfn_cheb_diff_matrix(n: usize, a: f64, b: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fem::spectral_pde::cheb_diff_matrix(n, a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) +} + +/// Differentiates values sampled at `chebyshev_points`. +/// +/// Errors: +/// +/// `SolveError::DimensionMismatch` if the sample count is not +/// `n + 1` for the matrix's `n`. +/// +/// Rust: `fem::spectral_pde::cheb_differentiate` +#[pyfunction] +#[pyo3(name = "cheb_differentiate", signature = (d, values))] +pub fn pyfn_cheb_differentiate<'py>(py: Python<'py>, d: crate::generated::types::PyMatrixArg, values: Vec) -> PyResult> { + let d = d.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fem::spectral_pde::cheb_differentiate(&d, &values))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// Solves `-(p u')' + q u = f` on `[a, b]` with Dirichlet ends by +/// Chebyshev collocation. +/// +/// The operator is assembled as `-D diag(p) D + diag(q)` and the +/// equation is imposed at the interior collocation points, with the two +/// end rows replaced by the boundary conditions. Returns the `n + 1` +/// values at `chebyshev_points`. +/// +/// Only Dirichlet conditions are offered. A flux condition in a +/// collocation method means replacing an end row by a row of the +/// differentiation matrix, which works but changes the conditioning +/// enough to deserve its own treatment rather than a flag here. +/// +/// The matrix is dense and the cost is `O(n^3)`, which is the trade the +/// method makes: far fewer unknowns for the same accuracy, each of them +/// coupled to all the others. +/// +/// Errors: +/// +/// `SolveError::InvalidArgument` for a degenerate interval, `n < 2`, +/// a non-positive `p`, or non-finite data; `SolveError::Singular` if +/// the collocation matrix is singular, which a reaction term negative +/// enough to hit an eigenvalue will do. +/// +/// Rust: `fem::spectral_pde::chebyshev_collocation_bvp` +#[pyfunction] +#[pyo3(name = "chebyshev_collocation_bvp", signature = (p, q, f, a, b, bc, n))] +pub fn pyfn_chebyshev_collocation_bvp(p: pyo3::Py, q: pyo3::Py, f: pyo3::Py, a: f64, b: f64, bc: (f64, f64), n: usize) -> PyResult> { + let __cb_p = std::rc::Rc::new(crate::runtime::Callback::new(p)); + let p = { let __cb = __cb_p.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __cb_q = std::rc::Rc::new(crate::runtime::Callback::new(q)); + let q = { let __cb = __cb_q.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let bc = (bc.0, bc.1); + let __r = crate::runtime::guard(|| rust_physics_engine::fem::spectral_pde::chebyshev_collocation_bvp(&p, &q, &f, a, b, bc, n)); + crate::runtime::callback::check(&[&__cb_p, &__cb_q, &__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// Solves `u'' = f` on a periodic interval of the given length, using +/// the true spectral symbol `-k^2`. +/// +/// `f` is sampled at `n` equally spaced points starting at the left end; +/// the point at the right end is the same as the first and is not +/// included. The solution is fixed by taking it mean-free, which is the +/// only choice available: a periodic Poisson problem determines `u` only +/// up to a constant, and it has no solution at all unless `f` itself has +/// zero mean. A nonzero mean in the data is silently dropped -- the +/// alternative is refusing perfectly good data over a rounding-level +/// mean -- and `spectral_poisson_periodic` returns the solution of the +/// mean-free part. +/// +/// Compare `transforms::fft::fft_poisson_2d`, which divides by +/// the five-point Laplacian's eigenvalue instead. See the module note: +/// they solve different problems and both are right. +/// +/// Errors: +/// +/// `SolveError::InvalidArgument` for fewer than two samples, a +/// non-positive length, or non-finite data. +/// +/// Rust: `fem::spectral_pde::spectral_poisson_periodic` +#[pyfunction] +#[pyo3(name = "spectral_poisson_periodic", signature = (f, length))] +pub fn pyfn_spectral_poisson_periodic<'py>(py: Python<'py>, f: Vec, length: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fem::spectral_pde::spectral_poisson_periodic(&f, length))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// Differentiates a periodic sample twice with the spectral symbol, +/// which is the exact inverse of `spectral_poisson_periodic` on +/// mean-free data. +/// +/// Errors: +/// +/// As `spectral_poisson_periodic`. +/// +/// Rust: `fem::spectral_pde::spectral_second_derivative` +#[pyfunction] +#[pyo3(name = "spectral_second_derivative", signature = (u, length))] +pub fn pyfn_spectral_second_derivative<'py>(py: Python<'py>, u: Vec, length: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fem::spectral_pde::spectral_second_derivative(&u, length))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// The largest error in the Chebyshev derivative of `f` at each degree +/// in `sizes`. +/// +/// The point of the function is the *shape* of what it returns, not any +/// one entry. For an analytic `f` the sequence falls geometrically and a +/// log-log fit against `n` finds no fixed slope at all; for an `f` with +/// `k` continuous derivatives it falls as `n^-k` and the fit finds +/// exactly `k`. Plotting one without the other is what makes spectral +/// accuracy look like magic rather than like a statement about +/// smoothness. +/// +/// Errors: +/// +/// `SolveError::InvalidArgument` if any size is below one or the +/// interval is degenerate. +/// +/// Rust: `fem::spectral_pde::spectral_convergence_demo` +#[pyfunction] +#[pyo3(name = "spectral_convergence_demo", signature = (f, df, a, b, sizes))] +pub fn pyfn_spectral_convergence_demo(f: pyo3::Py, df: pyo3::Py, a: f64, b: f64, sizes: Vec) -> PyResult> { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __cb_df = std::rc::Rc::new(crate::runtime::Callback::new(df)); + let df = { let __cb = __cb_df.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::fem::spectral_pde::spectral_convergence_demo(&f, &df, a, b, &sizes)); + crate::runtime::callback::check(&[&__cb_f, &__cb_df], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_chebyshev_points, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cheb_diff_matrix, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cheb_differentiate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_chebyshev_collocation_bvp, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spectral_poisson_periodic, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spectral_second_derivative, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spectral_convergence_demo, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_fields.rs b/bindings/python/src/generated/m_fields.rs new file mode 100644 index 0000000..b0d2845 --- /dev/null +++ b/bindings/python/src/generated/m_fields.rs @@ -0,0 +1,24 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_finance.rs b/bindings/python/src/generated/m_finance.rs new file mode 100644 index 0000000..c02bbf8 --- /dev/null +++ b/bindings/python/src/generated/m_finance.rs @@ -0,0 +1,22 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + Ok(()) +} diff --git a/bindings/python/src/generated/m_finance__options.rs b/bindings/python/src/generated/m_finance__options.rs new file mode 100644 index 0000000..099d9a1 --- /dev/null +++ b/bindings/python/src/generated/m_finance__options.rs @@ -0,0 +1,556 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// The Black-Scholes-Merton price of a European option. +/// +/// `S e^(-qT) N(d1) - K e^(-rT) N(d2)` for a call, and the mirror for a +/// put. The two terms are not "probability times payoff": the first is the +/// value of receiving the share if exercised, computed under a measure in +/// which the share is the numeraire, and the second is the strike times +/// the risk-neutral probability of exercise. Reading `N(d2)` as a +/// real-world probability is the commonest misreading of the formula -- +/// it is a probability under a measure chosen to make discounted prices +/// martingales, and has nothing to say about what the share will do. +/// +/// Zero volatility or zero time to expiry both collapse the formula to +/// the discounted intrinsic value, which is handled directly rather than +/// left to divide by zero. +/// +/// Errors: +/// Returns an error for a non-positive price or strike, a negative time or +/// volatility, or any input that is not finite. +/// +/// Rust: `finance::options::black_scholes` +#[pyfunction] +#[pyo3(name = "black_scholes", signature = (s, k, t, r, sigma, q, call))] +pub fn pyfn_black_scholes(s: f64, k: f64, t: f64, r: f64, sigma: f64, q: f64, call: bool) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::finance::options::black_scholes(s, k, t, r, sigma, q, call)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The Black-Scholes Greeks. +/// +/// `vega` is per unit of volatility (so divide by 100 for "per volatility +/// point"), `theta` is per year (divide by 365 for a daily decay), and +/// `rho` is per unit of rate. Those conventions differ between desks and +/// are the commonest source of a factor of a hundred. +/// +/// Gamma and vega are the same for a call and a put, because the two +/// differ by a forward contract, which is linear in the spot and does not +/// depend on volatility at all. That identity is exact and is what the +/// tests check rather than the individual numbers. +/// +/// Errors: +/// Returns an error for the same inputs as `black_scholes`, and for a +/// zero time or volatility, where the derivatives do not exist. +/// +/// Rust: `finance::options::bs_greeks` +#[pyfunction] +#[pyo3(name = "bs_greeks", signature = (s, k, t, r, sigma, q, call))] +pub fn pyfn_bs_greeks(s: f64, k: f64, t: f64, r: f64, sigma: f64, q: f64, call: bool) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::finance::options::bs_greeks(s, k, t, r, sigma, q, call)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyGreeks { inner: __v }) +} + +/// The volatility that reproduces an observed price, or `None` if no +/// volatility does. +/// +/// Price is strictly increasing in volatility, so the root is unique where +/// it exists; the search brackets it by doubling and then bisects, taking +/// Newton steps where vega is large enough to trust and falling back to +/// bisection where it is not. Deep out-of-the-money options have vega +/// near zero over a wide range of volatilities, which is exactly where a +/// pure Newton iteration diverges and where the answer is least +/// meaningful. +/// +/// `None` means no volatility can be recovered, for either of two +/// reasons. The price may be outside the model's range -- below the +/// no-arbitrage floor (the discounted intrinsic value), above the +/// ceiling, or unreachable at any volatility the doubling search reaches. +/// Or the price may simply not determine one: a deep in-the-money option +/// with weeks left has a vega around `1e-13`, and prices identically at +/// 5% and at 20% volatility to the last bit of a double. Returning a +/// number there would be reporting rounding noise as a measurement, so +/// the answer is withheld when vega falls below `1e-8` relative to the +/// price. +/// +/// Errors: +/// Returns an error for a non-positive price or strike, a non-positive +/// time, or a negative observed price. +/// +/// Rust: `finance::options::implied_volatility` +#[pyfunction] +#[pyo3(name = "implied_volatility", signature = (price, s, k, t, r, q, call))] +pub fn pyfn_implied_volatility(price: f64, s: f64, k: f64, t: f64, r: f64, q: f64, call: bool) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::finance::options::implied_volatility(price, s, k, t, r, q, call)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.map(|__x| __x)) +} + +/// The put-call parity residual: `C - P - S e^(-qT) + K e^(-rT)`. +/// +/// Zero for any pair of European prices that admit no arbitrage, +/// *whatever* model produced them, because the identity follows from the +/// payoffs alone: holding a call and selling a put is the same as holding +/// the forward. A residual is therefore a statement about the prices, not +/// about the model, and this is the sharpest check available on a pricing +/// routine that has no closed form to compare with. +/// +/// Rust: `finance::options::put_call_parity_check` +#[pyfunction] +#[pyo3(name = "put_call_parity_check", signature = (call, put, s, k, t, r, q))] +pub fn pyfn_put_call_parity_check(call: f64, put: f64, s: f64, k: f64, t: f64, r: f64, q: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::finance::options::put_call_parity_check(call, put, s, k, t, r, q)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Cox-Ross-Rubinstein binomial tree. +/// +/// Up and down moves of `e^(±sigma sqrt(dt))` with the risk-neutral +/// probability that makes the discounted price a martingale. Set +/// `american` to allow exercise at every node. +/// +/// Convergence to Black-Scholes is `O(1/steps)` but *oscillatory*: the +/// error alternates in sign as the strike moves between two adjacent +/// terminal nodes, so a tree with 101 steps can be further from the answer +/// than one with 100. Averaging two consecutive step counts removes most +/// of it, and is why an odd-even pair is the honest way to quote a +/// lattice price. +/// +/// Errors: +/// Returns an error for bad option parameters, zero steps, more than +/// twenty thousand steps, or a `dt` so large that the risk-neutral +/// probability leaves `[0, 1]` -- which happens when the drift outruns +/// what the volatility can span in one step. +/// +/// Rust: `finance::options::binomial_crr` +#[pyfunction] +#[pyo3(name = "binomial_crr", signature = (s, k, t, r, sigma, q, steps, call, american))] +pub fn pyfn_binomial_crr(s: f64, k: f64, t: f64, r: f64, sigma: f64, q: f64, steps: usize, call: bool, american: bool) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::finance::options::binomial_crr(s, k, t, r, sigma, q, steps, call, american)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// A trinomial tree with an up, down and unchanged move. +/// +/// The third branch buys a free parameter, used here to set the space step +/// to `sigma sqrt(3 dt)`, which is the choice that makes the tree stable +/// and its convergence smoother than the binomial's. It is the same +/// explicit finite-difference scheme as the binomial in different +/// clothing, and the extra branch is what keeps the scheme's coefficients +/// positive over a wider range of steps. +/// +/// The probabilities here match the first two moments of the *log* price. +/// That is the usual construction and it has a consequence worth knowing: +/// unlike Cox-Ross-Rubinstein, whose up-probability is chosen to make the +/// price itself a martingale exactly, this tree is a martingale only to +/// `O(dt^2)`. So its call and put prices satisfy put-call parity only to +/// that order -- a residual of about `2e-3` on a two-and-a-half-year +/// option at seven steps, falling as `1/steps^2` and reaching `4e-8` by +/// sixteen hundred. The tree is arbitrage-free in the limit and not +/// before it. Use `binomial_crr` where an exactly consistent call and +/// put matter more than a smooth convergence. +/// +/// Errors: +/// As `binomial_crr`, with a lower step ceiling since the work is +/// quadratic in the step count. +/// +/// Rust: `finance::options::trinomial` +#[pyfunction] +#[pyo3(name = "trinomial", signature = (s, k, t, r, sigma, q, steps, call, american))] +pub fn pyfn_trinomial(s: f64, k: f64, t: f64, r: f64, sigma: f64, q: f64, steps: usize, call: bool, american: bool) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::finance::options::trinomial(s, k, t, r, sigma, q, steps, call, american)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// A European option by Monte Carlo, returning `(price, standard error)`. +/// +/// Two variance reductions are applied, and both are exact rather than +/// heuristic: +/// +/// *Antithetic variates* price each draw with `z` and `-z`. The pair has +/// the same distribution as two independent draws, so the estimator stays +/// unbiased, and the negative correlation between the two payoffs shrinks +/// the variance of their mean. +/// +/// *A control variate* uses the discounted terminal price, whose expected +/// value under the risk-neutral measure is exactly `S e^(-qT)` -- known, +/// not estimated. Subtracting `beta` times its error from each payoff +/// cannot bias the result whatever `beta` is, and choosing `beta` by +/// regression on the same sample minimises the variance. +/// +/// The reported standard error is the error *of the reduced estimator*, +/// so it is the honest one to compare against the closed form: a price +/// two standard errors from Black-Scholes is a failure, and the tests +/// treat it as one. +/// +/// Errors: +/// Returns an error for bad option parameters or a path count outside +/// `[2, 2e7]`. +/// +/// Rust: `finance::options::monte_carlo_european` +#[pyfunction] +#[pyo3(name = "monte_carlo_european", signature = (s, k, t, r, sigma, q, call, paths, rng))] +pub fn pyfn_monte_carlo_european(s: f64, k: f64, t: f64, r: f64, sigma: f64, q: f64, call: bool, paths: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(f64, f64)> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::finance::options::monte_carlo_european(s, k, t, r, sigma, q, call, paths, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// An arithmetic-average Asian option by Monte Carlo, returning +/// `(price, standard error)`. +/// +/// The average is taken over the `steps` monitoring dates, excluding the +/// start. Averaging is what makes the option cheaper than its European +/// twin: the average of a lognormal has lower variance than its terminal +/// value, and lower variance means a lower option price at the same +/// forward. +/// +/// There is no closed form for the arithmetic average -- the sum of +/// lognormals is not lognormal -- which is why this is a simulation and +/// not a formula. The *geometric* average does have one, and that is what +/// makes a geometric control variate the standard variance reduction +/// here; it is not applied, so expect the error to fall only as the +/// square root of the path count. +/// +/// Errors: +/// Returns an error for bad option parameters, a path count outside +/// `[2, 2e7]`, a step count of zero, or more than fifty million total +/// steps. +/// +/// Rust: `finance::options::monte_carlo_asian` +#[pyfunction] +#[pyo3(name = "monte_carlo_asian", signature = (s, k, t, r, sigma, q, call, steps, paths, rng))] +pub fn pyfn_monte_carlo_asian(s: f64, k: f64, t: f64, r: f64, sigma: f64, q: f64, call: bool, steps: usize, paths: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(f64, f64)> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::finance::options::monte_carlo_asian(s, k, t, r, sigma, q, call, steps, paths, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// A barrier option by Monte Carlo, returning `(price, standard error)`. +/// +/// The barrier is checked only at the `steps` monitoring dates. That is a +/// *discretely monitored* option and it is worth strictly more than a +/// continuously monitored one, because a path can cross the barrier and +/// come back between observations. The gap closes slowly, like +/// `1/sqrt(steps)`, so a daily-monitored option priced with twelve steps +/// is materially mispriced -- the discretisation is a modelling choice +/// here, not a numerical detail. +/// +/// The in-out parity holds by construction: a knock-in and its matching +/// knock-out sum to the vanilla option, since every path pays into exactly +/// one of them. +/// +/// Errors: +/// As `monte_carlo_asian`, plus a non-positive barrier level. +/// +/// Rust: `finance::options::monte_carlo_barrier` +#[pyfunction] +#[pyo3(name = "monte_carlo_barrier", signature = (s, k, barrier, kind, t, r, sigma, q, call, steps, paths, rng))] +pub fn pyfn_monte_carlo_barrier(s: f64, k: f64, barrier: f64, kind: crate::generated::types::PyBarrier, t: f64, r: f64, sigma: f64, q: f64, call: bool, steps: usize, paths: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(f64, f64)> { + let kind = kind.to_rust(); + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::finance::options::monte_carlo_barrier(s, k, barrier, kind, t, r, sigma, q, call, steps, paths, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// A fixed-strike lookback option by Monte Carlo, returning +/// `(price, standard error)`. +/// +/// A call pays on the running maximum and a put on the running minimum, so +/// the holder is credited with the best price the path ever reached. It is +/// therefore worth at least as much as the European option with the same +/// strike, always and path by path, and the tests use that as an ordering +/// rather than a number. +/// +/// Discrete monitoring cuts the price for the same reason it raises a +/// knock-out's: the sampled extremum is closer to the terminal value than +/// the continuous one. +/// +/// Errors: +/// As `monte_carlo_asian`. +/// +/// Rust: `finance::options::monte_carlo_lookback` +#[pyfunction] +#[pyo3(name = "monte_carlo_lookback", signature = (s, k, t, r, sigma, q, call, steps, paths, rng))] +pub fn pyfn_monte_carlo_lookback(s: f64, k: f64, t: f64, r: f64, sigma: f64, q: f64, call: bool, steps: usize, paths: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(f64, f64)> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::finance::options::monte_carlo_lookback(s, k, t, r, sigma, q, call, steps, paths, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// The Longstaff-Schwartz price of an American option by least-squares +/// Monte Carlo. +/// +/// Working backwards from expiry, the continuation value at each exercise +/// date is regressed on a quadratic in the current price, using only the +/// paths that are in the money -- and the *fitted* value, not the +/// realised one, decides whether to exercise. Using the realised future +/// payoff to make the decision would be looking ahead, and would produce a +/// price above the true one. +/// +/// The estimate is biased low in principle, because the exercise rule +/// comes from a finite regression and any suboptimal rule undervalues the +/// option. In practice with a low-order basis it can also come out high +/// on the same sample the rule was fitted on, which is why the tests +/// compare it against a binomial tree with a tolerance rather than +/// asserting a direction. +/// +/// Errors: +/// As `monte_carlo_asian`, and for fewer than two exercise dates. +/// +/// Rust: `finance::options::longstaff_schwartz_american` +#[pyfunction] +#[pyo3(name = "longstaff_schwartz_american", signature = (s, k, t, r, sigma, q, call, steps, paths, rng))] +pub fn pyfn_longstaff_schwartz_american(s: f64, k: f64, t: f64, r: f64, sigma: f64, q: f64, call: bool, steps: usize, paths: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::finance::options::longstaff_schwartz_american(s, k, t, r, sigma, q, call, steps, paths, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Merton's jump-diffusion price, as a Poisson-weighted sum of +/// Black-Scholes prices. +/// +/// A jump arriving at rate `lambda` multiplies the price by a lognormal +/// factor with log-mean `jump_mean` and log-standard-deviation +/// `jump_vol`. Conditioning on the number of jumps makes each term +/// lognormal again, so the price is an exact infinite sum of Black-Scholes +/// prices with adjusted rate and volatility, truncated here once the +/// Poisson weights are exhausted. +/// +/// The drift compensator `-lambda * (e^(jump_mean + jump_vol^2/2) - 1)` +/// is what keeps the discounted price a martingale: jumps add expected +/// return, and it must be taken back out of the diffusion or the model +/// prices an arbitrage. +/// +/// Jumps are what generate a smile. A single lognormal cannot make +/// out-of-the-money options expensive relative to at-the-money ones; a +/// mixture over jump counts has fatter tails and does exactly that. +/// +/// Errors: +/// Returns an error for bad option parameters, a negative jump intensity +/// or volatility, or a non-positive maturity. +/// +/// Rust: `finance::options::merton_jump_price` +#[pyfunction] +#[pyo3(name = "merton_jump_price", signature = (s, k, t, r, sigma, q, lambda_, jump_mean, jump_vol, call))] +pub fn pyfn_merton_jump_price(s: f64, k: f64, t: f64, r: f64, sigma: f64, q: f64, lambda_: f64, jump_mean: f64, jump_vol: f64, call: bool) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::finance::options::merton_jump_price(s, k, t, r, sigma, q, lambda_, jump_mean, jump_vol, call)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// A Heston stochastic-volatility price by Monte Carlo, returning +/// `(price, standard error)`. +/// +/// The variance follows `dv = kappa (theta - v) dt + xi sqrt(v) dW`, with +/// the variance's Brownian motion correlated with the price's at `rho`. +/// That correlation is the model's point: a negative `rho` makes the +/// volatility rise as the price falls, which produces the downward-sloping +/// implied volatility skew that equity markets actually show, and which no +/// symmetric model can. +/// +/// The variance is simulated with a full-truncation Euler scheme -- the +/// variance is floored at zero wherever a step takes it negative. Exact +/// simulation of the variance process is possible but expensive, and +/// full truncation is the standard compromise; it biases the price +/// slightly, and the bias falls with the step count rather than the path +/// count, so refining paths alone will not remove it. +/// +/// Errors: +/// Returns an error for bad option parameters, a negative initial or +/// long-run variance, a non-positive mean reversion or volatility of +/// volatility, a correlation outside `[-1, 1]`, or a step or path count +/// outside its budget. +/// +/// Rust: `finance::options::heston_price_mc` +#[pyfunction] +#[pyo3(name = "heston_price_mc", signature = (s, k, t, r, q, v0, kappa, theta, xi, rho, call, steps, paths, rng))] +pub fn pyfn_heston_price_mc(s: f64, k: f64, t: f64, r: f64, q: f64, v0: f64, kappa: f64, theta: f64, xi: f64, rho: f64, call: bool, steps: usize, paths: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(f64, f64)> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::finance::options::heston_price_mc(s, k, t, r, q, v0, kappa, theta, xi, rho, call, steps, paths, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// Total implied variance under raw SVI: +/// `a + b (rho (k - m) + sqrt((k - m)^2 + sigma^2))`. +/// +/// `k` is log-moneyness `ln(K/F)` and the result is *total* variance +/// `sigma_implied^2 * T`, not annualised variance. SVI is a shape, not a +/// model: it has no process behind it and makes no prediction, and its +/// value is that five parameters fit an observed smile closely and the +/// wings are linear in `k`, which is what Lee's moment formula requires of +/// any arbitrage-free smile. +/// +/// Errors: +/// Returns an error for a negative `b`, a non-positive `sigma`, a `rho` +/// outside `[-1, 1]`, or a total variance that comes out negative -- which +/// is an arbitrage, not a small numerical matter. +/// +/// Rust: `finance::options::volatility_smile_svi` +#[pyfunction] +#[pyo3(name = "volatility_smile_svi", signature = (params, k))] +pub fn pyfn_volatility_smile_svi(params: crate::generated::types::PySviArg, k: f64) -> PyResult { + let params = params.0; + let __r = crate::runtime::guard(|| rust_physics_engine::finance::options::volatility_smile_svi(¶ms, k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Fits raw SVI to observed total variances by Nelder-Mead on the sum of +/// squared errors. +/// +/// The objective is not convex and the parameters trade off against each +/// other -- `b` and `sigma` in particular are nearly degenerate for a +/// shallow smile -- so the search is restarted from the best point found, +/// which is what rescues it from the flat valley a single pass stalls in. +/// A good fit here means the shape matches, not that the parameters are +/// identified. +/// +/// Errors: +/// Returns an error for fewer than five points, mismatched lengths, or a +/// non-positive total variance among the targets. +/// +/// Rust: `finance::options::svi_fit` +#[pyfunction] +#[pyo3(name = "svi_fit", signature = (log_moneyness, total_variance))] +pub fn pyfn_svi_fit(log_moneyness: Vec, total_variance: Vec) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::finance::options::svi_fit(&log_moneyness, &total_variance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PySvi { inner: __v }) +} + +/// The Black-Scholes PDE solved by Crank-Nicolson on a log-price grid. +/// +/// Solves `dV/dt + (r - q - sigma^2/2) dV/dx + (sigma^2/2) d2V/dx2 = rV` +/// backwards from the payoff, on `space` points spanning six standard +/// deviations either side of the log spot, with Dirichlet boundaries set +/// to the discounted no-arbitrage values. Set `american` to apply the +/// early-exercise constraint after each step, which makes the scheme a +/// projected one and costs its second-order accuracy in time near the +/// exercise boundary. +/// +/// Crank-Nicolson is used rather than a fully implicit scheme because it +/// is second order in time as well as space. The price of that is that it +/// is only *A*-stable and not *L*-stable: it damps high-frequency error +/// slowly, so the kink in the payoff at the strike rings for several steps +/// rather than being smoothed away, and the Greeks near the strike are +/// visibly noisier than the price. Starting with a few fully implicit +/// steps -- Rannacher smoothing -- is the standard remedy and is what the +/// first two steps here do. +/// +/// Errors: +/// Returns an error for bad option parameters, fewer than eleven space +/// points, no time steps, more than ten million grid cells, or a +/// tridiagonal system that will not solve. +/// +/// Rust: `finance::options::bs_pde_crank_nicolson` +#[pyfunction] +#[pyo3(name = "bs_pde_crank_nicolson", signature = (s, k, t, r, sigma, q, call, american, space, time_steps))] +pub fn pyfn_bs_pde_crank_nicolson(s: f64, k: f64, t: f64, r: f64, sigma: f64, q: f64, call: bool, american: bool, space: usize, time_steps: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::finance::options::bs_pde_crank_nicolson(s, k, t, r, sigma, q, call, american, space, time_steps)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Simulates delta hedging a short European option, returning +/// `(mean profit and loss, standard deviation)`. +/// +/// The option is sold at its Black-Scholes price and the position is +/// rehedged `rebalances` times at the model delta; the P&L is what remains +/// at expiry after the payoff is settled. +/// +/// The mean is near zero because the option was sold at its fair price, +/// but the *standard deviation* is the point: it falls like +/// `1/sqrt(rebalances)`, so cutting the residual risk in half costs four +/// times as many trades. That trade-off, not the mean, is what makes +/// continuous hedging a limit rather than a procedure -- with any +/// transaction cost at all, the total cost grows as `sqrt(rebalances)` +/// while the risk falls as `1/sqrt(rebalances)`, and an optimum exists at +/// a finite frequency. +/// +/// A hedge run at a volatility different from the one the path was +/// generated with does not have a zero mean; the difference is the +/// volatility arbitrage, and it is what the tests check rather than the +/// noise. +/// +/// Errors: +/// Returns an error for bad option parameters, no rebalances, a +/// non-positive maturity, or a path count outside `[2, 2e7]`. +/// +/// Rust: `finance::options::delta_hedging_sim` +#[pyfunction] +#[pyo3(name = "delta_hedging_sim", signature = (s, k, t, r, hedge_vol, realised_vol, q, call, rebalances, paths, rng))] +pub fn pyfn_delta_hedging_sim(s: f64, k: f64, t: f64, r: f64, hedge_vol: f64, realised_vol: f64, q: f64, call: bool, rebalances: usize, paths: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(f64, f64)> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::finance::options::delta_hedging_sim(s, k, t, r, hedge_vol, realised_vol, q, call, rebalances, paths, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_black_scholes, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bs_greeks, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_implied_volatility, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_put_call_parity_check, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_binomial_crr, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_trinomial, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_monte_carlo_european, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_monte_carlo_asian, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_monte_carlo_barrier, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_monte_carlo_lookback, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_longstaff_schwartz_american, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_merton_jump_price, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_heston_price_mc, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_volatility_smile_svi, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_svi_fit, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bs_pde_crank_nicolson, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_delta_hedging_sim, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_finance__portfolio.rs b/bindings/python/src/generated/m_finance__portfolio.rs new file mode 100644 index 0000000..2a9ead7 --- /dev/null +++ b/bindings/python/src/generated/m_finance__portfolio.rs @@ -0,0 +1,417 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Simple period returns `p[t]/p[t-1] - 1`. +/// +/// Errors: +/// Returns an error for fewer than two prices, or a non-positive or +/// non-finite price. +/// +/// Rust: `finance::portfolio::returns_from_prices` +#[pyfunction] +#[pyo3(name = "returns_from_prices", signature = (prices))] +pub fn pyfn_returns_from_prices<'py>(py: Python<'py>, prices: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::finance::portfolio::returns_from_prices(&prices))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Continuously compounded returns `ln(p[t]/p[t-1])`. +/// +/// These add across periods, which is what makes them the right thing to +/// average when the question is about growth over time rather than about +/// the next period. They are always smaller than the simple return, by +/// roughly half the variance, which is the whole content of the +/// arithmetic-geometric gap. +/// +/// Errors: +/// As `returns_from_prices`. +/// +/// Rust: `finance::portfolio::log_returns` +#[pyfunction] +#[pyo3(name = "log_returns", signature = (prices))] +pub fn pyfn_log_returns<'py>(py: Python<'py>, prices: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::finance::portfolio::log_returns(&prices))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The portfolio variance `w' C w`. +/// +/// Errors: +/// Returns an error for a malformed covariance matrix or mismatched +/// weights. +/// +/// Rust: `finance::portfolio::portfolio_variance` +#[pyfunction] +#[pyo3(name = "portfolio_variance", signature = (cov, weights))] +pub fn pyfn_portfolio_variance<'py>(py: Python<'py>, cov: crate::generated::types::PyMatrixArg, weights: Vec) -> PyResult { + let cov = cov.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::finance::portfolio::portfolio_variance(&cov, &weights))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The global minimum-variance weights, which sum to one. +/// +/// `w = C^-1 1 / (1' C^-1 1)`. Expected returns do not appear, which is +/// why this is the mean-variance output that survives real data: a +/// covariance matrix estimated from the same sample that produced a +/// hopeless mean estimate is still usually good enough to rank risk. +/// +/// Weights may be negative -- the problem as posed allows short positions, +/// and with correlated assets the minimum-variance solution frequently +/// takes them. +/// +/// Errors: +/// Returns an error for a malformed or singular covariance matrix, or one +/// whose implied weights do not sum to a usable total. +/// +/// Rust: `finance::portfolio::min_variance_weights` +#[pyfunction] +#[pyo3(name = "min_variance_weights", signature = (cov))] +pub fn pyfn_min_variance_weights<'py>(py: Python<'py>, cov: crate::generated::types::PyMatrixArg) -> PyResult> { + let cov = cov.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::finance::portfolio::min_variance_weights(&cov))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The tangency portfolio: the weights maximising the Sharpe ratio at a +/// given risk-free rate. +/// +/// `w = C^-1 (mu - rf) / (1' C^-1 (mu - rf))`. Every portfolio on the +/// efficient frontier with a risk-free asset available is a mix of this +/// one and cash, which is the two-fund separation theorem -- and it is +/// what makes "the market portfolio" a meaningful object in CAPM. +/// +/// The normalisation fails when the excess returns are orthogonal to the +/// inverse-covariance-weighted ones, and flips sign when the excess +/// returns are net negative, at which point the "tangency portfolio" is a +/// short position and the geometry has broken down. Both are reported +/// rather than returned as numbers. +/// +/// Errors: +/// Returns an error for a malformed or singular covariance matrix, +/// mismatched means, or excess returns that do not determine a tangency. +/// +/// Rust: `finance::portfolio::tangency_portfolio` +#[pyfunction] +#[pyo3(name = "tangency_portfolio", signature = (mu, cov, risk_free))] +pub fn pyfn_tangency_portfolio<'py>(py: Python<'py>, mu: Vec, cov: crate::generated::types::PyMatrixArg, risk_free: f64) -> PyResult> { + let cov = cov.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::finance::portfolio::tangency_portfolio(&mu, &cov, risk_free))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The efficient frontier as `(standard deviation, expected return, +/// weights)`, from the minimum-variance point up to the highest mean. +/// +/// Each point solves the two-constraint problem exactly through the +/// standard `a, b, c` scalars, so no numerical optimisation is involved. +/// The frontier is a hyperbola in mean-standard-deviation space and a +/// parabola in mean-variance space, and its lower half -- the same +/// variances at lower returns -- is dominated and not returned. +/// +/// Short positions are permitted throughout. A frontier computed with a +/// no-short constraint is a different and much better behaved object, +/// and it has no closed form. +/// +/// Errors: +/// Returns an error for a malformed or singular covariance matrix, +/// mismatched means, fewer than two points, more than ten thousand, or +/// means that are all equal, where the frontier degenerates to a point. +/// +/// Rust: `finance::portfolio::markowitz_frontier` +#[pyfunction] +#[pyo3(name = "markowitz_frontier", signature = (mu, cov, points))] +pub fn pyfn_markowitz_frontier<'py>(py: Python<'py>, mu: Vec, cov: crate::generated::types::PyMatrixArg, points: usize) -> PyResult)>> { + let cov = cov.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::finance::portfolio::markowitz_frontier(&mu, &cov, points))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, __x.2)).collect::>()) +} + +/// Risk-parity weights: each asset contributes the same share of total +/// portfolio risk. +/// +/// The condition is `w_i (C w)_i` equal across assets, which has no closed +/// form. It is solved here by the fixed point of +/// `w_i <- sqrt(w_i / (C w)_i)`, renormalised each pass: at rest that +/// gives `w_i^2 = k^2 w_i / (C w)_i`, so `w_i (C w)_i` is the same +/// constant for every asset, which is the condition itself. +/// +/// The square root is not decoration. The undamped update +/// `w_i <- w_i / (C w)_i` converges to `(C w)_i` equal across assets -- +/// which is the *minimum-variance* condition, not this one, and gives +/// visibly different weights whenever the assets differ in volatility. +/// +/// This is not the same as equal weights, nor as inverse-volatility +/// weights -- those coincide with it only when correlations are all +/// equal. The appeal is that it needs no expected returns at all, which +/// removes the input mean-variance optimisation is most damaged by. +/// +/// Weights are constrained positive, which is what makes the problem well +/// posed: the equal-risk-contribution condition has no positive solution +/// requirement built in, and shorting breaks the interpretation. +/// +/// Errors: +/// Returns an error for a malformed covariance matrix, or an iteration +/// that does not converge. +/// +/// Rust: `finance::portfolio::risk_parity_weights` +#[pyfunction] +#[pyo3(name = "risk_parity_weights", signature = (cov))] +pub fn pyfn_risk_parity_weights<'py>(py: Python<'py>, cov: crate::generated::types::PyMatrixArg) -> PyResult> { + let cov = cov.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::finance::portfolio::risk_parity_weights(&cov))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Each asset's share of total portfolio risk: `w_i (C w)_i / (w' C w)`. +/// +/// The shares sum to one by construction, which is what makes "risk +/// contribution" a decomposition rather than a metaphor -- variance is a +/// quadratic form and Euler's theorem splits it exactly. +/// +/// Errors: +/// As `portfolio_variance`, plus a portfolio with no variance. +/// +/// Rust: `finance::portfolio::risk_contributions` +#[pyfunction] +#[pyo3(name = "risk_contributions", signature = (cov, weights))] +pub fn pyfn_risk_contributions<'py>(py: Python<'py>, cov: crate::generated::types::PyMatrixArg, weights: Vec) -> PyResult> { + let cov = cov.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::finance::portfolio::risk_contributions(&cov, &weights))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The Sharpe ratio: mean excess return divided by its standard deviation. +/// +/// Per period, not annualised -- multiplying by the square root of the +/// periods per year is the usual annualisation and it assumes returns are +/// independent, which is exactly what a trending or mean-reverting series +/// is not. +/// +/// The denominator penalises upside and downside alike. A strategy that +/// occasionally doubles is punished for it, which is what `sortino` +/// addresses, and a strategy that sells insurance -- small steady gains +/// and a rare catastrophe -- scores well right up until the catastrophe. +/// The ratio says nothing about the shape of the distribution beyond its +/// first two moments. +/// +/// Errors: +/// Returns an error for fewer than two returns, a non-finite value, or a +/// series with no variation. +/// +/// Rust: `finance::portfolio::sharpe` +#[pyfunction] +#[pyo3(name = "sharpe", signature = (returns, risk_free))] +pub fn pyfn_sharpe<'py>(py: Python<'py>, returns: Vec, risk_free: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::finance::portfolio::sharpe(&returns, risk_free))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The Sortino ratio: mean excess return over the downside deviation. +/// +/// The denominator is the root mean square of the shortfalls below +/// `target`, counting periods above it as zero rather than dropping them. +/// That choice matters: dividing by the count of losing periods instead +/// would make a strategy look better simply for losing less often, and +/// the two conventions differ by a factor that grows as losses get rarer. +/// +/// Errors: +/// Returns an error for fewer than two returns, a non-finite value, or a +/// series that never falls below the target. +/// +/// Rust: `finance::portfolio::sortino` +#[pyfunction] +#[pyo3(name = "sortino", signature = (returns, risk_free, target))] +pub fn pyfn_sortino<'py>(py: Python<'py>, returns: Vec, risk_free: f64, target: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::finance::portfolio::sortino(&returns, risk_free, target))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The maximum drawdown: the largest peak-to-trough fall, as a positive +/// fraction of the peak. +/// +/// Computed against the running maximum, so it is a property of the path +/// and not of the endpoints. Two series with the same start and end can +/// have wildly different drawdowns, which is the point -- it measures what +/// an investor would have had to sit through. +/// +/// Errors: +/// Returns an error for fewer than two prices, or a non-positive price. +/// +/// Rust: `finance::portfolio::max_drawdown` +#[pyfunction] +#[pyo3(name = "max_drawdown", signature = (prices))] +pub fn pyfn_max_drawdown<'py>(py: Python<'py>, prices: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::finance::portfolio::max_drawdown(&prices))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The Calmar ratio: annualised return divided by maximum drawdown. +/// +/// `periods_per_year` converts the series' own period into a year. The +/// return used is the *geometric* one -- the constant rate that would have +/// produced the same total growth -- because that is what an investor +/// actually earned, unlike the arithmetic mean. +/// +/// Errors: +/// Returns an error for fewer than two prices, a non-positive price or +/// period count, or a series with no drawdown to divide by. +/// +/// Rust: `finance::portfolio::calmar` +#[pyfunction] +#[pyo3(name = "calmar", signature = (prices, periods_per_year))] +pub fn pyfn_calmar<'py>(py: Python<'py>, prices: Vec, periods_per_year: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::finance::portfolio::calmar(&prices, periods_per_year))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The information ratio: mean active return over its tracking error. +/// +/// Active return is the portfolio's minus the benchmark's, period by +/// period. It is the Sharpe ratio of a long-short position against the +/// benchmark, which is why it is the natural measure for a manager judged +/// relative to an index rather than to cash. +/// +/// Errors: +/// Returns an error for mismatched or too-short series, a non-finite +/// value, or an active series with no variation. +/// +/// Rust: `finance::portfolio::information_ratio` +#[pyfunction] +#[pyo3(name = "information_ratio", signature = (portfolio, benchmark))] +pub fn pyfn_information_ratio<'py>(py: Python<'py>, portfolio: Vec, benchmark: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::finance::portfolio::information_ratio(&portfolio, &benchmark))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The CAPM regression of an asset on the market, returning +/// `(alpha, beta)`. +/// +/// Beta is `cov(asset, market) / var(market)` and alpha is the intercept +/// that remains. Beta is an estimate of sensitivity and nothing more: it +/// is a single number summarising a scatter that may not be linear, it is +/// unstable across sample periods, and a high R-squared is required before +/// it means very much at all. +/// +/// Errors: +/// Returns an error for mismatched or too-short series, a non-finite +/// value, or a market series with no variation. +/// +/// Rust: `finance::portfolio::capm_beta` +#[pyfunction] +#[pyo3(name = "capm_beta", signature = (asset, market))] +pub fn pyfn_capm_beta<'py>(py: Python<'py>, asset: Vec, market: Vec) -> PyResult<(f64, f64)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::finance::portfolio::capm_beta(&asset, &market))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// The Kelly fraction for a discrete bet won with probability `p` paying +/// `b` to one: `p - (1 - p)/b`. +/// +/// Maximises the expected *logarithm* of wealth, which is the growth rate +/// achieved almost surely over many repetitions. A negative answer means +/// the bet has no edge and the optimal stake is nothing. +/// +/// The fraction assumes the edge is known exactly. Overestimating it +/// pushes the stake past the growth-optimal point, where growth falls +/// faster than it rose: staking twice the Kelly fraction earns no more +/// than the risk-free rate however large the edge, and beyond that it +/// loses. That is why practitioners bet a fraction of it. +/// +/// Errors: +/// Returns an error for a probability outside `[0, 1]` or a non-positive +/// payout. +/// +/// Rust: `finance::portfolio::kelly_fraction` +#[pyfunction] +#[pyo3(name = "kelly_fraction", signature = (p, b))] +pub fn pyfn_kelly_fraction(p: f64, b: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::finance::portfolio::kelly_fraction(p, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The continuous Kelly fraction `(mu - rf)/sigma^2`. +/// +/// The same object for a lognormal asset: the leverage maximising the +/// long-run growth rate. It is also the tangency portfolio's leverage +/// under one asset, which is not a coincidence -- both maximise the +/// Sharpe-like quantity `(mu - rf)/sigma` scaled by the risk taken. +/// +/// Errors: +/// Returns an error for a non-positive volatility or a non-finite input. +/// +/// Rust: `finance::portfolio::kelly_continuous` +#[pyfunction] +#[pyo3(name = "kelly_continuous", signature = (mu, sigma, risk_free))] +pub fn pyfn_kelly_continuous(mu: f64, sigma: f64, risk_free: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::finance::portfolio::kelly_continuous(mu, sigma, risk_free)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_returns_from_prices, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_log_returns, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_portfolio_variance, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_min_variance_weights, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tangency_portfolio, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_markowitz_frontier, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_risk_parity_weights, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_risk_contributions, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sharpe, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sortino, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_max_drawdown, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_calmar, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_information_ratio, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_capm_beta, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kelly_fraction, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kelly_continuous, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_finance__rates.rs b/bindings/python/src/generated/m_finance__rates.rs new file mode 100644 index 0000000..358076b --- /dev/null +++ b/bindings/python/src/generated/m_finance__rates.rs @@ -0,0 +1,499 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// The present value of one unit paid at time `t`. +/// +/// `(1 + r/m)^(-m t)` for `m` compounding periods a year, and `e^(-r t)` +/// continuously. The two agree in the limit `m -> infinity`, which is the +/// whole reason continuous compounding is used in pricing: it turns a +/// product over periods into an exponential and makes rates additive +/// across maturities. +/// +/// Errors: +/// Returns an error for a negative time, a non-finite rate, or a periodic +/// rate at or below `-100%` per period, where the growth factor is +/// non-positive and the discount factor does not exist. +/// +/// Rust: `finance::rates::discount_factor` +#[pyfunction] +#[pyo3(name = "discount_factor", signature = (rate, t, compounding))] +pub fn pyfn_discount_factor(rate: f64, t: f64, compounding: crate::generated::types::PyCompounding) -> PyResult { + let compounding = compounding.to_rust(); + let __r = crate::runtime::guard(|| rust_physics_engine::finance::rates::discount_factor(rate, t, compounding)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Converts a rate between compounding conventions, preserving the growth +/// factor over a year. +/// +/// The number changes but the money does not: 10% semi-annual and 9.7580% +/// continuous are the same investment written two ways. Quoting the +/// smaller number is a real practice and this is what makes the two +/// comparable. +/// +/// Errors: +/// Returns an error for a non-finite rate or a periodic rate at or below +/// `-100%` per period. +/// +/// Rust: `finance::rates::equivalent_rate` +#[pyfunction] +#[pyo3(name = "equivalent_rate", signature = (rate, from_, to))] +pub fn pyfn_equivalent_rate(rate: f64, from_: crate::generated::types::PyCompounding, to: crate::generated::types::PyCompounding) -> PyResult { + let from_ = from_.to_rust(); + let to = to.to_rust(); + let __r = crate::runtime::guard(|| rust_physics_engine::finance::rates::equivalent_rate(rate, from_, to)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The net present value of cashflows at times `0, 1, ..., n-1` periods. +/// +/// Discounted at the periodic rate `rate`, so `cashflows[0]` is undiscounted. +/// +/// Errors: +/// Returns an error for no cashflows, a non-finite value, or a rate at or +/// below `-100%`. +/// +/// Rust: `finance::rates::npv` +#[pyfunction] +#[pyo3(name = "npv", signature = (rate, cashflows))] +pub fn pyfn_npv<'py>(py: Python<'py>, rate: f64, cashflows: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::finance::rates::npv(rate, &cashflows))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The internal rate of return: the periodic rate at which the cashflows' +/// net present value is zero. +/// +/// Returns `None` when no rate in `(-99.99%, 1e6)` does, or when the +/// cashflows change sign more than once and the answer would not be +/// unique. That second case is the one worth knowing about: Descartes' +/// rule bounds the number of positive roots by the number of sign changes, +/// so a single change guarantees at most one rate, and a project that +/// alternates between spending and earning can genuinely have several +/// internal rates of return or none at all. Reporting one of them as +/// *the* return would be a mistake this refuses to make. +/// +/// Errors: +/// Returns an error for fewer than two cashflows, or a non-finite value. +/// +/// Rust: `finance::rates::irr` +#[pyfunction] +#[pyo3(name = "irr", signature = (cashflows))] +pub fn pyfn_irr<'py>(py: Python<'py>, cashflows: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::finance::rates::irr(&cashflows))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.map(|__x| __x)) +} + +/// The annualised internal rate of return for cashflows at irregular +/// dates, given in years from the first. +/// +/// The rate is annual with annual compounding, so a payment at 0.5 years +/// is discounted by `(1 + r)^-0.5`. That fractional exponent is why this +/// needs its own function rather than being IRR on a padded schedule: real +/// cashflows do not fall on period boundaries, and forcing them there +/// misprices by days of interest. +/// +/// Errors: +/// Returns an error for fewer than two flows, mismatched lengths, times +/// that are not increasing from zero, or a non-finite value. +/// +/// Rust: `finance::rates::xirr` +#[pyfunction] +#[pyo3(name = "xirr", signature = (times, cashflows))] +pub fn pyfn_xirr<'py>(py: Python<'py>, times: Vec, cashflows: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::finance::rates::xirr(×, &cashflows))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.map(|__x| __x)) +} + +/// The price of a bond paying `coupon` per period for `periods` periods +/// and `face` at the end, discounted at the periodic yield `ytm`. +/// +/// All three arguments are *per period*, not per year: a 6% annual coupon +/// on 100 face paid semi-annually for five years is `coupon = 3`, +/// `periods = 10`, and a yield quoted semi-annually. +/// +/// A bond trades above face when its coupon exceeds its yield and below +/// when it does not, and that is not a market opinion but arithmetic: the +/// price is the yield's own discounting applied to a coupon stream that +/// pays more or less than the yield demands. +/// +/// Errors: +/// Returns an error for zero periods, more than ten thousand, a +/// non-finite input, or a yield at or below `-100%` per period. +/// +/// Rust: `finance::rates::bond_price` +#[pyfunction] +#[pyo3(name = "bond_price", signature = (face, coupon, ytm, periods))] +pub fn pyfn_bond_price(face: f64, coupon: f64, ytm: f64, periods: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::finance::rates::bond_price(face, coupon, ytm, periods)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The periodic yield that reproduces an observed bond price. +/// +/// Unique whenever the coupons and face are non-negative and at least one +/// is positive: the price is then strictly decreasing in the yield, so +/// there is exactly one root. That is why a bond has *a* yield where a +/// project may have several internal rates of return -- the cashflows +/// after the purchase all point the same way. +/// +/// Errors: +/// Returns an error for a non-positive price, bad bond parameters, or a +/// price no yield in `(-99.99%, 1e6)` reaches. +/// +/// Rust: `finance::rates::ytm_solve` +#[pyfunction] +#[pyo3(name = "ytm_solve", signature = (price, face, coupon, periods))] +pub fn pyfn_ytm_solve(price: f64, face: f64, coupon: f64, periods: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::finance::rates::ytm_solve(price, face, coupon, periods)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The Macaulay duration in periods: the discounted-cashflow-weighted +/// average time to payment. +/// +/// It is a *centre of mass*, which is why it has units of time and why a +/// zero-coupon bond's duration is exactly its maturity: all the weight +/// sits at one date. Coupons pull the centre earlier, so a higher coupon +/// always shortens duration at the same maturity. +/// +/// Errors: +/// As `bond_price`, plus a bond whose price comes out non-positive. +/// +/// Rust: `finance::rates::duration_macaulay` +#[pyfunction] +#[pyo3(name = "duration_macaulay", signature = (face, coupon, ytm, periods))] +pub fn pyfn_duration_macaulay(face: f64, coupon: f64, ytm: f64, periods: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::finance::rates::duration_macaulay(face, coupon, ytm, periods)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The modified duration: Macaulay duration divided by `1 + ytm`. +/// +/// This is the one that answers "how much does the price move": it is +/// exactly `-(1/P) dP/dy`, so a modified duration of 7 means a price fall +/// of about 7% for a one-point rise in yield. The word "about" is doing +/// real work -- duration is the first derivative and the relationship is +/// convex, so it overstates the loss on a rise and understates the gain +/// on a fall. `convexity` is the correction. +/// +/// Errors: +/// As `duration_macaulay`. +/// +/// Rust: `finance::rates::duration_modified` +#[pyfunction] +#[pyo3(name = "duration_modified", signature = (face, coupon, ytm, periods))] +pub fn pyfn_duration_modified(face: f64, coupon: f64, ytm: f64, periods: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::finance::rates::duration_modified(face, coupon, ytm, periods)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The convexity in periods squared: `(1/P) d2P/dy2`. +/// +/// Always positive for an ordinary bond, which is the reason duration +/// alone is pessimistic in both directions. Between two bonds of equal +/// duration the more convex one gains more when yields move either way, +/// and its price reflects that -- convexity is not a free lunch, it is +/// paid for in yield. +/// +/// Errors: +/// As `duration_macaulay`. +/// +/// Rust: `finance::rates::convexity` +#[pyfunction] +#[pyo3(name = "convexity", signature = (face, coupon, ytm, periods))] +pub fn pyfn_convexity(face: f64, coupon: f64, ytm: f64, periods: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::finance::rates::convexity(face, coupon, ytm, periods)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Bootstraps a zero-coupon curve from bonds of increasing maturity, +/// returning `(maturity, continuously compounded zero rate)`. +/// +/// Each bond is stripped in turn: its earlier coupons are discounted at +/// the zero rates already recovered, and whatever discount factor the +/// final payment needs to make the price work is the new point. The +/// method is exact and sequential, and that is also its weakness -- an +/// error in an early quote propagates into every later rate, and there is +/// no least-squares smoothing to absorb it. +/// +/// Coupon dates that fall between known maturities are interpolated +/// linearly *in the zero rate*, which is a choice: interpolating in the +/// discount factor or the forward rate gives different curves from the +/// same bonds, and no market convention makes one correct. +/// +/// Errors: +/// Returns an error for no bonds, maturities that do not increase, a +/// non-positive price, frequency or maturity, a maturity that is not a +/// whole number of periods, or a final cashflow whose implied discount +/// factor is non-positive -- which means the quotes admit an arbitrage. +/// +/// Rust: `finance::rates::bootstrap_zero_curve` +#[pyfunction] +#[pyo3(name = "bootstrap_zero_curve", signature = (bonds))] +pub fn pyfn_bootstrap_zero_curve<'py>(py: Python<'py>, bonds: Vec) -> PyResult> { + let bonds = bonds.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::finance::rates::bootstrap_zero_curve(&bonds))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// The continuously compounded forward rate between two maturities: +/// `(z2 t2 - z1 t1) / (t2 - t1)`. +/// +/// This is the rate the curve implies for borrowing from `t1` to `t2`, and +/// it follows from no-arbitrage alone: investing to `t2` must pay the same +/// as investing to `t1` and rolling. It is far more volatile than the zero +/// rates it comes from, because it is a *difference* of two nearly equal +/// products -- a small error in a long zero rate becomes a large error in +/// the forward, which is why bootstrapped curves are usually smoothed +/// before forwards are read off them. +/// +/// Errors: +/// Returns an error for non-increasing maturities, a non-positive first +/// maturity, or a non-finite rate. +/// +/// Rust: `finance::rates::forward_rate` +#[pyfunction] +#[pyo3(name = "forward_rate", signature = (z1, t1, z2, t2))] +pub fn pyfn_forward_rate(z1: f64, t1: f64, z2: f64, t2: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::finance::rates::forward_rate(z1, t1, z2, t2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The Nelson-Siegel zero rate at maturity `t`. +/// +/// `b0 + (b1 + b2) (1 - e^-x)/x - b2 e^-x` with `x = t/tau`. The three +/// coefficients are usually read as level, slope and curvature: `b0` is +/// the long rate the curve tends to, `b0 + b1` is the short rate it starts +/// from, and `b2` is a hump whose position `tau` sets. +/// +/// Four parameters is not many for a yield curve, and that is the point: +/// the shape cannot fit noise, so it smooths, and it extrapolates to a +/// finite long rate rather than diverging as a polynomial would. What it +/// cannot do is fit more than one hump, which is where the Svensson +/// extension with two decay terms is used instead. +/// +/// Errors: +/// Returns an error for a non-positive `tau`, a negative `t`, or a +/// non-finite coefficient. +/// +/// Rust: `finance::rates::nelson_siegel` +#[pyfunction] +#[pyo3(name = "nelson_siegel", signature = (t, b0, b1, b2, tau))] +pub fn pyfn_nelson_siegel(t: f64, b0: f64, b1: f64, b2: f64, tau: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::finance::rates::nelson_siegel(t, b0, b1, b2, tau)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Fits Nelson-Siegel to observed yields, returning `(b0, b1, b2, tau)`. +/// +/// For a fixed `tau` the model is *linear* in the three coefficients, so +/// the fit is a three-parameter least squares that solves exactly. Only +/// `tau` needs searching, and it is searched over a grid rather than by +/// gradient because the objective in `tau` is not convex and a local +/// method lands wherever it started. That split -- exact where the model +/// is linear, brute force where it is not -- is what makes this reliable +/// where a five-parameter nonlinear search is not. +/// +/// Errors: +/// Returns an error for fewer than four points, mismatched lengths, a +/// non-positive maturity, a non-finite value, or maturities that do not +/// determine the fit. +/// +/// Rust: `finance::rates::ns_fit` +#[pyfunction] +#[pyo3(name = "ns_fit", signature = (maturities, yields))] +pub fn pyfn_ns_fit<'py>(py: Python<'py>, maturities: Vec, yields: Vec) -> PyResult<(f64, f64, f64, f64)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::finance::rates::ns_fit(&maturities, &yields))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1, __v.2, __v.3)) +} + +/// The Vasicek zero-coupon bond price under `dr = kappa (theta - r) dt + +/// sigma dW`. +/// +/// `P(t) = A(t) e^(-B(t) r0)` with `B = (1 - e^(-kappa t))/kappa`. The +/// model is affine and Gaussian, which is what makes the price a closed +/// form and also what makes the rate able to go negative -- for decades +/// that was the standard objection to Vasicek, and since 2014 it has been +/// the reason to use it. +/// +/// The long-run mean of the *rate* is `theta`, but the long-run mean of +/// the yield is `theta - sigma^2/(2 kappa^2)`, lower by a convexity term +/// that grows with volatility. Discounting is convex in the rate, so +/// uncertainty about future rates makes bonds worth more than the average +/// rate alone would say. +/// +/// Errors: +/// Returns an error for a non-positive `kappa`, a negative `sigma`, a +/// negative maturity, or a non-finite parameter. +/// +/// Rust: `finance::rates::vasicek_bond_price` +#[pyfunction] +#[pyo3(name = "vasicek_bond_price", signature = (r0, kappa, theta, sigma, t))] +pub fn pyfn_vasicek_bond_price(r0: f64, kappa: f64, theta: f64, sigma: f64, t: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::finance::rates::vasicek_bond_price(r0, kappa, theta, sigma, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The Cox-Ingersoll-Ross zero-coupon bond price under +/// `dr = kappa (theta - r) dt + sigma sqrt(r) dW`. +/// +/// The `sqrt(r)` diffusion is what keeps the rate non-negative: volatility +/// vanishes as the rate approaches zero, so the process cannot cross it. +/// Whether zero is even reached depends on the Feller condition +/// `2 kappa theta >= sigma^2` -- satisfied, the rate stays strictly +/// positive; violated, it touches zero and reflects. The price is still a +/// closed form either way, and `cir_feller_condition` reports which +/// regime the parameters are in. +/// +/// The formula raises a base tending to one to the power +/// `2 kappa theta / sigma^2`, so it loses precision as `sigma` shrinks: at +/// `sigma = 1e-6` the answer is off by about `1e-6` relative, which is a +/// thousand times larger than the convexity effect it is trying to +/// capture. `sigma = 0` is handled exactly by the deterministic limit; +/// between them, below roughly `1e-5`, the price is dominated by rounding +/// and `vasicek_bond_price` with a zero volatility is the better answer. +/// +/// Errors: +/// Returns an error for a negative initial rate, a non-positive `kappa` or +/// `theta`, a negative `sigma`, a negative maturity, or a non-finite +/// parameter. +/// +/// Rust: `finance::rates::cir_bond_price` +#[pyfunction] +#[pyo3(name = "cir_bond_price", signature = (r0, kappa, theta, sigma, t))] +pub fn pyfn_cir_bond_price(r0: f64, kappa: f64, theta: f64, sigma: f64, t: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::finance::rates::cir_bond_price(r0, kappa, theta, sigma, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Whether the Feller condition `2 kappa theta >= sigma^2` holds, which +/// decides whether a CIR rate can reach zero. +/// +/// Rust: `finance::rates::cir_feller_condition` +#[pyfunction] +#[pyo3(name = "cir_feller_condition", signature = (kappa, theta, sigma))] +pub fn pyfn_cir_feller_condition(kappa: f64, theta: f64, sigma: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::finance::rates::cir_feller_condition(kappa, theta, sigma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The level payment that repays `principal` over `n` periods at the +/// periodic rate `rate`. +/// +/// `P r / (1 - (1+r)^-n)`, which is the principal divided by the annuity +/// factor. At zero rate it degenerates to `P/n`, handled directly. +/// +/// Errors: +/// Returns an error for a non-positive principal, zero periods, more than +/// a hundred thousand periods, or a rate at or below `-100%`. +/// +/// Rust: `finance::rates::mortgage_payment` +#[pyfunction] +#[pyo3(name = "mortgage_payment", signature = (principal, rate, n))] +pub fn pyfn_mortgage_payment(principal: f64, rate: f64, n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::finance::rates::mortgage_payment(principal, rate, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The amortisation schedule as `(payment, interest, principal, balance)` +/// per period. +/// +/// The payment is level; what changes is its split. Early on almost all of +/// it is interest, because interest is charged on a balance that has +/// barely fallen, and the crossover to mostly-principal comes surprisingly +/// late -- past the halfway point of the term for any rate above a few +/// percent. That is the single most counterintuitive fact about a +/// mortgage and it falls straight out of the arithmetic. +/// +/// The final balance is forced to exactly zero, absorbing the accumulated +/// rounding into the last principal payment, which is what a lender does. +/// +/// Errors: +/// As `mortgage_payment`. +/// +/// Rust: `finance::rates::amortization_schedule` +#[pyfunction] +#[pyo3(name = "amortization_schedule", signature = (principal, rate, n))] +pub fn pyfn_amortization_schedule<'py>(py: Python<'py>, principal: f64, rate: f64, n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::finance::rates::amortization_schedule(principal, rate, n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, __x.2, __x.3)).collect::>()) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_discount_factor, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_equivalent_rate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_npv, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_irr, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_xirr, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bond_price, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ytm_solve, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_duration_macaulay, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_duration_modified, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_convexity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bootstrap_zero_curve, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_forward_rate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_nelson_siegel, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ns_fit, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_vasicek_bond_price, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cir_bond_price, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cir_feller_condition, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mortgage_payment, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_amortization_schedule, m)?)?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_finance__risk.rs b/bindings/python/src/generated/m_finance__risk.rs new file mode 100644 index 0000000..d0ef219 --- /dev/null +++ b/bindings/python/src/generated/m_finance__risk.rs @@ -0,0 +1,235 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Historical value at risk: the empirical `alpha` quantile of the losses. +/// +/// No distributional assumption at all -- the sample *is* the +/// distribution. That is its strength and its limit: it cannot produce a +/// loss larger than the worst one observed, so a 99% VaR from two hundred +/// days is estimated from two points and a 99.9% VaR from none. +/// +/// Returned positive for a loss. +/// +/// Errors: +/// Returns an error for fewer than two returns, a non-finite value, or an +/// `alpha` outside `(0, 1)`. +/// +/// Rust: `finance::risk::var_historical` +#[pyfunction] +#[pyo3(name = "var_historical", signature = (returns, alpha))] +pub fn pyfn_var_historical<'py>(py: Python<'py>, returns: Vec, alpha: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::finance::risk::var_historical(&returns, alpha))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Parametric value at risk under a normal distribution: +/// `-(mean + z_alpha * deviation)`. +/// +/// Fits two moments and reads the quantile off a Gaussian. Financial +/// returns are not Gaussian -- they have fat tails and negative skew -- so +/// this understates the tail systematically, and by more the further out +/// you go. At 95% the error is modest; at 99.9% it is a factor. +/// +/// Returned positive for a loss. +/// +/// Errors: +/// Returns an error for fewer than two returns, a non-finite value, an +/// `alpha` outside `(0, 1)`, or a series with no variation. +/// +/// Rust: `finance::risk::var_parametric` +#[pyfunction] +#[pyo3(name = "var_parametric", signature = (returns, alpha))] +pub fn pyfn_var_parametric<'py>(py: Python<'py>, returns: Vec, alpha: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::finance::risk::var_parametric(&returns, alpha))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Historical expected shortfall: the mean loss among the worst `alpha` +/// fraction of returns. +/// +/// Always at least the VaR at the same level, and strictly greater +/// whenever the tail has any spread at all. Unlike VaR it is *coherent* -- +/// in particular subadditive, so combining two portfolios can never make +/// the measured risk exceed the sum of the parts. VaR has no such +/// guarantee and can and does penalise diversification. +/// +/// Returned positive for a loss. +/// +/// Errors: +/// As `var_historical`, plus an `alpha` so small that no observation +/// falls in the tail. +/// +/// Rust: `finance::risk::cvar_historical` +#[pyfunction] +#[pyo3(name = "cvar_historical", signature = (returns, alpha))] +pub fn pyfn_cvar_historical<'py>(py: Python<'py>, returns: Vec, alpha: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::finance::risk::cvar_historical(&returns, alpha))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Cornish-Fisher value at risk: the Gaussian quantile corrected for the +/// sample's skewness and excess kurtosis. +/// +/// The expansion adjusts `z` by terms in the third and fourth moments, +/// which is enough to capture the direction and rough size of a fat tail +/// without fitting a distribution. Two limitations are worth stating +/// plainly, because both bite at ordinary parameters. +/// +/// *The kurtosis term changes sign inside the tail.* Its factor is +/// `z^3 - 3z`, which is zero at `z = -sqrt(3)`, or `alpha` of about 4.2%. +/// So a fat-tailed sample gets a larger VaR at 1% and a *smaller* one at +/// 5%, from the same correction. The expansion is meant for the far tail +/// and behaves sensibly there; near the 5% point the fourth-moment term +/// is doing something close to nothing, and just past it the wrong thing. +/// +/// *It is asymptotic, not convergent.* For mild moments it improves on +/// the Gaussian fit -- with a skew of -0.4 and an excess kurtosis of 0.8 +/// it moves a 1% VaR from 0.0197 to 0.0234 against a historical 0.0300. +/// For large ones it overshoots wildly: at a skew of -4.6 and an excess +/// kurtosis of 33.8 it returns 0.0729 where the sample's own 1% quantile +/// is 0.0309. There is no cheap test that separates the two, so the +/// moments must be checked before the answer is trusted. +/// +/// What *is* checked is the standard validity condition: the corrected +/// quantile must be increasing in `z`, since a quantile function that +/// decreases is not one. That catches the grossest failures and no more. +/// +/// Returned positive for a loss. +/// +/// Errors: +/// Returns an error for fewer than four returns, a non-finite value, an +/// `alpha` outside `(0, 1)`, a series with no variation, or moments large +/// enough to break the expansion. +/// +/// Rust: `finance::risk::var_cornish_fisher` +#[pyfunction] +#[pyo3(name = "var_cornish_fisher", signature = (returns, alpha))] +pub fn pyfn_var_cornish_fisher<'py>(py: Python<'py>, returns: Vec, alpha: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::finance::risk::var_cornish_fisher(&returns, alpha))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// A one-step-ahead parametric VaR from a fitted GARCH(1,1) model. +/// +/// Filters the conditional variance through the sample, projects one step +/// with `omega + alpha r_last^2 + beta sigma_last^2`, and reads a Gaussian +/// quantile off the result. The point is that VaR from a GARCH forecast +/// *responds*: after a volatile week it rises, where an unconditional +/// estimate over the same window barely moves. That responsiveness is +/// what a risk measure is for, and it is also why GARCH VaR breaches +/// cluster less than unconditional VaR breaches do. +/// +/// The Gaussian quantile still understates the tail; GARCH captures the +/// clustering of volatility, not the fatness of the conditional +/// distribution. +/// +/// Returned positive for a loss. +/// +/// Errors: +/// Returns an error for fewer than two returns, a non-finite value, an +/// `alpha` outside `(0, 1)`, or a model whose projected variance is not +/// positive. +/// +/// Rust: `finance::risk::garch_var_forecast` +#[pyfunction] +#[pyo3(name = "garch_var_forecast", signature = (model, returns, alpha))] +pub fn pyfn_garch_var_forecast<'py>(py: Python<'py>, model: crate::generated::types::PyGarch11Arg, returns: Vec, alpha: f64) -> PyResult { + let model = model.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::finance::risk::garch_var_forecast(&model, &returns, alpha))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Backtests a moving-average crossover: long while the fast average is +/// above the slow one, flat otherwise. +/// +/// Both averages are computed on the closing prices up to and including +/// the current bar, and the resulting position is applied to the *next* +/// bar's return. Applying it to the same bar would use the close to decide +/// a trade executed at that close, which is the commonest way a backtest +/// invents returns that were never available. +/// +/// There are no costs, no slippage and no borrowing charge, so the result +/// is an upper bound on what the rule could have earned rather than an +/// estimate of it. A crossover rule trades often enough that realistic +/// costs frequently reverse its sign. +/// +/// Errors: +/// Returns an error for a non-positive price, fewer prices than the slow +/// window needs, a zero window, or a fast window at or above the slow one. +/// +/// Rust: `finance::risk::backtest_sma_crossover` +#[pyfunction] +#[pyo3(name = "backtest_sma_crossover", signature = (prices, fast, slow))] +pub fn pyfn_backtest_sma_crossover(prices: Vec, fast: usize, slow: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::finance::risk::backtest_sma_crossover(&prices, fast, slow)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyBacktestStats { inner: __v }) +} + +/// Kupiec's unconditional coverage test: does the observed breach count +/// match the VaR model's claimed `alpha`? +/// +/// The likelihood ratio statistic is chi-squared with one degree of +/// freedom under the null that breaches occur at exactly rate `alpha`. A +/// small p-value means the model is miscalibrated -- too many breaches +/// and it understates risk, too few and it overstates it and wastes +/// capital. +/// +/// What it cannot see is *clustering*. A model that breaches on ten +/// consecutive days and never again can pass Kupiec with the right total, +/// while being useless: the breaches should be independent, and testing +/// that needs Christoffersen's conditional coverage test, which this is +/// only half of. +/// +/// Errors: +/// Returns an error for no observations, more breaches than observations, +/// or an `alpha` outside `(0, 1)`. +/// +/// Rust: `finance::risk::kupiec_test` +#[pyfunction] +#[pyo3(name = "kupiec_test", signature = (violations, observations, alpha))] +pub fn pyfn_kupiec_test(violations: usize, observations: usize, alpha: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::finance::risk::kupiec_test(violations, observations, alpha)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyTestResult { inner: __v }) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_var_historical, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_var_parametric, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cvar_historical, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_var_cornish_fisher, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_garch_var_forecast, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_backtest_sma_crossover, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kupiec_test, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_fluid_instabilities.rs b/bindings/python/src/generated/m_fluid_instabilities.rs new file mode 100644 index 0000000..d31e5a9 --- /dev/null +++ b/bindings/python/src/generated/m_fluid_instabilities.rs @@ -0,0 +1,284 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Growth rate of the Rayleigh-Taylor instability: γ = √(A g k). +/// +/// `atwood` is the Atwood number A = (ρ₂ − ρ₁)/(ρ₂ + ρ₁), `g` the gravitational +/// acceleration, and `wavenumber` the perturbation wavenumber k. +/// +/// Rust: `fluid_instabilities::rayleigh_taylor_growth_rate` +#[pyfunction] +#[pyo3(name = "rayleigh_taylor_growth_rate", signature = (g, atwood, wavenumber))] +pub fn pyfn_rayleigh_taylor_growth_rate(g: f64, atwood: f64, wavenumber: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluid_instabilities::rayleigh_taylor_growth_rate(g, atwood, wavenumber)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Atwood number: A = (ρ_heavy − ρ_light) / (ρ_heavy + ρ_light). +/// +/// Ranges from −1 to 1. Positive when `density_heavy > density_light`. +/// +/// Rust: `fluid_instabilities::atwood_number` +#[pyfunction] +#[pyo3(name = "atwood_number", signature = (density_heavy, density_light))] +pub fn pyfn_atwood_number(density_heavy: f64, density_light: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluid_instabilities::atwood_number(density_heavy, density_light)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Critical wavelength for capillary stabilization of RT instability: +/// λ_c = 2π √(σ / (Δρ g)). +/// +/// Perturbations shorter than λ_c are stabilized by surface tension σ. +/// +/// Rust: `fluid_instabilities::rt_critical_wavelength` +#[pyfunction] +#[pyo3(name = "rt_critical_wavelength", signature = (surface_tension, density_diff, g))] +pub fn pyfn_rt_critical_wavelength(surface_tension: f64, density_diff: f64, g: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluid_instabilities::rt_critical_wavelength(surface_tension, density_diff, g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Most unstable RT wavelength: λ_max = √3 × λ_c. +/// +/// Rust: `fluid_instabilities::rt_most_unstable_wavelength` +#[pyfunction] +#[pyo3(name = "rt_most_unstable_wavelength", signature = (surface_tension, density_diff, g))] +pub fn pyfn_rt_most_unstable_wavelength(surface_tension: f64, density_diff: f64, g: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluid_instabilities::rt_most_unstable_wavelength(surface_tension, density_diff, g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Growth rate of the Kelvin-Helmholtz instability: +/// γ = k |ΔV| √(ρ₁ρ₂) / (ρ₁ + ρ₂). +/// +/// Rust: `fluid_instabilities::kh_growth_rate` +#[pyfunction] +#[pyo3(name = "kh_growth_rate", signature = (density1, density2, velocity_diff, wavenumber))] +pub fn pyfn_kh_growth_rate(density1: f64, density2: f64, velocity_diff: f64, wavenumber: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluid_instabilities::kh_growth_rate(density1, density2, velocity_diff, wavenumber)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Critical velocity difference for onset of KH instability: +/// ΔV_c² = (ρ₁ + ρ₂)/(ρ₁ρ₂) × [g(ρ₂ − ρ₁)/k + σk]. +/// +/// Rust: `fluid_instabilities::kh_critical_velocity` +#[pyfunction] +#[pyo3(name = "kh_critical_velocity", signature = (density1, density2, surface_tension, wavenumber, g))] +pub fn pyfn_kh_critical_velocity(density1: f64, density2: f64, surface_tension: f64, wavenumber: f64, g: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluid_instabilities::kh_critical_velocity(density1, density2, surface_tension, wavenumber, g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Thermal Rayleigh number: Ra = g β ΔT H³ / (ν α). +/// +/// `beta` is the thermal expansion coefficient, `delta_t` the temperature +/// difference across the layer, `height` the layer thickness, `kinematic_viscosity` +/// is ν, and `thermal_diffusivity` is α. +/// +/// Rust: `fluid_instabilities::rayleigh_number_thermal` +#[pyfunction] +#[pyo3(name = "rayleigh_number_thermal", signature = (g, beta, delta_t, height, kinematic_viscosity, thermal_diffusivity))] +pub fn pyfn_rayleigh_number_thermal(g: f64, beta: f64, delta_t: f64, height: f64, kinematic_viscosity: f64, thermal_diffusivity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluid_instabilities::rayleigh_number_thermal(g, beta, delta_t, height, kinematic_viscosity, thermal_diffusivity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Critical Rayleigh number for rigid-rigid boundaries: Ra_c = 1708. +/// +/// Rust: `fluid_instabilities::critical_rayleigh_number` +#[pyfunction] +#[pyo3(name = "critical_rayleigh_number", signature = ())] +pub fn pyfn_critical_rayleigh_number() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluid_instabilities::critical_rayleigh_number()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Nusselt number from Rayleigh number for turbulent natural convection +/// (simplified for air, Pr ≈ 0.71): Nu = 0.069 × Ra^(1/3). +/// +/// Returns 1.0 (pure conduction) when Ra < Ra_c. +/// +/// Rust: `fluid_instabilities::nusselt_from_rayleigh` +#[pyfunction] +#[pyo3(name = "nusselt_from_rayleigh", signature = (rayleigh))] +pub fn pyfn_nusselt_from_rayleigh(rayleigh: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluid_instabilities::nusselt_from_rayleigh(rayleigh)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Returns `true` if the Rayleigh number exceeds the critical value (Ra > 1708), +/// indicating onset of convective motion. +/// +/// Rust: `fluid_instabilities::is_convecting` +#[pyfunction] +#[pyo3(name = "is_convecting", signature = (rayleigh))] +pub fn pyfn_is_convecting(rayleigh: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluid_instabilities::is_convecting(rayleigh)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Jeans length: λ_J = c_s √(π / (G ρ)). +/// +/// Density perturbations larger than λ_J undergo gravitational collapse. +/// +/// Rust: `fluid_instabilities::jeans_length` +#[pyfunction] +#[pyo3(name = "jeans_length", signature = (sound_speed, density))] +pub fn pyfn_jeans_length(sound_speed: f64, density: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluid_instabilities::jeans_length(sound_speed, density)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Jeans mass: M_J = (π/6) ρ λ_J³. +/// +/// Rust: `fluid_instabilities::jeans_mass` +#[pyfunction] +#[pyo3(name = "jeans_mass", signature = (sound_speed, density))] +pub fn pyfn_jeans_mass(sound_speed: f64, density: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluid_instabilities::jeans_mass(sound_speed, density)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Jeans angular frequency: ω_J = √(4πGρ). +/// +/// This is the frequency at the Jeans wavenumber k_J where ω² = k²c_s² − 4πGρ = 0. +/// +/// Rust: `fluid_instabilities::jeans_frequency` +#[pyfunction] +#[pyo3(name = "jeans_frequency", signature = (sound_speed, density))] +pub fn pyfn_jeans_frequency(sound_speed: f64, density: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluid_instabilities::jeans_frequency(sound_speed, density)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Growth rate of the Plateau-Rayleigh instability (simplified): +/// γ² = (σ / (ρ r³)) × (kr)(1 − (kr)²). +/// +/// Valid for kr < 1 (long-wavelength regime). Returns 0 for kr ≥ 1. +/// +/// Rust: `fluid_instabilities::plateau_rayleigh_growth_rate` +#[pyfunction] +#[pyo3(name = "plateau_rayleigh_growth_rate", signature = (surface_tension, density, radius, wavenumber))] +pub fn pyfn_plateau_rayleigh_growth_rate(surface_tension: f64, density: f64, radius: f64, wavenumber: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluid_instabilities::plateau_rayleigh_growth_rate(surface_tension, density, radius, wavenumber)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Critical wavelength for Plateau-Rayleigh instability: λ_c = 2πr. +/// +/// Perturbations with wavelength > λ_c (i.e., longer than the circumference) are unstable. +/// +/// Rust: `fluid_instabilities::plateau_rayleigh_critical_wavelength` +#[pyfunction] +#[pyo3(name = "plateau_rayleigh_critical_wavelength", signature = (radius))] +pub fn pyfn_plateau_rayleigh_critical_wavelength(radius: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluid_instabilities::plateau_rayleigh_critical_wavelength(radius)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Most unstable wavelength for Plateau-Rayleigh instability: λ_max ≈ 9.02 r. +/// +/// Rust: `fluid_instabilities::plateau_rayleigh_most_unstable` +#[pyfunction] +#[pyo3(name = "plateau_rayleigh_most_unstable", signature = (radius))] +pub fn pyfn_plateau_rayleigh_most_unstable(radius: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluid_instabilities::plateau_rayleigh_most_unstable(radius)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Linear growth rate of the Richtmyer-Meshkov instability: +/// dh/dt = A × ΔV × k × h₀. +/// +/// Unlike RT, RM growth is linear (not exponential). Returns the velocity +/// of the perturbation amplitude growth for unit initial amplitude (h₀ = 1). +/// +/// Rust: `fluid_instabilities::richtmyer_meshkov_growth_rate` +#[pyfunction] +#[pyo3(name = "richtmyer_meshkov_growth_rate", signature = (atwood, velocity_jump, wavenumber))] +pub fn pyfn_richtmyer_meshkov_growth_rate(atwood: f64, velocity_jump: f64, wavenumber: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluid_instabilities::richtmyer_meshkov_growth_rate(atwood, velocity_jump, wavenumber)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Gradient Richardson number: Ri = g (dρ/dz) / (ρ (du/dz)²). +/// +/// `density_gradient` is dρ/dz and `velocity_gradient` is du/dz. +/// +/// Rust: `fluid_instabilities::richardson_number` +#[pyfunction] +#[pyo3(name = "richardson_number", signature = (g, density_gradient, density, velocity_gradient))] +pub fn pyfn_richardson_number(g: f64, density_gradient: f64, density: f64, velocity_gradient: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluid_instabilities::richardson_number(g, density_gradient, density, velocity_gradient)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Returns `true` if the gradient Richardson number exceeds the critical value +/// of 0.25, indicating dynamic stability against shear-driven turbulence. +/// +/// Rust: `fluid_instabilities::is_dynamically_stable` +#[pyfunction] +#[pyo3(name = "is_dynamically_stable", signature = (richardson))] +pub fn pyfn_is_dynamically_stable(richardson: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluid_instabilities::is_dynamically_stable(richardson)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_rayleigh_taylor_growth_rate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_atwood_number, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rt_critical_wavelength, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rt_most_unstable_wavelength, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kh_growth_rate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kh_critical_velocity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rayleigh_number_thermal, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_critical_rayleigh_number, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_nusselt_from_rayleigh, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_is_convecting, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_jeans_length, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_jeans_mass, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_jeans_frequency, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_plateau_rayleigh_growth_rate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_plateau_rayleigh_critical_wavelength, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_plateau_rayleigh_most_unstable, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_richtmyer_meshkov_growth_rate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_richardson_number, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_is_dynamically_stable, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_fluids.rs b/bindings/python/src/generated/m_fluids.rs new file mode 100644 index 0000000..327b104 --- /dev/null +++ b/bindings/python/src/generated/m_fluids.rs @@ -0,0 +1,567 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Hydrostatic pressure: P = ρ * g * h +/// +/// Rust: `fluids::hydrostatic_pressure` +#[pyfunction] +#[pyo3(name = "hydrostatic_pressure", signature = (density, g, depth))] +pub fn pyfn_hydrostatic_pressure(density: f64, g: f64, depth: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::hydrostatic_pressure(density, g, depth)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Total pressure at depth: P = P_atm + ρ * g * h +/// +/// Rust: `fluids::total_pressure` +#[pyfunction] +#[pyo3(name = "total_pressure", signature = (atmospheric_pressure, density, g, depth))] +pub fn pyfn_total_pressure(atmospheric_pressure: f64, density: f64, g: f64, depth: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::total_pressure(atmospheric_pressure, density, g, depth)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Pascal's principle: F2 = F1 * (A2 / A1) +/// +/// Rust: `fluids::pascal_force` +#[pyfunction] +#[pyo3(name = "pascal_force", signature = (f1, a1, a2))] +pub fn pyfn_pascal_force(f1: f64, a1: f64, a2: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::pascal_force(f1, a1, a2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Pressure: P = F / A +/// +/// Rust: `fluids::pressure` +#[pyfunction] +#[pyo3(name = "pressure", signature = (force, area))] +pub fn pyfn_pressure(force: f64, area: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::pressure(force, area)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Buoyant force (Archimedes' principle): F_b = ρ_fluid * V_displaced * g +/// +/// Rust: `fluids::buoyant_force` +#[pyfunction] +#[pyo3(name = "buoyant_force", signature = (fluid_density, displaced_volume, g))] +pub fn pyfn_buoyant_force(fluid_density: f64, displaced_volume: f64, g: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::buoyant_force(fluid_density, displaced_volume, g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Fraction of object submerged (floating): f = ρ_object / ρ_fluid +/// +/// Rust: `fluids::fraction_submerged` +#[pyfunction] +#[pyo3(name = "fraction_submerged", signature = (object_density, fluid_density))] +pub fn pyfn_fraction_submerged(object_density: f64, fluid_density: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::fraction_submerged(object_density, fluid_density)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Apparent weight in fluid: W_app = W - F_b = mg - ρ_fluid * V * g +/// +/// Rust: `fluids::apparent_weight` +#[pyfunction] +#[pyo3(name = "apparent_weight", signature = (mass, object_volume, fluid_density, g))] +pub fn pyfn_apparent_weight(mass: f64, object_volume: f64, fluid_density: f64, g: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::apparent_weight(mass, object_volume, fluid_density, g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Continuity equation: A1 * v1 = A2 * v2 → v2 = A1 * v1 / A2 +/// +/// Rust: `fluids::continuity_velocity` +#[pyfunction] +#[pyo3(name = "continuity_velocity", signature = (a1, v1, a2))] +pub fn pyfn_continuity_velocity(a1: f64, v1: f64, a2: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::continuity_velocity(a1, v1, a2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Volume flow rate: Q = A * v +/// +/// Rust: `fluids::flow_rate` +#[pyfunction] +#[pyo3(name = "flow_rate", signature = (area, velocity))] +pub fn pyfn_flow_rate(area: f64, velocity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::flow_rate(area, velocity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Mass flow rate: ṁ = ρ * A * v +/// +/// Rust: `fluids::mass_flow_rate` +#[pyfunction] +#[pyo3(name = "mass_flow_rate", signature = (density, area, velocity))] +pub fn pyfn_mass_flow_rate(density: f64, area: f64, velocity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::mass_flow_rate(density, area, velocity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Bernoulli's equation: P1 + 0.5*ρ*v1^2 + ρ*g*h1 = P2 + 0.5*ρ*v2^2 + ρ*g*h2 +/// Returns P2 given all other quantities. +/// +/// Rust: `fluids::bernoulli_pressure` +#[pyfunction] +#[pyo3(name = "bernoulli_pressure", signature = (p1, density, v1, h1, v2, h2, g))] +pub fn pyfn_bernoulli_pressure(p1: f64, density: f64, v1: f64, h1: f64, v2: f64, h2: f64, g: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::bernoulli_pressure(p1, density, v1, h1, v2, h2, g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Torricelli's theorem: v = sqrt(2 * g * h) +/// +/// Rust: `fluids::torricelli_velocity` +#[pyfunction] +#[pyo3(name = "torricelli_velocity", signature = (g, height))] +pub fn pyfn_torricelli_velocity(g: f64, height: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::torricelli_velocity(g, height)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Venturi effect velocity from pressure difference: +/// v2 = sqrt(2 * (P1 - P2) / (ρ * (1 - (A2/A1)^2))) +/// +/// Rust: `fluids::venturi_velocity` +#[pyfunction] +#[pyo3(name = "venturi_velocity", signature = (p1, p2, density, a1, a2))] +pub fn pyfn_venturi_velocity(p1: f64, p2: f64, density: f64, a1: f64, a2: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::venturi_velocity(p1, p2, density, a1, a2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Drag force: F_d = 0.5 * C_d * ρ * A * v^2 +/// +/// Rust: `fluids::drag_force` +#[pyfunction] +#[pyo3(name = "drag_force", signature = (drag_coefficient, density, area, velocity))] +pub fn pyfn_drag_force(drag_coefficient: f64, density: f64, area: f64, velocity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::drag_force(drag_coefficient, density, area, velocity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Terminal velocity: v_t = sqrt(2 * m * g / (ρ * A * C_d)) +/// +/// Rust: `fluids::terminal_velocity` +#[pyfunction] +#[pyo3(name = "terminal_velocity", signature = (mass, g, density, area, drag_coefficient))] +pub fn pyfn_terminal_velocity(mass: f64, g: f64, density: f64, area: f64, drag_coefficient: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::terminal_velocity(mass, g, density, area, drag_coefficient)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Stokes' drag (low Reynolds number): F = 6π * μ * r * v +/// +/// Rust: `fluids::stokes_drag` +#[pyfunction] +#[pyo3(name = "stokes_drag", signature = (dynamic_viscosity, radius, velocity))] +pub fn pyfn_stokes_drag(dynamic_viscosity: f64, radius: f64, velocity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::stokes_drag(dynamic_viscosity, radius, velocity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Reynolds number: Re = ρ * v * L / μ +/// +/// Rust: `fluids::reynolds_number` +#[pyfunction] +#[pyo3(name = "reynolds_number", signature = (density, velocity, length, dynamic_viscosity))] +pub fn pyfn_reynolds_number(density: f64, velocity: f64, length: f64, dynamic_viscosity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::reynolds_number(density, velocity, length, dynamic_viscosity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Poiseuille's law (volume flow rate through a pipe): +/// Q = π * r^4 * ΔP / (8 * μ * L) +/// +/// Rust: `fluids::poiseuille_flow_rate` +#[pyfunction] +#[pyo3(name = "poiseuille_flow_rate", signature = (radius, pressure_drop, dynamic_viscosity, length))] +pub fn pyfn_poiseuille_flow_rate(radius: f64, pressure_drop: f64, dynamic_viscosity: f64, length: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::poiseuille_flow_rate(radius, pressure_drop, dynamic_viscosity, length)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Surface tension force along a line: F = γ * L +/// +/// Rust: `fluids::surface_tension_force` +#[pyfunction] +#[pyo3(name = "surface_tension_force", signature = (surface_tension, length))] +pub fn pyfn_surface_tension_force(surface_tension: f64, length: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::surface_tension_force(surface_tension, length)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Capillary rise: h = 2 * γ * cos(θ) / (ρ * g * r) +/// +/// Rust: `fluids::capillary_rise` +#[pyfunction] +#[pyo3(name = "capillary_rise", signature = (surface_tension, contact_angle_rad, density, g, tube_radius))] +pub fn pyfn_capillary_rise(surface_tension: f64, contact_angle_rad: f64, density: f64, g: f64, tube_radius: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::capillary_rise(surface_tension, contact_angle_rad, density, g, tube_radius)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Mach number: M = v / a +/// +/// Rust: `fluids::mach_number` +#[pyfunction] +#[pyo3(name = "mach_number", signature = (velocity, speed_of_sound))] +pub fn pyfn_mach_number(velocity: f64, speed_of_sound: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::mach_number(velocity, speed_of_sound)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Dynamic pressure: q = ½ρv² +/// +/// Rust: `fluids::dynamic_pressure` +#[pyfunction] +#[pyo3(name = "dynamic_pressure", signature = (density, velocity))] +pub fn pyfn_dynamic_pressure(density: f64, velocity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::dynamic_pressure(density, velocity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Stagnation pressure: P₀ = P + q +/// +/// Rust: `fluids::stagnation_pressure` +#[pyfunction] +#[pyo3(name = "stagnation_pressure", signature = (static_pressure, dynamic_pressure))] +pub fn pyfn_stagnation_pressure(static_pressure: f64, dynamic_pressure: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::stagnation_pressure(static_pressure, dynamic_pressure)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Isentropic pressure ratio: P/P₀ = (1 + (γ-1)/2 × M²)^(-γ/(γ-1)) +/// +/// Rust: `fluids::isentropic_pressure_ratio` +#[pyfunction] +#[pyo3(name = "isentropic_pressure_ratio", signature = (mach, gamma))] +pub fn pyfn_isentropic_pressure_ratio(mach: f64, gamma: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::isentropic_pressure_ratio(mach, gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Isentropic temperature ratio: T/T₀ = (1 + (γ-1)/2 × M²)^(-1) +/// +/// Rust: `fluids::isentropic_temperature_ratio` +#[pyfunction] +#[pyo3(name = "isentropic_temperature_ratio", signature = (mach, gamma))] +pub fn pyfn_isentropic_temperature_ratio(mach: f64, gamma: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::isentropic_temperature_ratio(mach, gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Vorticity in 2D: ω = ∂v_y/∂x - ∂v_x/∂y +/// +/// Rust: `fluids::vorticity_2d` +#[pyfunction] +#[pyo3(name = "vorticity_2d", signature = (dvx_dy, dvy_dx))] +pub fn pyfn_vorticity_2d(dvx_dy: f64, dvy_dx: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::vorticity_2d(dvx_dy, dvy_dx)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Circulation (uniform vorticity): Γ = ω × A +/// +/// Rust: `fluids::circulation` +#[pyfunction] +#[pyo3(name = "circulation", signature = (vorticity, area))] +pub fn pyfn_circulation(vorticity: f64, area: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::circulation(vorticity, area)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Kutta-Joukowski lift per unit span: L = ρ × V × Γ +/// +/// Rust: `fluids::kutta_joukowski_lift` +#[pyfunction] +#[pyo3(name = "kutta_joukowski_lift", signature = (density, velocity, circulation))] +pub fn pyfn_kutta_joukowski_lift(density: f64, velocity: f64, circulation: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::kutta_joukowski_lift(density, velocity, circulation)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Kinematic viscosity: ν = μ/ρ +/// +/// Rust: `fluids::kinematic_viscosity` +#[pyfunction] +#[pyo3(name = "kinematic_viscosity", signature = (dynamic_viscosity, density))] +pub fn pyfn_kinematic_viscosity(dynamic_viscosity: f64, density: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::kinematic_viscosity(dynamic_viscosity, density)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Pressure gradient in a pipe (Poiseuille inverse): dP = 8μLQ/(πr⁴) +/// +/// Rust: `fluids::pressure_gradient_pipe` +#[pyfunction] +#[pyo3(name = "pressure_gradient_pipe", signature = (flow_rate, dynamic_viscosity, radius, length))] +pub fn pyfn_pressure_gradient_pipe(flow_rate: f64, dynamic_viscosity: f64, radius: f64, length: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::pressure_gradient_pipe(flow_rate, dynamic_viscosity, radius, length)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Hydraulic diameter: D_h = 4A/P +/// +/// Rust: `fluids::hydraulic_diameter` +#[pyfunction] +#[pyo3(name = "hydraulic_diameter", signature = (area, wetted_perimeter))] +pub fn pyfn_hydraulic_diameter(area: f64, wetted_perimeter: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::hydraulic_diameter(area, wetted_perimeter)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Darcy friction factor for laminar pipe flow: f = 64/Re +/// +/// Rust: `fluids::darcy_friction_factor_laminar` +#[pyfunction] +#[pyo3(name = "darcy_friction_factor_laminar", signature = (reynolds))] +pub fn pyfn_darcy_friction_factor_laminar(reynolds: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::darcy_friction_factor_laminar(reynolds)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Darcy-Weisbach head loss: h_L = f × (L/D) × v²/(2g) +/// +/// Rust: `fluids::darcy_weisbach_head_loss` +#[pyfunction] +#[pyo3(name = "darcy_weisbach_head_loss", signature = (friction_factor, length, diameter, velocity, g))] +pub fn pyfn_darcy_weisbach_head_loss(friction_factor: f64, length: f64, diameter: f64, velocity: f64, g: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::darcy_weisbach_head_loss(friction_factor, length, diameter, velocity, g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Characteristic buoyancy velocity: v = √(gβΔTL) +/// +/// Rust: `fluids::buoyancy_velocity` +#[pyfunction] +#[pyo3(name = "buoyancy_velocity", signature = (g, beta, delta_temp, length))] +pub fn pyfn_buoyancy_velocity(g: f64, beta: f64, delta_temp: f64, length: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::buoyancy_velocity(g, beta, delta_temp, length)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Thermal expansion coefficient for ideal gas: β = 1/T +/// +/// Rust: `fluids::thermal_expansion_coefficient_ideal_gas` +#[pyfunction] +#[pyo3(name = "thermal_expansion_coefficient_ideal_gas", signature = (temperature))] +pub fn pyfn_thermal_expansion_coefficient_ideal_gas(temperature: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::thermal_expansion_coefficient_ideal_gas(temperature)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Sutherland's law for viscosity: μ = μ₀ × (T/T₀)^(3/2) × (T₀ + S)/(T + S) +/// +/// Rust: `fluids::viscosity_sutherland` +#[pyfunction] +#[pyo3(name = "viscosity_sutherland", signature = (mu0, t0, t, s))] +pub fn pyfn_viscosity_sutherland(mu0: f64, t0: f64, t: f64, s: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::viscosity_sutherland(mu0, t0, t, s)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Sutherland's law for thermal conductivity (same form as viscosity) +/// +/// Rust: `fluids::thermal_conductivity_gas` +#[pyfunction] +#[pyo3(name = "thermal_conductivity_gas", signature = (k0, t0, t, s))] +pub fn pyfn_thermal_conductivity_gas(k0: f64, t0: f64, t: f64, s: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::thermal_conductivity_gas(k0, t0, t, s)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Churchill-Chu correlation for natural convection on a vertical plate (air, Pr≈0.71): +/// Nu = (0.825 + 0.387 × Ra^(1/6) / 1.1936)² +/// +/// Rust: `fluids::natural_convection_nu_vertical` +#[pyfunction] +#[pyo3(name = "natural_convection_nu_vertical", signature = (rayleigh))] +pub fn pyfn_natural_convection_nu_vertical(rayleigh: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::natural_convection_nu_vertical(rayleigh)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Natural convection Nusselt number for hot horizontal plate facing up: +/// Nu = 0.54 × Ra^(1/4), valid for 10⁴ ≤ Ra ≤ 10⁷ +/// +/// Rust: `fluids::natural_convection_nu_horizontal_hot` +#[pyfunction] +#[pyo3(name = "natural_convection_nu_horizontal_hot", signature = (rayleigh))] +pub fn pyfn_natural_convection_nu_horizontal_hot(rayleigh: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::natural_convection_nu_horizontal_hot(rayleigh)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Marangoni number: Ma = -(dσ/dT) × L × ΔT / (μ × α) +/// +/// Rust: `fluids::marangoni_number` +#[pyfunction] +#[pyo3(name = "marangoni_number", signature = (surface_tension_gradient, length, delta_temp, dynamic_viscosity, thermal_diffusivity))] +pub fn pyfn_marangoni_number(surface_tension_gradient: f64, length: f64, delta_temp: f64, dynamic_viscosity: f64, thermal_diffusivity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::marangoni_number(surface_tension_gradient, length, delta_temp, dynamic_viscosity, thermal_diffusivity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Bond number: Bo = Δρ × g × L² / σ (gravity vs surface tension) +/// +/// Rust: `fluids::bond_number` +#[pyfunction] +#[pyo3(name = "bond_number", signature = (density_diff, g, length, surface_tension))] +pub fn pyfn_bond_number(density_diff: f64, g: f64, length: f64, surface_tension: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::bond_number(density_diff, g, length, surface_tension)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Weber number: We = ρv²L / σ (inertia vs surface tension) +/// +/// Rust: `fluids::weber_number` +#[pyfunction] +#[pyo3(name = "weber_number", signature = (density, velocity, length, surface_tension))] +pub fn pyfn_weber_number(density: f64, velocity: f64, length: f64, surface_tension: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::weber_number(density, velocity, length, surface_tension)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Froude number: Fr = v / √(gL) (inertia vs gravity in free surface flow) +/// +/// Rust: `fluids::froude_number` +#[pyfunction] +#[pyo3(name = "froude_number", signature = (velocity, g, length))] +pub fn pyfn_froude_number(velocity: f64, g: f64, length: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::froude_number(velocity, g, length)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Archimedes number: Ar = g × d³ × ρf × (ρp - ρf) / μ² +/// +/// Rust: `fluids::archimedes_number` +#[pyfunction] +#[pyo3(name = "archimedes_number", signature = (density_fluid, density_particle, diameter, dynamic_viscosity, g))] +pub fn pyfn_archimedes_number(density_fluid: f64, density_particle: f64, diameter: f64, dynamic_viscosity: f64, g: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::archimedes_number(density_fluid, density_particle, diameter, dynamic_viscosity, g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Peclet number: Pe = vL/α (advection vs diffusion) +/// +/// Rust: `fluids::peclet_number` +#[pyfunction] +#[pyo3(name = "peclet_number", signature = (velocity, length, diffusivity))] +pub fn pyfn_peclet_number(velocity: f64, length: f64, diffusivity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fluids::peclet_number(velocity, length, diffusivity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_hydrostatic_pressure, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_total_pressure, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pascal_force, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pressure, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_buoyant_force, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fraction_submerged, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_apparent_weight, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_continuity_velocity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_flow_rate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mass_flow_rate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bernoulli_pressure, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_torricelli_velocity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_venturi_velocity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_drag_force, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_terminal_velocity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_stokes_drag, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_reynolds_number, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_poiseuille_flow_rate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_surface_tension_force, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_capillary_rise, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mach_number, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dynamic_pressure, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_stagnation_pressure, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_isentropic_pressure_ratio, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_isentropic_temperature_ratio, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_vorticity_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_circulation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kutta_joukowski_lift, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kinematic_viscosity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pressure_gradient_pipe, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hydraulic_diameter, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_darcy_friction_factor_laminar, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_darcy_weisbach_head_loss, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_buoyancy_velocity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_thermal_expansion_coefficient_ideal_gas, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_viscosity_sutherland, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_thermal_conductivity_gas, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_natural_convection_nu_vertical, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_natural_convection_nu_horizontal_hot, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_marangoni_number, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bond_number, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_weber_number, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_froude_number, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_archimedes_number, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_peclet_number, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_fractals.rs b/bindings/python/src/generated/m_fractals.rs new file mode 100644 index 0000000..8ba64e1 --- /dev/null +++ b/bindings/python/src/generated/m_fractals.rs @@ -0,0 +1,208 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Mandelbrot escape-time iteration count: z_{n+1} = z_n² + c, returns iterations to escape |z| > 2. +/// +/// Rust: `fractals::mandelbrot_iterations` +#[pyfunction] +#[pyo3(name = "mandelbrot_iterations", signature = (c_re, c_im, max_iter))] +pub fn pyfn_mandelbrot_iterations(c_re: f64, c_im: f64, max_iter: u32) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::mandelbrot_iterations(c_re, c_im, max_iter)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Smooth Mandelbrot iteration count using continuous escape-time coloring. +/// +/// Rust: `fractals::mandelbrot_smooth` +#[pyfunction] +#[pyo3(name = "mandelbrot_smooth", signature = (c_re, c_im, max_iter))] +pub fn pyfn_mandelbrot_smooth(c_re: f64, c_im: f64, max_iter: u32) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::mandelbrot_smooth(c_re, c_im, max_iter)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Compute Mandelbrot iteration counts for an entire grid of pixels. +/// +/// Rust: `fractals::mandelbrot_grid` +#[pyfunction] +#[pyo3(name = "mandelbrot_grid", signature = (x_min, x_max, y_min, y_max, width, height, max_iter))] +pub fn pyfn_mandelbrot_grid<'py>(py: Python<'py>, x_min: f64, x_max: f64, y_min: f64, y_max: f64, width: usize, height: usize, max_iter: u32) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fractals::mandelbrot_grid(x_min, x_max, y_min, y_max, width, height, max_iter))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Julia set escape-time iteration count for a fixed c: z_{n+1} = z_n² + c. +/// +/// Rust: `fractals::julia_iterations` +#[pyfunction] +#[pyo3(name = "julia_iterations", signature = (z_re, z_im, c_re, c_im, max_iter))] +pub fn pyfn_julia_iterations(z_re: f64, z_im: f64, c_re: f64, c_im: f64, max_iter: u32) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::julia_iterations(z_re, z_im, c_re, c_im, max_iter)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Compute Julia set iteration counts for an entire grid of pixels. +/// +/// Rust: `fractals::julia_grid` +#[pyfunction] +#[pyo3(name = "julia_grid", signature = (c_re, c_im, x_min, x_max, y_min, y_max, width, height, max_iter))] +pub fn pyfn_julia_grid<'py>(py: Python<'py>, c_re: f64, c_im: f64, x_min: f64, x_max: f64, y_min: f64, y_max: f64, width: usize, height: usize, max_iter: u32) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fractals::julia_grid(c_re, c_im, x_min, x_max, y_min, y_max, width, height, max_iter))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Burning Ship fractal iteration count: z_{n+1} = (|Re(z_n)| + i|Im(z_n)|)² + c. +/// +/// Rust: `fractals::burning_ship_iterations` +#[pyfunction] +#[pyo3(name = "burning_ship_iterations", signature = (c_re, c_im, max_iter))] +pub fn pyfn_burning_ship_iterations(c_re: f64, c_im: f64, max_iter: u32) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::burning_ship_iterations(c_re, c_im, max_iter)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Newton fractal for f(z) = z³ - 1: returns (iterations, root_index) for convergence. +/// +/// Rust: `fractals::newton_fractal_iterations` +#[pyfunction] +#[pyo3(name = "newton_fractal_iterations", signature = (z_re, z_im, max_iter, tolerance))] +pub fn pyfn_newton_fractal_iterations(z_re: f64, z_im: f64, max_iter: u32, tolerance: f64) -> PyResult<(u32, u32)> { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::newton_fractal_iterations(z_re, z_im, max_iter, tolerance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Generate Sierpinski triangle points via the chaos game (iterated function system). +/// +/// Rust: `fractals::sierpinski_point` +#[pyfunction] +#[pyo3(name = "sierpinski_point", signature = (x, y, iterations))] +pub fn pyfn_sierpinski_point<'py>(py: Python<'py>, x: f64, y: f64, iterations: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fractals::sierpinski_point(x, y, iterations))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Generate Barnsley fern points via the iterated function system with four affine maps. +/// +/// Rust: `fractals::barnsley_fern_point` +#[pyfunction] +#[pyo3(name = "barnsley_fern_point", signature = (x, y, iterations))] +pub fn pyfn_barnsley_fern_point<'py>(py: Python<'py>, x: f64, y: f64, iterations: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fractals::barnsley_fern_point(x, y, iterations))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Count occupied grid cells for box-counting fractal dimension estimation. +/// +/// Rust: `fractals::box_count_2d` +#[pyfunction] +#[pyo3(name = "box_count_2d", signature = (points, grid_size, bounds))] +pub fn pyfn_box_count_2d<'py>(py: Python<'py>, points: Vec<(f64, f64)>, grid_size: usize, bounds: (f64, f64, f64, f64)) -> PyResult { + let points = points.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let bounds = (bounds.0, bounds.1, bounds.2, bounds.3); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fractals::box_count_2d(&points, grid_size, bounds))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Create a complex number from real and imaginary parts. +/// +/// Rust: `fractals::Complex::new` +#[pyfunction] +#[pyo3(name = "new", signature = (re, im))] +pub fn pyfn_complex_new<'py>(py: Python<'py>, re: f64, im: f64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::Complex::new(re, im)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::complex_out(py, __v)) +} + +/// Squared norm (modulus squared): |z|² = re² + im² +/// +/// Rust: `fractals::Complex::norm_sq` +#[pyfunction] +#[pyo3(name = "norm_sq", signature = (z))] +pub fn pyfn_complex_norm_sq(z: crate::runtime::coerce::ComplexArg) -> PyResult { + let z = z.0; + let __r = crate::runtime::guard(|| z.norm_sq()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Norm (modulus): |z| = √(re² + im²) +/// +/// Rust: `fractals::Complex::norm` +#[pyfunction] +#[pyo3(name = "norm", signature = (z))] +pub fn pyfn_complex_norm(z: crate::runtime::coerce::ComplexArg) -> PyResult { + let z = z.0; + let __r = crate::runtime::guard(|| z.norm()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Argument (phase angle): arg(z) = atan2(im, re) +/// +/// Rust: `fractals::Complex::arg` +#[pyfunction] +#[pyo3(name = "arg", signature = (z))] +pub fn pyfn_complex_arg(z: crate::runtime::coerce::ComplexArg) -> PyResult { + let z = z.0; + let __r = crate::runtime::guard(|| z.arg()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Complex conjugate: z* = re - im·i +/// +/// Rust: `fractals::Complex::conjugate` +#[pyfunction] +#[pyo3(name = "conjugate", signature = (z))] +pub fn pyfn_complex_conjugate<'py>(py: Python<'py>, z: crate::runtime::coerce::ComplexArg) -> PyResult> { + let z = z.0; + let __r = crate::runtime::guard(|| z.conjugate()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::complex_out(py, __v)) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_mandelbrot_iterations, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mandelbrot_smooth, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mandelbrot_grid, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_julia_iterations, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_julia_grid, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_burning_ship_iterations, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_newton_fractal_iterations, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sierpinski_point, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_barnsley_fern_point, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_box_count_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_complex_new, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_complex_norm_sq, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_complex_norm, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_complex_arg, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_complex_conjugate, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_fractals__attractors.rs b/bindings/python/src/generated/m_fractals__attractors.rs new file mode 100644 index 0000000..8b17c1d --- /dev/null +++ b/bindings/python/src/generated/m_fractals__attractors.rs @@ -0,0 +1,138 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Kaplan-Yorke (Lyapunov) dimension of a spectrum: with exponents +/// sorted descending and k the largest index with Σ₁ᵏ λᵢ ≥ 0, +/// D = k + Σ₁ᵏ λᵢ / |λ_{k+1}|. +/// +/// Rust: `fractals::attractors::kaplan_yorke_dimension` +#[pyfunction] +#[pyo3(name = "kaplan_yorke_dimension", signature = (spectrum))] +pub fn pyfn_kaplan_yorke_dimension<'py>(py: Python<'py>, spectrum: Vec) -> PyResult { + let spectrum = <[f64; 3]>::try_from(spectrum).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 3 values, got {}", __v.len())))?; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fractals::attractors::kaplan_yorke_dimension(spectrum))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Grassberger-Procaccia correlation dimension: the least-squares +/// slope of ln C(r) against ln r over `n_r` log-spaced radii, where +/// C(r) is the fraction of point pairs closer than r. +/// +/// Panics: +/// Panics unless there are >= 100 points, `0 < r_min < r_max`, and +/// `n_r >= 2`. +/// +/// Rust: `fractals::attractors::correlation_dimension` +#[pyfunction] +#[pyo3(name = "correlation_dimension", signature = (points, r_min, r_max, n_r))] +pub fn pyfn_correlation_dimension<'py>(py: Python<'py>, points: Vec, r_min: f64, r_max: f64, n_r: usize) -> PyResult { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fractals::attractors::correlation_dimension(&points, r_min, r_max, n_r))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Box-counting dimension of a 3-D point set: slope of ln N(s) +/// versus ln(1/s) over the given box sizes. +/// +/// Panics: +/// Panics unless points and at least two positive scales are given. +/// +/// Rust: `fractals::attractors::box_counting_dimension_3d` +#[pyfunction] +#[pyo3(name = "box_counting_dimension_3d", signature = (points, scales))] +pub fn pyfn_box_counting_dimension_3d<'py>(py: Python<'py>, points: Vec, scales: Vec) -> PyResult { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fractals::attractors::box_counting_dimension_3d(&points, &scales))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Time-delay embedding: vectors [x(i), x(i+τ), ..., x(i+(m−1)τ)]. +/// +/// Panics: +/// Panics unless the series is long enough for one vector. +/// +/// Rust: `fractals::attractors::delay_embedding` +#[pyfunction] +#[pyo3(name = "delay_embedding", signature = (series, dim, delay))] +pub fn pyfn_delay_embedding<'py>(py: Python<'py>, series: Vec, dim: usize, delay: usize) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fractals::attractors::delay_embedding(&series, dim, delay))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Recurrence plot: `R[i,j]` is true when embedded states i and j are +/// within `eps` (row-major over the n embedded points). +/// +/// Rust: `fractals::attractors::recurrence_plot` +#[pyfunction] +#[pyo3(name = "recurrence_plot", signature = (series, embed_dim, delay, eps))] +pub fn pyfn_recurrence_plot<'py>(py: Python<'py>, series: Vec, embed_dim: usize, delay: usize, eps: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fractals::attractors::recurrence_plot(&series, embed_dim, delay, eps))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Largest Lyapunov exponent from a scalar series by Rosenstein's +/// method: embed, pair each point with its nearest neighbor at +/// temporal distance > `mean_period`, and fit the slope of the mean +/// log divergence over `max_iter` steps. Returned per sample step. +/// +/// Panics: +/// Panics on a series too short for the embedding and tracking. +/// +/// Rust: `fractals::attractors::largest_lyapunov_rosenstein` +#[pyfunction] +#[pyo3(name = "largest_lyapunov_rosenstein", signature = (series, embed_dim, delay, mean_period, max_iter))] +pub fn pyfn_largest_lyapunov_rosenstein<'py>(py: Python<'py>, series: Vec, embed_dim: usize, delay: usize, mean_period: usize, max_iter: usize) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fractals::attractors::largest_lyapunov_rosenstein(&series, embed_dim, delay, mean_period, max_iter))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Feigenbaum δ estimate from successive bifurcation parameters: +/// δₙ = (bₙ₋₁ − bₙ₋₂)/(bₙ − bₙ₋₁) for the last triple. +/// +/// Panics: +/// Panics unless at least 3 bifurcation points are given. +/// +/// Rust: `fractals::attractors::feigenbaum_estimate` +#[pyfunction] +#[pyo3(name = "feigenbaum_estimate", signature = (bifurcations))] +pub fn pyfn_feigenbaum_estimate<'py>(py: Python<'py>, bifurcations: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fractals::attractors::feigenbaum_estimate(&bifurcations))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_kaplan_yorke_dimension, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_correlation_dimension, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_box_counting_dimension_3d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_delay_embedding, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_recurrence_plot, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_largest_lyapunov_rosenstein, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_feigenbaum_estimate, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_fractals__attractors__presets.rs b/bindings/python/src/generated/m_fractals__attractors__presets.rs new file mode 100644 index 0000000..0f2ee95 --- /dev/null +++ b/bindings/python/src/generated/m_fractals__attractors__presets.rs @@ -0,0 +1,447 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Lorenz 1963: ẋ = σ(y−x), ẏ = x(ρ−z) − y, ż = xy − βz. +/// +/// Rust: `fractals::attractors::presets::lorenz` +#[pyfunction] +#[pyo3(name = "lorenz", signature = (sigma, rho, beta))] +pub fn pyfn_lorenz(sigma: f64, rho: f64, beta: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::attractors::presets::lorenz(sigma, rho, beta)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAttractor3 { inner: __v }) +} + +/// Rössler 1976: ẋ = −y−z, ẏ = x+ay, ż = b + z(x−c). +/// +/// Rust: `fractals::attractors::presets::rossler` +#[pyfunction] +#[pyo3(name = "rossler", signature = (a, b, c))] +pub fn pyfn_rossler(a: f64, b: f64, c: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::attractors::presets::rossler(a, b, c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAttractor3 { inner: __v }) +} + +/// Aizawa attractor (a sphere-wrapped scroll). +/// +/// Rust: `fractals::attractors::presets::aizawa` +#[pyfunction] +#[pyo3(name = "aizawa", signature = ())] +pub fn pyfn_aizawa() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::attractors::presets::aizawa()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAttractor3 { inner: __v }) +} + +/// Thomas' cyclically symmetric attractor: ẋ = sin y − bx, ... +/// +/// Rust: `fractals::attractors::presets::thomas` +#[pyfunction] +#[pyo3(name = "thomas", signature = (b))] +pub fn pyfn_thomas(b: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::attractors::presets::thomas(b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAttractor3 { inner: __v }) +} + +/// Chen 1999 (a = 35, b = 3, c = 28). +/// +/// Rust: `fractals::attractors::presets::chen` +#[pyfunction] +#[pyo3(name = "chen", signature = ())] +pub fn pyfn_chen() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::attractors::presets::chen()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAttractor3 { inner: __v }) +} + +/// Lü 2002 (a = 36, b = 3, c = 20), bridging Lorenz and Chen. +/// +/// Rust: `fractals::attractors::presets::lu` +#[pyfunction] +#[pyo3(name = "lu", signature = ())] +pub fn pyfn_lu() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::attractors::presets::lu()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAttractor3 { inner: __v }) +} + +/// Halvorsen's cyclic attractor: ẋ = −ax − 4y − 4z − y². +/// +/// Rust: `fractals::attractors::presets::halvorsen` +#[pyfunction] +#[pyo3(name = "halvorsen", signature = (a))] +pub fn pyfn_halvorsen(a: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::attractors::presets::halvorsen(a)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAttractor3 { inner: __v }) +} + +/// Sprott case B: ẋ = yz, ẏ = x − y, ż = 1 − xy. +/// +/// Rust: `fractals::attractors::presets::sprott_b` +#[pyfunction] +#[pyo3(name = "sprott_b", signature = ())] +pub fn pyfn_sprott_b() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::attractors::presets::sprott_b()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAttractor3 { inner: __v }) +} + +/// Dadras attractor. +/// +/// Rust: `fractals::attractors::presets::dadras` +#[pyfunction] +#[pyo3(name = "dadras", signature = ())] +pub fn pyfn_dadras() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::attractors::presets::dadras()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAttractor3 { inner: __v }) +} + +/// Rabinovich-Fabrikant: ẋ = y(z − 1 + x²) + γx, ... +/// +/// Rust: `fractals::attractors::presets::rabinovich_fabrikant` +#[pyfunction] +#[pyo3(name = "rabinovich_fabrikant", signature = (a, g))] +pub fn pyfn_rabinovich_fabrikant(a: f64, g: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::attractors::presets::rabinovich_fabrikant(a, g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAttractor3 { inner: __v }) +} + +/// Three-scroll unified chaotic system (TSUCS-1). +/// +/// Rust: `fractals::attractors::presets::three_scroll` +#[pyfunction] +#[pyo3(name = "three_scroll", signature = ())] +pub fn pyfn_three_scroll() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::attractors::presets::three_scroll()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAttractor3 { inner: __v }) +} + +/// Arneodo-Coullet: ẋ = y, ẏ = z, ż = ax − by − z − x³ +/// with (a, b) = (5.5, 3.5). +/// +/// Rust: `fractals::attractors::presets::arneodo` +#[pyfunction] +#[pyo3(name = "arneodo", signature = ())] +pub fn pyfn_arneodo() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::attractors::presets::arneodo()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAttractor3 { inner: __v }) +} + +/// Nosé-Hoover oscillator (Sprott A): ẋ = y, ẏ = −x + yz, +/// ż = 1 − y². +/// +/// Rust: `fractals::attractors::presets::nose_hoover` +#[pyfunction] +#[pyo3(name = "nose_hoover", signature = ())] +pub fn pyfn_nose_hoover() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::attractors::presets::nose_hoover()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAttractor3 { inner: __v }) +} + +/// Four-wing attractor. +/// +/// Rust: `fractals::attractors::presets::four_wing` +#[pyfunction] +#[pyo3(name = "four_wing", signature = ())] +pub fn pyfn_four_wing() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::attractors::presets::four_wing()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAttractor3 { inner: __v }) +} + +/// Chua's circuit with the piecewise-linear diode +/// characteristic f(x) = m₁x + (m₀−m₁)(|x+1| − |x−1|)/2. +/// +/// Rust: `fractals::attractors::presets::chua` +#[pyfunction] +#[pyo3(name = "chua", signature = (alpha, beta, m0, m1))] +pub fn pyfn_chua(alpha: f64, beta: f64, m0: f64, m1: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::attractors::presets::chua(alpha, beta, m0, m1)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAttractor3 { inner: __v }) +} + +/// Rikitake two-disc dynamo (μ = 1, a = 5). +/// +/// Rust: `fractals::attractors::presets::rikitake` +#[pyfunction] +#[pyo3(name = "rikitake", signature = ())] +pub fn pyfn_rikitake() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::attractors::presets::rikitake()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAttractor3 { inner: __v }) +} + +/// Forced Duffing oscillator as an autonomous 3-D flow with +/// z = ωt: ẋ = y, ẏ = −δy − αx − βx³ + γ cos z, ż = ω. +/// +/// Rust: `fractals::attractors::presets::duffing_forced` +#[pyfunction] +#[pyo3(name = "duffing_forced", signature = (delta, alpha, beta, gamma, omega))] +pub fn pyfn_duffing_forced(delta: f64, alpha: f64, beta: f64, gamma: f64, omega: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::attractors::presets::duffing_forced(delta, alpha, beta, gamma, omega)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAttractor3 { inner: __v }) +} + +/// Clifford attractor: x' = sin(ay) + c cos(ax), ... +/// +/// Rust: `fractals::attractors::presets::clifford` +#[pyfunction] +#[pyo3(name = "clifford", signature = (a, b, c, d))] +pub fn pyfn_clifford(a: f64, b: f64, c: f64, d: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::attractors::presets::clifford(a, b, c, d)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAttractor2Map { inner: __v }) +} + +/// Peter de Jong attractor. +/// +/// Rust: `fractals::attractors::presets::de_jong` +#[pyfunction] +#[pyo3(name = "de_jong", signature = (a, b, c, d))] +pub fn pyfn_de_jong(a: f64, b: f64, c: f64, d: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::attractors::presets::de_jong(a, b, c, d)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAttractor2Map { inner: __v }) +} + +/// Ikeda map with t = 0.4 − 6/(1 + x² + y²). +/// +/// Rust: `fractals::attractors::presets::ikeda` +#[pyfunction] +#[pyo3(name = "ikeda", signature = (u))] +pub fn pyfn_ikeda(u: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::attractors::presets::ikeda(u)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAttractor2Map { inner: __v }) +} + +/// Tinkerbell map. +/// +/// Rust: `fractals::attractors::presets::tinkerbell` +#[pyfunction] +#[pyo3(name = "tinkerbell", signature = (a, b, c, d))] +pub fn pyfn_tinkerbell(a: f64, b: f64, c: f64, d: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::attractors::presets::tinkerbell(a, b, c, d)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAttractor2Map { inner: __v }) +} + +/// Gingerbreadman map: x' = 1 − y + |x|, y' = x. +/// +/// Rust: `fractals::attractors::presets::gingerbreadman` +#[pyfunction] +#[pyo3(name = "gingerbreadman", signature = ())] +pub fn pyfn_gingerbreadman() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::attractors::presets::gingerbreadman()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAttractor2Map { inner: __v }) +} + +/// Hénon map: x' = 1 − ax² + y, y' = bx. +/// +/// Rust: `fractals::attractors::presets::henon` +#[pyfunction] +#[pyo3(name = "henon", signature = (a, b))] +pub fn pyfn_henon(a: f64, b: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::attractors::presets::henon(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAttractor2Map { inner: __v }) +} + +/// Duffing map: x' = y, y' = −bx + ay − y³. +/// +/// Rust: `fractals::attractors::presets::duffing_map` +#[pyfunction] +#[pyo3(name = "duffing_map", signature = (a, b))] +pub fn pyfn_duffing_map(a: f64, b: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::attractors::presets::duffing_map(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAttractor2Map { inner: __v }) +} + +/// Bogdanov map. +/// +/// Rust: `fractals::attractors::presets::bogdanov` +#[pyfunction] +#[pyo3(name = "bogdanov", signature = (eps, k, mu))] +pub fn pyfn_bogdanov(eps: f64, k: f64, mu: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::attractors::presets::bogdanov(eps, k, mu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAttractor2Map { inner: __v }) +} + +/// Chirikov standard map on the torus [0, 2π)²: +/// p' = p + k sin θ, θ' = θ + p'. Area-preserving. +/// +/// Rust: `fractals::attractors::presets::standard_map` +#[pyfunction] +#[pyo3(name = "standard_map", signature = (k))] +pub fn pyfn_standard_map(k: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::attractors::presets::standard_map(k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAttractor2Map { inner: __v }) +} + +/// Gumowski-Mira map with g(x) = ax + 2(1−a)x²/(1+x²). +/// +/// Rust: `fractals::attractors::presets::gumowski_mira` +#[pyfunction] +#[pyo3(name = "gumowski_mira", signature = (a, b))] +pub fn pyfn_gumowski_mira(a: f64, b: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::attractors::presets::gumowski_mira(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAttractor2Map { inner: __v }) +} + +/// Barry Martin's hopalong: x' = y − sign(x)√|bx − c|, y' = a − x. +/// +/// Rust: `fractals::attractors::presets::hopalong` +#[pyfunction] +#[pyo3(name = "hopalong", signature = (a, b, c))] +pub fn pyfn_hopalong(a: f64, b: f64, c: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::attractors::presets::hopalong(a, b, c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAttractor2Map { inner: __v }) +} + +/// Bedhead attractor. +/// +/// Rust: `fractals::attractors::presets::bedhead` +#[pyfunction] +#[pyo3(name = "bedhead", signature = (a, b))] +pub fn pyfn_bedhead(a: f64, b: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::attractors::presets::bedhead(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAttractor2Map { inner: __v }) +} + +/// Johnny Svensson's attractor. +/// +/// Rust: `fractals::attractors::presets::svensson` +#[pyfunction] +#[pyo3(name = "svensson", signature = (a, b, c, d))] +pub fn pyfn_svensson(a: f64, b: f64, c: f64, d: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::attractors::presets::svensson(a, b, c, d)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAttractor2Map { inner: __v }) +} + +/// "Fractal dream" attractor. +/// +/// Rust: `fractals::attractors::presets::fractal_dream` +#[pyfunction] +#[pyo3(name = "fractal_dream", signature = (a, b, c, d))] +pub fn pyfn_fractal_dream(a: f64, b: f64, c: f64, d: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::attractors::presets::fractal_dream(a, b, c, d)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAttractor2Map { inner: __v }) +} + +/// Popcorn map: x' = x − h sin(y + tan 3y), ... +/// +/// Rust: `fractals::attractors::presets::popcorn` +#[pyfunction] +#[pyo3(name = "popcorn", signature = (h))] +pub fn pyfn_popcorn(h: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::attractors::presets::popcorn(h)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAttractor2Map { inner: __v }) +} + +/// Arnold's cat map on the unit torus. +/// +/// Rust: `fractals::attractors::presets::arnold_cat` +#[pyfunction] +#[pyo3(name = "arnold_cat", signature = ())] +pub fn pyfn_arnold_cat() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::attractors::presets::arnold_cat()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAttractor2Map { inner: __v }) +} + +/// Baker's map on the unit square. +/// +/// Rust: `fractals::attractors::presets::baker` +#[pyfunction] +#[pyo3(name = "baker", signature = ())] +pub fn pyfn_baker() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::attractors::presets::baker()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAttractor2Map { inner: __v }) +} + +/// Zaslavskii map (ε forcing, ν rotation, r damping). +/// +/// Rust: `fractals::attractors::presets::zaslavskii` +#[pyfunction] +#[pyo3(name = "zaslavskii", signature = (eps, nu, r))] +pub fn pyfn_zaslavskii(eps: f64, nu: f64, r: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::attractors::presets::zaslavskii(eps, nu, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAttractor2Map { inner: __v }) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_lorenz, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rossler, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_aizawa, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_thomas, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_chen, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lu, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_halvorsen, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sprott_b, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dadras, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rabinovich_fabrikant, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_three_scroll, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_arneodo, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_nose_hoover, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_four_wing, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_chua, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rikitake, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_duffing_forced, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_clifford, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_de_jong, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ikeda, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tinkerbell, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gingerbreadman, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_henon, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_duffing_map, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bogdanov, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_standard_map, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gumowski_mira, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hopalong, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bedhead, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_svensson, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fractal_dream, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_popcorn, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_arnold_cat, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_baker, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_zaslavskii, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_fractals__automata.rs b/bindings/python/src/generated/m_fractals__automata.rs new file mode 100644 index 0000000..a600c6f --- /dev/null +++ b/bindings/python/src/generated/m_fractals__automata.rs @@ -0,0 +1,344 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// The rule as a lookup table indexed by the 3-bit neighborhood +/// (left·4 + center·2 + right). +/// +/// Rust: `fractals::automata::rule_table` +#[pyfunction] +#[pyo3(name = "rule_table", signature = (rule))] +pub fn pyfn_rule_table<'py>(py: Python<'py>, rule: u8) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fractals::automata::rule_table(rule))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_vec()) +} + +/// True for additive (XOR-linear) rules like 90, 150, 60: the rule +/// commutes with XOR of configurations. +/// +/// Rust: `fractals::automata::rule_is_additive` +#[pyfunction] +#[pyo3(name = "rule_is_additive", signature = (rule))] +pub fn pyfn_rule_is_additive(rule: u8) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::automata::rule_is_additive(rule)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Heuristic Wolfram classification of an elementary rule: +/// 1 = dies out, 2 = periodic/fixed, 3 = chaotic (high sustained +/// block entropy), 4 = complex (intermediate, long transients). +/// Based on entropy and activity statistics from a random seed; the +/// boundary between classes 3 and 4 is inherently fuzzy. +/// +/// Rust: `fractals::automata::rule_classify_wolfram` +#[pyfunction] +#[pyo3(name = "rule_classify_wolfram", signature = (rule))] +pub fn pyfn_rule_classify_wolfram(rule: u8) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::automata::rule_classify_wolfram(rule)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Topples every cell with >= 4 grains (von Neumann neighbors, open +/// boundary: grains fall off the edge) until stable. Returns the +/// number of topplings. +/// +/// Panics: +/// Panics unless `grid.len() == w·h`. +/// +/// Rust: `fractals::automata::sandpile_abelian` +#[pyfunction] +#[pyo3(name = "sandpile_abelian", signature = (grid, w, h))] +pub fn pyfn_sandpile_abelian<'py>(grid: pyo3::Bound<'py, pyo3::PyAny>, w: usize, h: usize) -> PyResult { + let mut grid__v: Vec = grid.extract()?; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::automata::sandpile_abelian(&mut grid__v, w, h)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + crate::runtime::coerce::write_back(&grid, &grid__v)?; + Ok(__v) +} + +/// Identity element of the abelian sandpile group on the w×h grid: +/// stabilize(2·δ − stabilize(2·δ)) with δ the all-6 configuration. +/// Adding it to any recurrent configuration and stabilizing returns +/// that configuration. +/// +/// Rust: `fractals::automata::sandpile_identity` +#[pyfunction] +#[pyo3(name = "sandpile_identity", signature = (w, h))] +pub fn pyfn_sandpile_identity<'py>(py: Python<'py>, w: usize, h: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fractals::automata::sandpile_identity(w, h))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Drossel-Schwabl forest fire: 0 empty, 1 tree, 2 burning. Burning +/// cells become empty; trees with a burning neighbor (or struck by +/// lightning with probability `p_lightning`) burn; empty cells grow +/// a tree with probability `p_grow`. Returns every generation. +/// +/// Panics: +/// Panics unless the grid is at least 3×3. +/// +/// Rust: `fractals::automata::forest_fire` +#[pyfunction] +#[pyo3(name = "forest_fire", signature = (w, h, p_grow, p_lightning, steps, rng))] +pub fn pyfn_forest_fire(w: usize, h: usize, p_grow: f64, p_lightning: f64, steps: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult>> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::automata::forest_fire(w, h, p_grow, p_lightning, steps, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Greenberg-Hastings excitable medium: state 0 rests, 1 fires, +/// 2..states-1 are refractory. A resting cell fires when a von +/// Neumann neighbor fires; every other state advances and wraps to +/// rest. Returns every generation from a random start. +/// +/// Panics: +/// Panics unless the grid is at least 3×3 and `states >= 3`. +/// +/// Rust: `fractals::automata::greenberg_hastings` +#[pyfunction] +#[pyo3(name = "greenberg_hastings", signature = (w, h, states, steps, rng))] +pub fn pyfn_greenberg_hastings(w: usize, h: usize, states: u8, steps: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult>> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::automata::greenberg_hastings(w, h, states, steps, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Synchronous majority rule: each cell adopts the majority state of +/// its Moore neighborhood (including itself; ties keep the state). +/// +/// Panics: +/// Panics unless `cells.len() == w·h`. +/// +/// Rust: `fractals::automata::majority_rule` +#[pyfunction] +#[pyo3(name = "majority_rule", signature = (cells, w, h, steps))] +pub fn pyfn_majority_rule<'py>(cells: pyo3::Bound<'py, pyo3::PyAny>, w: usize, h: usize, steps: usize) -> PyResult<()> { + let mut cells__v: Vec = cells.extract()?; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::automata::majority_rule(&mut cells__v, w, h, steps)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + crate::runtime::coerce::write_back(&cells, &cells__v)?; + Ok(()) +} + +/// Voter model: each step a random cell copies a random von Neumann +/// neighbor (`steps` single-cell updates). +/// +/// Panics: +/// Panics unless `cells.len() == w·h`. +/// +/// Rust: `fractals::automata::voter_model` +#[pyfunction] +#[pyo3(name = "voter_model", signature = (cells, w, h, steps, rng))] +pub fn pyfn_voter_model<'py>(cells: pyo3::Bound<'py, pyo3::PyAny>, w: usize, h: usize, steps: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<()> { + let mut cells__v: Vec = cells.extract()?; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::automata::voter_model(&mut cells__v, w, h, steps, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + crate::runtime::coerce::write_back(&cells, &cells__v)?; + Ok(()) +} + +/// Schelling segregation: two agent types (1, 2) plus vacancies (0). +/// Unhappy agents (fewer than `threshold` same-type fraction among +/// occupied Moore neighbors) move to random vacancies. Returns the +/// final segregation index: the mean same-type fraction over +/// occupied neighbors of all agents (0.5 = mixed, 1 = segregated). +/// +/// Panics: +/// Panics unless `grid.len() == w·h` and threshold is in [0, 1]. +/// +/// Rust: `fractals::automata::schelling_segregation` +#[pyfunction] +#[pyo3(name = "schelling_segregation", signature = (grid, w, h, threshold, steps, rng))] +pub fn pyfn_schelling_segregation<'py>(grid: pyo3::Bound<'py, pyo3::PyAny>, w: usize, h: usize, threshold: f64, steps: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut grid__v: Vec = grid.extract()?; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::automata::schelling_segregation(&mut grid__v, w, h, threshold, steps, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + crate::runtime::coerce::write_back(&grid, &grid__v)?; + Ok(__v) +} + +/// Generic 1-D two-species reaction-diffusion by forward Euler with +/// zero-flux boundaries: `f(u, v)` returns the two reaction rates. +/// +/// Panics: +/// Panics unless the arrays match and have at least 3 cells, and +/// `dx > 0`, `dt > 0`. +/// +/// Rust: `fractals::automata::reaction_diffusion_1d` +#[pyfunction] +#[pyo3(name = "reaction_diffusion_1d", signature = (u, v, f, du, dv, dt, dx, steps))] +pub fn pyfn_reaction_diffusion_1d<'py>(u: pyo3::Bound<'py, pyo3::PyAny>, v: pyo3::Bound<'py, pyo3::PyAny>, f: pyo3::Py, du: f64, dv: f64, dt: f64, dx: f64, steps: usize) -> PyResult<()> { + let mut u__v: Vec = u.extract()?; + let mut v__v: Vec = v.extract()?; + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64, __a1: f64| -> (f64, f64) { __cb.call::<_, (f64, f64)>((__a0, __a1), (f64::NAN, f64::NAN)) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::automata::reaction_diffusion_1d(&mut u__v, &mut v__v, &f, du, dv, dt, dx, steps)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + crate::runtime::coerce::write_back(&u, &u__v)?; + crate::runtime::coerce::write_back(&v, &v__v)?; + Ok(()) +} + +/// Diffusion-limited aggregation on a lattice: random walkers +/// launched from a circle stick to the growing cluster with the +/// given probability. Returns the cluster mask (row-major). +/// +/// Panics: +/// Panics unless the grid is at least 16×16 and stickiness is in +/// (0, 1]. +/// +/// Rust: `fractals::automata::diffusion_limited_aggregation` +#[pyfunction] +#[pyo3(name = "diffusion_limited_aggregation", signature = (w, h, particles, stickiness, rng))] +pub fn pyfn_diffusion_limited_aggregation(w: usize, h: usize, particles: usize, stickiness: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::automata::diffusion_limited_aggregation(w, h, particles, stickiness, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Eden growth: repeatedly turns a random perimeter cell of the +/// cluster on (compact growth with a rough boundary). +/// +/// Panics: +/// Panics unless the grid is at least 8×8. +/// +/// Rust: `fractals::automata::eden_growth` +#[pyfunction] +#[pyo3(name = "eden_growth", signature = (w, h, steps, rng))] +pub fn pyfn_eden_growth(w: usize, h: usize, steps: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::automata::eden_growth(w, h, steps, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Invasion percolation: cells get random strengths; growth always +/// invades the weakest perimeter cell, until the cluster touches a +/// boundary. Returns the invaded mask. +/// +/// Panics: +/// Panics unless the grid is at least 8×8. +/// +/// Rust: `fractals::automata::invasion_percolation` +#[pyfunction] +#[pyo3(name = "invasion_percolation", signature = (w, h, rng))] +pub fn pyfn_invasion_percolation(w: usize, h: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::automata::invasion_percolation(w, h, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Labels the 4-connected clusters of `grid` (labels start at 1; +/// 0 = off) and reports whether any cluster spans top to bottom. +/// +/// Panics: +/// Panics unless `grid.len() == w·h`. +/// +/// Rust: `fractals::automata::percolation_cluster` +#[pyfunction] +#[pyo3(name = "percolation_cluster", signature = (grid, w, h))] +pub fn pyfn_percolation_cluster<'py>(py: Python<'py>, grid: Vec, w: usize, h: usize) -> PyResult<(Vec, bool)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fractals::automata::percolation_cluster(&grid, w, h))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Site-percolation threshold estimate: cells are enabled in a +/// random order until a cluster spans top to bottom; the spanning +/// fraction, averaged over `trials`, estimates p_c ≈ 0.5927 on the +/// square lattice. +/// +/// Panics: +/// Panics unless the grid is at least 8×8 and `trials >= 1`. +/// +/// Rust: `fractals::automata::percolation_threshold_estimate` +#[pyfunction] +#[pyo3(name = "percolation_threshold_estimate", signature = (w, h, trials, rng))] +pub fn pyfn_percolation_threshold_estimate(w: usize, h: usize, trials: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::automata::percolation_threshold_estimate(w, h, trials, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Mass-radius fractal dimension of a cluster mask: the slope of +/// ln N(r) versus ln r, where N(r) counts cluster cells within +/// distance r of the cluster centroid (radii doubling from 3 up to +/// 70% of the cluster extent, which avoids finite-size edge bias +/// that plagues box counting on sparse clusters). +/// +/// Panics: +/// Panics unless `cluster.len() == w·h` and the cluster has at +/// least 10 cells. +/// +/// Rust: `fractals::automata::dla_fractal_dimension` +#[pyfunction] +#[pyo3(name = "dla_fractal_dimension", signature = (cluster, w, h))] +pub fn pyfn_dla_fractal_dimension<'py>(py: Python<'py>, cluster: Vec, w: usize, h: usize) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fractals::automata::dla_fractal_dimension(&cluster, w, h))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_rule_table, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rule_is_additive, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rule_classify_wolfram, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sandpile_abelian, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sandpile_identity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_forest_fire, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_greenberg_hastings, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_majority_rule, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_voter_model, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_schelling_segregation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_reaction_diffusion_1d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_diffusion_limited_aggregation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_eden_growth, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_invasion_percolation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_percolation_cluster, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_percolation_threshold_estimate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dla_fractal_dimension, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_fractals__automata__patterns.rs b/bindings/python/src/generated/m_fractals__automata__patterns.rs new file mode 100644 index 0000000..653e3f7 --- /dev/null +++ b/bindings/python/src/generated/m_fractals__automata__patterns.rs @@ -0,0 +1,154 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Glider (travels (1, 1) every 4 generations). +/// +/// Rust: `fractals::automata::patterns::glider` +#[pyfunction] +#[pyo3(name = "glider", signature = ())] +pub fn pyfn_glider<'py>(py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fractals::automata::patterns::glider())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.to_string()).collect::>()) +} + +/// Lightweight spaceship (travels (2, 0) every 4 generations). +/// +/// Rust: `fractals::automata::patterns::lwss` +#[pyfunction] +#[pyo3(name = "lwss", signature = ())] +pub fn pyfn_lwss<'py>(py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fractals::automata::patterns::lwss())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.to_string()).collect::>()) +} + +/// Gosper glider gun (period 30, emits one glider per period). +/// +/// Rust: `fractals::automata::patterns::gosper_gun` +#[pyfunction] +#[pyo3(name = "gosper_gun", signature = ())] +pub fn pyfn_gosper_gun<'py>(py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fractals::automata::patterns::gosper_gun())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.to_string()).collect::>()) +} + +/// R-pentomino (long-lived methuselah). +/// +/// Rust: `fractals::automata::patterns::r_pentomino` +#[pyfunction] +#[pyo3(name = "r_pentomino", signature = ())] +pub fn pyfn_r_pentomino<'py>(py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fractals::automata::patterns::r_pentomino())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.to_string()).collect::>()) +} + +/// Acorn (methuselah, stabilizes after 5206 generations). +/// +/// Rust: `fractals::automata::patterns::acorn` +#[pyfunction] +#[pyo3(name = "acorn", signature = ())] +pub fn pyfn_acorn<'py>(py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fractals::automata::patterns::acorn())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.to_string()).collect::>()) +} + +/// Diehard (vanishes after 130 generations). +/// +/// Rust: `fractals::automata::patterns::diehard` +#[pyfunction] +#[pyo3(name = "diehard", signature = ())] +pub fn pyfn_diehard<'py>(py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fractals::automata::patterns::diehard())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.to_string()).collect::>()) +} + +/// Pulsar (period-3 oscillator). +/// +/// Rust: `fractals::automata::patterns::pulsar` +#[pyfunction] +#[pyo3(name = "pulsar", signature = ())] +pub fn pyfn_pulsar<'py>(py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fractals::automata::patterns::pulsar())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.to_string()).collect::>()) +} + +/// Pentadecathlon (period-15 oscillator). +/// +/// Rust: `fractals::automata::patterns::pentadecathlon` +#[pyfunction] +#[pyo3(name = "pentadecathlon", signature = ())] +pub fn pyfn_pentadecathlon<'py>(py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fractals::automata::patterns::pentadecathlon())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.to_string()).collect::>()) +} + +/// Block (still life). +/// +/// Rust: `fractals::automata::patterns::block` +#[pyfunction] +#[pyo3(name = "block", signature = ())] +pub fn pyfn_block<'py>(py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fractals::automata::patterns::block())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.to_string()).collect::>()) +} + +/// Beehive (still life). +/// +/// Rust: `fractals::automata::patterns::beehive` +#[pyfunction] +#[pyo3(name = "beehive", signature = ())] +pub fn pyfn_beehive<'py>(py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fractals::automata::patterns::beehive())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.to_string()).collect::>()) +} + +/// Blinker (period-2 oscillator). +/// +/// Rust: `fractals::automata::patterns::blinker` +#[pyfunction] +#[pyo3(name = "blinker", signature = ())] +pub fn pyfn_blinker<'py>(py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fractals::automata::patterns::blinker())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.to_string()).collect::>()) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_glider, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lwss, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gosper_gun, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_r_pentomino, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_acorn, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_diehard, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pulsar, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pentadecathlon, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_block, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_beehive, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_blinker, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_fractals__escape_time.rs b/bindings/python/src/generated/m_fractals__escape_time.rs new file mode 100644 index 0000000..ee81437 --- /dev/null +++ b/bindings/python/src/generated/m_fractals__escape_time.rs @@ -0,0 +1,395 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Iterates z ← f(z, c) until |z| exceeds the bailout, recording +/// smooth iteration counts and orbit-trap distances. `distance` is +/// `None` here (no derivative is tracked); use +/// `escape_time_with_derivative` for distance estimates. +/// +/// Panics: +/// Panics unless `max_iter >= 1` and `bailout > 1`. +/// +/// Rust: `fractals::escape_time::escape_time` +#[pyfunction] +#[pyo3(name = "escape_time", signature = (f, z0, c, params))] +pub fn pyfn_escape_time(f: pyo3::Py, z0: crate::runtime::coerce::ComplexArg, c: crate::runtime::coerce::ComplexArg, params: crate::generated::types::PyEscapeParams) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: rust_physics_engine::fractals::Complex, __a1: rust_physics_engine::fractals::Complex| -> rust_physics_engine::fractals::Complex { { let __r = __cb.call::<_, crate::runtime::coerce::ComplexArg>((crate::runtime::coerce::Cx(__a0), crate::runtime::coerce::Cx(__a1)), crate::runtime::coerce::ComplexArg(rust_physics_engine::fractals::Complex::new(f64::NAN, f64::NAN))); __r.0 } } }; + let z0 = z0.0; + let c = c.0; + let params = params.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::escape_time::escape_time(&f, z0, c, ¶ms)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyEscapeResult { inner: __v }) +} + +/// Escape-time iteration that also tracks the parameter-space +/// derivative dz ← (∂f/∂z)·dz + 1 (the recurrence for sets like the +/// Mandelbrot set where c varies per pixel and z₀ is fixed), giving +/// the exterior distance estimate |z| ln|z| / |dz| on escape. +/// +/// Panics: +/// Panics unless `max_iter >= 1` and `bailout > 1`. +/// +/// Rust: `fractals::escape_time::escape_time_with_derivative` +#[pyfunction] +#[pyo3(name = "escape_time_with_derivative", signature = (f, df_dz, z0, c, params))] +pub fn pyfn_escape_time_with_derivative(f: pyo3::Py, df_dz: pyo3::Py, z0: crate::runtime::coerce::ComplexArg, c: crate::runtime::coerce::ComplexArg, params: crate::generated::types::PyEscapeParams) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: rust_physics_engine::fractals::Complex, __a1: rust_physics_engine::fractals::Complex| -> rust_physics_engine::fractals::Complex { { let __r = __cb.call::<_, crate::runtime::coerce::ComplexArg>((crate::runtime::coerce::Cx(__a0), crate::runtime::coerce::Cx(__a1)), crate::runtime::coerce::ComplexArg(rust_physics_engine::fractals::Complex::new(f64::NAN, f64::NAN))); __r.0 } } }; + let __cb_df_dz = std::rc::Rc::new(crate::runtime::Callback::new(df_dz)); + let df_dz = { let __cb = __cb_df_dz.clone(); move |__a0: rust_physics_engine::fractals::Complex, __a1: rust_physics_engine::fractals::Complex| -> rust_physics_engine::fractals::Complex { { let __r = __cb.call::<_, crate::runtime::coerce::ComplexArg>((crate::runtime::coerce::Cx(__a0), crate::runtime::coerce::Cx(__a1)), crate::runtime::coerce::ComplexArg(rust_physics_engine::fractals::Complex::new(f64::NAN, f64::NAN))); __r.0 } } }; + let z0 = z0.0; + let c = c.0; + let params = params.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::escape_time::escape_time_with_derivative(&f, &df_dz, z0, c, ¶ms)); + crate::runtime::callback::check(&[&__cb_f, &__cb_df_dz], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyEscapeResult { inner: __v }) +} + +/// The Mandelbrot set iteration z ← z² + c from z₀ = 0; tracks the +/// derivative for distance estimates when requested. +/// +/// Rust: `fractals::escape_time::mandelbrot` +#[pyfunction] +#[pyo3(name = "mandelbrot", signature = (c, params))] +pub fn pyfn_mandelbrot(c: crate::runtime::coerce::ComplexArg, params: crate::generated::types::PyEscapeParams) -> PyResult { + let c = c.0; + let params = params.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::escape_time::mandelbrot(c, ¶ms)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyEscapeResult { inner: __v }) +} + +/// The multibrot iteration z ← z^power + c from z₀ = 0. +/// +/// Panics: +/// Panics unless `power > 1`. +/// +/// Rust: `fractals::escape_time::multibrot` +#[pyfunction] +#[pyo3(name = "multibrot", signature = (c, power, params))] +pub fn pyfn_multibrot(c: crate::runtime::coerce::ComplexArg, power: f64, params: crate::generated::types::PyEscapeParams) -> PyResult { + let c = c.0; + let params = params.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::escape_time::multibrot(c, power, ¶ms)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyEscapeResult { inner: __v }) +} + +/// The Julia set iteration z ← z² + c from the given z; tracks the +/// dynamic-space derivative for distance estimates when requested. +/// +/// Rust: `fractals::escape_time::julia` +#[pyfunction] +#[pyo3(name = "julia", signature = (z, c, params))] +pub fn pyfn_julia(z: crate::runtime::coerce::ComplexArg, c: crate::runtime::coerce::ComplexArg, params: crate::generated::types::PyEscapeParams) -> PyResult { + let z = z.0; + let c = c.0; + let params = params.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::escape_time::julia(z, c, ¶ms)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyEscapeResult { inner: __v }) +} + +/// The tricorn (Mandelbar): z ← conj(z)² + c. +/// +/// Rust: `fractals::escape_time::tricorn` +#[pyfunction] +#[pyo3(name = "tricorn", signature = (c, params))] +pub fn pyfn_tricorn(c: crate::runtime::coerce::ComplexArg, params: crate::generated::types::PyEscapeParams) -> PyResult { + let c = c.0; + let params = params.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::escape_time::tricorn(c, ¶ms)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyEscapeResult { inner: __v }) +} + +/// The burning ship: z ← (|Re z| + i |Im z|)² + c. +/// +/// Rust: `fractals::escape_time::burning_ship` +#[pyfunction] +#[pyo3(name = "burning_ship", signature = (c, params))] +pub fn pyfn_burning_ship(c: crate::runtime::coerce::ComplexArg, params: crate::generated::types::PyEscapeParams) -> PyResult { + let c = c.0; + let params = params.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::escape_time::burning_ship(c, ¶ms)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyEscapeResult { inner: __v }) +} + +/// The Phoenix fractal: z_{n+1} = z_n² + c + p·z_{n−1}. +/// +/// Rust: `fractals::escape_time::phoenix` +#[pyfunction] +#[pyo3(name = "phoenix", signature = (z, c, p, params))] +pub fn pyfn_phoenix(z: crate::runtime::coerce::ComplexArg, c: crate::runtime::coerce::ComplexArg, p: crate::runtime::coerce::ComplexArg, params: crate::generated::types::PyEscapeParams) -> PyResult { + let z = z.0; + let c = c.0; + let p = p.0; + let params = params.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::escape_time::phoenix(z, c, p, ¶ms)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyEscapeResult { inner: __v }) +} + +/// Newton fractal for the polynomial with coefficients `poly` +/// (constant term first): iterates z ← z − p(z)/p′(z) and returns +/// the index of the root reached (roots sorted by real then +/// imaginary part) and the iteration count. Index `degree` (one past +/// the last root) marks failure to converge within `max_iter`. +/// +/// Panics: +/// Panics unless the polynomial has degree >= 2. +/// +/// Rust: `fractals::escape_time::newton_fractal` +#[pyfunction] +#[pyo3(name = "newton_fractal", signature = (z, poly, params))] +pub fn pyfn_newton_fractal<'py>(py: Python<'py>, z: crate::runtime::coerce::ComplexArg, poly: Vec, params: crate::generated::types::PyEscapeParams) -> PyResult<(usize, u32)> { + let z = z.0; + let poly = poly.into_iter().map(|__e| __e.0).collect::>(); + let params = params.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fractals::escape_time::newton_fractal(z, &poly, ¶ms))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Nova fractal: relaxed Newton iteration on z^power − 1 with an +/// added constant, z ← z − relax·(z^p − 1)/(p·z^{p−1}) + c. +/// `escaped = true` records convergence to a fixed point (|Δz| < +/// 1e-9); `iterations` counts steps to convergence. +/// +/// Panics: +/// Panics unless `power > 1`. +/// +/// Rust: `fractals::escape_time::nova_fractal` +#[pyfunction] +#[pyo3(name = "nova_fractal", signature = (z, c, power, relax, params))] +pub fn pyfn_nova_fractal(z: crate::runtime::coerce::ComplexArg, c: crate::runtime::coerce::ComplexArg, power: f64, relax: f64, params: crate::generated::types::PyEscapeParams) -> PyResult { + let z = z.0; + let c = c.0; + let params = params.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::escape_time::nova_fractal(z, c, power, relax, ¶ms)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyEscapeResult { inner: __v }) +} + +/// Magnet fractal type I: z ← ((z² + c − 1)/(2z + c − 2))². +/// +/// Rust: `fractals::escape_time::magnet_type1` +#[pyfunction] +#[pyo3(name = "magnet_type1", signature = (c, params))] +pub fn pyfn_magnet_type1(c: crate::runtime::coerce::ComplexArg, params: crate::generated::types::PyEscapeParams) -> PyResult { + let c = c.0; + let params = params.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::escape_time::magnet_type1(c, ¶ms)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyEscapeResult { inner: __v }) +} + +/// Magnet fractal type II: +/// z ← ((z³ + 3(c−1)z + (c−1)(c−2)) / (3z² + 3(c−2)z + (c−1)(c−2) + 1))². +/// +/// Rust: `fractals::escape_time::magnet_type2` +#[pyfunction] +#[pyo3(name = "magnet_type2", signature = (c, params))] +pub fn pyfn_magnet_type2(c: crate::runtime::coerce::ComplexArg, params: crate::generated::types::PyEscapeParams) -> PyResult { + let c = c.0; + let params = params.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::escape_time::magnet_type2(c, ¶ms)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyEscapeResult { inner: __v }) +} + +/// Lyapunov exponent of the forced logistic map x ← r·x(1−x) where r +/// alternates between `a` and `b` according to `sequence` (a string +/// of 'A's and 'B's, cycled). Negative values mark stability +/// (colored regions of Markus-Lyapunov fractals), positive chaos. +/// +/// Panics: +/// Panics unless the sequence is non-empty and made of A/B, and +/// `iterations >= 1`. +/// +/// Rust: `fractals::escape_time::lyapunov_fractal` +#[pyfunction] +#[pyo3(name = "lyapunov_fractal", signature = (a, b, sequence, iterations, warmup))] +pub fn pyfn_lyapunov_fractal(a: f64, b: f64, sequence: String, iterations: usize, warmup: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::escape_time::lyapunov_fractal(a, b, &sequence, iterations, warmup)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Period of the attracting cycle at parameter c, by iterating to +/// the attractor and then measuring the first return within 1e-9. +/// `None` when the orbit escapes or no cycle of period <= 64 is +/// found within `max_iter` settling steps. +/// +/// Rust: `fractals::escape_time::mandelbrot_period` +#[pyfunction] +#[pyo3(name = "mandelbrot_period", signature = (c, max_iter))] +pub fn pyfn_mandelbrot_period(c: crate::runtime::coerce::ComplexArg, max_iter: u32) -> PyResult> { + let c = c.0; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::escape_time::mandelbrot_period(c, max_iter)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// True inside the main cardioid, where the fixed point is +/// attracting: q(q + Re c − 1/4) < (Im c)²/4 with q = |c − 1/4|². +/// +/// Rust: `fractals::escape_time::mandelbrot_in_main_cardioid` +#[pyfunction] +#[pyo3(name = "mandelbrot_in_main_cardioid", signature = (c))] +pub fn pyfn_mandelbrot_in_main_cardioid(c: crate::runtime::coerce::ComplexArg) -> PyResult { + let c = c.0; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::escape_time::mandelbrot_in_main_cardioid(c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// True inside the period-2 bulb |c + 1| < 1/4. +/// +/// Rust: `fractals::escape_time::mandelbrot_in_period2_bulb` +#[pyfunction] +#[pyo3(name = "mandelbrot_in_period2_bulb", signature = (c))] +pub fn pyfn_mandelbrot_in_period2_bulb(c: crate::runtime::coerce::ComplexArg) -> PyResult { + let c = c.0; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::escape_time::mandelbrot_in_period2_bulb(c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Buddhabrot: accumulates the escape orbits of random starting +/// parameters into a `res.0` × `res.1` grid over `bounds` +/// (row-major, x fastest). +/// +/// Panics: +/// Panics on an empty grid or degenerate bounds. +/// +/// Rust: `fractals::escape_time::buddhabrot` +#[pyfunction] +#[pyo3(name = "buddhabrot", signature = (samples, max_iter, res, bounds, rng))] +pub fn pyfn_buddhabrot(samples: usize, max_iter: u32, res: (usize, usize), bounds: crate::generated::types::PyRect, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let res = (res.0, res.1); + let bounds = bounds.inner; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::escape_time::buddhabrot(samples, max_iter, res, &bounds, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Random points near the Mandelbrot set boundary: rejection +/// sampling keeping parameters whose escape time falls in +/// [20, max_iter), i.e. neither deep exterior nor interior. +/// +/// Panics: +/// Panics unless `n >= 1`. +/// +/// Rust: `fractals::escape_time::mandelbrot_boundary_points` +#[pyfunction] +#[pyo3(name = "mandelbrot_boundary_points", signature = (n, rng))] +pub fn pyfn_mandelbrot_boundary_points<'py>(py: Python<'py>, n: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult>> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::escape_time::mandelbrot_boundary_points(n, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// Perturbation iteration for deep zooms: iterates the offset +/// δ ← 2·Z_n·δ + δ² + δ₀ against a precomputed reference orbit +/// Z_n (the orbit of `center_hi`), so pixels near the reference +/// need only f64 offsets. The reference orbit must start at +/// Z_0 = c_ref (the first iterate of 0). When the reference orbit +/// is shorter than the escape time, iteration continues directly. +/// +/// Panics: +/// Panics on an empty reference orbit. +/// +/// Rust: `fractals::escape_time::perturbation_mandelbrot` +#[pyfunction] +#[pyo3(name = "perturbation_mandelbrot", signature = (center_hi, delta, reference_orbit, params))] +pub fn pyfn_perturbation_mandelbrot(center_hi: (f64, f64), delta: crate::runtime::coerce::ComplexArg, reference_orbit: Vec, params: crate::generated::types::PyEscapeParams) -> PyResult { + let center_hi = (center_hi.0, center_hi.1); + let delta = delta.0; + let reference_orbit = reference_orbit.into_iter().map(|__e| __e.0).collect::>(); + let params = params.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::escape_time::perturbation_mandelbrot(center_hi, delta, &reference_orbit, ¶ms)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyEscapeResult { inner: __v }) +} + +/// Distance-estimate shading: 0 on the set, saturating to 1 at a +/// few pixels away; d/pixel_size clamped to [0, 1]. Interior points +/// (no distance) shade to 0. +/// +/// Panics: +/// Panics unless `pixel_size > 0`. +/// +/// Rust: `fractals::escape_time::color_distance_estimate` +#[pyfunction] +#[pyo3(name = "color_distance_estimate", signature = (r, pixel_size))] +pub fn pyfn_color_distance_estimate(r: crate::generated::types::PyEscapeResult, pixel_size: f64) -> PyResult { + let r = r.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::escape_time::color_distance_estimate(&r, pixel_size)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Reference orbit of the Mandelbrot iteration at c (Z_0 = c), +/// for `perturbation_mandelbrot`. Stops early on escape. +/// +/// Rust: `fractals::escape_time::mandelbrot_reference_orbit` +#[pyfunction] +#[pyo3(name = "mandelbrot_reference_orbit", signature = (c, max_iter))] +pub fn pyfn_mandelbrot_reference_orbit<'py>(py: Python<'py>, c: crate::runtime::coerce::ComplexArg, max_iter: u32) -> PyResult>> { + let c = c.0; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::escape_time::mandelbrot_reference_orbit(c, max_iter)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_escape_time, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_escape_time_with_derivative, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mandelbrot, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_multibrot, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_julia, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tricorn, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_burning_ship, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_phoenix, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_newton_fractal, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_nova_fractal, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_magnet_type1, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_magnet_type2, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lyapunov_fractal, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mandelbrot_period, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mandelbrot_in_main_cardioid, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mandelbrot_in_period2_bulb, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_buddhabrot, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mandelbrot_boundary_points, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_perturbation_mandelbrot, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_color_distance_estimate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mandelbrot_reference_orbit, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_fractals__ifs.rs b/bindings/python/src/generated/m_fractals__ifs.rs new file mode 100644 index 0000000..0a4cbe7 --- /dev/null +++ b/bindings/python/src/generated/m_fractals__ifs.rs @@ -0,0 +1,61 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Applies one flame variation to a point. Formulas follow the flame +/// paper's conventions: r = |p|, θ = atan2(x, y). +/// +/// Rust: `fractals::ifs::apply_variation` +#[pyfunction] +#[pyo3(name = "apply_variation", signature = (v, p))] +pub fn pyfn_apply_variation(v: crate::generated::types::PyVariation, p: crate::generated::types::PyVec2Arg) -> PyResult { + let v = v.to_rust(); + let p = p.0; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::ifs::apply_variation(v, p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) +} + +/// Fractal flame chaos game: each step applies a randomly chosen +/// affine map followed by its variation, and blends a per-map color +/// coordinate c ← (c + cᵢ)/2 with cᵢ = i/(m−1). Returns points with +/// their color coordinates; non-finite excursions restart from the +/// origin. +/// +/// Panics: +/// Panics on an empty map list or non-positive total probability. +/// +/// Rust: `fractals::ifs::fractal_flame` +#[pyfunction] +#[pyo3(name = "fractal_flame", signature = (maps, n, rng))] +pub fn pyfn_fractal_flame(maps: Vec<(crate::generated::types::PyAffine2, f64, crate::generated::types::PyVariation)>, n: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let maps = maps.into_iter().map(|__e| (__e.0.inner, __e.1, __e.2.to_rust())).collect::>(); + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::ifs::fractal_flame(&maps, n, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (crate::generated::types::PyVec2 { inner: __x.0 }, __x.1)).collect::>()) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_apply_variation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fractal_flame, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_fractals__ifs__presets.rs b/bindings/python/src/generated/m_fractals__ifs__presets.rs new file mode 100644 index 0000000..996012e --- /dev/null +++ b/bindings/python/src/generated/m_fractals__ifs__presets.rs @@ -0,0 +1,232 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Sierpinski triangle: three half-scale maps toward the corners +/// of an equilateral triangle. Dimension log 3 / log 2. +/// +/// Rust: `fractals::ifs::presets::sierpinski` +#[pyfunction] +#[pyo3(name = "sierpinski", signature = ())] +pub fn pyfn_sierpinski() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::ifs::presets::sierpinski()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyIfs { inner: __v }) +} + +/// Barnsley's fern (the classic four maps and probabilities). +/// +/// Rust: `fractals::ifs::presets::barnsley_fern` +#[pyfunction] +#[pyo3(name = "barnsley_fern", signature = ())] +pub fn pyfn_barnsley_fern() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::ifs::presets::barnsley_fern()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyIfs { inner: __v }) +} + +/// Koch curve as four 1/3-scale similitudes. Dimension +/// log 4 / log 3. +/// +/// Rust: `fractals::ifs::presets::koch` +#[pyfunction] +#[pyo3(name = "koch", signature = ())] +pub fn pyfn_koch() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::ifs::presets::koch()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyIfs { inner: __v }) +} + +/// Heighway dragon: z → (1+i)z/2 and z → 1 − (1−i)z/2. +/// +/// Rust: `fractals::ifs::presets::dragon` +#[pyfunction] +#[pyo3(name = "dragon", signature = ())] +pub fn pyfn_dragon() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::ifs::presets::dragon()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyIfs { inner: __v }) +} + +/// Lévy C curve: z → wz and z → w̄z + (1 − w̄), w = (1+i)/2. +/// +/// Rust: `fractals::ifs::presets::levy` +#[pyfunction] +#[pyo3(name = "levy", signature = ())] +pub fn pyfn_levy() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::ifs::presets::levy()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyIfs { inner: __v }) +} + +/// Maple leaf (a well-known four-map collage). +/// +/// Rust: `fractals::ifs::presets::maple_leaf` +#[pyfunction] +#[pyo3(name = "maple_leaf", signature = ())] +pub fn pyfn_maple_leaf() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::ifs::presets::maple_leaf()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyIfs { inner: __v }) +} + +/// Symmetric fractal tree: trunk, two rotated branches, and a +/// crown copy. +/// +/// Rust: `fractals::ifs::presets::tree` +#[pyfunction] +#[pyo3(name = "tree", signature = ())] +pub fn pyfn_tree() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::ifs::presets::tree()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyIfs { inner: __v }) +} + +/// Logarithmic spiral of copies: one strong rotation plus a +/// small displaced copy. +/// +/// Rust: `fractals::ifs::presets::spiral` +#[pyfunction] +#[pyo3(name = "spiral", signature = ())] +pub fn pyfn_spiral() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::ifs::presets::spiral()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyIfs { inner: __v }) +} + +/// Cantor dust: four 1/3-scale copies at the unit square's +/// corners. Dimension log 4 / log 3. +/// +/// Rust: `fractals::ifs::presets::cantor_dust` +#[pyfunction] +#[pyo3(name = "cantor_dust", signature = ())] +pub fn pyfn_cantor_dust() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::ifs::presets::cantor_dust()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyIfs { inner: __v }) +} + +/// Pythagoras tree with roof angle `angle`: the two square-to- +/// square similarities of the classic construction (the unit +/// square is the trunk). +/// +/// Panics: +/// Panics unless 0 < angle < π/2. +/// +/// Rust: `fractals::ifs::presets::pythagoras_tree` +#[pyfunction] +#[pyo3(name = "pythagoras_tree", signature = (angle))] +pub fn pyfn_pythagoras_tree(angle: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::ifs::presets::pythagoras_tree(angle)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyIfs { inner: __v }) +} + +/// Sierpinski carpet: eight 1/3-scale copies (all but the +/// center). Dimension log 8 / log 3. +/// +/// Rust: `fractals::ifs::presets::sierpinski_carpet` +#[pyfunction] +#[pyo3(name = "sierpinski_carpet", signature = ())] +pub fn pyfn_sierpinski_carpet() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::ifs::presets::sierpinski_carpet()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyIfs { inner: __v }) +} + +/// Vicsek fractal (plus sign): center and four edge cells at +/// 1/3 scale. Dimension log 5 / log 3. +/// +/// Rust: `fractals::ifs::presets::vicsek` +#[pyfunction] +#[pyo3(name = "vicsek", signature = ())] +pub fn pyfn_vicsek() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::ifs::presets::vicsek()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyIfs { inner: __v }) +} + +/// Menger sponge: the twenty 1/3-scale cells of the cube that +/// survive (drop face centers and the body center). Dimension +/// log 20 / log 3. +/// +/// Rust: `fractals::ifs::presets::menger_sponge_3d` +#[pyfunction] +#[pyo3(name = "menger_sponge_3d", signature = ())] +pub fn pyfn_menger_sponge_3d() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::ifs::presets::menger_sponge_3d()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyIfs3 { inner: __v }) +} + +/// Sierpinski tetrahedron: four half-scale maps toward the +/// vertices of a regular tetrahedron. Dimension 2. +/// +/// Rust: `fractals::ifs::presets::sierpinski_tetrahedron_3d` +#[pyfunction] +#[pyo3(name = "sierpinski_tetrahedron_3d", signature = ())] +pub fn pyfn_sierpinski_tetrahedron_3d() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::ifs::presets::sierpinski_tetrahedron_3d()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyIfs3 { inner: __v }) +} + +/// Pentaflake: five copies at the vertices of a regular pentagon +/// with contraction 1/(1+φ) = (3−√5)/2. Dimension +/// log 5 / log(1+φ). +/// +/// Rust: `fractals::ifs::presets::pentagon_flake` +#[pyfunction] +#[pyo3(name = "pentagon_flake", signature = ())] +pub fn pyfn_pentagon_flake() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::ifs::presets::pentagon_flake()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyIfs { inner: __v }) +} + +/// Hexaflake: six vertex copies plus the center at 1/3 scale. +/// Dimension log 7 / log 3. +/// +/// Rust: `fractals::ifs::presets::hexaflake` +#[pyfunction] +#[pyo3(name = "hexaflake", signature = ())] +pub fn pyfn_hexaflake() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::ifs::presets::hexaflake()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyIfs { inner: __v }) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_sierpinski, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_barnsley_fern, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_koch, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dragon, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_levy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_maple_leaf, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tree, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spiral, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cantor_dust, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pythagoras_tree, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sierpinski_carpet, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_vicsek, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_menger_sponge_3d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sierpinski_tetrahedron_3d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pentagon_flake, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hexaflake, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_fractals__lsystem.rs b/bindings/python/src/generated/m_fractals__lsystem.rs new file mode 100644 index 0000000..1ccf835 --- /dev/null +++ b/bindings/python/src/generated/m_fractals__lsystem.rs @@ -0,0 +1,59 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Chains segments that share endpoints into polylines (in drawing +/// order): a new polyline starts whenever the pen jumped. +/// +/// Rust: `fractals::lsystem::lsystem_to_polylines` +#[pyfunction] +#[pyo3(name = "lsystem_to_polylines", signature = (segments))] +pub fn pyfn_lsystem_to_polylines(segments: Vec) -> PyResult>> { + let segments = segments.into_iter().map(|__e| __e.inner).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::lsystem::lsystem_to_polylines(&segments)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()).collect::>()) +} + +/// Box-counting dimension estimate of the drawing produced by +/// `iterations` rewrites. The two grid resolutions are chosen so the +/// finest cell is no smaller than a turtle step — below that scale +/// every curve is one-dimensional and the count slope collapses to 1. +/// +/// Panics: +/// Panics if the drawing is empty or degenerate. +/// +/// Rust: `fractals::lsystem::fractal_dimension_lsystem` +#[pyfunction] +#[pyo3(name = "fractal_dimension_lsystem", signature = (ls, iterations))] +pub fn pyfn_fractal_dimension_lsystem(ls: crate::generated::types::PyLSystem, iterations: usize) -> PyResult { + let ls = ls.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::lsystem::fractal_dimension_lsystem(&ls, iterations)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_lsystem_to_polylines, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fractal_dimension_lsystem, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_fractals__lsystem__presets.rs b/bindings/python/src/generated/m_fractals__lsystem__presets.rs new file mode 100644 index 0000000..8eb3f77 --- /dev/null +++ b/bindings/python/src/generated/m_fractals__lsystem__presets.rs @@ -0,0 +1,302 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Koch curve: F → F+F−−F+F at 60°. +/// +/// Rust: `fractals::lsystem::presets::koch_curve` +#[pyfunction] +#[pyo3(name = "koch_curve", signature = ())] +pub fn pyfn_koch_curve() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::lsystem::presets::koch_curve()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLSystem { inner: __v }) +} + +/// Koch snowflake: the Koch rule on a triangle axiom. +/// +/// Rust: `fractals::lsystem::presets::koch_snowflake` +#[pyfunction] +#[pyo3(name = "koch_snowflake", signature = ())] +pub fn pyfn_koch_snowflake() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::lsystem::presets::koch_snowflake()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLSystem { inner: __v }) +} + +/// Quadratic Koch island (ABOP fig 1.7a): F → F+F−F−FF+F+F−F +/// on a square, 90°. +/// +/// Rust: `fractals::lsystem::presets::koch_island` +#[pyfunction] +#[pyo3(name = "koch_island", signature = ())] +pub fn pyfn_koch_island() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::lsystem::presets::koch_island()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLSystem { inner: __v }) +} + +/// Heighway dragon at 90°. +/// +/// Rust: `fractals::lsystem::presets::dragon` +#[pyfunction] +#[pyo3(name = "dragon", signature = ())] +pub fn pyfn_dragon() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::lsystem::presets::dragon()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLSystem { inner: __v }) +} + +/// Hilbert curve as an L-system (ABOP fig 1.11a), 90°. +/// +/// Rust: `fractals::lsystem::presets::hilbert` +#[pyfunction] +#[pyo3(name = "hilbert", signature = ())] +pub fn pyfn_hilbert() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::lsystem::presets::hilbert()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLSystem { inner: __v }) +} + +/// Peano curve variant filling a square, 90°. +/// +/// Rust: `fractals::lsystem::presets::peano` +#[pyfunction] +#[pyo3(name = "peano", signature = ())] +pub fn pyfn_peano() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::lsystem::presets::peano()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLSystem { inner: __v }) +} + +/// Gosper flowsnake at 60° (F and G both draw). +/// +/// Rust: `fractals::lsystem::presets::gosper` +#[pyfunction] +#[pyo3(name = "gosper", signature = ())] +pub fn pyfn_gosper() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::lsystem::presets::gosper()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLSystem { inner: __v }) +} + +/// Sierpinski triangle (F and G both draw), 120°. +/// +/// Rust: `fractals::lsystem::presets::sierpinski_triangle` +#[pyfunction] +#[pyo3(name = "sierpinski_triangle", signature = ())] +pub fn pyfn_sierpinski_triangle() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::lsystem::presets::sierpinski_triangle()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLSystem { inner: __v }) +} + +/// Sierpinski arrowhead curve, 60°. +/// +/// Rust: `fractals::lsystem::presets::sierpinski_arrowhead` +#[pyfunction] +#[pyo3(name = "sierpinski_arrowhead", signature = ())] +pub fn pyfn_sierpinski_arrowhead() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::lsystem::presets::sierpinski_arrowhead()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLSystem { inner: __v }) +} + +/// Lévy C curve, 45°. +/// +/// Rust: `fractals::lsystem::presets::levy_c` +#[pyfunction] +#[pyo3(name = "levy_c", signature = ())] +pub fn pyfn_levy_c() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::lsystem::presets::levy_c()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLSystem { inner: __v }) +} + +/// Cantor set on a line: F draws, f skips the removed middle +/// third. +/// +/// Rust: `fractals::lsystem::presets::cantor` +#[pyfunction] +#[pyo3(name = "cantor", signature = ())] +pub fn pyfn_cantor() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::lsystem::presets::cantor()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLSystem { inner: __v }) +} + +/// ABOP fig 1.24a: F → F[+F]F[−F]F at 25.7°. +/// +/// Rust: `fractals::lsystem::presets::plant_a` +#[pyfunction] +#[pyo3(name = "plant_a", signature = ())] +pub fn pyfn_plant_a() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::lsystem::presets::plant_a()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLSystem { inner: __v }) +} + +/// ABOP fig 1.24b: `F → F[+F]F[−F][F]` at 20°. +/// +/// Rust: `fractals::lsystem::presets::plant_b` +#[pyfunction] +#[pyo3(name = "plant_b", signature = ())] +pub fn pyfn_plant_b() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::lsystem::presets::plant_b()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLSystem { inner: __v }) +} + +/// ABOP fig 1.24c: F → FF−[−F+F+F]+[+F−F−F] at 22.5°. +/// +/// Rust: `fractals::lsystem::presets::plant_c` +#[pyfunction] +#[pyo3(name = "plant_c", signature = ())] +pub fn pyfn_plant_c() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::lsystem::presets::plant_c()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLSystem { inner: __v }) +} + +/// ABOP fig 1.24d: X → F[+X]F[−X]+X, F → FF at 20°. +/// +/// Rust: `fractals::lsystem::presets::plant_d` +#[pyfunction] +#[pyo3(name = "plant_d", signature = ())] +pub fn pyfn_plant_d() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::lsystem::presets::plant_d()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLSystem { inner: __v }) +} + +/// ABOP fig 1.24e: `X → F[+X][−X]FX`, `F → FF` at 25.7°. +/// +/// Rust: `fractals::lsystem::presets::plant_e` +#[pyfunction] +#[pyo3(name = "plant_e", signature = ())] +pub fn pyfn_plant_e() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::lsystem::presets::plant_e()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLSystem { inner: __v }) +} + +/// ABOP fig 1.24f: `X → F−[[X]+X]+F[+FX]−X`, `F → FF` at 22.5°. +/// +/// Rust: `fractals::lsystem::presets::plant_f` +#[pyfunction] +#[pyo3(name = "plant_f", signature = ())] +pub fn pyfn_plant_f() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::lsystem::presets::plant_f()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLSystem { inner: __v }) +} + +/// Simple 3-D tree: trunk then three tapered branches rolled +/// 120° apart (interpret with `Turtle3`). +/// +/// Rust: `fractals::lsystem::presets::tree_3d` +#[pyfunction] +#[pyo3(name = "tree_3d", signature = ())] +pub fn pyfn_tree_3d() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::lsystem::presets::tree_3d()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLSystem { inner: __v }) +} + +/// 3-D bush after ABOP fig 1.25 (interpret with `Turtle3`). +/// +/// Rust: `fractals::lsystem::presets::bush_3d` +#[pyfunction] +#[pyo3(name = "bush_3d", signature = ())] +pub fn pyfn_bush_3d() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::lsystem::presets::bush_3d()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLSystem { inner: __v }) +} + +/// Cesàro curve: F → F+F−−F+F at 85°. +/// +/// Rust: `fractals::lsystem::presets::cesaro` +#[pyfunction] +#[pyo3(name = "cesaro", signature = ())] +pub fn pyfn_cesaro() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::lsystem::presets::cesaro()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLSystem { inner: __v }) +} + +/// Pentaplexity (pentagonal flake curve), 36°. +/// +/// Rust: `fractals::lsystem::presets::pentaplexity` +#[pyfunction] +#[pyo3(name = "pentaplexity", signature = ())] +pub fn pyfn_pentaplexity() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::lsystem::presets::pentaplexity()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLSystem { inner: __v }) +} + +/// Penrose P3 rhombus tiling as an L-system (the classic +/// M/N/O/P system, angle 36°; draw F). +/// +/// Rust: `fractals::lsystem::presets::penrose_lsystem` +#[pyfunction] +#[pyo3(name = "penrose_lsystem", signature = ())] +pub fn pyfn_penrose_lsystem() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::lsystem::presets::penrose_lsystem()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLSystem { inner: __v }) +} + +/// Hexagonal Gosper curve (two-symbol XF form), 60°. +/// +/// Rust: `fractals::lsystem::presets::hexagonal_gosper` +#[pyfunction] +#[pyo3(name = "hexagonal_gosper", signature = ())] +pub fn pyfn_hexagonal_gosper() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::lsystem::presets::hexagonal_gosper()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLSystem { inner: __v }) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_koch_curve, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_koch_snowflake, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_koch_island, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dragon, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hilbert, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_peano, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gosper, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sierpinski_triangle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sierpinski_arrowhead, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_levy_c, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cantor, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_plant_a, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_plant_b, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_plant_c, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_plant_d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_plant_e, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_plant_f, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tree_3d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bush_3d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cesaro, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pentaplexity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_penrose_lsystem, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hexagonal_gosper, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_fractals__noise.rs b/bindings/python/src/generated/m_fractals__noise.rs new file mode 100644 index 0000000..e56afe7 --- /dev/null +++ b/bindings/python/src/generated/m_fractals__noise.rs @@ -0,0 +1,349 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// fBm: Σ amplitude·gainⁱ · n(frequency·lacunarityⁱ · x). +/// +/// Rust: `fractals::noise::fbm_2d` +#[pyfunction] +#[pyo3(name = "fbm_2d", signature = (n, x, y, p))] +pub fn pyfn_fbm_2d(n: pyo3::Py, x: f64, y: f64, p: crate::generated::types::PyFbmParams) -> PyResult { + let __cb_n = std::rc::Rc::new(crate::runtime::Callback::new(n)); + let n = { let __cb = __cb_n.clone(); move |__a0: f64, __a1: f64| -> f64 { __cb.call::<_, f64>((__a0, __a1), f64::NAN) } }; + let p = p.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::noise::fbm_2d(&n, x, y, &p)); + crate::runtime::callback::check(&[&__cb_n], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// 3-D fBm. +/// +/// Rust: `fractals::noise::fbm_3d` +#[pyfunction] +#[pyo3(name = "fbm_3d", signature = (n, x, y, z, p))] +pub fn pyfn_fbm_3d(n: pyo3::Py, x: f64, y: f64, z: f64, p: crate::generated::types::PyFbmParams) -> PyResult { + let __cb_n = std::rc::Rc::new(crate::runtime::Callback::new(n)); + let n = { let __cb = __cb_n.clone(); move |__a0: f64, __a1: f64, __a2: f64| -> f64 { __cb.call::<_, f64>((__a0, __a1, __a2), f64::NAN) } }; + let p = p.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::noise::fbm_3d(&n, x, y, z, &p)); + crate::runtime::callback::check(&[&__cb_n], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Turbulence: fBm of |n| (Perlin 1985's marble basis). +/// +/// Rust: `fractals::noise::turbulence_2d` +#[pyfunction] +#[pyo3(name = "turbulence_2d", signature = (n, x, y, p))] +pub fn pyfn_turbulence_2d(n: pyo3::Py, x: f64, y: f64, p: crate::generated::types::PyFbmParams) -> PyResult { + let __cb_n = std::rc::Rc::new(crate::runtime::Callback::new(n)); + let n = { let __cb = __cb_n.clone(); move |__a0: f64, __a1: f64| -> f64 { __cb.call::<_, f64>((__a0, __a1), f64::NAN) } }; + let p = p.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::noise::turbulence_2d(&n, x, y, &p)); + crate::runtime::callback::check(&[&__cb_n], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Musgrave's ridged multifractal: octaves of (offset − |n|)², +/// each weighted by the previous octave's signal. +/// +/// Rust: `fractals::noise::ridged_multifractal_2d` +#[pyfunction] +#[pyo3(name = "ridged_multifractal_2d", signature = (n, x, y, p, offset, gain))] +pub fn pyfn_ridged_multifractal_2d(n: pyo3::Py, x: f64, y: f64, p: crate::generated::types::PyFbmParams, offset: f64, gain: f64) -> PyResult { + let __cb_n = std::rc::Rc::new(crate::runtime::Callback::new(n)); + let n = { let __cb = __cb_n.clone(); move |__a0: f64, __a1: f64| -> f64 { __cb.call::<_, f64>((__a0, __a1), f64::NAN) } }; + let p = p.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::noise::ridged_multifractal_2d(&n, x, y, &p, offset, gain)); + crate::runtime::callback::check(&[&__cb_n], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Musgrave's hybrid multifractal (3-D): additive multifractal with +/// octave weights damped by the running product. +/// +/// Rust: `fractals::noise::hybrid_multifractal` +#[pyfunction] +#[pyo3(name = "hybrid_multifractal", signature = (n, x, y, z, p, offset))] +pub fn pyfn_hybrid_multifractal(n: pyo3::Py, x: f64, y: f64, z: f64, p: crate::generated::types::PyFbmParams, offset: f64) -> PyResult { + let __cb_n = std::rc::Rc::new(crate::runtime::Callback::new(n)); + let n = { let __cb = __cb_n.clone(); move |__a0: f64, __a1: f64, __a2: f64| -> f64 { __cb.call::<_, f64>((__a0, __a1, __a2), f64::NAN) } }; + let p = p.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::noise::hybrid_multifractal(&n, x, y, z, &p, offset)); + crate::runtime::callback::check(&[&__cb_n], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Billow: fBm of 2|n| − 1 (puffy cloud look). +/// +/// Rust: `fractals::noise::billow_2d` +#[pyfunction] +#[pyo3(name = "billow_2d", signature = (n, x, y, p))] +pub fn pyfn_billow_2d(n: pyo3::Py, x: f64, y: f64, p: crate::generated::types::PyFbmParams) -> PyResult { + let __cb_n = std::rc::Rc::new(crate::runtime::Callback::new(n)); + let n = { let __cb = __cb_n.clone(); move |__a0: f64, __a1: f64| -> f64 { __cb.call::<_, f64>((__a0, __a1), f64::NAN) } }; + let p = p.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::noise::billow_2d(&n, x, y, &p)); + crate::runtime::callback::check(&[&__cb_n], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Iterated domain warping (Quilez): the sample point is repeatedly +/// displaced by an fBm offset field before the final evaluation. +/// +/// Rust: `fractals::noise::domain_warp_2d` +#[pyfunction] +#[pyo3(name = "domain_warp_2d", signature = (n, x, y, p, warp_strength, iterations))] +pub fn pyfn_domain_warp_2d(n: pyo3::Py, x: f64, y: f64, p: crate::generated::types::PyFbmParams, warp_strength: f64, iterations: usize) -> PyResult { + let __cb_n = std::rc::Rc::new(crate::runtime::Callback::new(n)); + let n = { let __cb = __cb_n.clone(); move |__a0: f64, __a1: f64| -> f64 { __cb.call::<_, f64>((__a0, __a1), f64::NAN) } }; + let p = p.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::noise::domain_warp_2d(&n, x, y, &p, warp_strength, iterations)); + crate::runtime::callback::check(&[&__cb_n], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// 3-D iterated domain warping. +/// +/// Rust: `fractals::noise::domain_warp_3d` +#[pyfunction] +#[pyo3(name = "domain_warp_3d", signature = (n, x, y, z, p, strength, iterations))] +pub fn pyfn_domain_warp_3d(n: pyo3::Py, x: f64, y: f64, z: f64, p: crate::generated::types::PyFbmParams, strength: f64, iterations: usize) -> PyResult { + let __cb_n = std::rc::Rc::new(crate::runtime::Callback::new(n)); + let n = { let __cb = __cb_n.clone(); move |__a0: f64, __a1: f64, __a2: f64| -> f64 { __cb.call::<_, f64>((__a0, __a1, __a2), f64::NAN) } }; + let p = p.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::noise::domain_warp_3d(&n, x, y, z, &p, strength, iterations)); + crate::runtime::callback::check(&[&__cb_n], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Divergence-free 2-D flow from a scalar noise potential: +/// v = (∂ψ/∂y, −∂ψ/∂x) by central differences. +/// +/// Panics: +/// Panics unless `eps > 0`. +/// +/// Rust: `fractals::noise::curl_noise_2d` +#[pyfunction] +#[pyo3(name = "curl_noise_2d", signature = (n, x, y, eps))] +pub fn pyfn_curl_noise_2d(n: pyo3::Py, x: f64, y: f64, eps: f64) -> PyResult { + let __cb_n = std::rc::Rc::new(crate::runtime::Callback::new(n)); + let n = { let __cb = __cb_n.clone(); move |__a0: f64, __a1: f64| -> f64 { __cb.call::<_, f64>((__a0, __a1), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::noise::curl_noise_2d(&n, x, y, eps)); + crate::runtime::callback::check(&[&__cb_n], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) +} + +/// Divergence-free 3-D flow: curl of a vector potential whose three +/// components are offset copies of `n` (Bridson et al. 2007). +/// +/// Panics: +/// Panics unless `eps > 0`. +/// +/// Rust: `fractals::noise::curl_noise_3d` +#[pyfunction] +#[pyo3(name = "curl_noise_3d", signature = (n, x, y, z, eps))] +pub fn pyfn_curl_noise_3d(n: pyo3::Py, x: f64, y: f64, z: f64, eps: f64) -> PyResult { + let __cb_n = std::rc::Rc::new(crate::runtime::Callback::new(n)); + let n = { let __cb = __cb_n.clone(); move |__a0: f64, __a1: f64, __a2: f64| -> f64 { __cb.call::<_, f64>((__a0, __a1, __a2), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::noise::curl_noise_3d(&n, x, y, z, eps)); + crate::runtime::callback::check(&[&__cb_n], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Samples a noise function into a scalar field. +/// +/// Rust: `fractals::noise::noise_field_2d` +#[pyfunction] +#[pyo3(name = "noise_field_2d", signature = (n, bounds, w, h))] +pub fn pyfn_noise_field_2d(n: pyo3::Py, bounds: crate::generated::types::PyRect, w: usize, h: usize) -> PyResult { + let __cb_n = std::rc::Rc::new(crate::runtime::Callback::new(n)); + let n = { let __cb = __cb_n.clone(); move |__a0: f64, __a1: f64| -> f64 { __cb.call::<_, f64>((__a0, __a1), f64::NAN) } }; + let bounds = bounds.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::noise::noise_field_2d(&n, &bounds, w, h)); + crate::runtime::callback::check(&[&__cb_n], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyIsosurfaceScalarField2 { inner: __v }) +} + +/// Samples a noise function into a 3-D scalar field. +/// +/// Rust: `fractals::noise::noise_field_3d` +#[pyfunction] +#[pyo3(name = "noise_field_3d", signature = (n, bounds, res))] +pub fn pyfn_noise_field_3d(n: pyo3::Py, bounds: crate::generated::types::PyAabb, res: (usize, usize, usize)) -> PyResult { + let __cb_n = std::rc::Rc::new(crate::runtime::Callback::new(n)); + let n = { let __cb = __cb_n.clone(); move |__a0: f64, __a1: f64, __a2: f64| -> f64 { __cb.call::<_, f64>((__a0, __a1, __a2), f64::NAN) } }; + let bounds = bounds.inner; + let res = (res.0, res.1, res.2); + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::noise::noise_field_3d(&n, &bounds, res)); + crate::runtime::callback::check(&[&__cb_n], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyIsosurfaceScalarField3 { inner: __v }) +} + +/// fBm heightmap (row-major, `w` × `h`) with optional thermal +/// erosion: material moves down slopes exceeding the talus angle, +/// smoothing scree until the terrain settles. +/// +/// Panics: +/// Panics unless the grid has at least 2×2 samples. +/// +/// Rust: `fractals::noise::terrain_heightmap` +#[pyfunction] +#[pyo3(name = "terrain_heightmap", signature = (seed, w, h, p, erosion_iters))] +pub fn pyfn_terrain_heightmap<'py>(py: Python<'py>, seed: u64, w: usize, h: usize, p: crate::generated::types::PyFbmParams, erosion_iters: usize) -> PyResult> { + let p = p.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fractals::noise::terrain_heightmap(seed, w, h, &p, erosion_iters))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Simulates `droplets` water droplets over the heightmap, eroding +/// and depositing material along their paths. +/// +/// Panics: +/// Panics unless the grid is at least 3×3 and `height.len() == w·h`. +/// +/// Rust: `fractals::noise::hydraulic_erosion` +#[pyfunction] +#[pyo3(name = "hydraulic_erosion", signature = (height, w, h, droplets, rng, params))] +pub fn pyfn_hydraulic_erosion<'py>(height: pyo3::Bound<'py, pyo3::PyAny>, w: usize, h: usize, droplets: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>, params: crate::generated::types::PyErosionParams) -> PyResult<()> { + let mut height__v: Vec = height.extract()?; + let mut rng = rng; + let params = params.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::noise::hydraulic_erosion(&mut height__v, w, h, droplets, &mut rng.inner, ¶ms)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + crate::runtime::coerce::write_back(&height, &height__v)?; + Ok(()) +} + +/// Diamond-square (plasma) fractal heightmap on a +/// (2^size_pow2 + 1)² grid, row-major, roughness halving the random +/// amplitude at each subdivision. +/// +/// Panics: +/// Panics unless `1 <= size_pow2 <= 12`. +/// +/// Rust: `fractals::noise::diamond_square` +#[pyfunction] +#[pyo3(name = "diamond_square", signature = (size_pow2, roughness, seed))] +pub fn pyfn_diamond_square<'py>(py: Python<'py>, size_pow2: u32, roughness: f64, seed: u64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fractals::noise::diamond_square(size_pow2, roughness, seed))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// 1/f^β spectral synthesis by direct summation of 64 random plane +/// waves with amplitudes f^{−β/2} (row-major, values roughly in +/// [-1, 1] after normalization). +/// +/// Panics: +/// Panics unless the grid has at least 2×2 samples. +/// +/// Rust: `fractals::noise::spectral_synthesis_2d` +#[pyfunction] +#[pyo3(name = "spectral_synthesis_2d", signature = (w, h, beta, seed))] +pub fn pyfn_spectral_synthesis_2d<'py>(py: Python<'py>, w: usize, h: usize, beta: f64, seed: u64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fractals::noise::spectral_synthesis_2d(w, h, beta, seed))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Stateless hash white noise in [-1, 1]: the same (seed, x, y) +/// always yields the same value, with no correlation between +/// nearby inputs. +/// +/// Rust: `fractals::noise::white_noise_2d` +#[pyfunction] +#[pyo3(name = "white_noise_2d", signature = (seed, x, y))] +pub fn pyfn_white_noise_2d(seed: u64, x: f64, y: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::noise::white_noise_2d(seed, x, y)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Void-and-cluster blue-noise threshold texture (Ulichney 1993): +/// returns ranks normalized to [0, 1), toroidally tileable; every +/// rank appears exactly once. +/// +/// Panics: +/// Panics unless `w·h >= 4` (and `w, h >= 2`). +/// +/// Rust: `fractals::noise::blue_noise_texture` +#[pyfunction] +#[pyo3(name = "blue_noise_texture", signature = (w, h, seed))] +pub fn pyfn_blue_noise_texture<'py>(py: Python<'py>, w: usize, h: usize, seed: u64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fractals::noise::blue_noise_texture(w, h, seed))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Sparse Gabor noise: the sum of the kernels at (x, y) +/// (Lagae et al. 2009 with an explicit kernel list). +/// +/// Rust: `fractals::noise::gabor_noise_2d` +#[pyfunction] +#[pyo3(name = "gabor_noise_2d", signature = (x, y, kernels))] +pub fn pyfn_gabor_noise_2d<'py>(py: Python<'py>, x: f64, y: f64, kernels: Vec) -> PyResult { + let kernels = kernels.into_iter().map(|__e| __e.inner).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::fractals::noise::gabor_noise_2d(x, y, &kernels))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_fbm_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fbm_3d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_turbulence_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ridged_multifractal_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hybrid_multifractal, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_billow_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_domain_warp_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_domain_warp_3d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_curl_noise_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_curl_noise_3d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_noise_field_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_noise_field_3d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_terrain_heightmap, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hydraulic_erosion, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_diamond_square, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spectral_synthesis_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_white_noise_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_blue_noise_texture, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gabor_noise_2d, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_general_relativity.rs b/bindings/python/src/generated/m_general_relativity.rs new file mode 100644 index 0000000..be0b182 --- /dev/null +++ b/bindings/python/src/generated/m_general_relativity.rs @@ -0,0 +1,317 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Schwarzschild radius: r_s = 2GM/c² +/// +/// Rust: `general_relativity::schwarzschild_radius` +#[pyfunction] +#[pyo3(name = "schwarzschild_radius", signature = (mass))] +pub fn pyfn_schwarzschild_radius(mass: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::general_relativity::schwarzschild_radius(mass)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Event horizon radius (alias for schwarzschild_radius). +/// +/// Rust: `general_relativity::event_horizon_radius` +#[pyfunction] +#[pyo3(name = "event_horizon_radius", signature = (mass))] +pub fn pyfn_event_horizon_radius(mass: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::general_relativity::event_horizon_radius(mass)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Time-time component of the Schwarzschild metric: g_tt = -(1 - r_s/r) +/// +/// Rust: `general_relativity::schwarzschild_metric_tt` +#[pyfunction] +#[pyo3(name = "schwarzschild_metric_tt", signature = (mass, r))] +pub fn pyfn_schwarzschild_metric_tt(mass: f64, r: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::general_relativity::schwarzschild_metric_tt(mass, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Radial-radial component of the Schwarzschild metric: g_rr = 1/(1 - r_s/r) +/// +/// Rust: `general_relativity::schwarzschild_metric_rr` +#[pyfunction] +#[pyo3(name = "schwarzschild_metric_rr", signature = (mass, r))] +pub fn pyfn_schwarzschild_metric_rr(mass: f64, r: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::general_relativity::schwarzschild_metric_rr(mass, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Proper time factor: dτ/dt = √(1 - r_s/r) +/// +/// Rust: `general_relativity::proper_time_factor` +#[pyfunction] +#[pyo3(name = "proper_time_factor", signature = (mass, r))] +pub fn pyfn_proper_time_factor(mass: f64, r: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::general_relativity::proper_time_factor(mass, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Gravitational redshift factor between emitter at r_emit and observer at r_obs: +/// z_factor = √((1 - r_s/r_obs) / (1 - r_s/r_emit)) +/// +/// Rust: `general_relativity::gravitational_redshift_factor` +#[pyfunction] +#[pyo3(name = "gravitational_redshift_factor", signature = (mass, r_emit, r_obs))] +pub fn pyfn_gravitational_redshift_factor(mass: f64, r_emit: f64, r_obs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::general_relativity::gravitational_redshift_factor(mass, r_emit, r_obs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Innermost stable circular orbit for Schwarzschild: r_isco = 3 r_s = 6GM/c² +/// +/// Rust: `general_relativity::isco_radius` +#[pyfunction] +#[pyo3(name = "isco_radius", signature = (mass))] +pub fn pyfn_isco_radius(mass: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::general_relativity::isco_radius(mass)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Photon sphere radius: r_ph = 1.5 r_s = 3GM/c² +/// +/// Rust: `general_relativity::photon_sphere_radius` +#[pyfunction] +#[pyo3(name = "photon_sphere_radius", signature = (mass))] +pub fn pyfn_photon_sphere_radius(mass: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::general_relativity::photon_sphere_radius(mass)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Kerr outer event horizon: r+ = GM/c² + √((GM/c²)² - a²) +/// where a is the spin parameter (dimensions of length). +/// +/// Rust: `general_relativity::kerr_event_horizon` +#[pyfunction] +#[pyo3(name = "kerr_event_horizon", signature = (mass, spin))] +pub fn pyfn_kerr_event_horizon(mass: f64, spin: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::general_relativity::kerr_event_horizon(mass, spin)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Kerr ergosphere radius at polar angle theta: +/// r_ergo = GM/c² + √((GM/c²)² - a²cos²θ) +/// +/// Rust: `general_relativity::kerr_ergosphere_radius` +#[pyfunction] +#[pyo3(name = "kerr_ergosphere_radius", signature = (mass, spin, theta))] +pub fn pyfn_kerr_ergosphere_radius(mass: f64, spin: f64, theta: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::general_relativity::kerr_ergosphere_radius(mass, spin, theta)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// ISCO radius for a Kerr black hole using the exact Bardeen-Press-Teukolsky formula. +/// `spin` is the dimensionless spin parameter a/M (in geometric units, a* = Jc/(GM²)). +/// `prograde` selects co-rotating (true) or counter-rotating (false) orbits. +/// Returns the ISCO in metres. +/// +/// Rust: `general_relativity::kerr_isco` +#[pyfunction] +#[pyo3(name = "kerr_isco", signature = (mass, spin, prograde))] +pub fn pyfn_kerr_isco(mass: f64, spin: f64, prograde: bool) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::general_relativity::kerr_isco(mass, spin, prograde)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Frame-dragging angular velocity (weak-field / Lense-Thirring limit): +/// Ω = 2GMa / (c²r³) +/// Here `spin` is the spin parameter a with dimensions of length. +/// +/// Rust: `general_relativity::frame_dragging_rate` +#[pyfunction] +#[pyo3(name = "frame_dragging_rate", signature = (mass, spin, r))] +pub fn pyfn_frame_dragging_rate(mass: f64, spin: f64, r: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::general_relativity::frame_dragging_rate(mass, spin, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Radial geodesic acceleration in Schwarzschild spacetime (effective potential approach): +/// d²r/dτ² = -GM/r² + l²(r - 3GM/c²) / r⁴ +/// where l is the specific angular momentum (per unit mass). +/// +/// Rust: `general_relativity::geodesic_acceleration_schwarzschild` +#[pyfunction] +#[pyo3(name = "geodesic_acceleration_schwarzschild", signature = (mass, r, dr_dtau, l))] +pub fn pyfn_geodesic_acceleration_schwarzschild(mass: f64, r: f64, dr_dtau: f64, l: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::general_relativity::geodesic_acceleration_schwarzschild(mass, r, dr_dtau, l)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Effective potential for a massive particle in Schwarzschild spacetime: +/// V_eff = -GMm/r + l²/(2mr²) - GMl²/(mc²r³) +/// +/// Rust: `general_relativity::effective_potential_schwarzschild` +#[pyfunction] +#[pyo3(name = "effective_potential_schwarzschild", signature = (mass, r, l, particle_mass))] +pub fn pyfn_effective_potential_schwarzschild(mass: f64, r: f64, l: f64, particle_mass: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::general_relativity::effective_potential_schwarzschild(mass, r, l, particle_mass)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Specific energy of a circular orbit in Schwarzschild: +/// E/(mc²) = (1 - 2GM/(rc²)) / √(1 - 3GM/(rc²)) +/// +/// Rust: `general_relativity::circular_orbit_energy` +#[pyfunction] +#[pyo3(name = "circular_orbit_energy", signature = (mass, r))] +pub fn pyfn_circular_orbit_energy(mass: f64, r: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::general_relativity::circular_orbit_energy(mass, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Specific angular momentum of a circular orbit in Schwarzschild: +/// L/(mc) = r √(GM / (r c² - 3GM)) → simplified from the exact expression. +/// Returns L/(mc) (dimensionless when r and GM/c² share length units, but here in SI +/// it carries dimensions of length). +/// +/// Rust: `general_relativity::circular_orbit_angular_momentum` +#[pyfunction] +#[pyo3(name = "circular_orbit_angular_momentum", signature = (mass, r))] +pub fn pyfn_circular_orbit_angular_momentum(mass: f64, r: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::general_relativity::circular_orbit_angular_momentum(mass, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Friedmann equation (flat universe, matter-dominated): +/// H = √(8πGρ/3) +/// For the full form with curvature and cosmological constant the caller should +/// construct the effective density; this returns the Hubble parameter for a given +/// energy density. +/// +/// Rust: `general_relativity::friedmann_hubble` +#[pyfunction] +#[pyo3(name = "friedmann_hubble", signature = (density, curvature, cosmological_constant))] +pub fn pyfn_friedmann_hubble(density: f64, curvature: f64, cosmological_constant: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::general_relativity::friedmann_hubble(density, curvature, cosmological_constant)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Critical density of the universe: ρ_c = 3H² / (8πG) +/// +/// Rust: `general_relativity::critical_density` +#[pyfunction] +#[pyo3(name = "critical_density", signature = (hubble))] +pub fn pyfn_critical_density(hubble: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::general_relativity::critical_density(hubble)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Comoving distance via Hubble's law (valid for z << 1): d ≈ cz / H₀ +/// +/// Rust: `general_relativity::cosmological_redshift_distance` +#[pyfunction] +#[pyo3(name = "cosmological_redshift_distance", signature = (redshift, hubble))] +pub fn pyfn_cosmological_redshift_distance(redshift: f64, hubble: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::general_relativity::cosmological_redshift_distance(redshift, hubble)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Luminosity distance (first-order expansion): d_L = (c/H₀) z (1 + z/2) +/// +/// Rust: `general_relativity::luminosity_distance` +#[pyfunction] +#[pyo3(name = "luminosity_distance", signature = (redshift, hubble))] +pub fn pyfn_luminosity_distance(redshift: f64, hubble: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::general_relativity::luminosity_distance(redshift, hubble)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Lookback time (matter-dominated approximation): t ≈ z / (H₀ (1+z)) +/// +/// Rust: `general_relativity::lookback_time` +#[pyfunction] +#[pyo3(name = "lookback_time", signature = (redshift, hubble))] +pub fn pyfn_lookback_time(redshift: f64, hubble: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::general_relativity::lookback_time(redshift, hubble)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Scale factor from cosmological redshift: a = 1/(1+z) +/// +/// Rust: `general_relativity::scale_factor_from_redshift` +#[pyfunction] +#[pyo3(name = "scale_factor_from_redshift", signature = (redshift))] +pub fn pyfn_scale_factor_from_redshift(redshift: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::general_relativity::scale_factor_from_redshift(redshift)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// CMB temperature at a given redshift: T = T₀ (1+z) +/// +/// Rust: `general_relativity::temperature_at_redshift` +#[pyfunction] +#[pyo3(name = "temperature_at_redshift", signature = (t0, redshift))] +pub fn pyfn_temperature_at_redshift(t0: f64, redshift: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::general_relativity::temperature_at_redshift(t0, redshift)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_schwarzschild_radius, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_event_horizon_radius, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_schwarzschild_metric_tt, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_schwarzschild_metric_rr, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_proper_time_factor, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gravitational_redshift_factor, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_isco_radius, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_photon_sphere_radius, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kerr_event_horizon, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kerr_ergosphere_radius, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kerr_isco, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_frame_dragging_rate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_geodesic_acceleration_schwarzschild, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_effective_potential_schwarzschild, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_circular_orbit_energy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_circular_orbit_angular_momentum, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_friedmann_hubble, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_critical_density, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cosmological_redshift_distance, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_luminosity_distance, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lookback_time, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_scale_factor_from_redshift, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_temperature_at_redshift, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_geometry.rs b/bindings/python/src/generated/m_geometry.rs new file mode 100644 index 0000000..65222b3 --- /dev/null +++ b/bindings/python/src/generated/m_geometry.rs @@ -0,0 +1,382 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Area of a circle: A = πr² +/// +/// Rust: `geometry::area_circle` +#[pyfunction] +#[pyo3(name = "area_circle", signature = (radius))] +pub fn pyfn_area_circle(radius: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geometry::area_circle(radius)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Area of an ellipse: A = πab +/// +/// Rust: `geometry::area_ellipse` +#[pyfunction] +#[pyo3(name = "area_ellipse", signature = (semi_major, semi_minor))] +pub fn pyfn_area_ellipse(semi_major: f64, semi_minor: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geometry::area_ellipse(semi_major, semi_minor)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Area of a triangle: A = bh/2 +/// +/// Rust: `geometry::area_triangle` +#[pyfunction] +#[pyo3(name = "area_triangle", signature = (base, height))] +pub fn pyfn_area_triangle(base: f64, height: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geometry::area_triangle(base, height)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Area of a triangle via Heron's formula: A = √(s(s-a)(s-b)(s-c)) where s = (a+b+c)/2 +/// +/// Rust: `geometry::area_triangle_heron` +#[pyfunction] +#[pyo3(name = "area_triangle_heron", signature = (a, b, c))] +pub fn pyfn_area_triangle_heron(a: f64, b: f64, c: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geometry::area_triangle_heron(a, b, c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Area of a regular polygon: A = n·s²/(4·tan(π/n)) +/// +/// Rust: `geometry::area_regular_polygon` +#[pyfunction] +#[pyo3(name = "area_regular_polygon", signature = (n_sides, side_length))] +pub fn pyfn_area_regular_polygon(n_sides: u32, side_length: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geometry::area_regular_polygon(n_sides, side_length)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Area of a circular sector: A = r²θ/2 +/// +/// Rust: `geometry::area_sector` +#[pyfunction] +#[pyo3(name = "area_sector", signature = (radius, angle))] +pub fn pyfn_area_sector(radius: f64, angle: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geometry::area_sector(radius, angle)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Area of an annulus: A = π(R² - r²) +/// +/// Rust: `geometry::area_annulus` +#[pyfunction] +#[pyo3(name = "area_annulus", signature = (outer_r, inner_r))] +pub fn pyfn_area_annulus(outer_r: f64, inner_r: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geometry::area_annulus(outer_r, inner_r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Volume of a sphere: V = 4πr³/3 +/// +/// Rust: `geometry::volume_sphere` +#[pyfunction] +#[pyo3(name = "volume_sphere", signature = (radius))] +pub fn pyfn_volume_sphere(radius: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geometry::volume_sphere(radius)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Volume of a cylinder: V = πr²h +/// +/// Rust: `geometry::volume_cylinder` +#[pyfunction] +#[pyo3(name = "volume_cylinder", signature = (radius, height))] +pub fn pyfn_volume_cylinder(radius: f64, height: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geometry::volume_cylinder(radius, height)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Volume of a cone: V = πr²h/3 +/// +/// Rust: `geometry::volume_cone` +#[pyfunction] +#[pyo3(name = "volume_cone", signature = (radius, height))] +pub fn pyfn_volume_cone(radius: f64, height: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geometry::volume_cone(radius, height)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Volume of an ellipsoid: V = 4πabc/3 +/// +/// Rust: `geometry::volume_ellipsoid` +#[pyfunction] +#[pyo3(name = "volume_ellipsoid", signature = (a, b, c))] +pub fn pyfn_volume_ellipsoid(a: f64, b: f64, c: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geometry::volume_ellipsoid(a, b, c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Volume of a torus: V = 2π²Rr² +/// +/// Rust: `geometry::volume_torus` +#[pyfunction] +#[pyo3(name = "volume_torus", signature = (major_r, minor_r))] +pub fn pyfn_volume_torus(major_r: f64, minor_r: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geometry::volume_torus(major_r, minor_r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Volume of a frustum: V = πh(r₁² + r₁r₂ + r₂²)/3 +/// +/// Rust: `geometry::volume_frustum` +#[pyfunction] +#[pyo3(name = "volume_frustum", signature = (r1, r2, height))] +pub fn pyfn_volume_frustum(r1: f64, r2: f64, height: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geometry::volume_frustum(r1, r2, height)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Volume of a capsule (cylinder + sphere): V = πr²h + 4πr³/3 +/// +/// Rust: `geometry::volume_capsule` +#[pyfunction] +#[pyo3(name = "volume_capsule", signature = (radius, cylinder_height))] +pub fn pyfn_volume_capsule(radius: f64, cylinder_height: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geometry::volume_capsule(radius, cylinder_height)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Surface area of a sphere: A = 4πr² +/// +/// Rust: `geometry::surface_sphere` +#[pyfunction] +#[pyo3(name = "surface_sphere", signature = (radius))] +pub fn pyfn_surface_sphere(radius: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geometry::surface_sphere(radius)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Total surface area of a cylinder (lateral + both caps): A = 2πr(r + h) +/// +/// Rust: `geometry::surface_cylinder_total` +#[pyfunction] +#[pyo3(name = "surface_cylinder_total", signature = (radius, height))] +pub fn pyfn_surface_cylinder_total(radius: f64, height: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geometry::surface_cylinder_total(radius, height)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Lateral surface area of a cylinder: A = 2πrh +/// +/// Rust: `geometry::surface_cylinder_lateral` +#[pyfunction] +#[pyo3(name = "surface_cylinder_lateral", signature = (radius, height))] +pub fn pyfn_surface_cylinder_lateral(radius: f64, height: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geometry::surface_cylinder_lateral(radius, height)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Lateral surface area of a cone: A = πrl +/// +/// Rust: `geometry::surface_cone_lateral` +#[pyfunction] +#[pyo3(name = "surface_cone_lateral", signature = (radius, slant_height))] +pub fn pyfn_surface_cone_lateral(radius: f64, slant_height: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geometry::surface_cone_lateral(radius, slant_height)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Surface area of a torus: A = 4π²Rr +/// +/// Rust: `geometry::surface_torus` +#[pyfunction] +#[pyo3(name = "surface_torus", signature = (major_r, minor_r))] +pub fn pyfn_surface_torus(major_r: f64, minor_r: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geometry::surface_torus(major_r, minor_r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Solid angle subtended by a cone: Ω = 2π(1 - cos(θ)) +/// +/// Rust: `geometry::solid_angle_cone` +#[pyfunction] +#[pyo3(name = "solid_angle_cone", signature = (half_angle))] +pub fn pyfn_solid_angle_cone(half_angle: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geometry::solid_angle_cone(half_angle)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Solid angle of a full sphere: Ω = 4π steradians +/// +/// Rust: `geometry::solid_angle_full_sphere` +#[pyfunction] +#[pyo3(name = "solid_angle_full_sphere", signature = ())] +pub fn pyfn_solid_angle_full_sphere() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geometry::solid_angle_full_sphere()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Great-circle distance on a sphere: d = r·arccos(sin(φ₁)sin(φ₂) + cos(φ₁)cos(φ₂)cos(Δλ)) +/// +/// Rust: `geometry::great_circle_distance` +#[pyfunction] +#[pyo3(name = "great_circle_distance", signature = (r, lat1, lon1, lat2, lon2))] +pub fn pyfn_great_circle_distance(r: f64, lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geometry::great_circle_distance(r, lat1, lon1, lat2, lon2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Spherical excess of a spherical triangle: E = A + B + C - π +/// +/// Rust: `geometry::spherical_excess` +#[pyfunction] +#[pyo3(name = "spherical_excess", signature = (a, b, c))] +pub fn pyfn_spherical_excess(a: f64, b: f64, c: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geometry::spherical_excess(a, b, c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Moment of inertia of a solid sphere: I = 2mr²/5 +/// +/// Rust: `geometry::moi_solid_sphere` +#[pyfunction] +#[pyo3(name = "moi_solid_sphere", signature = (mass, radius))] +pub fn pyfn_moi_solid_sphere(mass: f64, radius: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geometry::moi_solid_sphere(mass, radius)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Moment of inertia of a hollow sphere (thin shell): I = 2mr²/3 +/// +/// Rust: `geometry::moi_hollow_sphere` +#[pyfunction] +#[pyo3(name = "moi_hollow_sphere", signature = (mass, radius))] +pub fn pyfn_moi_hollow_sphere(mass: f64, radius: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geometry::moi_hollow_sphere(mass, radius)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Moment of inertia of a solid cylinder about its axis: I = mr²/2 +/// +/// Rust: `geometry::moi_solid_cylinder` +#[pyfunction] +#[pyo3(name = "moi_solid_cylinder", signature = (mass, radius))] +pub fn pyfn_moi_solid_cylinder(mass: f64, radius: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geometry::moi_solid_cylinder(mass, radius)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Moment of inertia of a thin rod about its center: I = mL²/12 +/// +/// Rust: `geometry::moi_thin_rod_center` +#[pyfunction] +#[pyo3(name = "moi_thin_rod_center", signature = (mass, length))] +pub fn pyfn_moi_thin_rod_center(mass: f64, length: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geometry::moi_thin_rod_center(mass, length)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Moment of inertia of a thin rod about one end: I = mL²/3 +/// +/// Rust: `geometry::moi_thin_rod_end` +#[pyfunction] +#[pyo3(name = "moi_thin_rod_end", signature = (mass, length))] +pub fn pyfn_moi_thin_rod_end(mass: f64, length: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geometry::moi_thin_rod_end(mass, length)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Moment of inertia of a rectangular plate about its center: I = m(w² + h²)/12 +/// +/// Rust: `geometry::moi_rectangular_plate` +#[pyfunction] +#[pyo3(name = "moi_rectangular_plate", signature = (mass, width, height))] +pub fn pyfn_moi_rectangular_plate(mass: f64, width: f64, height: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geometry::moi_rectangular_plate(mass, width, height)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Parallel axis theorem: I = I_cm + md² +/// +/// Rust: `geometry::parallel_axis` +#[pyfunction] +#[pyo3(name = "parallel_axis", signature = (i_cm, mass, distance))] +pub fn pyfn_parallel_axis(i_cm: f64, mass: f64, distance: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geometry::parallel_axis(i_cm, mass, distance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_area_circle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_area_ellipse, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_area_triangle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_area_triangle_heron, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_area_regular_polygon, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_area_sector, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_area_annulus, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_volume_sphere, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_volume_cylinder, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_volume_cone, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_volume_ellipsoid, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_volume_torus, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_volume_frustum, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_volume_capsule, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_surface_sphere, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_surface_cylinder_total, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_surface_cylinder_lateral, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_surface_cone_lateral, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_surface_torus, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_solid_angle_cone, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_solid_angle_full_sphere, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_great_circle_distance, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spherical_excess, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_moi_solid_sphere, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_moi_hollow_sphere, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_moi_solid_cylinder, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_moi_thin_rod_center, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_moi_thin_rod_end, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_moi_rectangular_plate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_parallel_axis, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_geometry__delaunay.rs b/bindings/python/src/generated/m_geometry__delaunay.rs new file mode 100644 index 0000000..83b30aa --- /dev/null +++ b/bindings/python/src/generated/m_geometry__delaunay.rs @@ -0,0 +1,82 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Delaunay triangulation and Voronoi diagrams in the plane. +/// +/// Triangulation: Bowyer-Watson incremental insertion with a +/// super-triangle. Voronoi cells: half-plane intersection of the +/// perpendicular bisectors (the dual definition), clipped to the +/// bounding box of the sites — robust for boundary cells. +/// Circumcircle of triangle (a, b, c): (center, radius). +/// +/// Panics: +/// Panics if the points are collinear. +/// +/// Rust: `geometry::delaunay::circumcircle` +#[pyfunction] +#[pyo3(name = "circumcircle", signature = (a, b, c))] +pub fn pyfn_circumcircle(a: (f64, f64), b: (f64, f64), c: (f64, f64)) -> PyResult<((f64, f64), f64)> { + let a = (a.0, a.1); + let b = (b.0, b.1); + let c = (c.0, c.1); + let __r = crate::runtime::guard(|| rust_physics_engine::geometry::delaunay::circumcircle(a, b, c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(((__v.0.0, __v.0.1), __v.1)) +} + +/// Delaunay triangulation by Bowyer-Watson insertion; returns triangle +/// index triples into `points`. +/// +/// Panics: +/// Panics with fewer than 3 points or if all points are collinear. +/// +/// Rust: `geometry::delaunay::delaunay_2d` +#[pyfunction] +#[pyo3(name = "delaunay_2d", signature = (points))] +pub fn pyfn_delaunay_2d<'py>(py: Python<'py>, points: Vec<(f64, f64)>) -> PyResult>> { + let points = points.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::geometry::delaunay::delaunay_2d(&points))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.to_vec()).collect::>()) +} + +/// Voronoi cell polygons for each site, clipped to the sites' bounding +/// box (expanded by 10%). Cell i is the intersection of the half-planes +/// bounded by the perpendicular bisectors toward every other site — +/// the exact dual of the Delaunay triangulation. +/// +/// Panics: +/// Panics with fewer than 2 sites. +/// +/// Rust: `geometry::delaunay::voronoi_cells_2d` +#[pyfunction] +#[pyo3(name = "voronoi_cells_2d", signature = (points))] +pub fn pyfn_voronoi_cells_2d<'py>(py: Python<'py>, points: Vec<(f64, f64)>) -> PyResult>> { + let points = points.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::geometry::delaunay::voronoi_cells_2d(&points))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.into_iter().map(|__x| (__x.0, __x.1)).collect::>()).collect::>()) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_circumcircle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_delaunay_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_voronoi_cells_2d, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_geometry__geodesy.rs b/bindings/python/src/generated/m_geometry__geodesy.rs new file mode 100644 index 0000000..448c7e2 --- /dev/null +++ b/bindings/python/src/generated/m_geometry__geodesy.rs @@ -0,0 +1,101 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Vincenty inverse problem: geodesic distance (m) and forward/reverse +/// azimuths (rad) between two geodetic points. +/// +/// Returns `NoConvergence` for the nearly antipodal cases where +/// Vincenty's lambda iteration fails, and distance 0 with azimuth 0 +/// for coincident points. +/// +/// Rust: `geometry::geodesy::vincenty_inverse` +#[pyfunction] +#[pyo3(name = "vincenty_inverse", signature = (lat1, lon1, lat2, lon2, e))] +pub fn pyfn_vincenty_inverse(lat1: f64, lon1: f64, lat2: f64, lon2: f64, e: crate::generated::types::PyEllipsoidArg) -> PyResult<(f64, f64, f64)> { + let e = e.0; + let __r = crate::runtime::guard(|| rust_physics_engine::geometry::geodesy::vincenty_inverse(lat1, lon1, lat2, lon2, &e)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// Vincenty direct problem: destination (lat2, lon2) and final azimuth +/// after traveling `dist` meters from (lat1, lon1) on initial azimuth +/// `az1`. +/// +/// Rust: `geometry::geodesy::vincenty_direct` +#[pyfunction] +#[pyo3(name = "vincenty_direct", signature = (lat1, lon1, az1, dist, e))] +pub fn pyfn_vincenty_direct(lat1: f64, lon1: f64, az1: f64, dist: f64, e: crate::generated::types::PyEllipsoidArg) -> PyResult<(f64, f64, f64)> { + let e = e.0; + let __r = crate::runtime::guard(|| rust_physics_engine::geometry::geodesy::vincenty_direct(lat1, lon1, az1, dist, &e)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// Geodetic (lat, lon, height) → Earth-centered Earth-fixed Cartesian. +/// +/// Rust: `geometry::geodesy::geodetic_to_ecef` +#[pyfunction] +#[pyo3(name = "geodetic_to_ecef", signature = (lat, lon, h, e))] +pub fn pyfn_geodetic_to_ecef(lat: f64, lon: f64, h: f64, e: crate::generated::types::PyEllipsoidArg) -> PyResult { + let e = e.0; + let __r = crate::runtime::guard(|| rust_physics_engine::geometry::geodesy::geodetic_to_ecef(lat, lon, h, &e)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// ECEF Cartesian → geodetic (lat, lon, height) by fixed-point +/// iteration on the latitude (converges to sub-millimeter in a few +/// steps). +/// +/// Rust: `geometry::geodesy::ecef_to_geodetic` +#[pyfunction] +#[pyo3(name = "ecef_to_geodetic", signature = (p, e))] +pub fn pyfn_ecef_to_geodetic(p: crate::generated::types::PyVec3Arg, e: crate::generated::types::PyEllipsoidArg) -> PyResult<(f64, f64, f64)> { + let p = p.0; + let e = e.0; + let __r = crate::runtime::guard(|| rust_physics_engine::geometry::geodesy::ecef_to_geodetic(p, &e)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// ECEF point → local East-North-Up coordinates relative to the given +/// geodetic reference. +/// +/// Rust: `geometry::geodesy::ecef_to_enu` +#[pyfunction] +#[pyo3(name = "ecef_to_enu", signature = (p, ref_lat, ref_lon, ref_h, e))] +pub fn pyfn_ecef_to_enu(p: crate::generated::types::PyVec3Arg, ref_lat: f64, ref_lon: f64, ref_h: f64, e: crate::generated::types::PyEllipsoidArg) -> PyResult { + let p = p.0; + let e = e.0; + let __r = crate::runtime::guard(|| rust_physics_engine::geometry::geodesy::ecef_to_enu(p, ref_lat, ref_lon, ref_h, &e)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_vincenty_inverse, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_vincenty_direct, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_geodetic_to_ecef, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ecef_to_geodetic, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ecef_to_enu, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_geometry__hull.rs b/bindings/python/src/generated/m_geometry__hull.rs new file mode 100644 index 0000000..da6f251 --- /dev/null +++ b/bindings/python/src/generated/m_geometry__hull.rs @@ -0,0 +1,91 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Convex hull of 2-D points by monotone chain, returned in +/// counter-clockwise order without repetition of the first vertex. +/// Collinear boundary points are dropped. Fewer than 3 distinct points +/// return the distinct points themselves. +/// +/// Rust: `geometry::hull::convex_hull_2d` +#[pyfunction] +#[pyo3(name = "convex_hull_2d", signature = (points))] +pub fn pyfn_convex_hull_2d<'py>(py: Python<'py>, points: Vec<(f64, f64)>) -> PyResult> { + let points = points.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::geometry::hull::convex_hull_2d(&points))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Shoelace formula: positive for counter-clockwise vertex order. +/// +/// Panics: +/// Panics if the polygon has fewer than 3 vertices. +/// +/// Rust: `geometry::hull::polygon_area_signed` +#[pyfunction] +#[pyo3(name = "polygon_area_signed", signature = (poly))] +pub fn pyfn_polygon_area_signed<'py>(py: Python<'py>, poly: Vec<(f64, f64)>) -> PyResult { + let poly = poly.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::geometry::hull::polygon_area_signed(&poly))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Even-odd (ray casting) point-in-polygon test; boundary points count +/// as inside up to floating-point tolerance. +/// +/// Panics: +/// Panics if the polygon has fewer than 3 vertices. +/// +/// Rust: `geometry::hull::point_in_polygon` +#[pyfunction] +#[pyo3(name = "point_in_polygon", signature = (p, poly))] +pub fn pyfn_point_in_polygon<'py>(py: Python<'py>, p: (f64, f64), poly: Vec<(f64, f64)>) -> PyResult { + let p = (p.0, p.1); + let poly = poly.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::geometry::hull::point_in_polygon(p, &poly))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convex hull of 3-D points as outward-oriented triangle index +/// triples, by the incremental visible-face (quickhull-style) +/// algorithm. +/// +/// Panics: +/// Panics with fewer than 4 points or fully degenerate (coplanar) +/// input. +/// +/// Rust: `geometry::hull::convex_hull_3d` +#[pyfunction] +#[pyo3(name = "convex_hull_3d", signature = (points))] +pub fn pyfn_convex_hull_3d<'py>(py: Python<'py>, points: Vec) -> PyResult>> { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::geometry::hull::convex_hull_3d(&points))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.to_vec()).collect::>()) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_convex_hull_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_polygon_area_signed, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_point_in_polygon, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_convex_hull_3d, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_geometry__mesh.rs b/bindings/python/src/generated/m_geometry__mesh.rs new file mode 100644 index 0000000..e3fb1bd --- /dev/null +++ b/bindings/python/src/generated/m_geometry__mesh.rs @@ -0,0 +1,24 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_geophysics.rs b/bindings/python/src/generated/m_geophysics.rs new file mode 100644 index 0000000..e4416df --- /dev/null +++ b/bindings/python/src/generated/m_geophysics.rs @@ -0,0 +1,303 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// International gravity formula: g = 9.780327(1 + 0.0053024 sin²φ − 0.0000058 sin²2φ). +/// `latitude_rad` is the geodetic latitude in radians. +/// +/// Rust: `geophysics::gravity_at_latitude` +#[pyfunction] +#[pyo3(name = "gravity_at_latitude", signature = (latitude_rad))] +pub fn pyfn_gravity_at_latitude(latitude_rad: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geophysics::gravity_at_latitude(latitude_rad)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Free-air gravity correction: Δg = −0.3086 × h (mGal). +/// `height` is meters above the geoid. +/// +/// Rust: `geophysics::free_air_correction` +#[pyfunction] +#[pyo3(name = "free_air_correction", signature = (height))] +pub fn pyfn_free_air_correction(height: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geophysics::free_air_correction(height)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Bouguer slab correction: Δg = 2πGρh. +/// Accounts for the gravitational attraction of material between the +/// station and the geoid. Returns the correction in m/s². +/// +/// Rust: `geophysics::bouguer_correction` +#[pyfunction] +#[pyo3(name = "bouguer_correction", signature = (height, density))] +pub fn pyfn_bouguer_correction(height: f64, density: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geophysics::bouguer_correction(height, density)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Bouguer anomaly: Δg_B = g_obs − g_lat + free_air − bouguer. +/// All values in consistent units (typically mGal or m/s²). +/// +/// Rust: `geophysics::bouguer_anomaly` +#[pyfunction] +#[pyo3(name = "bouguer_anomaly", signature = (observed_g, latitude_g, free_air, bouguer))] +pub fn pyfn_bouguer_anomaly(observed_g: f64, latitude_g: f64, free_air: f64, bouguer: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geophysics::bouguer_anomaly(observed_g, latitude_g, free_air, bouguer)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Airy isostatic compensation depth: d = elevation × ρ_c / (ρ_m − ρ_c). +/// Returns the depth of the crustal root below normal Moho. +/// +/// Rust: `geophysics::isostatic_compensation_depth` +#[pyfunction] +#[pyo3(name = "isostatic_compensation_depth", signature = (elevation, crust_density, mantle_density))] +pub fn pyfn_isostatic_compensation_depth(elevation: f64, crust_density: f64, mantle_density: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geophysics::isostatic_compensation_depth(elevation, crust_density, mantle_density)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// P-wave travel time: t = d / v. +/// +/// Rust: `geophysics::p_wave_travel_time` +#[pyfunction] +#[pyo3(name = "p_wave_travel_time", signature = (distance, velocity))] +pub fn pyfn_p_wave_travel_time(distance: f64, velocity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geophysics::p_wave_travel_time(distance, velocity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// S-wave travel time: t = d / v. +/// +/// Rust: `geophysics::s_wave_travel_time` +#[pyfunction] +#[pyo3(name = "s_wave_travel_time", signature = (distance, velocity))] +pub fn pyfn_s_wave_travel_time(distance: f64, velocity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geophysics::s_wave_travel_time(distance, velocity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Epicentral distance from S−P lag time: d = Δt × vp × vs / (vp − vs). +/// +/// Rust: `geophysics::epicentral_distance_from_lag` +#[pyfunction] +#[pyo3(name = "epicentral_distance_from_lag", signature = (t_s_minus_p, vp, vs))] +pub fn pyfn_epicentral_distance_from_lag(t_s_minus_p: f64, vp: f64, vs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geophysics::epicentral_distance_from_lag(t_s_minus_p, vp, vs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Simplified Richter local magnitude: M_L = log₁₀(A) + 2.76 log₁₀(Δ) − 2.48. +/// `amplitude` is the maximum trace amplitude in mm, `distance_km` in km. +/// +/// Rust: `geophysics::richter_magnitude` +#[pyfunction] +#[pyo3(name = "richter_magnitude", signature = (amplitude, distance_km))] +pub fn pyfn_richter_magnitude(amplitude: f64, distance_km: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geophysics::richter_magnitude(amplitude, distance_km)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Moment magnitude from seismic moment (N·m): M_w = (2/3) log₁₀(M₀) − 6.07. +/// +/// Rust: `geophysics::moment_magnitude` +#[pyfunction] +#[pyo3(name = "moment_magnitude", signature = (seismic_moment))] +pub fn pyfn_moment_magnitude(seismic_moment: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geophysics::moment_magnitude(seismic_moment)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Seismic moment from moment magnitude (returns N·m): M₀ = 10^(1.5(M_w + 6.07)). +/// +/// Rust: `geophysics::seismic_moment` +#[pyfunction] +#[pyo3(name = "seismic_moment", signature = (magnitude))] +pub fn pyfn_seismic_moment(magnitude: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geophysics::seismic_moment(magnitude)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Seismic energy from magnitude (Gutenberg-Richter): E = 10^(1.5M + 4.8) joules. +/// +/// Rust: `geophysics::seismic_energy` +#[pyfunction] +#[pyo3(name = "seismic_energy", signature = (magnitude))] +pub fn pyfn_seismic_energy(magnitude: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geophysics::seismic_energy(magnitude)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Lithostatic pressure at depth (simplified): P ≈ ρgd. +/// `depth` in meters, `surface_density` in kg/m³, `g` in m/s². +/// +/// Rust: `geophysics::pressure_at_depth` +#[pyfunction] +#[pyo3(name = "pressure_at_depth", signature = (depth, surface_density, g))] +pub fn pyfn_pressure_at_depth(depth: f64, surface_density: f64, g: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geophysics::pressure_at_depth(depth, surface_density, g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Temperature at depth assuming a linear geothermal gradient: +/// T = T₀ + (dT/dz) × z. +/// `geothermal_gradient` is in K/m (typical ~0.025–0.030 K/m). +/// +/// Rust: `geophysics::temperature_at_depth` +#[pyfunction] +#[pyo3(name = "temperature_at_depth", signature = (surface_temp, geothermal_gradient, depth))] +pub fn pyfn_temperature_at_depth(surface_temp: f64, geothermal_gradient: f64, depth: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geophysics::temperature_at_depth(surface_temp, geothermal_gradient, depth)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Average continental Moho depth: ~35 km. +/// +/// Rust: `geophysics::moho_depth_continental` +#[pyfunction] +#[pyo3(name = "moho_depth_continental", signature = ())] +pub fn pyfn_moho_depth_continental() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geophysics::moho_depth_continental()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Average oceanic Moho depth: ~7 km. +/// +/// Rust: `geophysics::moho_depth_oceanic` +#[pyfunction] +#[pyo3(name = "moho_depth_oceanic", signature = ())] +pub fn pyfn_moho_depth_oceanic() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geophysics::moho_depth_oceanic()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Core-mantle boundary depth: 2891 km. +/// +/// Rust: `geophysics::core_mantle_boundary_depth` +#[pyfunction] +#[pyo3(name = "core_mantle_boundary_depth", signature = ())] +pub fn pyfn_core_mantle_boundary_depth() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geophysics::core_mantle_boundary_depth()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Fourier heat flow: q = k × dT/dz (W/m²). +/// `conductivity` in W/(m·K), `temperature_gradient` in K/m. +/// +/// Rust: `geophysics::heat_flow` +#[pyfunction] +#[pyo3(name = "heat_flow", signature = (conductivity, temperature_gradient))] +pub fn pyfn_heat_flow(conductivity: f64, temperature_gradient: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geophysics::heat_flow(conductivity, temperature_gradient)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Geothermal power extracted from a fluid: P = ṁ c ΔT. +/// `flow_rate` in kg/s, `specific_heat` in J/(kg·K), `delta_temp` in K. +/// +/// Rust: `geophysics::geothermal_power` +#[pyfunction] +#[pyo3(name = "geothermal_power", signature = (flow_rate, specific_heat, delta_temp))] +pub fn pyfn_geothermal_power(flow_rate: f64, specific_heat: f64, delta_temp: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geophysics::geothermal_power(flow_rate, specific_heat, delta_temp)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Plate velocity from an Euler pole: v = ω R sin(θ). +/// `omega` in rad/s, `radius` in meters, `angular_distance` in radians. +/// +/// Rust: `geophysics::plate_velocity_euler` +#[pyfunction] +#[pyo3(name = "plate_velocity_euler", signature = (omega, radius, angular_distance))] +pub fn pyfn_plate_velocity_euler(omega: f64, radius: f64, angular_distance: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geophysics::plate_velocity_euler(omega, radius, angular_distance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Age of seafloor from distance to ridge: t = d / (2v). +/// `distance` in meters, `spreading_rate` is the full rate in m/s. +/// Division by 2 accounts for the half-spreading rate. +/// +/// Rust: `geophysics::age_of_seafloor` +#[pyfunction] +#[pyo3(name = "age_of_seafloor", signature = (distance, spreading_rate))] +pub fn pyfn_age_of_seafloor(distance: f64, spreading_rate: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geophysics::age_of_seafloor(distance, spreading_rate)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Ocean depth from seafloor age (Parsons-Sclater model): +/// d = 2500 + 350√t. +/// `age_myr` is in millions of years; returns depth in meters. +/// +/// Rust: `geophysics::ocean_depth_from_age` +#[pyfunction] +#[pyo3(name = "ocean_depth_from_age", signature = (age_myr))] +pub fn pyfn_ocean_depth_from_age(age_myr: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geophysics::ocean_depth_from_age(age_myr)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_gravity_at_latitude, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_free_air_correction, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bouguer_correction, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bouguer_anomaly, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_isostatic_compensation_depth, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_p_wave_travel_time, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_s_wave_travel_time, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_epicentral_distance_from_lag, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_richter_magnitude, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_moment_magnitude, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_seismic_moment, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_seismic_energy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pressure_at_depth, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_temperature_at_depth, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_moho_depth_continental, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_moho_depth_oceanic, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_core_mantle_boundary_depth, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_heat_flow, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_geothermal_power, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_plate_velocity_euler, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_age_of_seafloor, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ocean_depth_from_age, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_graph.rs b/bindings/python/src/generated/m_graph.rs new file mode 100644 index 0000000..c02bbf8 --- /dev/null +++ b/bindings/python/src/generated/m_graph.rs @@ -0,0 +1,22 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + Ok(()) +} diff --git a/bindings/python/src/generated/m_graph__coloring.rs b/bindings/python/src/generated/m_graph__coloring.rs new file mode 100644 index 0000000..75bf1f4 --- /dev/null +++ b/bindings/python/src/generated/m_graph__coloring.rs @@ -0,0 +1,413 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Greedy colouring in the given vertex order. +/// +/// Returns one colour per vertex, numbered from zero. Every order yields a +/// proper colouring; the count of colours is what varies, and +/// `Order::SmallestLast` and `Order::Dsatur` carry the guarantees worth +/// having. Self-loops are ignored, since no colouring can respect one. +/// +/// Rust: `graph::coloring::greedy_coloring` +#[pyfunction] +#[pyo3(name = "greedy_coloring", signature = (g, order))] +pub fn pyfn_greedy_coloring<'py>(py: Python<'py>, g: crate::generated::types::PyGraph, order: crate::generated::types::PyOrder) -> PyResult> { + let g = g.inner; + let order = order.to_rust(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::coloring::greedy_coloring(&g, order))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The number of distinct colours a colouring uses. +/// +/// Rust: `graph::coloring::color_count` +#[pyfunction] +#[pyo3(name = "color_count", signature = (coloring))] +pub fn pyfn_color_count<'py>(py: Python<'py>, coloring: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::coloring::color_count(&coloring))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Whether a colouring gives no edge two ends of the same colour. +/// +/// A self-loop always fails, which is the correct answer: a graph with one +/// has no proper colouring at all. +/// +/// Rust: `graph::coloring::is_proper_coloring` +#[pyfunction] +#[pyo3(name = "is_proper_coloring", signature = (g, coloring))] +pub fn pyfn_is_proper_coloring<'py>(py: Python<'py>, g: crate::generated::types::PyGraph, coloring: Vec) -> PyResult { + let g = g.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::coloring::is_proper_coloring(&g, &coloring))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Welsh-Powell colouring: sort by descending degree, then fill one colour +/// class at a time by sweeping the list. +/// +/// This is the same colouring `greedy_coloring` produces under +/// `Order::LargestFirst`, and for the same reason: a vertex takes colour +/// `c` in the sweep exactly when every earlier class held a neighbour of it, +/// which is the greedy rule stated the other way round. The procedure is +/// kept in its own form because the bound it is quoted with -- +/// `max_i min(d_i + 1, i)` over the sorted degrees -- is a statement about +/// the sweep. +/// +/// Rust: `graph::coloring::welsh_powell` +#[pyfunction] +#[pyo3(name = "welsh_powell", signature = (g))] +pub fn pyfn_welsh_powell<'py>(py: Python<'py>, g: crate::generated::types::PyGraph) -> PyResult> { + let g = g.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::coloring::welsh_powell(&g))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Welsh-Powell bound on the number of colours: `max_i min(d_i + 1, i)` +/// over the degrees sorted descending, indexed from one. +/// +/// Rust: `graph::coloring::welsh_powell_bound` +#[pyfunction] +#[pyo3(name = "welsh_powell_bound", signature = (g))] +pub fn pyfn_welsh_powell_bound(g: crate::generated::types::PyGraph) -> PyResult { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::coloring::welsh_powell_bound(&g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The chromatic number, by exhaustive search. Intended for `n <= 20`. +/// +/// Bracketed first: a greedy clique gives a lower bound, since a clique of +/// size `q` needs `q` colours, and DSATUR gives an upper bound. Then each `k` +/// in between is decided exactly. On a graph the bracket already pins -- and +/// it often does -- no search runs at all. +/// +/// Panics: +/// Panics on a self-loop, which admits no proper colouring. +/// +/// Rust: `graph::coloring::chromatic_number_exact_small` +#[pyfunction] +#[pyo3(name = "chromatic_number_exact_small", signature = (g))] +pub fn pyfn_chromatic_number_exact_small(g: crate::generated::types::PyGraph) -> PyResult { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::coloring::chromatic_number_exact_small(&g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The chromatic polynomial, exactly, by deletion-contraction. For `n <= 12`. +/// +/// `P(G, x)` counts the proper colourings of `G` with `x` colours, and the +/// recursion is `P(G) = P(G - e) - P(G / e)`: colourings of `G - e` either +/// give `e`'s ends different colours, which is a colouring of `G`, or the +/// same colour, which is a colouring of the contraction. Both branches +/// shrink the graph -- deletion loses an edge, contraction loses a vertex -- +/// so the recursion terminates on the edgeless graph, whose polynomial is +/// `x^n`. Memoised on the canonical edge set, which is what makes it +/// tractable at all: the two branches meet again constantly. +/// +/// Panics: +/// Panics on a self-loop. Contraction can create one only from a parallel +/// edge, which is collapsed first. +/// +/// Rust: `graph::coloring::chromatic_polynomial_small` +#[pyfunction] +#[pyo3(name = "chromatic_polynomial_small", signature = (g))] +pub fn pyfn_chromatic_polynomial_small(g: crate::generated::types::PyGraph) -> PyResult { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::coloring::chromatic_polynomial_small(&g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyQ { inner: __v }) +} + +/// Vizing edge colouring by the Misra-Gries construction: one colour per +/// edge, no two edges sharing a vertex alike, in at most `Delta + 1` colours. +/// +/// Vizing's theorem says every simple graph needs `Delta` or `Delta + 1`, and +/// this reaches the upper end constructively. Each edge is coloured by +/// building a *fan* around one endpoint -- a run of neighbours where each +/// one's edge colour is free at the previous, so the whole run can shift +/// down by one -- then either rotating the fan to slide a free colour into +/// place, or first inverting a two-colour alternating path to make one free. +/// The alternating path is the part that makes the bound work: it repairs +/// the one obstruction rotation alone cannot, and it does so without +/// disturbing any other vertex, since every interior vertex of the path +/// simply exchanges its `c` for its `d`. +/// +/// Returns one colour per entry of `g.edges()`, in that order. +/// +/// Panics: +/// Panics if the graph is directed, or is not simple. A self-loop cannot be +/// coloured at all, and a parallel edge takes the bound outside Vizing's +/// theorem into Shannon's `Delta + mu`. +/// +/// Rust: `graph::coloring::edge_coloring_vizing` +#[pyfunction] +#[pyo3(name = "edge_coloring_vizing", signature = (g))] +pub fn pyfn_edge_coloring_vizing<'py>(py: Python<'py>, g: crate::generated::types::PyGraph) -> PyResult> { + let g = g.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::coloring::edge_coloring_vizing(&g))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Whether an edge colouring gives no two edges sharing a vertex the same +/// colour. +/// +/// Rust: `graph::coloring::is_proper_edge_coloring` +#[pyfunction] +#[pyo3(name = "is_proper_edge_coloring", signature = (g, coloring))] +pub fn pyfn_is_proper_edge_coloring<'py>(py: Python<'py>, g: crate::generated::types::PyGraph, coloring: Vec) -> PyResult { + let g = g.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::coloring::is_proper_edge_coloring(&g, &coloring))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Optimal colouring of an interval graph, given the intervals themselves. +/// +/// Two intervals conflict when they overlap, and the greedy sweep in order of +/// left endpoint is optimal here -- unlike on a general graph -- because at +/// the moment an interval opens, every interval it will ever conflict with +/// that came earlier is still open. So the colours in use are exactly the +/// current overlap, and the total is the maximum overlap, which is a lower +/// bound for any colouring. Half-open intervals: touching at an endpoint is +/// not an overlap, and an interval whose ends coincide is empty, meets +/// nothing, and shares the first colour. +/// +/// Returns one colour per interval, in the input order. +/// +/// Panics: +/// Panics if an interval has its end before its start, or is not finite. +/// +/// Rust: `graph::coloring::interval_graph_coloring` +#[pyfunction] +#[pyo3(name = "interval_graph_coloring", signature = (intervals))] +pub fn pyfn_interval_graph_coloring<'py>(py: Python<'py>, intervals: Vec<(f64, f64)>) -> PyResult> { + let intervals = intervals.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::coloring::interval_graph_coloring(&intervals))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// A proper `k`-colouring of a graph given by adjacency lists, or `None`. +/// +/// The classic map-colouring formulation: regions and the regions they +/// border. Straight chronological backtracking with forward checking, which +/// is what the four-colour problem was posed as long before it was a theorem. +/// +/// Panics: +/// Panics if an adjacency list names a region outside the range. +/// +/// Rust: `graph::coloring::map_coloring_backtrack` +#[pyfunction] +#[pyo3(name = "map_coloring_backtrack", signature = (adjacency, k))] +pub fn pyfn_map_coloring_backtrack<'py>(py: Python<'py>, adjacency: Vec>, k: usize) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::coloring::map_coloring_backtrack(&adjacency, k))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// Every maximal clique, by Bron-Kerbosch with pivoting. +/// +/// A clique is maximal when no vertex can be added; the algorithm grows one +/// while maintaining the candidates that could still join (`p`) and those +/// already ruled out (`x`), and reports when both are empty. The pivot is the +/// speedup: choosing a vertex `q` from `p | x` with the most neighbours in +/// `p`, and branching only on `p` minus `q`'s neighbourhood, skips the +/// branches that could only ever rediscover a clique through `q`. +/// +/// Panics: +/// Panics above 64 vertices. The output can be exponential in the input -- +/// a graph on `3j` vertices can have `3^j` maximal cliques -- so this is for +/// small graphs by construction. +/// +/// Rust: `graph::coloring::all_maximal_cliques` +#[pyfunction] +#[pyo3(name = "all_maximal_cliques", signature = (g))] +pub fn pyfn_all_maximal_cliques<'py>(py: Python<'py>, g: crate::generated::types::PyGraph) -> PyResult>> { + let g = g.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::coloring::all_maximal_cliques(&g))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// A maximum clique: the largest set of mutually adjacent vertices. +/// +/// Every maximum clique is maximal, so enumerating the maximal ones and +/// taking the largest is exact. Ties go to the lexicographically first. +/// +/// Panics: +/// Panics above 64 vertices. +/// +/// Rust: `graph::coloring::max_clique_bron_kerbosch` +#[pyfunction] +#[pyo3(name = "max_clique_bron_kerbosch", signature = (g))] +pub fn pyfn_max_clique_bron_kerbosch<'py>(py: Python<'py>, g: crate::generated::types::PyGraph) -> PyResult> { + let g = g.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::coloring::max_clique_bron_kerbosch(&g))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// A maximal independent set, greedily: repeatedly take a vertex of minimum +/// remaining degree and discard its neighbours. +/// +/// Minimum degree first is the right greed here: taking the vertex that +/// eliminates the fewest others leaves the most room for the rest. The +/// result is guaranteed maximal -- nothing can be added -- and at least +/// `sum_v 1/(d_v + 1)` in size by the Caro-Wei bound, but not maximum. +/// +/// Rust: `graph::coloring::independent_set_greedy` +#[pyfunction] +#[pyo3(name = "independent_set_greedy", signature = (g))] +pub fn pyfn_independent_set_greedy<'py>(py: Python<'py>, g: crate::generated::types::PyGraph) -> PyResult> { + let g = g.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::coloring::independent_set_greedy(&g))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// A maximum independent set, exactly, via the complement. +/// +/// An independent set in `G` is a clique in the complement of `G` and the +/// other way round, so this is the clique enumeration with the edges flipped. +/// +/// Panics: +/// Panics above 64 vertices. +/// +/// Rust: `graph::coloring::max_independent_set_small` +#[pyfunction] +#[pyo3(name = "max_independent_set_small", signature = (g))] +pub fn pyfn_max_independent_set_small<'py>(py: Python<'py>, g: crate::generated::types::PyGraph) -> PyResult> { + let g = g.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::coloring::max_independent_set_small(&g))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// A vertex cover within a factor of two of the smallest, by taking both ends +/// of a maximal matching. +/// +/// The matching's edges are disjoint, so any cover must contain at least one +/// end of each, giving `opt >= |M|`; taking both ends gives `2|M| <= 2 opt`. +/// The bound comes free with the construction and holds on every graph, which +/// is more than the best known algorithm can say about doing better. +/// +/// Rust: `graph::coloring::vertex_cover_2approx` +#[pyfunction] +#[pyo3(name = "vertex_cover_2approx", signature = (g))] +pub fn pyfn_vertex_cover_2approx<'py>(py: Python<'py>, g: crate::generated::types::PyGraph) -> PyResult> { + let g = g.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::coloring::vertex_cover_2approx(&g))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// A minimum vertex cover, exactly, as the complement of a maximum +/// independent set. +/// +/// Gallai's identity: a set covers every edge exactly when its complement +/// spans none, so the two problems are the same problem read twice, and +/// `tau + alpha = n`. +/// +/// Panics: +/// Panics above 64 vertices, or on a self-loop, whose vertex every cover must +/// contain and which the complement identity does not account for. +/// +/// Rust: `graph::coloring::vertex_cover_exact_small` +#[pyfunction] +#[pyo3(name = "vertex_cover_exact_small", signature = (g))] +pub fn pyfn_vertex_cover_exact_small<'py>(py: Python<'py>, g: crate::generated::types::PyGraph) -> PyResult> { + let g = g.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::coloring::vertex_cover_exact_small(&g))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// A dominating set, greedily: every vertex is in it or next to it. +/// +/// Set cover in disguise, with each vertex offering its closed neighbourhood, +/// so the greedy choice of whichever vertex newly dominates the most inherits +/// set cover's `ln(n) + 1` guarantee -- and its hardness, since matching that +/// factor in polynomial time would collapse the same complexity assumption. +/// +/// Rust: `graph::coloring::dominating_set_greedy` +#[pyfunction] +#[pyo3(name = "dominating_set_greedy", signature = (g))] +pub fn pyfn_dominating_set_greedy<'py>(py: Python<'py>, g: crate::generated::types::PyGraph) -> PyResult> { + let g = g.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::coloring::dominating_set_greedy(&g))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// A feedback arc set: arcs whose removal leaves a directed acyclic graph. +/// +/// By the Eades-Lin-Smyth ordering. It builds a linear order by repeatedly +/// taking sinks from the right, sources from the left, and otherwise the +/// vertex whose out-degree most exceeds its in-degree; the arcs pointing +/// backwards in that order are the answer. Removing them must leave a DAG, +/// since a linear order that every remaining arc respects is a topological +/// order. The count is within `m/2 - n/6` of the total, which is the bound +/// the heuristic is quoted for. +/// +/// Self-loops are always returned: no ordering can place a vertex before +/// itself. +/// +/// Panics: +/// Panics if the graph is undirected. +/// +/// Rust: `graph::coloring::feedback_arc_set_greedy` +#[pyfunction] +#[pyo3(name = "feedback_arc_set_greedy", signature = (g))] +pub fn pyfn_feedback_arc_set_greedy<'py>(py: Python<'py>, g: crate::generated::types::PyGraph) -> PyResult> { + let g = g.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::coloring::feedback_arc_set_greedy(&g))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_greedy_coloring, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_color_count, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_is_proper_coloring, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_welsh_powell, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_welsh_powell_bound, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_chromatic_number_exact_small, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_chromatic_polynomial_small, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_edge_coloring_vizing, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_is_proper_edge_coloring, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_interval_graph_coloring, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_map_coloring_backtrack, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_all_maximal_cliques, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_max_clique_bron_kerbosch, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_independent_set_greedy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_max_independent_set_small, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_vertex_cover_2approx, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_vertex_cover_exact_small, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dominating_set_greedy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_feedback_arc_set_greedy, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_graph__core.rs b/bindings/python/src/generated/m_graph__core.rs new file mode 100644 index 0000000..f7b7e25 --- /dev/null +++ b/bindings/python/src/generated/m_graph__core.rs @@ -0,0 +1,430 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// `K_n`: every pair joined. +/// +/// Rust: `graph::core::complete_graph` +#[pyfunction] +#[pyo3(name = "complete_graph", signature = (n))] +pub fn pyfn_complete_graph(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::graph::core::complete_graph(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGraph { inner: __v }) +} + +/// `C_n`: a single cycle. Needs at least three vertices to be a simple cycle. +/// +/// Panics: +/// Panics if `n` is below three. +/// +/// Rust: `graph::core::cycle_graph` +#[pyfunction] +#[pyo3(name = "cycle_graph", signature = (n))] +pub fn pyfn_cycle_graph(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::graph::core::cycle_graph(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGraph { inner: __v }) +} + +/// `P_n`: a single path. +/// +/// Rust: `graph::core::path_graph` +#[pyfunction] +#[pyo3(name = "path_graph", signature = (n))] +pub fn pyfn_path_graph(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::graph::core::path_graph(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGraph { inner: __v }) +} + +/// A star with `n` vertices: vertex 0 joined to every other. +/// +/// Rust: `graph::core::star_graph` +#[pyfunction] +#[pyo3(name = "star_graph", signature = (n))] +pub fn pyfn_star_graph(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::graph::core::star_graph(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGraph { inner: __v }) +} + +/// A wheel with `n` vertices: a hub at 0 joined to a cycle on the rest. +/// +/// Panics: +/// Panics if `n` is below four. +/// +/// Rust: `graph::core::wheel_graph` +#[pyfunction] +#[pyo3(name = "wheel_graph", signature = (n))] +pub fn pyfn_wheel_graph(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::graph::core::wheel_graph(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGraph { inner: __v }) +} + +/// A `w` by `h` grid, with vertex `(x, y)` at index `y * w + x`. +/// +/// Rust: `graph::core::grid_2d` +#[pyfunction] +#[pyo3(name = "grid_2d", signature = (w, h))] +pub fn pyfn_grid_2d(w: usize, h: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::graph::core::grid_2d(w, h)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGraph { inner: __v }) +} + +/// The `d`-dimensional hypercube: `2^d` vertices, joined when their labels +/// differ in one bit. +/// +/// Panics: +/// Panics if `d` exceeds 20. +/// +/// Rust: `graph::core::hypercube_graph` +#[pyfunction] +#[pyo3(name = "hypercube_graph", signature = (d))] +pub fn pyfn_hypercube_graph(d: u32) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::graph::core::hypercube_graph(d)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGraph { inner: __v }) +} + +/// The Petersen graph: the Kneser graph on the 2-subsets of a 5-set, joined +/// when disjoint. Three-regular, girth five, ten vertices. +/// +/// Rust: `graph::core::petersen_graph` +#[pyfunction] +#[pyo3(name = "petersen_graph", signature = ())] +pub fn pyfn_petersen_graph() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::graph::core::petersen_graph()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGraph { inner: __v }) +} + +/// `K_{m,n}`: the vertices `0..m` each joined to every vertex in `m..m+n`. +/// +/// Rust: `graph::core::complete_bipartite` +#[pyfunction] +#[pyo3(name = "complete_bipartite", signature = (m, n))] +pub fn pyfn_complete_bipartite(m: usize, n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::graph::core::complete_bipartite(m, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGraph { inner: __v }) +} + +/// The Erdos-Renyi model `G(n, p)`: each of the `C(n, 2)` pairs is an edge +/// independently with probability `p`. +/// +/// Panics: +/// Panics unless `p` is in `[0, 1]`. +/// +/// Rust: `graph::core::erdos_renyi` +#[pyfunction] +#[pyo3(name = "erdos_renyi", signature = (n, p, rng))] +pub fn pyfn_erdos_renyi(n: usize, p: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::core::erdos_renyi(n, p, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGraph { inner: __v }) +} + +/// The Barabasi-Albert preferential attachment model. +/// +/// Starts from a complete graph on `m` vertices and adds the rest one at a +/// time, each joining `m` distinct existing vertices chosen with probability +/// proportional to their degree. That is done by sampling from the list of +/// arc endpoints, in which a vertex appears once per incident edge, which is +/// exactly the degree distribution. +/// +/// Panics: +/// Panics unless `1 <= m < n`. +/// +/// Rust: `graph::core::barabasi_albert` +#[pyfunction] +#[pyo3(name = "barabasi_albert", signature = (n, m, rng))] +pub fn pyfn_barabasi_albert(n: usize, m: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::core::barabasi_albert(n, m, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGraph { inner: __v }) +} + +/// The Watts-Strogatz small-world model. +/// +/// Starts from a ring in which each vertex joins its `k / 2` nearest +/// neighbours on each side, then rewires each edge with probability `beta` to +/// a uniformly chosen vertex, refusing self-loops and duplicates. The result +/// keeps the ring's clustering while acquiring a short diameter. +/// +/// Panics: +/// Panics unless `k` is even and `2 <= k < n`, or if `beta` is outside +/// `[0, 1]`. +/// +/// Rust: `graph::core::watts_strogatz` +#[pyfunction] +#[pyo3(name = "watts_strogatz", signature = (n, k, beta, rng))] +pub fn pyfn_watts_strogatz(n: usize, k: usize, beta: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::core::watts_strogatz(n, k, beta, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGraph { inner: __v }) +} + +/// A random `d`-regular graph by the pairing (configuration) model with +/// rejection. +/// +/// Gives each vertex `d` half-edges, matches them uniformly at random, and +/// retries the whole draw if the matching produces a self-loop or a repeat. +/// That rejection is what makes the result uniform over simple `d`-regular +/// graphs rather than merely `d`-regular on average. +/// +/// Returns `None` if `n * d` is odd, when no such graph exists, or if the +/// rejection loop gives up. +/// +/// Panics: +/// Panics unless `d < n`. +/// +/// Rust: `graph::core::random_regular` +#[pyfunction] +#[pyo3(name = "random_regular", signature = (n, d, rng))] +pub fn pyfn_random_regular(n: usize, d: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::core::random_regular(n, d, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyGraph { inner: __x })) +} + +/// A random geometric graph: `n` points uniform in the unit square, joined +/// when within `radius`. +/// +/// Returns the graph and the positions, since the positions are what make the +/// model meaningful and are otherwise unrecoverable. +/// +/// Panics: +/// Panics if `radius` is negative. +/// +/// Rust: `graph::core::random_geometric` +#[pyfunction] +#[pyo3(name = "random_geometric", signature = (n, radius, rng))] +pub fn pyfn_random_geometric(n: usize, radius: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(crate::generated::types::PyGraph, Vec<(f64, f64)>)> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::core::random_geometric(n, radius, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((crate::generated::types::PyGraph { inner: __v.0 }, __v.1.into_iter().map(|__x| (__x.0, __x.1)).collect::>())) +} + +/// The stochastic block model: vertices split into blocks of the given sizes, +/// with an edge between blocks `i` and `j` drawn with probability +/// `p_matrix[i][j]`. +/// +/// Panics: +/// Panics if `p_matrix` is not square with one row per block, or if any entry +/// is outside `[0, 1]`. +/// +/// Rust: `graph::core::stochastic_block_model` +#[pyfunction] +#[pyo3(name = "stochastic_block_model", signature = (sizes, p_matrix, rng))] +pub fn pyfn_stochastic_block_model(sizes: Vec, p_matrix: Vec>, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::core::stochastic_block_model(&sizes, &p_matrix, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGraph { inner: __v }) +} + +/// The edge graph of a triangle mesh: one vertex per mesh vertex, joined when +/// they share a triangle edge. Weights are the edge lengths. +/// +/// Rust: `graph::core::graph_from_mesh` +#[pyfunction] +#[pyo3(name = "graph_from_mesh", signature = (mesh))] +pub fn pyfn_graph_from_mesh(mesh: crate::generated::types::PyMeshMesh) -> PyResult { + let mesh = mesh.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::core::graph_from_mesh(&mesh)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGraph { inner: __v }) +} + +/// The line graph: one vertex per edge of `g`, joined when the edges share an +/// endpoint. Returns the graph and the edge each vertex came from. +/// +/// Panics: +/// Panics if `g` is directed, for which the construction differs. +/// +/// Rust: `graph::core::line_graph` +#[pyfunction] +#[pyo3(name = "line_graph", signature = (g))] +pub fn pyfn_line_graph(g: crate::generated::types::PyGraph) -> PyResult<(crate::generated::types::PyGraph, Vec<(usize, usize)>)> { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::core::line_graph(&g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((crate::generated::types::PyGraph { inner: __v.0 }, __v.1.into_iter().map(|__x| (__x.0, __x.1)).collect::>())) +} + +/// The Cartesian product `g x h`: vertex `(u, x)` at index `u * h.n + x`, with +/// an edge when one coordinate is equal and the other adjacent. +/// +/// Rust: `graph::core::cartesian_product` +#[pyfunction] +#[pyo3(name = "cartesian_product", signature = (g, h))] +pub fn pyfn_cartesian_product(g: crate::generated::types::PyGraph, h: crate::generated::types::PyGraph) -> PyResult { + let g = g.inner; + let h = h.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::core::cartesian_product(&g, &h)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGraph { inner: __v }) +} + +/// The tensor (categorical) product: vertex `(u, x)` adjacent to `(v, y)` when +/// `u ~ v` and `x ~ y`. +/// +/// Rust: `graph::core::tensor_product` +#[pyfunction] +#[pyo3(name = "tensor_product", signature = (g, h))] +pub fn pyfn_tensor_product(g: crate::generated::types::PyGraph, h: crate::generated::types::PyGraph) -> PyResult { + let g = g.inner; + let h = h.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::core::tensor_product(&g, &h)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGraph { inner: __v }) +} + +/// A canonical form: the lexicographically least adjacency bitmask sequence +/// over all vertex relabellings. +/// +/// Two graphs are isomorphic exactly when their canonical forms agree, so this +/// is a complete invariant rather than a heuristic one. It searches all `n!` +/// relabellings with pruning by the sorted degree sequence, so it is only +/// affordable for small graphs. +/// +/// Panics: +/// Panics if `g` has more than 10 vertices. +/// +/// Rust: `graph::core::canonical_form_small` +#[pyfunction] +#[pyo3(name = "canonical_form_small", signature = (g))] +pub fn pyfn_canonical_form_small<'py>(py: Python<'py>, g: crate::generated::types::PyGraph) -> PyResult> { + let g = g.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::core::canonical_form_small(&g))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// True when `g` and `h` are isomorphic. +/// +/// Screens on the cheap invariants first -- vertex count, edge count, sorted +/// degree sequence, sorted triangle counts -- and only then compares canonical +/// forms. +/// +/// Panics: +/// Panics if either graph has more than 10 vertices. +/// +/// Rust: `graph::core::is_isomorphic_small` +#[pyfunction] +#[pyo3(name = "is_isomorphic_small", signature = (g, h))] +pub fn pyfn_is_isomorphic_small(g: crate::generated::types::PyGraph, h: crate::generated::types::PyGraph) -> PyResult { + let g = g.inner; + let h = h.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::core::is_isomorphic_small(&g, &h)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Encodes an undirected simple graph in the graph6 format. +/// +/// The format writes the vertex count, then the strict upper triangle of the +/// adjacency matrix read column by column, packed six bits per character with +/// 63 added so every byte is printable ASCII. +/// +/// Panics: +/// Panics if the graph is directed, or has more than 62 vertices, which is +/// where the format's single-character length prefix ends. +/// +/// Rust: `graph::core::graph6_encode` +#[pyfunction] +#[pyo3(name = "graph6_encode", signature = (g))] +pub fn pyfn_graph6_encode(g: crate::generated::types::PyGraph) -> PyResult { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::core::graph6_encode(&g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_string()) +} + +/// Decodes a graph6 string produced by `graph6_encode`. +/// +/// Panics: +/// Panics if the string is empty, contains a byte outside the printable range +/// the format uses, or is too short for the vertex count it declares. +/// +/// Rust: `graph::core::graph6_decode` +#[pyfunction] +#[pyo3(name = "graph6_decode", signature = (s))] +pub fn pyfn_graph6_decode(s: String) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::graph::core::graph6_decode(&s)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGraph { inner: __v }) +} + +/// The number of spanning trees, exactly, by the matrix-tree theorem. +/// +/// Kirchhoff's theorem says this is any cofactor of the Laplacian; the +/// determinant is taken over the integers by Bareiss fraction-free +/// elimination, so the answer is exact rather than a rounded float. +/// +/// Parallel edges count as distinct; weights are ignored. +/// +/// Panics: +/// Panics if the graph is directed. +/// +/// Rust: `graph::core::spanning_tree_count_exact` +#[pyfunction] +#[pyo3(name = "spanning_tree_count_exact", signature = (g))] +pub fn pyfn_spanning_tree_count_exact<'py>(py: Python<'py>, g: crate::generated::types::PyGraph) -> PyResult> { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::core::spanning_tree_count_exact(&g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_complete_graph, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cycle_graph, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_path_graph, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_star_graph, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wheel_graph, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_grid_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hypercube_graph, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_petersen_graph, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_complete_bipartite, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_erdos_renyi, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_barabasi_albert, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_watts_strogatz, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_random_regular, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_random_geometric, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_stochastic_block_model, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_graph_from_mesh, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_line_graph, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cartesian_product, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tensor_product, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_canonical_form_small, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_is_isomorphic_small, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_graph6_encode, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_graph6_decode, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spanning_tree_count_exact, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_graph__flow.rs b/bindings/python/src/generated/m_graph__flow.rs new file mode 100644 index 0000000..b7c5f6e --- /dev/null +++ b/bindings/python/src/generated/m_graph__flow.rs @@ -0,0 +1,335 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// The maximum flow from `s` to `t` by Dinic's algorithm, and the flow on each +/// arc as a matrix. +/// +/// Dinic repeatedly builds a level graph by breadth-first search and pushes +/// blocking flow through it, which bounds the number of phases by the vertex +/// count rather than by the flow value -- the difference between terminating +/// and not on a network with large capacities. +/// +/// The returned matrix holds the net flow: entry `(u, v)` is what crosses from +/// `u` to `v`, and is zero where nothing does. +/// +/// Panics: +/// Panics if `s` or `t` is out of range, if they are equal, or if any capacity +/// is negative or not finite. +/// +/// Rust: `graph::flow::max_flow_dinic` +#[pyfunction] +#[pyo3(name = "max_flow_dinic", signature = (g, s, t))] +pub fn pyfn_max_flow_dinic(g: crate::generated::types::PyGraph, s: usize, t: usize) -> PyResult<(f64, Vec>)> { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::flow::max_flow_dinic(&g, s, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// The maximum flow value by the push-relabel method with the highest-label +/// rule. +/// +/// A different algorithm from `max_flow_dinic` rather than a variation on +/// it: push-relabel never maintains a valid flow until it finishes, working +/// instead with a preflow that it gradually returns to feasibility. The two +/// agreeing is therefore evidence, not a tautology. +/// +/// Panics: +/// Panics under the same conditions as `max_flow_dinic`. +/// +/// Rust: `graph::flow::max_flow_push_relabel` +#[pyfunction] +#[pyo3(name = "max_flow_push_relabel", signature = (g, s, t))] +pub fn pyfn_max_flow_push_relabel(g: crate::generated::types::PyGraph, s: usize, t: usize) -> PyResult { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::flow::max_flow_push_relabel(&g, s, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The minimum `s`-`t` cut: its capacity and the source side. +/// +/// By the max-flow min-cut theorem the capacity equals the maximum flow, and +/// the source side is exactly what remains reachable from `s` in the residual +/// network once the flow is maximum. +/// +/// Panics: +/// Panics under the same conditions as `max_flow_dinic`. +/// +/// Rust: `graph::flow::min_cut` +#[pyfunction] +#[pyo3(name = "min_cut", signature = (g, s, t))] +pub fn pyfn_min_cut(g: crate::generated::types::PyGraph, s: usize, t: usize) -> PyResult<(f64, Vec)> { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::flow::min_cut(&g, s, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// The global minimum cut, by the Stoer-Wagner algorithm. +/// +/// Finds the cheapest way to split the graph in two without naming the two +/// sides, which no single `s`-`t` computation does. Each phase grows a set by +/// always adding the most tightly connected vertex, which makes the last two +/// added a valid `s`-`t` pair for free; merging them and repeating gives the +/// global optimum in `n - 1` phases. +/// +/// Returns the cut capacity and one side of it. +/// +/// Panics: +/// Panics if the graph is directed, or has fewer than two vertices. +/// +/// Rust: `graph::flow::global_min_cut_stoer_wagner` +#[pyfunction] +#[pyo3(name = "global_min_cut_stoer_wagner", signature = (g))] +pub fn pyfn_global_min_cut_stoer_wagner(g: crate::generated::types::PyGraph) -> PyResult<(f64, Vec)> { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::flow::global_min_cut_stoer_wagner(&g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// The minimum-cost maximum flow from `s` to `t`. +/// +/// `costs` gives the cost per unit on each arc, in the same order as +/// `g.edges()`. Returns the flow value and its total cost. +/// +/// Augments along a shortest path by cost each round, found with Bellman-Ford +/// so negative costs are allowed. Sending flow along a shortest path keeps the +/// residual network free of negative cycles, which is what makes the greedy +/// choice optimal rather than merely feasible. +/// +/// Panics: +/// Panics if `costs` does not have one entry per edge, or under the same +/// conditions as `max_flow_dinic`. +/// +/// Rust: `graph::flow::min_cost_max_flow` +#[pyfunction] +#[pyo3(name = "min_cost_max_flow", signature = (g, costs, s, t))] +pub fn pyfn_min_cost_max_flow<'py>(py: Python<'py>, g: crate::generated::types::PyGraph, costs: Vec, s: usize, t: usize) -> PyResult<(f64, f64)> { + let g = g.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::flow::min_cost_max_flow(&g, &costs, s, t))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// A feasible circulation meeting the given vertex demands, or `None` if none +/// exists. +/// +/// `demand[v]` is positive when `v` must receive that much and negative when +/// it must send it. `lower` gives the minimum flow on each edge, in the order +/// of `g.edges()`. Solved by the standard reduction: subtract the lower bounds +/// into the demands, then look for a saturating flow from a super-source to a +/// super-sink. +/// +/// Returns the flow on each edge in the order of `g.edges()`. +/// +/// Panics: +/// Panics unless `demand` has one entry per vertex, `lower` one per edge, the +/// demands sum to zero, and every lower bound is within its capacity. +/// +/// Rust: `graph::flow::circulation_with_demands` +#[pyfunction] +#[pyo3(name = "circulation_with_demands", signature = (g, demand, lower))] +pub fn pyfn_circulation_with_demands<'py>(py: Python<'py>, g: crate::generated::types::PyGraph, demand: Vec, lower: Vec) -> PyResult>> { + let g = g.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::flow::circulation_with_demands(&g, &demand, &lower))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// A maximum matching of a bipartite graph, found by maximum flow. +/// +/// `left` names the vertices on one side; the rest are the other side. Returns +/// the partner of each vertex, or `None` for the unmatched. +/// +/// Slower than `graph::matching::hopcroft_karp` but built from a +/// different primitive, so the two agreeing is evidence about both. +/// +/// Panics: +/// Panics if `left` names a vertex twice or out of range, or if an edge joins +/// two vertices on the same side. +/// +/// Rust: `graph::flow::max_bipartite_matching_via_flow` +#[pyfunction] +#[pyo3(name = "max_bipartite_matching_via_flow", signature = (g, left))] +pub fn pyfn_max_bipartite_matching_via_flow<'py>(py: Python<'py>, g: crate::generated::types::PyGraph, left: Vec) -> PyResult>> { + let g = g.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::flow::max_bipartite_matching_via_flow(&g, &left))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.map(|__x| __x)).collect::>()) +} + +/// The number of pairwise edge-disjoint paths from `s` to `t`. +/// +/// Menger's theorem says this equals the minimum number of edges whose removal +/// separates them, which is the maximum flow with every capacity one. +/// +/// Panics: +/// Panics if `s` or `t` is out of range, or they are equal. +/// +/// Rust: `graph::flow::edge_disjoint_paths` +#[pyfunction] +#[pyo3(name = "edge_disjoint_paths", signature = (g, s, t))] +pub fn pyfn_edge_disjoint_paths(g: crate::generated::types::PyGraph, s: usize, t: usize) -> PyResult { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::flow::edge_disjoint_paths(&g, s, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The number of pairwise internally vertex-disjoint paths from `s` to `t`. +/// +/// The vertex form of Menger's theorem. Each vertex other than `s` and `t` is +/// split into an in-copy and an out-copy joined by a unit arc, which caps how +/// many paths can use it; the answer is then the edge-disjoint count on the +/// split graph. +/// +/// Panics: +/// Panics if `s` or `t` is out of range, or they are equal. +/// +/// Rust: `graph::flow::vertex_disjoint_paths` +#[pyfunction] +#[pyo3(name = "vertex_disjoint_paths", signature = (g, s, t))] +pub fn pyfn_vertex_disjoint_paths(g: crate::generated::types::PyGraph, s: usize, t: usize) -> PyResult { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::flow::vertex_disjoint_paths(&g, s, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// A Gomory-Hu tree: an `n`-vertex tree in which the minimum cut between any +/// two vertices equals the lightest edge on the tree path between them. +/// +/// Built by Gusfield's simplification, which needs only `n - 1` maximum-flow +/// computations and no vertex contraction. The result encodes all `C(n, 2)` +/// pairwise minimum cuts in `n - 1` numbers. +/// +/// Panics: +/// Panics if the graph is directed. +/// +/// Rust: `graph::flow::gomory_hu_tree` +#[pyfunction] +#[pyo3(name = "gomory_hu_tree", signature = (g))] +pub fn pyfn_gomory_hu_tree(g: crate::generated::types::PyGraph) -> PyResult { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::flow::gomory_hu_tree(&g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGraph { inner: __v }) +} + +/// The maximum-weight closed subset of a directed graph. +/// +/// A closure is a vertex set containing every successor of every member. The +/// maximum-weight closure reduces to a minimum cut: positive vertices are +/// joined to a source with their weight, negative ones to a sink with its +/// magnitude, and each original arc is given infinite capacity so no cut can +/// break it, which is exactly the closure condition. +/// +/// Returns the weight and the membership flags. +/// +/// Panics: +/// Panics unless `weights` has one entry per vertex. +/// +/// Rust: `graph::flow::closure_problem` +#[pyfunction] +#[pyo3(name = "closure_problem", signature = (g, weights))] +pub fn pyfn_closure_problem<'py>(py: Python<'py>, g: crate::generated::types::PyGraph, weights: Vec) -> PyResult<(f64, Vec)> { + let g = g.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::flow::closure_problem(&g, &weights))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// The maximum profit of a project selection problem. +/// +/// Projects have revenues and require machines that cost money; a project may +/// only be taken if every machine it needs is bought. `project_revenue[i]` is +/// the revenue of project `i`, `machine_cost[j]` the cost of machine `j`, and +/// `requires[i]` the machines project `i` needs. +/// +/// This is `closure_problem` on the bipartite graph of projects and +/// machines, with revenues positive and costs negative. +/// +/// Panics: +/// Panics if `requires` does not have one entry per project, or names a +/// machine out of range. +/// +/// Rust: `graph::flow::project_selection` +#[pyfunction] +#[pyo3(name = "project_selection", signature = (project_revenue, machine_cost, requires))] +pub fn pyfn_project_selection<'py>(py: Python<'py>, project_revenue: Vec, machine_cost: Vec, requires: Vec>) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::flow::project_selection(&project_revenue, &machine_cost, &requires))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The maximum flow value as a plain number, for callers that do not want the +/// arc-by-arc matrix. +/// +/// Panics: +/// Panics under the same conditions as `max_flow_dinic`. +/// +/// Rust: `graph::flow::max_flow` +#[pyfunction] +#[pyo3(name = "max_flow", signature = (g, s, t))] +pub fn pyfn_max_flow(g: crate::generated::types::PyGraph, s: usize, t: usize) -> PyResult { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::flow::max_flow(&g, s, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The capacity of the cut defined by `side`: the total weight of the edges +/// leaving the `true` set. +/// +/// A directed graph counts only arcs from the `true` side to the `false` one, +/// which is the `s`-`t` cut convention; an undirected graph counts every edge +/// crossing. +/// +/// Panics: +/// Panics unless `side` has one flag per vertex. +/// +/// Rust: `graph::flow::cut_capacity` +#[pyfunction] +#[pyo3(name = "cut_capacity", signature = (g, side))] +pub fn pyfn_cut_capacity<'py>(py: Python<'py>, g: crate::generated::types::PyGraph, side: Vec) -> PyResult { + let g = g.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::flow::cut_capacity(&g, &side))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_max_flow_dinic, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_max_flow_push_relabel, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_min_cut, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_global_min_cut_stoer_wagner, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_min_cost_max_flow, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_circulation_with_demands, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_max_bipartite_matching_via_flow, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_edge_disjoint_paths, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_vertex_disjoint_paths, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gomory_hu_tree, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_closure_problem, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_project_selection, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_max_flow, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cut_capacity, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_graph__layout.rs b/bindings/python/src/generated/m_graph__layout.rs new file mode 100644 index 0000000..9ea5e1b --- /dev/null +++ b/bindings/python/src/generated/m_graph__layout.rs @@ -0,0 +1,388 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Hop distances between every pair, by breadth-first search from each +/// vertex. +/// +/// Edge weights are deliberately ignored: a drawing is laid out by graph +/// structure, and a weight of a thousand on one edge should not stretch the +/// picture by a factor of a thousand. Pairs in different components are given +/// one more than the largest finite distance, which is the usual convention +/// and keeps the stress function finite. +/// +/// Rust: `graph::layout::hop_distances` +#[pyfunction] +#[pyo3(name = "hop_distances", signature = (g))] +pub fn pyfn_hop_distances<'py>(py: Python<'py>, g: crate::generated::types::PyGraph) -> PyResult>> { + let g = g.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::layout::hop_distances(&g))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The stress of a two-dimensional drawing: the weighted squared mismatch +/// between drawn and graph distance, `sum_{i(py: Python<'py>, g: crate::generated::types::PyGraph, positions: Vec) -> PyResult { + let g = g.inner; + let positions = positions.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::layout::layout_stress(&g, &positions))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The same stress functional for a drawing in any number of dimensions. +/// +/// Panics: +/// Panics unless there is one position per vertex. +/// +/// Rust: `graph::layout::stress_nd` +#[pyfunction] +#[pyo3(name = "stress_nd", signature = (g, positions))] +pub fn pyfn_stress_nd<'py>(py: Python<'py>, g: crate::generated::types::PyGraph, positions: Vec) -> PyResult { + let g = g.inner; + let positions = positions.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::layout::stress_nd(&g, &positions))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// `n` points equally spaced around the unit circle, starting at `(1, 0)` and +/// going anticlockwise. +/// +/// The one layout with no free parameters and nothing to converge. Every +/// vertex is visible and no two coincide, which is why it is the standard +/// starting point for the iterative layouts here. +/// +/// Rust: `graph::layout::circular_layout` +#[pyfunction] +#[pyo3(name = "circular_layout", signature = (n))] +pub fn pyfn_circular_layout(n: usize) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::graph::layout::circular_layout(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// Concentric circles, one per shell, in the order given. +/// +/// A shell holding a single vertex is drawn at the centre; every other shell +/// `k` goes on the circle of radius `k + 1`, its members equally spaced. +/// Useful when the grouping is already known -- levels of a hierarchy, orbits +/// of a symmetry, distance classes from a root. +/// +/// Panics: +/// Panics unless the shells partition `0..g.n`. +/// +/// Rust: `graph::layout::shell_layout` +#[pyfunction] +#[pyo3(name = "shell_layout", signature = (g, shells))] +pub fn pyfn_shell_layout(g: crate::generated::types::PyGraph, shells: Vec>) -> PyResult> { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::layout::shell_layout(&g, &shells)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// Spectral layout: the two Laplacian eigenvectors just above the constant +/// one, used as coordinates. +/// +/// The constant vector is the Laplacian's zero eigenvector and carries no +/// information, so the drawing starts at the next two. Those minimise +/// `sum_edges |p_u - p_v|^2` subject to being centred and orthonormal, which +/// is to say they are the drawing that makes edges as short as possible +/// without collapsing everything to a point. Coordinates come out on the +/// scale of a unit vector; scale them for display. +/// +/// Panics: +/// Panics if the graph is directed, or has fewer than three vertices. +/// +/// Rust: `graph::layout::spectral_layout` +#[pyfunction] +#[pyo3(name = "spectral_layout", signature = (g))] +pub fn pyfn_spectral_layout(g: crate::generated::types::PyGraph) -> PyResult> { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::layout::spectral_layout(&g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// Kamada-Kawai layout: move one vertex at a time to the position that best +/// matches its graph distances to everything else. +/// +/// The energy is `layout_stress`. Each round picks the vertex whose +/// gradient is largest and takes a Newton step on its two coordinates, which +/// converges quadratically near the solution. The step is accepted only if +/// the energy actually falls, so the sequence of drawings is monotone: the +/// result is never worse than the circular layout it starts from. Newton on a +/// non-convex energy will otherwise happily step uphill. +/// +/// Panics: +/// Panics if the graph is directed. +/// +/// Rust: `graph::layout::kamada_kawai` +#[pyfunction] +#[pyo3(name = "kamada_kawai", signature = (g, iters))] +pub fn pyfn_kamada_kawai(g: crate::generated::types::PyGraph, iters: usize) -> PyResult> { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::layout::kamada_kawai(&g, iters)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// Stress majorization (SMACOF) in `dim` dimensions. +/// +/// Each round replaces the stress by a quadratic that touches it at the +/// current drawing and lies above it everywhere else, then jumps to that +/// quadratic's minimum. Because the surrogate is an upper bound, the true +/// stress cannot rise -- which is the whole point, and the reason this is +/// preferred to gradient descent on the same objective: there is no step size +/// to tune and no way to overshoot. +/// +/// The starting drawing is classical scaling, the closed-form embedding that +/// best reproduces the *squared* distances. Majorization only ever descends, +/// so where it starts decides which local minimum it reaches; starting from +/// the classical solution makes the result deterministic and already close. +/// +/// Panics: +/// Panics if the graph is directed, or `dim` is zero. +/// +/// Rust: `graph::layout::stress_majorization` +#[pyfunction] +#[pyo3(name = "stress_majorization", signature = (g, dim, iters))] +pub fn pyfn_stress_majorization(g: crate::generated::types::PyGraph, dim: usize, iters: usize) -> PyResult> { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::layout::stress_majorization(&g, dim, iters)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVecN { inner: __x }).collect::>()) +} + +/// Fruchterman-Reingold: vertices repel like charges, edges pull like +/// springs, and the whole thing cools. +/// +/// Repulsion is `k^2 / r` between every pair and attraction is `r^2 / k` +/// along every edge, for the ideal separation `k = sqrt(area / n)`. The two +/// balance at `r = k`, which is what sets the scale of the drawing. The +/// temperature caps how far any vertex may move in one round and falls +/// linearly to zero, so the layout freezes rather than oscillating -- the +/// method is a heuristic with no monotonicity guarantee, and the cooling is +/// what stands in for one. +/// +/// Panics: +/// Panics if the graph is directed. +/// +/// Rust: `graph::layout::fruchterman_reingold` +#[pyfunction] +#[pyo3(name = "fruchterman_reingold", signature = (g, iters, rng))] +pub fn pyfn_fruchterman_reingold(g: crate::generated::types::PyGraph, iters: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let g = g.inner; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::layout::fruchterman_reingold(&g, iters, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// Reingold-Tilford tree layout. +/// +/// Depth sets the vertical position and the horizontal one is chosen so that +/// three things hold at once: no two subtrees overlap, every parent sits at +/// the midpoint of its first and last child, and the drawing is as narrow as +/// those two allow. The third is what the algorithm is for -- centring a +/// parent over its children is easy, and doing it while packing sibling +/// subtrees as tightly as their outlines permit is not. +/// +/// Packing works on *contours*: the leftmost and rightmost position each +/// subtree occupies at every depth. Two siblings are pushed apart by the +/// largest overlap between the right contour of everything placed so far and +/// the left contour of the newcomer, so subtrees interlock where their shapes +/// leave room. +/// +/// The root is at `(0, 0)` and depth `k` at `y = -k`, so the tree hangs +/// downward. Vertices unreachable from the root keep the origin. +/// +/// Panics: +/// Panics if the graph is directed, `root` is out of range, or the graph has +/// a cycle reachable from the root -- the layout is defined on trees. +/// +/// Rust: `graph::layout::tree_layout_reingold_tilford` +#[pyfunction] +#[pyo3(name = "tree_layout_reingold_tilford", signature = (g, root))] +pub fn pyfn_tree_layout_reingold_tilford(g: crate::generated::types::PyGraph, root: usize) -> PyResult> { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::layout::tree_layout_reingold_tilford(&g, root)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// Sugiyama layered drawing of a directed acyclic graph. +/// +/// Layer `k` holds the vertices whose longest incoming path has `k` arcs, so +/// every arc goes from a strictly lower layer to a higher one and the drawing +/// reads in one direction. Within a layer the order is fixed by repeated +/// barycentre sweeps: put each vertex at the average position of its +/// neighbours in the adjacent layer, sort, and repeat, alternating direction. +/// That is Sugiyama's crossing-reduction heuristic; minimising crossings +/// exactly is NP-hard even for two layers. +/// +/// Returns `x` as the position within the layer and `y` as minus the layer, +/// so the arcs point downward. +/// +/// Panics: +/// Panics unless the graph is directed and acyclic. +/// +/// Rust: `graph::layout::sugiyama_layered` +#[pyfunction] +#[pyo3(name = "sugiyama_layered", signature = (dag))] +pub fn pyfn_sugiyama_layered(dag: crate::generated::types::PyGraph) -> PyResult> { + let dag = dag.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::layout::sugiyama_layered(&dag)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// The number of edge crossings in the straight-line drawing given by +/// `layout`. +/// +/// An upper bound on the graph's crossing number, and only that: the crossing +/// number is the minimum over all drawings, and a graph's best drawing need +/// not even be straight-line for a general graph. Edges sharing an endpoint +/// are never counted, and neither is a touching that is not a proper +/// crossing. +/// +/// Panics: +/// Panics unless there is one position per vertex. +/// +/// Rust: `graph::layout::crossing_number_estimate` +#[pyfunction] +#[pyo3(name = "crossing_number_estimate", signature = (g, layout))] +pub fn pyfn_crossing_number_estimate<'py>(py: Python<'py>, g: crate::generated::types::PyGraph, layout: Vec) -> PyResult { + let g = g.inner; + let layout = layout.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::layout::crossing_number_estimate(&g, &layout))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The edge sets of the biconnected components, each a maximal subgraph with +/// no cut vertex. +/// +/// A graph is planar exactly when every block is, which is what makes this +/// the right decomposition to plan a planarity test around: the blocks meet +/// only at single vertices, and a drawing of each can be rotated and scaled +/// into place around those without interfering. +/// +/// Self-loops are dropped and parallel edges collapsed, so each returned +/// block lists distinct simple edges. +/// +/// Panics: +/// Panics if the graph is directed. +/// +/// Rust: `graph::layout::biconnected_components` +#[pyfunction] +#[pyo3(name = "biconnected_components", signature = (g))] +pub fn pyfn_biconnected_components<'py>(py: Python<'py>, g: crate::generated::types::PyGraph) -> PyResult>> { + let g = g.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::layout::biconnected_components(&g))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.into_iter().map(|__x| (__x.0, __x.1)).collect::>()).collect::>()) +} + +/// A planar embedding of a biconnected graph, as its faces: each face is the +/// cyclic sequence of vertices bounding it. `None` if the graph is not +/// planar. +/// +/// By Demoucron's path-addition method. Start with any cycle, which divides +/// the plane into two faces, and grow: the parts of the graph not yet drawn +/// -- its *fragments* -- each attach to the drawn part at a set of vertices, +/// and a fragment can only go inside a face that contains all of them. If +/// some fragment fits nowhere, the graph is not planar. If a fragment fits in +/// exactly one face it is forced, so it is drawn first; otherwise any choice +/// will do, and that is the theorem the method rests on. Drawing a path of a +/// fragment across a face splits that face in two, and the process repeats +/// until every edge is drawn. +/// +/// The outer face is among those returned; which one it is depends on the +/// starting cycle, since on the sphere no face is distinguished. +/// +/// Panics: +/// Panics if the graph is directed, has a self-loop, has fewer than three +/// vertices, or is not biconnected. Use `planarity_test` for a graph that +/// is any of those: it decomposes into blocks first. +/// +/// Rust: `graph::layout::planar_embedding_small` +#[pyfunction] +#[pyo3(name = "planar_embedding_small", signature = (g))] +pub fn pyfn_planar_embedding_small(g: crate::generated::types::PyGraph) -> PyResult>>> { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::layout::planar_embedding_small(&g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// Whether the graph can be drawn in the plane with no edge crossings. +/// +/// Exact, not an estimate. Parallel edges and self-loops are ignored, since +/// neither can make a drawable graph undrawable, and the graph is split into +/// its blocks: planarity holds for the whole exactly when it holds for each, +/// and each block is biconnected, which is what +/// `planar_embedding_small` needs. +/// +/// Panics: +/// Panics if the graph is directed. +/// +/// Rust: `graph::layout::planarity_test` +#[pyfunction] +#[pyo3(name = "planarity_test", signature = (g))] +pub fn pyfn_planarity_test(g: crate::generated::types::PyGraph) -> PyResult { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::layout::planarity_test(&g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_hop_distances, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_layout_stress, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_stress_nd, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_circular_layout, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_shell_layout, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spectral_layout, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kamada_kawai, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_stress_majorization, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fruchterman_reingold, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tree_layout_reingold_tilford, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sugiyama_layered, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_crossing_number_estimate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_biconnected_components, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_planar_embedding_small, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_planarity_test, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_graph__matching.rs b/bindings/python/src/generated/m_graph__matching.rs new file mode 100644 index 0000000..c2b1dac --- /dev/null +++ b/bindings/python/src/generated/m_graph__matching.rs @@ -0,0 +1,252 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// A maximum matching of a bipartite graph, by Hopcroft-Karp. +/// +/// The left side is `0..left_n` and the right side `0..right_n`, numbered +/// separately; `edges` gives `(left, right)` pairs. The returned array is +/// indexed by left vertex and holds the right vertex matched to it. +/// +/// Hopcroft-Karp augments along a maximal set of shortest augmenting paths at +/// once rather than one at a time, which bounds the number of phases by +/// `sqrt(V)` instead of `V`. +/// +/// Panics: +/// Panics if an edge names a vertex outside its side. +/// +/// Rust: `graph::matching::hopcroft_karp` +#[pyfunction] +#[pyo3(name = "hopcroft_karp", signature = (left_n, right_n, edges))] +pub fn pyfn_hopcroft_karp<'py>(py: Python<'py>, left_n: usize, right_n: usize, edges: Vec<(usize, usize)>) -> PyResult>> { + let edges = edges.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::matching::hopcroft_karp(left_n, right_n, &edges))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.map(|__x| __x)).collect::>()) +} + +/// The minimum-cost perfect assignment, by the Hungarian algorithm in its +/// `O(n^3)` shortest-augmenting-path form. +/// +/// `cost` must be square. Returns the total cost and the column assigned to +/// each row. +/// +/// The algorithm maintains dual potentials that keep every reduced cost +/// non-negative, so each augmenting search is a Dijkstra rather than a +/// Bellman-Ford; that is what turns the naive `O(n^4)` into `O(n^3)`. +/// +/// Panics: +/// Panics if `cost` is not square or contains a non-finite entry. +/// +/// Rust: `graph::matching::hungarian` +#[pyfunction] +#[pyo3(name = "hungarian", signature = (cost))] +pub fn pyfn_hungarian<'py>(py: Python<'py>, cost: crate::generated::types::PyMatrixArg) -> PyResult<(f64, Vec)> { + let cost = cost.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::matching::hungarian(&cost))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// The minimum-cost assignment by the auction algorithm. +/// +/// Rows bid for columns, raising each column's price by at least `eps` to win +/// it. The final assignment is within `n * eps` of optimal, so a small `eps` +/// buys accuracy at the cost of more rounds. Scaling `eps` down geometrically +/// -- which this does -- reaches the exact optimum for integer costs and a +/// very good one otherwise. +/// +/// Returns the total cost and the column assigned to each row. +/// +/// Panics: +/// Panics if `cost` is not square, contains a non-finite entry, or `eps` is +/// not positive. +/// +/// Rust: `graph::matching::auction_assignment` +#[pyfunction] +#[pyo3(name = "auction_assignment", signature = (cost, eps))] +pub fn pyfn_auction_assignment<'py>(py: Python<'py>, cost: crate::generated::types::PyMatrixArg, eps: f64) -> PyResult<(f64, Vec)> { + let cost = cost.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::matching::auction_assignment(&cost, eps))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// A maximum matching of a general graph, by Edmonds' blossom algorithm. +/// +/// The bipartite algorithms fail on odd cycles: an augmenting search can enter +/// one and come back out at the same vertex with the wrong parity. Edmonds' +/// insight is to contract each such cycle -- a blossom -- to a single vertex, +/// search the contracted graph, and lift the result back. +/// +/// The lifting is the part that is easy to get wrong. Contracting is not +/// enough: when a blossom forms, the parent pointers of every vertex on the +/// odd cycle have to be rewired so that a later augmenting path can be traced +/// back *through* the blossom the long way round. Without that rewiring the +/// traceback leaves the tree by the wrong edge and produces an asymmetric +/// pairing. `mark_blossom_path` below is what does it. +/// +/// Returns the partner array over all vertices. +/// +/// Panics: +/// Panics if the graph is directed. +/// +/// Rust: `graph::matching::blossom_max_matching` +#[pyfunction] +#[pyo3(name = "blossom_max_matching", signature = (g))] +pub fn pyfn_blossom_max_matching<'py>(py: Python<'py>, g: crate::generated::types::PyGraph) -> PyResult>> { + let g = g.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::matching::blossom_max_matching(&g))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.map(|__x| __x)).collect::>()) +} + +/// A stable marriage by the Gale-Shapley algorithm. +/// +/// `prefs_a[i]` ranks every member of the other side in decreasing preference, +/// and likewise `prefs_b`. Returns, for each member of side A, the member of +/// side B they are matched to. +/// +/// The result is the A-optimal stable matching: every proposer gets the best +/// partner they could have in any stable matching, and every receiver the +/// worst. That asymmetry is a property of the algorithm, not an artefact. +/// +/// Panics: +/// Panics unless both preference lists are complete permutations of the other +/// side, and the two sides are the same size. +/// +/// Rust: `graph::matching::stable_marriage` +#[pyfunction] +#[pyo3(name = "stable_marriage", signature = (prefs_a, prefs_b))] +pub fn pyfn_stable_marriage<'py>(py: Python<'py>, prefs_a: Vec>, prefs_b: Vec>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::matching::stable_marriage(&prefs_a, &prefs_b))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// A stable roommates matching, or `None` when none exists. +/// +/// Unlike stable marriage, this is a single pool with no sides, and a stable +/// matching need not exist at all -- the smallest counterexample has four +/// people. Irving's algorithm: a proposal phase, then repeated elimination of +/// rotations. +/// +/// `prefs[i]` ranks the other `n - 1` people in decreasing preference. +/// +/// Panics: +/// Panics unless `n` is even and each list ranks exactly the other people. +/// +/// Rust: `graph::matching::stable_roommates` +#[pyfunction] +#[pyo3(name = "stable_roommates", signature = (prefs))] +pub fn pyfn_stable_roommates<'py>(py: Python<'py>, prefs: Vec>) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::matching::stable_roommates(&prefs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// A minimum vertex cover of a bipartite graph, by Konig's theorem. +/// +/// Konig's theorem says the minimum vertex cover of a bipartite graph has +/// exactly the size of its maximum matching, and names the cover: start an +/// alternating search from the unmatched left vertices, then take the left +/// vertices *not* reached together with the right vertices that are. +/// +/// `left` names one side; `matching` is a partner array over all vertices. +/// +/// Panics: +/// Panics if `matching` is not symmetric, or `left` names a vertex twice. +/// +/// Rust: `graph::matching::konig_vertex_cover` +#[pyfunction] +#[pyo3(name = "konig_vertex_cover", signature = (g, left, matching))] +pub fn pyfn_konig_vertex_cover<'py>(py: Python<'py>, g: crate::generated::types::PyGraph, left: Vec, matching: Vec>) -> PyResult> { + let g = g.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::matching::konig_vertex_cover(&g, &left, &matching))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Checks Hall's condition on a bipartite graph. +/// +/// Hall's theorem says a matching saturating the left side exists exactly when +/// every subset of the left has at least as many distinct neighbours as it has +/// members. Returns `Ok(())` when it holds, or the smallest violating subset +/// found. +/// +/// The violating set is not searched for over all `2^|L|` subsets: by Konig's +/// theorem the deficiency equals `|L|` minus the maximum matching, and the +/// unreached left vertices of the alternating search form a violating set. +/// +/// Errors: +/// Returns the violating subset of `left` when the condition fails. +/// +/// Rust: `graph::matching::hall_condition_check` +#[pyfunction] +#[pyo3(name = "hall_condition_check", signature = (g, left))] +pub fn pyfn_hall_condition_check<'py>(py: Python<'py>, g: crate::generated::types::PyGraph, left: Vec) -> PyResult<()> { + let g = g.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::matching::hall_condition_check(&g, &left))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::errors::map_debug)?; + Ok(()) +} + +/// The maximum-weight bipartite matching, allowing an unbalanced graph and +/// leaving a vertex unmatched when that pays better. +/// +/// `weights` is a left-by-right matrix. Reduces to the Hungarian algorithm by +/// padding to a square and negating, with the padding entries at zero so an +/// unprofitable match is never forced. +/// +/// Returns the total weight and the partner of each left vertex. +/// +/// Rust: `graph::matching::maximum_weight_bipartite` +#[pyfunction] +#[pyo3(name = "maximum_weight_bipartite", signature = (weights))] +pub fn pyfn_maximum_weight_bipartite<'py>(py: Python<'py>, weights: crate::generated::types::PyMatrixArg) -> PyResult<(f64, Vec>)> { + let weights = weights.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::matching::maximum_weight_bipartite(&weights))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1.into_iter().map(|__x| __x.map(|__x| __x)).collect::>())) +} + +/// The number of edges in a partner array. +/// +/// Rust: `graph::matching::matching_size` +#[pyfunction] +#[pyo3(name = "matching_size", signature = (m))] +pub fn pyfn_matching_size<'py>(py: Python<'py>, m: Vec>) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::matching::matching_size(&m))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_hopcroft_karp, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hungarian, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_auction_assignment, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_blossom_max_matching, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_stable_marriage, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_stable_roommates, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_konig_vertex_cover, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hall_condition_check, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_maximum_weight_bipartite, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_matching_size, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_graph__paths.rs b/bindings/python/src/generated/m_graph__paths.rs new file mode 100644 index 0000000..2e58094 --- /dev/null +++ b/bindings/python/src/generated/m_graph__paths.rs @@ -0,0 +1,526 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Single-source shortest paths with non-negative weights, by Dijkstra. +/// +/// Returns the distances and the predecessor array. Unreached vertices have +/// distance `f64::INFINITY` and no predecessor. +/// +/// Panics: +/// Panics if any weight is negative, where the algorithm is simply wrong +/// rather than merely slow -- use `bellman_ford` instead. +/// +/// Rust: `graph::paths::dijkstra` +#[pyfunction] +#[pyo3(name = "dijkstra", signature = (g, s))] +pub fn pyfn_dijkstra(g: crate::generated::types::PyGraph, s: usize) -> PyResult<(Vec, Vec>)> { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::paths::dijkstra(&g, s)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1.into_iter().map(|__x| __x.map(|__x| __x)).collect::>())) +} + +/// The shortest path from `s` to `t` and its length, or `None` if `t` is +/// unreachable. +/// +/// Rust: `graph::paths::dijkstra_target` +#[pyfunction] +#[pyo3(name = "dijkstra_target", signature = (g, s, t))] +pub fn pyfn_dijkstra_target(g: crate::generated::types::PyGraph, s: usize, t: usize) -> PyResult)>> { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::paths::dijkstra_target(&g, s, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| (__x.0, __x.1))) +} + +/// Single-source shortest paths allowing negative weights, by Bellman-Ford. +/// +/// Errors: +/// Returns `NegativeCycle` when a cycle of negative total weight is +/// reachable from `s`, which is detected by one relaxation pass beyond the +/// `n - 1` that suffice when none exists. +/// +/// Rust: `graph::paths::bellman_ford` +#[pyfunction] +#[pyo3(name = "bellman_ford", signature = (g, s))] +pub fn pyfn_bellman_ford(g: crate::generated::types::PyGraph, s: usize) -> PyResult<(Vec, Vec>)> { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::paths::bellman_ford(&g, s)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::errors::map_display)?; + Ok((__v.0, __v.1.into_iter().map(|__x| __x.map(|__x| __x)).collect::>())) +} + +/// All-pairs shortest paths by Floyd-Warshall, `O(n^3)`. +/// +/// Entry `(i, j)` is the distance, `f64::INFINITY` when unreachable. Negative +/// cycles are not detected here; a negative diagonal entry in the result is +/// the sign of one. +/// +/// Rust: `graph::paths::floyd_warshall` +#[pyfunction] +#[pyo3(name = "floyd_warshall", signature = (g))] +pub fn pyfn_floyd_warshall(g: crate::generated::types::PyGraph) -> PyResult { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::paths::floyd_warshall(&g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) +} + +/// All-pairs shortest paths by Johnson's algorithm: a Bellman-Ford pass from a +/// virtual source supplies potentials that make every weight non-negative, +/// then one Dijkstra per vertex. +/// +/// Faster than Floyd-Warshall on sparse graphs, and unlike plain Dijkstra it +/// tolerates negative weights. +/// +/// Errors: +/// Returns `NegativeCycle` if the graph contains one. +/// +/// Rust: `graph::paths::johnson` +#[pyfunction] +#[pyo3(name = "johnson", signature = (g))] +pub fn pyfn_johnson(g: crate::generated::types::PyGraph) -> PyResult { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::paths::johnson(&g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::errors::map_display)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) +} + +/// A* search with the heuristic `h`. +/// +/// Returns the path and its true length, or `None` if `t` is unreachable. The +/// result is optimal exactly when `h` is admissible -- never overestimating +/// the remaining distance -- and the search is efficient when `h` is also +/// consistent. An inadmissible heuristic still terminates but may return a +/// suboptimal path, which is the caller's trade to make. +/// +/// Panics: +/// Panics if any weight is negative. +/// +/// Rust: `graph::paths::a_star` +#[pyfunction] +#[pyo3(name = "a_star", signature = (g, s, t, h))] +pub fn pyfn_a_star(g: crate::generated::types::PyGraph, s: usize, t: usize, h: pyo3::Py) -> PyResult)>> { + let g = g.inner; + let __cb_h = std::rc::Rc::new(crate::runtime::Callback::new(h)); + let h = { let __cb = __cb_h.clone(); move |__a0: usize| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::paths::a_star(&g, s, t, &h)); + crate::runtime::callback::check(&[&__cb_h], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| (__x.0, __x.1))) +} + +/// Dijkstra from both ends at once, alternating between them. +/// +/// Both searches settle vertices; the answer is the best path through any +/// vertex either has reached, and the search stops once the two settled +/// radii sum to at least the best path found. On a graph where the reachable +/// set grows with the radius, this settles roughly the square root of the +/// vertices a one-sided search would. +/// +/// Panics: +/// Panics if any weight is negative. +/// +/// Rust: `graph::paths::bidirectional_dijkstra` +#[pyfunction] +#[pyo3(name = "bidirectional_dijkstra", signature = (g, s, t))] +pub fn pyfn_bidirectional_dijkstra(g: crate::generated::types::PyGraph, s: usize, t: usize) -> PyResult)>> { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::paths::bidirectional_dijkstra(&g, s, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| (__x.0, __x.1))) +} + +/// The `k` shortest loopless paths from `s` to `t`, by Yen's algorithm. +/// +/// Returns them in increasing length, and may return fewer than `k` when +/// fewer exist. Each candidate is found by forcing a shared prefix with an +/// already-accepted path and forbidding the arc it took next, which is what +/// keeps the results distinct and loopless. +/// +/// Rust: `graph::paths::k_shortest_paths_yen` +#[pyfunction] +#[pyo3(name = "k_shortest_paths_yen", signature = (g, s, t, k))] +pub fn pyfn_k_shortest_paths_yen<'py>(py: Python<'py>, g: crate::generated::types::PyGraph, s: usize, t: usize, k: usize) -> PyResult)>> { + let g = g.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::paths::k_shortest_paths_yen(&g, s, t, k))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// The widest path: the one whose narrowest edge is as wide as possible. +/// +/// Also called the bottleneck shortest path or the maximum capacity path. +/// Dijkstra with `min` in place of `+` and `max` in place of `min`, which is +/// valid because `min` is monotone in the same way `+` is. +/// +/// Rust: `graph::paths::widest_path` +#[pyfunction] +#[pyo3(name = "widest_path", signature = (g, s, t))] +pub fn pyfn_widest_path(g: crate::generated::types::PyGraph, s: usize, t: usize) -> PyResult)>> { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::paths::widest_path(&g, s, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| (__x.0, __x.1))) +} + +/// The minimax path: the one whose widest edge is as narrow as possible. +/// +/// The dual of `widest_path`, and the path a minimum spanning tree gives +/// between any two vertices. +/// +/// Rust: `graph::paths::minimax_path` +#[pyfunction] +#[pyo3(name = "minimax_path", signature = (g, s, t))] +pub fn pyfn_minimax_path(g: crate::generated::types::PyGraph, s: usize, t: usize) -> PyResult)>> { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::paths::minimax_path(&g, s, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| (__x.0, __x.1))) +} + +/// Shortest distances from `s` in a DAG, by relaxing in topological order. +/// +/// Linear time and correct with negative weights, neither of which Dijkstra +/// manages. +/// +/// Panics: +/// Panics if the graph is not a DAG. +/// +/// Rust: `graph::paths::dag_shortest` +#[pyfunction] +#[pyo3(name = "dag_shortest", signature = (g, s))] +pub fn pyfn_dag_shortest<'py>(py: Python<'py>, g: crate::generated::types::PyGraph, s: usize) -> PyResult> { + let g = g.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::paths::dag_shortest(&g, s))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Longest distances from `s` in a DAG. +/// +/// Longest path is NP-hard in general but linear on a DAG, since the +/// topological order removes any need to revisit. +/// +/// Panics: +/// Panics if the graph is not a DAG. +/// +/// Rust: `graph::paths::dag_longest` +#[pyfunction] +#[pyo3(name = "dag_longest", signature = (g, s))] +pub fn pyfn_dag_longest<'py>(py: Python<'py>, g: crate::generated::types::PyGraph, s: usize) -> PyResult> { + let g = g.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::paths::dag_longest(&g, s))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The number of distinct directed paths from `s` to `t` in a DAG. +/// +/// Exact, because the count grows exponentially: a grid DAG of side `n` has +/// `C(2n, n)` paths, past `u64` before `n = 34`. +/// +/// Panics: +/// Panics if the graph is not a DAG. +/// +/// Rust: `graph::paths::count_paths_dag` +#[pyfunction] +#[pyo3(name = "count_paths_dag", signature = (g, s, t))] +pub fn pyfn_count_paths_dag<'py>(py: Python<'py>, g: crate::generated::types::PyGraph, s: usize, t: usize) -> PyResult> { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::paths::count_paths_dag(&g, s, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// The reachability matrix: `[i][j]` is true when `j` is reachable from `i`. +/// +/// Every vertex reaches itself. +/// +/// Rust: `graph::paths::transitive_closure` +#[pyfunction] +#[pyo3(name = "transitive_closure", signature = (g))] +pub fn pyfn_transitive_closure<'py>(py: Python<'py>, g: crate::generated::types::PyGraph) -> PyResult>> { + let g = g.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::paths::transitive_closure(&g))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// A minimum spanning forest by Kruskal's algorithm: sort the edges, accept +/// each one that joins two different components. +/// +/// Returns the total weight and the edges, each with `u < v`. On a +/// disconnected graph this is a spanning forest, and the edge count is +/// `n - components` rather than `n - 1`. +/// +/// Rust: `graph::paths::minimum_spanning_tree_kruskal` +#[pyfunction] +#[pyo3(name = "minimum_spanning_tree_kruskal", signature = (g))] +pub fn pyfn_minimum_spanning_tree_kruskal(g: crate::generated::types::PyGraph) -> PyResult<(f64, Vec<(usize, usize)>)> { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::paths::minimum_spanning_tree_kruskal(&g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1.into_iter().map(|__x| (__x.0, __x.1)).collect::>())) +} + +/// A minimum spanning forest by Prim's algorithm: grow a tree from each +/// unvisited vertex, always taking the cheapest edge leaving it. +/// +/// Returns the same weight as Kruskal on any graph, though possibly a +/// different tree when weights tie. +/// +/// Rust: `graph::paths::minimum_spanning_tree_prim` +#[pyfunction] +#[pyo3(name = "minimum_spanning_tree_prim", signature = (g))] +pub fn pyfn_minimum_spanning_tree_prim(g: crate::generated::types::PyGraph) -> PyResult<(f64, Vec<(usize, usize)>)> { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::paths::minimum_spanning_tree_prim(&g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1.into_iter().map(|__x| (__x.0, __x.1)).collect::>())) +} + +/// A minimum spanning forest by Boruvka's algorithm: every component picks its +/// own cheapest outgoing edge, and all of them are added at once. +/// +/// Halves the component count per round, so `O(log n)` rounds suffice. Ties +/// are broken by edge index, which is what stops two components from each +/// picking the other's edge and forming a cycle. +/// +/// Rust: `graph::paths::minimum_spanning_tree_boruvka` +#[pyfunction] +#[pyo3(name = "minimum_spanning_tree_boruvka", signature = (g))] +pub fn pyfn_minimum_spanning_tree_boruvka(g: crate::generated::types::PyGraph) -> PyResult<(f64, Vec<(usize, usize)>)> { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::paths::minimum_spanning_tree_boruvka(&g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1.into_iter().map(|__x| (__x.0, __x.1)).collect::>())) +} + +/// The second-best spanning tree: the cheapest spanning tree that differs from +/// the minimum one in at least one edge. +/// +/// Found by swapping: for each non-tree edge, adding it creates one cycle, and +/// removing the heaviest tree edge on that cycle gives the cheapest tree +/// containing it. The best such swap is the answer. +/// +/// Returns `None` when the graph is disconnected or has no non-tree edge, so +/// no second tree exists. +/// +/// Rust: `graph::paths::second_best_mst` +#[pyfunction] +#[pyo3(name = "second_best_mst", signature = (g))] +pub fn pyfn_second_best_mst(g: crate::generated::types::PyGraph) -> PyResult)>> { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::paths::second_best_mst(&g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| (__x.0, __x.1.into_iter().map(|__x| (__x.0, __x.1)).collect::>()))) +} + +/// A minimum Steiner tree spanning the given terminals, by Dreyfus-Wagner. +/// +/// Returns the weight and the edges. The tree may use non-terminal vertices, +/// which is what separates the problem from a spanning tree. Costs +/// `O(3^t n + 2^t n^2)` for `t` terminals, so the terminal count is what has +/// to stay small, not the graph. +/// +/// Panics: +/// Panics if there are more than 12 terminals, or a terminal is out of range. +/// +/// Rust: `graph::paths::steiner_tree_small` +#[pyfunction] +#[pyo3(name = "steiner_tree_small", signature = (g, terminals))] +pub fn pyfn_steiner_tree_small<'py>(py: Python<'py>, g: crate::generated::types::PyGraph, terminals: Vec) -> PyResult<(f64, Vec<(usize, usize)>)> { + let g = g.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::paths::steiner_tree_small(&g, &terminals))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1.into_iter().map(|__x| (__x.0, __x.1)).collect::>())) +} + +/// The exact optimal travelling salesman tour, by Held-Karp. +/// +/// Returns the tour length and the tour as a vertex sequence starting and +/// ending at 0, with the final return implied rather than repeated. Costs +/// `O(2^n n^2)` time and `O(2^n n)` memory. +/// +/// Panics: +/// Panics if `dist` is not square, or has more than 20 rows. +/// +/// Rust: `graph::paths::traveling_salesman_exact` +#[pyfunction] +#[pyo3(name = "traveling_salesman_exact", signature = (dist))] +pub fn pyfn_traveling_salesman_exact<'py>(py: Python<'py>, dist: crate::generated::types::PyMatrixArg) -> PyResult<(f64, Vec)> { + let dist = dist.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::paths::traveling_salesman_exact(&dist))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// A nearest-neighbour tour: repeatedly walk to the closest unvisited city. +/// +/// Fast and usually poor: on a metric instance it can be a logarithmic factor +/// worse than optimal, so it is a starting point for `tsp_2opt` rather than +/// an answer. +/// +/// Panics: +/// Panics if `dist` is not square. +/// +/// Rust: `graph::paths::tsp_nearest_neighbor` +#[pyfunction] +#[pyo3(name = "tsp_nearest_neighbor", signature = (dist))] +pub fn pyfn_tsp_nearest_neighbor<'py>(py: Python<'py>, dist: crate::generated::types::PyMatrixArg) -> PyResult<(f64, Vec)> { + let dist = dist.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::paths::tsp_nearest_neighbor(&dist))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// The length of a closed tour under `dist`. +/// +/// Rust: `graph::paths::tour_length` +#[pyfunction] +#[pyo3(name = "tour_length", signature = (dist, tour))] +pub fn pyfn_tour_length<'py>(py: Python<'py>, dist: crate::generated::types::PyMatrixArg, tour: Vec) -> PyResult { + let dist = dist.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::paths::tour_length(&dist, &tour))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// 2-opt local search: repeatedly reverse a tour segment when that shortens +/// the tour, until no single reversal helps. +/// +/// The result is 2-optimal, not optimal. On a symmetric instance a reversal +/// changes only the two edges at its ends, which is what makes each move an +/// `O(1)` decision. +/// +/// Panics: +/// Panics if `dist` is not square, or `tour` is not a permutation of its rows. +/// +/// Rust: `graph::paths::tsp_2opt` +#[pyfunction] +#[pyo3(name = "tsp_2opt", signature = (dist, tour))] +pub fn pyfn_tsp_2opt<'py>(py: Python<'py>, dist: crate::generated::types::PyMatrixArg, tour: Vec) -> PyResult<(f64, Vec)> { + let dist = dist.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::paths::tsp_2opt(&dist, &tour))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Or-opt local search: relocate a run of one, two or three consecutive cities +/// elsewhere in the tour, in either orientation, while that shortens it. +/// +/// Complements 2-opt, which can only reverse: a run that belongs elsewhere +/// entirely is a move 2-opt cannot make in one step. +/// +/// Panics: +/// Panics if `dist` is not square, or `tour` is not a permutation of its rows. +/// +/// Rust: `graph::paths::tsp_or_opt` +#[pyfunction] +#[pyo3(name = "tsp_or_opt", signature = (dist, tour))] +pub fn pyfn_tsp_or_opt<'py>(py: Python<'py>, dist: crate::generated::types::PyMatrixArg, tour: Vec) -> PyResult<(f64, Vec)> { + let dist = dist.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::paths::tsp_or_opt(&dist, &tour))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Christofides' tour, which is within a factor of 1.5 of optimal on a metric +/// instance. +/// +/// Takes a minimum spanning tree, adds a minimum-weight perfect matching on +/// the odd-degree vertices to make every degree even, walks the resulting +/// Eulerian circuit, and shortcuts repeats. The matching here is exact by +/// brute force over pairings, which is affordable because a tree has few +/// odd-degree vertices on the instances this is used for, and is refused +/// beyond sixteen of them rather than silently degrading to a greedy one. +/// +/// Returns `None` when the odd set is too large for the exact matching. +/// +/// Panics: +/// Panics if `dist` is not square or is not symmetric, since the guarantee +/// needs a metric. +/// +/// Rust: `graph::paths::tsp_christofides` +#[pyfunction] +#[pyo3(name = "tsp_christofides", signature = (dist))] +pub fn pyfn_tsp_christofides<'py>(py: Python<'py>, dist: crate::generated::types::PyMatrixArg) -> PyResult)>> { + let dist = dist.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::paths::tsp_christofides(&dist))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| (__x.0, __x.1))) +} + +/// A shortest closed walk crossing every edge at least once: the Chinese +/// postman problem. +/// +/// Returns the walk's total weight and the vertex sequence. When every degree +/// is already even the answer is an Eulerian circuit and costs exactly the +/// total edge weight; otherwise the odd-degree vertices are paired up by a +/// minimum-weight perfect matching over shortest paths, and those paths are +/// duplicated. Returns `None` when the edges span more than one component, so +/// that no single closed walk can cross them all, or when the odd set is too +/// large for the exact matching. An edgeless graph has nothing to cross, so it +/// returns the empty route rather than failing on being disconnected. +/// +/// Panics: +/// Panics if the graph is directed, where the construction differs. +/// +/// Rust: `graph::paths::chinese_postman` +#[pyfunction] +#[pyo3(name = "chinese_postman", signature = (g))] +pub fn pyfn_chinese_postman(g: crate::generated::types::PyGraph) -> PyResult)>> { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::paths::chinese_postman(&g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| (__x.0, __x.1))) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_dijkstra, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dijkstra_target, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bellman_ford, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_floyd_warshall, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_johnson, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_a_star, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bidirectional_dijkstra, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_k_shortest_paths_yen, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_widest_path, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_minimax_path, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dag_shortest, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dag_longest, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_count_paths_dag, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_transitive_closure, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_minimum_spanning_tree_kruskal, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_minimum_spanning_tree_prim, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_minimum_spanning_tree_boruvka, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_second_best_mst, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_steiner_tree_small, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_traveling_salesman_exact, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tsp_nearest_neighbor, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tour_length, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tsp_2opt, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tsp_or_opt, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tsp_christofides, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_chinese_postman, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_graph__spectral.rs b/bindings/python/src/generated/m_graph__spectral.rs new file mode 100644 index 0000000..5ae3b24 --- /dev/null +++ b/bindings/python/src/generated/m_graph__spectral.rs @@ -0,0 +1,649 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// The combinatorial Laplacian `L = D - A`. +/// +/// The degree is the weighted degree, so `L` has row sums of exactly zero and +/// the all-ones vector is always in its kernel. Self-loops contribute to +/// neither the degree nor the adjacency, since they cancel. +/// +/// Panics: +/// Panics if the graph is directed. +/// +/// Rust: `graph::spectral::laplacian_matrix` +#[pyfunction] +#[pyo3(name = "laplacian_matrix", signature = (g))] +pub fn pyfn_laplacian_matrix(g: crate::generated::types::PyGraph) -> PyResult { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::spectral::laplacian_matrix(&g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) +} + +/// The weighted degree of each vertex, ignoring self-loops. +/// +/// Rust: `graph::spectral::weighted_degrees` +#[pyfunction] +#[pyo3(name = "weighted_degrees", signature = (g))] +pub fn pyfn_weighted_degrees<'py>(py: Python<'py>, g: crate::generated::types::PyGraph) -> PyResult> { + let g = g.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::spectral::weighted_degrees(&g))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The symmetric normalized Laplacian `I - D^(-1/2) A D^(-1/2)`. +/// +/// Its spectrum lies in `[0, 2]` whatever the graph, which is what makes it +/// the right object for comparing graphs of different sizes and densities. +/// The upper end is reached exactly on a bipartite component. An isolated +/// vertex has no degree to normalize by and is given a diagonal of zero. +/// +/// Panics: +/// Panics if the graph is directed. +/// +/// Rust: `graph::spectral::normalized_laplacian` +#[pyfunction] +#[pyo3(name = "normalized_laplacian", signature = (g))] +pub fn pyfn_normalized_laplacian(g: crate::generated::types::PyGraph) -> PyResult { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::spectral::normalized_laplacian(&g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) +} + +/// The adjacency eigenvalues, ascending. +/// +/// Panics: +/// Panics if the graph is directed, or the solver fails to converge. +/// +/// Rust: `graph::spectral::adjacency_spectrum` +#[pyfunction] +#[pyo3(name = "adjacency_spectrum", signature = (g))] +pub fn pyfn_adjacency_spectrum<'py>(py: Python<'py>, g: crate::generated::types::PyGraph) -> PyResult> { + let g = g.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::spectral::adjacency_spectrum(&g))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Laplacian eigenvalues, ascending. The first is always zero. +/// +/// Panics: +/// Panics if the graph is directed, or the solver fails to converge. +/// +/// Rust: `graph::spectral::laplacian_spectrum` +#[pyfunction] +#[pyo3(name = "laplacian_spectrum", signature = (g))] +pub fn pyfn_laplacian_spectrum<'py>(py: Python<'py>, g: crate::generated::types::PyGraph) -> PyResult> { + let g = g.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::spectral::laplacian_spectrum(&g))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The normalized Laplacian eigenvalues, ascending. All lie in `[0, 2]`. +/// +/// Panics: +/// Panics if the graph is directed, or the solver fails to converge. +/// +/// Rust: `graph::spectral::normalized_laplacian_spectrum` +#[pyfunction] +#[pyo3(name = "normalized_laplacian_spectrum", signature = (g))] +pub fn pyfn_normalized_laplacian_spectrum<'py>(py: Python<'py>, g: crate::generated::types::PyGraph) -> PyResult> { + let g = g.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::spectral::normalized_laplacian_spectrum(&g))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The algebraic connectivity: the second-smallest Laplacian eigenvalue. +/// +/// Zero exactly when the graph is disconnected, and larger the harder the +/// graph is to cut. Returns zero for fewer than two vertices. +/// +/// Panics: +/// Panics if the graph is directed. +/// +/// Rust: `graph::spectral::algebraic_connectivity` +#[pyfunction] +#[pyo3(name = "algebraic_connectivity", signature = (g))] +pub fn pyfn_algebraic_connectivity(g: crate::generated::types::PyGraph) -> PyResult { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::spectral::algebraic_connectivity(&g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Fiedler vector: the Laplacian eigenvector for the second-smallest +/// eigenvalue. +/// +/// Its sign pattern is the classic spectral bisection, and its ordering is a +/// good one-dimensional embedding of the graph. Normalized to unit length, +/// with the sign fixed so the first non-zero entry is positive -- an +/// eigenvector is only defined up to sign, and leaving that free would make +/// the output unreproducible. +/// +/// Panics: +/// Panics if the graph is directed, or has fewer than two vertices. +/// +/// Rust: `graph::spectral::fiedler_vector` +#[pyfunction] +#[pyo3(name = "fiedler_vector", signature = (g))] +pub fn pyfn_fiedler_vector<'py>(py: Python<'py>, g: crate::generated::types::PyGraph) -> PyResult> { + let g = g.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::spectral::fiedler_vector(&g))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Spectral bisection: split the vertices by the sign of the Fiedler vector. +/// +/// Panics: +/// Panics if the graph is directed, or has fewer than two vertices. +/// +/// Rust: `graph::spectral::spectral_bisection` +#[pyfunction] +#[pyo3(name = "spectral_bisection", signature = (g))] +pub fn pyfn_spectral_bisection<'py>(py: Python<'py>, g: crate::generated::types::PyGraph) -> PyResult> { + let g = g.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::spectral::spectral_bisection(&g))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Spectral clustering into `k` groups. +/// +/// Embeds each vertex in the `k` lowest Laplacian eigenvectors and runs +/// k-means there. The embedding is what does the work: in it, vertices that +/// are hard to separate by cutting edges sit close together, so a distance +/// clustering in that space corresponds to a good cut in the graph. +/// +/// Panics: +/// Panics if the graph is directed, `k` is zero, or `k` exceeds the vertex +/// count. +/// +/// Rust: `graph::spectral::spectral_clustering` +#[pyfunction] +#[pyo3(name = "spectral_clustering", signature = (g, k, rng))] +pub fn pyfn_spectral_clustering(g: crate::generated::types::PyGraph, k: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let g = g.inner; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::spectral::spectral_clustering(&g, k, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The number of spanning trees, by Kirchhoff's matrix-tree theorem. +/// +/// Any cofactor of the Laplacian gives the count; this uses the product of +/// the non-zero Laplacian eigenvalues divided by `n`, which is the same +/// number and needs no pivoting. Returns zero for a disconnected graph. +/// +/// The result is a float and is only exact while the count stays inside 53 +/// bits; `graph::core::spanning_tree_count_exact` does it over the +/// integers. +/// +/// Panics: +/// Panics if the graph is directed. +/// +/// Rust: `graph::spectral::number_spanning_trees` +#[pyfunction] +#[pyo3(name = "number_spanning_trees", signature = (g))] +pub fn pyfn_number_spanning_trees(g: crate::generated::types::PyGraph) -> PyResult { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::spectral::number_spanning_trees(&g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The number of spanning trees, exactly. +/// +/// Re-exported from `graph::core::spanning_tree_count_exact` so the +/// spectral module offers both the float and the exact form side by side. +/// +/// Panics: +/// Panics if the graph is directed. +/// +/// Rust: `graph::spectral::number_spanning_trees_exact` +#[pyfunction] +#[pyo3(name = "number_spanning_trees_exact", signature = (g))] +pub fn pyfn_number_spanning_trees_exact<'py>(py: Python<'py>, g: crate::generated::types::PyGraph) -> PyResult> { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::spectral::number_spanning_trees_exact(&g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// PageRank with the given damping factor. +/// +/// The rank vector is the stationary distribution of a random surfer who +/// follows an out-link with probability `damping` and teleports uniformly +/// otherwise. A vertex with no out-links would leak probability, so its mass +/// is redistributed uniformly -- without that the result would not sum to one. +/// +/// Returns a distribution summing to one. +/// +/// Panics: +/// Panics unless `damping` is in `[0, 1)` and `tol` is positive. +/// +/// Rust: `graph::spectral::pagerank` +#[pyfunction] +#[pyo3(name = "pagerank", signature = (g, damping, tol))] +pub fn pyfn_pagerank<'py>(py: Python<'py>, g: crate::generated::types::PyGraph, damping: f64, tol: f64) -> PyResult> { + let g = g.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::spectral::pagerank(&g, damping, tol))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// HITS: the hub and authority scores. +/// +/// A good authority is pointed to by good hubs and a good hub points to good +/// authorities, which is a mutual recurrence solved by alternating updates. +/// Both vectors are normalized to unit length. +/// +/// Panics: +/// Panics unless `tol` is positive. +/// +/// Rust: `graph::spectral::hits` +#[pyfunction] +#[pyo3(name = "hits", signature = (g, tol))] +pub fn pyfn_hits(g: crate::generated::types::PyGraph, tol: f64) -> PyResult<(Vec, Vec)> { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::spectral::hits(&g, tol)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Eigenvector centrality: the principal eigenvector of the adjacency matrix. +/// +/// A vertex is important when its neighbours are, which is exactly the +/// eigenvector equation. Found by power iteration; the result is +/// non-negative by Perron-Frobenius and is normalized to unit length. +/// +/// Panics: +/// Panics unless `tol` is positive. +/// +/// Rust: `graph::spectral::eigenvector_centrality` +#[pyfunction] +#[pyo3(name = "eigenvector_centrality", signature = (g, tol))] +pub fn pyfn_eigenvector_centrality<'py>(py: Python<'py>, g: crate::generated::types::PyGraph, tol: f64) -> PyResult> { + let g = g.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::spectral::eigenvector_centrality(&g, tol))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Katz centrality: the attenuated count of walks reaching each vertex. +/// +/// `x = (I - alpha A)^-1 * 1 - 1`, summed over walk lengths with each step +/// weighted by `alpha`. Converges only when `alpha` is below the reciprocal +/// of the largest adjacency eigenvalue, which is the caller's responsibility; +/// beyond that the walk count diverges and so does the series. +/// +/// Panics: +/// Panics unless `alpha` is positive. +/// +/// Rust: `graph::spectral::katz_centrality` +#[pyfunction] +#[pyo3(name = "katz_centrality", signature = (g, alpha))] +pub fn pyfn_katz_centrality<'py>(py: Python<'py>, g: crate::generated::types::PyGraph, alpha: f64) -> PyResult> { + let g = g.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::spectral::katz_centrality(&g, alpha))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Betweenness centrality, by Brandes' algorithm. +/// +/// The number of shortest paths through each vertex, summed over all source +/// and target pairs and normalized by how many shortest paths there are. +/// Brandes computes it in `O(VE)` by accumulating dependencies backwards +/// along one shortest-path DAG per source, rather than enumerating the +/// quadratically many pairs. +/// +/// Counts hops rather than weights. An undirected graph counts each unordered +/// pair once, so the values are halved. +/// +/// Rust: `graph::spectral::betweenness_centrality` +#[pyfunction] +#[pyo3(name = "betweenness_centrality", signature = (g))] +pub fn pyfn_betweenness_centrality<'py>(py: Python<'py>, g: crate::generated::types::PyGraph) -> PyResult> { + let g = g.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::spectral::betweenness_centrality(&g))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Closeness centrality: the reciprocal of the mean hop distance to every +/// reachable vertex, scaled by the fraction reachable. +/// +/// The scaling is what makes the value comparable across components: without +/// it, a vertex in a small tight component would outrank one in a large +/// well-connected component. +/// +/// Rust: `graph::spectral::closeness_centrality` +#[pyfunction] +#[pyo3(name = "closeness_centrality", signature = (g))] +pub fn pyfn_closeness_centrality<'py>(py: Python<'py>, g: crate::generated::types::PyGraph) -> PyResult> { + let g = g.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::spectral::closeness_centrality(&g))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Harmonic centrality: the sum of reciprocal distances. +/// +/// Unlike closeness this needs no special case for a disconnected graph -- an +/// unreachable vertex contributes `1/infinity = 0` -- which is why it is +/// preferred when the graph may not be connected. +/// +/// Rust: `graph::spectral::harmonic_centrality` +#[pyfunction] +#[pyo3(name = "harmonic_centrality", signature = (g))] +pub fn pyfn_harmonic_centrality<'py>(py: Python<'py>, g: crate::generated::types::PyGraph) -> PyResult> { + let g = g.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::spectral::harmonic_centrality(&g))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The effective resistance between two vertices, treating each edge as a +/// conductance equal to its weight. +/// +/// `R(u,v) = L+(u,u) + L+(v,v) - 2 L+(u,v)` for the Laplacian pseudoinverse. +/// Infinite when the two lie in different components. +/// +/// Panics: +/// Panics if the graph is directed, or an endpoint is out of range. +/// +/// Rust: `graph::spectral::effective_resistance` +#[pyfunction] +#[pyo3(name = "effective_resistance", signature = (g, u, v))] +pub fn pyfn_effective_resistance(g: crate::generated::types::PyGraph, u: usize, v: usize) -> PyResult { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::spectral::effective_resistance(&g, u, v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The effective resistance between every pair. +/// +/// Panics: +/// Panics if the graph is directed. +/// +/// Rust: `graph::spectral::resistance_matrix` +#[pyfunction] +#[pyo3(name = "resistance_matrix", signature = (g))] +pub fn pyfn_resistance_matrix(g: crate::generated::types::PyGraph) -> PyResult { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::spectral::resistance_matrix(&g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) +} + +/// The commute time between two vertices: the expected number of steps for a +/// random walk to go from `u` to `v` and back. +/// +/// Equal to `2m * R(u,v)` for total edge weight `m`, which is the theorem +/// that makes effective resistance a graph distance rather than merely an +/// analogy. +/// +/// Panics: +/// Panics if the graph is directed. +/// +/// Rust: `graph::spectral::commute_time` +#[pyfunction] +#[pyo3(name = "commute_time", signature = (g, u, v))] +pub fn pyfn_commute_time(g: crate::generated::types::PyGraph, u: usize, v: usize) -> PyResult { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::spectral::commute_time(&g, u, v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The stationary distribution of a simple random walk. +/// +/// On a connected undirected graph this is the degree distribution: the walk +/// spends time at a vertex in proportion to its weighted degree. +/// +/// Panics: +/// Panics if the graph is directed. +/// +/// Rust: `graph::spectral::random_walk_stationary` +#[pyfunction] +#[pyo3(name = "random_walk_stationary", signature = (g))] +pub fn pyfn_random_walk_stationary<'py>(py: Python<'py>, g: crate::generated::types::PyGraph) -> PyResult> { + let g = g.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::spectral::random_walk_stationary(&g))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// An estimate of the mixing time: how many steps until the walk is within +/// `eps` of stationary in total variation. +/// +/// Bounded by `log(1/(eps * pi_min)) / (1 - lambda2)` for the second-largest +/// transition eigenvalue in magnitude, which relates mixing to the spectral +/// gap. Infinite when the graph is disconnected or bipartite, where the walk +/// does not converge at all. +/// +/// Panics: +/// Panics if the graph is directed, or `eps` is not in `(0, 1)`. +/// +/// Rust: `graph::spectral::mixing_time_estimate` +#[pyfunction] +#[pyo3(name = "mixing_time_estimate", signature = (g, eps))] +pub fn pyfn_mixing_time_estimate(g: crate::generated::types::PyGraph, eps: f64) -> PyResult { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::spectral::mixing_time_estimate(&g, eps)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Cheeger bounds on the graph's conductance. +/// +/// Cheeger's inequality brackets the conductance `h` between `mu/2` and +/// `sqrt(2 mu)` for the second-smallest normalized Laplacian eigenvalue `mu`. +/// Returns `(lower, upper)`. +/// +/// Panics: +/// Panics if the graph is directed, or has fewer than two vertices. +/// +/// Rust: `graph::spectral::cheeger_bound` +#[pyfunction] +#[pyo3(name = "cheeger_bound", signature = (g))] +pub fn pyfn_cheeger_bound(g: crate::generated::types::PyGraph) -> PyResult<(f64, f64)> { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::spectral::cheeger_bound(&g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// True when the graph's spectral gap is at least `target_gap`. +/// +/// The gap is what makes an expander an expander: a large gap forces every +/// cut to be expensive, by Cheeger's inequality. +/// +/// Panics: +/// Panics if the graph is directed. +/// +/// Rust: `graph::spectral::expander_check` +#[pyfunction] +#[pyo3(name = "expander_check", signature = (g, target_gap))] +pub fn pyfn_expander_check(g: crate::generated::types::PyGraph, target_gap: f64) -> PyResult { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::spectral::expander_check(&g, target_gap)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The graph energy: the sum of the absolute adjacency eigenvalues. +/// +/// Panics: +/// Panics if the graph is directed. +/// +/// Rust: `graph::spectral::graph_energy` +#[pyfunction] +#[pyo3(name = "graph_energy", signature = (g))] +pub fn pyfn_graph_energy(g: crate::generated::types::PyGraph) -> PyResult { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::spectral::graph_energy(&g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Estrada index: the sum of `exp(lambda)` over the adjacency spectrum. +/// +/// Equal to the trace of `exp(A)`, which counts closed walks with each length +/// weighted by the reciprocal of its factorial. +/// +/// Panics: +/// Panics if the graph is directed. +/// +/// Rust: `graph::spectral::estrada_index` +#[pyfunction] +#[pyo3(name = "estrada_index", signature = (g))] +pub fn pyfn_estrada_index(g: crate::generated::types::PyGraph) -> PyResult { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::spectral::estrada_index(&g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// True when two graphs have the same adjacency spectrum to within `tol`. +/// +/// Isomorphic graphs are always isospectral; the converse is false, which is +/// what makes the spectrum a cheap but incomplete invariant. +/// +/// Panics: +/// Panics if either graph is directed. +/// +/// Rust: `graph::spectral::isospectral_check` +#[pyfunction] +#[pyo3(name = "isospectral_check", signature = (g, h, tol))] +pub fn pyfn_isospectral_check(g: crate::generated::types::PyGraph, h: crate::generated::types::PyGraph, tol: f64) -> PyResult { + let g = g.inner; + let h = h.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::spectral::isospectral_check(&g, &h, tol)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Newman's modularity of a vertex partition. +/// +/// The fraction of edge weight inside communities, minus what that fraction +/// would be if the same degrees were wired at random. Positive means the +/// partition captures more structure than chance; the maximum over all +/// partitions is what community detection tries to find. +/// +/// Panics: +/// Panics if the graph is directed, or `communities` does not have one label +/// per vertex. +/// +/// Rust: `graph::spectral::modularity` +#[pyfunction] +#[pyo3(name = "modularity", signature = (g, communities))] +pub fn pyfn_modularity<'py>(py: Python<'py>, g: crate::generated::types::PyGraph, communities: Vec) -> PyResult { + let g = g.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::graph::spectral::modularity(&g, &communities))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Community detection by the Louvain method. +/// +/// Two phases repeated: move each vertex to whichever neighbouring community +/// most improves modularity, then contract each community to a single vertex +/// and repeat on the smaller graph. The contraction is what lets it find +/// structure at several scales rather than only among immediate neighbours. +/// +/// Labels are renumbered from zero in order of first appearance. +/// +/// Panics: +/// Panics if the graph is directed. +/// +/// Rust: `graph::spectral::community_louvain` +#[pyfunction] +#[pyo3(name = "community_louvain", signature = (g, rng))] +pub fn pyfn_community_louvain(g: crate::generated::types::PyGraph, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let g = g.inner; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::spectral::community_louvain(&g, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Community detection by label propagation. +/// +/// Each vertex repeatedly adopts the label carried by the greatest weight +/// among its neighbours, ties broken at random. Near-linear and parameter- +/// free, but the outcome depends on the visiting order, which is why the +/// generator is a parameter rather than fixed. +/// +/// Panics: +/// Panics if the graph is directed. +/// +/// Rust: `graph::spectral::label_propagation` +#[pyfunction] +#[pyo3(name = "label_propagation", signature = (g, rng))] +pub fn pyfn_label_propagation(g: crate::generated::types::PyGraph, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let g = g.inner; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::spectral::label_propagation(&g, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_laplacian_matrix, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_weighted_degrees, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_normalized_laplacian, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_adjacency_spectrum, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_laplacian_spectrum, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_normalized_laplacian_spectrum, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_algebraic_connectivity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fiedler_vector, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spectral_bisection, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spectral_clustering, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_number_spanning_trees, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_number_spanning_trees_exact, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pagerank, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hits, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_eigenvector_centrality, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_katz_centrality, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_betweenness_centrality, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_closeness_centrality, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_harmonic_centrality, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_effective_resistance, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_resistance_matrix, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_commute_time, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_random_walk_stationary, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mixing_time_estimate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cheeger_bound, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_expander_check, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_graph_energy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_estrada_index, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_isospectral_check, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_modularity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_community_louvain, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_label_propagation, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_gravitation.rs b/bindings/python/src/generated/m_gravitation.rs new file mode 100644 index 0000000..14e6a7a --- /dev/null +++ b/bindings/python/src/generated/m_gravitation.rs @@ -0,0 +1,196 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Gravitational force magnitude between two masses: F = G * m1 * m2 / r^2 +/// +/// Rust: `gravitation::gravitational_force` +#[pyfunction] +#[pyo3(name = "gravitational_force", signature = (m1, m2, distance))] +pub fn pyfn_gravitational_force(m1: f64, m2: f64, distance: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::gravitation::gravitational_force(m1, m2, distance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Gravitational force vector from body1 toward body2. +/// +/// Rust: `gravitation::gravitational_force_vec` +#[pyfunction] +#[pyo3(name = "gravitational_force_vec", signature = (m1, pos1, m2, pos2))] +pub fn pyfn_gravitational_force_vec(m1: f64, pos1: crate::generated::types::PyVec3Arg, m2: f64, pos2: crate::generated::types::PyVec3Arg) -> PyResult { + let pos1 = pos1.0; + let pos2 = pos2.0; + let __r = crate::runtime::guard(|| rust_physics_engine::gravitation::gravitational_force_vec(m1, pos1, m2, pos2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Gravitational potential energy: U = -G * m1 * m2 / r +/// +/// Rust: `gravitation::gravitational_potential_energy` +#[pyfunction] +#[pyo3(name = "gravitational_potential_energy", signature = (m1, m2, distance))] +pub fn pyfn_gravitational_potential_energy(m1: f64, m2: f64, distance: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::gravitation::gravitational_potential_energy(m1, m2, distance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Gravitational field strength at distance r from mass M: g = G * M / r^2 +/// +/// Rust: `gravitation::gravitational_field` +#[pyfunction] +#[pyo3(name = "gravitational_field", signature = (mass, distance))] +pub fn pyfn_gravitational_field(mass: f64, distance: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::gravitation::gravitational_field(mass, distance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Escape velocity from a body of mass M and radius r: v = sqrt(2GM/r) +/// +/// Rust: `gravitation::escape_velocity` +#[pyfunction] +#[pyo3(name = "escape_velocity", signature = (mass, radius))] +pub fn pyfn_escape_velocity(mass: f64, radius: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::gravitation::escape_velocity(mass, radius)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Orbital velocity for a circular orbit: v = sqrt(GM/r) +/// +/// Rust: `gravitation::orbital_velocity` +#[pyfunction] +#[pyo3(name = "orbital_velocity", signature = (central_mass, orbital_radius))] +pub fn pyfn_orbital_velocity(central_mass: f64, orbital_radius: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::gravitation::orbital_velocity(central_mass, orbital_radius)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Orbital period (Kepler's third law): T = 2π * sqrt(r^3 / (G*M)) +/// +/// Rust: `gravitation::orbital_period` +#[pyfunction] +#[pyo3(name = "orbital_period", signature = (central_mass, orbital_radius))] +pub fn pyfn_orbital_period(central_mass: f64, orbital_radius: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::gravitation::orbital_period(central_mass, orbital_radius)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Semi-major axis from orbital period (inverse Kepler's third law): +/// a = (G*M*T^2 / (4π^2))^(1/3) +/// +/// Rust: `gravitation::semi_major_axis_from_period` +#[pyfunction] +#[pyo3(name = "semi_major_axis_from_period", signature = (central_mass, period))] +pub fn pyfn_semi_major_axis_from_period(central_mass: f64, period: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::gravitation::semi_major_axis_from_period(central_mass, period)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Schwarzschild radius of a black hole: r_s = 2GM / c^2 +/// +/// Rust: `gravitation::schwarzschild_radius` +#[pyfunction] +#[pyo3(name = "schwarzschild_radius", signature = (mass))] +pub fn pyfn_schwarzschild_radius(mass: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::gravitation::schwarzschild_radius(mass)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Gravitational time dilation factor at distance r from mass M: +/// sqrt(1 - 2GM/(rc^2)) +/// +/// Rust: `gravitation::gravitational_time_dilation` +#[pyfunction] +#[pyo3(name = "gravitational_time_dilation", signature = (mass, distance))] +pub fn pyfn_gravitational_time_dilation(mass: f64, distance: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::gravitation::gravitational_time_dilation(mass, distance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Roche limit (fluid body): d = R * (2 * ρ_M / ρ_m)^(1/3) +/// R = radius of primary, ρ_M = density of primary, ρ_m = density of satellite +/// +/// Rust: `gravitation::roche_limit` +#[pyfunction] +#[pyo3(name = "roche_limit", signature = (primary_radius, primary_density, satellite_density))] +pub fn pyfn_roche_limit(primary_radius: f64, primary_density: f64, satellite_density: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::gravitation::roche_limit(primary_radius, primary_density, satellite_density)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Vis-viva equation: v^2 = GM * (2/r - 1/a) +/// Returns the orbital speed at distance r for an orbit with semi-major axis a. +/// +/// Rust: `gravitation::vis_viva` +#[pyfunction] +#[pyo3(name = "vis_viva", signature = (central_mass, distance, semi_major_axis))] +pub fn pyfn_vis_viva(central_mass: f64, distance: f64, semi_major_axis: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::gravitation::vis_viva(central_mass, distance, semi_major_axis)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Specific orbital energy: ε = -GM / (2a) +/// +/// Rust: `gravitation::specific_orbital_energy` +#[pyfunction] +#[pyo3(name = "specific_orbital_energy", signature = (central_mass, semi_major_axis))] +pub fn pyfn_specific_orbital_energy(central_mass: f64, semi_major_axis: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::gravitation::specific_orbital_energy(central_mass, semi_major_axis)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Hill sphere radius: r_H ≈ a * (m / (3M))^(1/3) +/// +/// Rust: `gravitation::hill_sphere_radius` +#[pyfunction] +#[pyo3(name = "hill_sphere_radius", signature = (semi_major_axis, orbiting_mass, central_mass))] +pub fn pyfn_hill_sphere_radius(semi_major_axis: f64, orbiting_mass: f64, central_mass: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::gravitation::hill_sphere_radius(semi_major_axis, orbiting_mass, central_mass)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_gravitational_force, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gravitational_force_vec, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gravitational_potential_energy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gravitational_field, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_escape_velocity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_orbital_velocity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_orbital_period, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_semi_major_axis_from_period, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_schwarzschild_radius, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gravitational_time_dilation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_roche_limit, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_vis_viva, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_specific_orbital_energy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hill_sphere_radius, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_information_theory.rs b/bindings/python/src/generated/m_information_theory.rs new file mode 100644 index 0000000..a1d5212 --- /dev/null +++ b/bindings/python/src/generated/m_information_theory.rs @@ -0,0 +1,233 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Shannon information: entropy, divergence, and channel capacity. +/// +/// Entropy in bits and in nats, the maximum-entropy bound for a given +/// alphabet, and the entropy rate. Then the relations between two +/// distributions: cross entropy, Kullback-Leibler divergence, and the +/// Jensen-Shannon divergence -- which unlike KL is symmetric and bounded, +/// which is why it is the one that behaves like a distance. +/// +/// Mutual information and conditional entropy connect the two, and the +/// binary entropy function gives the capacity of a binary symmetric +/// channel as `C = 1 − H₂(p)`. Fisher information and the Cramér-Rao +/// bound cover the estimation side. +/// +/// For codes that approach these limits see `codes`. +/// H = -Σ pi × log₂(pi), skipping pi = 0. +/// +/// Rust: `information_theory::shannon_entropy` +#[pyfunction] +#[pyo3(name = "shannon_entropy", signature = (probabilities))] +pub fn pyfn_shannon_entropy<'py>(py: Python<'py>, probabilities: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::information_theory::shannon_entropy(&probabilities))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// H = -Σ pi × ln(pi), in nats. +/// +/// Rust: `information_theory::shannon_entropy_nats` +#[pyfunction] +#[pyo3(name = "shannon_entropy_nats", signature = (probabilities))] +pub fn pyfn_shannon_entropy_nats<'py>(py: Python<'py>, probabilities: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::information_theory::shannon_entropy_nats(&probabilities))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// H_max = log₂(N) for N equally likely symbols. +/// +/// Rust: `information_theory::max_entropy` +#[pyfunction] +#[pyo3(name = "max_entropy", signature = (n_symbols))] +pub fn pyfn_max_entropy(n_symbols: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::information_theory::max_entropy(n_symbols)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Identity function returning the conditional entropy value. Provided for API +/// completeness so callers can be explicit about what the quantity represents. +/// +/// Rust: `information_theory::entropy_rate` +#[pyfunction] +#[pyo3(name = "entropy_rate", signature = (conditional_entropy))] +pub fn pyfn_entropy_rate(conditional_entropy: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::information_theory::entropy_rate(conditional_entropy)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// D_KL(P||Q) = Σ pi × ln(pi/qi), skipping pi = 0. +/// +/// Rust: `information_theory::kl_divergence` +#[pyfunction] +#[pyo3(name = "kl_divergence", signature = (p, q))] +pub fn pyfn_kl_divergence<'py>(py: Python<'py>, p: Vec, q: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::information_theory::kl_divergence(&p, &q))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Jensen-Shannon divergence: JSD(P||Q) = (D_KL(P||M) + D_KL(Q||M)) / 2 +/// where M = (P + Q) / 2. +/// +/// Rust: `information_theory::js_divergence` +#[pyfunction] +#[pyo3(name = "js_divergence", signature = (p, q))] +pub fn pyfn_js_divergence<'py>(py: Python<'py>, p: Vec, q: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::information_theory::js_divergence(&p, &q))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// H(P, Q) = -Σ pi × log₂(qi). +/// +/// Rust: `information_theory::cross_entropy` +#[pyfunction] +#[pyo3(name = "cross_entropy", signature = (p, q))] +pub fn pyfn_cross_entropy<'py>(py: Python<'py>, p: Vec, q: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::information_theory::cross_entropy(&p, &q))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// I(X;Y) = Σ p(x,y) × ln(p(x,y) / (p(x) × p(y))). +/// `joint` is an nx×ny row-major probability table. +/// +/// Rust: `information_theory::mutual_information` +#[pyfunction] +#[pyo3(name = "mutual_information", signature = (joint, marginal_x, marginal_y, nx, ny))] +pub fn pyfn_mutual_information<'py>(py: Python<'py>, joint: Vec, marginal_x: Vec, marginal_y: Vec, nx: usize, ny: usize) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::information_theory::mutual_information(&joint, &marginal_x, &marginal_y, nx, ny))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// H(Y|X) = -Σ p(x,y) × ln(p(y|x)). +/// `joint` is an nx×ny row-major probability table, `marginal_condition` are +/// the marginal probabilities p(x). +/// +/// Rust: `information_theory::conditional_entropy` +#[pyfunction] +#[pyo3(name = "conditional_entropy", signature = (joint, marginal_condition, nx, ny))] +pub fn pyfn_conditional_entropy<'py>(py: Python<'py>, joint: Vec, marginal_condition: Vec, nx: usize, ny: usize) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::information_theory::conditional_entropy(&joint, &marginal_condition, nx, ny))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// H(p) = -p × log₂(p) - (1-p) × log₂(1-p). +/// +/// Rust: `information_theory::binary_entropy` +#[pyfunction] +#[pyo3(name = "binary_entropy", signature = (p))] +pub fn pyfn_binary_entropy(p: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::information_theory::binary_entropy(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// C = 1 - H(p) for a binary symmetric channel with crossover probability p. +/// +/// Rust: `information_theory::binary_symmetric_channel_capacity` +#[pyfunction] +#[pyo3(name = "binary_symmetric_channel_capacity", signature = (error_prob))] +pub fn pyfn_binary_symmetric_channel_capacity(error_prob: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::information_theory::binary_symmetric_channel_capacity(error_prob)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// R = original_bits / compressed_bits. +/// +/// Rust: `information_theory::compression_ratio` +#[pyfunction] +#[pyo3(name = "compression_ratio", signature = (original_bits, compressed_bits))] +pub fn pyfn_compression_ratio(original_bits: f64, compressed_bits: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::information_theory::compression_ratio(original_bits, compressed_bits)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// D = 1 - H / H_max. +/// +/// Rust: `information_theory::redundancy` +#[pyfunction] +#[pyo3(name = "redundancy", signature = (entropy, max_entropy))] +pub fn pyfn_redundancy(entropy: f64, max_entropy: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::information_theory::redundancy(entropy, max_entropy)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// η = H / L where L is average code length. +/// +/// Rust: `information_theory::efficiency` +#[pyfunction] +#[pyo3(name = "efficiency", signature = (entropy, avg_code_length))] +pub fn pyfn_efficiency(entropy: f64, avg_code_length: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::information_theory::efficiency(entropy, avg_code_length)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// I(μ) = 1/σ² for a Gaussian when estimating the mean. +/// +/// Rust: `information_theory::fisher_information_gaussian` +#[pyfunction] +#[pyo3(name = "fisher_information_gaussian", signature = (sigma))] +pub fn pyfn_fisher_information_gaussian(sigma: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::information_theory::fisher_information_gaussian(sigma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Cramér-Rao lower bound: var(θ̂) ≥ 1 / I(θ). +/// +/// Rust: `information_theory::cramer_rao_bound` +#[pyfunction] +#[pyo3(name = "cramer_rao_bound", signature = (fisher_info))] +pub fn pyfn_cramer_rao_bound(fisher_info: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::information_theory::cramer_rao_bound(fisher_info)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_shannon_entropy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_shannon_entropy_nats, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_max_entropy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_entropy_rate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kl_divergence, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_js_divergence, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cross_entropy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mutual_information, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_conditional_entropy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_binary_entropy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_binary_symmetric_channel_capacity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_compression_ratio, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_redundancy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_efficiency, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fisher_information_gaussian, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cramer_rao_bound, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_learn.rs b/bindings/python/src/generated/m_learn.rs new file mode 100644 index 0000000..c02bbf8 --- /dev/null +++ b/bindings/python/src/generated/m_learn.rs @@ -0,0 +1,22 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + Ok(()) +} diff --git a/bindings/python/src/generated/m_learn__cluster.rs b/bindings/python/src/generated/m_learn__cluster.rs new file mode 100644 index 0000000..caa223d --- /dev/null +++ b/bindings/python/src/generated/m_learn__cluster.rs @@ -0,0 +1,348 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Chooses `k` starting centres by the k-means++ rule: the first +/// uniformly at random, each subsequent one with probability +/// proportional to its squared distance from the nearest centre already +/// chosen. +/// +/// The rule matters. Uniform initialisation regularly puts two centres +/// in the same dense region and leaves another region unclaimed, and +/// Lloyd's algorithm cannot repair that -- it is a local method and the +/// bad split is a local optimum. The `D^2` weighting makes the expected +/// final inertia within a logarithmic factor of the best possible, +/// which is the only approximation guarantee k-means has. +/// +/// Errors: +/// +/// `SolveError::InvalidArgument` for an invalid dataset, `k == 0`, or +/// more centres than points. +/// +/// Rust: `learn::cluster::kmeans_pp_init` +#[pyfunction] +#[pyo3(name = "kmeans_pp_init", signature = (data, k, rng))] +pub fn pyfn_kmeans_pp_init(data: Vec>, k: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult>> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::learn::cluster::kmeans_pp_init(&data, k, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// Lloyd's algorithm, restarted `RESTARTS` (10) times from independent +/// k-means++ starts, keeping the run with the lowest inertia. +/// +/// The result carries the winning run's inertia history, which is +/// non-increasing within that run -- see the module note. Use +/// `kmeans_once` to observe a single trajectory. +/// +/// Errors: +/// +/// `SolveError::InvalidArgument` for an invalid dataset, `k == 0`, +/// more clusters than points, or zero iterations. +/// +/// Rust: `learn::cluster::kmeans` +#[pyfunction] +#[pyo3(name = "kmeans", signature = (data, k, iters, rng))] +pub fn pyfn_kmeans(data: Vec>, k: usize, iters: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::learn::cluster::kmeans(&data, k, iters, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(crate::generated::types::PyKMeans { inner: __v }) +} + +/// A single run of Lloyd's algorithm from one k-means++ start. +/// +/// Runs until the assignment stops changing or `iters` iterations have +/// passed. The inertia after each iteration is recorded, and it is +/// non-increasing by construction. +/// +/// An empty cluster is refilled with the point currently furthest from +/// its own centre. Leaving it empty would silently return fewer clusters +/// than were asked for, and the mean of no points is not a number. +/// +/// Errors: +/// +/// As `kmeans`. +/// +/// Rust: `learn::cluster::kmeans_once` +#[pyfunction] +#[pyo3(name = "kmeans_once", signature = (data, k, iters, rng))] +pub fn pyfn_kmeans_once(data: Vec>, k: usize, iters: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::learn::cluster::kmeans_once(&data, k, iters, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(crate::generated::types::PyKMeans { inner: __v }) +} + +/// The final inertia for each cluster count in `k_range`, for plotting +/// an elbow. +/// +/// Inertia falls monotonically with `k` in expectation and reaches zero +/// when every point is its own cluster, so the number alone says +/// nothing -- the elbow is where the fall stops being worth the extra +/// cluster, and that is a judgement rather than a computation. The +/// function returns the curve and declines to pick a point on it. +/// +/// Errors: +/// +/// As `kmeans`, or `SolveError::InvalidArgument` for an empty range. +/// +/// Rust: `learn::cluster::elbow_data` +#[pyfunction] +#[pyo3(name = "elbow_data", signature = (data, k_range, iters, rng))] +pub fn pyfn_elbow_data(data: Vec>, k_range: Vec, iters: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::learn::cluster::elbow_data(&data, &k_range, iters, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Density-based clustering. Returns a label per point, with `-1` for +/// noise. +/// +/// A point is a *core* point if at least `min_pts` points (itself +/// included) lie within `eps`. Clusters are the connected components of +/// the core points, plus the non-core points within `eps` of one. +/// +/// Core points are determined by the data alone. Border points are not: +/// one within reach of two clusters joins whichever reaches it first, +/// which depends on the order the points arrive in. That is in the +/// algorithm as defined, not an artefact here -- see the module note. +/// +/// Errors: +/// +/// `SolveError::InvalidArgument` for an invalid dataset, a +/// non-positive `eps`, or `min_pts == 0`. +/// +/// Rust: `learn::cluster::dbscan` +#[pyfunction] +#[pyo3(name = "dbscan", signature = (data, eps, min_pts))] +pub fn pyfn_dbscan<'py>(py: Python<'py>, data: Vec>, eps: f64, min_pts: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::learn::cluster::dbscan(&data, eps, min_pts))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// Agglomerative clustering, returning the merges in order as +/// `(left, right, height)`. +/// +/// Cluster indices below `n` are the original points; the merge at step +/// `t` creates cluster `n + t`. Heights are Euclidean distances under +/// the chosen linkage. +/// +/// Errors: +/// +/// `SolveError::InvalidArgument` for an invalid dataset or fewer than +/// two points. +/// +/// Rust: `learn::cluster::hierarchical_agglomerative` +#[pyfunction] +#[pyo3(name = "hierarchical_agglomerative", signature = (data, linkage))] +pub fn pyfn_hierarchical_agglomerative<'py>(py: Python<'py>, data: Vec>, linkage: crate::generated::types::PyLinkage) -> PyResult> { + let linkage = linkage.to_rust(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::learn::cluster::hierarchical_agglomerative(&data, linkage))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, __x.2)).collect::>()) +} + +/// Cuts a dendrogram into `k` clusters, returning a label per original +/// point. +/// +/// Errors: +/// +/// `SolveError::InvalidArgument` if `k` is zero or exceeds the point +/// count, or if the merge list is not `n - 1` long. +/// +/// Rust: `learn::cluster::dendrogram_cut` +#[pyfunction] +#[pyo3(name = "dendrogram_cut", signature = (merges, n, k))] +pub fn pyfn_dendrogram_cut<'py>(py: Python<'py>, merges: Vec<(usize, usize, f64)>, n: usize, k: usize) -> PyResult> { + let merges = merges.into_iter().map(|__e| (__e.0, __e.1, __e.2)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::learn::cluster::dendrogram_cut(&merges, n, k))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// Fits a Gaussian mixture by expectation-maximisation. +/// +/// Each step increases the log-likelihood, which is recorded so that the +/// monotonicity can be checked rather than assumed. +/// +/// Errors: +/// +/// `SolveError::InvalidArgument` for an invalid dataset, `k == 0`, +/// more components than points, or zero iterations; +/// `SolveError::NotPositiveDefinite` if a covariance cannot be +/// factored even with the floor applied. +/// +/// Rust: `learn::cluster::gaussian_mixture_em` +#[pyfunction] +#[pyo3(name = "gaussian_mixture_em", signature = (data, k, iters, rng))] +pub fn pyfn_gaussian_mixture_em(data: Vec>, k: usize, iters: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::learn::cluster::gaussian_mixture_em(&data, k, iters, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(crate::generated::types::PyGmm { inner: __v }) +} + +/// The mean silhouette over all points, in `[-1, 1]`. +/// +/// A point's silhouette compares the mean distance to its own cluster +/// against the mean distance to the nearest other cluster. One means +/// the clusters are tight and far apart; zero means the point sits on a +/// boundary; negative means it is closer to another cluster than its +/// own. A point alone in its cluster scores zero by convention -- there +/// is no within-cluster distance to compute, and calling it a perfect +/// one would reward splitting every point off. +/// +/// Errors: +/// +/// `SolveError::InvalidArgument` for an invalid dataset or fewer than +/// two distinct labels; `SolveError::DimensionMismatch` on a length +/// mismatch. +/// +/// Rust: `learn::cluster::silhouette_score` +#[pyfunction] +#[pyo3(name = "silhouette_score", signature = (data, labels))] +pub fn pyfn_silhouette_score<'py>(py: Python<'py>, data: Vec>, labels: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::learn::cluster::silhouette_score(&data, &labels))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// The adjusted Rand index between two partitions. +/// +/// Counts the pairs of points the two partitions agree about, then +/// subtracts what agreement would be expected by chance from partitions +/// with the same cluster sizes. Identical partitions score exactly one; +/// independent random ones score about zero, and may score below it. +/// +/// The correction is what makes the number usable. The unadjusted Rand +/// index of two random partitions of many points into a few clusters is +/// close to one, because most pairs are in different clusters under both +/// and that counts as agreement. +/// +/// Invariant under relabelling either partition, which is the minimum a +/// comparison between clusterings has to satisfy: a cluster index is not +/// a name. +/// +/// Errors: +/// +/// `SolveError::DimensionMismatch` if the two have different lengths; +/// `SolveError::InvalidArgument` if they are empty. +/// +/// Rust: `learn::cluster::adjusted_rand_index` +#[pyfunction] +#[pyo3(name = "adjusted_rand_index", signature = (a, b))] +pub fn pyfn_adjusted_rand_index<'py>(py: Python<'py>, a: Vec, b: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::learn::cluster::adjusted_rand_index(&a, &b))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// The Davies-Bouldin index: the mean over clusters of the worst ratio +/// of within-cluster spread to between-cluster separation. +/// +/// Lower is better, and zero is unattainable. Unlike the silhouette it +/// is unbounded above, and unlike the silhouette it uses only the +/// centroids, so it is cheap and it is blind to cluster shape. +/// +/// Errors: +/// +/// As `silhouette_score`. +/// +/// Rust: `learn::cluster::davies_bouldin` +#[pyfunction] +#[pyo3(name = "davies_bouldin", signature = (data, labels))] +pub fn pyfn_davies_bouldin<'py>(py: Python<'py>, data: Vec>, labels: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::learn::cluster::davies_bouldin(&data, &labels))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// Classifies `x` by a majority vote of its `k` nearest neighbours. +/// +/// Ties are broken towards the smaller label, which is arbitrary but +/// deterministic; an even `k` on a two-class problem can produce them, +/// which is the usual reason to prefer an odd one. +/// +/// Errors: +/// +/// `SolveError::InvalidArgument` for an invalid or empty training set, +/// `k == 0`, or more neighbours than points; +/// `SolveError::DimensionMismatch` on a label count or query +/// dimension mismatch. +/// +/// Rust: `learn::cluster::knn_classify` +#[pyfunction] +#[pyo3(name = "knn_classify", signature = (train, labels, x, k))] +pub fn pyfn_knn_classify<'py>(py: Python<'py>, train: Vec>, labels: Vec, x: Vec, k: usize) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::learn::cluster::knn_classify(&train, &labels, &x, k))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// Predicts a value for `x` as the mean of its `k` nearest neighbours' +/// targets. +/// +/// Errors: +/// +/// As `knn_classify`. +/// +/// Rust: `learn::cluster::knn_regress` +#[pyfunction] +#[pyo3(name = "knn_regress", signature = (train, targets, x, k))] +pub fn pyfn_knn_regress<'py>(py: Python<'py>, train: Vec>, targets: Vec, x: Vec, k: usize) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::learn::cluster::knn_regress(&train, &targets, &x, k))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_kmeans_pp_init, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kmeans, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kmeans_once, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_elbow_data, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dbscan, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hierarchical_agglomerative, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dendrogram_cut, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gaussian_mixture_em, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_silhouette_score, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_adjusted_rand_index, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_davies_bouldin, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_knn_classify, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_knn_regress, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_learn__gp.rs b/bindings/python/src/generated/m_learn__gp.rs new file mode 100644 index 0000000..80d62c4 --- /dev/null +++ b/bindings/python/src/generated/m_learn__gp.rs @@ -0,0 +1,45 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Draws sample functions from a prior with the given kernel. +/// +/// Errors: +/// +/// `SolveError::InvalidArgument` for an invalid kernel or an empty or +/// ragged point set; `SolveError::NotPositiveDefinite` if the +/// covariance matrix cannot be factored. +/// +/// Rust: `learn::gp::sample_prior` +#[pyfunction] +#[pyo3(name = "sample_prior", signature = (kernel, x, count, rng))] +pub fn pyfn_sample_prior(kernel: crate::generated::types::PyKernelFn, x: Vec>, count: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult>> { + let kernel = kernel.inner; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::learn::gp::sample_prior(&kernel, &x, count, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_sample_prior, m)?)?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_learn__nn.rs b/bindings/python/src/generated/m_learn__nn.rs new file mode 100644 index 0000000..7f26468 --- /dev/null +++ b/bindings/python/src/generated/m_learn__nn.rs @@ -0,0 +1,86 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// One convolution layer's forward pass: `kernels` applied to a single +/// channel image, with the given stride and zero padding. +/// +/// Returns one output plane per kernel, each row-major, along with the +/// output width and height. The convolution here is the cross-correlation +/// that every machine learning library calls a convolution -- the kernel +/// is *not* flipped. Against a symmetric kernel the two agree and the +/// distinction never shows; against an asymmetric one they differ by a +/// reflection, so a signal-processing convolution needs the kernel +/// reversed on the way in. +/// +/// Errors: +/// +/// `SolveError::InvalidArgument` for a zero stride, an empty kernel +/// set, a kernel larger than the padded image, or mismatched sizes; +/// `SolveError::DimensionMismatch` if the image is not `w * h`. +/// +/// Rust: `learn::nn::conv2d_forward` +#[pyfunction] +#[pyo3(name = "conv2d_forward", signature = (input, w, h, kernels, stride, pad))] +pub fn pyfn_conv2d_forward<'py>(py: Python<'py>, input: Vec, w: usize, h: usize, kernels: Vec<(Vec, usize, usize)>, stride: usize, pad: usize) -> PyResult<(Vec>, usize, usize)> { + let kernels = kernels.into_iter().map(|__e| (__e.0, __e.1, __e.2)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::learn::nn::conv2d_forward(&input, w, h, &kernels, stride, pad))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// Fits `y = X b` by gradient descent and reports how far the answer is +/// from the closed-form least-squares solution, relative to its size. +/// +/// The point is the comparison. Least squares has an exact answer +/// through the normal equations, so an iterative method solving the same +/// problem has somewhere to be checked against -- and that check is +/// worth more than any amount of watching a loss go down, because a +/// descent with the wrong gradient also produces a loss that goes down. +/// +/// The step size is taken as `1 / L` with `L` the largest eigenvalue of +/// `X^T X`, estimated by a few power iterations. That is the largest +/// step for which gradient descent on a quadratic is guaranteed to +/// converge, and going past it diverges rather than converging slowly. +/// +/// Errors: +/// +/// `SolveError::InvalidArgument` for an empty or ill-shaped problem; +/// whatever the least-squares solver reports otherwise. +/// +/// Rust: `learn::nn::linear_regression_gd_check` +#[pyfunction] +#[pyo3(name = "linear_regression_gd_check", signature = (x, y, iterations))] +pub fn pyfn_linear_regression_gd_check<'py>(py: Python<'py>, x: crate::generated::types::PyMatrixArg, y: Vec, iterations: usize) -> PyResult { + let x = x.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::learn::nn::linear_regression_gd_check(&x, &y, iterations))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_conv2d_forward, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_linear_regression_gd_check, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_learn__tree.rs b/bindings/python/src/generated/m_learn__tree.rs new file mode 100644 index 0000000..8e9ed89 --- /dev/null +++ b/bindings/python/src/generated/m_learn__tree.rs @@ -0,0 +1,250 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// The Gini impurity of a set of class counts, `1 - sum p^2`. +/// +/// Exactly zero for a pure node and exactly `1 - 1/k` for `k` classes in +/// equal proportion, which is its maximum. Both are identities rather +/// than limits. +/// +/// Compared with `entropy` it is cheaper -- no logarithm -- and the +/// two rank splits almost identically, which is why the choice between +/// them is very nearly arbitrary. +/// +/// Rust: `learn::tree::gini` +#[pyfunction] +#[pyo3(name = "gini", signature = (counts))] +pub fn pyfn_gini<'py>(py: Python<'py>, counts: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::learn::tree::gini(&counts))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Shannon entropy of a set of class counts, in nats. +/// +/// Zero for a pure node and `ln k` for `k` classes in equal proportion. +/// A count of zero contributes nothing, which is the continuous +/// extension of `p ln p` at the origin rather than a special case. +/// +/// Rust: `learn::tree::entropy` +#[pyfunction] +#[pyo3(name = "entropy", signature = (counts))] +pub fn pyfn_entropy<'py>(py: Python<'py>, counts: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::learn::tree::entropy(&counts))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Fits a classification tree by greedy Gini reduction. +/// +/// Errors: +/// +/// `SolveError::InvalidArgument` for an invalid dataset or a zero +/// `min_leaf`; `SolveError::DimensionMismatch` on a label count +/// mismatch. +/// +/// Rust: `learn::tree::decision_tree_fit` +#[pyfunction] +#[pyo3(name = "decision_tree_fit", signature = (x, y, max_depth, min_leaf))] +pub fn pyfn_decision_tree_fit(x: Vec>, y: Vec, max_depth: usize, min_leaf: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::learn::tree::decision_tree_fit(&x, &y, max_depth, min_leaf)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(crate::generated::types::PyTree { inner: __v }) +} + +/// Fits a regression tree by greedy variance reduction. +/// +/// Errors: +/// +/// As `decision_tree_fit`, and additionally for non-finite targets. +/// +/// Rust: `learn::tree::regression_tree_fit` +#[pyfunction] +#[pyo3(name = "regression_tree_fit", signature = (x, y, max_depth, min_leaf))] +pub fn pyfn_regression_tree_fit(x: Vec>, y: Vec, max_depth: usize, min_leaf: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::learn::tree::regression_tree_fit(&x, &y, max_depth, min_leaf)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(crate::generated::types::PyTree { inner: __v }) +} + +/// The class a tree predicts for a point. +/// +/// Errors: +/// +/// `SolveError::DimensionMismatch` if the point has the wrong width. +/// +/// Rust: `learn::tree::tree_predict` +#[pyfunction] +#[pyo3(name = "tree_predict", signature = (tree, x))] +pub fn pyfn_tree_predict<'py>(py: Python<'py>, tree: crate::generated::types::PyTree, x: Vec) -> PyResult { + let tree = tree.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::learn::tree::tree_predict(&tree, &x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// The value a regression tree predicts for a point. +/// +/// Errors: +/// +/// As `tree_predict`. +/// +/// Rust: `learn::tree::tree_predict_value` +#[pyfunction] +#[pyo3(name = "tree_predict_value", signature = (tree, x))] +pub fn pyfn_tree_predict_value<'py>(py: Python<'py>, tree: crate::generated::types::PyTree, x: Vec) -> PyResult { + let tree = tree.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::learn::tree::tree_predict_value(&tree, &x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// How much impurity each feature removed, summed over the splits that +/// used it and weighted by the samples that reached them. +/// +/// Nonnegative, because no split with a negative decrease is ever taken, +/// and summing to exactly the tree's total weighted impurity decrease. +/// Unnormalised on purpose: the total is a meaningful quantity, and +/// dividing by it throws away how much the tree explained in favour of +/// how it divided the credit. +/// +/// Rust: `learn::tree::feature_importance` +#[pyfunction] +#[pyo3(name = "feature_importance", signature = (tree))] +pub fn pyfn_feature_importance<'py>(py: Python<'py>, tree: crate::generated::types::PyTree) -> PyResult> { + let tree = tree.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::learn::tree::feature_importance(&tree))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Grows a random forest: `n_trees` classification trees, each on a +/// bootstrap resample, each split choosing among a random subset of +/// features. +/// +/// Both sources of randomness are needed. Bagging alone leaves the trees +/// too much alike, because whichever feature is most informative is +/// chosen at the root of nearly all of them; restricting the features +/// considered at each split is what decorrelates the errors, and +/// averaging only cancels errors that are not shared. +/// +/// `features_per_split` defaults to the square root of the feature +/// count when given as zero, which is the usual choice for +/// classification. +/// +/// Errors: +/// +/// As `decision_tree_fit`, plus `SolveError::InvalidArgument` for +/// zero trees. +/// +/// Rust: `learn::tree::random_forest_fit` +#[pyfunction] +#[pyo3(name = "random_forest_fit", signature = (x, y, n_trees, max_depth, min_leaf, features_per_split, rng))] +pub fn pyfn_random_forest_fit(x: Vec>, y: Vec, n_trees: usize, max_depth: usize, min_leaf: usize, features_per_split: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::learn::tree::random_forest_fit(&x, &y, n_trees, max_depth, min_leaf, features_per_split, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(crate::generated::types::PyForest { inner: __v }) +} + +/// The forest's majority vote. +/// +/// Errors: +/// +/// As `tree_predict`. +/// +/// Rust: `learn::tree::forest_predict` +#[pyfunction] +#[pyo3(name = "forest_predict", signature = (forest, x))] +pub fn pyfn_forest_predict<'py>(py: Python<'py>, forest: crate::generated::types::PyForest, x: Vec) -> PyResult { + let forest = forest.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::learn::tree::forest_predict(&forest, &x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// Fits a gradient boosted regressor under squared loss. +/// +/// Starts at the mean and adds `learning_rate` times a shallow tree +/// fitted to the current residual, `n_rounds` times. Under squared loss +/// the negative gradient *is* the residual, which is why this simplest +/// case looks like nothing more than fitting the errors -- for other +/// losses the tree is fitted to the gradient and the leaf values are +/// then corrected, which is where the name comes from. +/// +/// The loss falls monotonically for a learning rate at or below one, +/// because each tree reduces the squared residual it was fitted to and +/// shrinking a descent step cannot turn it into an ascent. +/// +/// Errors: +/// +/// As `regression_tree_fit`, plus `SolveError::InvalidArgument` for +/// a learning rate outside `(0, 1]`. +/// +/// Rust: `learn::tree::gradient_boosting_lite` +#[pyfunction] +#[pyo3(name = "gradient_boosting_lite", signature = (x, y, n_rounds, learning_rate, depth))] +pub fn pyfn_gradient_boosting_lite(x: Vec>, y: Vec, n_rounds: usize, learning_rate: f64, depth: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::learn::tree::gradient_boosting_lite(&x, &y, n_rounds, learning_rate, depth)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(crate::generated::types::PyGbm { inner: __v }) +} + +/// The boosted model's prediction. +/// +/// Errors: +/// +/// As `tree_predict_value`. +/// +/// Rust: `learn::tree::gbm_predict` +#[pyfunction] +#[pyo3(name = "gbm_predict", signature = (model, x))] +pub fn pyfn_gbm_predict<'py>(py: Python<'py>, model: crate::generated::types::PyGbm, x: Vec) -> PyResult { + let model = model.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::learn::tree::gbm_predict(&model, &x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_gini, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_entropy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_decision_tree_fit, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_regression_tree_fit, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tree_predict, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tree_predict_value, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_feature_importance, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_random_forest_fit, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_forest_predict, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gradient_boosting_lite, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gbm_predict, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_linalg.rs b/bindings/python/src/generated/m_linalg.rs new file mode 100644 index 0000000..55e14f8 --- /dev/null +++ b/bindings/python/src/generated/m_linalg.rs @@ -0,0 +1,146 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Rotation matrix about the x-axis by the given angle in radians. +/// +/// Rust: `linalg::rotation_x` +#[pyfunction] +#[pyo3(name = "rotation_x", signature = (angle))] +pub fn pyfn_rotation_x(angle: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::linalg::rotation_x(angle)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMat3 { inner: __v }) +} + +/// Rotation matrix about the y-axis by the given angle in radians. +/// +/// Rust: `linalg::rotation_y` +#[pyfunction] +#[pyo3(name = "rotation_y", signature = (angle))] +pub fn pyfn_rotation_y(angle: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::linalg::rotation_y(angle)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMat3 { inner: __v }) +} + +/// Rotation matrix about the z-axis by the given angle in radians. +/// +/// Rust: `linalg::rotation_z` +#[pyfunction] +#[pyo3(name = "rotation_z", signature = (angle))] +pub fn pyfn_rotation_z(angle: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::linalg::rotation_z(angle)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMat3 { inner: __v }) +} + +/// Rodrigues' rotation formula: rotate by `angle` radians about `axis`. +/// The axis is normalized internally. +/// +/// Rust: `linalg::rotation_axis_angle` +#[pyfunction] +#[pyo3(name = "rotation_axis_angle", signature = (axis, angle))] +pub fn pyfn_rotation_axis_angle(axis: crate::generated::types::PyVec3Arg, angle: f64) -> PyResult { + let axis = axis.0; + let __r = crate::runtime::guard(|| rust_physics_engine::linalg::rotation_axis_angle(axis, angle)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMat3 { inner: __v }) +} + +/// Returns (r, theta, phi) where theta is the polar angle from +z and phi is the azimuthal angle from +x. +/// +/// Rust: `linalg::cartesian_to_spherical` +#[pyfunction] +#[pyo3(name = "cartesian_to_spherical", signature = (x, y, z))] +pub fn pyfn_cartesian_to_spherical(x: f64, y: f64, z: f64) -> PyResult<(f64, f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::linalg::cartesian_to_spherical(x, y, z)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// Converts spherical coordinates (r, theta, phi) to Cartesian (x, y, z). +/// +/// Rust: `linalg::spherical_to_cartesian` +#[pyfunction] +#[pyo3(name = "spherical_to_cartesian", signature = (r, theta, phi))] +pub fn pyfn_spherical_to_cartesian(r: f64, theta: f64, phi: f64) -> PyResult<(f64, f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::linalg::spherical_to_cartesian(r, theta, phi)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// Returns (rho, phi, z) where rho is the radial distance in the xy-plane and phi is the azimuthal angle from +x. +/// +/// Rust: `linalg::cartesian_to_cylindrical` +#[pyfunction] +#[pyo3(name = "cartesian_to_cylindrical", signature = (x, y, z))] +pub fn pyfn_cartesian_to_cylindrical(x: f64, y: f64, z: f64) -> PyResult<(f64, f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::linalg::cartesian_to_cylindrical(x, y, z)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// Converts cylindrical coordinates (rho, phi, z) to Cartesian (x, y, z). +/// +/// Rust: `linalg::cylindrical_to_cartesian` +#[pyfunction] +#[pyo3(name = "cylindrical_to_cartesian", signature = (rho, phi, z))] +pub fn pyfn_cylindrical_to_cartesian(rho: f64, phi: f64, z: f64) -> PyResult<(f64, f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::linalg::cylindrical_to_cartesian(rho, phi, z)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// Converts 2D polar coordinates (r, theta) to Cartesian (x, y). +/// +/// Rust: `linalg::polar_to_cartesian` +#[pyfunction] +#[pyo3(name = "polar_to_cartesian", signature = (r, theta))] +pub fn pyfn_polar_to_cartesian(r: f64, theta: f64) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::linalg::polar_to_cartesian(r, theta)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Returns (r, theta) where theta is the angle from +x. +/// +/// Rust: `linalg::cartesian_to_polar` +#[pyfunction] +#[pyo3(name = "cartesian_to_polar", signature = (x, y))] +pub fn pyfn_cartesian_to_polar(x: f64, y: f64) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::linalg::cartesian_to_polar(x, y)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_rotation_x, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rotation_y, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rotation_z, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rotation_axis_angle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cartesian_to_spherical, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spherical_to_cartesian, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cartesian_to_cylindrical, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cylindrical_to_cartesian, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_polar_to_cartesian, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cartesian_to_polar, m)?)?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_linalg__cholesky.rs b/bindings/python/src/generated/m_linalg__cholesky.rs new file mode 100644 index 0000000..f8361cc --- /dev/null +++ b/bindings/python/src/generated/m_linalg__cholesky.rs @@ -0,0 +1,55 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Factors a symmetric positive-definite matrix as A = L·Lᵀ, returning +/// the lower-triangular factor L. +/// +/// Returns `InvalidArgument` for non-square or asymmetric input and +/// `NotPositiveDefinite` when a diagonal pivot is not strictly positive. +/// +/// Rust: `linalg::cholesky::cholesky` +#[pyfunction] +#[pyo3(name = "cholesky", signature = (a))] +pub fn pyfn_cholesky(a: crate::generated::types::PyMatrixArg) -> PyResult { + let a = a.0; + let __r = crate::runtime::guard(|| rust_physics_engine::linalg::cholesky::cholesky(&a)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) +} + +/// Solves A·x = b given the Cholesky factor L of A (A = L·Lᵀ), by one +/// forward and one back substitution. +/// +/// Rust: `linalg::cholesky::cholesky_solve` +#[pyfunction] +#[pyo3(name = "cholesky_solve", signature = (l, b))] +pub fn pyfn_cholesky_solve<'py>(py: Python<'py>, l: crate::generated::types::PyMatrixArg, b: Vec) -> PyResult> { + let l = l.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::linalg::cholesky::cholesky_solve(&l, &b))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_cholesky, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cholesky_solve, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_linalg__eigen.rs b/bindings/python/src/generated/m_linalg__eigen.rs new file mode 100644 index 0000000..679a220 --- /dev/null +++ b/bindings/python/src/generated/m_linalg__eigen.rs @@ -0,0 +1,59 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Cyclic Jacobi eigen-decomposition of a symmetric matrix. +/// +/// Sweeps Givens rotations over all off-diagonal pairs until the +/// off-diagonal Frobenius norm falls below `tol` (relative to ‖A‖) or +/// `max_sweeps` is exhausted, in which case `NoConvergence` is returned. +/// +/// Rust: `linalg::eigen::eigen_symmetric` +#[pyfunction] +#[pyo3(name = "eigen_symmetric", signature = (a, tol, max_sweeps))] +pub fn pyfn_eigen_symmetric(a: crate::generated::types::PyMatrixArg, tol: f64, max_sweeps: usize) -> PyResult { + let a = a.0; + let __r = crate::runtime::guard(|| rust_physics_engine::linalg::eigen::eigen_symmetric(&a, tol, max_sweeps)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(crate::generated::types::PySymEigen { inner: __v }) +} + +/// All eigenvalues (possibly complex) of a general real square matrix, +/// by Hessenberg reduction followed by Francis-shift QR iteration. +/// +/// `max_iter` bounds the QR iterations spent per eigenvalue (30 is the +/// classical choice). +/// +/// Rust: `linalg::eigen::eigenvalues_general` +#[pyfunction] +#[pyo3(name = "eigenvalues_general", signature = (a, max_iter))] +pub fn pyfn_eigenvalues_general<'py>(py: Python<'py>, a: crate::generated::types::PyMatrixArg, max_iter: usize) -> PyResult>> { + let a = a.0; + let __r = crate::runtime::guard(|| rust_physics_engine::linalg::eigen::eigenvalues_general(&a, max_iter)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_eigen_symmetric, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_eigenvalues_general, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_linalg__lu.rs b/bindings/python/src/generated/m_linalg__lu.rs new file mode 100644 index 0000000..1620e37 --- /dev/null +++ b/bindings/python/src/generated/m_linalg__lu.rs @@ -0,0 +1,54 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Factors a square matrix as P·A = L·U with partial (row) pivoting. +/// +/// Returns `SolveError::InvalidArgument` for non-square input and +/// `SolveError::Singular` when a pivot falls below the threshold. +/// +/// Rust: `linalg::lu::lu_decompose` +#[pyfunction] +#[pyo3(name = "lu_decompose", signature = (a))] +pub fn pyfn_lu_decompose(a: crate::generated::types::PyMatrixArg) -> PyResult { + let a = a.0; + let __r = crate::runtime::guard(|| rust_physics_engine::linalg::lu::lu_decompose(&a)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(crate::generated::types::PyLu { inner: __v }) +} + +/// Convenience wrapper: factor `a` and solve A·x = b in one call. +/// +/// Rust: `linalg::lu::solve` +#[pyfunction] +#[pyo3(name = "solve", signature = (a, b))] +pub fn pyfn_solve<'py>(py: Python<'py>, a: crate::generated::types::PyMatrixArg, b: Vec) -> PyResult> { + let a = a.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::linalg::lu::solve(&a, &b))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_lu_decompose, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_solve, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_linalg__matrix.rs b/bindings/python/src/generated/m_linalg__matrix.rs new file mode 100644 index 0000000..79213f1 --- /dev/null +++ b/bindings/python/src/generated/m_linalg__matrix.rs @@ -0,0 +1,23 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_linalg__qr.rs b/bindings/python/src/generated/m_linalg__qr.rs new file mode 100644 index 0000000..3a19f07 --- /dev/null +++ b/bindings/python/src/generated/m_linalg__qr.rs @@ -0,0 +1,56 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Factors A (m×n) as Q·R using Householder reflections. +/// +/// Q is m×m orthogonal; R is m×n with zeros below the diagonal. +/// +/// Rust: `linalg::qr::qr_householder` +#[pyfunction] +#[pyo3(name = "qr_householder", signature = (a))] +pub fn pyfn_qr_householder(a: crate::generated::types::PyMatrixArg) -> PyResult { + let a = a.0; + let __r = crate::runtime::guard(|| rust_physics_engine::linalg::qr::qr_householder(&a)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQr { inner: __v }) +} + +/// Solves the least-squares problem min ‖A·x − b‖₂ via QR. +/// +/// Requires m ≥ n and full column rank; returns `Singular` when R has a +/// negligible diagonal entry, `DimensionMismatch` when `b.len() != m`, +/// and `InvalidArgument` when the system is underdetermined (m < n). +/// +/// Rust: `linalg::qr::least_squares` +#[pyfunction] +#[pyo3(name = "least_squares", signature = (a, b))] +pub fn pyfn_least_squares<'py>(py: Python<'py>, a: crate::generated::types::PyMatrixArg, b: Vec) -> PyResult> { + let a = a.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::linalg::qr::least_squares(&a, &b))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_qr_householder, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_least_squares, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_linalg__sparse.rs b/bindings/python/src/generated/m_linalg__sparse.rs new file mode 100644 index 0000000..7fcac26 --- /dev/null +++ b/bindings/python/src/generated/m_linalg__sparse.rs @@ -0,0 +1,58 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Conjugate gradient for SPD systems A·x = b starting from `x0`. +/// +/// Converges when ‖r‖₂ ≤ tol·max(‖b‖₂, 1); returns `NoConvergence` +/// with the final residual otherwise. +/// +/// Rust: `linalg::sparse::conjugate_gradient` +#[pyfunction] +#[pyo3(name = "conjugate_gradient", signature = (a, b, x0, tol, max_iter))] +pub fn pyfn_conjugate_gradient<'py>(py: Python<'py>, a: crate::generated::types::PyCsrMatrix, b: Vec, x0: Vec, tol: f64, max_iter: usize) -> PyResult> { + let a = a.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::linalg::sparse::conjugate_gradient(&a, &b, &x0, tol, max_iter))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// Jacobi (diagonal) preconditioned conjugate gradient with x₀ = 0. +/// +/// Requires strictly positive diagonal entries (fails with +/// `NotPositiveDefinite` otherwise). Convergence criterion matches +/// `conjugate_gradient`. +/// +/// Rust: `linalg::sparse::pcg_jacobi` +#[pyfunction] +#[pyo3(name = "pcg_jacobi", signature = (a, b, tol, max_iter))] +pub fn pyfn_pcg_jacobi<'py>(py: Python<'py>, a: crate::generated::types::PyCsrMatrix, b: Vec, tol: f64, max_iter: usize) -> PyResult> { + let a = a.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::linalg::sparse::pcg_jacobi(&a, &b, tol, max_iter))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_conjugate_gradient, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pcg_jacobi, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_linalg__svd.rs b/bindings/python/src/generated/m_linalg__svd.rs new file mode 100644 index 0000000..bad6ccd --- /dev/null +++ b/bindings/python/src/generated/m_linalg__svd.rs @@ -0,0 +1,86 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// One-sided Jacobi SVD of an m×n matrix with m ≥ n; for m < n the +/// transpose is factored and the roles of U and V are swapped. +/// +/// Rust: `linalg::svd::svd` +#[pyfunction] +#[pyo3(name = "svd", signature = (a))] +pub fn pyfn_svd(a: crate::generated::types::PyMatrixArg) -> PyResult { + let a = a.0; + let __r = crate::runtime::guard(|| rust_physics_engine::linalg::svd::svd(&a)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(crate::generated::types::PySvd { inner: __v }) +} + +/// Moore-Penrose pseudoinverse A⁺ = V·Σ⁺·Uᵀ; singular values below +/// `rcond · σ_max` are treated as zero. +/// +/// Rust: `linalg::svd::pseudoinverse` +#[pyfunction] +#[pyo3(name = "pseudoinverse", signature = (a, rcond))] +pub fn pyfn_pseudoinverse(a: crate::generated::types::PyMatrixArg, rcond: f64) -> PyResult { + let a = a.0; + let __r = crate::runtime::guard(|| rust_physics_engine::linalg::svd::pseudoinverse(&a, rcond)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) +} + +/// Numerical rank: the number of singular values greater than `tol`. +/// +/// Rust: `linalg::svd::rank` +#[pyfunction] +#[pyo3(name = "rank", signature = (a, tol))] +pub fn pyfn_rank<'py>(py: Python<'py>, a: crate::generated::types::PyMatrixArg, tol: f64) -> PyResult { + let a = a.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::linalg::svd::rank(&a, tol))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Kabsch algorithm: the rotation R minimizing Σ‖R·pᵢ − qᵢ‖². +/// +/// Both point sets are used as given (no centroid subtraction); center +/// them first for the usual superposition problem. Fails with +/// `DimensionMismatch` when the sets differ in length and +/// `InvalidArgument` when fewer than 3 points are supplied. +/// +/// Rust: `linalg::svd::kabsch` +#[pyfunction] +#[pyo3(name = "kabsch", signature = (p, q))] +pub fn pyfn_kabsch(p: Vec, q: Vec) -> PyResult { + let p = p.into_iter().map(|__e| __e.0).collect::>(); + let q = q.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::linalg::svd::kabsch(&p, &q)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(crate::generated::types::PyMat3 { inner: __v }) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_svd, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pseudoinverse, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rank, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kabsch, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_linalg__tridiagonal.rs b/bindings/python/src/generated/m_linalg__tridiagonal.rs new file mode 100644 index 0000000..0b169f1 --- /dev/null +++ b/bindings/python/src/generated/m_linalg__tridiagonal.rs @@ -0,0 +1,58 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Solves a tridiagonal system with the Thomas algorithm. +/// +/// `diag` and `rhs` have length n; `sub` (below-diagonal) and `sup` +/// (above-diagonal) have length n−1. Numerically stable for diagonally +/// dominant or symmetric positive-definite systems. +/// +/// Rust: `linalg::tridiagonal::thomas_solve` +#[pyfunction] +#[pyo3(name = "thomas_solve", signature = (sub, diag, sup, rhs))] +pub fn pyfn_thomas_solve<'py>(py: Python<'py>, sub: Vec, diag: Vec, sup: Vec, rhs: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::linalg::tridiagonal::thomas_solve(&sub, &diag, &sup, &rhs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// Eigen-decomposition of a symmetric tridiagonal matrix by the QL +/// algorithm with implicit shifts (EISPACK `tql2`): returns eigenvalues +/// (ascending) and matching orthonormal eigenvectors. +/// +/// Errors: +/// Returns `DimensionMismatch` for inconsistent inputs and +/// `NoConvergence` if an eigenvalue fails to settle in 50 iterations. +/// +/// Rust: `linalg::tridiagonal::eigen_symmetric_tridiagonal` +#[pyfunction] +#[pyo3(name = "eigen_symmetric_tridiagonal", signature = (diag, off))] +pub fn pyfn_eigen_symmetric_tridiagonal<'py>(py: Python<'py>, diag: Vec, off: Vec) -> PyResult<(Vec, Vec>)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::linalg::tridiagonal::eigen_symmetric_tridiagonal(&diag, &off))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok((__v.0, __v.1)) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_thomas_solve, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_eigen_symmetric_tridiagonal, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_magnetohydrodynamics.rs b/bindings/python/src/generated/m_magnetohydrodynamics.rs new file mode 100644 index 0000000..cd841b4 --- /dev/null +++ b/bindings/python/src/generated/m_magnetohydrodynamics.rs @@ -0,0 +1,256 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Magnetic Reynolds number: Rm = μ₀ σ v L. +/// Quantifies the ratio of magnetic advection to diffusion. +/// +/// Rust: `magnetohydrodynamics::magnetic_reynolds_number` +#[pyfunction] +#[pyo3(name = "magnetic_reynolds_number", signature = (velocity, length, conductivity))] +pub fn pyfn_magnetic_reynolds_number(velocity: f64, length: f64, conductivity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::magnetohydrodynamics::magnetic_reynolds_number(velocity, length, conductivity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Magnetic diffusivity: η = 1/(μ₀ σ). +/// +/// Rust: `magnetohydrodynamics::magnetic_diffusivity` +#[pyfunction] +#[pyo3(name = "magnetic_diffusivity", signature = (conductivity))] +pub fn pyfn_magnetic_diffusivity(conductivity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::magnetohydrodynamics::magnetic_diffusivity(conductivity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Lundquist number: S = vₐ L / η. +/// Ratio of resistive diffusion time to Alfvén transit time. +/// +/// Rust: `magnetohydrodynamics::lundquist_number` +#[pyfunction] +#[pyo3(name = "lundquist_number", signature = (alfven_speed, length, diffusivity))] +pub fn pyfn_lundquist_number(alfven_speed: f64, length: f64, diffusivity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::magnetohydrodynamics::lundquist_number(alfven_speed, length, diffusivity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Hartmann number: Ha = B L √(σ / μ_visc). +/// Ratio of electromagnetic to viscous forces in a conducting fluid. +/// +/// Rust: `magnetohydrodynamics::hartmann_number` +#[pyfunction] +#[pyo3(name = "hartmann_number", signature = (b_field, length, conductivity, dynamic_viscosity))] +pub fn pyfn_hartmann_number(b_field: f64, length: f64, conductivity: f64, dynamic_viscosity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::magnetohydrodynamics::hartmann_number(b_field, length, conductivity, dynamic_viscosity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Magnetic pressure: P_B = B² / (2 μ₀). +/// +/// Rust: `magnetohydrodynamics::magnetic_pressure` +#[pyfunction] +#[pyo3(name = "magnetic_pressure", signature = (b_field))] +pub fn pyfn_magnetic_pressure(b_field: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::magnetohydrodynamics::magnetic_pressure(b_field)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Total pressure (gas + magnetic): P_total = P + B² / (2 μ₀). +/// +/// Rust: `magnetohydrodynamics::total_pressure` +#[pyfunction] +#[pyo3(name = "total_pressure", signature = (gas_pressure, b_field))] +pub fn pyfn_total_pressure(gas_pressure: f64, b_field: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::magnetohydrodynamics::total_pressure(gas_pressure, b_field)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Plasma beta: β = 2 μ₀ P / B². +/// Ratio of gas pressure to magnetic pressure. +/// +/// Rust: `magnetohydrodynamics::plasma_beta` +#[pyfunction] +#[pyo3(name = "plasma_beta", signature = (gas_pressure, b_field))] +pub fn pyfn_plasma_beta(gas_pressure: f64, b_field: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::magnetohydrodynamics::plasma_beta(gas_pressure, b_field)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Alfvén speed: vₐ = B / √(μ₀ ρ). +/// +/// Rust: `magnetohydrodynamics::alfven_speed` +#[pyfunction] +#[pyo3(name = "alfven_speed", signature = (b_field, density))] +pub fn pyfn_alfven_speed(b_field: f64, density: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::magnetohydrodynamics::alfven_speed(b_field, density)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Slow magnetosonic speed (perpendicular propagation): v_slow = min(vₐ, cₛ). +/// +/// Rust: `magnetohydrodynamics::slow_magnetosonic_speed` +#[pyfunction] +#[pyo3(name = "slow_magnetosonic_speed", signature = (alfven, sound))] +pub fn pyfn_slow_magnetosonic_speed(alfven: f64, sound: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::magnetohydrodynamics::slow_magnetosonic_speed(alfven, sound)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Fast magnetosonic speed (perpendicular propagation): v_fast = √(vₐ² + cₛ²). +/// +/// Rust: `magnetohydrodynamics::fast_magnetosonic_speed` +#[pyfunction] +#[pyo3(name = "fast_magnetosonic_speed", signature = (alfven, sound))] +pub fn pyfn_fast_magnetosonic_speed(alfven: f64, sound: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::magnetohydrodynamics::fast_magnetosonic_speed(alfven, sound)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Magnetosonic Mach number: M_ms = v / v_fast. +/// +/// Rust: `magnetohydrodynamics::magnetosonic_mach` +#[pyfunction] +#[pyo3(name = "magnetosonic_mach", signature = (velocity, alfven, sound))] +pub fn pyfn_magnetosonic_mach(velocity: f64, alfven: f64, sound: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::magnetohydrodynamics::magnetosonic_mach(velocity, alfven, sound)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Z-pinch pressure balance. +/// Computes the magnetic pressure from the azimuthal field B_θ = μ₀ I / (2π r). +/// +/// Rust: `magnetohydrodynamics::pinch_pressure_balance` +#[pyfunction] +#[pyo3(name = "pinch_pressure_balance", signature = (current, radius))] +pub fn pyfn_pinch_pressure_balance(current: f64, radius: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::magnetohydrodynamics::pinch_pressure_balance(current, radius)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Bennett pinch condition: checks whether I² ≈ 8π N k_B T / μ₀. +/// Returns `true` when the plasma is in pressure balance. +/// +/// Rust: `magnetohydrodynamics::bennett_pinch_condition` +#[pyfunction] +#[pyo3(name = "bennett_pinch_condition", signature = (current, line_density, temperature))] +pub fn pyfn_bennett_pinch_condition(current: f64, line_density: f64, temperature: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::magnetohydrodynamics::bennett_pinch_condition(current, line_density, temperature)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Rough Troyon-like beta limit: β_max ≈ 1 / aspect_ratio. +/// +/// Rust: `magnetohydrodynamics::grad_shafranov_beta_limit` +#[pyfunction] +#[pyo3(name = "grad_shafranov_beta_limit", signature = (aspect_ratio))] +pub fn pyfn_grad_shafranov_beta_limit(aspect_ratio: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::magnetohydrodynamics::grad_shafranov_beta_limit(aspect_ratio)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Sweet-Parker reconnection rate: v_in / vₐ = 1 / √S. +/// +/// Rust: `magnetohydrodynamics::sweet_parker_rate` +#[pyfunction] +#[pyo3(name = "sweet_parker_rate", signature = (alfven_speed, lundquist))] +pub fn pyfn_sweet_parker_rate(alfven_speed: f64, lundquist: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::magnetohydrodynamics::sweet_parker_rate(alfven_speed, lundquist)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Reconnection electric field: E = v_in × B (magnitude). +/// +/// Rust: `magnetohydrodynamics::reconnection_electric_field` +#[pyfunction] +#[pyo3(name = "reconnection_electric_field", signature = (b_field, inflow_velocity))] +pub fn pyfn_reconnection_electric_field(b_field: f64, inflow_velocity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::magnetohydrodynamics::reconnection_electric_field(b_field, inflow_velocity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Magnetic diffusion time: τ_d = L² / η. +/// +/// Rust: `magnetohydrodynamics::magnetic_diffusion_time` +#[pyfunction] +#[pyo3(name = "magnetic_diffusion_time", signature = (length, diffusivity))] +pub fn pyfn_magnetic_diffusion_time(length: f64, diffusivity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::magnetohydrodynamics::magnetic_diffusion_time(length, diffusivity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Advection time: τ_a = L / v. +/// +/// Rust: `magnetohydrodynamics::advection_time` +#[pyfunction] +#[pyo3(name = "advection_time", signature = (length, velocity))] +pub fn pyfn_advection_time(length: f64, velocity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::magnetohydrodynamics::advection_time(length, velocity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Returns `true` when Rm > 100, indicating the magnetic field is frozen into the plasma. +/// +/// Rust: `magnetohydrodynamics::is_frozen_in` +#[pyfunction] +#[pyo3(name = "is_frozen_in", signature = (reynolds_mag))] +pub fn pyfn_is_frozen_in(reynolds_mag: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::magnetohydrodynamics::is_frozen_in(reynolds_mag)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_magnetic_reynolds_number, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_magnetic_diffusivity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lundquist_number, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hartmann_number, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_magnetic_pressure, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_total_pressure, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_plasma_beta, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_alfven_speed, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_slow_magnetosonic_speed, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fast_magnetosonic_speed, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_magnetosonic_mach, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pinch_pressure_balance, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bennett_pinch_condition, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_grad_shafranov_beta_limit, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sweet_parker_rate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_reconnection_electric_field, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_magnetic_diffusion_time, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_advection_time, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_is_frozen_in, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_manifold.rs b/bindings/python/src/generated/m_manifold.rs new file mode 100644 index 0000000..c02bbf8 --- /dev/null +++ b/bindings/python/src/generated/m_manifold.rs @@ -0,0 +1,22 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + Ok(()) +} diff --git a/bindings/python/src/generated/m_manifold__clifford.rs b/bindings/python/src/generated/m_manifold__clifford.rs new file mode 100644 index 0000000..d5047b5 --- /dev/null +++ b/bindings/python/src/generated/m_manifold__clifford.rs @@ -0,0 +1,71 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Basis-blade multiplication table: `table[a][b] = (sign, result mask)`. +/// +/// Rust: `manifold::clifford::cayley_table` +#[pyfunction] +#[pyo3(name = "cayley_table", signature = (p, q, r))] +pub fn pyfn_cayley_table<'py>(py: Python<'py>, p: usize, q: usize, r: usize) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::manifold::clifford::cayley_table(p, q, r))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.into_iter().map(|__x| (__x.0, __x.1)).collect::>()).collect::>()) +} + +/// Name of a basis blade, e.g. "e12" (1-indexed factors). +/// +/// Rust: `manifold::clifford::blade_name` +#[pyfunction] +#[pyo3(name = "blade_name", signature = (mask, p, q, r))] +pub fn pyfn_blade_name(mask: usize, p: usize, q: usize, r: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::blade_name(mask, p, q, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_string()) +} + +/// Dimension of the algebra: 2^(p+q+r). +/// +/// Rust: `manifold::clifford::algebra_dimension` +#[pyfunction] +#[pyo3(name = "algebra_dimension", signature = (p, q, r))] +pub fn pyfn_algebra_dimension(p: usize, q: usize, r: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::algebra_dimension(p, q, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Classification of small Clifford algebras by isomorphism type. +/// +/// Rust: `manifold::clifford::is_isomorphic_to_known` +#[pyfunction] +#[pyo3(name = "is_isomorphic_to_known", signature = (p, q, r))] +pub fn pyfn_is_isomorphic_to_known(p: usize, q: usize, r: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::is_isomorphic_to_known(p, q, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_string()) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_cayley_table, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_blade_name, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_algebra_dimension, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_is_isomorphic_to_known, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_manifold__clifford__cga3.rs b/bindings/python/src/generated/m_manifold__clifford__cga3.rs new file mode 100644 index 0000000..8517f4f --- /dev/null +++ b/bindings/python/src/generated/m_manifold__clifford__cga3.rs @@ -0,0 +1,521 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// The null vector at infinity. +/// +/// Rust: `manifold::clifford::cga3::e_inf` +#[pyfunction] +#[pyo3(name = "e_inf", signature = ())] +pub fn pyfn_e_inf() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cga3::e_inf()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// The null origin vector. +/// +/// Rust: `manifold::clifford::cga3::e_0` +#[pyfunction] +#[pyo3(name = "e_0", signature = ())] +pub fn pyfn_e_0() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cga3::e_0()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// The positive-signature extra basis vector. +/// +/// Rust: `manifold::clifford::cga3::e_plus` +#[pyfunction] +#[pyo3(name = "e_plus", signature = ())] +pub fn pyfn_e_plus() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cga3::e_plus()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// The negative-signature extra basis vector. +/// +/// Rust: `manifold::clifford::cga3::e_minus` +#[pyfunction] +#[pyo3(name = "e_minus", signature = ())] +pub fn pyfn_e_minus() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cga3::e_minus()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// Conformal up-projection of a Euclidean point: +/// P = p + (1/2) p^2 e_inf + e_0. +/// +/// Rust: `manifold::clifford::cga3::point` +#[pyfunction] +#[pyo3(name = "point", signature = (p))] +pub fn pyfn_point(p: crate::generated::types::PyVec3Arg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cga3::point(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// Euclidean coordinates of a conformal point (None for ideal points). +/// +/// Rust: `manifold::clifford::cga3::down` +#[pyfunction] +#[pyo3(name = "down", signature = (x))] +pub fn pyfn_down(x: crate::generated::types::PyMultivector) -> PyResult> { + let x = x.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cga3::down(&x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyVec3 { inner: __x })) +} + +/// IPNS sphere with the given center and radius. +/// +/// Rust: `manifold::clifford::cga3::sphere` +#[pyfunction] +#[pyo3(name = "sphere", signature = (center, r))] +pub fn pyfn_sphere(center: crate::generated::types::PyVec3Arg, r: f64) -> PyResult { + let center = center.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cga3::sphere(center, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// IPNS plane n . x = d (unit normal recommended). +/// +/// Rust: `manifold::clifford::cga3::plane` +#[pyfunction] +#[pyo3(name = "plane", signature = (n, d))] +pub fn pyfn_plane(n: crate::generated::types::PyVec3Arg, d: f64) -> PyResult { + let n = n.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cga3::plane(n, d)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// IPNS circle through three points. +/// +/// Rust: `manifold::clifford::cga3::circle_from_points` +#[pyfunction] +#[pyo3(name = "circle_from_points", signature = (a, b, c))] +pub fn pyfn_circle_from_points(a: crate::generated::types::PyVec3Arg, b: crate::generated::types::PyVec3Arg, c: crate::generated::types::PyVec3Arg) -> PyResult { + let a = a.0; + let b = b.0; + let c = c.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cga3::circle_from_points(a, b, c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// IPNS line through two points. +/// +/// Rust: `manifold::clifford::cga3::line_from_points` +#[pyfunction] +#[pyo3(name = "line_from_points", signature = (a, b))] +pub fn pyfn_line_from_points(a: crate::generated::types::PyVec3Arg, b: crate::generated::types::PyVec3Arg) -> PyResult { + let a = a.0; + let b = b.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cga3::line_from_points(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// IPNS point pair. +/// +/// Rust: `manifold::clifford::cga3::point_pair` +#[pyfunction] +#[pyo3(name = "point_pair", signature = (a, b))] +pub fn pyfn_point_pair(a: crate::generated::types::PyVec3Arg, b: crate::generated::types::PyVec3Arg) -> PyResult { + let a = a.0; + let b = b.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cga3::point_pair(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// IPNS sphere through four points. +/// +/// Rust: `manifold::clifford::cga3::sphere_from_points` +#[pyfunction] +#[pyo3(name = "sphere_from_points", signature = (a, b, c, d))] +pub fn pyfn_sphere_from_points(a: crate::generated::types::PyVec3Arg, b: crate::generated::types::PyVec3Arg, c: crate::generated::types::PyVec3Arg, d: crate::generated::types::PyVec3Arg) -> PyResult { + let a = a.0; + let b = b.0; + let c = c.0; + let d = d.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cga3::sphere_from_points(a, b, c, d)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// Meet of IPNS objects (their intersection): the outer product. +/// +/// Rust: `manifold::clifford::cga3::meet` +#[pyfunction] +#[pyo3(name = "meet", signature = (a, b))] +pub fn pyfn_meet(a: crate::generated::types::PyMultivector, b: crate::generated::types::PyMultivector) -> PyResult { + let a = a.inner; + let b = b.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cga3::meet(&a, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// Center, radius, and plane normal of an IPNS circle. +/// +/// Rust: `manifold::clifford::cga3::circle_center_radius_normal` +#[pyfunction] +#[pyo3(name = "circle_center_radius_normal", signature = (c))] +pub fn pyfn_circle_center_radius_normal(c: crate::generated::types::PyMultivector) -> PyResult<(crate::generated::types::PyVec3, f64, crate::generated::types::PyVec3)> { + let c = c.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cga3::circle_center_radius_normal(&c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((crate::generated::types::PyVec3 { inner: __v.0 }, __v.1, crate::generated::types::PyVec3 { inner: __v.2 })) +} + +/// Center and radius of an IPNS sphere. +/// +/// Rust: `manifold::clifford::cga3::sphere_center_radius` +#[pyfunction] +#[pyo3(name = "sphere_center_radius", signature = (s))] +pub fn pyfn_sphere_center_radius(s: crate::generated::types::PyMultivector) -> PyResult<(crate::generated::types::PyVec3, f64)> { + let s = s.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cga3::sphere_center_radius(&s)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((crate::generated::types::PyVec3 { inner: __v.0 }, __v.1)) +} + +/// A point on an IPNS line and its direction. +/// +/// Rust: `manifold::clifford::cga3::line_point_direction` +#[pyfunction] +#[pyo3(name = "line_point_direction", signature = (l))] +pub fn pyfn_line_point_direction(l: crate::generated::types::PyMultivector) -> PyResult<(crate::generated::types::PyVec3, crate::generated::types::PyVec3)> { + let l = l.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cga3::line_point_direction(&l)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((crate::generated::types::PyVec3 { inner: __v.0 }, crate::generated::types::PyVec3 { inner: __v.1 })) +} + +/// Normal and offset of an IPNS plane (n . x = d). +/// +/// Rust: `manifold::clifford::cga3::plane_normal_distance` +#[pyfunction] +#[pyo3(name = "plane_normal_distance", signature = (pl))] +pub fn pyfn_plane_normal_distance(pl: crate::generated::types::PyMultivector) -> PyResult<(crate::generated::types::PyVec3, f64)> { + let pl = pl.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cga3::plane_normal_distance(&pl)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((crate::generated::types::PyVec3 { inner: __v.0 }, __v.1)) +} + +/// Classify an IPNS object by grade and flatness. +/// +/// Rust: `manifold::clifford::cga3::classify` +#[pyfunction] +#[pyo3(name = "classify", signature = (x))] +pub fn pyfn_classify(x: crate::generated::types::PyMultivector) -> PyResult { + let x = x.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cga3::classify(&x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCgaObject::from_rust(&__v)) +} + +/// Euclidean distance between two conformal points: d^2 = -2 A . B. +/// +/// Rust: `manifold::clifford::cga3::distance` +#[pyfunction] +#[pyo3(name = "distance", signature = (a, b))] +pub fn pyfn_distance(a: crate::generated::types::PyMultivector, b: crate::generated::types::PyMultivector) -> PyResult { + let a = a.inner; + let b = b.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cga3::distance(&a, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// True when the point lies strictly inside the sphere. +/// +/// Rust: `manifold::clifford::cga3::is_inside_sphere` +#[pyfunction] +#[pyo3(name = "is_inside_sphere", signature = (p, s))] +pub fn pyfn_is_inside_sphere(p: crate::generated::types::PyMultivector, s: crate::generated::types::PyMultivector) -> PyResult { + let p = p.inner; + let s = s.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cga3::is_inside_sphere(&p, &s)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Translator versor: T = 1 - (1/2) t e_inf. +/// +/// Rust: `manifold::clifford::cga3::translator` +#[pyfunction] +#[pyo3(name = "translator", signature = (t))] +pub fn pyfn_translator(t: crate::generated::types::PyVec3Arg) -> PyResult { + let t = t.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cga3::translator(t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// Euclidean rotor about an axis through the origin. +/// +/// Rust: `manifold::clifford::cga3::rotor` +#[pyfunction] +#[pyo3(name = "rotor", signature = (axis, angle))] +pub fn pyfn_rotor(axis: crate::generated::types::PyVec3Arg, angle: f64) -> PyResult { + let axis = axis.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cga3::rotor(axis, angle)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// Dilator scaling by `scale` about the origin. +/// +/// Rust: `manifold::clifford::cga3::dilator` +#[pyfunction] +#[pyo3(name = "dilator", signature = (scale))] +pub fn pyfn_dilator(scale: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cga3::dilator(scale)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// Transversor (special conformal) versor. +/// +/// Rust: `manifold::clifford::cga3::transversor` +#[pyfunction] +#[pyo3(name = "transversor", signature = (v))] +pub fn pyfn_transversor(v: crate::generated::types::PyVec3Arg) -> PyResult { + let v = v.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cga3::transversor(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// Inversion versor in a sphere (the sphere itself acts by sandwich). +/// +/// Rust: `manifold::clifford::cga3::inversion_in_sphere` +#[pyfunction] +#[pyo3(name = "inversion_in_sphere", signature = (s))] +pub fn pyfn_inversion_in_sphere(s: crate::generated::types::PyMultivector) -> PyResult { + let s = s.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cga3::inversion_in_sphere(&s)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// Rigid motor: translation then rotation. +/// +/// Rust: `manifold::clifford::cga3::motor` +#[pyfunction] +#[pyo3(name = "motor", signature = (t, axis, angle))] +pub fn pyfn_motor(t: crate::generated::types::PyVec3Arg, axis: crate::generated::types::PyVec3Arg, angle: f64) -> PyResult { + let t = t.0; + let axis = axis.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cga3::motor(t, axis, angle)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// Conformal versor for a similarity transform. +/// +/// Rust: `manifold::clifford::cga3::conformal_from_similarity` +#[pyfunction] +#[pyo3(name = "conformal_from_similarity", signature = (s))] +pub fn pyfn_conformal_from_similarity(s: crate::generated::types::PySim3) -> PyResult { + let s = s.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cga3::conformal_from_similarity(&s)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// Apply a versor by the sandwich product (with the grade involution +/// for odd versors such as spheres and planes). +/// +/// Rust: `manifold::clifford::cga3::apply` +#[pyfunction] +#[pyo3(name = "apply", signature = (versor, x))] +pub fn pyfn_apply(versor: crate::generated::types::PyMultivector, x: crate::generated::types::PyMultivector) -> PyResult { + let versor = versor.inner; + let x = x.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cga3::apply(&versor, &x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// Sphere inversion as a reflection: S X S normalized. +/// +/// Rust: `manifold::clifford::cga3::reflect_in_sphere` +#[pyfunction] +#[pyo3(name = "reflect_in_sphere", signature = (x, s))] +pub fn pyfn_reflect_in_sphere(x: crate::generated::types::PyMultivector, s: crate::generated::types::PyMultivector) -> PyResult { + let x = x.inner; + let s = s.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cga3::reflect_in_sphere(&x, &s)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// Linear versor interpolation with renormalization. +/// +/// Rust: `manifold::clifford::cga3::interpolate_versor` +#[pyfunction] +#[pyo3(name = "interpolate_versor", signature = (a, b, t))] +pub fn pyfn_interpolate_versor(a: crate::generated::types::PyMultivector, b: crate::generated::types::PyMultivector, t: f64) -> PyResult { + let a = a.inner; + let b = b.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cga3::interpolate_versor(&a, &b, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// Apollonius problem: spheres tangent to three given spheres (solved +/// in Euclidean form, returned as IPNS spheres). +/// +/// Rust: `manifold::clifford::cga3::apollonius_problem` +#[pyfunction] +#[pyo3(name = "apollonius_problem", signature = (c1, c2, c3))] +pub fn pyfn_apollonius_problem(c1: crate::generated::types::PyMultivector, c2: crate::generated::types::PyMultivector, c3: crate::generated::types::PyMultivector) -> PyResult> { + let c1 = c1.inner; + let c2 = c2.inner; + let c3 = c3.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cga3::apollonius_problem(&c1, &c2, &c3)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyMultivector { inner: __x }).collect::>()) +} + +/// The circle in which two spheres intersect. +/// +/// Rust: `manifold::clifford::cga3::circle_through_intersection` +#[pyfunction] +#[pyo3(name = "circle_through_intersection", signature = (s1, s2))] +pub fn pyfn_circle_through_intersection(s1: crate::generated::types::PyMultivector, s2: crate::generated::types::PyMultivector) -> PyResult { + let s1 = s1.inner; + let s2 = s2.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cga3::circle_through_intersection(&s1, &s2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// Tangent plane to a sphere at a point on it. +/// +/// Rust: `manifold::clifford::cga3::tangent_at` +#[pyfunction] +#[pyo3(name = "tangent_at", signature = (surface, pt))] +pub fn pyfn_tangent_at(surface: crate::generated::types::PyMultivector, pt: crate::generated::types::PyMultivector) -> PyResult { + let surface = surface.inner; + let pt = pt.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cga3::tangent_at(&surface, &pt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// The flat carrier of a round: the plane containing a circle. +/// +/// Rust: `manifold::clifford::cga3::carrier` +#[pyfunction] +#[pyo3(name = "carrier", signature = (x))] +pub fn pyfn_carrier(x: crate::generated::types::PyMultivector) -> PyResult { + let x = x.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cga3::carrier(&x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// IPNS <-> OPNS dualization. +/// +/// Rust: `manifold::clifford::cga3::dual_cga` +#[pyfunction] +#[pyo3(name = "dual_cga", signature = (x))] +pub fn pyfn_dual_cga(x: crate::generated::types::PyMultivector) -> PyResult { + let x = x.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cga3::dual_cga(&x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// Length of the tangent from a point to a sphere. +/// +/// Rust: `manifold::clifford::cga3::point_to_sphere_tangent_distance` +#[pyfunction] +#[pyo3(name = "point_to_sphere_tangent_distance", signature = (p, s))] +pub fn pyfn_point_to_sphere_tangent_distance(p: crate::generated::types::PyMultivector, s: crate::generated::types::PyMultivector) -> PyResult { + let p = p.inner; + let s = s.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cga3::point_to_sphere_tangent_distance(&p, &s)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Inverse stereographic projection R3 -> S3 via the conformal model. +/// +/// Rust: `manifold::clifford::cga3::stereographic_via_cga` +#[pyfunction] +#[pyo3(name = "stereographic_via_cga", signature = (p))] +pub fn pyfn_stereographic_via_cga(p: crate::generated::types::PyVec3Arg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cga3::stereographic_via_cga(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec4 { inner: __v }) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_e_inf, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_e_0, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_e_plus, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_e_minus, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_point, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_down, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sphere, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_plane, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_circle_from_points, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_line_from_points, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_point_pair, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sphere_from_points, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_meet, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_circle_center_radius_normal, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sphere_center_radius, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_line_point_direction, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_plane_normal_distance, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_classify, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_distance, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_is_inside_sphere, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_translator, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rotor, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dilator, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_transversor, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_inversion_in_sphere, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_motor, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_conformal_from_similarity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_apply, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_reflect_in_sphere, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_interpolate_versor, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_apollonius_problem, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_circle_through_intersection, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tangent_at, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_carrier, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dual_cga, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_point_to_sphere_tangent_distance, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_stereographic_via_cga, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_manifold__clifford__cl3.rs b/bindings/python/src/generated/m_manifold__clifford__cl3.rs new file mode 100644 index 0000000..861ef4f --- /dev/null +++ b/bindings/python/src/generated/m_manifold__clifford__cl3.rs @@ -0,0 +1,172 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Grade-1 vector. +/// +/// Rust: `manifold::clifford::cl3::vec` +#[pyfunction] +#[pyo3(name = "vec", signature = (v))] +pub fn pyfn_vec(v: crate::generated::types::PyVec3Arg) -> PyResult { + let v = v.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cl3::vec(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// Bivector dual to the vector b (the plane with normal b). +/// +/// Rust: `manifold::clifford::cl3::bivec` +#[pyfunction] +#[pyo3(name = "bivec", signature = (b))] +pub fn pyfn_bivec(b: crate::generated::types::PyVec3Arg) -> PyResult { + let b = b.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cl3::bivec(b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// The pseudoscalar e123. +/// +/// Rust: `manifold::clifford::cl3::pseudoscalar` +#[pyfunction] +#[pyo3(name = "pseudoscalar", signature = ())] +pub fn pyfn_pseudoscalar() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cl3::pseudoscalar()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// Rotor for a rotation about `axis` by `angle` (matches quaternion +/// rotation). +/// +/// Rust: `manifold::clifford::cl3::rotor` +#[pyfunction] +#[pyo3(name = "rotor", signature = (axis, angle))] +pub fn pyfn_rotor(axis: crate::generated::types::PyVec3Arg, angle: f64) -> PyResult { + let axis = axis.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cl3::rotor(axis, angle)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// Rotate a vector with a rotor: R v R~. +/// +/// Rust: `manifold::clifford::cl3::rotate` +#[pyfunction] +#[pyo3(name = "rotate", signature = (v, r))] +pub fn pyfn_rotate(v: crate::generated::types::PyVec3Arg, r: crate::generated::types::PyMultivector) -> PyResult { + let v = v.0; + let r = r.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cl3::rotate(v, &r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// The cross product via the wedge: a x b = -I (a ∧ b). +/// +/// Rust: `manifold::clifford::cl3::cross_via_wedge` +#[pyfunction] +#[pyo3(name = "cross_via_wedge", signature = (a, b))] +pub fn pyfn_cross_via_wedge(a: crate::generated::types::PyVec3Arg, b: crate::generated::types::PyVec3Arg) -> PyResult { + let a = a.0; + let b = b.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cl3::cross_via_wedge(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Reflect v in the plane with unit normal n. +/// +/// Rust: `manifold::clifford::cl3::reflect` +#[pyfunction] +#[pyo3(name = "reflect", signature = (v, n))] +pub fn pyfn_reflect(v: crate::generated::types::PyVec3Arg, n: crate::generated::types::PyVec3Arg) -> PyResult { + let v = v.0; + let n = n.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cl3::reflect(v, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Extract the grade-1 part as a Vec3 (None if other grades dominate). +/// +/// Rust: `manifold::clifford::cl3::to_vec3` +#[pyfunction] +#[pyo3(name = "to_vec3", signature = (m))] +pub fn pyfn_to_vec3(m: crate::generated::types::PyMultivector) -> PyResult> { + let m = m.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cl3::to_vec3(&m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyVec3 { inner: __x })) +} + +/// The plane (bivector) through three points, with weight twice the +/// triangle area. +/// +/// Rust: `manifold::clifford::cl3::plane_from_points` +#[pyfunction] +#[pyo3(name = "plane_from_points", signature = (a, b, c))] +pub fn pyfn_plane_from_points(a: crate::generated::types::PyVec3Arg, b: crate::generated::types::PyVec3Arg, c: crate::generated::types::PyVec3Arg) -> PyResult { + let a = a.0; + let b = b.0; + let c = c.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cl3::plane_from_points(a, b, c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// The line direction blade through two points (their difference). +/// +/// Rust: `manifold::clifford::cl3::line_from_points` +#[pyfunction] +#[pyo3(name = "line_from_points", signature = (a, b))] +pub fn pyfn_line_from_points(a: crate::generated::types::PyVec3Arg, b: crate::generated::types::PyVec3Arg) -> PyResult { + let a = a.0; + let b = b.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cl3::line_from_points(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// Rotor as quaternion. +/// +/// Rust: `manifold::clifford::cl3::rotor_to_quaternion` +#[pyfunction] +#[pyo3(name = "rotor_to_quaternion", signature = (r))] +pub fn pyfn_rotor_to_quaternion(r: crate::generated::types::PyMultivector) -> PyResult> { + let r = r.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::cl3::rotor_to_quaternion(&r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyQuaternion { inner: __x })) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_vec, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bivec, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pseudoscalar, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rotor, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rotate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cross_via_wedge, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_reflect, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_to_vec3, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_plane_from_points, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_line_from_points, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rotor_to_quaternion, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_manifold__clifford__pga3.rs b/bindings/python/src/generated/m_manifold__clifford__pga3.rs new file mode 100644 index 0000000..093768d --- /dev/null +++ b/bindings/python/src/generated/m_manifold__clifford__pga3.rs @@ -0,0 +1,409 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// The plane n . x + d = 0 as a grade-1 element. +/// +/// Rust: `manifold::clifford::pga3::plane` +#[pyfunction] +#[pyo3(name = "plane", signature = (n, d))] +pub fn pyfn_plane(n: crate::generated::types::PyVec3Arg, d: f64) -> PyResult { + let n = n.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::pga3::plane(n, d)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// A Euclidean point as the meet of three axis-aligned planes. +/// +/// Rust: `manifold::clifford::pga3::point` +#[pyfunction] +#[pyo3(name = "point", signature = (p))] +pub fn pyfn_point(p: crate::generated::types::PyVec3Arg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::pga3::point(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// Ideal (infinite) point in direction d. +/// +/// Rust: `manifold::clifford::pga3::point_at_infinity` +#[pyfunction] +#[pyo3(name = "point_at_infinity", signature = (d))] +pub fn pyfn_point_at_infinity(d: crate::generated::types::PyVec3Arg) -> PyResult { + let d = d.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::pga3::point_at_infinity(d)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// Line through two points (their join). +/// +/// Rust: `manifold::clifford::pga3::line_from_points` +#[pyfunction] +#[pyo3(name = "line_from_points", signature = (a, b))] +pub fn pyfn_line_from_points(a: crate::generated::types::PyVec3Arg, b: crate::generated::types::PyVec3Arg) -> PyResult { + let a = a.0; + let b = b.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::pga3::line_from_points(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// Line as the meet of two planes. +/// +/// Rust: `manifold::clifford::pga3::line_from_planes` +#[pyfunction] +#[pyo3(name = "line_from_planes", signature = (p, q))] +pub fn pyfn_line_from_planes(p: crate::generated::types::PyMultivector, q: crate::generated::types::PyMultivector) -> PyResult { + let p = p.inner; + let q = q.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::pga3::line_from_planes(&p, &q)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// Plane through three points (their join). +/// +/// Rust: `manifold::clifford::pga3::plane_from_points` +#[pyfunction] +#[pyo3(name = "plane_from_points", signature = (a, b, c))] +pub fn pyfn_plane_from_points(a: crate::generated::types::PyVec3Arg, b: crate::generated::types::PyVec3Arg, c: crate::generated::types::PyVec3Arg) -> PyResult { + let a = a.0; + let b = b.0; + let c = c.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::pga3::plane_from_points(a, b, c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// Meet (intersection): the outer product in the plane-based algebra. +/// +/// Rust: `manifold::clifford::pga3::meet` +#[pyfunction] +#[pyo3(name = "meet", signature = (a, b))] +pub fn pyfn_meet(a: crate::generated::types::PyMultivector, b: crate::generated::types::PyMultivector) -> PyResult { + let a = a.inner; + let b = b.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::pga3::meet(&a, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// Join (span): the regressive product. +/// +/// Rust: `manifold::clifford::pga3::join` +#[pyfunction] +#[pyo3(name = "join", signature = (a, b))] +pub fn pyfn_join(a: crate::generated::types::PyMultivector, b: crate::generated::types::PyMultivector) -> PyResult { + let a = a.inner; + let b = b.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::pga3::join(&a, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// Euclidean coordinates of a (normalized or unnormalized) point. +/// +/// Rust: `manifold::clifford::pga3::to_vec3` +#[pyfunction] +#[pyo3(name = "to_vec3", signature = (p))] +pub fn pyfn_to_vec3(p: crate::generated::types::PyMultivector) -> PyResult> { + let p = p.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::pga3::to_vec3(&p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyVec3 { inner: __x })) +} + +/// True for ideal (infinite) elements: zero weight. +/// +/// Rust: `manifold::clifford::pga3::is_ideal` +#[pyfunction] +#[pyo3(name = "is_ideal", signature = (x))] +pub fn pyfn_is_ideal(x: crate::generated::types::PyMultivector) -> PyResult { + let x = x.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::pga3::is_ideal(&x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Signed distance from a point to a plane (both normalized inside). +/// +/// Rust: `manifold::clifford::pga3::distance_point_plane` +#[pyfunction] +#[pyo3(name = "distance_point_plane", signature = (p, pl))] +pub fn pyfn_distance_point_plane(p: crate::generated::types::PyMultivector, pl: crate::generated::types::PyMultivector) -> PyResult { + let p = p.inner; + let pl = pl.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::pga3::distance_point_plane(&p, &pl)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Distance from a point to a line. +/// +/// Rust: `manifold::clifford::pga3::distance_point_line` +#[pyfunction] +#[pyo3(name = "distance_point_line", signature = (p, l))] +pub fn pyfn_distance_point_line(p: crate::generated::types::PyMultivector, l: crate::generated::types::PyMultivector) -> PyResult { + let p = p.inner; + let l = l.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::pga3::distance_point_line(&p, &l)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Distance between two lines. +/// +/// Rust: `manifold::clifford::pga3::distance_lines` +#[pyfunction] +#[pyo3(name = "distance_lines", signature = (l1, l2))] +pub fn pyfn_distance_lines(l1: crate::generated::types::PyMultivector, l2: crate::generated::types::PyMultivector) -> PyResult { + let l1 = l1.inner; + let l2 = l2.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::pga3::distance_lines(&l1, &l2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Angle between two planes. +/// +/// Rust: `manifold::clifford::pga3::angle_planes` +#[pyfunction] +#[pyo3(name = "angle_planes", signature = (p, q))] +pub fn pyfn_angle_planes(p: crate::generated::types::PyMultivector, q: crate::generated::types::PyMultivector) -> PyResult { + let p = p.inner; + let q = q.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::pga3::angle_planes(&p, &q)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Angle between two lines. +/// +/// Rust: `manifold::clifford::pga3::angle_lines` +#[pyfunction] +#[pyo3(name = "angle_lines", signature = (l1, l2))] +pub fn pyfn_angle_lines(l1: crate::generated::types::PyMultivector, l2: crate::generated::types::PyMultivector) -> PyResult { + let l1 = l1.inner; + let l2 = l2.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::pga3::angle_lines(&l1, &l2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Motor translating by t. +/// +/// Rust: `manifold::clifford::pga3::motor_translation` +#[pyfunction] +#[pyo3(name = "motor_translation", signature = (t))] +pub fn pyfn_motor_translation(t: crate::generated::types::PyVec3Arg) -> PyResult { + let t = t.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::pga3::motor_translation(t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// Motor rotating by `angle` about an axis line. +/// +/// Rust: `manifold::clifford::pga3::motor_rotation` +#[pyfunction] +#[pyo3(name = "motor_rotation", signature = (axis_line, angle))] +pub fn pyfn_motor_rotation(axis_line: crate::generated::types::PyMultivector, angle: f64) -> PyResult { + let axis_line = axis_line.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::pga3::motor_rotation(&axis_line, angle)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// Screw motor: rotate by `angle` about the line while translating +/// `dist` along it. +/// +/// Rust: `manifold::clifford::pga3::motor_screw` +#[pyfunction] +#[pyo3(name = "motor_screw", signature = (line, angle, dist))] +pub fn pyfn_motor_screw(line: crate::generated::types::PyMultivector, angle: f64, dist: f64) -> PyResult { + let line = line.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::pga3::motor_screw(&line, angle, dist)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// Motor from a rigid transform. +/// +/// Rust: `manifold::clifford::pga3::motor_from_se3` +#[pyfunction] +#[pyo3(name = "motor_from_se3", signature = (m))] +pub fn pyfn_motor_from_se3(m: crate::generated::types::PySe3) -> PyResult { + let m = m.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::pga3::motor_from_se3(&m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// Rigid transform from a motor. +/// +/// Rust: `manifold::clifford::pga3::motor_to_se3` +#[pyfunction] +#[pyo3(name = "motor_to_se3", signature = (m))] +pub fn pyfn_motor_to_se3(m: crate::generated::types::PyMultivector) -> PyResult { + let m = m.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::pga3::motor_to_se3(&m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySe3 { inner: __v }) +} + +/// Screw interpolation between motors (through Se3's exact screw). +/// +/// Rust: `manifold::clifford::pga3::motor_interpolate` +#[pyfunction] +#[pyo3(name = "motor_interpolate", signature = (a, b, t))] +pub fn pyfn_motor_interpolate(a: crate::generated::types::PyMultivector, b: crate::generated::types::PyMultivector, t: f64) -> PyResult { + let a = a.inner; + let b = b.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::pga3::motor_interpolate(&a, &b, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// Apply a motor by the sandwich product. +/// +/// Rust: `manifold::clifford::pga3::motor_apply` +#[pyfunction] +#[pyo3(name = "motor_apply", signature = (m, x))] +pub fn pyfn_motor_apply(m: crate::generated::types::PyMultivector, x: crate::generated::types::PyMultivector) -> PyResult { + let m = m.inner; + let x = x.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::pga3::motor_apply(&m, &x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// Orthogonal projection of a point onto a line. +/// +/// Rust: `manifold::clifford::pga3::project_point_on_line` +#[pyfunction] +#[pyo3(name = "project_point_on_line", signature = (p, l))] +pub fn pyfn_project_point_on_line(p: crate::generated::types::PyMultivector, l: crate::generated::types::PyMultivector) -> PyResult { + let p = p.inner; + let l = l.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::pga3::project_point_on_line(&p, &l)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// Orthogonal projection of a point onto a plane. +/// +/// Rust: `manifold::clifford::pga3::project_point_on_plane` +#[pyfunction] +#[pyo3(name = "project_point_on_plane", signature = (p, pl))] +pub fn pyfn_project_point_on_plane(p: crate::generated::types::PyMultivector, pl: crate::generated::types::PyMultivector) -> PyResult { + let p = p.inner; + let pl = pl.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::pga3::project_point_on_plane(&p, &pl)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// Orthogonal projection of a line onto a plane. +/// +/// Rust: `manifold::clifford::pga3::project_line_on_plane` +#[pyfunction] +#[pyo3(name = "project_line_on_plane", signature = (l, pl))] +pub fn pyfn_project_line_on_plane(l: crate::generated::types::PyMultivector, pl: crate::generated::types::PyMultivector) -> PyResult { + let l = l.inner; + let pl = pl.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::pga3::project_line_on_plane(&l, &pl)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// One explicit step of PGA rigid-body dynamics (Gunn): the motor +/// advances by its body-frame rate bivector. +/// +/// Rust: `manifold::clifford::pga3::rigid_body_step` +#[pyfunction] +#[pyo3(name = "rigid_body_step", signature = (motor, rate, dt))] +pub fn pyfn_rigid_body_step(motor: pyo3::PyRefMut<'_, crate::generated::types::PyMultivector>, rate: crate::generated::types::PyMultivector, dt: f64) -> PyResult<()> { + let mut motor = motor; + let rate = rate.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::pga3::rigid_body_step(&mut motor.inner, &rate, dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) +} + +/// Diagonal inertia map on body-rate bivectors: scales the rotational +/// components by (ixx, iyy, izz) and the translational by the mass. +/// +/// Rust: `manifold::clifford::pga3::inertia_dual_map` +#[pyfunction] +#[pyo3(name = "inertia_dual_map", signature = (rate, inertia, mass))] +pub fn pyfn_inertia_dual_map(rate: crate::generated::types::PyMultivector, inertia: Vec, mass: f64) -> PyResult { + let rate = rate.inner; + let inertia = <[f64; 3]>::try_from(inertia).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 3 values, got {}", __v.len())))?; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::pga3::inertia_dual_map(&rate, inertia, mass)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// Forque (force + torque) bivector of a force applied at a point: the +/// weighted line through the point in the force direction. +/// +/// Rust: `manifold::clifford::pga3::forque` +#[pyfunction] +#[pyo3(name = "forque", signature = (force, application_point))] +pub fn pyfn_forque(force: crate::generated::types::PyVec3Arg, application_point: crate::generated::types::PyVec3Arg) -> PyResult { + let force = force.0; + let application_point = application_point.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::pga3::forque(force, application_point)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_plane, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_point, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_point_at_infinity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_line_from_points, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_line_from_planes, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_plane_from_points, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_meet, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_join, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_to_vec3, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_is_ideal, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_distance_point_plane, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_distance_point_line, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_distance_lines, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_angle_planes, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_angle_lines, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_motor_translation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_motor_rotation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_motor_screw, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_motor_from_se3, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_motor_to_se3, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_motor_interpolate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_motor_apply, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_project_point_on_line, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_project_point_on_plane, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_project_line_on_plane, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rigid_body_step, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_inertia_dual_map, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_forque, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_manifold__clifford__sta.rs b/bindings/python/src/generated/m_manifold__clifford__sta.rs new file mode 100644 index 0000000..9d46af0 --- /dev/null +++ b/bindings/python/src/generated/m_manifold__clifford__sta.rs @@ -0,0 +1,183 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Spacetime event t gamma_0 + x . gamma. +/// +/// Rust: `manifold::clifford::sta::event` +#[pyfunction] +#[pyo3(name = "event", signature = (t, x))] +pub fn pyfn_event(t: f64, x: crate::generated::types::PyVec3Arg) -> PyResult { + let x = x.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::sta::event(t, x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// Boost rotor for velocity v (|v| < 1, c = 1). +/// +/// Rust: `manifold::clifford::sta::boost` +#[pyfunction] +#[pyo3(name = "boost", signature = (v))] +pub fn pyfn_boost(v: crate::generated::types::PyVec3Arg) -> PyResult { + let v = v.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::sta::boost(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// Spatial rotation rotor. +/// +/// Rust: `manifold::clifford::sta::rotation` +#[pyfunction] +#[pyo3(name = "rotation", signature = (axis, angle))] +pub fn pyfn_rotation(axis: crate::generated::types::PyVec3Arg, angle: f64) -> PyResult { + let axis = axis.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::sta::rotation(axis, angle)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// Apply a Lorentz rotor to an event: R e R~. +/// +/// Rust: `manifold::clifford::sta::lorentz_apply` +#[pyfunction] +#[pyo3(name = "lorentz_apply", signature = (r, e))] +pub fn pyfn_lorentz_apply(r: crate::generated::types::PyMultivector, e: crate::generated::types::PyMultivector) -> PyResult { + let r = r.inner; + let e = e.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::sta::lorentz_apply(&r, &e)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// Faraday bivector F = E . sigma + I B . sigma. +/// +/// Rust: `manifold::clifford::sta::bivector_em` +#[pyfunction] +#[pyo3(name = "bivector_em", signature = (e_field, b_field))] +pub fn pyfn_bivector_em(e_field: crate::generated::types::PyVec3Arg, b_field: crate::generated::types::PyVec3Arg) -> PyResult { + let e_field = e_field.0; + let b_field = b_field.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::sta::bivector_em(e_field, b_field)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// Electromagnetic invariants from F^2 = (E^2 - B^2) + 2 (E . B) I: +/// returns (E^2 - B^2, E . B). +/// +/// Rust: `manifold::clifford::sta::em_invariants` +#[pyfunction] +#[pyo3(name = "em_invariants", signature = (f))] +pub fn pyfn_em_invariants(f: crate::generated::types::PyMultivector) -> PyResult<(f64, f64)> { + let f = f.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::sta::em_invariants(&f)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Lorentz force: dp/dtau = q F . v (grade-1 contraction), for a +/// particle of charge q and mass m returns the 4-acceleration. +/// +/// Rust: `manifold::clifford::sta::lorentz_force_sta` +#[pyfunction] +#[pyo3(name = "lorentz_force_sta", signature = (f, velocity, q, m))] +pub fn pyfn_lorentz_force_sta(f: crate::generated::types::PyMultivector, velocity: crate::generated::types::PyMultivector, q: f64, m: f64) -> PyResult { + let f = f.inner; + let velocity = velocity.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::sta::lorentz_force_sta(&f, &velocity, q, m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// Proper time along a piecewise-linear worldline of events. +/// +/// Rust: `manifold::clifford::sta::proper_time` +#[pyfunction] +#[pyo3(name = "proper_time", signature = (path))] +pub fn pyfn_proper_time<'py>(py: Python<'py>, path: Vec) -> PyResult { + let path = path.into_iter().map(|__e| __e.inner).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::manifold::clifford::sta::proper_time(&path))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Rapidity of a speed: atanh(v). +/// +/// Rust: `manifold::clifford::sta::rapidity` +#[pyfunction] +#[pyo3(name = "rapidity", signature = (v))] +pub fn pyfn_rapidity(v: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::sta::rapidity(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Split an event into (time, space) relative to an observer 4-velocity +/// (default observer: gamma_0). +/// +/// Rust: `manifold::clifford::sta::spacetime_split` +#[pyfunction] +#[pyo3(name = "spacetime_split", signature = (x, observer))] +pub fn pyfn_spacetime_split(x: crate::generated::types::PyMultivector, observer: crate::generated::types::PyMultivector) -> PyResult<(f64, crate::generated::types::PyVec3)> { + let x = x.inner; + let observer = observer.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::sta::spacetime_split(&x, &observer)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, crate::generated::types::PyVec3 { inner: __v.1 })) +} + +/// The Dirac gamma matrices (Dirac basis) as 4x4 complex matrices. +/// +/// Rust: `manifold::clifford::sta::dirac_gamma_matrices` +#[pyfunction] +#[pyo3(name = "dirac_gamma_matrices", signature = ())] +pub fn pyfn_dirac_gamma_matrices<'py>(py: Python<'py>) -> PyResult>>>> { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::sta::dirac_gamma_matrices()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.into_iter().map(|__x| __x.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()).collect::>()).collect::>()) +} + +/// Map a Pauli/quaternion rotation to the STA spatial rotor. +/// +/// Rust: `manifold::clifford::sta::pauli_to_sta` +#[pyfunction] +#[pyo3(name = "pauli_to_sta", signature = (q))] +pub fn pyfn_pauli_to_sta(q: crate::generated::types::PyQuaternionArg) -> PyResult { + let q = q.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::sta::pauli_to_sta(&q)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_event, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_boost, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rotation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lorentz_apply, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bivector_em, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_em_invariants, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lorentz_force_sta, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_proper_time, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rapidity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spacetime_split, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dirac_gamma_matrices, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pauli_to_sta, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_manifold__dec.rs b/bindings/python/src/generated/m_manifold__dec.rs new file mode 100644 index 0000000..c52554e --- /dev/null +++ b/bindings/python/src/generated/m_manifold__dec.rs @@ -0,0 +1,68 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Vietoris-Rips persistent homology in dimensions 0 and 1: returns +/// (dimension, birth, death) pairs (essential classes get death = +/// `max_eps`). +/// +/// Rust: `manifold::dec::persistent_homology_vietoris_rips` +#[pyfunction] +#[pyo3(name = "persistent_homology_vietoris_rips", signature = (points, max_eps, max_dim))] +pub fn pyfn_persistent_homology_vietoris_rips<'py>(py: Python<'py>, points: Vec, max_eps: f64, max_dim: usize) -> PyResult> { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::manifold::dec::persistent_homology_vietoris_rips(&points, max_eps, max_dim))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, __x.2)).collect::>()) +} + +/// Bottleneck distance between two persistence diagrams (same dimension), +/// by binary search over candidate distances with greedy augmenting-path +/// matching (diagonal projections allowed). +/// +/// Rust: `manifold::dec::persistence_diagram_bottleneck` +#[pyfunction] +#[pyo3(name = "persistence_diagram_bottleneck", signature = (a, b))] +pub fn pyfn_persistence_diagram_bottleneck<'py>(py: Python<'py>, a: Vec<(f64, f64)>, b: Vec<(f64, f64)>) -> PyResult { + let a = a.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let b = b.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::manifold::dec::persistence_diagram_bottleneck(&a, &b))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Betti curve: Betti numbers (dims 0..2) as a function of the filtration +/// parameter, from the persistence pairs. +/// +/// Rust: `manifold::dec::betti_curve` +#[pyfunction] +#[pyo3(name = "betti_curve", signature = (pairs, eps_range))] +pub fn pyfn_betti_curve<'py>(py: Python<'py>, pairs: Vec<(usize, f64, f64)>, eps_range: Vec) -> PyResult)>> { + let pairs = pairs.into_iter().map(|__e| (__e.0, __e.1, __e.2)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::manifold::dec::betti_curve(&pairs, &eps_range))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1.to_vec())).collect::>()) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_persistent_homology_vietoris_rips, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_persistence_diagram_bottleneck, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_betti_curve, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_manifold__embedding.rs b/bindings/python/src/generated/m_manifold__embedding.rs new file mode 100644 index 0000000..9229e18 --- /dev/null +++ b/bindings/python/src/generated/m_manifold__embedding.rs @@ -0,0 +1,564 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Pairwise Euclidean distance matrix. +/// +/// Rust: `manifold::embedding::dist_matrix` +#[pyfunction] +#[pyo3(name = "dist_matrix", signature = (points))] +pub fn pyfn_dist_matrix(points: Vec) -> PyResult { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::embedding::dist_matrix(&points)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) +} + +/// Classical (Torgerson) multidimensional scaling from a distance matrix. +/// +/// Rust: `manifold::embedding::classical_mds` +#[pyfunction] +#[pyo3(name = "classical_mds", signature = (dist, dim))] +pub fn pyfn_classical_mds(dist: crate::generated::types::PyMatrixArg, dim: usize) -> PyResult> { + let dist = dist.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::embedding::classical_mds(&dist, dim)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVecN { inner: __x }).collect::>()) +} + +/// Metric MDS by SMACOF stress majorization. Returns (embedding, stress). +/// +/// Rust: `manifold::embedding::metric_mds_smacof` +#[pyfunction] +#[pyo3(name = "metric_mds_smacof", signature = (dist, dim, iters, rng))] +pub fn pyfn_metric_mds_smacof(dist: crate::generated::types::PyMatrixArg, dim: usize, iters: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(Vec, f64)> { + let dist = dist.0; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::embedding::metric_mds_smacof(&dist, dim, iters, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0.into_iter().map(|__x| crate::generated::types::PyVecN { inner: __x }).collect::>(), __v.1)) +} + +/// Nonmetric MDS: SMACOF against monotone-regressed disparities. +/// +/// Rust: `manifold::embedding::nonmetric_mds` +#[pyfunction] +#[pyo3(name = "nonmetric_mds", signature = (dist, dim, iters, rng))] +pub fn pyfn_nonmetric_mds(dist: crate::generated::types::PyMatrixArg, dim: usize, iters: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let dist = dist.0; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::embedding::nonmetric_mds(&dist, dim, iters, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVecN { inner: __x }).collect::>()) +} + +/// k-nearest-neighbor graph: for each point, its k neighbors and distances. +/// +/// Rust: `manifold::embedding::knn_graph` +#[pyfunction] +#[pyo3(name = "knn_graph", signature = (points, k))] +pub fn pyfn_knn_graph<'py>(py: Python<'py>, points: Vec, k: usize) -> PyResult>> { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::manifold::embedding::knn_graph(&points, k))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.into_iter().map(|__x| (__x.0, __x.1)).collect::>()).collect::>()) +} + +/// All-pairs shortest paths over a kNN graph (Floyd-Warshall; symmetrized). +/// +/// Rust: `manifold::embedding::geodesic_distance_matrix` +#[pyfunction] +#[pyo3(name = "geodesic_distance_matrix", signature = (knn))] +pub fn pyfn_geodesic_distance_matrix(knn: Vec>) -> PyResult { + let knn = knn.into_iter().map(|__e| __e.into_iter().map(|__e| (__e.0, __e.1)).collect::>()).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::embedding::geodesic_distance_matrix(&knn)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) +} + +/// Isomap: geodesic distances over the kNN graph fed to classical MDS. +/// +/// Rust: `manifold::embedding::isomap` +#[pyfunction] +#[pyo3(name = "isomap", signature = (points, k_neighbors, dim))] +pub fn pyfn_isomap(points: Vec, k_neighbors: usize, dim: usize) -> PyResult> { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::embedding::isomap(&points, k_neighbors, dim)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVecN { inner: __x }).collect::>()) +} + +/// Locally linear embedding. +/// +/// Rust: `manifold::embedding::lle` +#[pyfunction] +#[pyo3(name = "lle", signature = (points, k, dim, reg))] +pub fn pyfn_lle(points: Vec, k: usize, dim: usize, reg: f64) -> PyResult> { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::embedding::lle(&points, k, dim, reg)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVecN { inner: __x }).collect::>()) +} + +/// Laplacian eigenmaps with heat-kernel weights. +/// +/// Rust: `manifold::embedding::laplacian_eigenmaps` +#[pyfunction] +#[pyo3(name = "laplacian_eigenmaps", signature = (points, k, dim, sigma))] +pub fn pyfn_laplacian_eigenmaps(points: Vec, k: usize, dim: usize, sigma: f64) -> PyResult> { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::embedding::laplacian_eigenmaps(&points, k, dim, sigma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVecN { inner: __x }).collect::>()) +} + +/// Diffusion maps: eigenfunctions of the diffusion operator, scaled by +/// lambda^t. +/// +/// Rust: `manifold::embedding::diffusion_maps` +#[pyfunction] +#[pyo3(name = "diffusion_maps", signature = (points, eps, dim, t))] +pub fn pyfn_diffusion_maps(points: Vec, eps: f64, dim: usize, t: f64) -> PyResult> { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::embedding::diffusion_maps(&points, eps, dim, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVecN { inner: __x }).collect::>()) +} + +/// Spectral embedding of a graph adjacency matrix. +/// +/// Rust: `manifold::embedding::spectral_embedding` +#[pyfunction] +#[pyo3(name = "spectral_embedding", signature = (adjacency, dim))] +pub fn pyfn_spectral_embedding(adjacency: crate::generated::types::PyMatrixArg, dim: usize) -> PyResult> { + let adjacency = adjacency.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::embedding::spectral_embedding(&adjacency, dim)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVecN { inner: __x }).collect::>()) +} + +/// Principal component analysis: returns (projected points, explained +/// variance per component, components as rows). +/// +/// Rust: `manifold::embedding::pca` +#[pyfunction] +#[pyo3(name = "pca", signature = (points, dim))] +pub fn pyfn_pca(points: Vec, dim: usize) -> PyResult<(Vec, Vec, crate::generated::types::PyMatrix)> { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::embedding::pca(&points, dim)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0.into_iter().map(|__x| crate::generated::types::PyVecN { inner: __x }).collect::>(), __v.1, crate::generated::types::PyMatrix { inner: __v.2 })) +} + +/// Kernel PCA with a user-supplied kernel. +/// +/// Rust: `manifold::embedding::kernel_pca` +#[pyfunction] +#[pyo3(name = "kernel_pca", signature = (points, kernel, dim))] +pub fn pyfn_kernel_pca(points: Vec, kernel: pyo3::Py, dim: usize) -> PyResult> { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let __cb_kernel = std::rc::Rc::new(crate::runtime::Callback::new(kernel)); + let kernel = { let __cb = __cb_kernel.clone(); move |__a0: &rust_physics_engine::manifold::vecn::VecN, __a1: &rust_physics_engine::manifold::vecn::VecN| -> f64 { __cb.call::<_, f64>((crate::generated::types::PyVecN { inner: __a0.clone() }, crate::generated::types::PyVecN { inner: __a1.clone() }), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::embedding::kernel_pca(&points, &kernel, dim)); + crate::runtime::callback::check(&[&__cb_kernel], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVecN { inner: __x }).collect::>()) +} + +/// t-SNE (exact gradients; suitable for small point sets). +/// +/// Rust: `manifold::embedding::tsne` +#[pyfunction] +#[pyo3(name = "tsne", signature = (points, dim, perplexity, iters, lr, rng))] +pub fn pyfn_tsne(points: Vec, dim: usize, perplexity: f64, iters: usize, lr: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::embedding::tsne(&points, dim, perplexity, iters, lr, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVecN { inner: __x }).collect::>()) +} + +/// Lightweight UMAP: fuzzy kNN weights optimized by SGD attraction and +/// random-negative repulsion. +/// +/// Rust: `manifold::embedding::umap_lite` +#[pyfunction] +#[pyo3(name = "umap_lite", signature = (points, k, dim, min_dist, epochs, rng))] +pub fn pyfn_umap_lite(points: Vec, k: usize, dim: usize, min_dist: f64, epochs: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::embedding::umap_lite(&points, k, dim, min_dist, epochs, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVecN { inner: __x }).collect::>()) +} + +/// Levina-Bickel maximum-likelihood intrinsic dimension using k neighbors. +/// +/// Rust: `manifold::embedding::intrinsic_dimension_mle` +#[pyfunction] +#[pyo3(name = "intrinsic_dimension_mle", signature = (points, k))] +pub fn pyfn_intrinsic_dimension_mle<'py>(py: Python<'py>, points: Vec, k: usize) -> PyResult { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::manifold::embedding::intrinsic_dimension_mle(&points, k))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Correlation-dimension estimate: log-log slope of the correlation +/// integral over the radius range. +/// +/// Rust: `manifold::embedding::intrinsic_dimension_correlation` +#[pyfunction] +#[pyo3(name = "intrinsic_dimension_correlation", signature = (points, r_range))] +pub fn pyfn_intrinsic_dimension_correlation<'py>(py: Python<'py>, points: Vec, r_range: (f64, f64)) -> PyResult { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let r_range = (r_range.0, r_range.1); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::manifold::embedding::intrinsic_dimension_correlation(&points, r_range))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// TwoNN intrinsic dimension (Facco et al.): d = n / sum ln(r2/r1). +/// +/// Rust: `manifold::embedding::intrinsic_dimension_two_nn` +#[pyfunction] +#[pyo3(name = "intrinsic_dimension_two_nn", signature = (points))] +pub fn pyfn_intrinsic_dimension_two_nn<'py>(py: Python<'py>, points: Vec) -> PyResult { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::manifold::embedding::intrinsic_dimension_two_nn(&points))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Trustworthiness of a low-dimensional embedding (1 = perfect). +/// +/// Rust: `manifold::embedding::trustworthiness` +#[pyfunction] +#[pyo3(name = "trustworthiness", signature = (high, low, k))] +pub fn pyfn_trustworthiness<'py>(py: Python<'py>, high: Vec, low: Vec, k: usize) -> PyResult { + let high = high.into_iter().map(|__e| __e.0).collect::>(); + let low = low.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::manifold::embedding::trustworthiness(&high, &low, k))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Continuity of an embedding (trustworthiness with roles swapped). +/// +/// Rust: `manifold::embedding::continuity` +#[pyfunction] +#[pyo3(name = "continuity", signature = (high, low, k))] +pub fn pyfn_continuity<'py>(py: Python<'py>, high: Vec, low: Vec, k: usize) -> PyResult { + let high = high.into_iter().map(|__e| __e.0).collect::>(); + let low = low.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::manifold::embedding::continuity(&high, &low, k))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Kruskal stress between two distance matrices. +/// +/// Rust: `manifold::embedding::stress` +#[pyfunction] +#[pyo3(name = "stress", signature = (dist_high, dist_low))] +pub fn pyfn_stress<'py>(py: Python<'py>, dist_high: crate::generated::types::PyMatrixArg, dist_low: crate::generated::types::PyMatrixArg) -> PyResult { + let dist_high = dist_high.0; + let dist_low = dist_low.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::manifold::embedding::stress(&dist_high, &dist_low))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Fraction of k-nearest neighbors preserved by the embedding. +/// +/// Rust: `manifold::embedding::neighborhood_preservation` +#[pyfunction] +#[pyo3(name = "neighborhood_preservation", signature = (high, low, k))] +pub fn pyfn_neighborhood_preservation<'py>(py: Python<'py>, high: Vec, low: Vec, k: usize) -> PyResult { + let high = high.into_iter().map(|__e| __e.0).collect::>(); + let low = low.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::manifold::embedding::neighborhood_preservation(&high, &low, k))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Procrustes alignment of b onto a (rotation + scale + translation); +/// returns (aligned b, residual). +/// +/// Rust: `manifold::embedding::procrustes_align` +#[pyfunction] +#[pyo3(name = "procrustes_align", signature = (a, b))] +pub fn pyfn_procrustes_align(a: Vec, b: Vec) -> PyResult<(Vec, f64)> { + let a = a.into_iter().map(|__e| __e.0).collect::>(); + let b = b.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::embedding::procrustes_align(&a, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0.into_iter().map(|__x| crate::generated::types::PyVecN { inner: __x }).collect::>(), __v.1)) +} + +/// Swiss roll in R3 with the unrolled arc-length parameter as ground truth. +/// +/// Rust: `manifold::embedding::swiss_roll` +#[pyfunction] +#[pyo3(name = "swiss_roll", signature = (n, noise, rng))] +pub fn pyfn_swiss_roll(n: usize, noise: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(Vec, Vec)> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::embedding::swiss_roll(n, noise, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0.into_iter().map(|__x| crate::generated::types::PyVecN { inner: __x }).collect::>(), __v.1)) +} + +/// S-curve dataset with the curve parameter as ground truth. +/// +/// Rust: `manifold::embedding::s_curve` +#[pyfunction] +#[pyo3(name = "s_curve", signature = (n, noise, rng))] +pub fn pyfn_s_curve(n: usize, noise: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(Vec, Vec)> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::embedding::s_curve(n, noise, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0.into_iter().map(|__x| crate::generated::types::PyVecN { inner: __x }).collect::>(), __v.1)) +} + +/// Points on a torus with (u, v) angles as ground truth. +/// +/// Rust: `manifold::embedding::torus_sample` +#[pyfunction] +#[pyo3(name = "torus_sample", signature = (n, big_r, small_r, rng))] +pub fn pyfn_torus_sample(n: usize, big_r: f64, small_r: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(Vec, Vec<(f64, f64)>)> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::embedding::torus_sample(n, big_r, small_r, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0.into_iter().map(|__x| crate::generated::types::PyVecN { inner: __x }).collect::>(), __v.1.into_iter().map(|__x| (__x.0, __x.1)).collect::>())) +} + +/// Uniform points on the unit 2-sphere in R3. +/// +/// Rust: `manifold::embedding::sphere_sample` +#[pyfunction] +#[pyo3(name = "sphere_sample", signature = (n, rng))] +pub fn pyfn_sphere_sample(n: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::embedding::sphere_sample(n, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVecN { inner: __x }).collect::>()) +} + +/// Helix in R3 with the parameter as ground truth. +/// +/// Rust: `manifold::embedding::helix_sample` +#[pyfunction] +#[pyo3(name = "helix_sample", signature = (n, rng))] +pub fn pyfn_helix_sample(n: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(Vec, Vec)> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::embedding::helix_sample(n, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0.into_iter().map(|__x| crate::generated::types::PyVecN { inner: __x }).collect::>(), __v.1)) +} + +/// Points on a Mobius band. +/// +/// Rust: `manifold::embedding::mobius_sample` +#[pyfunction] +#[pyo3(name = "mobius_sample", signature = (n, rng))] +pub fn pyfn_mobius_sample(n: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::embedding::mobius_sample(n, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVecN { inner: __x }).collect::>()) +} + +/// Points on the figure-8 immersion of the Klein bottle in R3. +/// +/// Rust: `manifold::embedding::klein_sample` +#[pyfunction] +#[pyo3(name = "klein_sample", signature = (n, rng))] +pub fn pyfn_klein_sample(n: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::embedding::klein_sample(n, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVecN { inner: __x }).collect::>()) +} + +/// The two-moons dataset with labels. +/// +/// Rust: `manifold::embedding::two_moons` +#[pyfunction] +#[pyo3(name = "two_moons", signature = (n, noise, rng))] +pub fn pyfn_two_moons(n: usize, noise: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(Vec, Vec)> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::embedding::two_moons(n, noise, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0.into_iter().map(|__x| crate::generated::types::PyVecN { inner: __x }).collect::>(), __v.1)) +} + +/// Isotropic Gaussian blobs with labels. +/// +/// Rust: `manifold::embedding::blobs` +#[pyfunction] +#[pyo3(name = "blobs", signature = (n, centers, spread, rng))] +pub fn pyfn_blobs(n: usize, centers: Vec, spread: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(Vec, Vec)> { + let centers = centers.into_iter().map(|__e| __e.0).collect::>(); + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::embedding::blobs(n, ¢ers, spread, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0.into_iter().map(|__x| crate::generated::types::PyVecN { inner: __x }).collect::>(), __v.1)) +} + +/// Local tangent space at a point by PCA of its k neighbors: rows are an +/// orthonormal basis. +/// +/// Rust: `manifold::embedding::tangent_space_estimate` +#[pyfunction] +#[pyo3(name = "tangent_space_estimate", signature = (points, idx, k, dim))] +pub fn pyfn_tangent_space_estimate(points: Vec, idx: usize, k: usize, dim: usize) -> PyResult { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::embedding::tangent_space_estimate(&points, idx, k, dim)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) +} + +/// Curvature proxy at a point: residual variance fraction outside the local +/// tangent plane. +/// +/// Rust: `manifold::embedding::manifold_curvature_estimate` +#[pyfunction] +#[pyo3(name = "manifold_curvature_estimate", signature = (points, idx, k))] +pub fn pyfn_manifold_curvature_estimate<'py>(py: Python<'py>, points: Vec, idx: usize, k: usize) -> PyResult { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::manifold::embedding::manifold_curvature_estimate(&points, idx, k))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Grassmann distance between subspaces spanned by the rows of a and b +/// (square root of the sum of squared principal angles). +/// +/// Rust: `manifold::embedding::grassmann_distance` +#[pyfunction] +#[pyo3(name = "grassmann_distance", signature = (a, b))] +pub fn pyfn_grassmann_distance<'py>(py: Python<'py>, a: crate::generated::types::PyMatrixArg, b: crate::generated::types::PyMatrixArg) -> PyResult { + let a = a.0; + let b = b.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::manifold::embedding::grassmann_distance(&a, &b))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Project a matrix onto the Stiefel manifold (nearest orthonormal-column +/// matrix, via the polar factor). +/// +/// Rust: `manifold::embedding::stiefel_project` +#[pyfunction] +#[pyo3(name = "stiefel_project", signature = (m))] +pub fn pyfn_stiefel_project(m: crate::generated::types::PyMatrixArg) -> PyResult { + let m = m.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::embedding::stiefel_project(&m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) +} + +/// Riemannian gradient descent on the unit sphere. +/// +/// Rust: `manifold::embedding::riemannian_gradient_descent_sphere` +#[pyfunction] +#[pyo3(name = "riemannian_gradient_descent_sphere", signature = (f, grad, x0, iters, lr))] +pub fn pyfn_riemannian_gradient_descent_sphere(f: pyo3::Py, grad: pyo3::Py, x0: crate::generated::types::PyVecNArg, iters: usize, lr: f64) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: &rust_physics_engine::manifold::vecn::VecN| -> f64 { __cb.call::<_, f64>((crate::generated::types::PyVecN { inner: __a0.clone() },), f64::NAN) } }; + let __cb_grad = std::rc::Rc::new(crate::runtime::Callback::new(grad)); + let grad = { let __cb = __cb_grad.clone(); move |__a0: &rust_physics_engine::manifold::vecn::VecN| -> rust_physics_engine::manifold::vecn::VecN { { let __r = __cb.call::<_, crate::generated::types::PyVecNArg>((crate::generated::types::PyVecN { inner: __a0.clone() },), crate::generated::types::PyVecNArg(rust_physics_engine::manifold::vecn::VecN { data: Vec::new() })); __r.0 } } }; + let x0 = x0.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::embedding::riemannian_gradient_descent_sphere(&f, &grad, &x0, iters, lr)); + crate::runtime::callback::check(&[&__cb_f, &__cb_grad], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVecN { inner: __v }) +} + +/// k-means with distances and means taken in a Riemannian metric (uses the +/// metric's exp/log maps). Returns (centroids, labels). +/// +/// Rust: `manifold::embedding::geodesic_kmeans` +#[pyfunction] +#[pyo3(name = "geodesic_kmeans", signature = (metric, points, k, iters, rng))] +pub fn pyfn_geodesic_kmeans(metric: pyo3::PyRef<'_, crate::generated::types::PyMetricMetric>, points: Vec, k: usize, iters: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(Vec, Vec)> { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::embedding::geodesic_kmeans(&metric.inner, &points, k, iters, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0.into_iter().map(|__x| crate::generated::types::PyVecN { inner: __x }).collect::>(), __v.1)) +} + +/// Radial-basis interpolation of scattered manifold data. +/// +/// Rust: `manifold::embedding::manifold_interpolation_rbf` +#[pyfunction] +#[pyo3(name = "manifold_interpolation_rbf", signature = (points, values, query, kernel))] +pub fn pyfn_manifold_interpolation_rbf(points: Vec, values: Vec, query: crate::generated::types::PyVecNArg, kernel: pyo3::Py) -> PyResult { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let query = query.0; + let __cb_kernel = std::rc::Rc::new(crate::runtime::Callback::new(kernel)); + let kernel = { let __cb = __cb_kernel.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::embedding::manifold_interpolation_rbf(&points, &values, &query, &kernel)); + crate::runtime::callback::check(&[&__cb_kernel], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_dist_matrix, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_classical_mds, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_metric_mds_smacof, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_nonmetric_mds, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_knn_graph, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_geodesic_distance_matrix, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_isomap, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_laplacian_eigenmaps, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_diffusion_maps, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spectral_embedding, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pca, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kernel_pca, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tsne, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_umap_lite, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_intrinsic_dimension_mle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_intrinsic_dimension_correlation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_intrinsic_dimension_two_nn, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_trustworthiness, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_continuity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_stress, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_neighborhood_preservation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_procrustes_align, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_swiss_roll, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_s_curve, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_torus_sample, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sphere_sample, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_helix_sample, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mobius_sample, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_klein_sample, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_two_moons, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_blobs, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tangent_space_estimate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_manifold_curvature_estimate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_grassmann_distance, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_stiefel_project, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_riemannian_gradient_descent_sphere, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_geodesic_kmeans, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_manifold_interpolation_rbf, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_manifold__geodesic.rs b/bindings/python/src/generated/m_manifold__geodesic.rs new file mode 100644 index 0000000..eb4503a --- /dev/null +++ b/bindings/python/src/generated/m_manifold__geodesic.rs @@ -0,0 +1,133 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Verify that sphere geodesics are great circles: shoot a unit-speed +/// geodesic along the equator and check it closes after 2 pi r. +/// +/// Rust: `manifold::geodesic::great_circle_check` +#[pyfunction] +#[pyo3(name = "great_circle_check", signature = (r))] +pub fn pyfn_great_circle_check(r: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::geodesic::great_circle_check(r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Equatorial Schwarzschild orbit r(phi) starting at r0 with dr/dphi = 0, +/// angular momentum `l` per unit mass (the energy parameter `_e` is +/// determined by the turning-point condition and kept for signature +/// compatibility). Integrates u'' + u = M/L^2 + 3 M u^2 with RK4. Returns +/// (phi, r) samples. +/// +/// Rust: `manifold::geodesic::schwarzschild_orbit` +#[pyfunction] +#[pyo3(name = "schwarzschild_orbit", signature = (m, r0, l, e, phi_end, dt))] +pub fn pyfn_schwarzschild_orbit<'py>(py: Python<'py>, m: f64, r0: f64, l: f64, e: f64, phi_end: f64, dt: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::manifold::geodesic::schwarzschild_orbit(m, r0, l, e, phi_end, dt))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Leading-order perihelion precession per orbit: 6 pi M / (a (1 - e^2)). +/// +/// Rust: `manifold::geodesic::perihelion_precession` +#[pyfunction] +#[pyo3(name = "perihelion_precession", signature = (m, a, e))] +pub fn pyfn_perihelion_precession(m: f64, a: f64, e: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::geodesic::perihelion_precession(m, a, e)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Leading-order light deflection by a mass: 4 M / b. +/// +/// Rust: `manifold::geodesic::light_deflection` +#[pyfunction] +#[pyo3(name = "light_deflection", signature = (m, b))] +pub fn pyfn_light_deflection(m: f64, b: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::geodesic::light_deflection(m, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Shapiro time delay for a signal grazing at impact parameter `b` between +/// radii r1 and r2: 2M ln(4 r1 r2 / b^2). +/// +/// Rust: `manifold::geodesic::shapiro_delay` +#[pyfunction] +#[pyo3(name = "shapiro_delay", signature = (m, r1, r2, b))] +pub fn pyfn_shapiro_delay(m: f64, r1: f64, r2: f64, b: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::geodesic::shapiro_delay(m, r1, r2, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Lyapunov instability exponent of the circular photon orbit at r = 3M: +/// lambda = 1 / (3 sqrt(3) M) per unit affine time. +/// +/// Rust: `manifold::geodesic::photon_orbit_stability` +#[pyfunction] +#[pyo3(name = "photon_orbit_stability", signature = (m))] +pub fn pyfn_photon_orbit_stability(m: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::geodesic::photon_orbit_stability(m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Shortest path between two mesh vertices along mesh edges (Dijkstra — +/// an upper bound on the exact geodesic). Returns the vertex positions. +/// +/// Rust: `manifold::geodesic::geodesics_on_mesh_exact` +#[pyfunction] +#[pyo3(name = "geodesics_on_mesh_exact", signature = (mesh, a, b))] +pub fn pyfn_geodesics_on_mesh_exact(mesh: crate::generated::types::PyGeometryMeshMesh, a: usize, b: usize) -> PyResult> { + let mesh = mesh.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::geodesic::geodesics_on_mesh_exact(&mesh, a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) +} + +/// Heat-method geodesic distance from a source vertex (Crane et al.): +/// diffuse heat for time `t`, normalize the gradient per face, solve a +/// Poisson equation for the distance. Dense solves; suitable for small +/// meshes. +/// +/// Rust: `manifold::geodesic::heat_method_geodesic` +#[pyfunction] +#[pyo3(name = "heat_method_geodesic", signature = (mesh, source, t))] +pub fn pyfn_heat_method_geodesic<'py>(py: Python<'py>, mesh: crate::generated::types::PyGeometryMeshMesh, source: usize, t: f64) -> PyResult> { + let mesh = mesh.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::manifold::geodesic::heat_method_geodesic(&mesh, source, t))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_great_circle_check, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_schwarzschild_orbit, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_perihelion_precession, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_light_deflection, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_shapiro_delay, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_photon_orbit_stability, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_geodesics_on_mesh_exact, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_heat_method_geodesic, m)?)?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_manifold__hyperbolic.rs b/bindings/python/src/generated/m_manifold__hyperbolic.rs new file mode 100644 index 0000000..50973df --- /dev/null +++ b/bindings/python/src/generated/m_manifold__hyperbolic.rs @@ -0,0 +1,603 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Poincare disk distance. +/// +/// Rust: `manifold::hyperbolic::hyp_distance_disk` +#[pyfunction] +#[pyo3(name = "hyp_distance_disk", signature = (z, w))] +pub fn pyfn_hyp_distance_disk(z: crate::runtime::coerce::ComplexArg, w: crate::runtime::coerce::ComplexArg) -> PyResult { + let z = z.0; + let w = w.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::hyperbolic::hyp_distance_disk(z, w)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Upper half-plane distance. +/// +/// Rust: `manifold::hyperbolic::hyp_distance_uhp` +#[pyfunction] +#[pyo3(name = "hyp_distance_uhp", signature = (z, w))] +pub fn pyfn_hyp_distance_uhp(z: crate::runtime::coerce::ComplexArg, w: crate::runtime::coerce::ComplexArg) -> PyResult { + let z = z.0; + let w = w.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::hyperbolic::hyp_distance_uhp(z, w)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Hyperboloid-model distance acosh(-) with the Minkowski form +/// = -x0 y0 + sum xi yi. +/// +/// Rust: `manifold::hyperbolic::hyp_distance_hyperboloid` +#[pyfunction] +#[pyo3(name = "hyp_distance_hyperboloid", signature = (x, y))] +pub fn pyfn_hyp_distance_hyperboloid(x: crate::generated::types::PyVecNArg, y: crate::generated::types::PyVecNArg) -> PyResult { + let x = x.0; + let y = y.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::hyperbolic::hyp_distance_hyperboloid(&x, &y)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Center and radius of the circular arc through z and w orthogonal to the +/// unit circle; None when the geodesic is a diameter. +/// +/// Rust: `manifold::hyperbolic::hyp_geodesic_circle_disk` +#[pyfunction] +#[pyo3(name = "hyp_geodesic_circle_disk", signature = (z, w))] +pub fn pyfn_hyp_geodesic_circle_disk<'py>(py: Python<'py>, z: crate::runtime::coerce::ComplexArg, w: crate::runtime::coerce::ComplexArg) -> PyResult, f64)>> { + let z = z.0; + let w = w.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::hyperbolic::hyp_geodesic_circle_disk(z, w)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| (crate::runtime::coerce::complex_out(py, __x.0), __x.1))) +} + +/// Sample the disk geodesic between z and w at n+1 points. +/// +/// Rust: `manifold::hyperbolic::hyp_geodesic_disk` +#[pyfunction] +#[pyo3(name = "hyp_geodesic_disk", signature = (z, w, n))] +pub fn pyfn_hyp_geodesic_disk<'py>(py: Python<'py>, z: crate::runtime::coerce::ComplexArg, w: crate::runtime::coerce::ComplexArg, n: usize) -> PyResult>> { + let z = z.0; + let w = w.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::hyperbolic::hyp_geodesic_disk(z, w, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// Hyperbolic circle in the disk: a Euclidean circle with offset center. +/// Returns n boundary samples. +/// +/// Rust: `manifold::hyperbolic::hyp_circle_disk` +#[pyfunction] +#[pyo3(name = "hyp_circle_disk", signature = (center, radius, n))] +pub fn pyfn_hyp_circle_disk<'py>(py: Python<'py>, center: crate::runtime::coerce::ComplexArg, radius: f64, n: usize) -> PyResult>> { + let center = center.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::hyperbolic::hyp_circle_disk(center, radius, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// Area of a hyperbolic triangle from its angles: pi - (alpha + beta + gamma). +/// +/// Rust: `manifold::hyperbolic::hyp_area_triangle` +#[pyfunction] +#[pyo3(name = "hyp_area_triangle", signature = (alpha, beta, gamma))] +pub fn pyfn_hyp_area_triangle(alpha: f64, beta: f64, gamma: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::hyperbolic::hyp_area_triangle(alpha, beta, gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// A triangle with prescribed angles (alpha at the origin, beta and gamma at +/// the other vertices), realized in the Poincare disk. +/// +/// Rust: `manifold::hyperbolic::hyp_triangle_from_angles` +#[pyfunction] +#[pyo3(name = "hyp_triangle_from_angles", signature = (alpha, beta, gamma))] +pub fn pyfn_hyp_triangle_from_angles<'py>(py: Python<'py>, alpha: f64, beta: f64, gamma: f64) -> PyResult>> { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::hyperbolic::hyp_triangle_from_angles(alpha, beta, gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// Hyperbolic law of cosines: cosh c = cosh a cosh b - sinh a sinh b cos gamma. +/// +/// Rust: `manifold::hyperbolic::hyp_law_of_cosines` +#[pyfunction] +#[pyo3(name = "hyp_law_of_cosines", signature = (a, b, gamma))] +pub fn pyfn_hyp_law_of_cosines(a: f64, b: f64, gamma: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::hyperbolic::hyp_law_of_cosines(a, b, gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Hyperbolic law of sines: returns sin(alpha) for side a opposite alpha, +/// given (a, b, beta) via sin(alpha)/sinh(a) = sin(beta)/sinh(b). +/// +/// Rust: `manifold::hyperbolic::hyp_law_of_sines` +#[pyfunction] +#[pyo3(name = "hyp_law_of_sines", signature = (a, b, beta))] +pub fn pyfn_hyp_law_of_sines(a: f64, b: f64, beta: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::hyperbolic::hyp_law_of_sines(a, b, beta)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Angle of parallelism Pi(d) = 2 atan(e^{-d}). +/// +/// Rust: `manifold::hyperbolic::hyp_angle_of_parallelism` +#[pyfunction] +#[pyo3(name = "hyp_angle_of_parallelism", signature = (d))] +pub fn pyfn_hyp_angle_of_parallelism(d: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::hyperbolic::hyp_angle_of_parallelism(d)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Circumference of a hyperbolic circle: 2 pi sinh r. +/// +/// Rust: `manifold::hyperbolic::hyp_circumference` +#[pyfunction] +#[pyo3(name = "hyp_circumference", signature = (r))] +pub fn pyfn_hyp_circumference(r: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::hyperbolic::hyp_circumference(r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Area of a hyperbolic disk: 2 pi (cosh r - 1). +/// +/// Rust: `manifold::hyperbolic::hyp_area_circle` +#[pyfunction] +#[pyo3(name = "hyp_area_circle", signature = (r))] +pub fn pyfn_hyp_area_circle(r: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::hyperbolic::hyp_area_circle(r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Volume of a hyperbolic ball in `dim` dimensions: +/// vol(S^{n-1}) * integral of sinh^{n-1}. +/// +/// Rust: `manifold::hyperbolic::hyp_volume_ball` +#[pyfunction] +#[pyo3(name = "hyp_volume_ball", signature = (r, dim))] +pub fn pyfn_hyp_volume_ball(r: f64, dim: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::hyperbolic::hyp_volume_ball(r, dim)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Mobius isometry of the disk: z -> e^{i theta} (z - a)/(1 - conj(a) z). +/// +/// Rust: `manifold::hyperbolic::mobius_disk` +#[pyfunction] +#[pyo3(name = "mobius_disk", signature = (z, a, theta))] +pub fn pyfn_mobius_disk<'py>(py: Python<'py>, z: crate::runtime::coerce::ComplexArg, a: crate::runtime::coerce::ComplexArg, theta: f64) -> PyResult> { + let z = z.0; + let a = a.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::hyperbolic::mobius_disk(z, a, theta)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::complex_out(py, __v)) +} + +/// Mobius action of an SL(2, R) element on the upper half-plane. +/// +/// Rust: `manifold::hyperbolic::mobius_uhp` +#[pyfunction] +#[pyo3(name = "mobius_uhp", signature = (z, m))] +pub fn pyfn_mobius_uhp<'py>(py: Python<'py>, z: crate::runtime::coerce::ComplexArg, m: crate::generated::types::PySl2R) -> PyResult> { + let z = z.0; + let m = m.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::hyperbolic::mobius_uhp(z, &m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::complex_out(py, __v)) +} + +/// The disk isometry (a, theta) sending z1 -> w1 and z2 -> w2 when the +/// distances agree (None otherwise). +/// +/// Rust: `manifold::hyperbolic::isometry_disk_from_two_points` +#[pyfunction] +#[pyo3(name = "isometry_disk_from_two_points", signature = (z1, z2, w1, w2))] +pub fn pyfn_isometry_disk_from_two_points<'py>(py: Python<'py>, z1: crate::runtime::coerce::ComplexArg, z2: crate::runtime::coerce::ComplexArg, w1: crate::runtime::coerce::ComplexArg, w2: crate::runtime::coerce::ComplexArg) -> PyResult, f64)>> { + let z1 = z1.0; + let z2 = z2.0; + let w1 = w1.0; + let w2 = w2.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::hyperbolic::isometry_disk_from_two_points(z1, z2, w1, w2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| (crate::runtime::coerce::complex_out(py, __x.0), __x.1))) +} + +/// SL(2, R) hyperbolic translation by `dist` along the geodesic in the +/// direction `direction` (an angle in the UHP tangent at i). +/// +/// Rust: `manifold::hyperbolic::hyperbolic_translation` +#[pyfunction] +#[pyo3(name = "hyperbolic_translation", signature = (dist, direction))] +pub fn pyfn_hyperbolic_translation(dist: f64, direction: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::hyperbolic::hyperbolic_translation(dist, direction)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySl2R { inner: __v }) +} + +/// Elliptic rotation about i in the upper half-plane by angle theta. +/// +/// Rust: `manifold::hyperbolic::hyperbolic_rotation` +#[pyfunction] +#[pyo3(name = "hyperbolic_rotation", signature = (theta))] +pub fn pyfn_hyperbolic_rotation(theta: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::hyperbolic::hyperbolic_rotation(theta)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySl2R { inner: __v }) +} + +/// Parabolic translation z -> z + t. +/// +/// Rust: `manifold::hyperbolic::parabolic` +#[pyfunction] +#[pyo3(name = "parabolic", signature = (t))] +pub fn pyfn_parabolic(t: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::hyperbolic::parabolic(t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySl2R { inner: __v }) +} + +/// Cayley transform disk -> upper half-plane. +/// +/// Rust: `manifold::hyperbolic::disk_to_uhp` +#[pyfunction] +#[pyo3(name = "disk_to_uhp", signature = (z))] +pub fn pyfn_disk_to_uhp<'py>(py: Python<'py>, z: crate::runtime::coerce::ComplexArg) -> PyResult> { + let z = z.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::hyperbolic::disk_to_uhp(z)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::complex_out(py, __v)) +} + +/// Inverse Cayley transform. +/// +/// Rust: `manifold::hyperbolic::uhp_to_disk` +#[pyfunction] +#[pyo3(name = "uhp_to_disk", signature = (w))] +pub fn pyfn_uhp_to_disk<'py>(py: Python<'py>, w: crate::runtime::coerce::ComplexArg) -> PyResult> { + let w = w.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::hyperbolic::uhp_to_disk(w)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::complex_out(py, __v)) +} + +/// Poincare disk -> Klein disk. +/// +/// Rust: `manifold::hyperbolic::disk_to_klein` +#[pyfunction] +#[pyo3(name = "disk_to_klein", signature = (z))] +pub fn pyfn_disk_to_klein<'py>(py: Python<'py>, z: crate::runtime::coerce::ComplexArg) -> PyResult> { + let z = z.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::hyperbolic::disk_to_klein(z)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::complex_out(py, __v)) +} + +/// Klein disk -> Poincare disk. +/// +/// Rust: `manifold::hyperbolic::klein_to_disk` +#[pyfunction] +#[pyo3(name = "klein_to_disk", signature = (k))] +pub fn pyfn_klein_to_disk<'py>(py: Python<'py>, k: crate::runtime::coerce::ComplexArg) -> PyResult> { + let k = k.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::hyperbolic::klein_to_disk(k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::complex_out(py, __v)) +} + +/// Poincare disk -> hyperboloid (x0, x1, x2). +/// +/// Rust: `manifold::hyperbolic::disk_to_hyperboloid` +#[pyfunction] +#[pyo3(name = "disk_to_hyperboloid", signature = (z))] +pub fn pyfn_disk_to_hyperboloid(z: crate::runtime::coerce::ComplexArg) -> PyResult { + let z = z.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::hyperbolic::disk_to_hyperboloid(z)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVecN { inner: __v }) +} + +/// Hyperboloid -> Poincare disk. +/// +/// Rust: `manifold::hyperbolic::hyperboloid_to_disk` +#[pyfunction] +#[pyo3(name = "hyperboloid_to_disk", signature = (x))] +pub fn pyfn_hyperboloid_to_disk<'py>(py: Python<'py>, x: crate::generated::types::PyVecNArg) -> PyResult> { + let x = x.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::hyperbolic::hyperboloid_to_disk(&x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::complex_out(py, __v)) +} + +/// Poincare ball -> upper half-space (3D). +/// +/// Rust: `manifold::hyperbolic::ball_to_half_space` +#[pyfunction] +#[pyo3(name = "ball_to_half_space", signature = (p))] +pub fn pyfn_ball_to_half_space(p: crate::generated::types::PyVec3Arg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::hyperbolic::ball_to_half_space(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Lorentz boost that carries the hyperboloid basepoint (1, 0, ..) to the +/// given hyperboloid point (an isometry of the model). +/// +/// Rust: `manifold::hyperbolic::lorentz_boost_hyperboloid` +#[pyfunction] +#[pyo3(name = "lorentz_boost_hyperboloid", signature = (v))] +pub fn pyfn_lorentz_boost_hyperboloid(v: crate::generated::types::PyVecNArg) -> PyResult { + let v = v.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::hyperbolic::lorentz_boost_hyperboloid(&v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) +} + +/// True when a regular {p, q} tiling is hyperbolic: 1/p + 1/q < 1/2. +/// +/// Rust: `manifold::hyperbolic::hyp_tiling_exists` +#[pyfunction] +#[pyo3(name = "hyp_tiling_exists", signature = (p, q))] +pub fn pyfn_hyp_tiling_exists(p: u32, q: u32) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::hyperbolic::hyp_tiling_exists(p, q)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Regular {p, q} tiling of the disk generated by reflections, to the given +/// recursion depth. Returns the polygons as vertex lists. +/// +/// Rust: `manifold::hyperbolic::hyp_tiling` +#[pyfunction] +#[pyo3(name = "hyp_tiling", signature = (p, q, depth, model))] +pub fn pyfn_hyp_tiling<'py>(py: Python<'py>, p: u32, q: u32, depth: usize, model: crate::generated::types::PyHypModel) -> PyResult>>> { + let model = model.to_rust(); + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::hyperbolic::hyp_tiling(p, q, depth, model)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()).collect::>()) +} + +/// Vertices of the regular 4g-gon fundamental polygon for a genus-g surface +/// (all angles sum to 2 pi). +/// +/// Rust: `manifold::hyperbolic::fundamental_polygon_genus` +#[pyfunction] +#[pyo3(name = "fundamental_polygon_genus", signature = (g))] +pub fn pyfn_fundamental_polygon_genus<'py>(py: Python<'py>, g: usize) -> PyResult>> { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::hyperbolic::fundamental_polygon_genus(g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// Approximate hyperbolic Voronoi cells: sample directions around each site +/// and march to the bisector. Returns one polygon per site. +/// +/// Rust: `manifold::hyperbolic::hyp_voronoi_disk` +#[pyfunction] +#[pyo3(name = "hyp_voronoi_disk", signature = (sites, n_res))] +pub fn pyfn_hyp_voronoi_disk<'py>(py: Python<'py>, sites: Vec, n_res: usize) -> PyResult>>> { + let sites = sites.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::hyperbolic::hyp_voronoi_disk(&sites, n_res)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()).collect::>()) +} + +/// Hyperbolic Delaunay triangulation by the empty-circumdisk test in the +/// Klein model (hyperbolic Delaunay = Euclidean Delaunay of Klein points). +/// +/// Rust: `manifold::hyperbolic::hyp_delaunay_disk` +#[pyfunction] +#[pyo3(name = "hyp_delaunay_disk", signature = (sites))] +pub fn pyfn_hyp_delaunay_disk<'py>(py: Python<'py>, sites: Vec) -> PyResult>> { + let sites = sites.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::manifold::hyperbolic::hyp_delaunay_disk(&sites))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.to_vec()).collect::>()) +} + +/// Hyperbolic convex hull via the Klein model (geodesics are straight +/// there). Returns hull vertices in order. +/// +/// Rust: `manifold::hyperbolic::hyp_convex_hull_disk` +#[pyfunction] +#[pyo3(name = "hyp_convex_hull_disk", signature = (points))] +pub fn pyfn_hyp_convex_hull_disk<'py>(py: Python<'py>, points: Vec) -> PyResult>> { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::hyperbolic::hyp_convex_hull_disk(&points)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// Hyperbolic centroid (Karcher mean) of disk points. +/// +/// Rust: `manifold::hyperbolic::hyp_centroid_disk` +#[pyfunction] +#[pyo3(name = "hyp_centroid_disk", signature = (points, iters))] +pub fn pyfn_hyp_centroid_disk<'py>(py: Python<'py>, points: Vec, iters: usize) -> PyResult> { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::hyperbolic::hyp_centroid_disk(&points, iters)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::complex_out(py, __v)) +} + +/// Sarkar's low-distortion embedding of a tree into the Poincare disk. +/// +/// Rust: `manifold::hyperbolic::hyp_embed_tree` +#[pyfunction] +#[pyo3(name = "hyp_embed_tree", signature = (adjacency, root))] +pub fn pyfn_hyp_embed_tree<'py>(py: Python<'py>, adjacency: Vec>, root: usize) -> PyResult>> { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::hyperbolic::hyp_embed_tree(&adjacency, root)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// Stress-majorization MDS into hyperbolic space (Poincare ball of the +/// given dimension) matching the target distance matrix. +/// +/// Rust: `manifold::hyperbolic::hyp_embed_graph_mds` +#[pyfunction] +#[pyo3(name = "hyp_embed_graph_mds", signature = (dist, dim, iters))] +pub fn pyfn_hyp_embed_graph_mds(dist: crate::generated::types::PyMatrixArg, dim: usize, iters: usize) -> PyResult> { + let dist = dist.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::hyperbolic::hyp_embed_graph_mds(&dist, dim, iters)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVecN { inner: __x }).collect::>()) +} + +/// Nickel-Kiela Poincare embedding of a graph by Riemannian SGD on +/// edge-distance loss (connected pairs pulled together, random negatives +/// pushed apart). +/// +/// Rust: `manifold::hyperbolic::poincare_embedding_train` +#[pyfunction] +#[pyo3(name = "poincare_embedding_train", signature = (graph_edges, dim, epochs, lr, rng))] +pub fn pyfn_poincare_embedding_train(graph_edges: Vec<(usize, usize)>, dim: usize, epochs: usize, lr: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let graph_edges = graph_edges.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::hyperbolic::poincare_embedding_train(&graph_edges, dim, epochs, lr, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVecN { inner: __x }).collect::>()) +} + +/// Curve-shortening flow of a closed disk polygon under the hyperbolic +/// metric (explicit steps toward the hyperbolic midpoint of neighbors). +/// +/// Rust: `manifold::hyperbolic::hyp_mean_curvature_flow` +#[pyfunction] +#[pyo3(name = "hyp_mean_curvature_flow", signature = (curve, dt, steps))] +pub fn pyfn_hyp_mean_curvature_flow<'py>(py: Python<'py>, curve: Vec, dt: f64, steps: usize) -> PyResult>> { + let curve = curve.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::hyperbolic::hyp_mean_curvature_flow(&curve, dt, steps)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// Horocycle at the ideal point through a given interior point: a Euclidean +/// circle tangent to the boundary at the ideal point. +/// +/// Rust: `manifold::hyperbolic::horocycle_disk` +#[pyfunction] +#[pyo3(name = "horocycle_disk", signature = (ideal_point, through, n))] +pub fn pyfn_horocycle_disk<'py>(py: Python<'py>, ideal_point: crate::runtime::coerce::ComplexArg, through: crate::runtime::coerce::ComplexArg, n: usize) -> PyResult>> { + let ideal_point = ideal_point.0; + let through = through.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::hyperbolic::horocycle_disk(ideal_point, through, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// Equidistant curve at hyperbolic distance `d` from the geodesic through +/// two boundary-anchored points (sampled along one side). +/// +/// Rust: `manifold::hyperbolic::equidistant_curve_disk` +#[pyfunction] +#[pyo3(name = "equidistant_curve_disk", signature = (geodesic, d, n))] +pub fn pyfn_equidistant_curve_disk<'py>(py: Python<'py>, geodesic: (crate::runtime::coerce::ComplexArg, crate::runtime::coerce::ComplexArg), d: f64, n: usize) -> PyResult>> { + let geodesic = (geodesic.0.0, geodesic.1.0); + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::hyperbolic::equidistant_curve_disk(geodesic, d, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// Limit set of a Schottky-like group: orbit of a basepoint under words in +/// the generators up to the given length, keeping the deepest images. +/// +/// Rust: `manifold::hyperbolic::limit_set_schottky` +#[pyfunction] +#[pyo3(name = "limit_set_schottky", signature = (generators, depth))] +pub fn pyfn_limit_set_schottky<'py>(py: Python<'py>, generators: Vec, depth: usize) -> PyResult>> { + let generators = generators.into_iter().map(|__e| __e.inner).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::hyperbolic::limit_set_schottky(&generators, depth)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// Circles of an Apollonian gasket generated from the classic +/// (-1, 2, 2, 3) Descartes configuration by Vieta reflection; returns +/// (center, radius) pairs (the bounding circle first). +/// +/// Rust: `manifold::hyperbolic::apollonian_from_mobius` +#[pyfunction] +#[pyo3(name = "apollonian_from_mobius", signature = (depth))] +pub fn pyfn_apollonian_from_mobius<'py>(py: Python<'py>, depth: usize) -> PyResult, f64)>> { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::hyperbolic::apollonian_from_mobius(depth)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (crate::runtime::coerce::complex_out(py, __x.0), __x.1)).collect::>()) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_hyp_distance_disk, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hyp_distance_uhp, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hyp_distance_hyperboloid, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hyp_geodesic_circle_disk, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hyp_geodesic_disk, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hyp_circle_disk, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hyp_area_triangle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hyp_triangle_from_angles, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hyp_law_of_cosines, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hyp_law_of_sines, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hyp_angle_of_parallelism, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hyp_circumference, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hyp_area_circle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hyp_volume_ball, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mobius_disk, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mobius_uhp, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_isometry_disk_from_two_points, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hyperbolic_translation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hyperbolic_rotation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_parabolic, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_disk_to_uhp, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_uhp_to_disk, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_disk_to_klein, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_klein_to_disk, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_disk_to_hyperboloid, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hyperboloid_to_disk, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ball_to_half_space, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lorentz_boost_hyperboloid, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hyp_tiling_exists, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hyp_tiling, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fundamental_polygon_genus, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hyp_voronoi_disk, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hyp_delaunay_disk, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hyp_convex_hull_disk, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hyp_centroid_disk, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hyp_embed_tree, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hyp_embed_graph_mds, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_poincare_embedding_train, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hyp_mean_curvature_flow, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_horocycle_disk, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_equidistant_curve_disk, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_limit_set_schottky, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_apollonian_from_mobius, m)?)?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_manifold__lie.rs b/bindings/python/src/generated/m_manifold__lie.rs new file mode 100644 index 0000000..f84d6ad --- /dev/null +++ b/bindings/python/src/generated/m_manifold__lie.rs @@ -0,0 +1,254 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Commutator [A, B] = AB - BA. +/// +/// Rust: `manifold::lie::lie_bracket_matrix` +#[pyfunction] +#[pyo3(name = "lie_bracket_matrix", signature = (a, b))] +pub fn pyfn_lie_bracket_matrix(a: crate::generated::types::PyMatrixArg, b: crate::generated::types::PyMatrixArg) -> PyResult { + let a = a.0; + let b = b.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::lie_bracket_matrix(&a, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) +} + +/// Matrix exponential by scaling-and-squaring with a Taylor/Pade core. +/// +/// Rust: `manifold::lie::matrix_exp` +#[pyfunction] +#[pyo3(name = "matrix_exp", signature = (m))] +pub fn pyfn_matrix_exp(m: crate::generated::types::PyMatrixArg) -> PyResult { + let m = m.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::matrix_exp(&m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) +} + +/// Principal matrix square root by the Denman-Beavers iteration. +/// +/// Rust: `manifold::lie::matrix_sqrt` +#[pyfunction] +#[pyo3(name = "matrix_sqrt", signature = (m))] +pub fn pyfn_matrix_sqrt(m: crate::generated::types::PyMatrixArg) -> PyResult { + let m = m.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::matrix_sqrt(&m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) +} + +/// Principal matrix logarithm by inverse scaling-and-squaring with a +/// Gregory series core. Requires eigenvalues off the negative real axis. +/// +/// Rust: `manifold::lie::matrix_log` +#[pyfunction] +#[pyo3(name = "matrix_log", signature = (m))] +pub fn pyfn_matrix_log(m: crate::generated::types::PyMatrixArg) -> PyResult { + let m = m.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::matrix_log(&m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) +} + +/// Killing form B_ij = tr(ad_i ad_j) for a Lie algebra basis of matrices. +/// +/// Rust: `manifold::lie::killing_form` +#[pyfunction] +#[pyo3(name = "killing_form", signature = (basis))] +pub fn pyfn_killing_form(basis: Vec) -> PyResult { + let basis = basis.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::killing_form(&basis)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) +} + +/// Structure constants c^k_{ij} with [b_i, b_j] = c^k_{ij} b_k, obtained by +/// least squares in the vectorized basis. +/// +/// Rust: `manifold::lie::structure_constants` +#[pyfunction] +#[pyo3(name = "structure_constants", signature = (basis))] +pub fn pyfn_structure_constants(basis: Vec) -> PyResult { + let basis = basis.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::structure_constants(&basis)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTensorN { inner: __v }) +} + +/// Casimir eigenvalue of the spin-j representation of so(3): j(j+1). +/// +/// Rust: `manifold::lie::casimir_so3` +#[pyfunction] +#[pyo3(name = "casimir_so3", signature = (j))] +pub fn pyfn_casimir_so3(j: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::casimir_so3(j)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Wigner small-d matrix element d^j_{m1 m2}(beta) (Wigner's sum formula). +/// +/// Rust: `manifold::lie::wigner_d_small` +#[pyfunction] +#[pyo3(name = "wigner_d_small", signature = (j, m1, m2, beta))] +pub fn pyfn_wigner_d_small(j: f64, m1: f64, m2: f64, beta: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::wigner_d_small(j, m1, m2, beta)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Full Wigner D-matrix element +/// D^j_{m1 m2}(alpha, beta, gamma) = e^{-i m1 alpha} d^j_{m1 m2}(beta) +/// e^{-i m2 gamma}. +/// +/// Rust: `manifold::lie::wigner_d` +#[pyfunction] +#[pyo3(name = "wigner_d", signature = (j, m1, m2, alpha, beta, gamma))] +pub fn pyfn_wigner_d<'py>(py: Python<'py>, j: f64, m1: f64, m2: f64, alpha: f64, beta: f64, gamma: f64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::wigner_d(j, m1, m2, alpha, beta, gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::complex_out(py, __v)) +} + +/// Clebsch-Gordan coefficient (Racah's formula). +/// +/// Rust: `manifold::lie::clebsch_gordan` +#[pyfunction] +#[pyo3(name = "clebsch_gordan", signature = (j1, m1, j2, m2, j, m))] +pub fn pyfn_clebsch_gordan(j1: f64, m1: f64, j2: f64, m2: f64, j: f64, m: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::clebsch_gordan(j1, m1, j2, m2, j, m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Rotate the degree-l band of complex spherical-harmonic coefficients +/// (ordered m = -l..l) by the rotation `r` using the Wigner D-matrix with +/// zyz Euler angles. +/// +/// Rust: `manifold::lie::rotate_spherical_harmonics` +#[pyfunction] +#[pyo3(name = "rotate_spherical_harmonics", signature = (coeffs, l, r))] +pub fn pyfn_rotate_spherical_harmonics<'py>(py: Python<'py>, coeffs: Vec, l: usize, r: crate::generated::types::PySo3) -> PyResult>> { + let coeffs = coeffs.into_iter().map(|__e| __e.0).collect::>(); + let r = r.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::rotate_spherical_harmonics(&coeffs, l, &r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// Near-uniform deterministic grid on SO(3) built from a Fibonacci sphere +/// of axes and a golden-ratio sweep of angles. +/// +/// Rust: `manifold::lie::so3_uniform_grid` +#[pyfunction] +#[pyo3(name = "so3_uniform_grid", signature = (n))] +pub fn pyfn_so3_uniform_grid(n: usize) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::so3_uniform_grid(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PySo3 { inner: __x }).collect::>()) +} + +/// Haar measure density over the rotation angle in [0, pi]: +/// rho(theta) = (1 - cos theta)/pi, normalized to integrate to 1. +/// +/// Rust: `manifold::lie::so3_haar_measure_density` +#[pyfunction] +#[pyo3(name = "so3_haar_measure_density", signature = (angle))] +pub fn pyfn_so3_haar_measure_density(angle: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::so3_haar_measure_density(angle)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Park-Martin hand-eye calibration: solve AX = XB from motion pairs. +/// +/// Rust: `manifold::lie::hand_eye_calibration` +#[pyfunction] +#[pyo3(name = "hand_eye_calibration", signature = (a, b))] +pub fn pyfn_hand_eye_calibration(a: Vec, b: Vec) -> PyResult { + let a = a.into_iter().map(|__e| __e.inner).collect::>(); + let b = b.into_iter().map(|__e| __e.inner).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::hand_eye_calibration(&a, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySe3 { inner: __v }) +} + +/// Umeyama similarity alignment: the Sim3 (or Se3 when `with_scale` is +/// false) minimizing sum |dst_i - (s R src_i + t)|^2. +/// +/// Rust: `manifold::lie::umeyama_alignment` +#[pyfunction] +#[pyo3(name = "umeyama_alignment", signature = (src, dst, with_scale))] +pub fn pyfn_umeyama_alignment(src: Vec, dst: Vec, with_scale: bool) -> PyResult { + let src = src.into_iter().map(|__e| __e.0).collect::>(); + let dst = dst.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::umeyama_alignment(&src, &dst, with_scale)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySim3 { inner: __v }) +} + +/// Rotation averaging: chordal L2 mean (projected arithmetic mean of the +/// matrices) refined by a few IRLS iterations in the tangent space. +/// +/// Rust: `manifold::lie::rotation_averaging` +#[pyfunction] +#[pyo3(name = "rotation_averaging", signature = (rots, weights))] +pub fn pyfn_rotation_averaging(rots: Vec, weights: Vec) -> PyResult { + let rots = rots.into_iter().map(|__e| __e.inner).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::rotation_averaging(&rots, &weights)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySo3 { inner: __v }) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_lie_bracket_matrix, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_matrix_exp, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_matrix_sqrt, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_matrix_log, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_killing_form, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_structure_constants, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_casimir_so3, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wigner_d_small, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wigner_d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_clebsch_gordan, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rotate_spherical_harmonics, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_so3_uniform_grid, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_so3_haar_measure_density, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hand_eye_calibration, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_umeyama_alignment, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rotation_averaging, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_manifold__metric.rs b/bindings/python/src/generated/m_manifold__metric.rs new file mode 100644 index 0000000..b1746de --- /dev/null +++ b/bindings/python/src/generated/m_manifold__metric.rs @@ -0,0 +1,36 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Kerr metric in Boyer-Lindquist coordinates (t, r, theta, phi). +/// +/// Rust: `manifold::metric::kerr_boyer_lindquist` +#[pyfunction] +#[pyo3(name = "kerr_boyer_lindquist", signature = (m, a))] +pub fn pyfn_kerr_boyer_lindquist(m: f64, a: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::metric::kerr_boyer_lindquist(m, a)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMetricMetric { inner: __v }) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_kerr_boyer_lindquist, m)?)?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_manifold__polytope4.rs b/bindings/python/src/generated/m_manifold__polytope4.rs new file mode 100644 index 0000000..5ce9650 --- /dev/null +++ b/bindings/python/src/generated/m_manifold__polytope4.rs @@ -0,0 +1,368 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// The six coordinate rotation planes of R4. +/// +/// Rust: `manifold::polytope4::rotation_4d_planes` +#[pyfunction] +#[pyo3(name = "rotation_4d_planes", signature = ())] +pub fn pyfn_rotation_4d_planes<'py>(py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::manifold::polytope4::rotation_4d_planes())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Rotate a point in a single coordinate plane. +/// +/// Rust: `manifold::polytope4::rotate_4d` +#[pyfunction] +#[pyo3(name = "rotate_4d", signature = (p, plane, angle))] +pub fn pyfn_rotate_4d(p: crate::generated::types::PyVec4Arg, plane: (usize, usize), angle: f64) -> PyResult { + let p = p.0; + let plane = (plane.0, plane.1); + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::polytope4::rotate_4d(p, plane, angle)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec4 { inner: __v }) +} + +/// Double rotation: xy-plane by `angle_xy`, zw-plane by `angle_zw`. +/// +/// Rust: `manifold::polytope4::rotate_4d_double` +#[pyfunction] +#[pyo3(name = "rotate_4d_double", signature = (p, angle_xy, angle_zw))] +pub fn pyfn_rotate_4d_double(p: crate::generated::types::PyVec4Arg, angle_xy: f64, angle_zw: f64) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::polytope4::rotate_4d_double(p, angle_xy, angle_zw)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec4 { inner: __v }) +} + +/// Point on the Clifford torus in S3: parameter angles (u, v), aspect r. +/// +/// Rust: `manifold::polytope4::clifford_torus` +#[pyfunction] +#[pyo3(name = "clifford_torus", signature = (u, v, r))] +pub fn pyfn_clifford_torus(u: f64, v: f64, r: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::polytope4::clifford_torus(u, v, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec4 { inner: __v }) +} + +/// Sampled Clifford torus grid. +/// +/// Rust: `manifold::polytope4::clifford_torus_mesh` +#[pyfunction] +#[pyo3(name = "clifford_torus_mesh", signature = (nu, nv, r))] +pub fn pyfn_clifford_torus_mesh(nu: usize, nv: usize, r: f64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::polytope4::clifford_torus_mesh(nu, nv, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec4 { inner: __x }).collect::>()) +} + +/// Near-uniform points on S3 (from the super-Fibonacci quaternions). +/// +/// Rust: `manifold::polytope4::hypersphere_s3_points` +#[pyfunction] +#[pyo3(name = "hypersphere_s3_points", signature = (n))] +pub fn pyfn_hypersphere_s3_points(n: usize) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::polytope4::hypersphere_s3_points(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec4 { inner: __x }).collect::>()) +} + +/// Volume of the n-ball of radius r (alias into the spherical module). +/// +/// Rust: `manifold::polytope4::hypersphere_volume` +#[pyfunction] +#[pyo3(name = "hypersphere_volume", signature = (r, n))] +pub fn pyfn_hypersphere_volume(r: f64, n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::polytope4::hypersphere_volume(r, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Regular n-simplex: n+1 vertices in R^n, unit circumradius, with edges. +/// +/// Rust: `manifold::polytope4::simplex_n` +#[pyfunction] +#[pyo3(name = "simplex_n", signature = (n))] +pub fn pyfn_simplex_n(n: usize) -> PyResult<(Vec, Vec<(usize, usize)>)> { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::polytope4::simplex_n(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0.into_iter().map(|__x| crate::generated::types::PyVecN { inner: __x }).collect::>(), __v.1.into_iter().map(|__x| (__x.0, __x.1)).collect::>())) +} + +/// n-cube vertices (coordinates +-1/2) with edges. +/// +/// Rust: `manifold::polytope4::hypercube_n` +#[pyfunction] +#[pyo3(name = "hypercube_n", signature = (n))] +pub fn pyfn_hypercube_n(n: usize) -> PyResult<(Vec, Vec<(usize, usize)>)> { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::polytope4::hypercube_n(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0.into_iter().map(|__x| crate::generated::types::PyVecN { inner: __x }).collect::>(), __v.1.into_iter().map(|__x| (__x.0, __x.1)).collect::>())) +} + +/// n-dimensional cross-polytope (unit vertices +-e_i) with edges. +/// +/// Rust: `manifold::polytope4::cross_polytope_n` +#[pyfunction] +#[pyo3(name = "cross_polytope_n", signature = (n))] +pub fn pyfn_cross_polytope_n(n: usize) -> PyResult<(Vec, Vec<(usize, usize)>)> { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::polytope4::cross_polytope_n(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0.into_iter().map(|__x| crate::generated::types::PyVecN { inner: __x }).collect::>(), __v.1.into_iter().map(|__x| (__x.0, __x.1)).collect::>())) +} + +/// Edges of the n-cube graph (bitmask vertices, Hamming distance 1). +/// +/// Rust: `manifold::polytope4::hypercube_graph_n` +#[pyfunction] +#[pyo3(name = "hypercube_graph_n", signature = (n))] +pub fn pyfn_hypercube_graph_n<'py>(py: Python<'py>, n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::manifold::polytope4::hypercube_graph_n(n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Project n-dimensional points into 3D with the given (orthonormal) basis. +/// +/// Rust: `manifold::polytope4::project_n_to_3` +#[pyfunction] +#[pyo3(name = "project_n_to_3", signature = (points, basis))] +pub fn pyfn_project_n_to_3(points: Vec, basis: Vec) -> PyResult> { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let basis = <[rust_physics_engine::manifold::vecn::VecN; 3]>::try_from(basis.into_iter().map(|__e| __e.0).collect::>()).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 3 values, got {}", __v.len())))?; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::polytope4::project_n_to_3(&points, basis)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) +} + +/// Project n-dimensional points into 2D. +/// +/// Rust: `manifold::polytope4::project_n_to_2` +#[pyfunction] +#[pyo3(name = "project_n_to_2", signature = (points, basis))] +pub fn pyfn_project_n_to_2(points: Vec, basis: Vec) -> PyResult> { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let basis = <[rust_physics_engine::manifold::vecn::VecN; 2]>::try_from(basis.into_iter().map(|__e| __e.0).collect::>()).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 2 values, got {}", __v.len())))?; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::polytope4::project_n_to_2(&points, basis)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// Petrie polygon projection of a regular 4-polytope: its vertices +/// projected into the Coxeter plane of the matching symmetry group, using +/// root systems realized in the polytope's own coordinates. +/// +/// Rust: `manifold::polytope4::petrie_polygon_projection` +#[pyfunction] +#[pyo3(name = "petrie_polygon_projection", signature = (p))] +pub fn pyfn_petrie_polygon_projection(p: crate::generated::types::PyPolytope4) -> PyResult> { + let p = p.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::polytope4::petrie_polygon_projection(&p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// Project points into the Coxeter plane of the given group ("B4", "D4", +/// "F4", "H4", "E8"), with root systems in standard coordinates. The plane +/// is the invariant plane of a Coxeter element, found as the 2D eigenspace +/// of (w + w^T)/2 with eigenvalue cos(2 pi/h). +/// +/// Rust: `manifold::polytope4::coxeter_plane_projection` +#[pyfunction] +#[pyo3(name = "coxeter_plane_projection", signature = (vertices, group))] +pub fn pyfn_coxeter_plane_projection(vertices: Vec, group: String) -> PyResult> { + let vertices = vertices.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::polytope4::coxeter_plane_projection(&vertices, &group)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// The 240 roots of E8 (norm sqrt 2). +/// +/// Rust: `manifold::polytope4::e8_roots` +#[pyfunction] +#[pyo3(name = "e8_roots", signature = ())] +pub fn pyfn_e8_roots() -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::polytope4::e8_roots()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVecN { inner: __x }).collect::>()) +} + +/// Nearest E8 lattice point (D8 plus glue-vector decoding). +/// +/// Rust: `manifold::polytope4::e8_lattice_nearest` +#[pyfunction] +#[pyo3(name = "e8_lattice_nearest", signature = (p))] +pub fn pyfn_e8_lattice_nearest(p: crate::generated::types::PyVecNArg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::polytope4::e8_lattice_nearest(&p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVecN { inner: __v }) +} + +/// The Leech lattice minimal-vector count (kissing number in 24D). +/// +/// Rust: `manifold::polytope4::leech_lattice_min_vectors_count` +#[pyfunction] +#[pyo3(name = "leech_lattice_min_vectors_count", signature = ())] +pub fn pyfn_leech_lattice_min_vectors_count() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::polytope4::leech_lattice_min_vectors_count()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// D4 lattice points (integer coordinates, even sum) within radius r. +/// +/// Rust: `manifold::polytope4::d4_lattice_points` +#[pyfunction] +#[pyo3(name = "d4_lattice_points", signature = (r))] +pub fn pyfn_d4_lattice_points(r: f64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::polytope4::d4_lattice_points(r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec4 { inner: __x }).collect::>()) +} + +/// The 48 roots of F4. +/// +/// Rust: `manifold::polytope4::f4_roots` +#[pyfunction] +#[pyo3(name = "f4_roots", signature = ())] +pub fn pyfn_f4_roots() -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::polytope4::f4_roots()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec4 { inner: __x }).collect::>()) +} + +/// The 120 roots of H4 (the unit icosians). +/// +/// Rust: `manifold::polytope4::h4_roots` +#[pyfunction] +#[pyo3(name = "h4_roots", signature = ())] +pub fn pyfn_h4_roots() -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::polytope4::h4_roots()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec4 { inner: __x }).collect::>()) +} + +/// Known kissing numbers by dimension. +/// +/// Rust: `manifold::polytope4::kissing_number_known` +#[pyfunction] +#[pyo3(name = "kissing_number_known", signature = (n))] +pub fn pyfn_kissing_number_known(n: usize) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::polytope4::kissing_number_known(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// (n-1)-volume of the slice of the unit n-cube `[0,1]^n` by the hyperplane +/// sum(x) = s, times sqrt(n) (the Irwin-Hall density scaled to a volume). +/// +/// Rust: `manifold::polytope4::hypercube_slicing_volume` +#[pyfunction] +#[pyo3(name = "hypercube_slicing_volume", signature = (n, s))] +pub fn pyfn_hypercube_slicing_volume(n: usize, s: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::polytope4::hypercube_slicing_volume(n, s)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Fraction of the (n-1)-sphere's surface within angle theta of a pole: +/// regularized incomplete beta I_{sin^2 theta}((n-1)/2, 1/2) / 2 for +/// theta <= pi/2. +/// +/// Rust: `manifold::polytope4::hypersphere_cap_fraction` +#[pyfunction] +#[pyo3(name = "hypersphere_cap_fraction", signature = (n, theta))] +pub fn pyfn_hypersphere_cap_fraction(n: usize, theta: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::polytope4::hypersphere_cap_fraction(n, theta)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Gaussian mass concentrates at radius sqrt(n). +/// +/// Rust: `manifold::polytope4::gaussian_concentration_radius` +#[pyfunction] +#[pyo3(name = "gaussian_concentration_radius", signature = (n))] +pub fn pyfn_gaussian_concentration_radius(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::polytope4::gaussian_concentration_radius(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Monte Carlo probability that a simple random walk on Z^n returns to the +/// origin within `steps` steps (deterministic internal seed). +/// +/// Rust: `manifold::polytope4::random_walk_n_return_prob` +#[pyfunction] +#[pyo3(name = "random_walk_n_return_prob", signature = (n, steps))] +pub fn pyfn_random_walk_n_return_prob(n: usize, steps: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::polytope4::random_walk_n_return_prob(n, steps)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Ratio of the volume of the inscribed ball to the unit cube in n +/// dimensions (goes to zero fast). +/// +/// Rust: `manifold::polytope4::volume_ball_vs_cube_ratio` +#[pyfunction] +#[pyo3(name = "volume_ball_vs_cube_ratio", signature = (n))] +pub fn pyfn_volume_ball_vs_cube_ratio(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::polytope4::volume_ball_vs_cube_ratio(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_rotation_4d_planes, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rotate_4d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rotate_4d_double, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_clifford_torus, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_clifford_torus_mesh, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hypersphere_s3_points, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hypersphere_volume, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_simplex_n, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hypercube_n, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cross_polytope_n, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hypercube_graph_n, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_project_n_to_3, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_project_n_to_2, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_petrie_polygon_projection, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_coxeter_plane_projection, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_e8_roots, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_e8_lattice_nearest, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_leech_lattice_min_vectors_count, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_d4_lattice_points, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_f4_roots, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_h4_roots, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kissing_number_known, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hypercube_slicing_volume, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hypersphere_cap_fraction, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gaussian_concentration_radius, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_random_walk_n_return_prob, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_volume_ball_vs_cube_ratio, m)?)?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_manifold__spacetime.rs b/bindings/python/src/generated/m_manifold__spacetime.rs new file mode 100644 index 0000000..2dc311a --- /dev/null +++ b/bindings/python/src/generated/m_manifold__spacetime.rs @@ -0,0 +1,411 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Classify the separation b - a on the light cone of `a`. +/// +/// Rust: `manifold::spacetime::light_cone_check` +#[pyfunction] +#[pyo3(name = "light_cone_check", signature = (a, b))] +pub fn pyfn_light_cone_check(a: crate::generated::types::PyFourVector, b: crate::generated::types::PyFourVector) -> PyResult { + let a = a.inner; + let b = b.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spacetime::light_cone_check(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCausal::from_rust(&__v)) +} + +/// The simultaneity hyperplane through `event` for an observer moving at +/// `observer_vel`: events x with u . (x - event) = 0 for the observer +/// four-velocity u. +/// +/// Rust: `manifold::spacetime::simultaneity_plane` +#[pyfunction] +#[pyo3(name = "simultaneity_plane", signature = (observer_vel, event))] +pub fn pyfn_simultaneity_plane(observer_vel: crate::generated::types::PyVec3Arg, event: crate::generated::types::PyFourVector) -> PyResult { + let observer_vel = observer_vel.0; + let event = event.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spacetime::simultaneity_plane(observer_vel, event)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySpacetimePlane { inner: __v }) +} + +/// Ages (stay-at-home, traveler) after coordinate time `t_coordinate` with +/// the traveler cruising at speed `v`. +/// +/// Rust: `manifold::spacetime::twin_paradox_ages` +#[pyfunction] +#[pyo3(name = "twin_paradox_ages", signature = (v, t_coordinate))] +pub fn pyfn_twin_paradox_ages(v: f64, t_coordinate: f64) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spacetime::twin_paradox_ages(v, t_coordinate)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Relativistic rocket with constant proper acceleration: returns +/// (coordinate time, distance, speed) after proper time `tau`. +/// +/// Rust: `manifold::spacetime::relativistic_rocket` +#[pyfunction] +#[pyo3(name = "relativistic_rocket", signature = (accel_proper, tau))] +pub fn pyfn_relativistic_rocket(accel_proper: f64, tau: f64) -> PyResult<(f64, f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spacetime::relativistic_rocket(accel_proper, tau)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// Rindler coordinates (eta, xi) of the Minkowski event (t, x) in the +/// right wedge x > |t|, normalized so the observer at proper acceleration +/// `a` sits at xi = 1/a: t = xi sinh(a eta), x = xi cosh(a eta). +/// +/// Rust: `manifold::spacetime::rindler_coords` +#[pyfunction] +#[pyo3(name = "rindler_coords", signature = (t, x, a))] +pub fn pyfn_rindler_coords(t: f64, x: f64, a: f64) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spacetime::rindler_coords(t, x, a)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Distance from a uniformly accelerated observer to their Rindler +/// horizon: c^2 / a (geometric units: 1/a). +/// +/// Rust: `manifold::spacetime::rindler_horizon` +#[pyfunction] +#[pyo3(name = "rindler_horizon", signature = (a))] +pub fn pyfn_rindler_horizon(a: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spacetime::rindler_horizon(a)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Unruh temperature of a uniformly accelerated observer (SI units): +/// T = hbar a / (2 pi c k_B). +/// +/// Rust: `manifold::spacetime::unruh_temperature` +#[pyfunction] +#[pyo3(name = "unruh_temperature", signature = (a))] +pub fn pyfn_unruh_temperature(a: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spacetime::unruh_temperature(a)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Kruskal-Szekeres coordinates (T, X) of the Schwarzschild event (t, r), +/// smooth across the horizon r = 2M (exterior region I and interior +/// region II). +/// +/// Rust: `manifold::spacetime::kruskal_from_schwarzschild` +#[pyfunction] +#[pyo3(name = "kruskal_from_schwarzschild", signature = (t, r, m))] +pub fn pyfn_kruskal_from_schwarzschild(t: f64, r: f64, m: f64) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spacetime::kruskal_from_schwarzschild(t, r, m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Penrose diagram coordinates: Kruskal null coordinates compactified with +/// arctangent; returns (T, X) of the conformal diagram. +/// +/// Rust: `manifold::spacetime::penrose_diagram_coords` +#[pyfunction] +#[pyo3(name = "penrose_diagram_coords", signature = (t, r, m))] +pub fn pyfn_penrose_diagram_coords(t: f64, r: f64, m: f64) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spacetime::penrose_diagram_coords(t, r, m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Ingoing Eddington-Finkelstein null coordinate v = t + r* with the +/// tortoise coordinate r* = r + 2M ln|r/2M - 1|. +/// +/// Rust: `manifold::spacetime::eddington_finkelstein` +#[pyfunction] +#[pyo3(name = "eddington_finkelstein", signature = (t, r, m))] +pub fn pyfn_eddington_finkelstein(t: f64, r: f64, m: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spacetime::eddington_finkelstein(t, r, m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Schwarzschild metric wired into the finite-difference `Metric` +/// machinery (coordinates t, r, theta, phi; signature -+++). +/// +/// Rust: `manifold::spacetime::schwarzschild_geodesic_metric` +#[pyfunction] +#[pyo3(name = "schwarzschild_geodesic_metric", signature = (m))] +pub fn pyfn_schwarzschild_geodesic_metric(m: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spacetime::schwarzschild_geodesic_metric(m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMetricMetric { inner: __v }) +} + +/// Full timelike Schwarzschild orbit in the equatorial plane from the +/// first integrals: energy `e` and angular momentum `l` per unit mass, +/// starting at r0 (infalling if r0 is not a turning point). Returns +/// (t, r, phi, tau) samples every proper-time step `dt`. +/// +/// Rust: `manifold::spacetime::orbit_schwarzschild_full` +#[pyfunction] +#[pyo3(name = "orbit_schwarzschild_full", signature = (m, e, l, r0, tau_end, dt))] +pub fn pyfn_orbit_schwarzschild_full<'py>(py: Python<'py>, m: f64, e: f64, l: f64, r0: f64, tau_end: f64, dt: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::manifold::spacetime::orbit_schwarzschild_full(m, e, l, r0, tau_end, dt))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, __x.2, __x.3)).collect::>()) +} + +/// Photon trajectory around a Schwarzschild black hole with impact +/// parameter `b`, from the orbit equation u'' + u = 3 M u^2 starting at +/// infinity. Returns (phi, r) samples; stops at `phi_max`, escape, or +/// capture inside the photon sphere. +/// +/// Rust: `manifold::spacetime::photon_ray_trace_schwarzschild` +#[pyfunction] +#[pyo3(name = "photon_ray_trace_schwarzschild", signature = (m, b, phi_max))] +pub fn pyfn_photon_ray_trace_schwarzschild<'py>(py: Python<'py>, m: f64, b: f64, phi_max: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::manifold::spacetime::photon_ray_trace_schwarzschild(m, b, phi_max))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Apparent black hole shadow radius for a Kerr hole of spin `a` seen at +/// `inclination` (radians from the spin axis), averaged over the shadow +/// boundary via Bardeen's celestial coordinates; sqrt(27) M for a = 0. +/// +/// Rust: `manifold::spacetime::black_hole_shadow_radius` +#[pyfunction] +#[pyo3(name = "black_hole_shadow_radius", signature = (m, a, inclination))] +pub fn pyfn_black_hole_shadow_radius(m: f64, a: f64, inclination: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spacetime::black_hole_shadow_radius(m, a, inclination)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Bundle the Kerr geodesic constants (energy, axial angular momentum, and +/// Carter constant per unit rest mass) with their potentials. +/// +/// Rust: `manifold::spacetime::kerr_geodesic_constants` +#[pyfunction] +#[pyo3(name = "kerr_geodesic_constants", signature = (m, a, e, l, q))] +pub fn pyfn_kerr_geodesic_constants(m: f64, a: f64, e: f64, l: f64, q: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spacetime::kerr_geodesic_constants(m, a, e, l, q)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyKerrConstants { inner: __v }) +} + +/// Einstein ring angular radius (geometric units, angles in radians): +/// theta_E = sqrt(4 M d_ls / (d_l d_s)). +/// +/// Rust: `manifold::spacetime::gravitational_lens_einstein_radius` +#[pyfunction] +#[pyo3(name = "gravitational_lens_einstein_radius", signature = (m, d_l, d_s, d_ls))] +pub fn pyfn_gravitational_lens_einstein_radius(m: f64, d_l: f64, d_s: f64, d_ls: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spacetime::gravitational_lens_einstein_radius(m, d_l, d_s, d_ls)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Total point-lens magnification at impact parameter u (in Einstein +/// radii): (u^2 + 2) / (u sqrt(u^2 + 4)). +/// +/// Rust: `manifold::spacetime::point_lens_magnification` +#[pyfunction] +#[pyo3(name = "point_lens_magnification", signature = (u))] +pub fn pyfn_point_lens_magnification(u: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spacetime::point_lens_magnification(u)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Solve the lens equation beta = theta - alpha(theta) for image positions +/// given the deflection profile `mass_model` (alpha as a function of +/// theta, odd in theta). Returns all real images found on both sides. +/// +/// Rust: `manifold::spacetime::lens_equation_solve` +#[pyfunction] +#[pyo3(name = "lens_equation_solve", signature = (beta, mass_model))] +pub fn pyfn_lens_equation_solve(beta: f64, mass_model: pyo3::Py) -> PyResult> { + let __cb_mass_model = std::rc::Rc::new(crate::runtime::Callback::new(mass_model)); + let mass_model = { let __cb = __cb_mass_model.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spacetime::lens_equation_solve(beta, &mass_model)); + crate::runtime::callback::check(&[&__cb_mass_model], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Hawking temperature of a Schwarzschild black hole (SI): +/// T = hbar c^3 / (8 pi G M k_B). +/// +/// Rust: `manifold::spacetime::hawking_temperature` +#[pyfunction] +#[pyo3(name = "hawking_temperature", signature = (m))] +pub fn pyfn_hawking_temperature(m: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spacetime::hawking_temperature(m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Bekenstein-Hawking entropy (SI): S = 4 pi G M^2 k_B / (hbar c). +/// +/// Rust: `manifold::spacetime::bekenstein_entropy` +#[pyfunction] +#[pyo3(name = "bekenstein_entropy", signature = (m))] +pub fn pyfn_bekenstein_entropy(m: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spacetime::bekenstein_entropy(m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Black hole evaporation time (SI): t = 5120 pi G^2 M^3 / (hbar c^4). +/// +/// Rust: `manifold::spacetime::evaporation_time` +#[pyfunction] +#[pyo3(name = "evaporation_time", signature = (m))] +pub fn pyfn_evaporation_time(m: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spacetime::evaporation_time(m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Cosmological distances in a flat-ish FRW universe (SI: `h0` in 1/s, +/// distances in meters, lookback time in seconds): returns (comoving, +/// angular-diameter, luminosity, lookback). +/// +/// Rust: `manifold::spacetime::cosmological_distances` +#[pyfunction] +#[pyo3(name = "cosmological_distances", signature = (z, h0, omega_m, omega_l))] +pub fn pyfn_cosmological_distances(z: f64, h0: f64, omega_m: f64, omega_l: f64) -> PyResult<(f64, f64, f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spacetime::cosmological_distances(z, h0, omega_m, omega_l)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2, __v.3)) +} + +/// Chirp mass (m1 m2)^(3/5) / (m1 + m2)^(1/5). +/// +/// Rust: `manifold::spacetime::gw_chirp_mass` +#[pyfunction] +#[pyo3(name = "gw_chirp_mass", signature = (m1, m2))] +pub fn pyfn_gw_chirp_mass(m1: f64, m2: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spacetime::gw_chirp_mass(m1, m2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Leading-order (Newtonian chirp) inspiral waveform at luminosity +/// distance `d` (SI units, face-on): returns (h_plus, h_cross) sampled at +/// the times `t`, with coalescence at the last sample. +/// +/// Rust: `manifold::spacetime::gw_waveform_inspiral` +#[pyfunction] +#[pyo3(name = "gw_waveform_inspiral", signature = (m1, m2, d, t))] +pub fn pyfn_gw_waveform_inspiral<'py>(py: Python<'py>, m1: f64, m2: f64, d: f64, t: Vec) -> PyResult<(Vec, Vec)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::manifold::spacetime::gw_waveform_inspiral(m1, m2, d, &t))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Reduce a 5D Kaluza-Klein geodesic to 4D charged-particle data: the +/// conserved fifth momentum gives the charge-to-mass ratio (valid when +/// the gauge potential vanishes at the initial point and phi = 1), and +/// the positions project to the 4D worldline. +/// +/// Rust: `manifold::spacetime::kk_reduce_geodesic_to_charged` +#[pyfunction] +#[pyo3(name = "kk_reduce_geodesic_to_charged", signature = (geo5, radius))] +pub fn pyfn_kk_reduce_geodesic_to_charged(geo5: Vec, radius: f64) -> PyResult<(f64, Vec)> { + let geo5 = geo5.into_iter().map(|__e| __e.inner).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spacetime::kk_reduce_geodesic_to_charged(&geo5, radius)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1.into_iter().map(|__x| crate::generated::types::PyVecN { inner: __x }).collect::>())) +} + +/// Kaluza-Klein tower masses n / R for mode numbers 0..=n_max. +/// +/// Rust: `manifold::spacetime::kk_compactification_mass_spectrum` +#[pyfunction] +#[pyo3(name = "kk_compactification_mass_spectrum", signature = (radius, n_max))] +pub fn pyfn_kk_compactification_mass_spectrum<'py>(py: Python<'py>, radius: f64, n_max: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::manifold::spacetime::kk_compactification_mass_spectrum(radius, n_max))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Gravitational force law with `n_extra` compact extra dimensions of size +/// `size` (normalized to 1/r^2 at large r): 1/r^2 outside, continuously +/// matched to size^n / r^(2+n) inside. +/// +/// Rust: `manifold::spacetime::extra_dimension_gravity_law` +#[pyfunction] +#[pyo3(name = "extra_dimension_gravity_law", signature = (r, n_extra, size))] +pub fn pyfn_extra_dimension_gravity_law(r: f64, n_extra: usize, size: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spacetime::extra_dimension_gravity_law(r, n_extra, size)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Cross-validate the spacetime algebra boost rotor against the matrix +/// Lorentz boost: the maximum component difference over a set of basis +/// events (the STA rotor R e R~ realizes the inverse boost, so it is +/// compared against B(-v)). +/// +/// Rust: `manifold::spacetime::sta_vs_matrix_lorentz_check` +#[pyfunction] +#[pyo3(name = "sta_vs_matrix_lorentz_check", signature = (v))] +pub fn pyfn_sta_vs_matrix_lorentz_check(v: crate::generated::types::PyVec3Arg) -> PyResult { + let v = v.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spacetime::sta_vs_matrix_lorentz_check(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_light_cone_check, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_simultaneity_plane, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_twin_paradox_ages, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_relativistic_rocket, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rindler_coords, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rindler_horizon, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_unruh_temperature, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kruskal_from_schwarzschild, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_penrose_diagram_coords, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_eddington_finkelstein, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_schwarzschild_geodesic_metric, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_orbit_schwarzschild_full, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_photon_ray_trace_schwarzschild, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_black_hole_shadow_radius, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kerr_geodesic_constants, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gravitational_lens_einstein_radius, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_point_lens_magnification, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lens_equation_solve, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hawking_temperature, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bekenstein_entropy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_evaporation_time, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cosmological_distances, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gw_chirp_mass, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gw_waveform_inspiral, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kk_reduce_geodesic_to_charged, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kk_compactification_mass_spectrum, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_extra_dimension_gravity_law, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sta_vs_matrix_lorentz_check, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_manifold__spherical.rs b/bindings/python/src/generated/m_manifold__spherical.rs new file mode 100644 index 0000000..c92d75d --- /dev/null +++ b/bindings/python/src/generated/m_manifold__spherical.rs @@ -0,0 +1,927 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Geodesic (angular) distance on the unit n-sphere. +/// +/// Rust: `manifold::spherical::sphere_distance_n` +#[pyfunction] +#[pyo3(name = "sphere_distance_n", signature = (a, b))] +pub fn pyfn_sphere_distance_n(a: crate::generated::types::PyVecNArg, b: crate::generated::types::PyVecNArg) -> PyResult { + let a = a.0; + let b = b.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::sphere_distance_n(&a, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Slerp along the great circle from a to b. +/// +/// Rust: `manifold::spherical::sphere_geodesic_n` +#[pyfunction] +#[pyo3(name = "sphere_geodesic_n", signature = (a, b, t))] +pub fn pyfn_sphere_geodesic_n(a: crate::generated::types::PyVecNArg, b: crate::generated::types::PyVecNArg, t: f64) -> PyResult { + let a = a.0; + let b = b.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::sphere_geodesic_n(&a, &b, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVecN { inner: __v }) +} + +/// Exponential map at p: follow the great circle in direction v (tangent, +/// |v| = arc length). +/// +/// Rust: `manifold::spherical::sphere_exp_n` +#[pyfunction] +#[pyo3(name = "sphere_exp_n", signature = (p, v))] +pub fn pyfn_sphere_exp_n(p: crate::generated::types::PyVecNArg, v: crate::generated::types::PyVecNArg) -> PyResult { + let p = p.0; + let v = v.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::sphere_exp_n(&p, &v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVecN { inner: __v }) +} + +/// Logarithm map: tangent vector at p pointing toward q with |v| equal to +/// the geodesic distance. +/// +/// Rust: `manifold::spherical::sphere_log_n` +#[pyfunction] +#[pyo3(name = "sphere_log_n", signature = (p, q))] +pub fn pyfn_sphere_log_n(p: crate::generated::types::PyVecNArg, q: crate::generated::types::PyVecNArg) -> PyResult { + let p = p.0; + let q = q.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::sphere_log_n(&p, &q)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVecN { inner: __v }) +} + +/// Parallel transport of tangent vector v from p to q along the connecting +/// geodesic. +/// +/// Rust: `manifold::spherical::sphere_parallel_transport_n` +#[pyfunction] +#[pyo3(name = "sphere_parallel_transport_n", signature = (v, p, q))] +pub fn pyfn_sphere_parallel_transport_n(v: crate::generated::types::PyVecNArg, p: crate::generated::types::PyVecNArg, q: crate::generated::types::PyVecNArg) -> PyResult { + let v = v.0; + let p = p.0; + let q = q.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::sphere_parallel_transport_n(&v, &p, &q)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVecN { inner: __v }) +} + +/// Area of a spherical triangle on the unit sphere via l'Huilier's theorem. +/// +/// Rust: `manifold::spherical::spherical_triangle_area` +#[pyfunction] +#[pyo3(name = "spherical_triangle_area", signature = (a, b, c))] +pub fn pyfn_spherical_triangle_area(a: crate::generated::types::PyVec3Arg, b: crate::generated::types::PyVec3Arg, c: crate::generated::types::PyVec3Arg) -> PyResult { + let a = a.0; + let b = b.0; + let c = c.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::spherical_triangle_area(a, b, c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Interior angles of a spherical triangle at vertices (a, b, c). +/// +/// Rust: `manifold::spherical::spherical_triangle_angles` +#[pyfunction] +#[pyo3(name = "spherical_triangle_angles", signature = (a, b, c))] +pub fn pyfn_spherical_triangle_angles(a: crate::generated::types::PyVec3Arg, b: crate::generated::types::PyVec3Arg, c: crate::generated::types::PyVec3Arg) -> PyResult<(f64, f64, f64)> { + let a = a.0; + let b = b.0; + let c = c.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::spherical_triangle_angles(a, b, c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// Spherical law of cosines: cos c = cos a cos b + sin a sin b cos gamma. +/// +/// Rust: `manifold::spherical::spherical_law_of_cosines` +#[pyfunction] +#[pyo3(name = "spherical_law_of_cosines", signature = (a, b, gamma))] +pub fn pyfn_spherical_law_of_cosines(a: f64, b: f64, gamma: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::spherical_law_of_cosines(a, b, gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Spherical law of sines: alpha from (a, b, beta) via +/// sin alpha / sin a = sin beta / sin b. +/// +/// Rust: `manifold::spherical::spherical_law_of_sines` +#[pyfunction] +#[pyo3(name = "spherical_law_of_sines", signature = (a, b, beta))] +pub fn pyfn_spherical_law_of_sines(a: f64, b: f64, beta: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::spherical_law_of_sines(a, b, beta)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Haversine great-circle distance on a sphere of radius r. +/// +/// Rust: `manifold::spherical::haversine` +#[pyfunction] +#[pyo3(name = "haversine", signature = (lat1, lon1, lat2, lon2, r))] +pub fn pyfn_haversine(lat1: f64, lon1: f64, lat2: f64, lon2: f64, r: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::haversine(lat1, lon1, lat2, lon2, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Area of a spherical polygon (unit sphere) by summing triangle fan areas +/// with orientation from the spherical excess formula. +/// +/// Rust: `manifold::spherical::spherical_polygon_area` +#[pyfunction] +#[pyo3(name = "spherical_polygon_area", signature = (vertices))] +pub fn pyfn_spherical_polygon_area<'py>(py: Python<'py>, vertices: Vec) -> PyResult { + let vertices = vertices.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::manifold::spherical::spherical_polygon_area(&vertices))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Spherical centroid: normalized arithmetic mean. +/// +/// Rust: `manifold::spherical::spherical_centroid` +#[pyfunction] +#[pyo3(name = "spherical_centroid", signature = (points))] +pub fn pyfn_spherical_centroid(points: Vec) -> PyResult { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::spherical_centroid(&points)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Weighted spherical mean. +/// +/// Rust: `manifold::spherical::spherical_mean_weighted` +#[pyfunction] +#[pyo3(name = "spherical_mean_weighted", signature = (points, weights))] +pub fn pyfn_spherical_mean_weighted(points: Vec, weights: Vec) -> PyResult { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::spherical_mean_weighted(&points, &weights)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Spherical Delaunay triangulation by the empty-circumcap test (brute +/// force; suitable for modest site counts). +/// +/// Rust: `manifold::spherical::spherical_delaunay` +#[pyfunction] +#[pyo3(name = "spherical_delaunay", signature = (sites))] +pub fn pyfn_spherical_delaunay<'py>(py: Python<'py>, sites: Vec) -> PyResult>> { + let sites = sites.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::manifold::spherical::spherical_delaunay(&sites))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.to_vec()).collect::>()) +} + +/// Spherical Voronoi cells as the dual of the Delaunay triangulation: each +/// cell is the list of circumcenters of triangles incident to the site, +/// ordered by angle. +/// +/// Rust: `manifold::spherical::spherical_voronoi` +#[pyfunction] +#[pyo3(name = "spherical_voronoi", signature = (sites))] +pub fn pyfn_spherical_voronoi(sites: Vec) -> PyResult>> { + let sites = sites.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::spherical_voronoi(&sites)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()).collect::>()) +} + +/// Indices of points on the 3D convex hull (brute-force facet search). +/// +/// Rust: `manifold::spherical::spherical_convex_hull` +#[pyfunction] +#[pyo3(name = "spherical_convex_hull", signature = (points))] +pub fn pyfn_spherical_convex_hull<'py>(py: Python<'py>, points: Vec) -> PyResult> { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::manifold::spherical::spherical_convex_hull(&points))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Stereographic projection from the north pole onto the equatorial plane. +/// +/// Rust: `manifold::spherical::stereographic` +#[pyfunction] +#[pyo3(name = "stereographic", signature = (p))] +pub fn pyfn_stereographic(p: crate::generated::types::PyVec3Arg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::stereographic(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) +} + +/// Inverse stereographic projection. +/// +/// Rust: `manifold::spherical::inverse_stereographic` +#[pyfunction] +#[pyo3(name = "inverse_stereographic", signature = (q))] +pub fn pyfn_inverse_stereographic(q: crate::generated::types::PyVec2Arg) -> PyResult { + let q = q.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::inverse_stereographic(q)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Stereographic projection of the unit n-sphere from the last-coordinate +/// pole. +/// +/// Rust: `manifold::spherical::stereographic_n` +#[pyfunction] +#[pyo3(name = "stereographic_n", signature = (p))] +pub fn pyfn_stereographic_n(p: crate::generated::types::PyVecNArg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::stereographic_n(&p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVecN { inner: __v }) +} + +/// Gnomonic projection about `center` (great circles map to lines). +/// +/// Rust: `manifold::spherical::gnomonic` +#[pyfunction] +#[pyo3(name = "gnomonic", signature = (p, center))] +pub fn pyfn_gnomonic(p: crate::generated::types::PyVec3Arg, center: crate::generated::types::PyVec3Arg) -> PyResult { + let p = p.0; + let center = center.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::gnomonic(p, center)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) +} + +/// Inverse gnomonic projection. +/// +/// Rust: `manifold::spherical::gnomonic_inverse` +#[pyfunction] +#[pyo3(name = "gnomonic_inverse", signature = (q, center))] +pub fn pyfn_gnomonic_inverse(q: crate::generated::types::PyVec2Arg, center: crate::generated::types::PyVec3Arg) -> PyResult { + let q = q.0; + let center = center.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::gnomonic_inverse(q, center)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Orthographic projection about `center`. +/// +/// Rust: `manifold::spherical::orthographic` +#[pyfunction] +#[pyo3(name = "orthographic", signature = (p, center))] +pub fn pyfn_orthographic(p: crate::generated::types::PyVec3Arg, center: crate::generated::types::PyVec3Arg) -> PyResult { + let p = p.0; + let center = center.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::orthographic(p, center)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) +} + +/// Inverse orthographic (near-side solution). +/// +/// Rust: `manifold::spherical::orthographic_inverse` +#[pyfunction] +#[pyo3(name = "orthographic_inverse", signature = (q, center))] +pub fn pyfn_orthographic_inverse(q: crate::generated::types::PyVec2Arg, center: crate::generated::types::PyVec3Arg) -> PyResult { + let q = q.0; + let center = center.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::orthographic_inverse(q, center)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Mercator projection (x = lon, y = ln tan(pi/4 + lat/2)). +/// +/// Rust: `manifold::spherical::mercator` +#[pyfunction] +#[pyo3(name = "mercator", signature = (p))] +pub fn pyfn_mercator(p: crate::generated::types::PyVec3Arg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::mercator(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) +} + +/// Inverse Mercator. +/// +/// Rust: `manifold::spherical::mercator_inverse` +#[pyfunction] +#[pyo3(name = "mercator_inverse", signature = (q))] +pub fn pyfn_mercator_inverse(q: crate::generated::types::PyVec2Arg) -> PyResult { + let q = q.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::mercator_inverse(q)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Lambert azimuthal equal-area projection about the north pole. +/// +/// Rust: `manifold::spherical::lambert_azimuthal_equal_area` +#[pyfunction] +#[pyo3(name = "lambert_azimuthal_equal_area", signature = (p))] +pub fn pyfn_lambert_azimuthal_equal_area(p: crate::generated::types::PyVec3Arg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::lambert_azimuthal_equal_area(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) +} + +/// Inverse Lambert azimuthal equal-area. +/// +/// Rust: `manifold::spherical::lambert_azimuthal_equal_area_inverse` +#[pyfunction] +#[pyo3(name = "lambert_azimuthal_equal_area_inverse", signature = (q))] +pub fn pyfn_lambert_azimuthal_equal_area_inverse(q: crate::generated::types::PyVec2Arg) -> PyResult { + let q = q.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::lambert_azimuthal_equal_area_inverse(q)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Mollweide projection (equal-area pseudocylindrical). +/// +/// Rust: `manifold::spherical::mollweide` +#[pyfunction] +#[pyo3(name = "mollweide", signature = (p))] +pub fn pyfn_mollweide(p: crate::generated::types::PyVec3Arg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::mollweide(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) +} + +/// Inverse Mollweide. +/// +/// Rust: `manifold::spherical::mollweide_inverse` +#[pyfunction] +#[pyo3(name = "mollweide_inverse", signature = (q))] +pub fn pyfn_mollweide_inverse(q: crate::generated::types::PyVec2Arg) -> PyResult { + let q = q.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::mollweide_inverse(q)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Equirectangular projection (x = lon, y = lat). +/// +/// Rust: `manifold::spherical::equirectangular` +#[pyfunction] +#[pyo3(name = "equirectangular", signature = (p))] +pub fn pyfn_equirectangular(p: crate::generated::types::PyVec3Arg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::equirectangular(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) +} + +/// Inverse equirectangular. +/// +/// Rust: `manifold::spherical::equirectangular_inverse` +#[pyfunction] +#[pyo3(name = "equirectangular_inverse", signature = (q))] +pub fn pyfn_equirectangular_inverse(q: crate::generated::types::PyVec2Arg) -> PyResult { + let q = q.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::equirectangular_inverse(q)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Azimuthal equidistant projection about the north pole. +/// +/// Rust: `manifold::spherical::azimuthal_equidistant` +#[pyfunction] +#[pyo3(name = "azimuthal_equidistant", signature = (p))] +pub fn pyfn_azimuthal_equidistant(p: crate::generated::types::PyVec3Arg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::azimuthal_equidistant(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) +} + +/// Inverse azimuthal equidistant. +/// +/// Rust: `manifold::spherical::azimuthal_equidistant_inverse` +#[pyfunction] +#[pyo3(name = "azimuthal_equidistant_inverse", signature = (q))] +pub fn pyfn_azimuthal_equidistant_inverse(q: crate::generated::types::PyVec2Arg) -> PyResult { + let q = q.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::azimuthal_equidistant_inverse(q)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Robinson projection (table-interpolated pseudocylindrical). +/// +/// Rust: `manifold::spherical::robinson` +#[pyfunction] +#[pyo3(name = "robinson", signature = (p))] +pub fn pyfn_robinson(p: crate::generated::types::PyVec3Arg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::robinson(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) +} + +/// Inverse Robinson (bisection on the latitude table). +/// +/// Rust: `manifold::spherical::robinson_inverse` +#[pyfunction] +#[pyo3(name = "robinson_inverse", signature = (q))] +pub fn pyfn_robinson_inverse(q: crate::generated::types::PyVec2Arg) -> PyResult { + let q = q.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::robinson_inverse(q)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Hopf map S3 -> S2: q -> q k q^-1 image of the base point, giving +/// (2(xz + wy), 2(yz - wx), w^2 + z^2 - x^2 - y^2). +/// +/// Rust: `manifold::spherical::hopf_fibration` +#[pyfunction] +#[pyo3(name = "hopf_fibration", signature = (q))] +pub fn pyfn_hopf_fibration(q: crate::generated::types::PyQuaternionArg) -> PyResult { + let q = q.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::hopf_fibration(q)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// The circle fiber in S3 above a point of S2, sampled at n quaternions. +/// +/// Rust: `manifold::spherical::hopf_fiber` +#[pyfunction] +#[pyo3(name = "hopf_fiber", signature = (p, n))] +pub fn pyfn_hopf_fiber(p: crate::generated::types::PyVec3Arg, n: usize) -> PyResult> { + let p = p.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::hopf_fiber(p, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyQuaternion { inner: __x }).collect::>()) +} + +/// Hopf fiber stereographically projected to R3 (a Villarceau circle). +/// +/// Rust: `manifold::spherical::hopf_fiber_stereographic` +#[pyfunction] +#[pyo3(name = "hopf_fiber_stereographic", signature = (p, n))] +pub fn pyfn_hopf_fiber_stereographic(p: crate::generated::types::PyVec3Arg, n: usize) -> PyResult> { + let p = p.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::hopf_fiber_stereographic(p, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) +} + +/// Geodesic on S3 between unit quaternions (slerp). +/// +/// Rust: `manifold::spherical::s3_geodesic` +#[pyfunction] +#[pyo3(name = "s3_geodesic", signature = (a, b, t))] +pub fn pyfn_s3_geodesic(a: crate::generated::types::PyQuaternionArg, b: crate::generated::types::PyQuaternionArg, t: f64) -> PyResult { + let a = a.0; + let b = b.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::s3_geodesic(a, b, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuaternion { inner: __v }) +} + +/// Near-uniform deterministic points on S3 (super-Fibonacci spiral). +/// +/// Rust: `manifold::spherical::s3_uniform_points` +#[pyfunction] +#[pyo3(name = "s3_uniform_points", signature = (n))] +pub fn pyfn_s3_uniform_points(n: usize) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::s3_uniform_points(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyQuaternion { inner: __x }).collect::>()) +} + +/// Uniform random points on the unit (dim-1)-sphere in R^dim. +/// +/// Rust: `manifold::spherical::sphere_uniform_points_n` +#[pyfunction] +#[pyo3(name = "sphere_uniform_points_n", signature = (n, dim, rng))] +pub fn pyfn_sphere_uniform_points_n(n: usize, dim: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::sphere_uniform_points_n(n, dim, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVecN { inner: __x }).collect::>()) +} + +/// Volume of the n-ball of radius r. +/// +/// Rust: `manifold::spherical::sphere_volume_n` +#[pyfunction] +#[pyo3(name = "sphere_volume_n", signature = (r, n))] +pub fn pyfn_sphere_volume_n(r: f64, n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::sphere_volume_n(r, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Surface area of the (n-1)-sphere of radius r in R^n. +/// +/// Rust: `manifold::spherical::sphere_surface_n` +#[pyfunction] +#[pyo3(name = "sphere_surface_n", signature = (r, n))] +pub fn pyfn_sphere_surface_n(r: f64, n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::sphere_surface_n(r, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Area of a spherical cap of opening angle theta on a sphere of radius r. +/// +/// Rust: `manifold::spherical::sphere_cap_area` +#[pyfunction] +#[pyo3(name = "sphere_cap_area", signature = (r, theta))] +pub fn pyfn_sphere_cap_area(r: f64, theta: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::sphere_cap_area(r, theta)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Volume of the corresponding solid cap. +/// +/// Rust: `manifold::spherical::sphere_cap_volume` +#[pyfunction] +#[pyo3(name = "sphere_cap_volume", signature = (r, theta))] +pub fn pyfn_sphere_cap_volume(r: f64, theta: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::sphere_cap_volume(r, theta)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Complex spherical harmonic Y_l^m(theta, phi) with the Condon-Shortley +/// phase. +/// +/// Rust: `manifold::spherical::spherical_harmonics_complex` +#[pyfunction] +#[pyo3(name = "spherical_harmonics_complex", signature = (l, m, theta, phi))] +pub fn pyfn_spherical_harmonics_complex<'py>(py: Python<'py>, l: u32, m: i32, theta: f64, phi: f64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::spherical_harmonics_complex(l, m, theta, phi)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::complex_out(py, __v)) +} + +/// Forward spherical harmonic transform up to `l_max` by quadrature on an +/// (n_theta x n_phi) grid. Coefficients are ordered (l, m) with +/// index l^2 + l + m. +/// +/// Rust: `manifold::spherical::spherical_harmonic_transform` +#[pyfunction] +#[pyo3(name = "spherical_harmonic_transform", signature = (f, l_max, n_theta, n_phi))] +pub fn pyfn_spherical_harmonic_transform<'py>(py: Python<'py>, f: pyo3::Py, l_max: u32, n_theta: usize, n_phi: usize) -> PyResult>> { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64, __a1: f64| -> f64 { __cb.call::<_, f64>((__a0, __a1), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::spherical_harmonic_transform(&f, l_max, n_theta, n_phi)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// Evaluate a coefficient vector at (theta, phi). +/// +/// Rust: `manifold::spherical::spherical_harmonic_inverse` +#[pyfunction] +#[pyo3(name = "spherical_harmonic_inverse", signature = (coeffs, l_max, theta, phi))] +pub fn pyfn_spherical_harmonic_inverse<'py>(py: Python<'py>, coeffs: Vec, l_max: u32, theta: f64, phi: f64) -> PyResult { + let coeffs = coeffs.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::manifold::spherical::spherical_harmonic_inverse(&coeffs, l_max, theta, phi))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Spectral convolution with a zonal kernel: multiplies each (l, m) +/// coefficient by sqrt(4 pi/(2l+1)) g_l0. +/// +/// Rust: `manifold::spherical::spherical_convolution` +#[pyfunction] +#[pyo3(name = "spherical_convolution", signature = (f_coeffs, g_coeffs, l_max))] +pub fn pyfn_spherical_convolution<'py>(py: Python<'py>, f_coeffs: Vec, g_coeffs: Vec, l_max: u32) -> PyResult>> { + let f_coeffs = f_coeffs.into_iter().map(|__e| __e.0).collect::>(); + let g_coeffs = g_coeffs.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::spherical_convolution(&f_coeffs, &g_coeffs, l_max)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// Spectral Laplace-Beltrami: multiplies each degree-l coefficient by +/// -l(l+1). +/// +/// Rust: `manifold::spherical::spherical_laplacian_spectral` +#[pyfunction] +#[pyo3(name = "spherical_laplacian_spectral", signature = (coeffs, l_max))] +pub fn pyfn_spherical_laplacian_spectral<'py>(py: Python<'py>, coeffs: Vec, l_max: u32) -> PyResult>> { + let coeffs = coeffs.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::spherical_laplacian_spectral(&coeffs, l_max)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// Heat flow on the sphere: coefficients decay as exp(-l(l+1) t). +/// +/// Rust: `manifold::spherical::spherical_heat_flow` +#[pyfunction] +#[pyo3(name = "spherical_heat_flow", signature = (coeffs, l_max, t))] +pub fn pyfn_spherical_heat_flow<'py>(py: Python<'py>, coeffs: Vec, l_max: u32, t: f64) -> PyResult>> { + let coeffs = coeffs.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::spherical_heat_flow(&coeffs, l_max, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// Simple spherical wavelet band-pass: difference of two heat kernels at +/// scales t and 2t applied spectrally. +/// +/// Rust: `manifold::spherical::spherical_wavelets` +#[pyfunction] +#[pyo3(name = "spherical_wavelets", signature = (coeffs, l_max, t))] +pub fn pyfn_spherical_wavelets<'py>(py: Python<'py>, coeffs: Vec, l_max: u32, t: f64) -> PyResult>> { + let coeffs = coeffs.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::spherical_wavelets(&coeffs, l_max, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// Number of HEALPix pixels: 12 nside^2. +/// +/// Rust: `manifold::spherical::healpix_npix` +#[pyfunction] +#[pyo3(name = "healpix_npix", signature = (nside))] +pub fn pyfn_healpix_npix(nside: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::healpix_npix(nside)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// HEALPix ring-scheme pixel index for direction (theta, phi). +/// +/// Rust: `manifold::spherical::healpix_ang2pix` +#[pyfunction] +#[pyo3(name = "healpix_ang2pix", signature = (nside, theta, phi))] +pub fn pyfn_healpix_ang2pix(nside: usize, theta: f64, phi: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::healpix_ang2pix(nside, theta, phi)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Center direction (theta, phi) of a HEALPix ring-scheme pixel. +/// +/// Rust: `manifold::spherical::healpix_pix2ang` +#[pyfunction] +#[pyo3(name = "healpix_pix2ang", signature = (nside, pix))] +pub fn pyfn_healpix_pix2ang(nside: usize, pix: usize) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::healpix_pix2ang(nside, pix)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Estimate of the Tammes-problem packing angle for n caps (empirical +/// asymptotic bound). +/// +/// Rust: `manifold::spherical::spherical_cap_packing` +#[pyfunction] +#[pyo3(name = "spherical_cap_packing", signature = (n))] +pub fn pyfn_spherical_cap_packing(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::spherical_cap_packing(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Thomson problem: minimize Coulomb energy of n charges by projected +/// gradient descent. Returns the final configuration. +/// +/// Rust: `manifold::spherical::thomson_problem` +#[pyfunction] +#[pyo3(name = "thomson_problem", signature = (n, iters, rng))] +pub fn pyfn_thomson_problem(n: usize, iters: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::thomson_problem(n, iters, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) +} + +/// Minimum pairwise angular distance of a spherical code. +/// +/// Rust: `manifold::spherical::spherical_code_min_angle` +#[pyfunction] +#[pyo3(name = "spherical_code_min_angle", signature = (points))] +pub fn pyfn_spherical_code_min_angle<'py>(py: Python<'py>, points: Vec) -> PyResult { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::manifold::spherical::spherical_code_min_angle(&points))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Rotate a point set by a rotation. +/// +/// Rust: `manifold::spherical::rotate_sphere_points` +#[pyfunction] +#[pyo3(name = "rotate_sphere_points", signature = (points, r))] +pub fn pyfn_rotate_sphere_points(points: Vec, r: crate::generated::types::PySo3) -> PyResult> { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let r = r.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::rotate_sphere_points(&points, &r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) +} + +/// Spherical k-means with cosine distance. Returns (centroids, labels). +/// +/// Rust: `manifold::spherical::spherical_kmeans` +#[pyfunction] +#[pyo3(name = "spherical_kmeans", signature = (points, k, iters, rng))] +pub fn pyfn_spherical_kmeans(points: Vec, k: usize, iters: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(Vec, Vec)> { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::spherical_kmeans(&points, k, iters, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>(), __v.1)) +} + +/// Von Mises-Fisher density on S2. +/// +/// Rust: `manifold::spherical::von_mises_fisher_pdf` +#[pyfunction] +#[pyo3(name = "von_mises_fisher_pdf", signature = (x, mu, kappa))] +pub fn pyfn_von_mises_fisher_pdf(x: crate::generated::types::PyVec3Arg, mu: crate::generated::types::PyVec3Arg, kappa: f64) -> PyResult { + let x = x.0; + let mu = mu.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::von_mises_fisher_pdf(x, mu, kappa)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Sample from the von Mises-Fisher distribution on S2 (Ulrich/Wood). +/// +/// Rust: `manifold::spherical::vmf_sample` +#[pyfunction] +#[pyo3(name = "vmf_sample", signature = (mu, kappa, rng))] +pub fn pyfn_vmf_sample(mu: crate::generated::types::PyVec3Arg, kappa: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mu = mu.0; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::vmf_sample(mu, kappa, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Fit (mu, kappa) of a von Mises-Fisher distribution from samples. +/// +/// Rust: `manifold::spherical::vmf_fit` +#[pyfunction] +#[pyo3(name = "vmf_fit", signature = (points))] +pub fn pyfn_vmf_fit(points: Vec) -> PyResult<(crate::generated::types::PyVec3, f64)> { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::vmf_fit(&points)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((crate::generated::types::PyVec3 { inner: __v.0 }, __v.1)) +} + +/// Kent (Fisher-Bingham 5-parameter) density up to normalization refinement: +/// f = C exp(kappa g1.x + beta ((g2.x)^2 - (g3.x)^2)). +/// +/// Rust: `manifold::spherical::kent_distribution_pdf` +#[pyfunction] +#[pyo3(name = "kent_distribution_pdf", signature = (x, g1, g2, g3, kappa, beta))] +pub fn pyfn_kent_distribution_pdf(x: crate::generated::types::PyVec3Arg, g1: crate::generated::types::PyVec3Arg, g2: crate::generated::types::PyVec3Arg, g3: crate::generated::types::PyVec3Arg, kappa: f64, beta: f64) -> PyResult { + let x = x.0; + let g1 = g1.0; + let g2 = g2.0; + let g3 = g3.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::kent_distribution_pdf(x, g1, g2, g3, kappa, beta)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Small spherical t-designs from tables: t = 1 (antipodes), 2 +/// (tetrahedron), 3 (octahedron), 5 (icosahedron). None otherwise. +/// +/// Rust: `manifold::spherical::spherical_t_design` +#[pyfunction] +#[pyo3(name = "spherical_t_design", signature = (t))] +pub fn pyfn_spherical_t_design(t: usize) -> PyResult>> { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::spherical_t_design(t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>())) +} + +/// Lebedev quadrature nodes and weights for orders 6, 14, and 26 (weights +/// sum to 1; integrates times 4 pi). +/// +/// Panics: +/// Panics for unsupported orders. +/// +/// Rust: `manifold::spherical::lebedev_quadrature` +#[pyfunction] +#[pyo3(name = "lebedev_quadrature", signature = (order))] +pub fn pyfn_lebedev_quadrature(order: usize) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spherical::lebedev_quadrature(order)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (crate::generated::types::PyVec3 { inner: __x.0 }, __x.1)).collect::>()) +} + +/// Product Gauss-Legendre x uniform-phi quadrature on the sphere: returns +/// (theta, phi, weight) with weights summing to 4 pi. +/// +/// Rust: `manifold::spherical::gauss_legendre_sphere` +#[pyfunction] +#[pyo3(name = "gauss_legendre_sphere", signature = (n_theta, n_phi))] +pub fn pyfn_gauss_legendre_sphere<'py>(py: Python<'py>, n_theta: usize, n_phi: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::manifold::spherical::gauss_legendre_sphere(n_theta, n_phi))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, __x.2)).collect::>()) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_sphere_distance_n, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sphere_geodesic_n, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sphere_exp_n, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sphere_log_n, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sphere_parallel_transport_n, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spherical_triangle_area, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spherical_triangle_angles, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spherical_law_of_cosines, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spherical_law_of_sines, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_haversine, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spherical_polygon_area, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spherical_centroid, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spherical_mean_weighted, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spherical_delaunay, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spherical_voronoi, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spherical_convex_hull, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_stereographic, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_inverse_stereographic, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_stereographic_n, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gnomonic, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gnomonic_inverse, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_orthographic, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_orthographic_inverse, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mercator, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mercator_inverse, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lambert_azimuthal_equal_area, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lambert_azimuthal_equal_area_inverse, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mollweide, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mollweide_inverse, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_equirectangular, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_equirectangular_inverse, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_azimuthal_equidistant, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_azimuthal_equidistant_inverse, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_robinson, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_robinson_inverse, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hopf_fibration, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hopf_fiber, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hopf_fiber_stereographic, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_s3_geodesic, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_s3_uniform_points, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sphere_uniform_points_n, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sphere_volume_n, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sphere_surface_n, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sphere_cap_area, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sphere_cap_volume, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spherical_harmonics_complex, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spherical_harmonic_transform, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spherical_harmonic_inverse, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spherical_convolution, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spherical_laplacian_spectral, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spherical_heat_flow, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spherical_wavelets, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_healpix_npix, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_healpix_ang2pix, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_healpix_pix2ang, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spherical_cap_packing, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_thomson_problem, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spherical_code_min_angle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rotate_sphere_points, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spherical_kmeans, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_von_mises_fisher_pdf, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_vmf_sample, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_vmf_fit, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kent_distribution_pdf, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spherical_t_design, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lebedev_quadrature, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gauss_legendre_sphere, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_manifold__vecn.rs b/bindings/python/src/generated/m_manifold__vecn.rs new file mode 100644 index 0000000..2ea96ea --- /dev/null +++ b/bindings/python/src/generated/m_manifold__vecn.rs @@ -0,0 +1,53 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Wedge product of antisymmetric forms: antisymmetrization of the tensor +/// product with the standard combinatorial normalization +/// (a ^ b)_{i...j...} = (p+q)!/(p! q!) Alt(a (x) b). +/// +/// Rust: `manifold::vecn::wedge` +#[pyfunction] +#[pyo3(name = "wedge", signature = (a, b))] +pub fn pyfn_wedge(a: crate::generated::types::PyTensorN, b: crate::generated::types::PyTensorN) -> PyResult { + let a = a.inner; + let b = b.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::vecn::wedge(&a, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTensorN { inner: __v }) +} + +/// Determinant of a square matrix via LU decomposition. +/// +/// Rust: `manifold::vecn::determinant_n` +#[pyfunction] +#[pyo3(name = "determinant_n", signature = (m))] +pub fn pyfn_determinant_n<'py>(py: Python<'py>, m: crate::generated::types::PyMatrixArg) -> PyResult { + let m = m.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::manifold::vecn::determinant_n(&m))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_wedge, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_determinant_n, m)?)?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_materials.rs b/bindings/python/src/generated/m_materials.rs new file mode 100644 index 0000000..c02bbf8 --- /dev/null +++ b/bindings/python/src/generated/m_materials.rs @@ -0,0 +1,22 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + Ok(()) +} diff --git a/bindings/python/src/generated/m_materials__common.rs b/bindings/python/src/generated/m_materials__common.rs new file mode 100644 index 0000000..93f4ba1 --- /dev/null +++ b/bindings/python/src/generated/m_materials__common.rs @@ -0,0 +1,49 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Looks up a material by name using case-insensitive ASCII comparison. +/// +/// Zero-allocation: uses `eq_ignore_ascii_case` instead of `to_lowercase`. +/// +/// Rust: `materials::common::by_name` +#[pyfunction] +#[pyo3(name = "by_name", signature = (name))] +pub fn pyfn_by_name(name: String) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::materials::common::by_name(&name)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyMaterial { inner: __x.clone() })) +} + +/// Returns a slice of all common engineering materials. +/// +/// Rust: `materials::common::all` +#[pyfunction] +#[pyo3(name = "all", signature = ())] +pub fn pyfn_all() -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::materials::common::all()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_vec().into_iter().map(|__x| crate::generated::types::PyMaterial { inner: __x }).collect::>()) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_by_name, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_all, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_materials__elements.rs b/bindings/python/src/generated/m_materials__elements.rs new file mode 100644 index 0000000..e4be6de --- /dev/null +++ b/bindings/python/src/generated/m_materials__elements.rs @@ -0,0 +1,97 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Looks up an element by its atomic number (1..=118). +/// +/// Returns `None` if `z` is outside the valid range. +/// This is an O(1) direct index lookup. +/// +/// Rust: `materials::elements::by_atomic_number` +#[pyfunction] +#[pyo3(name = "by_atomic_number", signature = (z))] +pub fn pyfn_by_atomic_number(z: u32) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::materials::elements::by_atomic_number(z)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyElementsElement { inner: __x.clone() })) +} + +/// Looks up an element by its chemical symbol (case-sensitive, e.g. "He", "Fe"). +/// +/// Performs a linear scan over the 118 elements with direct string comparison. +/// +/// Rust: `materials::elements::by_symbol` +#[pyfunction] +#[pyo3(name = "by_symbol", signature = (symbol))] +pub fn pyfn_by_symbol(symbol: String) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::materials::elements::by_symbol(&symbol)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyElementsElement { inner: __x.clone() })) +} + +/// O(1) lookup for commonly used element symbols, falling back to linear scan. +/// +/// Covers the 27 most frequently looked-up elements (H, He, Li, Be, B, C, N, +/// O, F, Ne, Na, Mg, Al, Si, P, S, Cl, Ar, K, Ca, Fe, Cu, Zn, Ag, Au, Pb, U) +/// via a match statement that returns a direct index into the static array. +/// All other symbols fall back to `by_symbol`. +/// +/// Rust: `materials::elements::by_symbol_static` +#[pyfunction] +#[pyo3(name = "by_symbol_static", signature = (symbol))] +pub fn pyfn_by_symbol_static(symbol: String) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::materials::elements::by_symbol_static(&symbol)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyElementsElement { inner: __x.clone() })) +} + +/// Looks up an element by name using case-insensitive ASCII comparison. +/// +/// Zero-allocation: uses `eq_ignore_ascii_case` instead of `to_lowercase`. +/// +/// Rust: `materials::elements::by_name` +#[pyfunction] +#[pyo3(name = "by_name", signature = (name))] +pub fn pyfn_by_name(name: String) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::materials::elements::by_name(&name)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyElementsElement { inner: __x.clone() })) +} + +/// Returns a slice of all 118 elements, ordered by atomic number. +/// +/// Rust: `materials::elements::all` +#[pyfunction] +#[pyo3(name = "all", signature = ())] +pub fn pyfn_all() -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::materials::elements::all()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_vec().into_iter().map(|__x| crate::generated::types::PyElementsElement { inner: __x }).collect::>()) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_by_atomic_number, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_by_symbol, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_by_symbol_static, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_by_name, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_all, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_materials__fluids.rs b/bindings/python/src/generated/m_materials__fluids.rs new file mode 100644 index 0000000..dd8b85d --- /dev/null +++ b/bindings/python/src/generated/m_materials__fluids.rs @@ -0,0 +1,49 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Looks up a fluid by name using case-insensitive ASCII comparison. +/// +/// Zero-allocation: uses `eq_ignore_ascii_case` instead of `to_lowercase`. +/// +/// Rust: `materials::fluids::by_name` +#[pyfunction] +#[pyo3(name = "by_name", signature = (name))] +pub fn pyfn_by_name(name: String) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::materials::fluids::by_name(&name)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyFluid { inner: __x.clone() })) +} + +/// Returns a slice of all fluids in the database. +/// +/// Rust: `materials::fluids::all` +#[pyfunction] +#[pyo3(name = "all", signature = ())] +pub fn pyfn_all() -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::materials::fluids::all()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_vec().into_iter().map(|__x| crate::generated::types::PyFluid { inner: __x }).collect::>()) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_by_name, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_all, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_materials__gases.rs b/bindings/python/src/generated/m_materials__gases.rs new file mode 100644 index 0000000..a460216 --- /dev/null +++ b/bindings/python/src/generated/m_materials__gases.rs @@ -0,0 +1,49 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Looks up a gas by name using case-insensitive ASCII comparison. +/// +/// Zero-allocation: uses `eq_ignore_ascii_case` instead of `to_lowercase`. +/// +/// Rust: `materials::gases::by_name` +#[pyfunction] +#[pyo3(name = "by_name", signature = (name))] +pub fn pyfn_by_name(name: String) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::materials::gases::by_name(&name)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyGas { inner: __x.clone() })) +} + +/// Returns a slice of all gases in the database. +/// +/// Rust: `materials::gases::all` +#[pyfunction] +#[pyo3(name = "all", signature = ())] +pub fn pyfn_all() -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::materials::gases::all()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_vec().into_iter().map(|__x| crate::generated::types::PyGas { inner: __x }).collect::>()) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_by_name, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_all, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_math.rs b/bindings/python/src/generated/m_math.rs new file mode 100644 index 0000000..46f9d3b --- /dev/null +++ b/bindings/python/src/generated/m_math.rs @@ -0,0 +1,24 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_math__constants.rs b/bindings/python/src/generated/m_math__constants.rs new file mode 100644 index 0000000..92cb864 --- /dev/null +++ b/bindings/python/src/generated/m_math__constants.rs @@ -0,0 +1,82 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add("PI", rust_physics_engine::math::constants::PI)?; + m.add("TAU", rust_physics_engine::math::constants::TAU)?; + m.add("E", rust_physics_engine::math::constants::E)?; + m.add("SQRT_2", rust_physics_engine::math::constants::SQRT_2)?; + m.add("LN_2", rust_physics_engine::math::constants::LN_2)?; + m.add("LN_10", rust_physics_engine::math::constants::LN_10)?; + m.add("C", rust_physics_engine::math::constants::C)?; + m.add("G", rust_physics_engine::math::constants::G)?; + m.add("H", rust_physics_engine::math::constants::H)?; + m.add("HBAR", rust_physics_engine::math::constants::HBAR)?; + m.add("K_B", rust_physics_engine::math::constants::K_B)?; + m.add("E_CHARGE", rust_physics_engine::math::constants::E_CHARGE)?; + m.add("N_A", rust_physics_engine::math::constants::N_A)?; + m.add("R", rust_physics_engine::math::constants::R)?; + m.add("FARADAY", rust_physics_engine::math::constants::FARADAY)?; + m.add("G_ACCEL", rust_physics_engine::math::constants::G_ACCEL)?; + m.add("M_ELECTRON", rust_physics_engine::math::constants::M_ELECTRON)?; + m.add("M_PROTON", rust_physics_engine::math::constants::M_PROTON)?; + m.add("M_NEUTRON", rust_physics_engine::math::constants::M_NEUTRON)?; + m.add("AMU", rust_physics_engine::math::constants::AMU)?; + m.add("EPSILON_0", rust_physics_engine::math::constants::EPSILON_0)?; + m.add("MU_0", rust_physics_engine::math::constants::MU_0)?; + m.add("K_E", rust_physics_engine::math::constants::K_E)?; + m.add("VACUUM_IMPEDANCE", rust_physics_engine::math::constants::VACUUM_IMPEDANCE)?; + m.add("MAGNETIC_FLUX_QUANTUM", rust_physics_engine::math::constants::MAGNETIC_FLUX_QUANTUM)?; + m.add("CONDUCTANCE_QUANTUM", rust_physics_engine::math::constants::CONDUCTANCE_QUANTUM)?; + m.add("VON_KLITZING", rust_physics_engine::math::constants::VON_KLITZING)?; + m.add("JOSEPHSON", rust_physics_engine::math::constants::JOSEPHSON)?; + m.add("SIGMA", rust_physics_engine::math::constants::SIGMA)?; + m.add("WIEN_DISPLACEMENT", rust_physics_engine::math::constants::WIEN_DISPLACEMENT)?; + m.add("FIRST_RADIATION", rust_physics_engine::math::constants::FIRST_RADIATION)?; + m.add("SECOND_RADIATION", rust_physics_engine::math::constants::SECOND_RADIATION)?; + m.add("RYDBERG", rust_physics_engine::math::constants::RYDBERG)?; + m.add("RYDBERG_ENERGY", rust_physics_engine::math::constants::RYDBERG_ENERGY)?; + m.add("BOHR_RADIUS", rust_physics_engine::math::constants::BOHR_RADIUS)?; + m.add("BOHR_MAGNETON", rust_physics_engine::math::constants::BOHR_MAGNETON)?; + m.add("NUCLEAR_MAGNETON", rust_physics_engine::math::constants::NUCLEAR_MAGNETON)?; + m.add("ALPHA", rust_physics_engine::math::constants::ALPHA)?; + m.add("ALPHA_INV", rust_physics_engine::math::constants::ALPHA_INV)?; + m.add("PLANCK_MASS", rust_physics_engine::math::constants::PLANCK_MASS)?; + m.add("PLANCK_LENGTH", rust_physics_engine::math::constants::PLANCK_LENGTH)?; + m.add("PLANCK_TIME", rust_physics_engine::math::constants::PLANCK_TIME)?; + m.add("PLANCK_TEMPERATURE", rust_physics_engine::math::constants::PLANCK_TEMPERATURE)?; + m.add("PLANCK_CHARGE", rust_physics_engine::math::constants::PLANCK_CHARGE)?; + m.add("SOLAR_MASS", rust_physics_engine::math::constants::SOLAR_MASS)?; + m.add("SOLAR_RADIUS", rust_physics_engine::math::constants::SOLAR_RADIUS)?; + m.add("SOLAR_LUMINOSITY", rust_physics_engine::math::constants::SOLAR_LUMINOSITY)?; + m.add("SOLAR_TEMPERATURE", rust_physics_engine::math::constants::SOLAR_TEMPERATURE)?; + m.add("EARTH_MASS", rust_physics_engine::math::constants::EARTH_MASS)?; + m.add("EARTH_RADIUS", rust_physics_engine::math::constants::EARTH_RADIUS)?; + m.add("EARTH_MOON_DISTANCE", rust_physics_engine::math::constants::EARTH_MOON_DISTANCE)?; + m.add("AU", rust_physics_engine::math::constants::AU)?; + m.add("LIGHT_YEAR", rust_physics_engine::math::constants::LIGHT_YEAR)?; + m.add("PARSEC", rust_physics_engine::math::constants::PARSEC)?; + m.add("HUBBLE", rust_physics_engine::math::constants::HUBBLE)?; + m.add("CMB_TEMPERATURE", rust_physics_engine::math::constants::CMB_TEMPERATURE)?; + m.add("EV_TO_JOULES", rust_physics_engine::math::constants::EV_TO_JOULES)?; + m.add("CALORIE", rust_physics_engine::math::constants::CALORIE)?; + m.add("ATM", rust_physics_engine::math::constants::ATM)?; + m.add("TORR", rust_physics_engine::math::constants::TORR)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_mesh.rs b/bindings/python/src/generated/m_mesh.rs new file mode 100644 index 0000000..e7ce55b --- /dev/null +++ b/bindings/python/src/generated/m_mesh.rs @@ -0,0 +1,23 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_mesh__analyze.rs b/bindings/python/src/generated/m_mesh__analyze.rs new file mode 100644 index 0000000..63b9d06 --- /dev/null +++ b/bindings/python/src/generated/m_mesh__analyze.rs @@ -0,0 +1,321 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Computes all `MeshStats` in one pass over the mesh. +/// +/// Rust: `mesh::analyze::stats` +#[pyfunction] +#[pyo3(name = "stats", signature = (m))] +pub fn pyfn_stats(m: crate::generated::types::PyMeshMesh) -> PyResult { + let m = m.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::analyze::stats(&m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMeshStats { inner: __v }) +} + +/// Euler characteristic χ = V − E + F. +/// +/// Rust: `mesh::analyze::euler_characteristic` +#[pyfunction] +#[pyo3(name = "euler_characteristic", signature = (m))] +pub fn pyfn_euler_characteristic(m: crate::generated::types::PyMeshMesh) -> PyResult { + let m = m.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::analyze::euler_characteristic(&m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// True when every edge belongs to one or two faces and the faces +/// around every vertex form a single fan (connected through shared +/// edges at that vertex). +/// +/// Rust: `mesh::analyze::is_manifold` +#[pyfunction] +#[pyo3(name = "is_manifold", signature = (m))] +pub fn pyfn_is_manifold(m: crate::generated::types::PyMeshMesh) -> PyResult { + let m = m.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::analyze::is_manifold(&m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// True when every edge belongs to exactly two faces. +/// +/// Rust: `mesh::analyze::is_closed` +#[pyfunction] +#[pyo3(name = "is_closed", signature = (m))] +pub fn pyfn_is_closed(m: crate::generated::types::PyMeshMesh) -> PyResult { + let m = m.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::analyze::is_closed(&m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// True when every shared edge is traversed once in each direction +/// (faces agree on winding). +/// +/// Rust: `mesh::analyze::is_consistently_oriented` +#[pyfunction] +#[pyo3(name = "is_consistently_oriented", signature = (m))] +pub fn pyfn_is_consistently_oriented(m: crate::generated::types::PyMeshMesh) -> PyResult { + let m = m.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::analyze::is_consistently_oriented(&m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Makes the orientation consistent per connected component by BFS +/// flipping. Returns false (leaving a best-effort result) when the +/// mesh is non-orientable (e.g. a Möbius band) or an edge has more +/// than two faces. A closed, consistently oriented component with +/// negative volume is flipped outward. +/// +/// Rust: `mesh::analyze::fix_orientation` +#[pyfunction] +#[pyo3(name = "fix_orientation", signature = (m))] +pub fn pyfn_fix_orientation(m: pyo3::PyRefMut<'_, crate::generated::types::PyMeshMesh>) -> PyResult { + let mut m = m; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::analyze::fix_orientation(&mut m.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Boundary loops as ordered vertex index cycles (each loop closed +/// implicitly; first vertex not repeated). +/// +/// Rust: `mesh::analyze::boundary_loops` +#[pyfunction] +#[pyo3(name = "boundary_loops", signature = (m))] +pub fn pyfn_boundary_loops<'py>(py: Python<'py>, m: crate::generated::types::PyMeshMesh) -> PyResult>> { + let m = m.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::mesh::analyze::boundary_loops(&m))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Splits into connected components (each with its own compacted +/// vertex list), ordered by smallest original vertex index. +/// +/// Rust: `mesh::analyze::connected_components` +#[pyfunction] +#[pyo3(name = "connected_components", signature = (m))] +pub fn pyfn_connected_components(m: crate::generated::types::PyMeshMesh) -> PyResult> { + let m = m.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::analyze::connected_components(&m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyMeshMesh { inner: __x }).collect::>()) +} + +/// Edges belonging to more than two faces. +/// +/// Rust: `mesh::analyze::non_manifold_edges` +#[pyfunction] +#[pyo3(name = "non_manifold_edges", signature = (m))] +pub fn pyfn_non_manifold_edges<'py>(py: Python<'py>, m: crate::generated::types::PyMeshMesh) -> PyResult> { + let m = m.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::mesh::analyze::non_manifold_edges(&m))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Pairs of faces with identical vertex sets (regardless of winding), +/// each pair reported once as `(earlier, later)`. +/// +/// Rust: `mesh::analyze::duplicate_faces` +#[pyfunction] +#[pyo3(name = "duplicate_faces", signature = (m))] +pub fn pyfn_duplicate_faces<'py>(py: Python<'py>, m: crate::generated::types::PyMeshMesh) -> PyResult> { + let m = m.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::mesh::analyze::duplicate_faces(&m))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Face pairs that intersect without sharing a vertex index. Pass the +/// mesh's BVH (`Mesh::build_bvh`) to prune candidate pairs. +/// +/// Rust: `mesh::analyze::self_intersections` +#[pyfunction] +#[pyo3(name = "self_intersections", signature = (m, bvh=None))] +pub fn pyfn_self_intersections<'py>(py: Python<'py>, m: crate::generated::types::PyMeshMesh, bvh: Option) -> PyResult> { + let m = m.inner; + let bvh = bvh.map(|__o| __o.inner); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::mesh::analyze::self_intersections(&m, bvh.as_ref().map(|__o| __o)))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Garland-Heckbert quadric error metric decimation ("Surface +/// Simplification Using Quadric Error Metrics", SIGGRAPH 1997): +/// greedily collapses the cheapest edge whose collapse keeps the mesh +/// manifold (link condition) and does not flip surviving faces, until +/// at most `target_faces` faces remain or no valid collapse exists. +/// +/// Rust: `mesh::analyze::decimate_edge_collapse` +#[pyfunction] +#[pyo3(name = "decimate_edge_collapse", signature = (m, target_faces))] +pub fn pyfn_decimate_edge_collapse(m: crate::generated::types::PyMeshMesh, target_faces: usize) -> PyResult { + let m = m.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::analyze::decimate_edge_collapse(&m, target_faces)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMeshMesh { inner: __v }) +} + +/// Number of distinct neighbors of each vertex. +/// +/// Rust: `mesh::analyze::vertex_valence` +#[pyfunction] +#[pyo3(name = "vertex_valence", signature = (m))] +pub fn pyfn_vertex_valence<'py>(py: Python<'py>, m: crate::generated::types::PyMeshMesh) -> PyResult> { + let m = m.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::mesh::analyze::vertex_valence(&m))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Angle between the normals of the two faces at each interior edge +/// (0 for coplanar faces), aligned with `Mesh::edges` order; +/// boundary and non-manifold edges get 0. +/// +/// Rust: `mesh::analyze::dihedral_angles` +#[pyfunction] +#[pyo3(name = "dihedral_angles", signature = (m))] +pub fn pyfn_dihedral_angles<'py>(py: Python<'py>, m: crate::generated::types::PyMeshMesh) -> PyResult> { + let m = m.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::mesh::analyze::dihedral_angles(&m))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Interior edges whose faces' normals differ by more than the +/// threshold angle (radians). +/// +/// Rust: `mesh::analyze::sharp_edges` +#[pyfunction] +#[pyo3(name = "sharp_edges", signature = (m, angle_threshold_rad))] +pub fn pyfn_sharp_edges<'py>(py: Python<'py>, m: crate::generated::types::PyMeshMesh, angle_threshold_rad: f64) -> PyResult> { + let m = m.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::mesh::analyze::sharp_edges(&m, angle_threshold_rad))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Integrated Gaussian curvature per vertex as the angle deficit: +/// 2π − Σ incident angles (π − Σ on the boundary). Summing over a +/// closed mesh gives exactly 2πχ (discrete Gauss-Bonnet). +/// +/// Rust: `mesh::analyze::discrete_gaussian_curvature` +#[pyfunction] +#[pyo3(name = "discrete_gaussian_curvature", signature = (m))] +pub fn pyfn_discrete_gaussian_curvature<'py>(py: Python<'py>, m: crate::generated::types::PyMeshMesh) -> PyResult> { + let m = m.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::mesh::analyze::discrete_gaussian_curvature(&m))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Discrete (unsigned) mean curvature per vertex via the cotangent +/// Laplacian: H = |Σ (cot α + cot β)(vᵢ − vⱼ)| / (4 A) with A the +/// mixed Voronoi vertex area (Meyer et al. 2003). Zero on boundary +/// vertices. +/// +/// Rust: `mesh::analyze::discrete_mean_curvature` +#[pyfunction] +#[pyo3(name = "discrete_mean_curvature", signature = (m))] +pub fn pyfn_discrete_mean_curvature<'py>(py: Python<'py>, m: crate::generated::types::PyMeshMesh) -> PyResult> { + let m = m.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::mesh::analyze::discrete_mean_curvature(&m))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Graph-shortest-path distance along mesh edges from `source` to +/// every vertex (an upper bound on true geodesic distance). +/// +/// Panics: +/// Panics when `source` is out of range. +/// +/// Rust: `mesh::analyze::geodesic_distance_dijkstra` +#[pyfunction] +#[pyo3(name = "geodesic_distance_dijkstra", signature = (m, source))] +pub fn pyfn_geodesic_distance_dijkstra<'py>(py: Python<'py>, m: crate::generated::types::PyMeshMesh, source: usize) -> PyResult> { + let m = m.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::mesh::analyze::geodesic_distance_dijkstra(&m, source))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// First-order fast marching on the triangle mesh (Kimmel & Sethian +/// 1998): distances propagate as planar wavefronts across triangles, +/// converging to the true geodesic distance under refinement (unlike +/// edge-graph Dijkstra). +/// +/// Panics: +/// Panics when `source` is out of range. +/// +/// Rust: `mesh::analyze::geodesic_distance_fast_marching` +#[pyfunction] +#[pyo3(name = "geodesic_distance_fast_marching", signature = (m, source))] +pub fn pyfn_geodesic_distance_fast_marching<'py>(py: Python<'py>, m: crate::generated::types::PyMeshMesh, source: usize) -> PyResult> { + let m = m.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::mesh::analyze::geodesic_distance_fast_marching(&m, source))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Shortest edge path between two vertices (inclusive); empty when +/// unreachable. +/// +/// Panics: +/// Panics when either endpoint is out of range. +/// +/// Rust: `mesh::analyze::geodesic_path` +#[pyfunction] +#[pyo3(name = "geodesic_path", signature = (m, from_, to))] +pub fn pyfn_geodesic_path<'py>(py: Python<'py>, m: crate::generated::types::PyMeshMesh, from_: usize, to: usize) -> PyResult> { + let m = m.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::mesh::analyze::geodesic_path(&m, from_, to))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_stats, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_euler_characteristic, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_is_manifold, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_is_closed, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_is_consistently_oriented, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fix_orientation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_boundary_loops, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_connected_components, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_non_manifold_edges, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_duplicate_faces, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_self_intersections, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_decimate_edge_collapse, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_vertex_valence, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dihedral_angles, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sharp_edges, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_discrete_gaussian_curvature, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_discrete_mean_curvature, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_geodesic_distance_dijkstra, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_geodesic_distance_fast_marching, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_geodesic_path, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_mesh__generate.rs b/bindings/python/src/generated/m_mesh__generate.rs new file mode 100644 index 0000000..605115d --- /dev/null +++ b/bindings/python/src/generated/m_mesh__generate.rs @@ -0,0 +1,276 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Latitude/longitude sphere: `rings` latitude bands, `segments` +/// meridians, poles as single vertices. Closed manifold (Euler +/// characteristic 2). +/// +/// Panics: +/// Panics unless `radius > 0`, `segments >= 3`, and `rings >= 2`. +/// +/// Rust: `mesh::generate::uv_sphere` +#[pyfunction] +#[pyo3(name = "uv_sphere", signature = (radius, segments, rings))] +pub fn pyfn_uv_sphere(radius: f64, segments: usize, rings: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::generate::uv_sphere(radius, segments, rings)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMeshMesh { inner: __v }) +} + +/// Geodesic sphere: an icosahedron subdivided `subdivisions` times, +/// vertices projected to the sphere. Closed manifold. +/// +/// Panics: +/// Panics unless `radius > 0`. +/// +/// Rust: `mesh::generate::icosphere` +#[pyfunction] +#[pyo3(name = "icosphere", signature = (radius, subdivisions))] +pub fn pyfn_icosphere(radius: f64, subdivisions: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::generate::icosphere(radius, subdivisions)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMeshMesh { inner: __v }) +} + +/// Axis-aligned box with the given half extents: 8 shared vertices, 12 +/// triangles. Closed manifold. +/// +/// Panics: +/// Panics unless every half extent is positive. +/// +/// Rust: `mesh::generate::box_mesh` +#[pyfunction] +#[pyo3(name = "box_mesh", signature = (half))] +pub fn pyfn_box_mesh(half: crate::generated::types::PyVec3Arg) -> PyResult { + let half = half.0; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::generate::box_mesh(half)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMeshMesh { inner: __v }) +} + +/// Cylinder along the y axis, centered at the origin, spanning +/// `[-height/2, height/2]`. With `capped`, both ends are closed by +/// fans sharing the rim vertices (closed manifold). +/// +/// Panics: +/// Panics unless `radius > 0`, `height > 0`, `segments >= 3`. +/// +/// Rust: `mesh::generate::cylinder` +#[pyfunction] +#[pyo3(name = "cylinder", signature = (radius, height, segments, capped))] +pub fn pyfn_cylinder(radius: f64, height: f64, segments: usize, capped: bool) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::generate::cylinder(radius, height, segments, capped)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMeshMesh { inner: __v }) +} + +/// Cone with its base disk in the y = 0 plane (centered at the origin) +/// and apex at `(0, height, 0)`. Closed manifold. +/// +/// Panics: +/// Panics unless `radius > 0`, `height > 0`, `segments >= 3`. +/// +/// Rust: `mesh::generate::cone` +#[pyfunction] +#[pyo3(name = "cone", signature = (radius, height, segments))] +pub fn pyfn_cone(radius: f64, height: f64, segments: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::generate::cone(radius, height, segments)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMeshMesh { inner: __v }) +} + +/// Torus around the y axis: tube of radius `minor` swept along a +/// circle of radius `major` in the xz plane. Closed manifold (Euler +/// characteristic 0). +/// +/// Panics: +/// Panics unless `0 < minor < major` and both segment counts are >= 3. +/// +/// Rust: `mesh::generate::torus` +#[pyfunction] +#[pyo3(name = "torus", signature = (major, minor, major_segs, minor_segs))] +pub fn pyfn_torus(major: f64, minor: f64, major_segs: usize, minor_segs: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::generate::torus(major, minor, major_segs, minor_segs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMeshMesh { inner: __v }) +} + +/// Flat grid in the xz plane centered at the origin, normals facing +/// +y, `nx` by `nz` cells. Open (has a boundary). +/// +/// Panics: +/// Panics unless `width > 0`, `depth > 0`, `nx >= 1`, `nz >= 1`. +/// +/// Rust: `mesh::generate::plane_grid` +#[pyfunction] +#[pyo3(name = "plane_grid", signature = (width, depth, nx, nz))] +pub fn pyfn_plane_grid(width: f64, depth: f64, nx: usize, nz: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::generate::plane_grid(width, depth, nx, nz)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMeshMesh { inner: __v }) +} + +/// Capsule along the y axis: a cylinder of length `height` between two +/// hemispherical caps of the given radius (`rings` latitude bands per +/// hemisphere). Closed manifold. +/// +/// Panics: +/// Panics unless `radius > 0`, `height >= 0`, `segments >= 3`, +/// `rings >= 1`. +/// +/// Rust: `mesh::generate::capsule` +#[pyfunction] +#[pyo3(name = "capsule", signature = (radius, height, segments, rings))] +pub fn pyfn_capsule(radius: f64, height: f64, segments: usize, rings: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::generate::capsule(radius, height, segments, rings)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMeshMesh { inner: __v }) +} + +/// Flat disk in the y = 0 plane centered at the origin, normal +y. +/// Open (has a boundary). +/// +/// Panics: +/// Panics unless `radius > 0` and `segments >= 3`. +/// +/// Rust: `mesh::generate::disk` +#[pyfunction] +#[pyo3(name = "disk", signature = (radius, segments))] +pub fn pyfn_disk(radius: f64, segments: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::generate::disk(radius, segments)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMeshMesh { inner: __v }) +} + +/// Tube of the given radius swept along a polyline using parallel +/// transport frames (rotation-minimizing, so the tube does not twist). +/// A closed path joins the last ring back to the first; an open path +/// leaves the ends uncapped. +/// +/// Panics: +/// Panics unless `radius > 0`, `segments >= 3`, and the path has at +/// least two points with nonzero consecutive tangents. +/// +/// Rust: `mesh::generate::tube_along_polyline` +#[pyfunction] +#[pyo3(name = "tube_along_polyline", signature = (path, radius, segments))] +pub fn pyfn_tube_along_polyline(path: crate::generated::types::PyPolyline, radius: f64, segments: usize) -> PyResult { + let path = path.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::generate::tube_along_polyline(&path, radius, segments)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMeshMesh { inner: __v }) +} + +/// Extrudes a simple polygon (in the xy plane) along +z from z = 0 to +/// z = `height`, with ear-clipped caps. Closed manifold for a simple +/// polygon. Clockwise input is treated as its counterclockwise +/// reversal. +/// +/// Panics: +/// Panics unless the polygon has at least 3 vertices and +/// `height > 0`. +/// +/// Rust: `mesh::generate::extrude_polygon` +#[pyfunction] +#[pyo3(name = "extrude_polygon", signature = (poly, height))] +pub fn pyfn_extrude_polygon(poly: crate::generated::types::PyPolygon2, height: f64) -> PyResult { + let poly = poly.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::generate::extrude_polygon(&poly, height)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMeshMesh { inner: __v }) +} + +/// Revolves a profile polyline around the y axis. Each profile point +/// `(x, y)` gives radius `x` at height `y`; points with `x == 0` +/// become poles. The profile should ascend in y for outward normals; +/// a profile that starts and ends on the axis yields a closed +/// manifold. +/// +/// Panics: +/// Panics unless the profile has >= 2 points, `segments >= 3`, and no +/// profile radius is negative. +/// +/// Rust: `mesh::generate::revolve_profile` +#[pyfunction] +#[pyo3(name = "revolve_profile", signature = (profile, segments))] +pub fn pyfn_revolve_profile(profile: Vec, segments: usize) -> PyResult { + let profile = profile.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::generate::revolve_profile(&profile, segments)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMeshMesh { inner: __v }) +} + +/// Height field surface: vertex `(i, j)` sits at +/// `(i * dx, heights[j * nx + i], j * dz)`, triangles facing +y for +/// positive `dx`, `dz`. Open. +/// +/// Panics: +/// Panics unless `nx >= 2`, `nz >= 2`, +/// `heights.len() == nx * nz`, and `dx, dz > 0`. +/// +/// Rust: `mesh::generate::heightfield` +#[pyfunction] +#[pyo3(name = "heightfield", signature = (heights, nx, nz, dx, dz))] +pub fn pyfn_heightfield(heights: Vec, nx: usize, nz: usize, dx: f64, dz: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::generate::heightfield(&heights, nx, nz, dx, dz)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMeshMesh { inner: __v }) +} + +/// Samples a parametric surface on an `nu` by `nv` cell grid. +/// `close_u`/`close_v` wrap the respective direction (the last row of +/// samples is omitted and faces reference the first, so periodic +/// surfaces come out watertight). Triangles are wound so normals +/// follow `∂f/∂u × ∂f/∂v`. +/// +/// Panics: +/// Panics unless `nu >= 1`, `nv >= 1` (>= 3 for a closed direction) +/// and each range is nonempty. +/// +/// Rust: `mesh::generate::from_parametric` +#[pyfunction] +#[pyo3(name = "from_parametric", signature = (f, u_range, v_range, nu, nv, close_u, close_v))] +pub fn pyfn_from_parametric(f: pyo3::Py, u_range: (f64, f64), v_range: (f64, f64), nu: usize, nv: usize, close_u: bool, close_v: bool) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64, __a1: f64| -> rust_physics_engine::math::Vec3 { { let __r = __cb.call::<_, crate::generated::types::PyVec3Arg>((__a0, __a1), crate::generated::types::PyVec3Arg(rust_physics_engine::math::Vec3 { x: f64::NAN, y: f64::NAN, z: f64::NAN })); __r.0 } } }; + let u_range = (u_range.0, u_range.1); + let v_range = (v_range.0, v_range.1); + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::generate::from_parametric(&f, u_range, v_range, nu, nv, close_u, close_v)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMeshMesh { inner: __v }) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_uv_sphere, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_icosphere, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_box_mesh, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cylinder, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cone, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_torus, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_plane_grid, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_capsule, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_disk, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tube_along_polyline, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_extrude_polygon, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_revolve_profile, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_heightfield, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_from_parametric, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_mesh__isosurface.rs b/bindings/python/src/generated/m_mesh__isosurface.rs new file mode 100644 index 0000000..ddd3441 --- /dev/null +++ b/bindings/python/src/generated/m_mesh__isosurface.rs @@ -0,0 +1,159 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// All isocontour crossings as directed segments (inside on the left). +/// +/// Rust: `mesh::isosurface::marching_squares` +#[pyfunction] +#[pyo3(name = "marching_squares", signature = (field, iso))] +pub fn pyfn_marching_squares(field: crate::generated::types::PyIsosurfaceScalarField2, iso: f64) -> PyResult> { + let field = field.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::isosurface::marching_squares(&field, iso)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyPrimitivesSegment2 { inner: __x }).collect::>()) +} + +/// Isocontours joined into polylines. Closed loops repeat their first +/// point at the end; open chains (which begin and end on the grid +/// boundary) do not. Inside (< iso) lies on the left of the direction +/// of travel. +/// +/// Rust: `mesh::isosurface::marching_squares_polylines` +#[pyfunction] +#[pyo3(name = "marching_squares_polylines", signature = (field, iso))] +pub fn pyfn_marching_squares_polylines(field: crate::generated::types::PyIsosurfaceScalarField2, iso: f64) -> PyResult>> { + let field = field.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::isosurface::marching_squares_polylines(&field, iso)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()).collect::>()) +} + +/// Joined contours for each requested level. +/// +/// Rust: `mesh::isosurface::contour_levels` +#[pyfunction] +#[pyo3(name = "contour_levels", signature = (field, levels))] +pub fn pyfn_contour_levels(field: crate::generated::types::PyIsosurfaceScalarField2, levels: Vec) -> PyResult>)>> { + let field = field.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::isosurface::contour_levels(&field, &levels)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1.into_iter().map(|__x| __x.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()).collect::>())).collect::>()) +} + +/// Extracts the isosurface by marching cubes (Lorensen & Cline 1987; +/// the case table is generated by face-consistent cycle construction, +/// see `mc_table`). Output vertices are shared across cells (keyed +/// by grid edge), so the mesh is watertight wherever the surface does +/// not leave the grid. +/// +/// Rust: `mesh::isosurface::marching_cubes` +#[pyfunction] +#[pyo3(name = "marching_cubes", signature = (field, iso))] +pub fn pyfn_marching_cubes(field: crate::generated::types::PyIsosurfaceScalarField3, iso: f64) -> PyResult { + let field = field.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::isosurface::marching_cubes(&field, iso)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMeshMesh { inner: __v }) +} + +/// Extracts the isosurface by marching tetrahedra over the Freudenthal +/// (Kuhn) 6-tetrahedra decomposition of each cell. The decomposition +/// cuts every cell face along its min-to-max diagonal, which depends +/// only on global grid coordinates, so adjacent cells agree and the +/// output is watertight. No ambiguous cases exist; the surface is +/// finer (more triangles) than marching cubes for the same grid. +/// +/// Rust: `mesh::isosurface::marching_tetrahedra` +#[pyfunction] +#[pyo3(name = "marching_tetrahedra", signature = (field, iso))] +pub fn pyfn_marching_tetrahedra(field: crate::generated::types::PyIsosurfaceScalarField3, iso: f64) -> PyResult { + let field = field.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::isosurface::marching_tetrahedra(&field, iso)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMeshMesh { inner: __v }) +} + +/// Naive surface nets (Gibson 1998): each crossed cell gets the +/// centroid of its edge crossings; quads connect cells around crossed +/// interior grid edges. Smoother than marching cubes at the same +/// resolution but not guaranteed to stay inside each cell. +/// +/// Rust: `mesh::isosurface::surface_nets` +#[pyfunction] +#[pyo3(name = "surface_nets", signature = (field, iso))] +pub fn pyfn_surface_nets(field: crate::generated::types::PyIsosurfaceScalarField3, iso: f64) -> PyResult { + let field = field.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::isosurface::surface_nets(&field, iso)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMeshMesh { inner: __v }) +} + +/// Dual contouring (Ju et al. 2002): like surface nets, but each cell +/// vertex minimizes the quadratic error function +/// Σ (nᵢ · (x − pᵢ))² over the cell's edge crossings, with normals +/// supplied by `normals` (e.g. an SDF gradient). Reproduces sharp +/// features. The QEF is solved by regularized normal equations +/// (Gaussian elimination with partial pivoting), and the vertex is +/// clamped into its cell. +/// +/// Rust: `mesh::isosurface::dual_contouring` +#[pyfunction] +#[pyo3(name = "dual_contouring", signature = (field, iso, normals))] +pub fn pyfn_dual_contouring(field: crate::generated::types::PyIsosurfaceScalarField3, iso: f64, normals: pyo3::Py) -> PyResult { + let field = field.inner; + let __cb_normals = std::rc::Rc::new(crate::runtime::Callback::new(normals)); + let normals = { let __cb = __cb_normals.clone(); move |__a0: rust_physics_engine::math::Vec3| -> rust_physics_engine::math::Vec3 { { let __r = __cb.call::<_, crate::generated::types::PyVec3Arg>((crate::generated::types::PyVec3 { inner: __a0 },), crate::generated::types::PyVec3Arg(rust_physics_engine::math::Vec3 { x: f64::NAN, y: f64::NAN, z: f64::NAN })); __r.0 } } }; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::isosurface::dual_contouring(&field, iso, &normals)); + crate::runtime::callback::check(&[&__cb_normals], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMeshMesh { inner: __v }) +} + +/// Polygonizes a metaball (blobby) surface: field +/// Σ rᵢ² / |p − cᵢ|² compared against `threshold`, marched on a +/// `res`³ grid over `bounds`. Larger `threshold` shrinks the blobs. +/// +/// Panics: +/// Panics unless `threshold > 0`, `res >= 2`, and `centers` is +/// nonempty. +/// +/// Rust: `mesh::isosurface::metaballs` +#[pyfunction] +#[pyo3(name = "metaballs", signature = (centers, bounds, res, threshold))] +pub fn pyfn_metaballs(centers: Vec<(crate::generated::types::PyVec3Arg, f64)>, bounds: crate::generated::types::PyAabb, res: usize, threshold: f64) -> PyResult { + let centers = centers.into_iter().map(|__e| (__e.0.0, __e.1)).collect::>(); + let bounds = bounds.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::isosurface::metaballs(¢ers, &bounds, res, threshold)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMeshMesh { inner: __v }) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_marching_squares, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_marching_squares_polylines, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_contour_levels, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_marching_cubes, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_marching_tetrahedra, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_surface_nets, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dual_contouring, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_metaballs, m)?)?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_mesh__parameterize.rs b/bindings/python/src/generated/m_mesh__parameterize.rs new file mode 100644 index 0000000..4ccabc9 --- /dev/null +++ b/bindings/python/src/generated/m_mesh__parameterize.rs @@ -0,0 +1,159 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Spherical texture coordinates for a (genus-0) mesh: each vertex +/// direction from the centroid maps to +/// u = ½ + atan2(d_y, d_x)/2π, v = acos(d_z)/π. Writes `m.uvs`. +/// +/// Panics: +/// Panics on an empty mesh. +/// +/// Rust: `mesh::parameterize::spherical_uv` +#[pyfunction] +#[pyo3(name = "spherical_uv", signature = (m))] +pub fn pyfn_spherical_uv(m: pyo3::PyRefMut<'_, crate::generated::types::PyMeshMesh>) -> PyResult<()> { + let mut m = m; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::parameterize::spherical_uv(&mut m.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) +} + +/// Planar projection along `normal`, normalized so the projected +/// bounding box spans [0, 1]². Writes `m.uvs`. +/// +/// Panics: +/// Panics on an empty mesh or a zero normal. +/// +/// Rust: `mesh::parameterize::planar_uv` +#[pyfunction] +#[pyo3(name = "planar_uv", signature = (m, normal))] +pub fn pyfn_planar_uv(m: pyo3::PyRefMut<'_, crate::generated::types::PyMeshMesh>, normal: crate::generated::types::PyVec3Arg) -> PyResult<()> { + let mut m = m; + let normal = normal.0; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::parameterize::planar_uv(&mut m.inner, normal)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) +} + +/// Cylindrical projection about `axis` through the centroid: +/// u = angle/2π around the axis, v = normalized height along it. +/// Writes `m.uvs`. +/// +/// Panics: +/// Panics on an empty mesh or a zero axis. +/// +/// Rust: `mesh::parameterize::cylindrical_uv` +#[pyfunction] +#[pyo3(name = "cylindrical_uv", signature = (m, axis))] +pub fn pyfn_cylindrical_uv(m: pyo3::PyRefMut<'_, crate::generated::types::PyMeshMesh>, axis: crate::generated::types::PyVec3Arg) -> PyResult<()> { + let mut m = m; + let axis = axis.0; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::parameterize::cylindrical_uv(&mut m.inner, axis)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) +} + +/// Harmonic (cotangent-weight) parameterization of a disk-topology +/// mesh: the boundary loop is pinned to the target shape and the +/// interior solves the discrete Laplace equation, giving the unique +/// harmonic extension (identity up to similarity on flat meshes; +/// convexity of the target keeps the map injective for meshes with +/// non-negative weights). Returns one UV per vertex. +/// +/// Panics: +/// Panics unless the mesh has disk topology and the linear solve +/// converges. +/// +/// Rust: `mesh::parameterize::harmonic_parameterization` +#[pyfunction] +#[pyo3(name = "harmonic_parameterization", signature = (m, boundary_shape))] +pub fn pyfn_harmonic_parameterization(m: crate::generated::types::PyMeshMesh, boundary_shape: crate::generated::types::PyBoundaryShape) -> PyResult> { + let m = m.inner; + let boundary_shape = boundary_shape.to_rust(); + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::parameterize::harmonic_parameterization(&m, boundary_shape)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// Least-squares conformal map: minimizes the conformal energy +/// Σ_T A_T |∇u rotated 90° − ∇v|² with two pinned vertices removing +/// the similarity ambiguity. Solved through the normal equations by +/// conjugate gradients. Returns one UV per vertex. +/// +/// Panics: +/// Panics unless the two pins are distinct valid vertices and the +/// solve converges. +/// +/// Rust: `mesh::parameterize::lscm` +#[pyfunction] +#[pyo3(name = "lscm", signature = (m, pinned))] +pub fn pyfn_lscm(m: crate::generated::types::PyMeshMesh, pinned: Vec<(usize, crate::generated::types::PyVec2Arg)>) -> PyResult> { + let m = m.inner; + let pinned = <[(usize, rust_physics_engine::math::Vec2); 2]>::try_from(pinned.into_iter().map(|__e| (__e.0, __e.1.0)).collect::>()).map_err(|__v: Vec<(usize, rust_physics_engine::math::Vec2)>| pyo3::exceptions::PyValueError::new_err(format!("expected 2 values, got {}", __v.len())))?; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::parameterize::lscm(&m, pinned)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// Per-triangle conformal distortion σ₁/σ₂ of the parameterization +/// (1 = angle-preserving; degenerate triangles report 1). +/// +/// Panics: +/// Panics unless `uv` has one entry per vertex. +/// +/// Rust: `mesh::parameterize::conformal_distortion` +#[pyfunction] +#[pyo3(name = "conformal_distortion", signature = (m, uv))] +pub fn pyfn_conformal_distortion<'py>(py: Python<'py>, m: crate::generated::types::PyMeshMesh, uv: Vec) -> PyResult> { + let m = m.inner; + let uv = uv.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::mesh::parameterize::conformal_distortion(&m, &uv))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Per-triangle area distortion: the UV/3-D area ratio normalized by +/// the global ratio, so a globally scaled isometry reports 1 +/// everywhere. +/// +/// Panics: +/// Panics unless `uv` has one entry per vertex. +/// +/// Rust: `mesh::parameterize::area_distortion` +#[pyfunction] +#[pyo3(name = "area_distortion", signature = (m, uv))] +pub fn pyfn_area_distortion<'py>(py: Python<'py>, m: crate::generated::types::PyMeshMesh, uv: Vec) -> PyResult> { + let m = m.inner; + let uv = uv.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::mesh::parameterize::area_distortion(&m, &uv))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_spherical_uv, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_planar_uv, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cylindrical_uv, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_harmonic_parameterization, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lscm, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_conformal_distortion, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_area_distortion, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_mesh__subdivide.rs b/bindings/python/src/generated/m_mesh__subdivide.rs new file mode 100644 index 0000000..9703744 --- /dev/null +++ b/bindings/python/src/generated/m_mesh__subdivide.rs @@ -0,0 +1,156 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// One level of Loop subdivision (Loop 1987): each triangle splits +/// into four; new edge vertices use the 3/8-1/8 stencil, old vertices +/// the valence-dependent β stencil, with the standard boundary rules +/// (midpoint and 3/4-1/8-1/8). +/// +/// Rust: `mesh::subdivide::loop_subdivide` +#[pyfunction] +#[pyo3(name = "loop_subdivide", signature = (m))] +pub fn pyfn_loop_subdivide(m: crate::generated::types::PyMeshMesh) -> PyResult { + let m = m.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::subdivide::loop_subdivide(&m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMeshMesh { inner: __v }) +} + +/// `n` levels of Loop subdivision. +/// +/// Rust: `mesh::subdivide::loop_subdivide_n` +#[pyfunction] +#[pyo3(name = "loop_subdivide_n", signature = (m, n))] +pub fn pyfn_loop_subdivide_n(m: crate::generated::types::PyMeshMesh, n: usize) -> PyResult { + let m = m.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::subdivide::loop_subdivide_n(&m, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMeshMesh { inner: __v }) +} + +/// One level of Catmull-Clark subdivision (Catmull & Clark 1978): +/// face points at centroids, edge points averaging endpoints and face +/// points, old vertices moved by (F + 2R + (n-3)P)/n, standard +/// boundary rules. Each quad becomes four. +/// +/// Rust: `mesh::subdivide::catmull_clark` +#[pyfunction] +#[pyo3(name = "catmull_clark", signature = (q))] +pub fn pyfn_catmull_clark(q: crate::generated::types::PyQuadMesh) -> PyResult { + let q = q.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::subdivide::catmull_clark(&q)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuadMesh { inner: __v }) +} + +/// `n` levels of Catmull-Clark. +/// +/// Rust: `mesh::subdivide::catmull_clark_n` +#[pyfunction] +#[pyo3(name = "catmull_clark_n", signature = (q, n))] +pub fn pyfn_catmull_clark_n(q: crate::generated::types::PyQuadMesh, n: usize) -> PyResult { + let q = q.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::subdivide::catmull_clark_n(&q, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuadMesh { inner: __v }) +} + +/// One level of sqrt(3) subdivision (Kobbelt 2000): a centroid vertex +/// per face, original interior edges flipped, old vertices smoothed by +/// the α_n stencil (boundary vertices stay). +/// +/// Rust: `mesh::subdivide::sqrt3_subdivide` +#[pyfunction] +#[pyo3(name = "sqrt3_subdivide", signature = (m))] +pub fn pyfn_sqrt3_subdivide(m: crate::generated::types::PyMeshMesh) -> PyResult { + let m = m.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::subdivide::sqrt3_subdivide(&m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMeshMesh { inner: __v }) +} + +/// One 1-to-4 split at edge midpoints with no repositioning: geometry +/// is unchanged (flat faces stay flat). +/// +/// Rust: `mesh::subdivide::midpoint_subdivide` +#[pyfunction] +#[pyo3(name = "midpoint_subdivide", signature = (m))] +pub fn pyfn_midpoint_subdivide(m: crate::generated::types::PyMeshMesh) -> PyResult { + let m = m.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::subdivide::midpoint_subdivide(&m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMeshMesh { inner: __v }) +} + +/// Uniform-weight Laplacian smoothing: each iteration moves every +/// vertex by `lambda` toward the average of its neighbors. Shrinks +/// closed meshes. +/// +/// Rust: `mesh::subdivide::laplacian_smooth` +#[pyfunction] +#[pyo3(name = "laplacian_smooth", signature = (m, iterations, lambda_))] +pub fn pyfn_laplacian_smooth(m: pyo3::PyRefMut<'_, crate::generated::types::PyMeshMesh>, iterations: usize, lambda_: f64) -> PyResult<()> { + let mut m = m; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::subdivide::laplacian_smooth(&mut m.inner, iterations, lambda_)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) +} + +/// Taubin's λ|μ smoothing (Taubin 1995): alternating positive +/// (`lambda`) and negative (`mu`, with `mu < -lambda` typically) +/// steps smooth without significant shrinkage. +/// +/// Rust: `mesh::subdivide::taubin_smooth` +#[pyfunction] +#[pyo3(name = "taubin_smooth", signature = (m, iterations, lambda_, mu))] +pub fn pyfn_taubin_smooth(m: pyo3::PyRefMut<'_, crate::generated::types::PyMeshMesh>, iterations: usize, lambda_: f64, mu: f64) -> PyResult<()> { + let mut m = m; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::subdivide::taubin_smooth(&mut m.inner, iterations, lambda_, mu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) +} + +/// HC-Laplacian smoothing (Vollmer, Mencl & Müller 1999): a Laplacian +/// step followed by a correction that pushes points back toward a +/// blend of their original (`alpha`) and previous positions, the +/// correction itself averaged over neighbors (`beta`). +/// +/// Rust: `mesh::subdivide::hc_laplacian_smooth` +#[pyfunction] +#[pyo3(name = "hc_laplacian_smooth", signature = (m, iterations, alpha, beta))] +pub fn pyfn_hc_laplacian_smooth(m: pyo3::PyRefMut<'_, crate::generated::types::PyMeshMesh>, iterations: usize, alpha: f64, beta: f64) -> PyResult<()> { + let mut m = m; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::subdivide::hc_laplacian_smooth(&mut m.inner, iterations, alpha, beta)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_loop_subdivide, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_loop_subdivide_n, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_catmull_clark, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_catmull_clark_n, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sqrt3_subdivide, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_midpoint_subdivide, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_laplacian_smooth, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_taubin_smooth, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hc_laplacian_smooth, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_mesh__surfaces.rs b/bindings/python/src/generated/m_mesh__surfaces.rs new file mode 100644 index 0000000..eafb416 --- /dev/null +++ b/bindings/python/src/generated/m_mesh__surfaces.rs @@ -0,0 +1,309 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Point of the surface of revolution of a planar profile +/// `t ↦ (radius, height)` rotated by `theta` around the y axis. +/// +/// Rust: `mesh::surfaces::surface_of_revolution` +#[pyfunction] +#[pyo3(name = "surface_of_revolution", signature = (profile, t, theta))] +pub fn pyfn_surface_of_revolution(profile: pyo3::Py, t: f64, theta: f64) -> PyResult { + let __cb_profile = std::rc::Rc::new(crate::runtime::Callback::new(profile)); + let profile = { let __cb = __cb_profile.clone(); move |__a0: f64| -> rust_physics_engine::math::Vec2 { { let __r = __cb.call::<_, crate::generated::types::PyVec2Arg>((__a0,), crate::generated::types::PyVec2Arg(rust_physics_engine::math::Vec2 { x: f64::NAN, y: f64::NAN })); __r.0 } } }; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::surfaces::surface_of_revolution(&profile, t, theta)); + crate::runtime::callback::check(&[&__cb_profile], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Ruled surface: linear blend between two curves, +/// `(1 − v) c1(u) + v c2(u)`. +/// +/// Rust: `mesh::surfaces::ruled_surface` +#[pyfunction] +#[pyo3(name = "ruled_surface", signature = (c1, c2, u, v))] +pub fn pyfn_ruled_surface(c1: pyo3::Py, c2: pyo3::Py, u: f64, v: f64) -> PyResult { + let __cb_c1 = std::rc::Rc::new(crate::runtime::Callback::new(c1)); + let c1 = { let __cb = __cb_c1.clone(); move |__a0: f64| -> rust_physics_engine::math::Vec3 { { let __r = __cb.call::<_, crate::generated::types::PyVec3Arg>((__a0,), crate::generated::types::PyVec3Arg(rust_physics_engine::math::Vec3 { x: f64::NAN, y: f64::NAN, z: f64::NAN })); __r.0 } } }; + let __cb_c2 = std::rc::Rc::new(crate::runtime::Callback::new(c2)); + let c2 = { let __cb = __cb_c2.clone(); move |__a0: f64| -> rust_physics_engine::math::Vec3 { { let __r = __cb.call::<_, crate::generated::types::PyVec3Arg>((__a0,), crate::generated::types::PyVec3Arg(rust_physics_engine::math::Vec3 { x: f64::NAN, y: f64::NAN, z: f64::NAN })); __r.0 } } }; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::surfaces::ruled_surface(&c1, &c2, u, v)); + crate::runtime::callback::check(&[&__cb_c1, &__cb_c2], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Bilinearly blended Coons patch interpolating four boundary curves: +/// `c0` (v = 0), `c1` (v = 1) over u, and `d0` (u = 0), `d1` (u = 1) +/// over v. The curves must agree at the corners. +/// +/// Rust: `mesh::surfaces::coons_patch` +#[pyfunction] +#[pyo3(name = "coons_patch", signature = (c0, c1, d0, d1, u, v))] +pub fn pyfn_coons_patch(c0: pyo3::Py, c1: pyo3::Py, d0: pyo3::Py, d1: pyo3::Py, u: f64, v: f64) -> PyResult { + let __cb_c0 = std::rc::Rc::new(crate::runtime::Callback::new(c0)); + let c0 = { let __cb = __cb_c0.clone(); move |__a0: f64| -> rust_physics_engine::math::Vec3 { { let __r = __cb.call::<_, crate::generated::types::PyVec3Arg>((__a0,), crate::generated::types::PyVec3Arg(rust_physics_engine::math::Vec3 { x: f64::NAN, y: f64::NAN, z: f64::NAN })); __r.0 } } }; + let __cb_c1 = std::rc::Rc::new(crate::runtime::Callback::new(c1)); + let c1 = { let __cb = __cb_c1.clone(); move |__a0: f64| -> rust_physics_engine::math::Vec3 { { let __r = __cb.call::<_, crate::generated::types::PyVec3Arg>((__a0,), crate::generated::types::PyVec3Arg(rust_physics_engine::math::Vec3 { x: f64::NAN, y: f64::NAN, z: f64::NAN })); __r.0 } } }; + let __cb_d0 = std::rc::Rc::new(crate::runtime::Callback::new(d0)); + let d0 = { let __cb = __cb_d0.clone(); move |__a0: f64| -> rust_physics_engine::math::Vec3 { { let __r = __cb.call::<_, crate::generated::types::PyVec3Arg>((__a0,), crate::generated::types::PyVec3Arg(rust_physics_engine::math::Vec3 { x: f64::NAN, y: f64::NAN, z: f64::NAN })); __r.0 } } }; + let __cb_d1 = std::rc::Rc::new(crate::runtime::Callback::new(d1)); + let d1 = { let __cb = __cb_d1.clone(); move |__a0: f64| -> rust_physics_engine::math::Vec3 { { let __r = __cb.call::<_, crate::generated::types::PyVec3Arg>((__a0,), crate::generated::types::PyVec3Arg(rust_physics_engine::math::Vec3 { x: f64::NAN, y: f64::NAN, z: f64::NAN })); __r.0 } } }; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::surfaces::coons_patch(&c0, &c1, &d0, &d1, u, v)); + crate::runtime::callback::check(&[&__cb_c0, &__cb_c1, &__cb_d0, &__cb_d1], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Fundamental forms by central finite differences with step `h`. +/// +/// Panics: +/// Panics unless `h > 0` and the surface is regular (nonzero +/// `f_u × f_v`) at `(u, v)`. +/// +/// Rust: `mesh::surfaces::fundamental_forms` +#[pyfunction] +#[pyo3(name = "fundamental_forms", signature = (f, u, v, h))] +pub fn pyfn_fundamental_forms(f: pyo3::Py, u: f64, v: f64, h: f64) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64, __a1: f64| -> rust_physics_engine::math::Vec3 { { let __r = __cb.call::<_, crate::generated::types::PyVec3Arg>((__a0, __a1), crate::generated::types::PyVec3Arg(rust_physics_engine::math::Vec3 { x: f64::NAN, y: f64::NAN, z: f64::NAN })); __r.0 } } }; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::surfaces::fundamental_forms(&f, u, v, h)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyFundamentalForms { inner: __v }) +} + +/// Gaussian curvature K = (LN − M²) / (EG − F²). +/// +/// Rust: `mesh::surfaces::gaussian_curvature` +#[pyfunction] +#[pyo3(name = "gaussian_curvature", signature = (forms))] +pub fn pyfn_gaussian_curvature(forms: crate::generated::types::PyFundamentalFormsArg) -> PyResult { + let forms = forms.0; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::surfaces::gaussian_curvature(&forms)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Mean curvature H = (EN − 2FM + GL) / (2 (EG − F²)). +/// +/// Rust: `mesh::surfaces::mean_curvature` +#[pyfunction] +#[pyo3(name = "mean_curvature", signature = (forms))] +pub fn pyfn_mean_curvature(forms: crate::generated::types::PyFundamentalFormsArg) -> PyResult { + let forms = forms.0; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::surfaces::mean_curvature(&forms)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Principal curvatures `(κ₁, κ₂)` with κ₁ ≥ κ₂: +/// H ± sqrt(H² − K). +/// +/// Rust: `mesh::surfaces::principal_curvatures` +#[pyfunction] +#[pyo3(name = "principal_curvatures", signature = (forms))] +pub fn pyfn_principal_curvatures(forms: crate::generated::types::PyFundamentalFormsArg) -> PyResult<(f64, f64)> { + let forms = forms.0; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::surfaces::principal_curvatures(&forms)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Surface area of a parametric patch by the midpoint rule on +/// `nu` x `nv` cells: Σ |f_u × f_v| du dv, partials by central +/// differences at each cell midpoint. +/// +/// Panics: +/// Panics unless `nu >= 1` and `nv >= 1`. +/// +/// Rust: `mesh::surfaces::surface_area_parametric` +#[pyfunction] +#[pyo3(name = "surface_area_parametric", signature = (f, u_range, v_range, nu, nv))] +pub fn pyfn_surface_area_parametric(f: pyo3::Py, u_range: (f64, f64), v_range: (f64, f64), nu: usize, nv: usize) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64, __a1: f64| -> rust_physics_engine::math::Vec3 { { let __r = __cb.call::<_, crate::generated::types::PyVec3Arg>((__a0, __a1), crate::generated::types::PyVec3Arg(rust_physics_engine::math::Vec3 { x: f64::NAN, y: f64::NAN, z: f64::NAN })); __r.0 } } }; + let u_range = (u_range.0, u_range.1); + let v_range = (v_range.0, v_range.1); + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::surfaces::surface_area_parametric(&f, u_range, v_range, nu, nv)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Möbius strip of center radius `r` and half-width `w`: +/// u ∈ [0, 2π) around, v ∈ [−1, 1] across. +/// +/// Rust: `mesh::surfaces::mobius_strip` +#[pyfunction] +#[pyo3(name = "mobius_strip", signature = (u, v, r, w))] +pub fn pyfn_mobius_strip(u: f64, v: f64, r: f64, w: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::surfaces::mobius_strip(u, v, r, w)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Figure-8 immersion of the Klein bottle, u, v ∈ [0, 2π). +/// +/// Rust: `mesh::surfaces::klein_bottle` +#[pyfunction] +#[pyo3(name = "klein_bottle", signature = (u, v))] +pub fn pyfn_klein_bottle(u: f64, v: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::surfaces::klein_bottle(u, v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Enneper's minimal surface. +/// +/// Rust: `mesh::surfaces::enneper` +#[pyfunction] +#[pyo3(name = "enneper", signature = (u, v))] +pub fn pyfn_enneper(u: f64, v: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::surfaces::enneper(u, v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Catenoid (minimal): u around, v along the axis, waist radius `c`. +/// +/// Rust: `mesh::surfaces::catenoid` +#[pyfunction] +#[pyo3(name = "catenoid", signature = (u, v, c))] +pub fn pyfn_catenoid(u: f64, v: f64, c: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::surfaces::catenoid(u, v, c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Helicoid (minimal): pitch parameter `c`. +/// +/// Rust: `mesh::surfaces::helicoid` +#[pyfunction] +#[pyo3(name = "helicoid", signature = (u, v, c))] +pub fn pyfn_helicoid(u: f64, v: f64, c: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::surfaces::helicoid(u, v, c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Monkey saddle z = u³ − 3uv². +/// +/// Rust: `mesh::surfaces::monkey_saddle` +#[pyfunction] +#[pyo3(name = "monkey_saddle", signature = (u, v))] +pub fn pyfn_monkey_saddle(u: f64, v: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::surfaces::monkey_saddle(u, v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Dini's surface (constant negative curvature −1/(a² + b²)): +/// a twisted pseudosphere. v ∈ (0, π). +/// +/// Rust: `mesh::surfaces::dini` +#[pyfunction] +#[pyo3(name = "dini", signature = (u, v, a, b))] +pub fn pyfn_dini(u: f64, v: f64, a: f64, b: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::surfaces::dini(u, v, a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Boy's surface (Apéry parametrization of the real projective +/// plane), u ∈ [−π/2, π/2], v ∈ [0, π/2]. +/// +/// Rust: `mesh::surfaces::boy_surface` +#[pyfunction] +#[pyo3(name = "boy_surface", signature = (u, v))] +pub fn pyfn_boy_surface(u: f64, v: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::surfaces::boy_surface(u, v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Superellipsoid with semi-axes `a` and exponents `e1` (latitude), +/// `e2` (longitude): u = longitude ∈ [−π, π], v = latitude ∈ +/// [−π/2, π/2]. +/// +/// Rust: `mesh::surfaces::superellipsoid` +#[pyfunction] +#[pyo3(name = "superellipsoid", signature = (u, v, a, e1, e2))] +pub fn pyfn_superellipsoid(u: f64, v: f64, a: crate::generated::types::PyVec3Arg, e1: f64, e2: f64) -> PyResult { + let a = a.0; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::surfaces::superellipsoid(u, v, a, e1, e2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Gielis superformula in the plane: +/// r(θ) = (|cos(mθ/4)/a|^n2 + |sin(mθ/4)/b|^n3)^(−1/n1). +/// +/// Panics: +/// Panics unless `a, b > 0` and `n1 != 0`. +/// +/// Rust: `mesh::surfaces::supershape_2d` +#[pyfunction] +#[pyo3(name = "supershape_2d", signature = (theta, m, n1, n2, n3, a, b))] +pub fn pyfn_supershape_2d(theta: f64, m: f64, n1: f64, n2: f64, n3: f64, a: f64, b: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::surfaces::supershape_2d(theta, m, n1, n2, n3, a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) +} + +/// 3-D supershape: the spherical product of two superformulas, +/// `params = [m1, n11, n12, n13, a1, b1, m2, n21, n22, n23, a2, b2]` +/// with θ = longitude ∈ [−π, π], φ = latitude ∈ [−π/2, π/2]. +/// +/// Rust: `mesh::surfaces::supershape_3d` +#[pyfunction] +#[pyo3(name = "supershape_3d", signature = (theta, phi, params))] +pub fn pyfn_supershape_3d(theta: f64, phi: f64, params: Vec) -> PyResult { + let params = <[f64; 12]>::try_from(params).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 12 values, got {}", __v.len())))?; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::surfaces::supershape_3d(theta, phi, ¶ms)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_surface_of_revolution, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ruled_surface, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_coons_patch, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fundamental_forms, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gaussian_curvature, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mean_curvature, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_principal_curvatures, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_surface_area_parametric, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mobius_strip, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_klein_bottle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_enneper, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_catenoid, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_helicoid, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_monkey_saddle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dini, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_boy_surface, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_superellipsoid, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_supershape_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_supershape_3d, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_monte_carlo.rs b/bindings/python/src/generated/m_monte_carlo.rs new file mode 100644 index 0000000..571cfe6 --- /dev/null +++ b/bindings/python/src/generated/m_monte_carlo.rs @@ -0,0 +1,200 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Monte Carlo integration of a 1D function over [a, b] using uniform random sampling. +/// +/// Rust: `monte_carlo::mc_integrate_1d` +#[pyfunction] +#[pyo3(name = "mc_integrate_1d", signature = (f, a, b, n, rng))] +pub fn pyfn_mc_integrate_1d(f: pyo3::Py, a: f64, b: f64, n: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::monte_carlo::mc_integrate_1d(&f, a, b, n, &mut rng.inner)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Monte Carlo integration of a 2D function over a rectangular domain using uniform random sampling. +/// +/// Rust: `monte_carlo::mc_integrate_2d` +#[pyfunction] +#[pyo3(name = "mc_integrate_2d", signature = (f, x_range, y_range, n, rng))] +pub fn pyfn_mc_integrate_2d(f: pyo3::Py, x_range: (f64, f64), y_range: (f64, f64), n: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64, __a1: f64| -> f64 { __cb.call::<_, f64>((__a0, __a1), f64::NAN) } }; + let x_range = (x_range.0, x_range.1); + let y_range = (y_range.0, y_range.1); + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::monte_carlo::mc_integrate_2d(&f, x_range, y_range, n, &mut rng.inner)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Estimates pi by uniform random sampling inside the unit square: π ≈ 4 × (points inside unit circle) / N. +/// +/// Rust: `monte_carlo::mc_estimate_pi` +#[pyfunction] +#[pyo3(name = "mc_estimate_pi", signature = (n, rng))] +pub fn pyfn_mc_estimate_pi(n: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::monte_carlo::mc_estimate_pi(n, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Simulates a 1D symmetric random walk with equal probability of stepping left or right. +/// +/// Rust: `monte_carlo::random_walk_1d` +#[pyfunction] +#[pyo3(name = "random_walk_1d", signature = (steps, step_size, rng))] +pub fn pyfn_random_walk_1d(steps: usize, step_size: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::monte_carlo::random_walk_1d(steps, step_size, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Simulates a 2D random walk with uniformly distributed step direction in [0, 2π). +/// +/// Rust: `monte_carlo::random_walk_2d` +#[pyfunction] +#[pyo3(name = "random_walk_2d", signature = (steps, step_size, rng))] +pub fn pyfn_random_walk_2d(steps: usize, step_size: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::monte_carlo::random_walk_2d(steps, step_size, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Simulates a 3D random walk with uniformly distributed direction via Marsaglia's Gaussian method. +/// +/// Rust: `monte_carlo::random_walk_3d` +#[pyfunction] +#[pyo3(name = "random_walk_3d", signature = (steps, step_size, rng))] +pub fn pyfn_random_walk_3d(steps: usize, step_size: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::monte_carlo::random_walk_3d(steps, step_size, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, __x.2)).collect::>()) +} + +/// Generates a discretized Wiener process (Brownian motion): W(t+dt) = W(t) + √dt × N(0,1). +/// +/// Rust: `monte_carlo::wiener_process` +#[pyfunction] +#[pyo3(name = "wiener_process", signature = (n_steps, dt, rng))] +pub fn pyfn_wiener_process(n_steps: usize, dt: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::monte_carlo::wiener_process(n_steps, dt, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Simulates an Ornstein-Uhlenbeck process: dx = θ(μ - x)dt + σdW. +/// +/// Rust: `monte_carlo::ornstein_uhlenbeck` +#[pyfunction] +#[pyo3(name = "ornstein_uhlenbeck", signature = (n_steps, dt, theta, mu, sigma, x0, rng))] +pub fn pyfn_ornstein_uhlenbeck(n_steps: usize, dt: f64, theta: f64, mu: f64, sigma: f64, x0: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::monte_carlo::ornstein_uhlenbeck(n_steps, dt, theta, mu, sigma, x0, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Performs one Langevin dynamics step with thermal noise: dv = (F/m - γv)dt + √(2γk_BT/m) dW. +/// +/// Rust: `monte_carlo::langevin_step` +#[pyfunction] +#[pyo3(name = "langevin_step", signature = (x, v, force, mass, gamma, temperature, dt, rng))] +pub fn pyfn_langevin_step(x: f64, v: f64, force: f64, mass: f64, gamma: f64, temperature: f64, dt: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(f64, f64)> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::monte_carlo::langevin_step(x, v, force, mass, gamma, temperature, dt, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Metropolis acceptance criterion: accepts if ΔE < 0, otherwise accepts with probability exp(-ΔE / k_BT). +/// +/// Rust: `monte_carlo::metropolis_step` +#[pyfunction] +#[pyo3(name = "metropolis_step", signature = (energy_current, energy_proposed, temperature, rng))] +pub fn pyfn_metropolis_step(energy_current: f64, energy_proposed: f64, temperature: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::monte_carlo::metropolis_step(energy_current, energy_proposed, temperature, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Computes the 1D Ising model energy: E = -J Σ s_i s_{i+1} - H Σ s_i. +/// +/// Rust: `monte_carlo::ising_energy_1d` +#[pyfunction] +#[pyo3(name = "ising_energy_1d", signature = (spins, j_coupling, h_field))] +pub fn pyfn_ising_energy_1d<'py>(py: Python<'py>, spins: Vec, j_coupling: f64, h_field: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::monte_carlo::ising_energy_1d(&spins, j_coupling, h_field))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Computes the mean magnetization of a spin configuration: M = (Σ s_i) / N. +/// +/// Rust: `monte_carlo::ising_magnetization` +#[pyfunction] +#[pyo3(name = "ising_magnetization", signature = (spins))] +pub fn pyfn_ising_magnetization<'py>(py: Python<'py>, spins: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::monte_carlo::ising_magnetization(&spins))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Performs one Metropolis single-spin-flip update on a 1D Ising model. +/// +/// Rust: `monte_carlo::ising_step_1d` +#[pyfunction] +#[pyo3(name = "ising_step_1d", signature = (spins, j_coupling, h_field, temperature, rng))] +pub fn pyfn_ising_step_1d<'py>(spins: pyo3::Bound<'py, pyo3::PyAny>, j_coupling: f64, h_field: f64, temperature: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<()> { + let mut spins__v: Vec = spins.extract()?; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::monte_carlo::ising_step_1d(&mut spins__v, j_coupling, h_field, temperature, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + crate::runtime::coerce::write_back(&spins, &spins__v)?; + Ok(()) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_mc_integrate_1d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mc_integrate_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mc_estimate_pi, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_random_walk_1d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_random_walk_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_random_walk_3d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wiener_process, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ornstein_uhlenbeck, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_langevin_step, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_metropolis_step, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ising_energy_1d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ising_magnetization, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ising_step_1d, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_monte_carlo__quasi.rs b/bindings/python/src/generated/m_monte_carlo__quasi.rs new file mode 100644 index 0000000..ef3f5ee --- /dev/null +++ b/bindings/python/src/generated/m_monte_carlo__quasi.rs @@ -0,0 +1,43 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Quasi-Monte Carlo integration of f over [0, 1]^dim with the first n +/// Sobol points. +/// +/// Panics: +/// Panics unless n > 0 and dim is Sobol-supported. +/// +/// Rust: `monte_carlo::quasi::mc_integrate_sobol` +#[pyfunction] +#[pyo3(name = "mc_integrate_sobol", signature = (f, dim, n))] +pub fn pyfn_mc_integrate_sobol(f: pyo3::Py, dim: usize, n: usize) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: &[f64]| -> f64 { __cb.call::<_, f64>((__a0.to_vec(),), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::monte_carlo::quasi::mc_integrate_sobol(&f, dim, n)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_mc_integrate_sobol, m)?)?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_neutronics.rs b/bindings/python/src/generated/m_neutronics.rs new file mode 100644 index 0000000..d14b250 --- /dev/null +++ b/bindings/python/src/generated/m_neutronics.rs @@ -0,0 +1,290 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Effective multiplication factor: k_eff = production_rate / loss_rate +/// +/// Rust: `neutronics::k_effective` +#[pyfunction] +#[pyo3(name = "k_effective", signature = (production_rate, loss_rate))] +pub fn pyfn_k_effective(production_rate: f64, loss_rate: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::neutronics::k_effective(production_rate, loss_rate)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Reactivity: ρ = (k - 1) / k +/// +/// Rust: `neutronics::reactivity` +#[pyfunction] +#[pyo3(name = "reactivity", signature = (k_eff))] +pub fn pyfn_reactivity(k_eff: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::neutronics::reactivity(k_eff)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Doubling time for supercritical reactor: T = l × ln(2) / (k - 1) +/// Returns f64::INFINITY if k_eff <= 1.0 (not supercritical). +/// +/// Rust: `neutronics::doubling_time` +#[pyfunction] +#[pyo3(name = "doubling_time", signature = (k_eff, neutron_lifetime))] +pub fn pyfn_doubling_time(k_eff: f64, neutron_lifetime: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::neutronics::doubling_time(k_eff, neutron_lifetime)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Six-factor formula: k_eff = η × f × p × ε × P_FNL × P_TNL +/// +/// Rust: `neutronics::six_factor_formula` +#[pyfunction] +#[pyo3(name = "six_factor_formula", signature = (eta, f, p, epsilon, p_fnl, p_tnl))] +pub fn pyfn_six_factor_formula(eta: f64, f: f64, p: f64, epsilon: f64, p_fnl: f64, p_tnl: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::neutronics::six_factor_formula(eta, f, p, epsilon, p_fnl, p_tnl)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Reproduction factor: η = ν × σ_f / σ_a +/// +/// Rust: `neutronics::reproduction_factor` +#[pyfunction] +#[pyo3(name = "reproduction_factor", signature = (nu, sigma_f, sigma_a))] +pub fn pyfn_reproduction_factor(nu: f64, sigma_f: f64, sigma_a: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::neutronics::reproduction_factor(nu, sigma_f, sigma_a)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Diffusion coefficient: D = λ_tr / 3 +/// +/// Rust: `neutronics::diffusion_coefficient` +#[pyfunction] +#[pyo3(name = "diffusion_coefficient", signature = (transport_mfp))] +pub fn pyfn_diffusion_coefficient(transport_mfp: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::neutronics::diffusion_coefficient(transport_mfp)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Diffusion length: L = √(D / Σ_a) +/// +/// Rust: `neutronics::diffusion_length` +#[pyfunction] +#[pyo3(name = "diffusion_length", signature = (diffusion_coeff, absorption_xs))] +pub fn pyfn_diffusion_length(diffusion_coeff: f64, absorption_xs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::neutronics::diffusion_length(diffusion_coeff, absorption_xs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Migration length: M = √(L² + τ) where τ is the Fermi age (slowing-down area) +/// +/// Rust: `neutronics::migration_length` +#[pyfunction] +#[pyo3(name = "migration_length", signature = (diffusion_length, slowing_down_length))] +pub fn pyfn_migration_length(diffusion_length: f64, slowing_down_length: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::neutronics::migration_length(diffusion_length, slowing_down_length)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Thermal utilization factor: f = Σ_a_fuel / Σ_a_total +/// +/// Rust: `neutronics::thermal_utilization` +#[pyfunction] +#[pyo3(name = "thermal_utilization", signature = (sigma_a_fuel, sigma_a_total))] +pub fn pyfn_thermal_utilization(sigma_a_fuel: f64, sigma_a_total: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::neutronics::thermal_utilization(sigma_a_fuel, sigma_a_total)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Neutron flux in a slab reactor with uniform source: +/// φ(x) = (S / Σ_a) × (1 - cosh(x/L) / cosh(a/L)) +/// where L = √(D/Σ_a) and a = half-thickness (extrapolated). +/// +/// Rust: `neutronics::neutron_flux_slab` +#[pyfunction] +#[pyo3(name = "neutron_flux_slab", signature = (source, diffusion_coeff, sigma_a, x, half_thickness))] +pub fn pyfn_neutron_flux_slab(source: f64, diffusion_coeff: f64, sigma_a: f64, x: f64, half_thickness: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::neutronics::neutron_flux_slab(source, diffusion_coeff, sigma_a, x, half_thickness)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Macroscopic cross section from microscopic: Σ = N × σ +/// +/// Rust: `neutronics::microscopic_to_macroscopic` +#[pyfunction] +#[pyo3(name = "microscopic_to_macroscopic", signature = (micro_xs, number_density))] +pub fn pyfn_microscopic_to_macroscopic(micro_xs: f64, number_density: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::neutronics::microscopic_to_macroscopic(micro_xs, number_density)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Number density from bulk density and molar mass: N = ρ × N_A / M +/// +/// Rust: `neutronics::number_density` +#[pyfunction] +#[pyo3(name = "number_density", signature = (density, molar_mass))] +pub fn pyfn_number_density(density: f64, molar_mass: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::neutronics::number_density(density, molar_mass)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Mean free path for neutrons: λ = 1 / Σ +/// +/// Rust: `neutronics::mean_free_path_neutron` +#[pyfunction] +#[pyo3(name = "mean_free_path_neutron", signature = (macro_xs))] +pub fn pyfn_mean_free_path_neutron(macro_xs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::neutronics::mean_free_path_neutron(macro_xs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Neutron reaction rate: R = Σ × φ +/// +/// Rust: `neutronics::reaction_rate_neutron` +#[pyfunction] +#[pyo3(name = "reaction_rate_neutron", signature = (macro_xs, flux))] +pub fn pyfn_reaction_rate_neutron(macro_xs: f64, flux: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::neutronics::reaction_rate_neutron(macro_xs, flux)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// 1/v cross section law for thermal neutrons: σ(E) = σ₀ × √(E₀ / E) +/// +/// Rust: `neutronics::one_over_v_xs` +#[pyfunction] +#[pyo3(name = "one_over_v_xs", signature = (sigma_0, e_0, energy))] +pub fn pyfn_one_over_v_xs(sigma_0: f64, e_0: f64, energy: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::neutronics::one_over_v_xs(sigma_0, e_0, energy)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Reactor thermal power: P = R_f × E_f +/// +/// Rust: `neutronics::reactor_power` +#[pyfunction] +#[pyo3(name = "reactor_power", signature = (fission_rate, energy_per_fission))] +pub fn pyfn_reactor_power(fission_rate: f64, energy_per_fission: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::neutronics::reactor_power(fission_rate, energy_per_fission)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Burnup: BU = P × t / M (MWd/kg when units are consistent) +/// +/// Rust: `neutronics::burnup` +#[pyfunction] +#[pyo3(name = "burnup", signature = (power, time, mass_heavy_metal))] +pub fn pyfn_burnup(power: f64, time: f64, mass_heavy_metal: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::neutronics::burnup(power, time, mass_heavy_metal)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Decay heat fraction (Way-Wigner approximation for long prior operation): +/// P/P₀ ≈ 0.066 × t^(-0.2) +/// +/// Rust: `neutronics::decay_heat_fraction` +#[pyfunction] +#[pyo3(name = "decay_heat_fraction", signature = (time_after_shutdown))] +pub fn pyfn_decay_heat_fraction(time_after_shutdown: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::neutronics::decay_heat_fraction(time_after_shutdown)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Transmission factor (uncollided): I/I₀ = e^(-Σx) +/// +/// Rust: `neutronics::transmission_factor` +#[pyfunction] +#[pyo3(name = "transmission_factor", signature = (macro_xs, thickness))] +pub fn pyfn_transmission_factor(macro_xs: f64, thickness: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::neutronics::transmission_factor(macro_xs, thickness)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Half-value layer: HVL = ln(2) / Σ +/// +/// Rust: `neutronics::half_value_layer` +#[pyfunction] +#[pyo3(name = "half_value_layer", signature = (macro_xs))] +pub fn pyfn_half_value_layer(macro_xs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::neutronics::half_value_layer(macro_xs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Tenth-value layer: TVL = ln(10) / Σ +/// +/// Rust: `neutronics::tenth_value_layer` +#[pyfunction] +#[pyo3(name = "tenth_value_layer", signature = (macro_xs))] +pub fn pyfn_tenth_value_layer(macro_xs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::neutronics::tenth_value_layer(macro_xs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Linear buildup factor approximation for thin shields: B ≈ 1 + Σx +/// +/// Rust: `neutronics::buildup_factor_approx` +#[pyfunction] +#[pyo3(name = "buildup_factor_approx", signature = (macro_xs, thickness))] +pub fn pyfn_buildup_factor_approx(macro_xs: f64, thickness: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::neutronics::buildup_factor_approx(macro_xs, thickness)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_k_effective, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_reactivity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_doubling_time, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_six_factor_formula, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_reproduction_factor, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_diffusion_coefficient, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_diffusion_length, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_migration_length, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_thermal_utilization, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_neutron_flux_slab, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_microscopic_to_macroscopic, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_number_density, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mean_free_path_neutron, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_reaction_rate_neutron, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_one_over_v_xs, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_reactor_power, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_burnup, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_decay_heat_fraction, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_transmission_factor, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_half_value_layer, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tenth_value_layer, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_buildup_factor_approx, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_nonlinear.rs b/bindings/python/src/generated/m_nonlinear.rs new file mode 100644 index 0000000..7deb4c8 --- /dev/null +++ b/bindings/python/src/generated/m_nonlinear.rs @@ -0,0 +1,203 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Chaos in low-dimensional systems. +/// +/// The logistic map and its period-doubling route to chaos, the Hénon +/// map, and the Lorenz and Rössler flows given as derivative functions to +/// hand to an integrator from `numerical`. +/// +/// Lyapunov exponents are the quantitative test: a positive exponent means +/// nearby trajectories separate exponentially, which is what makes a +/// system chaotic rather than merely complicated. Dimension estimators -- +/// box counting and the correlation dimension -- measure the attractor +/// that results. +/// +/// For strange attractors as drawable objects, escape-time fractals and +/// cellular automata see `fractals`. +/// Computes one iteration of the logistic map: x_{n+1} = r × x × (1 - x). +/// +/// Rust: `nonlinear::logistic_map` +#[pyfunction] +#[pyo3(name = "logistic_map", signature = (r, x))] +pub fn pyfn_logistic_map(r: f64, x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::nonlinear::logistic_map(r, x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Iterates the logistic map n times from x0, returning the full trajectory. +/// +/// Rust: `nonlinear::logistic_map_iterate` +#[pyfunction] +#[pyo3(name = "logistic_map_iterate", signature = (r, x0, n))] +pub fn pyfn_logistic_map_iterate<'py>(py: Python<'py>, r: f64, x0: f64, n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::nonlinear::logistic_map_iterate(r, x0, n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Discards an initial transient, then collects post-transient samples from the logistic map. +/// +/// Rust: `nonlinear::logistic_map_converge` +#[pyfunction] +#[pyo3(name = "logistic_map_converge", signature = (r, x0, transient, samples))] +pub fn pyfn_logistic_map_converge<'py>(py: Python<'py>, r: f64, x0: f64, transient: usize, samples: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::nonlinear::logistic_map_converge(r, x0, transient, samples))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Computes the Lyapunov exponent of the logistic map: λ = (1/N) Σ ln|r(1 - 2x_n)|. +/// +/// Rust: `nonlinear::lyapunov_exponent_logistic` +#[pyfunction] +#[pyo3(name = "lyapunov_exponent_logistic", signature = (r, x0, n))] +pub fn pyfn_lyapunov_exponent_logistic(r: f64, x0: f64, n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::nonlinear::lyapunov_exponent_logistic(r, x0, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Computes the Lyapunov exponent of a general 1D map: λ = (1/N) Σ ln|f'(x_n)|. +/// +/// Rust: `nonlinear::lyapunov_exponent_1d` +#[pyfunction] +#[pyo3(name = "lyapunov_exponent_1d", signature = (f, df, x0, n))] +pub fn pyfn_lyapunov_exponent_1d(f: pyo3::Py, df: pyo3::Py, x0: f64, n: usize) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __cb_df = std::rc::Rc::new(crate::runtime::Callback::new(df)); + let df = { let __cb = __cb_df.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::nonlinear::lyapunov_exponent_1d(&f, &df, x0, n)); + crate::runtime::callback::check(&[&__cb_f, &__cb_df], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Computes the Lorenz system derivatives: dx/dt = σ(y-x), dy/dt = x(ρ-z)-y, dz/dt = xy-βz. +/// +/// Rust: `nonlinear::lorenz_derivatives` +#[pyfunction] +#[pyo3(name = "lorenz_derivatives", signature = (state, sigma, rho, beta))] +pub fn pyfn_lorenz_derivatives<'py>(py: Python<'py>, state: Vec, sigma: f64, rho: f64, beta: f64) -> PyResult> { + let state = <[f64; 3]>::try_from(state).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 3 values, got {}", __v.len())))?; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::nonlinear::lorenz_derivatives(&state, sigma, rho, beta))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_vec()) +} + +/// Computes the Roessler system derivatives: dx/dt = -y-z, dy/dt = x+ay, dz/dt = b+z(x-c). +/// +/// Rust: `nonlinear::rossler_derivatives` +#[pyfunction] +#[pyo3(name = "rossler_derivatives", signature = (state, a, b, c))] +pub fn pyfn_rossler_derivatives<'py>(py: Python<'py>, state: Vec, a: f64, b: f64, c: f64) -> PyResult> { + let state = <[f64; 3]>::try_from(state).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 3 values, got {}", __v.len())))?; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::nonlinear::rossler_derivatives(&state, a, b, c))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_vec()) +} + +/// Computes one iteration of the Henon map: x_{n+1} = 1 - ax² + y, y_{n+1} = bx. +/// +/// Rust: `nonlinear::henon_map` +#[pyfunction] +#[pyo3(name = "henon_map", signature = (x, y, a, b))] +pub fn pyfn_henon_map(x: f64, y: f64, a: f64, b: f64) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::nonlinear::henon_map(x, y, a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Iterates the Henon map n times from (x0, y0), returning the full trajectory of (x, y) pairs. +/// +/// Rust: `nonlinear::henon_iterate` +#[pyfunction] +#[pyo3(name = "henon_iterate", signature = (x0, y0, a, b, n))] +pub fn pyfn_henon_iterate<'py>(py: Python<'py>, x0: f64, y0: f64, a: f64, b: f64, n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::nonlinear::henon_iterate(x0, y0, a, b, n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Estimates the correlation integral C(r) as the fraction of pairwise distances below threshold r. +/// +/// Rust: `nonlinear::correlation_dimension_estimate` +#[pyfunction] +#[pyo3(name = "correlation_dimension_estimate", signature = (distances, r))] +pub fn pyfn_correlation_dimension_estimate<'py>(py: Python<'py>, distances: Vec, r: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::nonlinear::correlation_dimension_estimate(&distances, r))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Estimates the box-counting (Minkowski) dimension via least-squares fit of log(N) vs log(1/ε). +/// +/// Rust: `nonlinear::box_counting_dimension` +#[pyfunction] +#[pyo3(name = "box_counting_dimension", signature = (occupied_boxes, grid_sizes))] +pub fn pyfn_box_counting_dimension<'py>(py: Python<'py>, occupied_boxes: Vec<(usize, usize)>, grid_sizes: Vec) -> PyResult { + let occupied_boxes = occupied_boxes.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::nonlinear::box_counting_dimension(&occupied_boxes, &grid_sizes))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Finds a fixed point of f by iterating x_{n+1} = f(x_n) until convergence within tolerance. +/// +/// Rust: `nonlinear::fixed_point_iterate` +#[pyfunction] +#[pyo3(name = "fixed_point_iterate", signature = (f, x0, tol, max_iter))] +pub fn pyfn_fixed_point_iterate(f: pyo3::Py, x0: f64, tol: f64, max_iter: usize) -> PyResult> { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::nonlinear::fixed_point_iterate(&f, x0, tol, max_iter)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// Returns true if the fixed point is stable, i.e., |f'(x*)| < 1. +/// +/// Rust: `nonlinear::is_stable_fixed_point` +#[pyfunction] +#[pyo3(name = "is_stable_fixed_point", signature = (df_at_fixed))] +pub fn pyfn_is_stable_fixed_point(df_at_fixed: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::nonlinear::is_stable_fixed_point(df_at_fixed)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_logistic_map, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_logistic_map_iterate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_logistic_map_converge, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lyapunov_exponent_logistic, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lyapunov_exponent_1d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lorenz_derivatives, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rossler_derivatives, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_henon_map, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_henon_iterate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_correlation_dimension_estimate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_box_counting_dimension, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fixed_point_iterate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_is_stable_fixed_point, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_nuclear.rs b/bindings/python/src/generated/m_nuclear.rs new file mode 100644 index 0000000..5fc29ff --- /dev/null +++ b/bindings/python/src/generated/m_nuclear.rs @@ -0,0 +1,265 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Number of remaining nuclei: N(t) = N_0 * e^(-λt) +/// +/// Rust: `nuclear::remaining_nuclei` +#[pyfunction] +#[pyo3(name = "remaining_nuclei", signature = (initial_count, decay_constant, time))] +pub fn pyfn_remaining_nuclei(initial_count: f64, decay_constant: f64, time: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::nuclear::remaining_nuclei(initial_count, decay_constant, time)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Activity (decay rate): A = λ * N = λ * N_0 * e^(-λt) +/// +/// Rust: `nuclear::activity` +#[pyfunction] +#[pyo3(name = "activity", signature = (initial_count, decay_constant, time))] +pub fn pyfn_activity(initial_count: f64, decay_constant: f64, time: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::nuclear::activity(initial_count, decay_constant, time)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Half-life from decay constant: t_1/2 = ln(2) / λ +/// +/// Rust: `nuclear::half_life` +#[pyfunction] +#[pyo3(name = "half_life", signature = (decay_constant))] +pub fn pyfn_half_life(decay_constant: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::nuclear::half_life(decay_constant)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Decay constant from half-life: λ = ln(2) / t_1/2 +/// +/// Rust: `nuclear::decay_constant` +#[pyfunction] +#[pyo3(name = "decay_constant", signature = (half_life))] +pub fn pyfn_decay_constant(half_life: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::nuclear::decay_constant(half_life)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Mean lifetime: τ = 1 / λ +/// +/// Rust: `nuclear::mean_lifetime` +#[pyfunction] +#[pyo3(name = "mean_lifetime", signature = (decay_constant))] +pub fn pyfn_mean_lifetime(decay_constant: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::nuclear::mean_lifetime(decay_constant)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Number of nuclei remaining after n half-lives: N = N_0 / 2^n +/// +/// Rust: `nuclear::remaining_after_half_lives` +#[pyfunction] +#[pyo3(name = "remaining_after_half_lives", signature = (initial_count, num_half_lives))] +pub fn pyfn_remaining_after_half_lives(initial_count: f64, num_half_lives: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::nuclear::remaining_after_half_lives(initial_count, num_half_lives)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Number of half-lives elapsed: n = t / t_1/2 +/// +/// Rust: `nuclear::num_half_lives` +#[pyfunction] +#[pyo3(name = "num_half_lives", signature = (time, half_life))] +pub fn pyfn_num_half_lives(time: f64, half_life: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::nuclear::num_half_lives(time, half_life)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Mass defect: Δm = Z*m_p + N*m_n - M_nucleus +/// +/// Rust: `nuclear::mass_defect` +#[pyfunction] +#[pyo3(name = "mass_defect", signature = (num_protons, num_neutrons, nucleus_mass))] +pub fn pyfn_mass_defect(num_protons: u32, num_neutrons: u32, nucleus_mass: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::nuclear::mass_defect(num_protons, num_neutrons, nucleus_mass)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Binding energy from mass defect: E_b = Δm * c^2 +/// +/// Rust: `nuclear::binding_energy` +#[pyfunction] +#[pyo3(name = "binding_energy", signature = (mass_defect))] +pub fn pyfn_binding_energy(mass_defect: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::nuclear::binding_energy(mass_defect)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Binding energy per nucleon: E_b / A +/// +/// Rust: `nuclear::binding_energy_per_nucleon` +#[pyfunction] +#[pyo3(name = "binding_energy_per_nucleon", signature = (total_binding_energy, mass_number))] +pub fn pyfn_binding_energy_per_nucleon(total_binding_energy: f64, mass_number: u32) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::nuclear::binding_energy_per_nucleon(total_binding_energy, mass_number)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Q-value of a nuclear reaction: Q = (m_reactants - m_products) * c^2 +/// +/// Rust: `nuclear::q_value` +#[pyfunction] +#[pyo3(name = "q_value", signature = (reactant_mass, product_mass))] +pub fn pyfn_q_value(reactant_mass: f64, product_mass: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::nuclear::q_value(reactant_mass, product_mass)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Energy released from mass conversion: E = Δm * c^2 +/// +/// Rust: `nuclear::mass_energy` +#[pyfunction] +#[pyo3(name = "mass_energy", signature = (mass))] +pub fn pyfn_mass_energy(mass: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::nuclear::mass_energy(mass)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Energy from fission/fusion (given mass difference in amu): +/// E = Δm(amu) * 931.5 MeV +/// +/// Rust: `nuclear::energy_from_amu` +#[pyfunction] +#[pyo3(name = "energy_from_amu", signature = (mass_difference_amu))] +pub fn pyfn_energy_from_amu(mass_difference_amu: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::nuclear::energy_from_amu(mass_difference_amu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Reaction rate: R = n * σ * Φ +/// n = target number density, σ = cross section, Φ = flux +/// +/// Rust: `nuclear::reaction_rate` +#[pyfunction] +#[pyo3(name = "reaction_rate", signature = (number_density, cross_section, flux))] +pub fn pyfn_reaction_rate(number_density: f64, cross_section: f64, flux: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::nuclear::reaction_rate(number_density, cross_section, flux)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Mean free path in nuclear context: λ = 1 / (n * σ) +/// +/// Rust: `nuclear::nuclear_mean_free_path` +#[pyfunction] +#[pyo3(name = "nuclear_mean_free_path", signature = (number_density, cross_section))] +pub fn pyfn_nuclear_mean_free_path(number_density: f64, cross_section: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::nuclear::nuclear_mean_free_path(number_density, cross_section)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Nuclear radius (empirical): R = R_0 * A^(1/3) where R_0 ≈ 1.2 fm +/// +/// Rust: `nuclear::nuclear_radius` +#[pyfunction] +#[pyo3(name = "nuclear_radius", signature = (mass_number))] +pub fn pyfn_nuclear_radius(mass_number: u32) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::nuclear::nuclear_radius(mass_number)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Nuclear density (approximately constant): ρ ≈ 3m_p / (4π * R_0^3) +/// +/// Rust: `nuclear::nuclear_density` +#[pyfunction] +#[pyo3(name = "nuclear_density", signature = ())] +pub fn pyfn_nuclear_density() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::nuclear::nuclear_density()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Absorbed dose: D = E / m (Gray, Gy = J/kg) +/// +/// Rust: `nuclear::absorbed_dose` +#[pyfunction] +#[pyo3(name = "absorbed_dose", signature = (energy, mass))] +pub fn pyfn_absorbed_dose(energy: f64, mass: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::nuclear::absorbed_dose(energy, mass)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Equivalent dose: H = D * w_R (Sievert, Sv) +/// w_R is the radiation weighting factor +/// +/// Rust: `nuclear::equivalent_dose` +#[pyfunction] +#[pyo3(name = "equivalent_dose", signature = (absorbed_dose, weighting_factor))] +pub fn pyfn_equivalent_dose(absorbed_dose: f64, weighting_factor: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::nuclear::equivalent_dose(absorbed_dose, weighting_factor)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Inverse square law for radiation intensity: I = I_0 * (r_0 / r)^2 +/// +/// Rust: `nuclear::radiation_intensity_distance` +#[pyfunction] +#[pyo3(name = "radiation_intensity_distance", signature = (initial_intensity, initial_distance, new_distance))] +pub fn pyfn_radiation_intensity_distance(initial_intensity: f64, initial_distance: f64, new_distance: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::nuclear::radiation_intensity_distance(initial_intensity, initial_distance, new_distance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_remaining_nuclei, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_activity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_half_life, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_decay_constant, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mean_lifetime, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_remaining_after_half_lives, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_num_half_lives, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mass_defect, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_binding_energy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_binding_energy_per_nucleon, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_q_value, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mass_energy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_energy_from_amu, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_reaction_rate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_nuclear_mean_free_path, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_nuclear_radius, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_nuclear_density, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_absorbed_dose, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_equivalent_dose, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_radiation_intensity_distance, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_numerical.rs b/bindings/python/src/generated/m_numerical.rs new file mode 100644 index 0000000..c02bbf8 --- /dev/null +++ b/bindings/python/src/generated/m_numerical.rs @@ -0,0 +1,22 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + Ok(()) +} diff --git a/bindings/python/src/generated/m_numerical__bvp.rs b/bindings/python/src/generated/m_numerical__bvp.rs new file mode 100644 index 0000000..07ae16d --- /dev/null +++ b/bindings/python/src/generated/m_numerical__bvp.rs @@ -0,0 +1,67 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Solves y'' = f(t, y, y') with y(t0) = y0 and y(t1) = y1_target by +/// the shooting method: the unknown initial slope is bracketed by +/// [guess_lo, guess_hi] and found with Brent's method. +/// +/// Returns the (t, y) trajectory of the converged solution. Fails with +/// `InvalidArgument` if the bracket does not straddle the target. +/// +/// Rust: `numerical::bvp::shooting` +#[pyfunction] +#[pyo3(name = "shooting", signature = (f, t0, t1, y0, y1_target, guess_lo, guess_hi, tol))] +pub fn pyfn_shooting(f: pyo3::Py, t0: f64, t1: f64, y0: f64, y1_target: f64, guess_lo: f64, guess_hi: f64, tol: f64) -> PyResult> { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64, __a1: f64, __a2: f64| -> f64 { __cb.call::<_, f64>((__a0, __a1, __a2), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::numerical::bvp::shooting(&f, t0, t1, y0, y1_target, guess_lo, guess_hi, tol)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Solves the linear BVP y'' + p(x)·y' + q(x)·y = r(x) on [a, b] with +/// y(a) = ya, y(b) = yb using n interior points and second-order +/// central differences; the tridiagonal system goes to `thomas_solve`. +/// +/// Returns the full grid of n + 2 values including both boundaries. +/// +/// Rust: `numerical::bvp::finite_difference_linear_bvp` +#[pyfunction] +#[pyo3(name = "finite_difference_linear_bvp", signature = (p, q, r, a, b, ya, yb, n))] +pub fn pyfn_finite_difference_linear_bvp(p: pyo3::Py, q: pyo3::Py, r: pyo3::Py, a: f64, b: f64, ya: f64, yb: f64, n: usize) -> PyResult> { + let __cb_p = std::rc::Rc::new(crate::runtime::Callback::new(p)); + let p = { let __cb = __cb_p.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __cb_q = std::rc::Rc::new(crate::runtime::Callback::new(q)); + let q = { let __cb = __cb_q.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __cb_r = std::rc::Rc::new(crate::runtime::Callback::new(r)); + let r = { let __cb = __cb_r.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::numerical::bvp::finite_difference_linear_bvp(&p, &q, &r, a, b, ya, yb, n)); + crate::runtime::callback::check(&[&__cb_p, &__cb_q, &__cb_r], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_shooting, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_finite_difference_linear_bvp, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_numerical__integrate.rs b/bindings/python/src/generated/m_numerical__integrate.rs new file mode 100644 index 0000000..4ff52e7 --- /dev/null +++ b/bindings/python/src/generated/m_numerical__integrate.rs @@ -0,0 +1,159 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Trapezoidal rule for numerical integration of f over [a, b] with n subintervals. +/// Sample values are accumulated with Neumaier compensated summation. +/// +/// Rust: `numerical::integrate::trapezoid` +#[pyfunction] +#[pyo3(name = "trapezoid", signature = (f, a, b, n))] +pub fn pyfn_trapezoid(f: pyo3::Py, a: f64, b: f64, n: usize) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::numerical::integrate::trapezoid(&f, a, b, n)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Simpson's 1/3 rule for numerical integration of f over [a, b] with n subintervals. +/// If n is odd it is rounded up to the next even number. +/// +/// Rust: `numerical::integrate::simpson` +#[pyfunction] +#[pyo3(name = "simpson", signature = (f, a, b, n))] +pub fn pyfn_simpson(f: pyo3::Py, a: f64, b: f64, n: usize) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::numerical::integrate::simpson(&f, a, b, n)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// 5-point Gauss-Legendre quadrature of f over [a, b]. +/// +/// Rust: `numerical::integrate::gaussian_quadrature_5` +#[pyfunction] +#[pyo3(name = "gaussian_quadrature_5", signature = (f, a, b))] +pub fn pyfn_gaussian_quadrature_5(f: pyo3::Py, a: f64, b: f64) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::numerical::integrate::gaussian_quadrature_5(&f, a, b)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// 15-point Gauss-Kronrod quadrature of f over [a, b]. +/// +/// `value` is the K15 estimate; `error` is |K15 − G7|, the classical +/// (conservative) error bound from the embedded 7-point Gauss rule. +/// +/// Rust: `numerical::integrate::gauss_kronrod_15` +#[pyfunction] +#[pyo3(name = "gauss_kronrod_15", signature = (f, a, b))] +pub fn pyfn_gauss_kronrod_15(f: pyo3::Py, a: f64, b: f64) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::numerical::integrate::gauss_kronrod_15(&f, a, b)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuadResult { inner: __v }) +} + +/// Adaptive quadrature: recursive bisection with the GK15 rule until +/// each panel's error estimate is below its share of `tol`. +/// +/// Rust: `numerical::integrate::adaptive_quad` +#[pyfunction] +#[pyo3(name = "adaptive_quad", signature = (f, a, b, tol, max_depth))] +pub fn pyfn_adaptive_quad(f: pyo3::Py, a: f64, b: f64, tol: f64, max_depth: usize) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::numerical::integrate::adaptive_quad(&f, a, b, tol, max_depth)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(crate::generated::types::PyQuadResult { inner: __v }) +} + +/// Romberg integration: trapezoid estimates at h, h/2, h/4, … with +/// Richardson extrapolation across the levels (NR §4.3). Converges when +/// two successive diagonal entries agree within `tol`. +/// +/// Rust: `numerical::integrate::romberg` +#[pyfunction] +#[pyo3(name = "romberg", signature = (f, a, b, max_levels, tol))] +pub fn pyfn_romberg(f: pyo3::Py, a: f64, b: f64, max_levels: usize, tol: f64) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::numerical::integrate::romberg(&f, a, b, max_levels, tol)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(crate::generated::types::PyQuadResult { inner: __v }) +} + +/// One generalized Richardson extrapolation pass over estimates whose +/// step sizes shrink by `ratio` between entries and whose error expands +/// in powers of h^order (E = c₁·h^p + c₂·h^{2p} + …). Returns the +/// highest-order extrapolant. +/// +/// Panics: +/// Panics if `estimates` is empty, `ratio <= 1`, or `order == 0`. +/// +/// Rust: `numerical::integrate::richardson_extrapolate` +#[pyfunction] +#[pyo3(name = "richardson_extrapolate", signature = (estimates, ratio, order))] +pub fn pyfn_richardson_extrapolate<'py>(py: Python<'py>, estimates: Vec, ratio: f64, order: u32) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::numerical::integrate::richardson_extrapolate(&estimates, ratio, order))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Integral of f over (−∞, ∞) via the substitution x = t/(1−t²), +/// dx = (1+t²)/(1−t²)² dt, mapped onto t ∈ (−1, 1) and evaluated with +/// `adaptive_quad`. Requires f to decay at infinity. +/// +/// Rust: `numerical::integrate::integrate_infinite` +#[pyfunction] +#[pyo3(name = "integrate_infinite", signature = (f, tol))] +pub fn pyfn_integrate_infinite(f: pyo3::Py, tol: f64) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::numerical::integrate::integrate_infinite(&f, tol)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(crate::generated::types::PyQuadResult { inner: __v }) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_trapezoid, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_simpson, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gaussian_quadrature_5, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gauss_kronrod_15, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_adaptive_quad, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_romberg, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_richardson_extrapolate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_integrate_infinite, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_numerical__interpolate.rs b/bindings/python/src/generated/m_numerical__interpolate.rs new file mode 100644 index 0000000..496cd91 --- /dev/null +++ b/bindings/python/src/generated/m_numerical__interpolate.rs @@ -0,0 +1,116 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Interpolation routines. +/// Linear interpolation between a and b: a + t*(b - a). +/// +/// Rust: `numerical::interpolate::lerp` +#[pyfunction] +#[pyo3(name = "lerp", signature = (a, b, t))] +pub fn pyfn_lerp(a: f64, b: f64, t: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::numerical::interpolate::lerp(a, b, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Piecewise linear interpolation in sorted (x_data, y_data). +/// Clamps to the endpoint values if x is outside the data range. +/// Panics if data slices are empty or mismatched in length. +/// +/// Rust: `numerical::interpolate::linear_interp` +#[pyfunction] +#[pyo3(name = "linear_interp", signature = (x_data, y_data, x))] +pub fn pyfn_linear_interp<'py>(py: Python<'py>, x_data: Vec, y_data: Vec, x: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::numerical::interpolate::linear_interp(&x_data, &y_data, x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Natural cubic spline interpolation for a single query point. +/// Falls back to linear interpolation if fewer than 4 data points. +/// Panics if data slices are empty or mismatched in length. +/// +/// Rust: `numerical::interpolate::cubic_interp` +#[pyfunction] +#[pyo3(name = "cubic_interp", signature = (x_data, y_data, x))] +pub fn pyfn_cubic_interp<'py>(py: Python<'py>, x_data: Vec, y_data: Vec, x: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::numerical::interpolate::cubic_interp(&x_data, &y_data, x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Uniform Catmull-Rom spline through `points`, parameterized so that +/// `t = i` lands exactly on `points[i]` (`t ∈ [0, n−1]`; endpoints use +/// duplicated boundary points). +/// +/// Panics: +/// Panics unless there are at least 2 points and t is within range. +/// +/// Rust: `numerical::interpolate::catmull_rom` +#[pyfunction] +#[pyo3(name = "catmull_rom", signature = (points, t))] +pub fn pyfn_catmull_rom(points: Vec, t: f64) -> PyResult { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::numerical::interpolate::catmull_rom(&points, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// 2-D uniform Catmull-Rom spline (same parameterization as +/// `catmull_rom`). +/// +/// Panics: +/// Panics unless there are at least 2 points and t is within range. +/// +/// Rust: `numerical::interpolate::catmull_rom_2d` +#[pyfunction] +#[pyo3(name = "catmull_rom_2d", signature = (points, t))] +pub fn pyfn_catmull_rom_2d<'py>(py: Python<'py>, points: Vec<(f64, f64)>, t: f64) -> PyResult<(f64, f64)> { + let points = points.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::numerical::interpolate::catmull_rom_2d(&points, t))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Arbitrary-degree Bezier curve point by the de Casteljau algorithm. +/// +/// Panics: +/// Panics if `control` is empty or t is outside [0, 1]. +/// +/// Rust: `numerical::interpolate::de_casteljau` +#[pyfunction] +#[pyo3(name = "de_casteljau", signature = (control, t))] +pub fn pyfn_de_casteljau(control: Vec, t: f64) -> PyResult { + let control = control.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::numerical::interpolate::de_casteljau(&control, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_lerp, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_linear_interp, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cubic_interp, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_catmull_rom, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_catmull_rom_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_de_casteljau, m)?)?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_numerical__ode.rs b/bindings/python/src/generated/m_numerical__ode.rs new file mode 100644 index 0000000..c02bbf8 --- /dev/null +++ b/bindings/python/src/generated/m_numerical__ode.rs @@ -0,0 +1,22 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + Ok(()) +} diff --git a/bindings/python/src/generated/m_numerical__ode__adaptive.rs b/bindings/python/src/generated/m_numerical__ode__adaptive.rs new file mode 100644 index 0000000..a24b744 --- /dev/null +++ b/bindings/python/src/generated/m_numerical__ode__adaptive.rs @@ -0,0 +1,64 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Integrates dy/dt = f(t, y) from t0 to t1 with adaptive step size, +/// recording every accepted step. +/// +/// The local error per step is kept near `atol + rtol·|y|` (RMS over +/// components). Fails with `NoConvergence` if the step count budget is +/// exhausted or the step size underflows. +/// +/// Rust: `numerical::ode::adaptive::dormand_prince` +#[pyfunction] +#[pyo3(name = "dormand_prince", signature = (f, t0, t1, y0, rtol, atol, h0))] +pub fn pyfn_dormand_prince(f: pyo3::Py, t0: f64, t1: f64, y0: Vec, rtol: f64, atol: f64, h0: f64) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64, __a1: &[f64]| -> Vec { __cb.call::<_, Vec>((__a0, __a1.to_vec()), Vec::new()) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::numerical::ode::adaptive::dormand_prince(&f, t0, t1, &y0, rtol, atol, h0)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(crate::generated::types::PyAdaptiveResult { inner: __v }) +} + +/// Like `dormand_prince` but returns the solution interpolated at +/// `sample_times` (cubic Hermite between accepted steps, using the +/// stored derivatives at the step endpoints). +/// +/// `sample_times` must be non-decreasing and lie within [t0, t1]. +/// +/// Rust: `numerical::ode::adaptive::dormand_prince_dense` +#[pyfunction] +#[pyo3(name = "dormand_prince_dense", signature = (f, t0, t1, y0, rtol, atol, h0, sample_times))] +pub fn pyfn_dormand_prince_dense(f: pyo3::Py, t0: f64, t1: f64, y0: Vec, rtol: f64, atol: f64, h0: f64, sample_times: Vec) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64, __a1: &[f64]| -> Vec { __cb.call::<_, Vec>((__a0, __a1.to_vec()), Vec::new()) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::numerical::ode::adaptive::dormand_prince_dense(&f, t0, t1, &y0, rtol, atol, h0, &sample_times)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(crate::generated::types::PyAdaptiveResult { inner: __v }) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_dormand_prince, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dormand_prince_dense, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_numerical__ode__explicit.rs b/bindings/python/src/generated/m_numerical__ode__explicit.rs new file mode 100644 index 0000000..cedfbbc --- /dev/null +++ b/bindings/python/src/generated/m_numerical__ode__explicit.rs @@ -0,0 +1,84 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Explicit fixed-step ODE integrators. +/// Single forward Euler step: y_next = y + dt * f(t, y). +/// +/// Rust: `numerical::ode::explicit::euler_step` +#[pyfunction] +#[pyo3(name = "euler_step", signature = (f, t, y, dt))] +pub fn pyfn_euler_step(f: pyo3::Py, t: f64, y: f64, dt: f64) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64, __a1: f64| -> f64 { __cb.call::<_, f64>((__a0, __a1), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::numerical::ode::explicit::euler_step(&f, t, y, dt)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Single step of the classic 4th-order Runge-Kutta method. +/// +/// Rust: `numerical::ode::explicit::rk4_step` +#[pyfunction] +#[pyo3(name = "rk4_step", signature = (f, t, y, dt))] +pub fn pyfn_rk4_step(f: pyo3::Py, t: f64, y: f64, dt: f64) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64, __a1: f64| -> f64 { __cb.call::<_, f64>((__a0, __a1), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::numerical::ode::explicit::rk4_step(&f, t, y, dt)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Full RK4 integration of dy/dt = f(t, y) from t0 to t_end, returning (t, y) pairs. +/// +/// Rust: `numerical::ode::explicit::rk4_solve` +#[pyfunction] +#[pyo3(name = "rk4_solve", signature = (f, t0, y0, t_end, dt))] +pub fn pyfn_rk4_solve(f: pyo3::Py, t0: f64, y0: f64, t_end: f64, dt: f64) -> PyResult> { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64, __a1: f64| -> f64 { __cb.call::<_, f64>((__a0, __a1), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::numerical::ode::explicit::rk4_solve(&f, t0, y0, t_end, dt)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Single RK4 step for a system of ODEs (vector state). +/// `f(t, y)` returns a `Vec` of derivatives matching the length of `y`. +/// +/// Rust: `numerical::ode::explicit::rk4_step_vec` +#[pyfunction] +#[pyo3(name = "rk4_step_vec", signature = (f, t, y, dt))] +pub fn pyfn_rk4_step_vec(f: pyo3::Py, t: f64, y: Vec, dt: f64) -> PyResult> { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64, __a1: &[f64]| -> Vec { __cb.call::<_, Vec>((__a0, __a1.to_vec()), Vec::new()) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::numerical::ode::explicit::rk4_step_vec(&f, t, &y, dt)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_euler_step, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rk4_step, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rk4_solve, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rk4_step_vec, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_numerical__ode__implicit.rs b/bindings/python/src/generated/m_numerical__ode__implicit.rs new file mode 100644 index 0000000..c02bbf8 --- /dev/null +++ b/bindings/python/src/generated/m_numerical__ode__implicit.rs @@ -0,0 +1,22 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + Ok(()) +} diff --git a/bindings/python/src/generated/m_numerical__ode__symplectic.rs b/bindings/python/src/generated/m_numerical__ode__symplectic.rs new file mode 100644 index 0000000..51ca69c --- /dev/null +++ b/bindings/python/src/generated/m_numerical__ode__symplectic.rs @@ -0,0 +1,92 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Symplectic integrators for second-order systems x'' = a(x). +/// +/// These preserve phase-space volume, so energy errors stay bounded +/// instead of drifting. References: Verlet (1967); Yoshida, "Construction +/// of higher order symplectic integrators", Phys. Lett. A 150 (1990). +/// One velocity-Verlet step (kick-drift-kick): +/// v½ = v + a(x)·dt/2; x₁ = x + v½·dt; v₁ = v½ + a(x₁)·dt/2. +/// +/// Panics: +/// Panics if `x` and `v` differ in length or `acc` returns the wrong +/// length. +/// +/// Rust: `numerical::ode::symplectic::velocity_verlet` +#[pyfunction] +#[pyo3(name = "velocity_verlet", signature = (acc, x, v, dt))] +pub fn pyfn_velocity_verlet<'py>(acc: pyo3::Py, x: pyo3::Bound<'py, pyo3::PyAny>, v: pyo3::Bound<'py, pyo3::PyAny>, dt: f64) -> PyResult<()> { + let __cb_acc = std::rc::Rc::new(crate::runtime::Callback::new(acc)); + let acc = { let __cb = __cb_acc.clone(); move |__a0: &[f64]| -> Vec { __cb.call::<_, Vec>((__a0.to_vec(),), Vec::new()) } }; + let mut x__v: Vec = x.extract()?; + let mut v__v: Vec = v.extract()?; + let __r = crate::runtime::guard(|| rust_physics_engine::numerical::ode::symplectic::velocity_verlet(&acc, &mut x__v, &mut v__v, dt)); + crate::runtime::callback::check(&[&__cb_acc], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + crate::runtime::coerce::write_back(&x, &x__v)?; + crate::runtime::coerce::write_back(&v, &v__v)?; + Ok(()) +} + +/// Alias of the kick-drift-kick leapfrog scheme (identical to velocity +/// Verlet in this synchronized form). +/// +/// Rust: `numerical::ode::symplectic::leapfrog_kick_drift_kick` +#[pyfunction] +#[pyo3(name = "leapfrog_kick_drift_kick", signature = (acc, x, v, dt))] +pub fn pyfn_leapfrog_kick_drift_kick<'py>(acc: pyo3::Py, x: pyo3::Bound<'py, pyo3::PyAny>, v: pyo3::Bound<'py, pyo3::PyAny>, dt: f64) -> PyResult<()> { + let __cb_acc = std::rc::Rc::new(crate::runtime::Callback::new(acc)); + let acc = { let __cb = __cb_acc.clone(); move |__a0: &[f64]| -> Vec { __cb.call::<_, Vec>((__a0.to_vec(),), Vec::new()) } }; + let mut x__v: Vec = x.extract()?; + let mut v__v: Vec = v.extract()?; + let __r = crate::runtime::guard(|| rust_physics_engine::numerical::ode::symplectic::leapfrog_kick_drift_kick(&acc, &mut x__v, &mut v__v, dt)); + crate::runtime::callback::check(&[&__cb_acc], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + crate::runtime::coerce::write_back(&x, &x__v)?; + crate::runtime::coerce::write_back(&v, &v__v)?; + Ok(()) +} + +/// One 4th-order Yoshida step: composition of three velocity-Verlet +/// sub-steps with weights w1, w0, w1 where +/// w1 = 1/(2 − 2^(1/3)), w0 = −2^(1/3)·w1. +/// +/// Rust: `numerical::ode::symplectic::yoshida4` +#[pyfunction] +#[pyo3(name = "yoshida4", signature = (acc, x, v, dt))] +pub fn pyfn_yoshida4<'py>(acc: pyo3::Py, x: pyo3::Bound<'py, pyo3::PyAny>, v: pyo3::Bound<'py, pyo3::PyAny>, dt: f64) -> PyResult<()> { + let __cb_acc = std::rc::Rc::new(crate::runtime::Callback::new(acc)); + let acc = { let __cb = __cb_acc.clone(); move |__a0: &[f64]| -> Vec { __cb.call::<_, Vec>((__a0.to_vec(),), Vec::new()) } }; + let mut x__v: Vec = x.extract()?; + let mut v__v: Vec = v.extract()?; + let __r = crate::runtime::guard(|| rust_physics_engine::numerical::ode::symplectic::yoshida4(&acc, &mut x__v, &mut v__v, dt)); + crate::runtime::callback::check(&[&__cb_acc], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + crate::runtime::coerce::write_back(&x, &x__v)?; + crate::runtime::coerce::write_back(&v, &v__v)?; + Ok(()) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_velocity_verlet, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_leapfrog_kick_drift_kick, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_yoshida4, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_numerical__roots.rs b/bindings/python/src/generated/m_numerical__roots.rs new file mode 100644 index 0000000..6a513c2 --- /dev/null +++ b/bindings/python/src/generated/m_numerical__roots.rs @@ -0,0 +1,143 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Bisection method for finding a root of f in [a, b]. +/// Returns `None` if f(a) and f(b) have the same sign. +/// +/// Rust: `numerical::roots::bisection` +#[pyfunction] +#[pyo3(name = "bisection", signature = (f, a, b, tol, max_iter))] +pub fn pyfn_bisection(f: pyo3::Py, a: f64, b: f64, tol: f64, max_iter: usize) -> PyResult> { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::numerical::roots::bisection(&f, a, b, tol, max_iter)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// Newton-Raphson method starting from x0. +/// Returns `None` if the derivative is zero or the method does not converge within max_iter. +/// +/// Rust: `numerical::roots::newton_raphson` +#[pyfunction] +#[pyo3(name = "newton_raphson", signature = (f, df, x0, tol, max_iter))] +pub fn pyfn_newton_raphson(f: pyo3::Py, df: pyo3::Py, x0: f64, tol: f64, max_iter: usize) -> PyResult> { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __cb_df = std::rc::Rc::new(crate::runtime::Callback::new(df)); + let df = { let __cb = __cb_df.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::numerical::roots::newton_raphson(&f, &df, x0, tol, max_iter)); + crate::runtime::callback::check(&[&__cb_f, &__cb_df], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// Secant method starting from two initial guesses x0 and x1. +/// Returns `None` if the method does not converge within max_iter. +/// +/// Rust: `numerical::roots::secant` +#[pyfunction] +#[pyo3(name = "secant", signature = (f, x0, x1, tol, max_iter))] +pub fn pyfn_secant(f: pyo3::Py, x0: f64, x1: f64, tol: f64, max_iter: usize) -> PyResult> { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::numerical::roots::secant(&f, x0, x1, tol, max_iter)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// Evaluates a real polynomial by Horner's rule; `coeffs` are ordered +/// highest degree first. +/// +/// Panics: +/// Panics if `coeffs` is empty. +/// +/// Rust: `numerical::roots::polynomial_eval` +#[pyfunction] +#[pyo3(name = "polynomial_eval", signature = (coeffs, x))] +pub fn pyfn_polynomial_eval<'py>(py: Python<'py>, coeffs: Vec, x: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::numerical::roots::polynomial_eval(&coeffs, x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Horner evaluation of a real-coefficient polynomial at a complex +/// point; `coeffs` are ordered highest degree first. +/// +/// Panics: +/// Panics if `coeffs` is empty. +/// +/// Rust: `numerical::roots::polynomial_eval_complex` +#[pyfunction] +#[pyo3(name = "polynomial_eval_complex", signature = (coeffs, z))] +pub fn pyfn_polynomial_eval_complex<'py>(py: Python<'py>, coeffs: Vec, z: crate::runtime::coerce::ComplexArg) -> PyResult> { + let z = z.0; + let __r = crate::runtime::guard(|| rust_physics_engine::numerical::roots::polynomial_eval_complex(&coeffs, z)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::complex_out(py, __v)) +} + +/// All complex roots of a real polynomial (coefficients highest degree +/// first) via the Durand-Kerner (Weierstrass) simultaneous iteration. +/// +/// Leading zeros are stripped; the root count equals the degree. +/// Returns `InvalidArgument` for constant (degree-0) or all-zero input +/// and `NoConvergence` if the iteration stalls. +/// +/// Rust: `numerical::roots::polynomial_roots` +#[pyfunction] +#[pyo3(name = "polynomial_roots", signature = (coeffs))] +pub fn pyfn_polynomial_roots<'py>(py: Python<'py>, coeffs: Vec) -> PyResult>> { + let __r = crate::runtime::guard(|| rust_physics_engine::numerical::roots::polynomial_roots(&coeffs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// Brent's method: bracketing root finder combining bisection, secant, +/// and inverse quadratic interpolation (Brent 1973; NR §9.3). +/// Superlinear convergence with guaranteed bracket retention. +/// +/// Returns `InvalidArgument` unless f(a) and f(b) have opposite signs. +/// +/// Rust: `numerical::roots::brent_root` +#[pyfunction] +#[pyo3(name = "brent_root", signature = (f, a, b, tol, max_iter))] +pub fn pyfn_brent_root(f: pyo3::Py, a: f64, b: f64, tol: f64, max_iter: usize) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::numerical::roots::brent_root(&f, a, b, tol, max_iter)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_bisection, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_newton_raphson, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_secant, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_polynomial_eval, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_polynomial_eval_complex, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_polynomial_roots, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_brent_root, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_optics.rs b/bindings/python/src/generated/m_optics.rs new file mode 100644 index 0000000..25883e5 --- /dev/null +++ b/bindings/python/src/generated/m_optics.rs @@ -0,0 +1,266 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Snell's law: n1 * sin(θ1) = n2 * sin(θ2) → θ2 = asin(n1 * sin(θ1) / n2) +/// Returns the refraction angle in radians, or None for total internal reflection. +/// +/// Rust: `optics::snells_law` +#[pyfunction] +#[pyo3(name = "snells_law", signature = (n1, angle1_rad, n2))] +pub fn pyfn_snells_law(n1: f64, angle1_rad: f64, n2: f64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::optics::snells_law(n1, angle1_rad, n2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// Critical angle for total internal reflection: θ_c = asin(n2 / n1) +/// Only valid when n1 > n2. Returns None otherwise. +/// +/// Rust: `optics::critical_angle` +#[pyfunction] +#[pyo3(name = "critical_angle", signature = (n1, n2))] +pub fn pyfn_critical_angle(n1: f64, n2: f64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::optics::critical_angle(n1, n2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// Brewster's angle: θ_B = atan(n2 / n1) +/// +/// Rust: `optics::brewster_angle` +#[pyfunction] +#[pyo3(name = "brewster_angle", signature = (n1, n2))] +pub fn pyfn_brewster_angle(n1: f64, n2: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::optics::brewster_angle(n1, n2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Index of refraction: n = c / v +/// +/// Rust: `optics::refractive_index` +#[pyfunction] +#[pyo3(name = "refractive_index", signature = (speed_in_medium))] +pub fn pyfn_refractive_index(speed_in_medium: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::optics::refractive_index(speed_in_medium)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Speed of light in a medium: v = c / n +/// +/// Rust: `optics::speed_in_medium` +#[pyfunction] +#[pyo3(name = "speed_in_medium", signature = (refractive_index))] +pub fn pyfn_speed_in_medium(refractive_index: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::optics::speed_in_medium(refractive_index)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Mirror/thin lens equation: 1/f = 1/d_o + 1/d_i → d_i = f*d_o / (d_o - f) +/// +/// Rust: `optics::image_distance` +#[pyfunction] +#[pyo3(name = "image_distance", signature = (focal_length, object_distance))] +pub fn pyfn_image_distance(focal_length: f64, object_distance: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::optics::image_distance(focal_length, object_distance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Magnification: m = -d_i / d_o +/// +/// Rust: `optics::magnification` +#[pyfunction] +#[pyo3(name = "magnification", signature = (image_distance, object_distance))] +pub fn pyfn_magnification(image_distance: f64, object_distance: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::optics::magnification(image_distance, object_distance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Magnification from image and object heights: m = h_i / h_o +/// +/// Rust: `optics::magnification_from_heights` +#[pyfunction] +#[pyo3(name = "magnification_from_heights", signature = (image_height, object_height))] +pub fn pyfn_magnification_from_heights(image_height: f64, object_height: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::optics::magnification_from_heights(image_height, object_height)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Lens maker's equation: 1/f = (n-1) * (1/R1 - 1/R2) +/// +/// Rust: `optics::lens_focal_length` +#[pyfunction] +#[pyo3(name = "lens_focal_length", signature = (n, r1, r2))] +pub fn pyfn_lens_focal_length(n: f64, r1: f64, r2: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::optics::lens_focal_length(n, r1, r2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Power of a lens: P = 1/f (in diopters when f is in meters) +/// +/// Rust: `optics::lens_power` +#[pyfunction] +#[pyo3(name = "lens_power", signature = (focal_length))] +pub fn pyfn_lens_power(focal_length: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::optics::lens_power(focal_length)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Combined focal length of two thin lenses in contact: 1/f = 1/f1 + 1/f2 +/// +/// Rust: `optics::combined_focal_length` +#[pyfunction] +#[pyo3(name = "combined_focal_length", signature = (f1, f2))] +pub fn pyfn_combined_focal_length(f1: f64, f2: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::optics::combined_focal_length(f1, f2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Mirror radius of curvature: R = 2f +/// +/// Rust: `optics::radius_of_curvature` +#[pyfunction] +#[pyo3(name = "radius_of_curvature", signature = (focal_length))] +pub fn pyfn_radius_of_curvature(focal_length: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::optics::radius_of_curvature(focal_length)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Single slit diffraction minima: a * sin(θ) = m * λ → θ = asin(m * λ / a) +/// Returns angle in radians for the m-th minimum. +/// +/// Rust: `optics::single_slit_minimum` +#[pyfunction] +#[pyo3(name = "single_slit_minimum", signature = (order, wavelength, slit_width))] +pub fn pyfn_single_slit_minimum(order: i32, wavelength: f64, slit_width: f64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::optics::single_slit_minimum(order, wavelength, slit_width)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// Double slit maxima: d * sin(θ) = m * λ → θ = asin(m * λ / d) +/// +/// Rust: `optics::double_slit_maximum` +#[pyfunction] +#[pyo3(name = "double_slit_maximum", signature = (order, wavelength, slit_separation))] +pub fn pyfn_double_slit_maximum(order: i32, wavelength: f64, slit_separation: f64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::optics::double_slit_maximum(order, wavelength, slit_separation)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// Diffraction grating: d * sin(θ) = m * λ +/// +/// Rust: `optics::diffraction_grating_angle` +#[pyfunction] +#[pyo3(name = "diffraction_grating_angle", signature = (order, wavelength, grating_spacing))] +pub fn pyfn_diffraction_grating_angle(order: i32, wavelength: f64, grating_spacing: f64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::optics::diffraction_grating_angle(order, wavelength, grating_spacing)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// Rayleigh criterion (angular resolution): θ = 1.22 * λ / D +/// +/// Rust: `optics::rayleigh_resolution` +#[pyfunction] +#[pyo3(name = "rayleigh_resolution", signature = (wavelength, aperture_diameter))] +pub fn pyfn_rayleigh_resolution(wavelength: f64, aperture_diameter: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::optics::rayleigh_resolution(wavelength, aperture_diameter)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Thin film interference (constructive, normal incidence): +/// 2 * n * t = (m + 0.5) * λ for reflection with one phase change +/// +/// Rust: `optics::thin_film_constructive_thickness` +#[pyfunction] +#[pyo3(name = "thin_film_constructive_thickness", signature = (order, wavelength, film_index))] +pub fn pyfn_thin_film_constructive_thickness(order: u32, wavelength: f64, film_index: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::optics::thin_film_constructive_thickness(order, wavelength, film_index)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Path difference for constructive interference: Δ = m * λ +/// +/// Rust: `optics::constructive_path_diff` +#[pyfunction] +#[pyo3(name = "constructive_path_diff", signature = (order, wavelength))] +pub fn pyfn_constructive_path_diff(order: i32, wavelength: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::optics::constructive_path_diff(order, wavelength)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Path difference for destructive interference: Δ = (m + 0.5) * λ +/// +/// Rust: `optics::destructive_path_diff` +#[pyfunction] +#[pyo3(name = "destructive_path_diff", signature = (order, wavelength))] +pub fn pyfn_destructive_path_diff(order: i32, wavelength: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::optics::destructive_path_diff(order, wavelength)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Malus's law: I = I_0 * cos^2(θ) +/// +/// Rust: `optics::malus_law` +#[pyfunction] +#[pyo3(name = "malus_law", signature = (initial_intensity, angle_rad))] +pub fn pyfn_malus_law(initial_intensity: f64, angle_rad: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::optics::malus_law(initial_intensity, angle_rad)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_snells_law, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_critical_angle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_brewster_angle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_refractive_index, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_speed_in_medium, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_image_distance, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_magnification, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_magnification_from_heights, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lens_focal_length, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lens_power, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_combined_focal_length, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_radius_of_curvature, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_single_slit_minimum, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_double_slit_maximum, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_diffraction_grating_angle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rayleigh_resolution, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_thin_film_constructive_thickness, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_constructive_path_diff, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_destructive_path_diff, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_malus_law, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_optimization.rs b/bindings/python/src/generated/m_optimization.rs new file mode 100644 index 0000000..819ac54 --- /dev/null +++ b/bindings/python/src/generated/m_optimization.rs @@ -0,0 +1,198 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Golden-section search for the minimum of `f` on `[a, b]`. +/// +/// Returns the x value that minimizes f within tolerance `tol`. +/// +/// Rust: `optimization::golden_section_min` +#[pyfunction] +#[pyo3(name = "golden_section_min", signature = (f, a, b, tol, max_iter))] +pub fn pyfn_golden_section_min(f: pyo3::Py, a: f64, b: f64, tol: f64, max_iter: usize) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::golden_section_min(&f, a, b, tol, max_iter)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Brent's method for 1-D minimization, combining golden-section search with +/// parabolic interpolation. +/// +/// Rust: `optimization::brent_min` +#[pyfunction] +#[pyo3(name = "brent_min", signature = (f, a, b, tol, max_iter))] +pub fn pyfn_brent_min(f: pyo3::Py, a: f64, b: f64, tol: f64, max_iter: usize) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::brent_min(&f, a, b, tol, max_iter)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Central-difference numerical gradient of a scalar function of n variables. +/// +/// Rust: `optimization::numerical_gradient_vec` +#[pyfunction] +#[pyo3(name = "numerical_gradient_vec", signature = (f, x, h))] +pub fn pyfn_numerical_gradient_vec(f: pyo3::Py, x: Vec, h: f64) -> PyResult> { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: &[f64]| -> f64 { __cb.call::<_, f64>((__a0.to_vec(),), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::numerical_gradient_vec(&f, &x, h)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Vanilla gradient descent: x ← x − α∇f. +/// +/// Rust: `optimization::gradient_descent` +#[pyfunction] +#[pyo3(name = "gradient_descent", signature = (f, grad, x0, learning_rate, tol, max_iter))] +pub fn pyfn_gradient_descent(f: pyo3::Py, grad: pyo3::Py, x0: Vec, learning_rate: f64, tol: f64, max_iter: usize) -> PyResult> { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: &[f64]| -> f64 { __cb.call::<_, f64>((__a0.to_vec(),), f64::NAN) } }; + let __cb_grad = std::rc::Rc::new(crate::runtime::Callback::new(grad)); + let grad = { let __cb = __cb_grad.clone(); move |__a0: &[f64]| -> Vec { __cb.call::<_, Vec>((__a0.to_vec(),), Vec::new()) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::gradient_descent(&f, &grad, &x0, learning_rate, tol, max_iter)); + crate::runtime::callback::check(&[&__cb_f, &__cb_grad], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Gradient descent with momentum: v ← μv − α∇f, x ← x + v. +/// +/// Rust: `optimization::gradient_descent_momentum` +#[pyfunction] +#[pyo3(name = "gradient_descent_momentum", signature = (f, grad, x0, learning_rate, momentum, tol, max_iter))] +pub fn pyfn_gradient_descent_momentum(f: pyo3::Py, grad: pyo3::Py, x0: Vec, learning_rate: f64, momentum: f64, tol: f64, max_iter: usize) -> PyResult> { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: &[f64]| -> f64 { __cb.call::<_, f64>((__a0.to_vec(),), f64::NAN) } }; + let __cb_grad = std::rc::Rc::new(crate::runtime::Callback::new(grad)); + let grad = { let __cb = __cb_grad.clone(); move |__a0: &[f64]| -> Vec { __cb.call::<_, Vec>((__a0.to_vec(),), Vec::new()) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::gradient_descent_momentum(&f, &grad, &x0, learning_rate, momentum, tol, max_iter)); + crate::runtime::callback::check(&[&__cb_f, &__cb_grad], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Adam optimizer (β1=0.9, β2=0.999, ε=1e-8). +/// +/// Rust: `optimization::adam` +#[pyfunction] +#[pyo3(name = "adam", signature = (f, grad, x0, learning_rate, tol, max_iter))] +pub fn pyfn_adam(f: pyo3::Py, grad: pyo3::Py, x0: Vec, learning_rate: f64, tol: f64, max_iter: usize) -> PyResult> { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: &[f64]| -> f64 { __cb.call::<_, f64>((__a0.to_vec(),), f64::NAN) } }; + let __cb_grad = std::rc::Rc::new(crate::runtime::Callback::new(grad)); + let grad = { let __cb = __cb_grad.clone(); move |__a0: &[f64]| -> Vec { __cb.call::<_, Vec>((__a0.to_vec(),), Vec::new()) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::adam(&f, &grad, &x0, learning_rate, tol, max_iter)); + crate::runtime::callback::check(&[&__cb_f, &__cb_grad], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Nelder-Mead simplex algorithm for unconstrained minimization. +/// +/// Rust: `optimization::nelder_mead` +#[pyfunction] +#[pyo3(name = "nelder_mead", signature = (f, x0, step, tol, max_iter))] +pub fn pyfn_nelder_mead(f: pyo3::Py, x0: Vec, step: f64, tol: f64, max_iter: usize) -> PyResult> { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: &[f64]| -> f64 { __cb.call::<_, f64>((__a0.to_vec(),), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::nelder_mead(&f, &x0, step, tol, max_iter)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Simulated annealing for unconstrained minimization. +/// +/// T = temp_initial × cooling_rate^iter. Accepts uphill moves with probability +/// exp(−ΔE / T). LCG seeded deterministically from `x0`. +/// +/// Rust: `optimization::simulated_annealing` +#[pyfunction] +#[pyo3(name = "simulated_annealing", signature = (f, x0, temp_initial, cooling_rate, step_size, max_iter))] +pub fn pyfn_simulated_annealing(f: pyo3::Py, x0: Vec, temp_initial: f64, cooling_rate: f64, step_size: f64, max_iter: usize) -> PyResult> { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: &[f64]| -> f64 { __cb.call::<_, f64>((__a0.to_vec(),), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::simulated_annealing(&f, &x0, temp_initial, cooling_rate, step_size, max_iter)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Ordinary linear regression: minimizes ‖a0 + a1·x − y‖₂ via +/// Householder-QR least squares (`linalg::qr::least_squares`). +/// +/// Returns `(slope, intercept)`. +/// +/// Rust: `optimization::linear_regression` +#[pyfunction] +#[pyo3(name = "linear_regression", signature = (x, y))] +pub fn pyfn_linear_regression<'py>(py: Python<'py>, x: Vec, y: Vec) -> PyResult<(f64, f64)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::linear_regression(&x, &y))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Fit a polynomial of the given degree to (x, y) data by QR least +/// squares on the Vandermonde matrix, falling back to normal equations +/// with Gaussian elimination when the system is rank deficient. +/// +/// Returns coefficients `[a0, a1, …, a_degree]` such that +/// ŷ = a0 + a1·x + a2·x² + … +/// +/// Rust: `optimization::polynomial_fit` +#[pyfunction] +#[pyo3(name = "polynomial_fit", signature = (x, y, degree))] +pub fn pyfn_polynomial_fit<'py>(py: Python<'py>, x: Vec, y: Vec, degree: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::polynomial_fit(&x, &y, degree))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Coefficient of determination R² = 1 − SS_res / SS_tot. +/// +/// Rust: `optimization::r_squared` +#[pyfunction] +#[pyo3(name = "r_squared", signature = (y_actual, y_predicted))] +pub fn pyfn_r_squared<'py>(py: Python<'py>, y_actual: Vec, y_predicted: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::r_squared(&y_actual, &y_predicted))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_golden_section_min, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_brent_min, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_numerical_gradient_vec, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gradient_descent, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gradient_descent_momentum, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_adam, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_nelder_mead, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_simulated_annealing, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_linear_regression, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_polynomial_fit, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_r_squared, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_optimization__convex.rs b/bindings/python/src/generated/m_optimization__convex.rs new file mode 100644 index 0000000..9de3393 --- /dev/null +++ b/bindings/python/src/generated/m_optimization__convex.rs @@ -0,0 +1,784 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Backtracking line search satisfying the Armijo sufficient-decrease +/// condition. +/// +/// Halves the step until `f(x + t d) <= f(x) + c t g . d`. The condition is +/// what stops a long step that reduces the objective by less than the +/// gradient promised, which is how a descent method diverges on a curved +/// function despite every step going downhill. +/// +/// Panics: +/// Panics unless the direction is a descent direction and `c` lies in +/// `(0, 1)`. +/// +/// Rust: `optimization::convex::backtracking` +#[pyfunction] +#[pyo3(name = "backtracking", signature = (f, x, direction, gradient, c, max_halvings))] +pub fn pyfn_backtracking(f: pyo3::Py, x: Vec, direction: Vec, gradient: Vec, c: f64, max_halvings: usize) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: &[f64]| -> f64 { __cb.call::<_, f64>((__a0.to_vec(),), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::convex::backtracking(&f, &x, &direction, &gradient, c, max_halvings)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// A line search satisfying the strong Wolfe conditions. +/// +/// Armijo alone allows arbitrarily *short* steps, which stalls a quasi-Newton +/// method: the curvature information it accumulates comes from the difference +/// between successive gradients, and a step too short to change the gradient +/// carries none. The second Wolfe condition, +/// `|g(x + t d) . d| <= c2 |g(x) . d|`, rules that out by demanding the slope +/// actually flatten. Together they are what makes the BFGS update +/// well defined. +/// +/// Panics: +/// Panics unless `0 < c1 < c2 < 1` and the direction descends. +/// +/// Rust: `optimization::convex::line_search_wolfe` +#[pyfunction] +#[pyo3(name = "line_search_wolfe", signature = (f, grad, x, direction, c1, c2))] +pub fn pyfn_line_search_wolfe(f: pyo3::Py, grad: pyo3::Py, x: Vec, direction: Vec, c1: f64, c2: f64) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: &[f64]| -> f64 { __cb.call::<_, f64>((__a0.to_vec(),), f64::NAN) } }; + let __cb_grad = std::rc::Rc::new(crate::runtime::Callback::new(grad)); + let grad = { let __cb = __cb_grad.clone(); move |__a0: &[f64]| -> Vec { __cb.call::<_, Vec>((__a0.to_vec(),), Vec::new()) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::convex::line_search_wolfe(&f, &grad, &x, &direction, c1, c2)); + crate::runtime::callback::check(&[&__cb_f, &__cb_grad], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// An exact line search, by root-finding on the directional derivative. +/// +/// The minimiser of `phi(t) = f(x + t d)` is where `phi'(t) = g(x + t d) . d` +/// vanishes. Since `phi'(0) < 0` for a descent direction, all that is needed +/// is a `t` where the slope has turned non-negative; bisection then locates +/// the root to machine precision. On a quadratic `phi'` is affine, so the +/// answer is exact to rounding. +/// +/// Returns `None` when no such bracket exists within a doubling cap, which +/// means the function is unbounded below along the direction -- there is no +/// minimiser to find, and the caller should use an inexact search instead. +/// +/// Panics: +/// Panics unless the direction descends. +/// +/// Rust: `optimization::convex::exact_line_search` +#[pyfunction] +#[pyo3(name = "exact_line_search", signature = (grad, x, direction))] +pub fn pyfn_exact_line_search(grad: pyo3::Py, x: Vec, direction: Vec) -> PyResult> { + let __cb_grad = std::rc::Rc::new(crate::runtime::Callback::new(grad)); + let grad = { let __cb = __cb_grad.clone(); move |__a0: &[f64]| -> Vec { __cb.call::<_, Vec>((__a0.to_vec(),), Vec::new()) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::convex::exact_line_search(&grad, &x, &direction)); + crate::runtime::callback::check(&[&__cb_grad], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// Nesterov's accelerated gradient method. +/// +/// Evaluates the gradient at an extrapolated point rather than the current +/// one, which is the whole difference from heavy-ball momentum: the method +/// gets to see where the momentum is taking it before committing. That +/// changes the convergence rate on a smooth convex function from `O(1/k)` to +/// `O(1/k^2)`, which is optimal for a method that only ever sees gradients. +/// +/// Panics: +/// Panics if the learning rate is not positive. +/// +/// Rust: `optimization::convex::nesterov` +#[pyfunction] +#[pyo3(name = "nesterov", signature = (grad, x0, learning_rate, momentum, iterations))] +pub fn pyfn_nesterov(grad: pyo3::Py, x0: Vec, learning_rate: f64, momentum: f64, iterations: usize) -> PyResult> { + let __cb_grad = std::rc::Rc::new(crate::runtime::Callback::new(grad)); + let grad = { let __cb = __cb_grad.clone(); move |__a0: &[f64]| -> Vec { __cb.call::<_, Vec>((__a0.to_vec(),), Vec::new()) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::convex::nesterov(&grad, &x0, learning_rate, momentum, iterations)); + crate::runtime::callback::check(&[&__cb_grad], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Adagrad: scale each coordinate's step by the inverse root of its +/// accumulated squared gradient. +/// +/// Coordinates with consistently large gradients get short steps and rare +/// coordinates get long ones, which is what makes it suit sparse problems. +/// The accumulator only grows, so the effective learning rate decays +/// monotonically to zero -- helpful for convergence, fatal if the problem +/// needs to keep moving, which is what `rmsprop` fixes. +/// +/// Panics: +/// Panics if the learning rate is not positive. +/// +/// Rust: `optimization::convex::adagrad` +#[pyfunction] +#[pyo3(name = "adagrad", signature = (grad, x0, learning_rate, iterations))] +pub fn pyfn_adagrad(grad: pyo3::Py, x0: Vec, learning_rate: f64, iterations: usize) -> PyResult> { + let __cb_grad = std::rc::Rc::new(crate::runtime::Callback::new(grad)); + let grad = { let __cb = __cb_grad.clone(); move |__a0: &[f64]| -> Vec { __cb.call::<_, Vec>((__a0.to_vec(),), Vec::new()) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::convex::adagrad(&grad, &x0, learning_rate, iterations)); + crate::runtime::callback::check(&[&__cb_grad], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// RMSProp: Adagrad with an exponentially weighted accumulator. +/// +/// Forgetting old gradients keeps the effective learning rate from decaying +/// to zero, so the method can keep making progress indefinitely. +/// +/// Panics: +/// Panics unless the learning rate is positive and `decay` lies in `[0, 1)`. +/// +/// Rust: `optimization::convex::rmsprop` +#[pyfunction] +#[pyo3(name = "rmsprop", signature = (grad, x0, learning_rate, decay, iterations))] +pub fn pyfn_rmsprop(grad: pyo3::Py, x0: Vec, learning_rate: f64, decay: f64, iterations: usize) -> PyResult> { + let __cb_grad = std::rc::Rc::new(crate::runtime::Callback::new(grad)); + let grad = { let __cb = __cb_grad.clone(); move |__a0: &[f64]| -> Vec { __cb.call::<_, Vec>((__a0.to_vec(),), Vec::new()) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::convex::rmsprop(&grad, &x0, learning_rate, decay, iterations)); + crate::runtime::callback::check(&[&__cb_grad], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// AdamW: Adam with the weight decay applied to the parameters directly +/// rather than folded into the gradient. +/// +/// The distinction matters because Adam divides the gradient by its own +/// running scale. A decay term added to the gradient gets divided too, so its +/// strength ends up depending on how large the other gradients happen to be; +/// applied to the parameters it does not. That is the entire content of the +/// change, and it is why the two behave differently at the same nominal decay. +/// +/// Panics: +/// Panics unless the learning rate is positive and both moment decays lie in +/// `[0, 1)`. +/// +/// Rust: `optimization::convex::adamw` +#[pyfunction] +#[pyo3(name = "adamw", signature = (grad, x0, learning_rate, weight_decay, iterations))] +pub fn pyfn_adamw(grad: pyo3::Py, x0: Vec, learning_rate: f64, weight_decay: f64, iterations: usize) -> PyResult> { + let __cb_grad = std::rc::Rc::new(crate::runtime::Callback::new(grad)); + let grad = { let __cb = __cb_grad.clone(); move |__a0: &[f64]| -> Vec { __cb.call::<_, Vec>((__a0.to_vec(),), Vec::new()) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::convex::adamw(&grad, &x0, learning_rate, weight_decay, iterations)); + crate::runtime::callback::check(&[&__cb_grad], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Subgradient descent for a convex objective that is not differentiable. +/// +/// A subgradient is not a descent direction -- moving along it can increase +/// the objective, which is why the best value seen has to be tracked +/// separately rather than read off the last iterate. With a step size going +/// to zero but summing to infinity the method converges, at `O(1/sqrt(k))`: +/// far worse than the smooth case, and the price of giving up +/// differentiability. +/// +/// Returns the best point found. +/// +/// Panics: +/// Panics if the initial step is not positive. +/// +/// Rust: `optimization::convex::subgradient_method` +#[pyfunction] +#[pyo3(name = "subgradient_method", signature = (f, subgradient, x0, initial_step, iterations))] +pub fn pyfn_subgradient_method(f: pyo3::Py, subgradient: pyo3::Py, x0: Vec, initial_step: f64, iterations: usize) -> PyResult<(Vec, f64)> { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: &[f64]| -> f64 { __cb.call::<_, f64>((__a0.to_vec(),), f64::NAN) } }; + let __cb_subgradient = std::rc::Rc::new(crate::runtime::Callback::new(subgradient)); + let subgradient = { let __cb = __cb_subgradient.clone(); move |__a0: &[f64]| -> Vec { __cb.call::<_, Vec>((__a0.to_vec(),), Vec::new()) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::convex::subgradient_method(&f, &subgradient, &x0, initial_step, iterations)); + crate::runtime::callback::check(&[&__cb_f, &__cb_subgradient], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// BFGS with a Wolfe line search. +/// +/// Maintains an approximation to the *inverse* Hessian, updated from the +/// change in gradient across each step, so a Newton-like direction costs one +/// matrix-vector product and no solve. The update preserves positive +/// definiteness whenever the curvature condition `y . s > 0` holds, which the +/// Wolfe line search guarantees -- the two are designed together, and pairing +/// BFGS with a plain Armijo search is a classic way to make it fail. +/// +/// Errors: +/// Returns an error if the starting point is empty. +/// +/// Rust: `optimization::convex::bfgs` +#[pyfunction] +#[pyo3(name = "bfgs", signature = (f, grad, x0, tol, max_iter))] +pub fn pyfn_bfgs(f: pyo3::Py, grad: pyo3::Py, x0: Vec, tol: f64, max_iter: usize) -> PyResult<(Vec, f64)> { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: &[f64]| -> f64 { __cb.call::<_, f64>((__a0.to_vec(),), f64::NAN) } }; + let __cb_grad = std::rc::Rc::new(crate::runtime::Callback::new(grad)); + let grad = { let __cb = __cb_grad.clone(); move |__a0: &[f64]| -> Vec { __cb.call::<_, Vec>((__a0.to_vec(),), Vec::new()) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::convex::bfgs(&f, &grad, &x0, tol, max_iter)); + crate::runtime::callback::check(&[&__cb_f, &__cb_grad], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// Limited-memory BFGS. +/// +/// Stores the last `m` pairs of step and gradient change instead of a full +/// matrix, and reconstructs the search direction by a two-loop recursion. +/// Memory drops from `n^2` to `mn`, which is what makes the method usable +/// where `n` runs to millions and a dense inverse Hessian could not be stored +/// at all, let alone factored. +/// +/// Errors: +/// Returns an error if the starting point is empty or `m` is zero. +/// +/// Rust: `optimization::convex::lbfgs` +#[pyfunction] +#[pyo3(name = "lbfgs", signature = (f, grad, x0, m, tol, max_iter))] +pub fn pyfn_lbfgs(f: pyo3::Py, grad: pyo3::Py, x0: Vec, m: usize, tol: f64, max_iter: usize) -> PyResult<(Vec, f64)> { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: &[f64]| -> f64 { __cb.call::<_, f64>((__a0.to_vec(),), f64::NAN) } }; + let __cb_grad = std::rc::Rc::new(crate::runtime::Callback::new(grad)); + let grad = { let __cb = __cb_grad.clone(); move |__a0: &[f64]| -> Vec { __cb.call::<_, Vec>((__a0.to_vec(),), Vec::new()) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::convex::lbfgs(&f, &grad, &x0, m, tol, max_iter)); + crate::runtime::callback::check(&[&__cb_f, &__cb_grad], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// Nonlinear conjugate gradients with the Polak-Ribiere update. +/// +/// On a quadratic the directions produced are mutually conjugate, so the +/// method reaches the exact minimum in at most `n` steps -- an exact finite +/// termination, not a rate. Away from a quadratic that guarantee lapses, +/// and the restart when `beta` goes negative is what keeps the directions +/// descending regardless. +/// +/// Errors: +/// Returns an error if the starting point is empty. +/// +/// Rust: `optimization::convex::conjugate_gradient_nonlinear` +#[pyfunction] +#[pyo3(name = "conjugate_gradient_nonlinear", signature = (f, grad, x0, tol, max_iter))] +pub fn pyfn_conjugate_gradient_nonlinear(f: pyo3::Py, grad: pyo3::Py, x0: Vec, tol: f64, max_iter: usize) -> PyResult<(Vec, f64)> { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: &[f64]| -> f64 { __cb.call::<_, f64>((__a0.to_vec(),), f64::NAN) } }; + let __cb_grad = std::rc::Rc::new(crate::runtime::Callback::new(grad)); + let grad = { let __cb = __cb_grad.clone(); move |__a0: &[f64]| -> Vec { __cb.call::<_, Vec>((__a0.to_vec(),), Vec::new()) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::convex::conjugate_gradient_nonlinear(&f, &grad, &x0, tol, max_iter)); + crate::runtime::callback::check(&[&__cb_f, &__cb_grad], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// The proximal operator of `t ||x||_1`: soft thresholding. +/// +/// `prox(v) = sign(v) max(|v| - t, 0)`, which is the exact minimiser of +/// `||x - v||^2 / 2 + t ||x||_1`. It is what makes L1 penalties produce +/// genuinely zero coefficients rather than merely small ones -- the operator +/// maps a whole interval to exactly zero, which no smooth penalty does. +/// +/// Rust: `optimization::convex::prox_l1` +#[pyfunction] +#[pyo3(name = "prox_l1", signature = (v, t))] +pub fn pyfn_prox_l1<'py>(py: Python<'py>, v: Vec, t: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::convex::prox_l1(&v, t))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The proximal operator of `t ||x||_2` (the norm, not its square): block +/// soft thresholding. +/// +/// Shrinks the whole vector toward zero and sets it to exactly zero once its +/// norm falls below `t`. Unlike `prox_l1` it acts on the vector as a unit, +/// which is what group-sparse penalties need. +/// +/// Rust: `optimization::convex::prox_l2` +#[pyfunction] +#[pyo3(name = "prox_l2", signature = (v, t))] +pub fn pyfn_prox_l2<'py>(py: Python<'py>, v: Vec, t: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::convex::prox_l2(&v, t))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The proximal operator of a box constraint: clamping. +/// +/// The proximal operator of an indicator function is the projection onto the +/// set, and for a box that is coordinatewise clamping. +/// +/// Rust: `optimization::convex::prox_box` +#[pyfunction] +#[pyo3(name = "prox_box", signature = (v, lo, hi))] +pub fn pyfn_prox_box<'py>(py: Python<'py>, v: Vec, lo: f64, hi: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::convex::prox_box(&v, lo, hi))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Euclidean projection onto the probability simplex. +/// +/// Sort, find the threshold at which the shifted positive parts sum to one, +/// and subtract it. The result is the closest point of the simplex, which is +/// not simply the clamped-and-renormalised vector -- that is a common +/// substitute and it is a different point. +/// +/// Panics: +/// Panics if the vector is empty. +/// +/// Rust: `optimization::convex::prox_simplex` +#[pyfunction] +#[pyo3(name = "prox_simplex", signature = (v))] +pub fn pyfn_prox_simplex<'py>(py: Python<'py>, v: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::convex::prox_simplex(&v))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Proximal gradient descent, also called ISTA: a gradient step on the smooth +/// part followed by the proximal operator of the rest. +/// +/// The whole point is that the non-smooth part never needs a gradient. It +/// only has to have a proximal operator that can be evaluated, and for the +/// penalties that matter -- L1, group norms, indicator functions -- that +/// operator is a closed form. +/// +/// Converges at `O(1/k)`. +/// +/// Panics: +/// Panics if the step size is not positive. +/// +/// Rust: `optimization::convex::proximal_gradient` +#[pyfunction] +#[pyo3(name = "proximal_gradient", signature = (smooth_grad, prox, x0, step, iterations))] +pub fn pyfn_proximal_gradient(smooth_grad: pyo3::Py, prox: pyo3::Py, x0: Vec, step: f64, iterations: usize) -> PyResult> { + let __cb_smooth_grad = std::rc::Rc::new(crate::runtime::Callback::new(smooth_grad)); + let smooth_grad = { let __cb = __cb_smooth_grad.clone(); move |__a0: &[f64]| -> Vec { __cb.call::<_, Vec>((__a0.to_vec(),), Vec::new()) } }; + let __cb_prox = std::rc::Rc::new(crate::runtime::Callback::new(prox)); + let prox = { let __cb = __cb_prox.clone(); move |__a0: &[f64], __a1: f64| -> Vec { __cb.call::<_, Vec>((__a0.to_vec(), __a1), Vec::new()) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::convex::proximal_gradient(&smooth_grad, &prox, &x0, step, iterations)); + crate::runtime::callback::check(&[&__cb_smooth_grad, &__cb_prox], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// FISTA: proximal gradient descent with Nesterov's extrapolation. +/// +/// The same two operations per iteration as `proximal_gradient`, applied at +/// an extrapolated point, which improves the rate from `O(1/k)` to `O(1/k^2)` +/// for no extra cost per step. The momentum sequence +/// `t_{k+1} = (1 + sqrt(1 + 4 t_k^2)) / 2` is what makes the accelerated +/// bound come out; an arbitrary momentum does not. +/// +/// Panics: +/// Panics if the step size is not positive. +/// +/// Rust: `optimization::convex::fista` +#[pyfunction] +#[pyo3(name = "fista", signature = (smooth_grad, prox, x0, step, iterations))] +pub fn pyfn_fista(smooth_grad: pyo3::Py, prox: pyo3::Py, x0: Vec, step: f64, iterations: usize) -> PyResult> { + let __cb_smooth_grad = std::rc::Rc::new(crate::runtime::Callback::new(smooth_grad)); + let smooth_grad = { let __cb = __cb_smooth_grad.clone(); move |__a0: &[f64]| -> Vec { __cb.call::<_, Vec>((__a0.to_vec(),), Vec::new()) } }; + let __cb_prox = std::rc::Rc::new(crate::runtime::Callback::new(prox)); + let prox = { let __cb = __cb_prox.clone(); move |__a0: &[f64], __a1: f64| -> Vec { __cb.call::<_, Vec>((__a0.to_vec(), __a1), Vec::new()) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::convex::fista(&smooth_grad, &prox, &x0, step, iterations)); + crate::runtime::callback::check(&[&__cb_smooth_grad, &__cb_prox], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Projected gradient descent for a constrained smooth problem. +/// +/// Take a gradient step, then project back onto the feasible set. Correct +/// whenever the set is convex and the projection is available; the projection +/// is what makes or breaks it, since for most sets it is itself an +/// optimisation problem. +/// +/// Panics: +/// Panics if the step size is not positive. +/// +/// Rust: `optimization::convex::projected_gradient` +#[pyfunction] +#[pyo3(name = "projected_gradient", signature = (grad, project, x0, step, iterations))] +pub fn pyfn_projected_gradient(grad: pyo3::Py, project: pyo3::Py, x0: Vec, step: f64, iterations: usize) -> PyResult> { + let __cb_grad = std::rc::Rc::new(crate::runtime::Callback::new(grad)); + let grad = { let __cb = __cb_grad.clone(); move |__a0: &[f64]| -> Vec { __cb.call::<_, Vec>((__a0.to_vec(),), Vec::new()) } }; + let __cb_project = std::rc::Rc::new(crate::runtime::Callback::new(project)); + let project = { let __cb = __cb_project.clone(); move |__a0: &[f64]| -> Vec { __cb.call::<_, Vec>((__a0.to_vec(),), Vec::new()) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::convex::projected_gradient(&grad, &project, &x0, step, iterations)); + crate::runtime::callback::check(&[&__cb_grad, &__cb_project], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Frank-Wolfe method, also called conditional gradient. +/// +/// Instead of projecting, it minimises a linear approximation over the +/// feasible set and moves toward that vertex. The iterate stays feasible +/// automatically as a convex combination of feasible points, so no projection +/// is ever needed -- which is the reason to use it when a linear minimisation +/// over the set is cheap and a projection is not. +/// +/// `linear_oracle` returns the minimiser of a linear function over the set. +/// +/// Panics: +/// Panics if the starting point is empty. +/// +/// Rust: `optimization::convex::frank_wolfe` +#[pyfunction] +#[pyo3(name = "frank_wolfe", signature = (grad, linear_oracle, x0, iterations))] +pub fn pyfn_frank_wolfe(grad: pyo3::Py, linear_oracle: pyo3::Py, x0: Vec, iterations: usize) -> PyResult> { + let __cb_grad = std::rc::Rc::new(crate::runtime::Callback::new(grad)); + let grad = { let __cb = __cb_grad.clone(); move |__a0: &[f64]| -> Vec { __cb.call::<_, Vec>((__a0.to_vec(),), Vec::new()) } }; + let __cb_linear_oracle = std::rc::Rc::new(crate::runtime::Callback::new(linear_oracle)); + let linear_oracle = { let __cb = __cb_linear_oracle.clone(); move |__a0: &[f64]| -> Vec { __cb.call::<_, Vec>((__a0.to_vec(),), Vec::new()) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::convex::frank_wolfe(&grad, &linear_oracle, &x0, iterations)); + crate::runtime::callback::check(&[&__cb_grad, &__cb_linear_oracle], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Mirror descent on the probability simplex, with the entropy mirror map. +/// +/// The multiplicative update `x_i <- x_i exp(-t g_i)` followed by +/// renormalisation. Because the geometry matches the constraint set, the +/// dependence on dimension is `sqrt(log n)` rather than the `sqrt(n)` a +/// Euclidean projected gradient pays -- a large difference when the simplex +/// is over thousands of outcomes. +/// +/// Panics: +/// Panics if the starting point is empty or the step is not positive. +/// +/// Rust: `optimization::convex::mirror_descent_simplex` +#[pyfunction] +#[pyo3(name = "mirror_descent_simplex", signature = (grad, x0, step, iterations))] +pub fn pyfn_mirror_descent_simplex(grad: pyo3::Py, x0: Vec, step: f64, iterations: usize) -> PyResult> { + let __cb_grad = std::rc::Rc::new(crate::runtime::Callback::new(grad)); + let grad = { let __cb = __cb_grad.clone(); move |__a0: &[f64]| -> Vec { __cb.call::<_, Vec>((__a0.to_vec(),), Vec::new()) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::convex::mirror_descent_simplex(&grad, &x0, step, iterations)); + crate::runtime::callback::check(&[&__cb_grad], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Ridge regression in closed form: solve `(A'A + lambda I) x = A'b`. +/// +/// The one regularised regression with an exact answer, because the penalty +/// is smooth and quadratic like the loss. The added `lambda I` is also what +/// makes the system solvable when `A'A` is singular -- ridge regression +/// regularises the numerics as much as the statistics. +/// +/// Errors: +/// Returns an error on a shape mismatch, a negative penalty, or a system that +/// is singular even after regularisation. +/// +/// Rust: `optimization::convex::ridge_closed_form` +#[pyfunction] +#[pyo3(name = "ridge_closed_form", signature = (a, b, lambda_))] +pub fn pyfn_ridge_closed_form<'py>(py: Python<'py>, a: crate::generated::types::PyMatrixArg, b: Vec, lambda_: f64) -> PyResult> { + let a = a.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::convex::ridge_closed_form(&a, &b, lambda_))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Lasso by cyclic coordinate descent. +/// +/// Each coordinate is minimised exactly with the others held fixed, and that +/// one-dimensional problem has the soft-threshold closed form. Coordinate +/// descent works here precisely because the non-smooth part is *separable*: +/// the L1 penalty splits across coordinates, so a coordinatewise minimum is a +/// genuine minimum. On a non-separable penalty the same loop can stall at a +/// point that is optimal in every single direction and not optimal at all. +/// +/// Minimises `||A x - b||^2 / (2 n) + lambda ||x||_1`. +/// +/// Errors: +/// Returns an error on a shape mismatch or a negative penalty. +/// +/// Rust: `optimization::convex::lasso_coordinate_descent` +#[pyfunction] +#[pyo3(name = "lasso_coordinate_descent", signature = (a, b, lambda_, iterations))] +pub fn pyfn_lasso_coordinate_descent<'py>(py: Python<'py>, a: crate::generated::types::PyMatrixArg, b: Vec, lambda_: f64, iterations: usize) -> PyResult> { + let a = a.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::convex::lasso_coordinate_descent(&a, &b, lambda_, iterations))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The lasso by the alternating direction method of multipliers. +/// +/// Splits the objective into the smooth least-squares part and the L1 part +/// with a copy of the variable, then alternates: a ridge solve, a soft +/// threshold, and a dual update. The factorisation of the ridge system does +/// not change between iterations, so it can be computed once -- which is what +/// makes ADMM cheap here despite doing a linear solve every step. +/// +/// Solves the same problem as `lasso_coordinate_descent` and must agree +/// with it. +/// +/// Errors: +/// Returns an error on a shape mismatch, a non-positive penalty parameter, or +/// a singular system. +/// +/// Rust: `optimization::convex::admm_lasso` +#[pyfunction] +#[pyo3(name = "admm_lasso", signature = (a, b, lambda_, rho, iterations))] +pub fn pyfn_admm_lasso<'py>(py: Python<'py>, a: crate::generated::types::PyMatrixArg, b: Vec, lambda_: f64, rho: f64, iterations: usize) -> PyResult> { + let a = a.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::convex::admm_lasso(&a, &b, lambda_, rho, iterations))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// A generic two-block ADMM. +/// +/// Minimises `f(x) + g(z)` subject to `x = z`, given only the proximal +/// operator of each part. The two halves never need to be handled together, +/// which is the point: a problem that is hard as a whole is often two easy +/// problems joined by a constraint. +/// +/// Panics: +/// Panics if `rho` is not positive or the starting point is empty. +/// +/// Rust: `optimization::convex::admm_generic` +#[pyfunction] +#[pyo3(name = "admm_generic", signature = (prox_f, prox_g, x0, rho, iterations))] +pub fn pyfn_admm_generic(prox_f: pyo3::Py, prox_g: pyo3::Py, x0: Vec, rho: f64, iterations: usize) -> PyResult> { + let __cb_prox_f = std::rc::Rc::new(crate::runtime::Callback::new(prox_f)); + let prox_f = { let __cb = __cb_prox_f.clone(); move |__a0: &[f64], __a1: f64| -> Vec { __cb.call::<_, Vec>((__a0.to_vec(), __a1), Vec::new()) } }; + let __cb_prox_g = std::rc::Rc::new(crate::runtime::Callback::new(prox_g)); + let prox_g = { let __cb = __cb_prox_g.clone(); move |__a0: &[f64], __a1: f64| -> Vec { __cb.call::<_, Vec>((__a0.to_vec(), __a1), Vec::new()) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::convex::admm_generic(&prox_f, &prox_g, &x0, rho, iterations)); + crate::runtime::callback::check(&[&__cb_prox_f, &__cb_prox_g], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Elastic net regression: an L1 and an L2 penalty together. +/// +/// The L1 part selects variables and the L2 part keeps correlated ones +/// together. Pure lasso picks arbitrarily among a group of correlated +/// predictors and zeroes the rest, which is unstable under resampling; the +/// ridge term removes that arbitrariness. At `l1 = 0` it is ridge and at +/// `l2 = 0` it is the lasso, and the tests check both limits. +/// +/// Errors: +/// Returns an error on a shape mismatch or a negative penalty. +/// +/// Rust: `optimization::convex::elastic_net` +#[pyfunction] +#[pyo3(name = "elastic_net", signature = (a, b, l1, l2, iterations))] +pub fn pyfn_elastic_net<'py>(py: Python<'py>, a: crate::generated::types::PyMatrixArg, b: Vec, l1: f64, l2: f64, iterations: usize) -> PyResult> { + let a = a.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::convex::elastic_net(&a, &b, l1, l2, iterations))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// L2-penalised logistic regression, fitted by Newton's method. +/// +/// The penalised log-likelihood is strictly concave for any positive penalty, +/// so the maximum is unique and Newton's method converges quadratically to +/// it. Without the penalty, perfectly separable data has no finite maximiser +/// at all -- the coefficients run to infinity as the fitted probabilities +/// approach zero and one -- which is a property of the data rather than a +/// failure of the solver, and the penalty is what makes the problem +/// well posed. +/// +/// `y` holds zeros and ones. Returns the coefficients. +/// +/// Errors: +/// Returns an error on a shape mismatch, a label outside `{0, 1}`, or a +/// non-positive penalty. +/// +/// Rust: `optimization::convex::logistic_regression_fit` +#[pyfunction] +#[pyo3(name = "logistic_regression_fit", signature = (x, y, lambda_, iterations))] +pub fn pyfn_logistic_regression_fit<'py>(py: Python<'py>, x: crate::generated::types::PyMatrixArg, y: Vec, lambda_: f64, iterations: usize) -> PyResult> { + let x = x.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::convex::logistic_regression_fit(&x, &y, lambda_, iterations))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// A convex quadratic program with equality constraints, by the active-set +/// idea applied to the equalities alone. +/// +/// Minimises `x'Qx/2 + c'x` subject to `Ax = b`. With only equalities the +/// active set is fixed, so the whole problem is one KKT linear system: +/// stationarity and feasibility stacked together. The solution satisfies +/// `Qx + c + A'y = 0` exactly, which is what the tests check rather than +/// merely that the objective looks small. +/// +/// Errors: +/// Returns an error on a shape mismatch or a singular KKT system. +/// +/// Rust: `optimization::convex::quadratic_program_active_set` +#[pyfunction] +#[pyo3(name = "quadratic_program_active_set", signature = (q, c, a, b))] +pub fn pyfn_quadratic_program_active_set<'py>(py: Python<'py>, q: crate::generated::types::PyMatrixArg, c: Vec, a: crate::generated::types::PyMatrixArg, b: Vec) -> PyResult<(Vec, Vec)> { + let q = q.0; + let a = a.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::convex::quadratic_program_active_set(&q, &c, &a, &b))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// The norm of the Karush-Kuhn-Tucker residual at a candidate point. +/// +/// Stacks the stationarity condition `grad f + sum y_i grad c_i` and the +/// feasibility conditions `c_i(x) = 0`. Zero exactly at a constrained +/// stationary point, which makes it the natural way to check a constrained +/// solver: it tests the conditions the answer must satisfy rather than +/// comparing against another solver that could share the same mistake. +/// +/// Rust: `optimization::convex::kkt_residual` +#[pyfunction] +#[pyo3(name = "kkt_residual", signature = (objective_gradient, constraint_values, constraint_gradients, multipliers))] +pub fn pyfn_kkt_residual<'py>(py: Python<'py>, objective_gradient: Vec, constraint_values: Vec, constraint_gradients: Vec>, multipliers: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::convex::kkt_residual(&objective_gradient, &constraint_values, &constraint_gradients, &multipliers))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Dual ascent for an equality-constrained problem. +/// +/// Alternates minimising the Lagrangian over `x` with a gradient step on the +/// multipliers, whose gradient is the constraint violation itself. It +/// converges only under strong assumptions -- strict convexity of the +/// objective, chiefly -- which is precisely the gap that the augmented +/// Lagrangian and ADMM close by adding a penalty term. +/// +/// `minimise_lagrangian` returns the minimiser of `f(x) + y . c(x)` for the +/// given multipliers. +/// +/// Panics: +/// Panics if the step is not positive. +/// +/// Rust: `optimization::convex::dual_ascent` +#[pyfunction] +#[pyo3(name = "dual_ascent", signature = (minimise_lagrangian, constraints, multipliers0, step, iterations))] +pub fn pyfn_dual_ascent(minimise_lagrangian: pyo3::Py, constraints: pyo3::Py, multipliers0: Vec, step: f64, iterations: usize) -> PyResult<(Vec, Vec)> { + let __cb_minimise_lagrangian = std::rc::Rc::new(crate::runtime::Callback::new(minimise_lagrangian)); + let minimise_lagrangian = { let __cb = __cb_minimise_lagrangian.clone(); move |__a0: &[f64]| -> Vec { __cb.call::<_, Vec>((__a0.to_vec(),), Vec::new()) } }; + let __cb_constraints = std::rc::Rc::new(crate::runtime::Callback::new(constraints)); + let constraints = { let __cb = __cb_constraints.clone(); move |__a0: &[f64]| -> Vec { __cb.call::<_, Vec>((__a0.to_vec(),), Vec::new()) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::convex::dual_ascent(&minimise_lagrangian, &constraints, &multipliers0, step, iterations)); + crate::runtime::callback::check(&[&__cb_minimise_lagrangian, &__cb_constraints], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Tests convexity numerically by sampling the midpoint inequality. +/// +/// A convex function satisfies `f((a+b)/2) <= (f(a) + f(b)) / 2` for every +/// pair. Sampling can only ever *refute* convexity, never establish it: a +/// single violating pair is a proof of non-convexity, while a million +/// satisfying pairs prove nothing about the pairs not tried. The return value +/// should be read accordingly -- `false` is a fact and `true` is an absence +/// of evidence. +/// +/// Panics: +/// Panics if the bounds are empty or `trials` is zero. +/// +/// Rust: `optimization::convex::convexity_check_numeric` +#[pyfunction] +#[pyo3(name = "convexity_check_numeric", signature = (f, bounds, trials, rng))] +pub fn pyfn_convexity_check_numeric(f: pyo3::Py, bounds: Vec<(f64, f64)>, trials: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: &[f64]| -> f64 { __cb.call::<_, f64>((__a0.to_vec(),), f64::NAN) } }; + let bounds = bounds.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::convex::convexity_check_numeric(&f, &bounds, trials, &mut rng.inner)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Iterations that gradient descent and conjugate gradients need on a +/// two-dimensional quadratic of the given condition number. +/// +/// Returns `(gradient descent, conjugate gradients)`. The contrast is the +/// whole point: gradient descent's error contracts by `(k-1)/(k+1)` per +/// step, so its count grows linearly in the condition number, while +/// conjugate gradients terminate in at most `n` steps whatever the +/// conditioning. At a condition number of a thousand that is hundreds of +/// iterations against two. +/// +/// Panics: +/// Panics if the condition number is below one. +/// +/// Rust: `optimization::convex::condition_number_effect_demo` +#[pyfunction] +#[pyo3(name = "condition_number_effect_demo", signature = (kappa))] +pub fn pyfn_condition_number_effect_demo(kappa: f64) -> PyResult<(usize, usize)> { + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::convex::condition_number_effect_demo(kappa)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_backtracking, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_line_search_wolfe, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_exact_line_search, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_nesterov, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_adagrad, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rmsprop, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_adamw, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_subgradient_method, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bfgs, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lbfgs, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_conjugate_gradient_nonlinear, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_prox_l1, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_prox_l2, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_prox_box, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_prox_simplex, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_proximal_gradient, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fista, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_projected_gradient, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_frank_wolfe, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mirror_descent_simplex, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ridge_closed_form, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lasso_coordinate_descent, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_admm_lasso, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_admm_generic, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_elastic_net, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_logistic_regression_fit, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_quadratic_program_active_set, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kkt_residual, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dual_ascent, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_convexity_check_numeric, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_condition_number_effect_demo, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_optimization__game_theory.rs b/bindings/python/src/generated/m_optimization__game_theory.rs new file mode 100644 index 0000000..5c14439 --- /dev/null +++ b/bindings/python/src/generated/m_optimization__game_theory.rs @@ -0,0 +1,887 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// The value of a two-player zero-sum game and the optimal mixed strategies, +/// as `(value, row strategy, column strategy)`. +/// +/// The row player's guaranteed floor and the column player's guaranteed +/// ceiling coincide. That coincidence is the minimax theorem, and it is not +/// assumed here: the two players' programs are LP duals, so strong duality +/// delivers it. What makes the result surprising is that it fails without +/// mixing -- in matching pennies the pure maximin is -1 and the pure minimax +/// is +1 -- so the theorem is really a statement about the power of +/// randomisation. +/// +/// Errors: +/// Returns an error if the underlying program has no optimum, which for a +/// finite game means a numerical failure rather than a modelling one. +/// +/// Rust: `optimization::game_theory::minimax_value` +#[pyfunction] +#[pyo3(name = "minimax_value", signature = (payoff))] +pub fn pyfn_minimax_value<'py>(py: Python<'py>, payoff: crate::generated::types::PyMatrixArg) -> PyResult<(f64, Vec, Vec)> { + let payoff = payoff.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::game_theory::minimax_value(&payoff))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// The row indices strictly dominated by some other pure row. +/// +/// Strict domination is the one elimination that is always safe: a strictly +/// dominated strategy is played with probability zero in every equilibrium, +/// so removing it removes no equilibria. *Weak* domination does not have that +/// property, which is why only the strict version is offered. +/// +/// Rust: `optimization::game_theory::dominated_strategies` +#[pyfunction] +#[pyo3(name = "dominated_strategies", signature = (payoff))] +pub fn pyfn_dominated_strategies<'py>(py: Python<'py>, payoff: crate::generated::types::PyMatrixArg) -> PyResult> { + let payoff = payoff.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::game_theory::dominated_strategies(&payoff))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Iterated elimination of strictly dominated strategies, returning the row +/// and column indices that survive. +/// +/// The order of elimination does not matter for strict domination: the +/// surviving set is the same however the eliminations are sequenced. That is +/// a genuine theorem and it is what makes the procedure well defined -- the +/// weak-domination analogue is order dependent and so is not a solution +/// concept at all. +/// +/// `a` is the row player's payoff and `b` the column player's. +/// +/// Errors: +/// Returns an error if the two payoff matrices have different shapes. +/// +/// Rust: `optimization::game_theory::iterated_elimination` +#[pyfunction] +#[pyo3(name = "iterated_elimination", signature = (a, b))] +pub fn pyfn_iterated_elimination<'py>(py: Python<'py>, a: crate::generated::types::PyMatrixArg, b: crate::generated::types::PyMatrixArg) -> PyResult<(Vec, Vec)> { + let a = a.0; + let b = b.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::game_theory::iterated_elimination(&a, &b))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// The pure best responses to an opponent's mixed strategy. +/// +/// Returns every index attaining the maximum, not just one. The set matters: +/// a mixed equilibrium exists precisely because a player is indifferent among +/// several best responses, so an implementation that returned a single index +/// would be unable to express one. +/// +/// `payoff` is the responding player's own payoff matrix, with the responder +/// indexing rows. +/// +/// Panics: +/// Panics if the opponent's strategy has the wrong length. +/// +/// Rust: `optimization::game_theory::best_response` +#[pyfunction] +#[pyo3(name = "best_response", signature = (payoff, opponent_mixed))] +pub fn pyfn_best_response<'py>(py: Python<'py>, payoff: crate::generated::types::PyMatrixArg, opponent_mixed: Vec) -> PyResult> { + let payoff = payoff.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::game_theory::best_response(&payoff, &opponent_mixed))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The largest gain any player could get by deviating unilaterally from the +/// given strategy profile. +/// +/// Zero -- to tolerance -- is exactly the definition of a Nash equilibrium, +/// so this is the certificate that any equilibrium-finding routine should be +/// held to. A deviation only ever needs to be checked against *pure* +/// strategies, since the payoff is linear in one's own mixture and a linear +/// function on a simplex attains its maximum at a vertex. +/// +/// Errors: +/// Returns an error on a shape mismatch between the payoffs and the profile. +/// +/// Rust: `optimization::game_theory::nash_deviation_gain` +#[pyfunction] +#[pyo3(name = "nash_deviation_gain", signature = (a, b, p, q))] +pub fn pyfn_nash_deviation_gain<'py>(py: Python<'py>, a: crate::generated::types::PyMatrixArg, b: crate::generated::types::PyMatrixArg, p: Vec, q: Vec) -> PyResult { + let a = a.0; + let b = b.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::game_theory::nash_deviation_gain(&a, &b, &p, &q))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Every Nash equilibrium of a 2x2 bimatrix game, pure and mixed. +/// +/// Small enough to enumerate completely, which makes it the reference the +/// general algorithms are checked against. The mixed equilibrium, when it +/// exists, has the property that trips people up: each player's mixture is +/// chosen to make the *opponent* indifferent, not themselves. One's own +/// payoff plays no part in one's own probabilities. +/// +/// Errors: +/// Returns an error unless both matrices are 2x2. +/// +/// Rust: `optimization::game_theory::nash_2x2` +#[pyfunction] +#[pyo3(name = "nash_2x2", signature = (a, b))] +pub fn pyfn_nash_2x2<'py>(py: Python<'py>, a: crate::generated::types::PyMatrixArg, b: crate::generated::types::PyMatrixArg) -> PyResult, Vec)>> { + let a = a.0; + let b = b.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::game_theory::nash_2x2(&a, &b))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Nash equilibria by support enumeration. +/// +/// For each pair of candidate supports, the indifference conditions are a +/// linear system: every strategy in a player's support must earn the same +/// expected payoff, and the probabilities must sum to one. Solving it and +/// then *checking* the result -- non-negative probabilities, and no +/// unsupported strategy earning more -- is what makes the method sound. The +/// checking is not optional bookkeeping: most supports produce a solution to +/// the linear system that is not an equilibrium at all. +/// +/// Exponential in the number of strategies, so `max_support` bounds the +/// support size considered. +/// +/// Errors: +/// Returns an error on a shape mismatch. +/// +/// Rust: `optimization::game_theory::nash_support_enumeration` +#[pyfunction] +#[pyo3(name = "nash_support_enumeration", signature = (a, b, max_support))] +pub fn pyfn_nash_support_enumeration<'py>(py: Python<'py>, a: crate::generated::types::PyMatrixArg, b: crate::generated::types::PyMatrixArg, max_support: usize) -> PyResult, Vec)>> { + let a = a.0; + let b = b.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::game_theory::nash_support_enumeration(&a, &b, max_support))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// One Nash equilibrium of a bimatrix game by the Lemke-Howson algorithm. +/// +/// Complementary pivoting on the two players' best-response polytopes. Every +/// vertex pair is labelled by the strategies that are either unplayed or +/// unprofitable; a pair carrying all labels is an equilibrium, and the +/// algorithm walks an edge path from the artificial equilibrium at the origin +/// to one that does. The path cannot revisit a vertex and the polytopes are +/// finite, so it terminates -- which is a constructive proof that a Nash +/// equilibrium exists, independent of Kakutani's fixed-point theorem. +/// +/// `initial_label` selects which strategy's label is dropped to start the +/// path; different choices generally reach different equilibria. +/// +/// Errors: +/// Returns an error on a shape mismatch, an out-of-range label, or a +/// degenerate game where the pivot becomes ambiguous. +/// +/// Rust: `optimization::game_theory::nash_bimatrix_lemke_howson` +#[pyfunction] +#[pyo3(name = "nash_bimatrix_lemke_howson", signature = (a, b, initial_label))] +pub fn pyfn_nash_bimatrix_lemke_howson<'py>(py: Python<'py>, a: crate::generated::types::PyMatrixArg, b: crate::generated::types::PyMatrixArg, initial_label: usize) -> PyResult<(Vec, Vec)> { + let a = a.0; + let b = b.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::game_theory::nash_bimatrix_lemke_howson(&a, &b, initial_label))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// A correlated equilibrium of maximum expected total payoff, as a joint +/// distribution over strategy profiles. +/// +/// The reason this is an LP and Nash equilibrium is not: the unknown is the +/// joint distribution itself rather than each player's marginal, so the +/// incentive constraints -- obeying the recommendation beats any deviation, +/// *conditional* on having received it -- are linear. Every Nash equilibrium +/// is a correlated equilibrium (take the product of the marginals), so the +/// set is never empty, and it is generally larger: correlation can achieve +/// payoffs outside the convex hull of the Nash outcomes. +/// +/// Errors: +/// Returns an error on a shape mismatch or if the program has no optimum. +/// +/// Rust: `optimization::game_theory::correlated_equilibrium_lp` +#[pyfunction] +#[pyo3(name = "correlated_equilibrium_lp", signature = (a, b))] +pub fn pyfn_correlated_equilibrium_lp(a: crate::generated::types::PyMatrixArg, b: crate::generated::types::PyMatrixArg) -> PyResult { + let a = a.0; + let b = b.0; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::game_theory::correlated_equilibrium_lp(&a, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) +} + +/// Fictitious play: each player best-responds to the empirical frequency of +/// the other's past moves. +/// +/// Returns the two empirical frequency vectors. It converges to equilibrium +/// in zero-sum games, in 2xN games, and in games solvable by iterated strict +/// dominance -- and famously does *not* converge in general, Shapley's 3x3 +/// example cycling forever. So this is a model of learning that sometimes +/// finds equilibrium, not an algorithm for computing one. +/// +/// Errors: +/// Returns an error on a shape mismatch. +/// +/// Rust: `optimization::game_theory::fictitious_play` +#[pyfunction] +#[pyo3(name = "fictitious_play", signature = (a, b, iterations))] +pub fn pyfn_fictitious_play<'py>(py: Python<'py>, a: crate::generated::types::PyMatrixArg, b: crate::generated::types::PyMatrixArg, iterations: usize) -> PyResult<(Vec, Vec)> { + let a = a.0; + let b = b.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::game_theory::fictitious_play(&a, &b, iterations))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// The replicator dynamic for a symmetric game, returning the trajectory. +/// +/// `dx_i/dt = x_i (e_i . A x - x . A x)`: a strategy grows when it does +/// better than the population average. The equation arises from asexual +/// reproduction proportional to payoff, and its fixed points include every +/// symmetric Nash equilibrium -- but not only those, since every vertex of +/// the simplex is a fixed point whether or not it is an equilibrium. The +/// simplex is invariant, which is what makes the dynamic well posed. +/// +/// Errors: +/// Returns an error unless the payoff is square, the initial population is a +/// distribution over its strategies, and the step is positive. +/// +/// Rust: `optimization::game_theory::replicator_dynamics` +#[pyfunction] +#[pyo3(name = "replicator_dynamics", signature = (payoff, x0, t_end, dt))] +pub fn pyfn_replicator_dynamics<'py>(py: Python<'py>, payoff: crate::generated::types::PyMatrixArg, x0: Vec, t_end: f64, dt: f64) -> PyResult>> { + let payoff = payoff.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::game_theory::replicator_dynamics(&payoff, &x0, t_end, dt))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Whether a strategy is evolutionarily stable in a symmetric game. +/// +/// Maynard Smith's two conditions: the strategy is a symmetric Nash +/// equilibrium, and against any alternative best response it does strictly +/// better than that alternative does against itself. The second condition is +/// what "stable" adds to "equilibrium" -- it says a small invading mutant +/// earns less than the resident and so dies out, which a mere Nash +/// equilibrium does not guarantee. +/// +/// Checked against pure alternatives, which suffices: the payoff is linear in +/// the mutant's mixture, so if no pure mutant invades then none does. +/// +/// Errors: +/// Returns an error unless the payoff is square and the strategy is a +/// distribution over its rows. +/// +/// Rust: `optimization::game_theory::evolutionarily_stable_check` +#[pyfunction] +#[pyo3(name = "evolutionarily_stable_check", signature = (payoff, strategy, tol))] +pub fn pyfn_evolutionarily_stable_check<'py>(py: Python<'py>, payoff: crate::generated::types::PyMatrixArg, strategy: Vec, tol: f64) -> PyResult { + let payoff = payoff.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::game_theory::evolutionarily_stable_check(&payoff, &strategy, tol))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The hawk-dove game: contesting a resource worth `v` at an injury cost `c`. +/// +/// Returns the symmetric payoff matrix with hawk first. When `c > v` the +/// game has a mixed ESS playing hawk with probability `v / c`, which is the +/// canonical demonstration that a population can be stable while every +/// individual in it is randomising. +/// +/// Panics: +/// Panics unless the cost is positive. +/// +/// Rust: `optimization::game_theory::hawk_dove` +#[pyfunction] +#[pyo3(name = "hawk_dove", signature = (v, c))] +pub fn pyfn_hawk_dove(v: f64, c: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::game_theory::hawk_dove(v, c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) +} + +/// The prisoner's dilemma with the conventional temptation, reward, +/// punishment and sucker payoffs. Cooperate is strategy zero. +/// +/// Panics: +/// Panics unless `t > r > p > s`, which is what makes it a dilemma at all -- +/// defection strictly dominates while mutual cooperation beats mutual +/// defection. +/// +/// Rust: `optimization::game_theory::prisoners_dilemma` +#[pyfunction] +#[pyo3(name = "prisoners_dilemma", signature = (t, r, p, s))] +pub fn pyfn_prisoners_dilemma(t: f64, r: f64, p: f64, s: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::game_theory::prisoners_dilemma(t, r, p, s)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) +} + +/// The stag hunt: two pure equilibria, one payoff dominant and one risk +/// dominant. Hunting stag is strategy zero. +/// +/// Rust: `optimization::game_theory::stag_hunt` +#[pyfunction] +#[pyo3(name = "stag_hunt", signature = ())] +pub fn pyfn_stag_hunt() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::game_theory::stag_hunt()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) +} + +/// Chicken, also called hawk-dove in its ordinal form: two asymmetric pure +/// equilibria and one mixed. Swerving is strategy zero. +/// +/// Rust: `optimization::game_theory::chicken` +#[pyfunction] +#[pyo3(name = "chicken", signature = ())] +pub fn pyfn_chicken() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::game_theory::chicken()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) +} + +/// Matching pennies: the smallest zero-sum game with no pure equilibrium. +/// +/// Rust: `optimization::game_theory::matching_pennies` +#[pyfunction] +#[pyo3(name = "matching_pennies", signature = ())] +pub fn pyfn_matching_pennies() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::game_theory::matching_pennies()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) +} + +/// Rock-paper-scissors as a zero-sum payoff matrix, in that order. +/// +/// Rust: `optimization::game_theory::rock_paper_scissors` +#[pyfunction] +#[pyo3(name = "rock_paper_scissors", signature = ())] +pub fn pyfn_rock_paper_scissors() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::game_theory::rock_paper_scissors()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) +} + +/// The Shapley value of a cooperative game given by its characteristic +/// function on coalitions encoded as bitmasks. +/// +/// Player `i`'s value is the average over all orderings of the players of +/// what `i` adds to the coalition already formed. The averaging is what makes +/// it fair in a precise sense: it is the *unique* allocation satisfying +/// efficiency, symmetry, the null-player property and additivity, so any +/// objection to the Shapley value has to be an objection to one of those. +/// +/// Exact, and so exponential: `2^n` coalitions. +/// +/// Errors: +/// Returns an error unless `1 <= n <= 20`, beyond which the enumeration is +/// not worth attempting. +/// +/// Rust: `optimization::game_theory::shapley_value` +#[pyfunction] +#[pyo3(name = "shapley_value", signature = (v, n))] +pub fn pyfn_shapley_value(v: pyo3::Py, n: usize) -> PyResult> { + let __cb_v = std::rc::Rc::new(crate::runtime::Callback::new(v)); + let v = { let __cb = __cb_v.clone(); move |__a0: u64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::game_theory::shapley_value(&v, n)); + crate::runtime::callback::check(&[&__cb_v], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The Shapley value estimated by sampling random orderings. +/// +/// The same average as `shapley_value`, taken over sampled permutations +/// instead of all of them. Unbiased, with error falling as the reciprocal +/// square root of the sample count, which is what makes it the only option +/// once the player count passes about twenty. +/// +/// Errors: +/// Returns an error if there are no players or no samples. +/// +/// Rust: `optimization::game_theory::shapley_monte_carlo` +#[pyfunction] +#[pyo3(name = "shapley_monte_carlo", signature = (v, n, samples, rng))] +pub fn pyfn_shapley_monte_carlo(v: pyo3::Py, n: usize, samples: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let __cb_v = std::rc::Rc::new(crate::runtime::Callback::new(v)); + let v = { let __cb = __cb_v.clone(); move |__a0: u64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::game_theory::shapley_monte_carlo(&v, n, samples, &mut rng.inner)); + crate::runtime::callback::check(&[&__cb_v], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The normalised Banzhaf index: each player's share of the swings they can +/// make. +/// +/// Differs from the Shapley value in what it averages over -- coalitions +/// rather than orderings -- and so weights the sizes differently. The two +/// disagree, and the disagreement is the point: there is no single correct +/// measure of power, only different axiomatisations of it. +/// +/// A game in which no player ever swings anything has no power to apportion, +/// and the shares come back as zeros rather than as a division by zero. +/// +/// Errors: +/// Returns an error unless `1 <= n <= 20`. +/// +/// Rust: `optimization::game_theory::banzhaf_index` +#[pyfunction] +#[pyo3(name = "banzhaf_index", signature = (v, n))] +pub fn pyfn_banzhaf_index(v: pyo3::Py, n: usize) -> PyResult> { + let __cb_v = std::rc::Rc::new(crate::runtime::Callback::new(v)); + let v = { let __cb = __cb_v.clone(); move |__a0: u64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::game_theory::banzhaf_index(&v, n)); + crate::runtime::callback::check(&[&__cb_v], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Whether an allocation lies in the core: efficient, and unimprovable by any +/// coalition. +/// +/// The core can be empty -- three players splitting a pound where any two can +/// take it all has no core allocation at all -- which is exactly why the +/// Shapley value, which always exists, is worth having as well. +/// +/// Errors: +/// Returns an error on a bad player count or allocation length. +/// +/// Rust: `optimization::game_theory::core_check_small` +#[pyfunction] +#[pyo3(name = "core_check_small", signature = (v, n, allocation))] +pub fn pyfn_core_check_small(v: pyo3::Py, n: usize, allocation: Vec) -> PyResult { + let __cb_v = std::rc::Rc::new(crate::runtime::Callback::new(v)); + let v = { let __cb = __cb_v.clone(); move |__a0: u64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::game_theory::core_check_small(&v, n, &allocation)); + crate::runtime::callback::check(&[&__cb_v], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The nucleolus of a small cooperative game. +/// +/// Lexicographically minimises the vector of coalition excesses -- how much +/// each coalition is short of what it could get on its own -- worst first. +/// Solved as a sequence of linear programs: maximise the smallest slack, fix +/// whichever coalitions are then tight, repeat on the rest. Unlike the core +/// it is never empty, and unlike the Shapley value it always lies in the core +/// when the core is non-empty, which is the property that motivates it. +/// +/// Errors: +/// Returns an error for more than about a dozen players, or if a program +/// fails. +/// +/// Rust: `optimization::game_theory::nucleolus_small` +#[pyfunction] +#[pyo3(name = "nucleolus_small", signature = (v, n))] +pub fn pyfn_nucleolus_small(v: pyo3::Py, n: usize) -> PyResult> { + let __cb_v = std::rc::Rc::new(crate::runtime::Callback::new(v)); + let v = { let __cb = __cb_v.clone(); move |__a0: u64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::game_theory::nucleolus_small(&v, n)); + crate::runtime::callback::check(&[&__cb_v], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The Banzhaf power of each voter in a weighted voting game. +/// +/// The point of the exercise is that power is not proportional to weight. A +/// voter with a large weight can have the same power as a small one -- and a +/// voter with positive weight can be a dummy with no power at all, if no +/// coalition ever needs them. +/// +/// Errors: +/// Returns an error for an empty or oversized electorate. +/// +/// Rust: `optimization::game_theory::voting_power_weighted` +#[pyfunction] +#[pyo3(name = "voting_power_weighted", signature = (weights, quota))] +pub fn pyfn_voting_power_weighted<'py>(py: Python<'py>, weights: Vec, quota: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::game_theory::voting_power_weighted(&weights, quota))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The symmetric equilibrium bid shading factor in a first-price sealed-bid +/// auction with `n` bidders whose values are uniform on `[0, 1]`. +/// +/// The equilibrium bid is `(n - 1) / n` times one's value. Shading is not a +/// mistake: bidding one's value in a first-price auction guarantees zero +/// surplus whether one wins or not. As the field grows the shading vanishes, +/// which is the mechanism behind revenue equivalence. +/// +/// Panics: +/// Panics unless there are at least two bidders. +/// +/// Rust: `optimization::game_theory::first_price_auction_equilibrium_uniform` +#[pyfunction] +#[pyo3(name = "first_price_auction_equilibrium_uniform", signature = (n))] +pub fn pyfn_first_price_auction_equilibrium_uniform(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::game_theory::first_price_auction_equilibrium_uniform(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Confirms by exhaustive case analysis that truthful bidding weakly +/// dominates in a second-price auction. +/// +/// Returns true when no misreport ever beats the truth, over a grid of +/// values, bids and highest-rival bids. The argument is a two-case one -- +/// bidding above one's value can only win auctions one regrets, bidding below +/// can only lose auctions one wanted -- and neither case depends on beliefs +/// about the rivals, which is what makes the dominance so strong. +/// +/// Rust: `optimization::game_theory::second_price_dominant_check` +#[pyfunction] +#[pyo3(name = "second_price_dominant_check", signature = ())] +pub fn pyfn_second_price_dominant_check() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::game_theory::second_price_dominant_check()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Simulates first- and second-price auctions with uniform values, returning +/// the two average revenues. +/// +/// The revenue equivalence theorem says they coincide: any two mechanisms +/// that allocate to the highest value and give a zero-value bidder zero +/// surplus raise the same expected revenue. The first-price auction collects +/// a shaded bid from the winner, the second-price auction collects the +/// runner-up's full value, and in expectation those are the same number. +/// +/// Errors: +/// Returns an error for fewer than two bidders or no trials. +/// +/// Rust: `optimization::game_theory::revenue_equivalence_sim` +#[pyfunction] +#[pyo3(name = "revenue_equivalence_sim", signature = (n, trials, rng))] +pub fn pyfn_revenue_equivalence_sim(n: usize, trials: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(f64, f64)> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::game_theory::revenue_equivalence_sim(n, trials, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// A VCG auction for distinct items, one per winner. +/// +/// `bids[i][k]` is bidder `i`'s value for item `k`. Returns the item assigned +/// to each bidder, if any, and each bidder's payment. The payment is the +/// externality imposed: the welfare others would have had in one's absence, +/// less the welfare they actually get. That is what makes truthful bidding +/// dominant -- one's own report shifts only the allocation, never the price +/// one pays for it. +/// +/// The welfare-maximising assignment is found by exhaustive search, so this is +/// for small instances. +/// +/// Errors: +/// Returns an error for ragged bids or more than eight bidders or items. +/// +/// Rust: `optimization::game_theory::vcg_auction` +#[pyfunction] +#[pyo3(name = "vcg_auction", signature = (bids, items))] +pub fn pyfn_vcg_auction<'py>(py: Python<'py>, bids: Vec>, items: usize) -> PyResult<(Vec>, Vec)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::game_theory::vcg_auction(&bids, items))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0.into_iter().map(|__x| __x.map(|__x| __x)).collect::>(), __v.1)) +} + +/// Divide and choose over a cake whose value density differs between the two +/// players, returning each one's share of their own total value. +/// +/// `density_a` and `density_b` give the two valuations over `[0, 1]`, sampled +/// on `resolution` intervals. The cutter divides at their own halfway point +/// and the chooser takes the piece they prefer, so the cutter gets exactly a +/// half by their own measure and the chooser at least a half by theirs. That +/// is envy-freeness for two players -- and it does not extend: no analogous +/// finite protocol was known for three until 1960, and for four until 2016. +/// +/// Errors: +/// Returns an error if the resolution is zero or a valuation is not positive. +/// +/// Rust: `optimization::game_theory::cake_cutting_divide_choose` +#[pyfunction] +#[pyo3(name = "cake_cutting_divide_choose", signature = (density_a, density_b, resolution))] +pub fn pyfn_cake_cutting_divide_choose(density_a: pyo3::Py, density_b: pyo3::Py, resolution: usize) -> PyResult<(f64, f64)> { + let __cb_density_a = std::rc::Rc::new(crate::runtime::Callback::new(density_a)); + let density_a = { let __cb = __cb_density_a.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __cb_density_b = std::rc::Rc::new(crate::runtime::Callback::new(density_b)); + let density_b = { let __cb = __cb_density_b.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::game_theory::cake_cutting_divide_choose(&density_a, &density_b, resolution)); + crate::runtime::callback::check(&[&__cb_density_a, &__cb_density_b], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// Confirms that the deferred-acceptance matching is stable and optimal for +/// the proposing side. +/// +/// Gale-Shapley's guarantee is sharper than stability: among *all* stable +/// matchings, every proposer gets their best possible partner and every +/// receiver their worst. So the same algorithm run from the other side gives +/// a different matching, and which side proposes is a distributional +/// decision, not an implementation detail. Both halves are checked here by +/// enumerating the stable matchings directly. +/// +/// Errors: +/// Returns an error unless the preference lists are square, complete, and no +/// larger than seven a side. +/// +/// Rust: `optimization::game_theory::gale_shapley_optimality_check` +#[pyfunction] +#[pyo3(name = "gale_shapley_optimality_check", signature = (prefs_a, prefs_b))] +pub fn pyfn_gale_shapley_optimality_check<'py>(py: Python<'py>, prefs_a: Vec>, prefs_b: Vec>) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::game_theory::gale_shapley_optimality_check(&prefs_a, &prefs_b))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The Stackelberg equilibrium of a 2x2 game where the row player commits +/// first to a *pure* strategy, as +/// `(leader move, follower move, leader payoff, follower payoff)`. +/// +/// Committing to a pure strategy is at least as good as any pure equilibrium +/// -- the leader can commit to what they would have played anyway, and the +/// follower's reply is unchanged -- and it is often strictly better, which is +/// what first-mover advantage means. +/// +/// It is not, however, at least as good as every *mixed* equilibrium. The +/// general theorem is about commitment to mixed strategies; restricted to +/// pure ones a leader can end up below their mixed Nash payoff, since the +/// mixture they would have randomised over is no longer available to them. +/// +/// Errors: +/// Returns an error unless both matrices are 2x2. +/// +/// Rust: `optimization::game_theory::stackelberg_2x2` +#[pyfunction] +#[pyo3(name = "stackelberg_2x2", signature = (a, b))] +pub fn pyfn_stackelberg_2x2<'py>(py: Python<'py>, a: crate::generated::types::PyMatrixArg, b: crate::generated::types::PyMatrixArg) -> PyResult<(usize, usize, f64, f64)> { + let a = a.0; + let b = b.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::game_theory::stackelberg_2x2(&a, &b))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1, __v.2, __v.3)) +} + +/// The Cournot equilibrium quantities for `n` firms with constant marginal +/// costs facing a linear inverse demand `p = intercept - slope * Q`. +/// +/// Each firm's best response is linear in the others' total, and the system +/// solves in closed form. The comparison with Bertrand is the standard +/// lesson: competing in quantities leaves price above marginal cost however +/// many firms there are, while competing in prices drives it to marginal cost +/// with only two. +/// +/// Errors: +/// Returns an error for no firms, a non-positive slope, or a cost above the +/// choke price. +/// +/// Rust: `optimization::game_theory::cournot_equilibrium` +#[pyfunction] +#[pyo3(name = "cournot_equilibrium", signature = (demand_intercept, demand_slope, costs))] +pub fn pyfn_cournot_equilibrium<'py>(py: Python<'py>, demand_intercept: f64, demand_slope: f64, costs: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::game_theory::cournot_equilibrium(demand_intercept, demand_slope, &costs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The Bertrand equilibrium price with identical firms: marginal cost. +/// +/// Two firms suffice. Any price above cost is undercut by a rival who then +/// takes the whole market, so the only equilibrium is the competitive one -- +/// the "Bertrand paradox", since it predicts that a duopoly behaves like +/// perfect competition. With asymmetric costs the low-cost firm prices just +/// under the rival's cost, which is what this returns. +/// +/// Errors: +/// Returns an error for fewer than two firms. +/// +/// Rust: `optimization::game_theory::bertrand_equilibrium` +#[pyfunction] +#[pyo3(name = "bertrand_equilibrium", signature = (costs))] +pub fn pyfn_bertrand_equilibrium<'py>(py: Python<'py>, costs: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::game_theory::bertrand_equilibrium(&costs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// A public goods game with a linear return, returning the average +/// contribution per round. +/// +/// Each of `n` players contributes some fraction of an endowment to a pot +/// that is multiplied by `multiplier` and split evenly. A unit contributed +/// costs its contributor one and returns `multiplier / n` to them, so the +/// threshold is at `multiplier = n`: below it contributing is individually +/// irrational and collectively optimal, which is the free-rider problem in +/// its simplest form, and above it the two coincide. +/// +/// Players here are conditional cooperators, matching what the others gave +/// and adjusting by their own marginal return -- the rule the laboratory +/// evidence supports. Note that imitating the highest *earner* instead would +/// drive contributions to zero at any multiplier whatever, because within a +/// round every player receives the same share and so the smallest contributor +/// always earns most. That comparison is between players, and the incentive +/// that matters is the effect of a player's own contribution on their own +/// earnings; conflating the two is an easy way to build a model that cannot +/// represent the threshold at all. +/// +/// Errors: +/// Returns an error for bad parameters. +/// +/// Rust: `optimization::game_theory::public_goods_game_sim` +#[pyfunction] +#[pyo3(name = "public_goods_game_sim", signature = (n, multiplier, rounds, rng))] +pub fn pyfn_public_goods_game_sim(n: usize, multiplier: f64, rounds: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::game_theory::public_goods_game_sim(n, multiplier, rounds, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// A Colonel Blotto tournament between random allocations, returning the +/// win-rate matrix between the sampled strategies. +/// +/// Troops are split across fields and each field goes to whoever committed +/// more. The game has no pure equilibrium and no dominant allocation: every +/// deterministic plan is beaten by some other, so the equilibrium is +/// necessarily in mixed strategies. The matrix is the empirical payoff of the +/// sampled strategies against one another. +/// +/// Errors: +/// Returns an error for fewer than two fields, no troops, or fewer than two +/// sampled strategies. +/// +/// Rust: `optimization::game_theory::colonel_blotto_sim` +#[pyfunction] +#[pyo3(name = "colonel_blotto_sim", signature = (fields, troops, strategies, rng))] +pub fn pyfn_colonel_blotto_sim(fields: usize, troops: usize, strategies: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::game_theory::colonel_blotto_sim(fields, troops, strategies, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) +} + +/// Backward induction on a game tree, returning the equilibrium path of moves +/// and the payoffs it reaches. +/// +/// Solving from the leaves upward gives a subgame perfect equilibrium, which +/// rules out the equilibria of the normal form that rest on threats the +/// threatener would not want to carry out. That is the whole content of the +/// refinement: a Nash equilibrium can be sustained by a promise to behave +/// irrationally off the path, and backward induction cannot represent one. +/// +/// Errors: +/// Returns an error if a decision node has no children or the payoff vectors +/// disagree in length. +/// +/// Rust: `optimization::game_theory::backward_induction` +#[pyfunction] +#[pyo3(name = "backward_induction", signature = (tree))] +pub fn pyfn_backward_induction(tree: crate::generated::types::PyGameTree) -> PyResult<(Vec, Vec)> { + let tree = tree.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::game_theory::backward_induction(&tree)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_minimax_value, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dominated_strategies, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_iterated_elimination, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_best_response, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_nash_deviation_gain, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_nash_2x2, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_nash_support_enumeration, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_nash_bimatrix_lemke_howson, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_correlated_equilibrium_lp, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fictitious_play, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_replicator_dynamics, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_evolutionarily_stable_check, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hawk_dove, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_prisoners_dilemma, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_stag_hunt, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_chicken, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_matching_pennies, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rock_paper_scissors, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_shapley_value, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_shapley_monte_carlo, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_banzhaf_index, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_core_check_small, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_nucleolus_small, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_voting_power_weighted, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_first_price_auction_equilibrium_uniform, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_second_price_dominant_check, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_revenue_equivalence_sim, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_vcg_auction, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cake_cutting_divide_choose, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gale_shapley_optimality_check, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_stackelberg_2x2, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cournot_equilibrium, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bertrand_equilibrium, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_public_goods_game_sim, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_colonel_blotto_sim, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_backward_induction, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_optimization__integer.rs b/bindings/python/src/generated/m_optimization__integer.rs new file mode 100644 index 0000000..e006563 --- /dev/null +++ b/bindings/python/src/generated/m_optimization__integer.rs @@ -0,0 +1,660 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Solves a mixed-integer linear program by branch and bound. +/// +/// Solves the linear relaxation; if the named variables all came out integral +/// the answer is optimal, and otherwise one fractional variable is chosen and +/// the problem split into the branch where it is rounded down and the branch +/// where it is rounded up. The relaxation's value bounds every integer +/// solution below it, so a branch whose relaxation is already worse than the +/// best integer solution found can be discarded whole -- which is the entire +/// content of the method, and the reason it beats enumeration. +/// +/// `node_limit` caps the search. Returns `None` if the problem is infeasible +/// over the integers, or if the limit is reached before any integer solution +/// is found. +/// +/// Errors: +/// Returns an error if a named variable is out of range, or the underlying +/// linear program is malformed. +/// +/// Rust: `optimization::integer::branch_and_bound` +#[pyfunction] +#[pyo3(name = "branch_and_bound", signature = (p, integer_vars, node_limit))] +pub fn pyfn_branch_and_bound<'py>(py: Python<'py>, p: crate::generated::types::PyLpProblem, integer_vars: Vec, node_limit: usize) -> PyResult, f64)>> { + let p = p.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::integer::branch_and_bound(&p, &integer_vars, node_limit))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.map(|__x| (__x.0, __x.1))) +} + +/// Adds Chvatal-Gomory rounding cuts to a linear program. +/// +/// A cut is only worth the name if it is valid: satisfied by every integer +/// point of the feasible region, while removing part of the fractional +/// relaxation. The rounding cut earns that as follows. Scale a `<=` row by +/// some `lambda > 0`, so `lambda a . x <= lambda b` still holds. Rounding each +/// coefficient down can only lower the left-hand side when `x >= 0`, so +/// `floor(lambda a) . x <= lambda b`. But the left-hand side is now an integer +/// combination of integers, hence an integer, so it is bounded by the floor of +/// the right: +/// +/// +/// Every non-negative integer point survives that, and a fractional one need +/// not. Multipliers are tried at the reciprocals of the row's own +/// coefficients and at a few small fractions, and a cut is kept only when the +/// current relaxation optimum actually violates it. +/// +/// Errors: +/// Returns an error unless every variable is integer and non-negative, which +/// is what the rounding argument requires, or if the relaxation has no +/// optimum. +/// +/// Rust: `optimization::integer::gomory_cuts` +#[pyfunction] +#[pyo3(name = "gomory_cuts", signature = (p, integer_vars, max_cuts))] +pub fn pyfn_gomory_cuts(p: crate::generated::types::PyLpProblem, integer_vars: Vec, max_cuts: usize) -> PyResult { + let p = p.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::integer::gomory_cuts(&p, &integer_vars, max_cuts)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyLpProblem { inner: __v }) +} + +/// The 0/1 knapsack by dynamic programming: each item taken at most once. +/// +/// Returns the best value and which items to take. The table is +/// `O(n * capacity)`, which is polynomial in the *value* of the capacity but +/// exponential in the number of digits it takes to write it down -- the +/// problem is NP-hard, and the table is pseudo-polynomial rather than a +/// contradiction of that. +/// +/// Panics: +/// Panics unless the value and weight lists have the same length. +/// +/// Rust: `optimization::integer::knapsack_01` +#[pyfunction] +#[pyo3(name = "knapsack_01", signature = (values, weights, capacity))] +pub fn pyfn_knapsack_01<'py>(py: Python<'py>, values: Vec, weights: Vec, capacity: u64) -> PyResult<(u64, Vec)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::integer::knapsack_01(&values, &weights, capacity))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// The unbounded knapsack: each item available without limit. +/// +/// Returns the best value and how many of each item to take. A one-dimensional +/// table suffices, because an item may be reused within the same pass. +/// +/// Panics: +/// Panics unless the lists match in length and every weight is positive. +/// +/// Rust: `optimization::integer::knapsack_unbounded` +#[pyfunction] +#[pyo3(name = "knapsack_unbounded", signature = (values, weights, capacity))] +pub fn pyfn_knapsack_unbounded<'py>(py: Python<'py>, values: Vec, weights: Vec, capacity: u64) -> PyResult<(u64, Vec)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::integer::knapsack_unbounded(&values, &weights, capacity))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// The bounded knapsack: each item available up to its own limit. +/// +/// Expanded by binary splitting -- an item with a limit of `k` becomes items +/// of multiplicity `1, 2, 4, ...` summing to `k` -- so any count up to the +/// limit is expressible and the 0/1 solver applies. That costs +/// `O(log k)` copies rather than the `k` a naive expansion would need. +/// +/// Panics: +/// Panics unless all three lists match in length. +/// +/// Rust: `optimization::integer::knapsack_bounded` +#[pyfunction] +#[pyo3(name = "knapsack_bounded", signature = (values, weights, limits, capacity))] +pub fn pyfn_knapsack_bounded<'py>(py: Python<'py>, values: Vec, weights: Vec, limits: Vec, capacity: u64) -> PyResult<(u64, Vec)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::integer::knapsack_bounded(&values, &weights, &limits, capacity))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// The multiple knapsack: several bins, each item into at most one. +/// +/// Solved greedily by value density with a first-fit placement, which is not +/// exact -- the problem is NP-hard even with two bins -- so the result is a +/// lower bound on the optimum. Returns the total value and the bin each item +/// went into, `None` for an item left out. +/// +/// Panics: +/// Panics unless the lists match in length. +/// +/// Rust: `optimization::integer::knapsack_multiple` +#[pyfunction] +#[pyo3(name = "knapsack_multiple", signature = (values, weights, capacities))] +pub fn pyfn_knapsack_multiple<'py>(py: Python<'py>, values: Vec, weights: Vec, capacities: Vec) -> PyResult<(u64, Vec>)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::integer::knapsack_multiple(&values, &weights, &capacities))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1.into_iter().map(|__x| __x.map(|__x| __x)).collect::>())) +} + +/// The 0/1 knapsack by branch and bound over the fractional relaxation. +/// +/// The relaxation of a knapsack is solved by taking items in density order +/// and splitting the last one, which gives a bound in linear time once the +/// items are sorted. Nodes whose bound cannot beat the incumbent are pruned. +/// +/// Exact, and must agree with `knapsack_01` on every instance -- one walks a +/// table and the other a search tree, so their agreement is a real check on +/// both. +/// +/// Panics: +/// Panics unless the lists match in length. +/// +/// Rust: `optimization::integer::knapsack_branch_bound` +#[pyfunction] +#[pyo3(name = "knapsack_branch_bound", signature = (values, weights, capacity))] +pub fn pyfn_knapsack_branch_bound<'py>(py: Python<'py>, values: Vec, weights: Vec, capacity: u64) -> PyResult<(u64, Vec)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::integer::knapsack_branch_bound(&values, &weights, capacity))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Indices of a subset summing exactly to `target`, if one exists. +/// +/// Panics: +/// Panics if the values are large enough that the table would not fit. +/// +/// Rust: `optimization::integer::subset_sum` +#[pyfunction] +#[pyo3(name = "subset_sum", signature = (xs, target))] +pub fn pyfn_subset_sum<'py>(py: Python<'py>, xs: Vec, target: u64) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::integer::subset_sum(&xs, target))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// How many subsets sum exactly to `target`. +/// +/// Counted as a `BigInt`, since the number of subsets of an `n`-element set +/// is `2^n` and the count routinely overflows a machine word well before the +/// table does. +/// +/// Rust: `optimization::integer::subset_sum_count` +#[pyfunction] +#[pyo3(name = "subset_sum_count", signature = (xs, target))] +pub fn pyfn_subset_sum_count<'py>(py: Python<'py>, xs: Vec, target: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::integer::subset_sum_count(&xs, target)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// Splits the values into two groups whose totals are as close as possible. +/// +/// Returns the difference and the membership flags. The problem is +/// NP-hard in general and solved here by the subset-sum table over half the +/// total, which is exact and pseudo-polynomial. +/// +/// Rust: `optimization::integer::partition_min_diff` +#[pyfunction] +#[pyo3(name = "partition_min_diff", signature = (xs))] +pub fn pyfn_partition_min_diff<'py>(py: Python<'py>, xs: Vec) -> PyResult<(u64, Vec)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::integer::partition_min_diff(&xs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Bin packing by first-fit-decreasing: sort the items large to small and put +/// each into the first bin it fits. +/// +/// Returns the item indices in each bin. The rule uses at most +/// `11/9 OPT + 6/9` bins, a bound that is tight -- so the tests check the +/// guarantee against an exact answer rather than checking the result merely +/// looks reasonable. +/// +/// Panics: +/// Panics if any item exceeds the bin capacity, which makes packing +/// impossible rather than merely hard. +/// +/// Rust: `optimization::integer::bin_packing_ffd` +#[pyfunction] +#[pyo3(name = "bin_packing_ffd", signature = (sizes, capacity))] +pub fn pyfn_bin_packing_ffd<'py>(py: Python<'py>, sizes: Vec, capacity: f64) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::integer::bin_packing_ffd(&sizes, capacity))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The fewest bins any packing could use: the total size divided by the +/// capacity, rounded up. +/// +/// A valid lower bound because a bin holds at most `capacity`, so no packing +/// can use fewer. It is not always attainable -- three items of size 0.4 need +/// two bins though their total is 1.2 -- which is exactly why it is a bound +/// and not an answer. +/// +/// Rust: `optimization::integer::bin_packing_lower_bound` +#[pyfunction] +#[pyo3(name = "bin_packing_lower_bound", signature = (sizes, capacity))] +pub fn pyfn_bin_packing_lower_bound<'py>(py: Python<'py>, sizes: Vec, capacity: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::integer::bin_packing_lower_bound(&sizes, capacity))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The exact minimum number of bins, by trying each count in turn. +/// +/// Exponential, and meant for the small instances the tests use to check the +/// first-fit-decreasing guarantee. Returns the packing. +/// +/// Panics: +/// Panics under the same conditions as `bin_packing_ffd`. +/// +/// Rust: `optimization::integer::bin_packing_exact_small` +#[pyfunction] +#[pyo3(name = "bin_packing_exact_small", signature = (sizes, capacity))] +pub fn pyfn_bin_packing_exact_small<'py>(py: Python<'py>, sizes: Vec, capacity: f64) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::integer::bin_packing_exact_small(&sizes, capacity))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Greedy set cover: repeatedly take the set covering the most of what is +/// still uncovered. +/// +/// Returns the indices of the chosen sets, or `None` if the sets do not cover +/// the universe at all. Greedy uses at most `H_n` times the optimal number of +/// sets, where `H_n` is the `n`-th harmonic number, and no polynomial +/// algorithm does asymptotically better unless P equals NP -- so this is not +/// a placeholder for something better. +/// +/// Rust: `optimization::integer::set_cover_greedy` +#[pyfunction] +#[pyo3(name = "set_cover_greedy", signature = (universe_n, sets))] +pub fn pyfn_set_cover_greedy<'py>(py: Python<'py>, universe_n: usize, sets: Vec>) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::integer::set_cover_greedy(universe_n, &sets))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// The exact minimum set cover, by trying every subset of the sets in order of +/// size. +/// +/// For the small instances that make the greedy ratio checkable. +/// +/// Panics: +/// Panics if there are more than 20 sets, where the enumeration stops being +/// reasonable. +/// +/// Rust: `optimization::integer::set_cover_exact_small` +#[pyfunction] +#[pyo3(name = "set_cover_exact_small", signature = (universe_n, sets))] +pub fn pyfn_set_cover_exact_small<'py>(py: Python<'py>, universe_n: usize, sets: Vec>) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::integer::set_cover_exact_small(universe_n, &sets))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// Uncapacitated facility location, solved greedily. +/// +/// `open_costs[i]` is the fixed cost of opening facility `i` and +/// `serve_costs[(i, j)]` the cost of serving client `j` from it. Facilities +/// are opened one at a time, each time the one whose opening cost plus +/// improved service most reduces the total. +/// +/// Returns the total cost and which facilities to open. +/// +/// Panics: +/// Panics unless the shapes agree and there is at least one facility. +/// +/// Rust: `optimization::integer::facility_location_greedy` +#[pyfunction] +#[pyo3(name = "facility_location_greedy", signature = (open_costs, serve_costs))] +pub fn pyfn_facility_location_greedy<'py>(py: Python<'py>, open_costs: Vec, serve_costs: crate::generated::types::PyMatrixArg) -> PyResult<(f64, Vec)> { + let serve_costs = serve_costs.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::integer::facility_location_greedy(&open_costs, &serve_costs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// The cutting stock problem by column generation, relaxed. +/// +/// Each cutting pattern is a column of the linear program, and there are far +/// too many to write down, so patterns are generated on demand: solve the +/// relaxation over the patterns in hand, read the dual prices off it, and ask +/// which single new pattern would be most profitable at those prices. That +/// subproblem is an unbounded knapsack, and when its best pattern is not +/// profitable the relaxation is optimal over *all* patterns without ever +/// having enumerated them. +/// +/// Returns the relaxed number of stock lengths needed, which lower-bounds the +/// integer answer. +/// +/// Errors: +/// Returns an error if the inputs disagree in length, or a piece is longer +/// than the stock. +/// +/// Rust: `optimization::integer::cutting_stock_column_generation` +#[pyfunction] +#[pyo3(name = "cutting_stock_column_generation", signature = (demand, lengths, stock_length, max_rounds))] +pub fn pyfn_cutting_stock_column_generation<'py>(py: Python<'py>, demand: Vec, lengths: Vec, stock_length: u64, max_rounds: usize) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::integer::cutting_stock_column_generation(&demand, &lengths, stock_length, max_rounds))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The fewest coins summing to `amount`, or `None` if no combination does. +/// +/// Returns how many of each denomination. Greedy is wrong for general +/// denominations -- with coins 1, 3, 4 and an amount of 6, greedy takes +/// 4 + 1 + 1 while two threes do it -- so this is a table, not a loop. +/// +/// Rust: `optimization::integer::coin_change_min` +#[pyfunction] +#[pyo3(name = "coin_change_min", signature = (coins, amount))] +pub fn pyfn_coin_change_min<'py>(py: Python<'py>, coins: Vec, amount: u64) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::integer::coin_change_min(&coins, amount))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// How many combinations of coins sum to `amount`, order disregarded. +/// +/// Iterating coins in the outer loop is what makes this count combinations +/// rather than permutations: each coin is considered once for the whole table, +/// so `1 + 2` and `2 + 1` are never both counted. +/// +/// Rust: `optimization::integer::coin_change_count` +#[pyfunction] +#[pyo3(name = "coin_change_count", signature = (coins, amount))] +pub fn pyfn_coin_change_count<'py>(py: Python<'py>, coins: Vec, amount: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::integer::coin_change_count(&coins, amount)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::bigint_out(py, &__v)?) +} + +/// Indices of a longest strictly increasing subsequence, in `O(n log n)`. +/// +/// The trick is to keep, for each length, the smallest value that can end a +/// subsequence of that length. That list is sorted by construction, so the +/// position each new element belongs at is a binary search rather than a scan +/// -- which is what turns the quadratic table into an `n log n` sweep. +/// +/// Rust: `optimization::integer::longest_increasing_subsequence` +#[pyfunction] +#[pyo3(name = "longest_increasing_subsequence", signature = (x))] +pub fn pyfn_longest_increasing_subsequence<'py>(py: Python<'py>, x: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::integer::longest_increasing_subsequence(&x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Levenshtein distance: the fewest single-symbol insertions, deletions +/// and substitutions turning `a` into `b`. +/// +/// It is a metric on sequences -- symmetric, zero only between equal +/// sequences, and obeying the triangle inequality -- which is what makes it +/// usable for clustering and nearest-neighbour search rather than merely a +/// similarity score. +/// +/// Rust: `optimization::integer::edit_distance` +#[pyfunction] +#[pyo3(name = "edit_distance", signature = (a, b))] +pub fn pyfn_edit_distance<'py>(py: Python<'py>, a: Vec, b: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::integer::edit_distance(&a, &b))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The edits themselves, in order, from a full table. +/// +/// Applying them to `a` reproduces `b`, and their count of non-`Keep` +/// operations is exactly `edit_distance`. +/// +/// Rust: `optimization::integer::edit_distance_ops` +#[pyfunction] +#[pyo3(name = "edit_distance_ops", signature = (a, b))] +pub fn pyfn_edit_distance_ops(a: Vec, b: Vec) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::integer::edit_distance_ops(&a, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyEditOp { inner: __x }).collect::>()) +} + +/// A longest common subsequence of two sequences. +/// +/// Rust: `optimization::integer::longest_common_subsequence` +#[pyfunction] +#[pyo3(name = "longest_common_subsequence", signature = (a, b))] +pub fn pyfn_longest_common_subsequence<'py>(py: Python<'py>, a: Vec, b: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::integer::longest_common_subsequence(&a, &b))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The cheapest way to parenthesise a chain of matrix multiplications. +/// +/// `dims` holds the shared dimensions: matrix `k` is `dims[k]` by +/// `dims[k + 1]`. Returns the scalar multiplication count and the +/// parenthesisation as a string. +/// +/// The order matters enormously -- multiplying a `1x100`, `100x1` and `1x100` +/// chain costs 200 one way and 20,000 the other -- and the number of +/// parenthesisations is Catalan, so the table is what makes it tractable. +/// +/// Panics: +/// Panics unless there are at least two dimensions. +/// +/// Rust: `optimization::integer::matrix_chain_order` +#[pyfunction] +#[pyo3(name = "matrix_chain_order", signature = (dims))] +pub fn pyfn_matrix_chain_order<'py>(py: Python<'py>, dims: Vec) -> PyResult<(u64, String)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::integer::matrix_chain_order(&dims))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1.to_string())) +} + +/// The most valuable way to cut a rod of length `n` into pieces. +/// +/// `prices[k]` is what a piece of length `k + 1` sells for. Returns the value +/// and the piece lengths. +/// +/// Rust: `optimization::integer::rod_cutting` +#[pyfunction] +#[pyo3(name = "rod_cutting", signature = (prices, n))] +pub fn pyfn_rod_cutting<'py>(py: Python<'py>, prices: Vec, n: usize) -> PyResult<(u64, Vec)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::integer::rod_cutting(&prices, n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// The fewest drops that always determine the critical floor, with `eggs` +/// eggs and `floors` floors. +/// +/// The classic answer for two eggs and a hundred floors is fourteen: drop +/// from 14, then 27, then 39, and so on, each interval one shorter than the +/// last so the worst case stays flat. +/// +/// Rust: `optimization::integer::egg_drop` +#[pyfunction] +#[pyo3(name = "egg_drop", signature = (eggs, floors))] +pub fn pyfn_egg_drop(eggs: usize, floors: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::integer::egg_drop(eggs, floors)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The expected search cost of the optimal binary search tree over keys with +/// the given access frequencies. +/// +/// Frequencies are taken in key order. The optimum is not the balanced tree: +/// a key accessed far more often than the rest belongs near the root even if +/// that unbalances everything else. +/// +/// Rust: `optimization::integer::optimal_bst` +#[pyfunction] +#[pyo3(name = "optimal_bst", signature = (frequencies))] +pub fn pyfn_optimal_bst<'py>(py: Python<'py>, frequencies: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::integer::optimal_bst(&frequencies))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The least-cost state path through a trellis. +/// +/// `transition[(a, b)]` is the cost of moving from state `a` to state `b`, and +/// `emission[(s, t)]` the cost of state `s` at time `t`. Returns the best path. +/// +/// The same recursion as the probabilistic Viterbi algorithm in +/// `stochastic::hmm`, stated in costs rather than log-probabilities -- which +/// is the more general form, since any additive path cost works. +/// +/// Errors: +/// Returns an error if the matrices disagree in shape or there are no steps. +/// +/// Rust: `optimization::integer::viterbi_generic` +#[pyfunction] +#[pyo3(name = "viterbi_generic", signature = (transition, emission))] +pub fn pyfn_viterbi_generic<'py>(py: Python<'py>, transition: crate::generated::types::PyMatrixArg, emission: crate::generated::types::PyMatrixArg) -> PyResult> { + let transition = transition.0; + let emission = emission.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::integer::viterbi_generic(&transition, &emission))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Solves an exact cover problem: choose rows so that every column is covered +/// exactly once. +/// +/// Implemented as Knuth's Algorithm X with the column-selection heuristic that +/// makes dancing links effective -- always branch on the column with the +/// fewest remaining options, which fails fast and keeps the search tree +/// narrow. The doubly linked list of the classic implementation is replaced +/// here by bitmask bookkeeping, which is the same algorithm with the same +/// search order for the column counts this module needs. +/// +/// Returns the chosen row indices, or `None` if no exact cover exists. +/// +/// Errors: +/// Returns an error for a ragged matrix or more than 64 columns. +/// +/// Rust: `optimization::integer::exact_cover_dlx` +#[pyfunction] +#[pyo3(name = "exact_cover_dlx", signature = (matrix))] +pub fn pyfn_exact_cover_dlx<'py>(py: Python<'py>, matrix: Vec>) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::integer::exact_cover_dlx(&matrix))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.map(|__x| __x)) +} + +/// Every placement of `n` non-attacking queens, each as the column of the +/// queen in each row. +/// +/// Panics: +/// Panics if `n` exceeds 12, where the count runs into the hundreds of +/// thousands and the list stops being a sensible return value. +/// +/// Rust: `optimization::integer::n_queens` +#[pyfunction] +#[pyo3(name = "n_queens", signature = (n))] +pub fn pyfn_n_queens<'py>(py: Python<'py>, n: usize) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::integer::n_queens(n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// How many placements of `n` non-attacking queens exist. +/// +/// The sequence begins 1, 0, 0, 2, 10, 4, 40, 92 for boards one to eight wide +/// -- there is no solution on a three-square board, and a six-square board has +/// fewer than a five-square one, which is the usual surprise. +/// +/// Panics: +/// Panics if `n` exceeds 14. +/// +/// Rust: `optimization::integer::n_queens_count` +#[pyfunction] +#[pyo3(name = "n_queens_count", signature = (n))] +pub fn pyfn_n_queens_count(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::integer::n_queens_count(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Arc consistency by AC-3: prunes values that cannot participate in any +/// solution. +/// +/// `domains[i]` is a bitmask of the values variable `i` may take, and +/// `constraints` lists pairs `(i, j)` that must differ. Repeatedly removes any +/// value in one domain with no support in a neighbour's, until nothing +/// changes. +/// +/// Returns the reduced domains, or `None` if some domain empties -- which +/// proves the constraints unsatisfiable without any search. AC-3 never removes +/// a value that appears in a solution, so the reduced domains are a sound +/// simplification rather than a heuristic. +/// +/// Rust: `optimization::integer::constraint_propagation_ac3` +#[pyfunction] +#[pyo3(name = "constraint_propagation_ac3", signature = (domains, constraints))] +pub fn pyfn_constraint_propagation_ac3<'py>(py: Python<'py>, domains: Vec, constraints: Vec<(usize, usize)>) -> PyResult>> { + let constraints = constraints.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::integer::constraint_propagation_ac3(&domains, &constraints))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_branch_and_bound, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gomory_cuts, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_knapsack_01, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_knapsack_unbounded, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_knapsack_bounded, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_knapsack_multiple, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_knapsack_branch_bound, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_subset_sum, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_subset_sum_count, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_partition_min_diff, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bin_packing_ffd, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bin_packing_lower_bound, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bin_packing_exact_small, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_set_cover_greedy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_set_cover_exact_small, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_facility_location_greedy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cutting_stock_column_generation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_coin_change_min, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_coin_change_count, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_longest_increasing_subsequence, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_edit_distance, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_edit_distance_ops, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_longest_common_subsequence, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_matrix_chain_order, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rod_cutting, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_egg_drop, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_optimal_bst, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_viterbi_generic, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_exact_cover_dlx, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_n_queens, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_n_queens_count, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_constraint_propagation_ac3, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_optimization__least_squares.rs b/bindings/python/src/generated/m_optimization__least_squares.rs new file mode 100644 index 0000000..43e3831 --- /dev/null +++ b/bindings/python/src/generated/m_optimization__least_squares.rs @@ -0,0 +1,54 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Fits y ≈ A·e^(−k·t), returning (A, k). Initial guess from a +/// log-linear regression over the positive samples, refined by LM. +/// +/// Fails with `InvalidArgument` unless there are ≥ 2 samples with +/// matching lengths and at least two positive y values. +/// +/// Rust: `optimization::least_squares::fit_exponential_decay` +#[pyfunction] +#[pyo3(name = "fit_exponential_decay", signature = (t, y))] +pub fn pyfn_fit_exponential_decay<'py>(py: Python<'py>, t: Vec, y: Vec) -> PyResult<(f64, f64)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::least_squares::fit_exponential_decay(&t, &y))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok((__v.0, __v.1)) +} + +/// Fits y ≈ A·exp(−(x−μ)²/(2σ²)), returning (A, μ, σ). Initial guess +/// from the sample peak and moment-based width, refined by LM. +/// +/// Rust: `optimization::least_squares::fit_gaussian_peak` +#[pyfunction] +#[pyo3(name = "fit_gaussian_peak", signature = (x, y))] +pub fn pyfn_fit_gaussian_peak<'py>(py: Python<'py>, x: Vec, y: Vec) -> PyResult<(f64, f64, f64)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::least_squares::fit_gaussian_peak(&x, &y))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_fit_exponential_decay, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fit_gaussian_peak, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_optimization__lp.rs b/bindings/python/src/generated/m_optimization__lp.rs new file mode 100644 index 0000000..6b5487d --- /dev/null +++ b/bindings/python/src/generated/m_optimization__lp.rs @@ -0,0 +1,377 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Solves a linear program by the two-phase simplex method. +/// +/// Phase one minimises the total artificial infeasibility from an +/// artificial-variable basis; a positive optimum there proves the problem +/// infeasible, since that value is the least total violation achievable. +/// Phase two then optimises the real objective from the feasible basis phase +/// one produced. +/// +/// Bland's rule is used throughout, so the method terminates on any problem, +/// including degenerate ones where a faster pivoting rule would cycle. +/// +/// Errors: +/// Returns an error if the problem's parts disagree in shape. +/// +/// Rust: `optimization::lp::simplex` +#[pyfunction] +#[pyo3(name = "simplex", signature = (p))] +pub fn pyfn_simplex(p: crate::generated::types::PyLpProblem) -> PyResult { + let p = p.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::lp::simplex(&p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyLpResult { inner: __v }) +} + +/// The dual linear program. +/// +/// For a minimisation `min c'x` subject to rows compared against `b` with +/// `x >= 0`, the dual is `max b'y` subject to `A'y <= c`, with each `y_i` +/// signed by the sense of its row: non-positive for a `<=` row, non-negative +/// for a `>=` row, free for an equality. Maximisation mirrors it. +/// +/// Solving the dual gives the same optimal value as the primal and its +/// solution is the primal's vector of shadow prices, which is the practical +/// content of duality: the answer to "what is this constraint costing me" is +/// a solution to a different linear program of the same size. +/// +/// Errors: +/// Returns an error unless every primal variable carries the default bounds +/// `(0, inf)`. A bounded variable contributes an extra dual row, which would +/// change the problem's shape rather than transpose it. +/// +/// Rust: `optimization::lp::lp_dual` +#[pyfunction] +#[pyo3(name = "lp_dual", signature = (p))] +pub fn pyfn_lp_dual(p: crate::generated::types::PyLpProblem) -> PyResult { + let p = p.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::lp::lp_dual(&p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyLpProblem { inner: __v }) +} + +/// Ranges over which the optimal basis survives, as +/// `(objective coefficient ranges, right-hand side ranges)`. +/// +/// Inside a right-hand side's range the shadow price is constant, so the +/// objective moves by exactly `duals[i]` per unit of `b[i]`. That linearity +/// is the point of the exercise and is what the tests check; outside the +/// range the basis changes and the rate does too. +/// +/// Inside an objective coefficient's range the optimal *point* does not move +/// at all, only the value. +/// +/// Errors: +/// Returns an error if the problem is not solved to an optimum, or if any +/// variable carries non-default bounds -- a finite upper bound becomes an +/// extra row during standardisation, and the ranges would then be reported +/// against rows the caller never wrote. +/// +/// Rust: `optimization::lp::sensitivity_ranges` +#[pyfunction] +#[pyo3(name = "sensitivity_ranges", signature = (p))] +pub fn pyfn_sensitivity_ranges(p: crate::generated::types::PyLpProblem) -> PyResult<(Vec<(f64, f64)>, Vec<(f64, f64)>)> { + let p = p.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::lp::sensitivity_ranges(&p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0.into_iter().map(|__x| (__x.0, __x.1)).collect::>(), __v.1.into_iter().map(|__x| (__x.0, __x.1)).collect::>())) +} + +/// The dual simplex method, started from a given basis. +/// +/// Where the primal simplex keeps every basic variable non-negative and works +/// toward optimality, the dual simplex keeps the reduced costs optimal and +/// works toward feasibility. That is the right way round after a right-hand +/// side changes -- the old basis stays dual-feasible while becoming primal +/// infeasible, so re-solving costs a few pivots instead of a fresh start. +/// +/// `basis` names one standard-form column per constraint row. Column indices +/// run over the structural variables first, then the slack and surplus +/// columns in row order. +/// +/// Errors: +/// Returns an error if the basis has the wrong length, names a column out of +/// range, or is singular. A basis that is not dual-feasible is reported as +/// `GeomError::Degenerate` rather than silently repaired. +/// +/// Rust: `optimization::lp::dual_simplex` +#[pyfunction] +#[pyo3(name = "dual_simplex", signature = (p, basis))] +pub fn pyfn_dual_simplex(p: crate::generated::types::PyLpProblem, basis: Vec) -> PyResult { + let p = p.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::lp::dual_simplex(&p, &basis)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyLpResult { inner: __v }) +} + +/// Solves a linear program by a primal-dual path-following interior point +/// method. +/// +/// The method keeps `x > 0` and `s > 0` strictly, and drives the duality +/// measure `x's/n` toward zero along the central path. Each iteration solves +/// one Newton system, reduced to the normal equations `A D A' dy = r` with +/// `D = diag(x_i / s_i)` and factored by Cholesky. Unlike the simplex method +/// it never lands exactly on a vertex, and unlike the simplex method its +/// iteration count barely grows with the size of the problem. +/// +/// The starting point is deliberately infeasible -- all ones -- and the primal +/// and dual residuals are driven to zero alongside the duality gap. That +/// avoids needing a phase one, but means infeasibility shows up as a failure +/// to converge rather than as a proof, so an unconverged run is reported as +/// `LpResult::Infeasible` only when the residuals are still large while the +/// gap has closed. +/// +/// Errors: +/// Returns an error if the problem's parts disagree in shape or `tol` is not +/// positive. +/// +/// Rust: `optimization::lp::interior_point` +#[pyfunction] +#[pyo3(name = "interior_point", signature = (p, tol))] +pub fn pyfn_interior_point(p: crate::generated::types::PyLpProblem, tol: f64) -> PyResult { + let p = p.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::lp::interior_point(&p, tol)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyLpResult { inner: __v }) +} + +/// Parses a linear program from text. +/// +/// The grammar is deliberately tiny: +/// +/// +/// The first line gives the sense and the objective. Everything after +/// `subject to` (or `st`, or `s.t.`) is a constraint row until an optional +/// `bounds` section, where single-variable lines set bounds rather than adding +/// rows and `free x` removes a variable's lower bound. Blank lines and `#` +/// comments are ignored, coefficients may be omitted, and variables are +/// numbered in order of first appearance. +/// +/// Errors: +/// Returns `GeomError::InvalidArgument` naming the first thing that could +/// not be read. +/// +/// Rust: `optimization::lp::lp_from_str` +#[pyfunction] +#[pyo3(name = "lp_from_str", signature = (text))] +pub fn pyfn_lp_from_str(text: String) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::lp::lp_from_str(&text)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyLpProblem { inner: __v }) +} + +/// Stigler's diet problem: the cheapest combination of foods meeting every +/// nutritional minimum. +/// +/// `costs` gives the price per unit of each food, `nutrients` holds the amount +/// of nutrient `k` in one unit of food `j` at `(k, j)`, and `requirements` +/// the minimum of each nutrient. +/// +/// Errors: +/// Returns an error if the shapes disagree. +/// +/// Rust: `optimization::lp::diet_problem` +#[pyfunction] +#[pyo3(name = "diet_problem", signature = (costs, nutrients, requirements))] +pub fn pyfn_diet_problem(costs: Vec, nutrients: crate::generated::types::PyMatrixArg, requirements: Vec) -> PyResult { + let nutrients = nutrients.0; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::lp::diet_problem(&costs, &nutrients, &requirements)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyLpProblem { inner: __v }) +} + +/// A production plan: how much of each product to make to maximise profit +/// under resource limits. +/// +/// `usage` holds the amount of resource `k` consumed per unit of product `j` +/// at `(k, j)`, and `available` the stock of each resource. +/// +/// Errors: +/// Returns an error if the shapes disagree. +/// +/// Rust: `optimization::lp::production_planning` +#[pyfunction] +#[pyo3(name = "production_planning", signature = (profits, usage, available))] +pub fn pyfn_production_planning(profits: Vec, usage: crate::generated::types::PyMatrixArg, available: Vec) -> PyResult { + let usage = usage.0; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::lp::production_planning(&profits, &usage, &available)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyLpProblem { inner: __v }) +} + +/// The transportation problem: ship from sources to sinks at least cost. +/// +/// `costs` holds the unit cost from source `i` to sink `j` at `(i, j)`. +/// Supply is an upper limit and demand a lower one, so unbalanced instances +/// are handled without inventing a dummy row. +/// +/// The constraint matrix is totally unimodular, so with integer supplies and +/// demands the simplex optimum is automatically integral -- no branch and +/// bound is needed, which is why the problem is solved as a linear program at +/// all. +/// +/// Errors: +/// Returns an error if the shapes disagree or total demand exceeds total +/// supply, which is infeasible by inspection. +/// +/// Rust: `optimization::lp::transportation_problem` +#[pyfunction] +#[pyo3(name = "transportation_problem", signature = (supply, demand, costs))] +pub fn pyfn_transportation_problem(supply: Vec, demand: Vec, costs: crate::generated::types::PyMatrixArg) -> PyResult { + let costs = costs.0; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::lp::transportation_problem(&supply, &demand, &costs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyLpResult { inner: __v }) +} + +/// Solves a two-player zero-sum game, returning +/// `(row strategy, column strategy, value)`. +/// +/// `payoff` holds the row player's gain at `(i, j)`. The row player maximises +/// the worst case and the column player minimises the best case, and von +/// Neumann's minimax theorem says the two coincide -- which here is not an +/// extra assumption but a consequence of LP duality, since the two players' +/// programs are duals of each other. The column strategy is read directly off +/// the row program's shadow prices. +/// +/// The payoff is shifted to be strictly positive before solving, since the +/// standard formulation divides by the value; the shift is undone on the way +/// out. +/// +/// Errors: +/// Returns an error if the resulting program has no optimum, which cannot +/// happen for a finite game and would indicate a numerical failure. +/// +/// Rust: `optimization::lp::two_player_zero_sum_lp` +#[pyfunction] +#[pyo3(name = "two_player_zero_sum_lp", signature = (payoff))] +pub fn pyfn_two_player_zero_sum_lp<'py>(py: Python<'py>, payoff: crate::generated::types::PyMatrixArg) -> PyResult<(Vec, Vec, f64)> { + let payoff = payoff.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::lp::two_player_zero_sum_lp(&payoff))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// The Chebyshev centre of the polyhedron `{x : a_i . x <= b_i}`: the point +/// furthest from every face, and that distance. +/// +/// Maximises `r` subject to `a_i . x + r ||a_i|| <= b_i`. The norm term is +/// what turns "satisfy the constraint" into "stay `r` away from it", and it is +/// why the problem is linear at all -- the distance from a point to a +/// hyperplane is linear in the point. +/// +/// Returns `(centre, radius)`. The radius is always unique, but the centre +/// need not be: in a box four wide and six tall the largest inscribed circle +/// has radius two and can sit anywhere along a vertical segment. Only the +/// coordinates that the touching faces pin down are determined, and the +/// returned point is one vertex of that optimal face. +/// +/// An unbounded polyhedron gives an infinite radius; an empty one is an error. +/// +/// Errors: +/// Returns an error for a shape mismatch, a zero row, or an infeasible system. +/// +/// Rust: `optimization::lp::chebyshev_center` +#[pyfunction] +#[pyo3(name = "chebyshev_center", signature = (a, b))] +pub fn pyfn_chebyshev_center<'py>(py: Python<'py>, a: crate::generated::types::PyMatrixArg, b: Vec) -> PyResult<(Vec, f64)> { + let a = a.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::lp::chebyshev_center(&a, &b))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// Least-absolute-deviations regression, solved as a linear program. +/// +/// Minimises `sum |y_i - x_i . beta|` by splitting each residual into a +/// positive and a negative part. The result is far less sensitive to an +/// outlier than a least-squares fit, because the cost of a large residual +/// grows linearly rather than quadratically -- an outlier at ten standard +/// deviations pulls a hundred times harder on a least-squares fit than on +/// this one. +/// +/// `x` holds one row per observation. Add a column of ones for an intercept. +/// +/// Errors: +/// Returns an error on a shape mismatch or if the program has no optimum. +/// +/// Rust: `optimization::lp::l1_regression_lp` +#[pyfunction] +#[pyo3(name = "l1_regression_lp", signature = (x, y))] +pub fn pyfn_l1_regression_lp<'py>(py: Python<'py>, x: crate::generated::types::PyMatrixArg, y: Vec) -> PyResult> { + let x = x.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::lp::l1_regression_lp(&x, &y))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Chebyshev (minimax) regression, solved as a linear program. +/// +/// Minimises the largest absolute residual rather than their sum. Where the +/// L1 fit ignores an outlier, this one is dominated by it -- the fit is +/// pinned by the extreme points and by nothing else, which is exactly what is +/// wanted when the residuals are bounded errors rather than noise. +/// +/// Errors: +/// Returns an error on a shape mismatch or if the program has no optimum. +/// +/// Rust: `optimization::lp::linf_regression_lp` +#[pyfunction] +#[pyo3(name = "linf_regression_lp", signature = (x, y))] +pub fn pyfn_linf_regression_lp<'py>(py: Python<'py>, x: crate::generated::types::PyMatrixArg, y: Vec) -> PyResult> { + let x = x.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::lp::linf_regression_lp(&x, &y))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_simplex, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lp_dual, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sensitivity_ranges, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dual_simplex, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_interior_point, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lp_from_str, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_diet_problem, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_production_planning, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_transportation_problem, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_two_player_zero_sum_lp, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_chebyshev_center, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_l1_regression_lp, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_linf_regression_lp, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_optimization__metaheuristics.rs b/bindings/python/src/generated/m_optimization__metaheuristics.rs new file mode 100644 index 0000000..d1b7504 --- /dev/null +++ b/bindings/python/src/generated/m_optimization__metaheuristics.rs @@ -0,0 +1,332 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Compass pattern search: probe one coordinate step in each direction, move +/// to any improvement, and halve the step when none is found. +/// +/// The simplest direct search that still has a convergence proof: on a +/// smooth function the step only shrinks when the current point beats all +/// `2n` neighbours, which forces the gradient toward zero as the step does. +/// Slower than Nelder-Mead in practice and far more robust, since it never +/// deforms its search pattern and so cannot collapse into a degenerate +/// simplex. +/// +/// Panics: +/// Panics unless the starting point is non-empty and the step and tolerance +/// are positive. +/// +/// Rust: `optimization::metaheuristics::pattern_search` +#[pyfunction] +#[pyo3(name = "pattern_search", signature = (f, x0, step, tol, max_iter))] +pub fn pyfn_pattern_search(f: pyo3::Py, x0: Vec, step: f64, tol: f64, max_iter: usize) -> PyResult<(Vec, f64)> { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: &[f64]| -> f64 { __cb.call::<_, f64>((__a0.to_vec(),), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::metaheuristics::pattern_search(&f, &x0, step, tol, max_iter)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Basin hopping: repeated local descent from perturbed starting points, +/// keeping the perturbation only when it leads somewhere better. +/// +/// The Metropolis acceptance is applied to the *local minima*, not to the raw +/// function, which is what makes it a search over basins rather than over +/// points. On a landscape of many narrow wells separated by high barriers -- +/// the case that defeats plain annealing -- collapsing each well to its floor +/// first turns the problem into a much smoother one. +/// +/// Panics: +/// Panics unless the temperature and step are positive. +/// +/// Rust: `optimization::metaheuristics::basin_hopping` +#[pyfunction] +#[pyo3(name = "basin_hopping", signature = (f, x0, step, temperature, hops, rng))] +pub fn pyfn_basin_hopping(f: pyo3::Py, x0: Vec, step: f64, temperature: f64, hops: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(Vec, f64)> { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: &[f64]| -> f64 { __cb.call::<_, f64>((__a0.to_vec(),), f64::NAN) } }; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::metaheuristics::basin_hopping(&f, &x0, step, temperature, hops, &mut rng.inner)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Repeated local search from random starting points inside a box. +/// +/// The cheapest defence against a multimodal landscape, and a fair baseline: +/// any population method that cannot beat enough random restarts to match its +/// evaluation budget is not earning its complexity. +/// +/// Panics: +/// Panics if `bounds` is empty or `starts` is zero. +/// +/// Rust: `optimization::metaheuristics::multistart_local` +#[pyfunction] +#[pyo3(name = "multistart_local", signature = (f, bounds, starts, rng))] +pub fn pyfn_multistart_local(f: pyo3::Py, bounds: Vec<(f64, f64)>, starts: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(Vec, f64)> { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: &[f64]| -> f64 { __cb.call::<_, f64>((__a0.to_vec(),), f64::NAN) } }; + let bounds = bounds.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::metaheuristics::multistart_local(&f, &bounds, starts, &mut rng.inner)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Differential evolution: mutate by adding a scaled difference of two +/// population members to a third, then cross over with the target. +/// +/// The difference vector is the whole idea. Early on the population is spread +/// out and the differences are large, so the search is global; as it +/// converges the differences shrink with it and the search becomes local. +/// Nobody has to schedule that -- the step size is read off the population's +/// own spread, which is why the method has so few parameters and why they +/// transfer between problems. +/// +/// `cr` is the crossover rate in `[0, 1]` and `weight` the differential +/// scaling, conventionally near `0.8`. +/// +/// Panics: +/// Panics unless the population is at least four, `cr` lies in `[0, 1]`, and +/// the bounds are non-empty. +/// +/// Rust: `optimization::metaheuristics::differential_evolution` +#[pyfunction] +#[pyo3(name = "differential_evolution", signature = (f, bounds, population, cr, weight, generations, rng))] +pub fn pyfn_differential_evolution(f: pyo3::Py, bounds: Vec<(f64, f64)>, population: usize, cr: f64, weight: f64, generations: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(Vec, f64)> { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: &[f64]| -> f64 { __cb.call::<_, f64>((__a0.to_vec(),), f64::NAN) } }; + let bounds = bounds.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::metaheuristics::differential_evolution(&f, &bounds, population, cr, weight, generations, &mut rng.inner)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Particle swarm optimisation: each particle carries a velocity pulled +/// toward its own best and the swarm's best. +/// +/// `inertia` retains the previous velocity, `cognitive` weights the pull +/// toward the particle's own history and `social` the pull toward the +/// swarm's. The classic failure is setting inertia too high, where the swarm +/// never settles, or too low, where it collapses onto the first decent point +/// found and stops exploring. +/// +/// Panics: +/// Panics unless the swarm is non-empty and the bounds are non-empty. +/// +/// Rust: `optimization::metaheuristics::particle_swarm` +#[pyfunction] +#[pyo3(name = "particle_swarm", signature = (f, bounds, particles, inertia, cognitive, social, iterations, rng))] +pub fn pyfn_particle_swarm(f: pyo3::Py, bounds: Vec<(f64, f64)>, particles: usize, inertia: f64, cognitive: f64, social: f64, iterations: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(Vec, f64)> { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: &[f64]| -> f64 { __cb.call::<_, f64>((__a0.to_vec(),), f64::NAN) } }; + let bounds = bounds.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::metaheuristics::particle_swarm(&f, &bounds, particles, inertia, cognitive, social, iterations, &mut rng.inner)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// The covariance matrix adaptation evolution strategy. +/// +/// Samples a population from a multivariate normal, keeps the better half, +/// and updates the mean, the step size and the full covariance from them. The +/// covariance is what sets it apart: after enough generations it approximates +/// the inverse Hessian up to scale, so the sampling distribution stretches +/// along the valley floor of a badly conditioned problem instead of +/// stumbling across it. That is the same information Newton's method uses, +/// obtained without a single derivative. +/// +/// The step size is adapted separately, by comparing the length of the path +/// the mean has actually travelled against the length a random walk would +/// have covered; a mean that keeps moving in one direction is taking steps +/// that are too small. +/// +/// Panics: +/// Panics unless the starting point is non-empty and `sigma0` is positive. +/// +/// Rust: `optimization::metaheuristics::cma_es` +#[pyfunction] +#[pyo3(name = "cma_es", signature = (f, x0, sigma0, generations, rng))] +pub fn pyfn_cma_es(f: pyo3::Py, x0: Vec, sigma0: f64, generations: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(Vec, f64)> { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: &[f64]| -> f64 { __cb.call::<_, f64>((__a0.to_vec(),), f64::NAN) } }; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::metaheuristics::cma_es(&f, &x0, sigma0, generations, &mut rng.inner)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// A real-valued genetic algorithm with tournament selection, blend +/// crossover and Gaussian mutation. +/// +/// Elitism is what makes the best-so-far monotone: without carrying the best +/// members over untouched, a generation can be strictly worse than the last, +/// and the algorithm has no memory to recover it from. +/// +/// Minimises `f`. +/// +/// Panics: +/// Panics unless the population exceeds the elite count and the bounds are +/// non-empty. +/// +/// Rust: `optimization::metaheuristics::genetic_algorithm` +#[pyfunction] +#[pyo3(name = "genetic_algorithm", signature = (f, bounds, config, rng))] +pub fn pyfn_genetic_algorithm(f: pyo3::Py, bounds: Vec<(f64, f64)>, config: crate::generated::types::PyGaConfig, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(Vec, f64)> { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: &[f64]| -> f64 { __cb.call::<_, f64>((__a0.to_vec(),), f64::NAN) } }; + let bounds = bounds.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let config = config.inner; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::metaheuristics::genetic_algorithm(&f, &bounds, &config, &mut rng.inner)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// A genetic algorithm over permutations, with order crossover and swap +/// mutation. +/// +/// Blend crossover is meaningless on a permutation -- averaging two orderings +/// does not give an ordering. Order crossover instead copies a slice from one +/// parent and fills the rest in the order the other parent visits them, which +/// preserves relative order from both and always produces a valid +/// permutation. That closure property is the whole difficulty of the +/// permutation case. +/// +/// Minimises `cost`. +/// +/// Panics: +/// Panics unless `n >= 2` and the population exceeds the elite count. +/// +/// Rust: `optimization::metaheuristics::genetic_algorithm_permutation` +#[pyfunction] +#[pyo3(name = "genetic_algorithm_permutation", signature = (cost, n, config, rng))] +pub fn pyfn_genetic_algorithm_permutation(cost: pyo3::Py, n: usize, config: crate::generated::types::PyGaConfig, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(Vec, f64)> { + let __cb_cost = std::rc::Rc::new(crate::runtime::Callback::new(cost)); + let cost = { let __cb = __cb_cost.clone(); move |__a0: &[usize]| -> f64 { __cb.call::<_, f64>((__a0.to_vec(),), f64::NAN) } }; + let config = config.inner; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::metaheuristics::genetic_algorithm_permutation(&cost, n, &config, &mut rng.inner)); + crate::runtime::callback::check(&[&__cb_cost], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Indices of the non-dominated points: those no other point beats on every +/// objective while beating it on at least one. +/// +/// Minimisation in every coordinate. The result is the Pareto front, and the +/// point of computing it is that without further information there is no +/// reason to prefer any member of it to any other -- a single "best" answer +/// only exists once the objectives are weighted, which is a decision the +/// optimiser cannot make. +/// +/// Rust: `optimization::metaheuristics::pareto_front` +#[pyfunction] +#[pyo3(name = "pareto_front", signature = (points))] +pub fn pyfn_pareto_front<'py>(py: Python<'py>, points: Vec>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::metaheuristics::pareto_front(&points))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The area dominated by a two-objective front, bounded by a reference point. +/// +/// The standard scalar summary of a front's quality, and the only common one +/// that is strictly monotone: adding a point that is not already dominated +/// can only increase it, so it cannot reward a front for losing coverage. +/// Points not dominating the reference contribute nothing. +/// +/// Panics: +/// Panics if a front point is not two-dimensional. +/// +/// Rust: `optimization::metaheuristics::hypervolume_2d` +#[pyfunction] +#[pyo3(name = "hypervolume_2d", signature = (front, reference))] +pub fn pyfn_hypervolume_2d<'py>(py: Python<'py>, front: Vec>, reference: (f64, f64)) -> PyResult { + let reference = (reference.0, reference.1); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::metaheuristics::hypervolume_2d(&front, reference))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The standard test landscapes, in two dimensions. +/// +/// They are chosen to fail different methods. Sphere is convex and separable +/// and everything solves it. Rosenbrock's optimum sits at the end of a curved +/// valley whose floor is nearly flat, which punishes anything that treats the +/// coordinates independently. Rastrigin and Ackley add a regular lattice of +/// local minima on top of a global structure, so a purely local method stops +/// at the first one. Griewank's local minima vanish as the dimension grows, +/// which makes it *easier* in higher dimensions and is a standing warning +/// about extrapolating benchmark results. Schwefel puts its optimum near a +/// corner, far from the centre where most methods are initialised. +/// +/// The recorded optima are verified by dense sampling in this module's tests +/// rather than taken on trust. +/// +/// Rust: `optimization::metaheuristics::benchmark_functions` +#[pyfunction] +#[pyo3(name = "benchmark_functions", signature = ())] +pub fn pyfn_benchmark_functions() -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::metaheuristics::benchmark_functions()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyBenchmark { inner: __x }).collect::>()) +} + +/// The running best of a sequence of objective values. +/// +/// Monotone non-increasing by construction, which is what makes two runs +/// comparable: the raw values of a stochastic search jump around and say +/// nothing about progress. +/// +/// Rust: `optimization::metaheuristics::convergence_curve` +#[pyfunction] +#[pyo3(name = "convergence_curve", signature = (history))] +pub fn pyfn_convergence_curve<'py>(py: Python<'py>, history: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::metaheuristics::convergence_curve(&history))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_pattern_search, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_basin_hopping, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_multistart_local, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_differential_evolution, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_particle_swarm, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cma_es, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_genetic_algorithm, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_genetic_algorithm_permutation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pareto_front, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hypervolume_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_benchmark_functions, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_convergence_curve, m)?)?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_optimization__network.rs b/bindings/python/src/generated/m_optimization__network.rs new file mode 100644 index 0000000..ef6eb2a --- /dev/null +++ b/bindings/python/src/generated/m_optimization__network.rs @@ -0,0 +1,411 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// The transshipment problem: ship from sources to sinks through intermediate +/// nodes at least cost. +/// +/// `supply[i]` is positive at a source, negative at a sink, and zero at a pure +/// transshipment node; the entries must sum to zero. `arcs` lists +/// `(from, to, unit cost, capacity)`. +/// +/// Generalises the transportation problem by allowing goods to pass through a +/// node rather than only from a source directly to a sink, which is what makes +/// it a network rather than a bipartite matching. +/// +/// Errors: +/// Returns an error if an arc names a node out of range or the supplies do not +/// balance. +/// +/// Rust: `optimization::network::transshipment` +#[pyfunction] +#[pyo3(name = "transshipment", signature = (supply, arcs))] +pub fn pyfn_transshipment(supply: Vec, arcs: Vec<(usize, usize, f64, f64)>) -> PyResult { + let arcs = arcs.into_iter().map(|__e| (__e.0, __e.1, __e.2, __e.3)).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::network::transshipment(&supply, &arcs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyLpResult { inner: __v }) +} + +/// The length of a shortest path, computed as a linear program. +/// +/// The dual of the shortest path problem asks for node potentials that +/// maximise the gap between source and target while no arc rises by more than +/// its length -- so the answer comes out of a linear program whose constraint +/// matrix is a node-arc incidence matrix, which is totally unimodular. +/// +/// Its purpose is to check the graph module's Dijkstra against a completely +/// different method. Slower by a wide margin, and worth it only as +/// verification. +/// +/// Errors: +/// Returns an error if the endpoints are out of range, or the graph has a +/// negative-length arc, where the linear program is unbounded rather than +/// merely wrong. +/// +/// Rust: `optimization::network::shortest_path_lp_check` +#[pyfunction] +#[pyo3(name = "shortest_path_lp_check", signature = (g, s, t))] +pub fn pyfn_shortest_path_lp_check(g: crate::generated::types::PyGraph, s: usize, t: usize) -> PyResult> { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::network::shortest_path_lp_check(&g, s, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.map(|__x| __x)) +} + +/// The value of a maximum flow, computed as a linear program. +/// +/// Maximises the net outflow from the source subject to conservation at every +/// other node and each arc's capacity. Like the shortest path formulation this +/// exists to check the graph module's combinatorial algorithms rather than to +/// replace them. +/// +/// Errors: +/// Returns an error if the endpoints are out of range or coincide. +/// +/// Rust: `optimization::network::max_flow_lp_check` +#[pyfunction] +#[pyo3(name = "max_flow_lp_check", signature = (g, s, t))] +pub fn pyfn_max_flow_lp_check(g: crate::generated::types::PyGraph, s: usize, t: usize) -> PyResult> { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::network::max_flow_lp_check(&g, s, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.map(|__x| __x)) +} + +/// A minimum-cost flow by the network simplex, expressed through the general +/// simplex method. +/// +/// `arcs` are `(from, to, unit cost, capacity)` and `balance[i]` the net +/// supply at node `i`, summing to zero. The genuine network simplex maintains +/// a spanning tree basis and pivots in `O(m)` per step rather than solving a +/// linear system; this routes the same problem through the general solver, +/// which is correct and slower, and is named "lite" for that reason. +/// +/// Errors: +/// Returns an error under the same conditions as `transshipment`. +/// +/// Rust: `optimization::network::network_simplex_lite` +#[pyfunction] +#[pyo3(name = "network_simplex_lite", signature = (balance, arcs))] +pub fn pyfn_network_simplex_lite(balance: Vec, arcs: Vec<(usize, usize, f64, f64)>) -> PyResult { + let arcs = arcs.into_iter().map(|__e| (__e.0, __e.1, __e.2, __e.3)).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::network::network_simplex_lite(&balance, &arcs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyLpResult { inner: __v }) +} + +/// The critical path method: the shortest possible project duration, which +/// tasks cannot slip, and every task's four schedule times. +/// +/// `tasks[i]` is `(duration, predecessors)`. Returns +/// `(duration, critical task indices, times)`. +/// +/// The critical path is the longest path through the precedence graph, and the +/// project cannot finish sooner than that however many resources are thrown at +/// it -- which is the point of computing it. A task is critical exactly when +/// its slack is zero, so shortening a non-critical task buys nothing at all. +/// +/// Errors: +/// Returns an error if a predecessor is out of range or the precedences +/// contain a cycle, which makes the project unschedulable. +/// +/// Rust: `optimization::network::critical_path_method` +#[pyfunction] +#[pyo3(name = "critical_path_method", signature = (tasks))] +pub fn pyfn_critical_path_method(tasks: Vec<(f64, Vec)>) -> PyResult<(f64, Vec, Vec)> { + let tasks = tasks.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::network::critical_path_method(&tasks)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1, __v.2.into_iter().map(|__x| crate::generated::types::PyTaskTimes { inner: __x }).collect::>())) +} + +/// PERT: the mean and variance of the project duration under three-point +/// estimates. +/// +/// `tasks[i]` is `(optimistic, most likely, pessimistic, predecessors)`. Each +/// task's duration is taken as a beta distribution with mean +/// `(a + 4m + b) / 6` and standard deviation `(b - a) / 6`, and the project +/// duration as the sum along the critical path. +/// +/// The variance is the sum of the *critical path's* variances only, which is +/// the method's known weakness: a near-critical path with high variance can +/// overtake the critical one and PERT will not see it, so the figure +/// understates the true spread. It is reported because it is what PERT means, +/// not because it is the whole answer. +/// +/// Errors: +/// Returns an error if an estimate is out of order or the precedences are +/// unschedulable. +/// +/// Rust: `optimization::network::pert` +#[pyfunction] +#[pyo3(name = "pert", signature = (tasks))] +pub fn pyfn_pert<'py>(py: Python<'py>, tasks: Vec<(f64, f64, f64, Vec)>) -> PyResult<(f64, f64)> { + let tasks = tasks.into_iter().map(|__e| (__e.0, __e.1, __e.2, __e.3)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::network::pert(&tasks))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// Clarke-Wright savings for the capacitated vehicle routing problem. +/// +/// Every customer starts on its own out-and-back route. Merging the routes +/// ending at `i` and beginning at `j` saves `d(0,i) + d(0,j) - d(i,j)` -- the +/// two depot legs replaced by one direct leg -- so merges are tried in +/// decreasing order of that saving, subject to capacity. +/// +/// Returns the routes as customer sequences, excluding the depot at each end. +/// +/// Errors: +/// Returns an error if the distance matrix is the wrong shape, or a customer's +/// demand exceeds a vehicle's capacity, which makes routing impossible. +/// +/// Rust: `optimization::network::vehicle_routing_savings` +#[pyfunction] +#[pyo3(name = "vehicle_routing_savings", signature = (distance, demand, capacity))] +pub fn pyfn_vehicle_routing_savings<'py>(py: Python<'py>, distance: crate::generated::types::PyMatrixArg, demand: Vec, capacity: f64) -> PyResult>> { + let distance = distance.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::network::vehicle_routing_savings(&distance, &demand, capacity))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// A lower bound on a job shop makespan by the shifting bottleneck idea, +/// simplified. +/// +/// `jobs[j]` lists `(machine, duration)` in the order job `j` must visit them. +/// Returns the larger of the busiest machine's total load and the longest +/// job's total work -- both of which any schedule must exceed, since a machine +/// cannot process two operations at once and a job cannot be in two places. +/// +/// The full shifting bottleneck procedure solves a one-machine sequencing +/// problem per machine and iterates; this reports the elementary bound those +/// iterations start from. +/// +/// Errors: +/// Returns an error if a machine index exceeds the machine count. +/// +/// Rust: `optimization::network::job_shop_shifting_bottleneck_lite` +#[pyfunction] +#[pyo3(name = "job_shop_shifting_bottleneck_lite", signature = (jobs, machines))] +pub fn pyfn_job_shop_shifting_bottleneck_lite<'py>(py: Python<'py>, jobs: Vec>, machines: usize) -> PyResult { + let jobs = jobs.into_iter().map(|__e| __e.into_iter().map(|__e| (__e.0, __e.1)).collect::>()).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::network::job_shop_shifting_bottleneck_lite(&jobs, machines))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Shortest processing time first: the order minimising mean flow time on one +/// machine. +/// +/// Optimal by an exchange argument -- swapping an adjacent out-of-order pair +/// always improves the total -- and optimal for nothing else. It can make one +/// long job arbitrarily late while the average looks excellent, which is why +/// the objective has to be chosen before the rule. +/// +/// `jobs[i]` is a processing time. Returns the job order. +/// +/// Rust: `optimization::network::scheduling_spt` +#[pyfunction] +#[pyo3(name = "scheduling_spt", signature = (jobs))] +pub fn pyfn_scheduling_spt<'py>(py: Python<'py>, jobs: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::network::scheduling_spt(&jobs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Earliest due date first: the order minimising maximum lateness on one +/// machine. +/// +/// Jackson's rule. Also by an exchange argument, and again optimal only for +/// its own objective: it makes no attempt to reduce the *number* of late jobs, +/// which is what `moore_hodgson` is for. +/// +/// `jobs[i]` is `(processing time, due date)`. Returns the job order. +/// +/// Rust: `optimization::network::scheduling_edd` +#[pyfunction] +#[pyo3(name = "scheduling_edd", signature = (jobs))] +pub fn pyfn_scheduling_edd<'py>(py: Python<'py>, jobs: Vec<(f64, f64)>) -> PyResult> { + let jobs = jobs.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::network::scheduling_edd(&jobs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Moore-Hodgson rule: the order minimising the *number* of late jobs on +/// one machine. +/// +/// Work through the jobs by due date; whenever the schedule falls behind, +/// throw out the longest job accepted so far. That one removal buys the most +/// time back, and the jobs thrown out are exactly the late ones, which is what +/// makes the rule optimal rather than merely sensible. +/// +/// Returns the order: the on-time jobs first in due-date order, then the late +/// ones. +/// +/// `jobs[i]` is `(processing time, due date)`. +/// +/// Rust: `optimization::network::moore_hodgson` +#[pyfunction] +#[pyo3(name = "moore_hodgson", signature = (jobs))] +pub fn pyfn_moore_hodgson<'py>(py: Python<'py>, jobs: Vec<(f64, f64)>) -> PyResult> { + let jobs = jobs.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::network::moore_hodgson(&jobs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Johnson's rule: the order minimising makespan through two machines in +/// series. +/// +/// Every job visits machine one then machine two. Jobs whose first operation +/// is the shorter go first, in increasing order of that operation; the rest go +/// last, in decreasing order of their second. The first group fills machine +/// two's queue quickly and the second keeps it busy at the end, which is what +/// the exchange argument formalises. +/// +/// `jobs[i]` is `(time on machine one, time on machine two)`. +/// +/// Rust: `optimization::network::johnson_two_machine` +#[pyfunction] +#[pyo3(name = "johnson_two_machine", signature = (jobs))] +pub fn pyfn_johnson_two_machine<'py>(py: Python<'py>, jobs: Vec<(f64, f64)>) -> PyResult> { + let jobs = jobs.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::network::johnson_two_machine(&jobs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The makespan of a two-machine flow shop under a given order. +/// +/// Machine two cannot start a job before machine one finishes it, nor before +/// it finishes the previous job, which is the whole recursion. +/// +/// Rust: `optimization::network::two_machine_makespan` +#[pyfunction] +#[pyo3(name = "two_machine_makespan", signature = (jobs, order))] +pub fn pyfn_two_machine_makespan<'py>(py: Python<'py>, jobs: Vec<(f64, f64)>, order: Vec) -> PyResult { + let jobs = jobs.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::network::two_machine_makespan(&jobs, &order))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Longest processing time first onto identical parallel machines. +/// +/// Returns the makespan and which machine each job went to. The rule finishes +/// within `4/3 - 1/(3m)` of the optimum, and that bound is tight -- so it is a +/// guarantee rather than an observation, and the tests check it against an +/// exact answer. +/// +/// Panics: +/// Panics if `machines` is zero. +/// +/// Rust: `optimization::network::lpt_makespan` +#[pyfunction] +#[pyo3(name = "lpt_makespan", signature = (jobs, machines))] +pub fn pyfn_lpt_makespan<'py>(py: Python<'py>, jobs: Vec, machines: usize) -> PyResult<(f64, Vec)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::network::lpt_makespan(&jobs, machines))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// The largest set of pairwise disjoint intervals, by earliest finish time. +/// +/// The greedy choice is optimal, and the proof is the reason: whatever the +/// optimal set, replacing its first interval by the one that finishes earliest +/// leaves it still valid and no smaller, so an optimal solution containing the +/// greedy choice always exists. +/// +/// `intervals[i]` is `(start, end)`. Returns the chosen indices. +/// +/// Rust: `optimization::network::interval_scheduling_max` +#[pyfunction] +#[pyo3(name = "interval_scheduling_max", signature = (intervals))] +pub fn pyfn_interval_scheduling_max<'py>(py: Python<'py>, intervals: Vec<(f64, f64)>) -> PyResult> { + let intervals = intervals.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::network::interval_scheduling_max(&intervals))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The most valuable set of pairwise disjoint intervals. +/// +/// Weights break the greedy argument completely -- one long valuable interval +/// can be worth more than any number of short ones -- so this is a table: +/// sort by finish time and, for each interval, either take it and jump to the +/// last compatible one or skip it. +/// +/// `intervals[i]` is `(start, end, weight)`. Returns the total and the chosen +/// indices. +/// +/// Rust: `optimization::network::weighted_interval_scheduling` +#[pyfunction] +#[pyo3(name = "weighted_interval_scheduling", signature = (intervals))] +pub fn pyfn_weighted_interval_scheduling<'py>(py: Python<'py>, intervals: Vec<(f64, f64, f64)>) -> PyResult<(f64, Vec)> { + let intervals = intervals.into_iter().map(|__e| (__e.0, __e.1, __e.2)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::network::weighted_interval_scheduling(&intervals))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Turns a single-machine job order into `(job, start, finish)` bars. +/// +/// Jobs run back to back in the given order from time zero, which is what a +/// single-machine sequencing rule assumes. +/// +/// Rust: `optimization::network::gantt_data` +#[pyfunction] +#[pyo3(name = "gantt_data", signature = (processing, order))] +pub fn pyfn_gantt_data<'py>(py: Python<'py>, processing: Vec, order: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::optimization::network::gantt_data(&processing, &order))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, __x.2)).collect::>()) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_transshipment, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_shortest_path_lp_check, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_max_flow_lp_check, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_network_simplex_lite, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_critical_path_method, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pert, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_vehicle_routing_savings, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_job_shop_shifting_bottleneck_lite, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_scheduling_spt, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_scheduling_edd, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_moore_hodgson, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_johnson_two_machine, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_two_machine_makespan, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lpt_makespan, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_interval_scheduling_max, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_weighted_interval_scheduling, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gantt_data, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_particle_physics.rs b/bindings/python/src/generated/m_particle_physics.rs new file mode 100644 index 0000000..7ed386c --- /dev/null +++ b/bindings/python/src/generated/m_particle_physics.rs @@ -0,0 +1,283 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Invariant mass from total energy and scalar momentum magnitude. +/// m = √(E² - p²c²) / c² +/// +/// Rust: `particle_physics::invariant_mass` +#[pyfunction] +#[pyo3(name = "invariant_mass", signature = (energy, momentum))] +pub fn pyfn_invariant_mass(energy: f64, momentum: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::particle_physics::invariant_mass(energy, momentum)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Invariant mass of a two-body system from individual four-momenta. +/// m² = (E1+E2)² - |p1+p2|²c², return m/c². +/// +/// Rust: `particle_physics::invariant_mass_two_body` +#[pyfunction] +#[pyo3(name = "invariant_mass_two_body", signature = (e1, px1, py1, pz1, e2, px2, py2, pz2))] +pub fn pyfn_invariant_mass_two_body(e1: f64, px1: f64, py1: f64, pz1: f64, e2: f64, px2: f64, py2: f64, pz2: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::particle_physics::invariant_mass_two_body(e1, px1, py1, pz1, e2, px2, py2, pz2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Center-of-mass energy √s for two particles with given energies and momenta. +/// √s = √((E1+E2)² - (p1+p2)²c²) +/// +/// Rust: `particle_physics::center_of_mass_energy` +#[pyfunction] +#[pyo3(name = "center_of_mass_energy", signature = (e_beam, e_target, p_beam, p_target))] +pub fn pyfn_center_of_mass_energy(e_beam: f64, e_target: f64, p_beam: f64, p_target: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::particle_physics::center_of_mass_energy(e_beam, e_target, p_beam, p_target)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Fixed-target center-of-mass energy (high-energy approximation). +/// √s ≈ √(2 × E_beam × m_target × c²) +/// +/// Rust: `particle_physics::fixed_target_com_energy` +#[pyfunction] +#[pyo3(name = "fixed_target_com_energy", signature = (beam_energy, target_mass))] +pub fn pyfn_fixed_target_com_energy(beam_energy: f64, target_mass: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::particle_physics::fixed_target_com_energy(beam_energy, target_mass)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Lorentz boost of energy along z. E' = γ(E - β pz c) +/// +/// Rust: `particle_physics::lorentz_boost_energy` +#[pyfunction] +#[pyo3(name = "lorentz_boost_energy", signature = (energy, momentum_z, beta))] +pub fn pyfn_lorentz_boost_energy(energy: f64, momentum_z: f64, beta: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::particle_physics::lorentz_boost_energy(energy, momentum_z, beta)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Lorentz boost of z-momentum. pz' = γ(pz - β E/c) +/// +/// Rust: `particle_physics::lorentz_boost_pz` +#[pyfunction] +#[pyo3(name = "lorentz_boost_pz", signature = (energy, momentum_z, beta))] +pub fn pyfn_lorentz_boost_pz(energy: f64, momentum_z: f64, beta: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::particle_physics::lorentz_boost_pz(energy, momentum_z, beta)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Rapidity y = 0.5 × ln((E + pz c) / (E - pz c)) +/// +/// Rust: `particle_physics::rapidity` +#[pyfunction] +#[pyo3(name = "rapidity", signature = (energy, pz))] +pub fn pyfn_rapidity(energy: f64, pz: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::particle_physics::rapidity(energy, pz)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Pseudorapidity η = -ln(tan(θ/2)) +/// +/// Rust: `particle_physics::pseudorapidity` +#[pyfunction] +#[pyo3(name = "pseudorapidity", signature = (theta))] +pub fn pyfn_pseudorapidity(theta: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::particle_physics::pseudorapidity(theta)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Transverse momentum pT = √(px² + py²) +/// +/// Rust: `particle_physics::transverse_momentum` +#[pyfunction] +#[pyo3(name = "transverse_momentum", signature = (px, py))] +pub fn pyfn_transverse_momentum(px: f64, py: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::particle_physics::transverse_momentum(px, py)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Rutherford scattering differential cross section. +/// dσ/dΩ = (Z1 Z2 k_e e² / (4E))² / sin⁴(θ/2) +/// +/// Rust: `particle_physics::rutherford_cross_section` +#[pyfunction] +#[pyo3(name = "rutherford_cross_section", signature = (z1, z2, energy, angle))] +pub fn pyfn_rutherford_cross_section(z1: f64, z2: f64, energy: f64, angle: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::particle_physics::rutherford_cross_section(z1, z2, energy, angle)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Non-relativistic Breit-Wigner resonance (normalized to peak = 1). +/// BW(E) = (Γ/2)² / ((E - M)² + (Γ/2)²) +/// +/// Rust: `particle_physics::breit_wigner` +#[pyfunction] +#[pyo3(name = "breit_wigner", signature = (energy, mass, width))] +pub fn pyfn_breit_wigner(energy: f64, mass: f64, width: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::particle_physics::breit_wigner(energy, mass, width)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Decay rate from lifetime. Γ = ℏ / τ +/// +/// Rust: `particle_physics::decay_rate_from_lifetime` +#[pyfunction] +#[pyo3(name = "decay_rate_from_lifetime", signature = (lifetime))] +pub fn pyfn_decay_rate_from_lifetime(lifetime: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::particle_physics::decay_rate_from_lifetime(lifetime)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Lifetime from decay width. τ = ℏ / Γ +/// +/// Rust: `particle_physics::lifetime_from_width` +#[pyfunction] +#[pyo3(name = "lifetime_from_width", signature = (width_joules))] +pub fn pyfn_lifetime_from_width(width_joules: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::particle_physics::lifetime_from_width(width_joules)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Branching ratio BR = Γ_i / Γ_total +/// +/// Rust: `particle_physics::branching_ratio` +#[pyfunction] +#[pyo3(name = "branching_ratio", signature = (partial_width, total_width))] +pub fn pyfn_branching_ratio(partial_width: f64, total_width: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::particle_physics::branching_ratio(partial_width, total_width)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Mean free path λ = 1 / (n σ) +/// +/// Rust: `particle_physics::mean_free_path_particle` +#[pyfunction] +#[pyo3(name = "mean_free_path_particle", signature = (cross_section, number_density))] +pub fn pyfn_mean_free_path_particle(cross_section: f64, number_density: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::particle_physics::mean_free_path_particle(cross_section, number_density)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Event rate R = L × σ +/// +/// Rust: `particle_physics::luminosity_to_event_rate` +#[pyfunction] +#[pyo3(name = "luminosity_to_event_rate", signature = (luminosity, cross_section))] +pub fn pyfn_luminosity_to_event_rate(luminosity: f64, cross_section: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::particle_physics::luminosity_to_event_rate(luminosity, cross_section)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Check charge conservation: sum of input charges ≈ sum of output charges. +/// +/// Rust: `particle_physics::is_charge_conserved` +#[pyfunction] +#[pyo3(name = "is_charge_conserved", signature = (charges_in, charges_out))] +pub fn pyfn_is_charge_conserved<'py>(py: Python<'py>, charges_in: Vec, charges_out: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::particle_physics::is_charge_conserved(&charges_in, &charges_out))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Check lepton number conservation. +/// +/// Rust: `particle_physics::is_lepton_number_conserved` +#[pyfunction] +#[pyo3(name = "is_lepton_number_conserved", signature = (leptons_in, leptons_out))] +pub fn pyfn_is_lepton_number_conserved<'py>(py: Python<'py>, leptons_in: Vec, leptons_out: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::particle_physics::is_lepton_number_conserved(&leptons_in, &leptons_out))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Check baryon number conservation. +/// +/// Rust: `particle_physics::is_baryon_number_conserved` +#[pyfunction] +#[pyo3(name = "is_baryon_number_conserved", signature = (baryons_in, baryons_out))] +pub fn pyfn_is_baryon_number_conserved<'py>(py: Python<'py>, baryons_in: Vec, baryons_out: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::particle_physics::is_baryon_number_conserved(&baryons_in, &baryons_out))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Four-momentum magnitude (invariant mass × c). +/// √(E² - p²c²) / c = mc +/// +/// Rust: `particle_physics::four_momentum_magnitude` +#[pyfunction] +#[pyo3(name = "four_momentum_magnitude", signature = (energy, px, py, pz))] +pub fn pyfn_four_momentum_magnitude(energy: f64, px: f64, py: f64, pz: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::particle_physics::four_momentum_magnitude(energy, px, py, pz)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_invariant_mass, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_invariant_mass_two_body, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_center_of_mass_energy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fixed_target_com_energy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lorentz_boost_energy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lorentz_boost_pz, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rapidity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pseudorapidity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_transverse_momentum, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rutherford_cross_section, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_breit_wigner, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_decay_rate_from_lifetime, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lifetime_from_width, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_branching_ratio, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mean_free_path_particle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_luminosity_to_event_rate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_is_charge_conserved, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_is_lepton_number_conserved, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_is_baryon_number_conserved, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_four_momentum_magnitude, m)?)?; + m.add("M_MUON", rust_physics_engine::particle_physics::M_MUON)?; + m.add("M_TAU", rust_physics_engine::particle_physics::M_TAU)?; + m.add("M_PION_CHARGED", rust_physics_engine::particle_physics::M_PION_CHARGED)?; + m.add("M_PION_NEUTRAL", rust_physics_engine::particle_physics::M_PION_NEUTRAL)?; + m.add("M_KAON", rust_physics_engine::particle_physics::M_KAON)?; + m.add("M_W_BOSON", rust_physics_engine::particle_physics::M_W_BOSON)?; + m.add("M_Z_BOSON", rust_physics_engine::particle_physics::M_Z_BOSON)?; + m.add("M_HIGGS", rust_physics_engine::particle_physics::M_HIGGS)?; + m.add("M_TOP_QUARK", rust_physics_engine::particle_physics::M_TOP_QUARK)?; + m.add("CHARGE_UP", rust_physics_engine::particle_physics::CHARGE_UP)?; + m.add("CHARGE_DOWN", rust_physics_engine::particle_physics::CHARGE_DOWN)?; + m.add("FINE_STRUCTURE", rust_physics_engine::particle_physics::FINE_STRUCTURE)?; + m.add("WEAK_MIXING_ANGLE_SIN2", rust_physics_engine::particle_physics::WEAK_MIXING_ANGLE_SIN2)?; + m.add("STRONG_COUPLING", rust_physics_engine::particle_physics::STRONG_COUPLING)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_patterns.rs b/bindings/python/src/generated/m_patterns.rs new file mode 100644 index 0000000..c02bbf8 --- /dev/null +++ b/bindings/python/src/generated/m_patterns.rs @@ -0,0 +1,22 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + Ok(()) +} diff --git a/bindings/python/src/generated/m_patterns__aperiodic.rs b/bindings/python/src/generated/m_patterns__aperiodic.rs new file mode 100644 index 0000000..1e9718c --- /dev/null +++ b/bindings/python/src/generated/m_patterns__aperiodic.rs @@ -0,0 +1,227 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Deflates P2 (kite/dart) tiles `iterations` times; each round +/// shrinks edges by 1/φ. +/// +/// Rust: `patterns::aperiodic::penrose_p2_deflate` +#[pyfunction] +#[pyo3(name = "penrose_p2_deflate", signature = (tiles, iterations))] +pub fn pyfn_penrose_p2_deflate(tiles: Vec, iterations: usize) -> PyResult> { + let tiles = tiles.into_iter().map(|__e| __e.inner).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::aperiodic::penrose_p2_deflate(&tiles, iterations)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyPlacedTile { inner: __x }).collect::>()) +} + +/// Deflates P3 (rhomb) tiles `iterations` times. +/// +/// Rust: `patterns::aperiodic::penrose_p3_deflate` +#[pyfunction] +#[pyo3(name = "penrose_p3_deflate", signature = (tiles, iterations))] +pub fn pyfn_penrose_p3_deflate(tiles: Vec, iterations: usize) -> PyResult> { + let tiles = tiles.into_iter().map(|__e| __e.inner).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::aperiodic::penrose_p3_deflate(&tiles, iterations)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyPlacedTile { inner: __x }).collect::>()) +} + +/// P3 "sun" seed: ten half-thin triangles around the origin (rhomb +/// edges of length `radius`). +/// +/// Panics: +/// Panics unless `radius > 0`. +/// +/// Rust: `patterns::aperiodic::penrose_p3_sun` +#[pyfunction] +#[pyo3(name = "penrose_p3_sun", signature = (radius))] +pub fn pyfn_penrose_p3_sun(radius: f64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::aperiodic::penrose_p3_sun(radius)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyPlacedTile { inner: __x }).collect::>()) +} + +/// P3 "star" seed: the mirrored wheel, deflated once so full rhombs +/// exist. +/// +/// Panics: +/// Panics unless `radius > 0`. +/// +/// Rust: `patterns::aperiodic::penrose_p3_star` +#[pyfunction] +#[pyo3(name = "penrose_p3_star", signature = (radius))] +pub fn pyfn_penrose_p3_star(radius: f64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::aperiodic::penrose_p3_star(radius)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyPlacedTile { inner: __x }).collect::>()) +} + +/// P2 "sun" seed: five kites around the origin (kite long edges of +/// length `radius`). +/// +/// Panics: +/// Panics unless `radius > 0`. +/// +/// Rust: `patterns::aperiodic::penrose_p2_seed` +#[pyfunction] +#[pyo3(name = "penrose_p2_seed", signature = (radius))] +pub fn pyfn_penrose_p2_seed(radius: f64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::aperiodic::penrose_p2_seed(radius)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyPlacedTile { inner: __x }).collect::>()) +} + +/// Number ratio of thick rhombs (plus kites) to thin rhombs (plus +/// darts); converges to φ under deflation. +/// +/// Panics: +/// Panics when the denominator count is zero. +/// +/// Rust: `patterns::aperiodic::ratio_thick_to_thin` +#[pyfunction] +#[pyo3(name = "ratio_thick_to_thin", signature = (tiles))] +pub fn pyfn_ratio_thick_to_thin<'py>(py: Python<'py>, tiles: Vec) -> PyResult { + let tiles = tiles.into_iter().map(|__e| __e.inner).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::patterns::aperiodic::ratio_thick_to_thin(&tiles))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Penrose P3 rhombs by de Bruijn's pentagrid projection: five line +/// grids with the given offsets (their sum should be an integer for a +/// true Penrose tiling; generic values give a generalized tiling). +/// +/// Rust: `patterns::aperiodic::penrose_by_projection` +#[pyfunction] +#[pyo3(name = "penrose_by_projection", signature = (extent, offsets))] +pub fn pyfn_penrose_by_projection(extent: crate::generated::types::PyRect, offsets: Vec) -> PyResult> { + let extent = extent.inner; + let offsets = <[f64; 5]>::try_from(offsets).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 5 values, got {}", __v.len())))?; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::aperiodic::penrose_by_projection(&extent, offsets)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyPlacedTile { inner: __x }).collect::>()) +} + +/// Ammann-Beenker (octagonal) tiling of squares and 45° rhombs by the +/// four-grid de Bruijn dual. The `iterations` argument scales the +/// generated patch density (offsets stay fixed), kept for signature +/// compatibility with substitution-style generators. +/// +/// Rust: `patterns::aperiodic::ammann_beenker` +#[pyfunction] +#[pyo3(name = "ammann_beenker", signature = (extent, iterations))] +pub fn pyfn_ammann_beenker(extent: crate::generated::types::PyRect, iterations: usize) -> PyResult> { + let extent = extent.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::aperiodic::ammann_beenker(&extent, iterations)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyPolygon2 { inner: __x }).collect::>()) +} + +/// A patch of hat monotiles (Smith, Myers, Kaplan & Goodman-Strauss +/// 2023) built by `iterations` rounds of the H/T/P/F metatile +/// substitution; hats whose centroid lies in `extent` are returned +/// (hat edge lengths 1 and √3, fixed scale — grow the extent or the +/// iteration count for more tiles). +/// +/// Rust: `patterns::aperiodic::hat_monotile` +#[pyfunction] +#[pyo3(name = "hat_monotile", signature = (extent, iterations))] +pub fn pyfn_hat_monotile(extent: crate::generated::types::PyRect, iterations: usize) -> PyResult> { + let extent = extent.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::aperiodic::hat_monotile(&extent, iterations)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyPolygon2 { inner: __x }).collect::>()) +} + +/// A patch of spectre monotiles ("A chiral aperiodic monotile", +/// Smith, Myers, Kaplan & Goodman-Strauss 2023) built by `iterations` +/// substitution rounds; spectres with centroid inside `extent` are +/// returned (unit edge length, fixed scale). +/// +/// Rust: `patterns::aperiodic::spectre_monotile` +#[pyfunction] +#[pyo3(name = "spectre_monotile", signature = (extent, iterations))] +pub fn pyfn_spectre_monotile(extent: crate::generated::types::PyRect, iterations: usize) -> PyResult> { + let extent = extent.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::aperiodic::spectre_monotile(&extent, iterations)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyPolygon2 { inner: __x }).collect::>()) +} + +/// Pinwheel tiling (Radin 1994): 1:2:√5 right triangles subdivided +/// `iterations` times, seeded by two triangles covering the extent. +/// +/// Rust: `patterns::aperiodic::pinwheel` +#[pyfunction] +#[pyo3(name = "pinwheel", signature = (extent, iterations))] +pub fn pyfn_pinwheel(extent: crate::generated::types::PyRect, iterations: usize) -> PyResult> { + let extent = extent.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::aperiodic::pinwheel(&extent, iterations)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyPolygon2 { inner: __x }).collect::>()) +} + +/// The Fibonacci word: fixed point of a→ab, b→a. Returns the first +/// `n` letters (`true` = a). +/// +/// Rust: `patterns::aperiodic::fibonacci_word` +#[pyfunction] +#[pyo3(name = "fibonacci_word", signature = (n))] +pub fn pyfn_fibonacci_word<'py>(py: Python<'py>, n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::patterns::aperiodic::fibonacci_word(n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// 1-D quasicrystal by the canonical cut-and-project scheme: lattice +/// points of ℤ² whose perpendicular coordinate falls in the canonical +/// window are projected onto the line of the given slope. Returns +/// sorted positions with |x| <= extent. Irrational slopes give +/// aperiodic point sets (slope 1/φ gives the Fibonacci chain). +/// +/// Panics: +/// Panics unless `extent > 0`. +/// +/// Rust: `patterns::aperiodic::cut_and_project_1d` +#[pyfunction] +#[pyo3(name = "cut_and_project_1d", signature = (slope, extent))] +pub fn pyfn_cut_and_project_1d<'py>(py: Python<'py>, slope: f64, extent: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::patterns::aperiodic::cut_and_project_1d(slope, extent))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_penrose_p2_deflate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_penrose_p3_deflate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_penrose_p3_sun, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_penrose_p3_star, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_penrose_p2_seed, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ratio_thick_to_thin, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_penrose_by_projection, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ammann_beenker, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hat_monotile, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spectre_monotile, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pinwheel, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fibonacci_word, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cut_and_project_1d, m)?)?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_patterns__knots.rs b/bindings/python/src/generated/m_patterns__knots.rs new file mode 100644 index 0000000..77f9327 --- /dev/null +++ b/bindings/python/src/generated/m_patterns__knots.rs @@ -0,0 +1,365 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Point on the (p, q) torus knot at parameter `t` ∈ [0, 2π): +/// winds `p` times around the torus axis and `q` times through the +/// hole of the torus with radii `r_major` > `r_minor`. +/// +/// x = (R + r cos qt) cos pt, y = (R + r cos qt) sin pt, z = r sin qt. +/// +/// Panics: +/// Panics unless `p, q >= 1` and `r_major > r_minor > 0`. +/// +/// Rust: `patterns::knots::torus_knot` +#[pyfunction] +#[pyo3(name = "torus_knot", signature = (p, q, r_major, r_minor, t))] +pub fn pyfn_torus_knot(p: u32, q: u32, r_major: f64, r_minor: f64, t: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::knots::torus_knot(p, q, r_major, r_minor, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Closed polyline sampling of the (p, q) torus knot with `n` +/// vertices. +/// +/// Panics: +/// Panics unless `n >= 3` (and the `torus_knot` preconditions hold). +/// +/// Rust: `patterns::knots::torus_knot_curve` +#[pyfunction] +#[pyo3(name = "torus_knot_curve", signature = (p, q, r_major, r_minor, n))] +pub fn pyfn_torus_knot_curve(p: u32, q: u32, r_major: f64, r_minor: f64, n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::knots::torus_knot_curve(p, q, r_major, r_minor, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyline { inner: __v }) +} + +/// Point on a Lissajous knot: x = cos(nx t + φx), y = cos(ny t + φy), +/// z = cos(nz t + φz). Coprime frequencies with generic phases give +/// knotted closed curves (e.g. (3, 2, 7) with φ = (0.7, 0.2, 0)). +/// +/// Rust: `patterns::knots::lissajous_knot` +#[pyfunction] +#[pyo3(name = "lissajous_knot", signature = (nx, ny, nz, phase_x, phase_y, phase_z, t))] +pub fn pyfn_lissajous_knot(nx: u32, ny: u32, nz: u32, phase_x: f64, phase_y: f64, phase_z: f64, t: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::knots::lissajous_knot(nx, ny, nz, phase_x, phase_y, phase_z, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// The trefoil knot 3₁ in its symmetric parametrization: +/// (sin t + 2 sin 2t, cos t − 2 cos 2t, −sin 3t), t ∈ [0, 2π). +/// +/// Rust: `patterns::knots::trefoil` +#[pyfunction] +#[pyo3(name = "trefoil", signature = (t))] +pub fn pyfn_trefoil(t: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::knots::trefoil(t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// The figure-eight knot 4₁: +/// ((2 + cos 2t) cos 3t, (2 + cos 2t) sin 3t, sin 4t), t ∈ [0, 2π). +/// +/// Rust: `patterns::knots::figure_eight_knot` +#[pyfunction] +#[pyo3(name = "figure_eight_knot", signature = (t))] +pub fn pyfn_figure_eight_knot(t: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::knots::figure_eight_knot(t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// The cinquefoil (Solomon's seal) knot 5₁ = (2, 5) torus knot on +/// the torus R = 2, r = 1. +/// +/// Rust: `patterns::knots::cinquefoil` +#[pyfunction] +#[pyo3(name = "cinquefoil", signature = (t))] +pub fn pyfn_cinquefoil(t: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::knots::cinquefoil(t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Frenet frame of a curve at `t` by central differences with step +/// `h`: x axis = unit tangent T, y = principal normal N, z = +/// binormal B = T × N. `None` where the frame is undefined (zero +/// speed or zero curvature). +/// +/// Panics: +/// Panics unless `h > 0`. +/// +/// Rust: `patterns::knots::frenet_frame` +#[pyfunction] +#[pyo3(name = "frenet_frame", signature = (curve, t, h))] +pub fn pyfn_frenet_frame(curve: pyo3::Py, t: f64, h: f64) -> PyResult> { + let __cb_curve = std::rc::Rc::new(crate::runtime::Callback::new(curve)); + let curve = { let __cb = __cb_curve.clone(); move |__a0: f64| -> rust_physics_engine::math::Vec3 { { let __r = __cb.call::<_, crate::generated::types::PyVec3Arg>((__a0,), crate::generated::types::PyVec3Arg(rust_physics_engine::math::Vec3 { x: f64::NAN, y: f64::NAN, z: f64::NAN })); __r.0 } } }; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::knots::frenet_frame(&curve, t, h)); + crate::runtime::callback::check(&[&__cb_curve], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyFrame { inner: __x })) +} + +/// Discrete Frenet frames at every vertex of a polyline (tangent by +/// central difference, normal from the discrete curvature vector). +/// Straight stretches inherit the previous normal so the field stays +/// continuous. +/// +/// Panics: +/// Panics unless the polyline has at least 2 points. +/// +/// Rust: `patterns::knots::frenet_frames_polyline` +#[pyfunction] +#[pyo3(name = "frenet_frames_polyline", signature = (pl))] +pub fn pyfn_frenet_frames_polyline(pl: crate::generated::types::PyPolyline) -> PyResult> { + let pl = pl.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::knots::frenet_frames_polyline(&pl)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyFrame { inner: __x }).collect::>()) +} + +/// Rotation-minimizing frames along a polyline by the double +/// reflection method (Wang, Jüttler, Zheng & Liu 2008): each step +/// reflects the previous frame in the chord bisector plane and then +/// in the tangent bisector plane, which transports the normal with +/// no spurious twist (fourth-order accurate for smooth curves). +/// +/// Panics: +/// Panics unless the polyline has at least 2 points. +/// +/// Rust: `patterns::knots::parallel_transport_frames` +#[pyfunction] +#[pyo3(name = "parallel_transport_frames", signature = (pl))] +pub fn pyfn_parallel_transport_frames(pl: crate::generated::types::PyPolyline) -> PyResult> { + let pl = pl.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::knots::parallel_transport_frames(&pl)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyFrame { inner: __x }).collect::>()) +} + +/// Curvature and torsion of a curve at `t` by finite differences: +/// κ = |c′ × c″| / |c′|³ and τ = (c′ × c″)·c‴ / |c′ × c″|². +/// +/// Panics: +/// Panics unless `h > 0`. +/// +/// Rust: `patterns::knots::curvature_torsion` +#[pyfunction] +#[pyo3(name = "curvature_torsion", signature = (curve, t, h))] +pub fn pyfn_curvature_torsion(curve: pyo3::Py, t: f64, h: f64) -> PyResult<(f64, f64)> { + let __cb_curve = std::rc::Rc::new(crate::runtime::Callback::new(curve)); + let curve = { let __cb = __cb_curve.clone(); move |__a0: f64| -> rust_physics_engine::math::Vec3 { { let __r = __cb.call::<_, crate::generated::types::PyVec3Arg>((__a0,), crate::generated::types::PyVec3Arg(rust_physics_engine::math::Vec3 { x: f64::NAN, y: f64::NAN, z: f64::NAN })); __r.0 } } }; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::knots::curvature_torsion(&curve, t, h)); + crate::runtime::callback::check(&[&__cb_curve], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Total curvature of a polyline: the sum of exterior turning angles +/// between consecutive segments. For closed knotted curves this is +/// at least 4π (Fáry-Milnor). +/// +/// Rust: `patterns::knots::total_curvature` +#[pyfunction] +#[pyo3(name = "total_curvature", signature = (pl))] +pub fn pyfn_total_curvature(pl: crate::generated::types::PyPolyline) -> PyResult { + let pl = pl.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::knots::total_curvature(&pl)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Writhe of a closed polyline: the Gauss double integral +/// Wr = (1/4π) ∮∮ (dr₁ × dr₂)·(r₁ − r₂)/|r₁ − r₂|³, evaluated +/// exactly over segment pairs by the solid-angle formula. Planar +/// curves have writhe 0. +/// +/// Panics: +/// Panics unless the polyline is closed with at least 3 points. +/// +/// Rust: `patterns::knots::writhe` +#[pyfunction] +#[pyo3(name = "writhe", signature = (pl))] +pub fn pyfn_writhe(pl: crate::generated::types::PyPolyline) -> PyResult { + let pl = pl.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::knots::writhe(&pl)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Linking number of two closed polylines by the Gauss double sum; +/// the result is an integer for disjoint closed curves. +/// +/// Panics: +/// Panics unless both polylines are closed with at least 3 points. +/// +/// Rust: `patterns::knots::linking_number` +#[pyfunction] +#[pyo3(name = "linking_number", signature = (a, b))] +pub fn pyfn_linking_number(a: crate::generated::types::PyPolyline, b: crate::generated::types::PyPolyline) -> PyResult { + let a = a.inner; + let b = b.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::knots::linking_number(&a, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Number of crossings in the projection of the polyline along +/// `direction` (transverse double points of the diagram). +/// +/// Panics: +/// Panics unless the polyline is closed and `direction` is non-zero. +/// +/// Rust: `patterns::knots::crossing_number_projection` +#[pyfunction] +#[pyo3(name = "crossing_number_projection", signature = (pl, direction))] +pub fn pyfn_crossing_number_projection(pl: crate::generated::types::PyPolyline, direction: crate::generated::types::PyVec3Arg) -> PyResult { + let pl = pl.inner; + let direction = direction.0; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::knots::crossing_number_projection(&pl, direction)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Alexander polynomial coefficients (lowest degree first) computed +/// from the diagram of the closed polyline projected along +z. Arcs +/// run between undercrossings; each crossing contributes the +/// abelianized Fox-derivative row of its Wirtinger relation +/// (over-arc 1 − t, incoming under-arc t, outgoing under-arc −1 for +/// a positive crossing), one row and one column are deleted, and the +/// determinant is recovered by evaluation at integer points and +/// Lagrange interpolation. Normalized so the constant term is +/// non-zero and the leading coefficient positive; the unknot (no +/// crossings) gives `[1]`. +/// +/// The projection must be regular: only transverse double points. +/// Sample the curve finely enough that no segment participates in +/// two crossings with nearly equal positions. +/// +/// Panics: +/// Panics unless the polyline is closed with at least 3 points. +/// +/// Rust: `patterns::knots::alexander_polynomial_coeffs` +#[pyfunction] +#[pyo3(name = "alexander_polynomial_coeffs", signature = (pl))] +pub fn pyfn_alexander_polynomial_coeffs<'py>(py: Python<'py>, pl: crate::generated::types::PyPolyline) -> PyResult> { + let pl = pl.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::patterns::knots::alexander_polynomial_coeffs(&pl))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Sweeps a circle of `radius` along the polyline (delegates to +/// `mesh::generate::tube_along_polyline`). +/// +/// Rust: `patterns::knots::knot_tube` +#[pyfunction] +#[pyo3(name = "knot_tube", signature = (pl, radius, segments))] +pub fn pyfn_knot_tube(pl: crate::generated::types::PyPolyline, radius: f64, segments: usize) -> PyResult { + let pl = pl.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::knots::knot_tube(&pl, radius, segments)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMeshMesh { inner: __v }) +} + +/// Circular helix of given radius, pitch (rise per turn), and number +/// of turns, sampled at `n` points. +/// +/// Panics: +/// Panics unless `radius > 0`, `turns > 0`, and `n >= 2`. +/// +/// Rust: `patterns::knots::helix` +#[pyfunction] +#[pyo3(name = "helix", signature = (radius, pitch, turns, n))] +pub fn pyfn_helix(radius: f64, pitch: f64, turns: f64, n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::knots::helix(radius, pitch, turns, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyline { inner: __v }) +} + +/// Two helices on the same axis separated by `phase` radians (DNA +/// uses phase ≈ 2.1 rad for the minor/major groove asymmetry). +/// +/// Rust: `patterns::knots::double_helix` +#[pyfunction] +#[pyo3(name = "double_helix", signature = (radius, pitch, turns, n, phase))] +pub fn pyfn_double_helix(radius: f64, pitch: f64, turns: f64, n: usize, phase: f64) -> PyResult<(crate::generated::types::PyPolyline, crate::generated::types::PyPolyline)> { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::knots::double_helix(radius, pitch, turns, n, phase)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((crate::generated::types::PyPolyline { inner: __v.0 }, crate::generated::types::PyPolyline { inner: __v.1 })) +} + +/// Viviani's curve: the intersection of the sphere of radius 2a with +/// the cylinder of radius a tangent to its vertical axis: +/// (a(1 + cos t), a sin t, 2a sin(t/2)), t ∈ [0, 4π) for the full +/// figure-eight. +/// +/// Panics: +/// Panics unless `a > 0`. +/// +/// Rust: `patterns::knots::viviani_curve` +#[pyfunction] +#[pyo3(name = "viviani_curve", signature = (a, t))] +pub fn pyfn_viviani_curve(a: f64, t: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::knots::viviani_curve(a, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Tennis-ball seam curve: (a cos t + b cos 3t, a sin t − b sin 3t, +/// 2 √(ab) sin 2t) lies on the sphere of radius a + b. +/// +/// Panics: +/// Panics unless `a, b > 0`. +/// +/// Rust: `patterns::knots::tennis_ball_curve` +#[pyfunction] +#[pyo3(name = "tennis_ball_curve", signature = (a, b, t))] +pub fn pyfn_tennis_ball_curve(a: f64, b: f64, t: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::knots::tennis_ball_curve(a, b, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_torus_knot, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_torus_knot_curve, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lissajous_knot, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_trefoil, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_figure_eight_knot, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cinquefoil, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_frenet_frame, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_frenet_frames_polyline, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_parallel_transport_frames, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_curvature_torsion, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_total_curvature, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_writhe, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_linking_number, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_crossing_number_projection, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_alexander_polynomial_coeffs, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_knot_tube, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_helix, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_double_helix, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_viviani_curve, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tennis_ball_curve, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_patterns__packing.rs b/bindings/python/src/generated/m_patterns__packing.rs new file mode 100644 index 0000000..a3c8294 --- /dev/null +++ b/bindings/python/src/generated/m_patterns__packing.rs @@ -0,0 +1,312 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Descartes circle theorem: curvatures of the two circles tangent to +/// three mutually tangent circles with curvatures k1, k2, k3: +/// k4 = k1 + k2 + k3 ± 2√(k1k2 + k2k3 + k3k1). +/// +/// Panics: +/// Panics when the discriminant is negative (not a tangent triple). +/// +/// Rust: `patterns::packing::descartes_fourth_circle` +#[pyfunction] +#[pyo3(name = "descartes_fourth_circle", signature = (k1, k2, k3))] +pub fn pyfn_descartes_fourth_circle(k1: f64, k2: f64, k3: f64) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::packing::descartes_fourth_circle(k1, k2, k3)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Apollonian gasket inside `outer`: the two seed circles have +/// curvatures `k2`, `k3` (both tangent to the outer circle and each +/// other, placed on the horizontal axis), recursively filled to +/// `depth`. Returns all circles including the outer and seeds. +/// +/// Panics: +/// Panics unless the curvatures are compatible: `k2, k3 > 1/R` and +/// `1/k2 + 1/k3 = 2R - ...` — concretely both seed radii must fit: +/// `1/k2 + 1/k3 == R` is required for a tangent chain on the axis. +/// +/// Rust: `patterns::packing::apollonian_gasket` +#[pyfunction] +#[pyo3(name = "apollonian_gasket", signature = (outer, k2, k3, depth))] +pub fn pyfn_apollonian_gasket(outer: crate::generated::types::PyCircle, k2: f64, k3: f64, depth: usize) -> PyResult> { + let outer = outer.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::packing::apollonian_gasket(&outer, k2, k3, depth)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyCircle { inner: __x }).collect::>()) +} + +/// The classic integral Apollonian gasket with curvatures +/// (−1, 2, 2, 3, 3): outer unit circle, two half circles. +/// +/// Rust: `patterns::packing::apollonian_gasket_integral` +#[pyfunction] +#[pyo3(name = "apollonian_gasket_integral", signature = (depth))] +pub fn pyfn_apollonian_gasket_integral(depth: usize) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::packing::apollonian_gasket_integral(depth)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyCircle { inner: __x }).collect::>()) +} + +/// Greedy random packing: for each radius in order, up to `attempts` +/// random placements inside the polygon (respecting the boundary and +/// previously placed circles); radii that do not fit are skipped. +/// +/// Rust: `patterns::packing::circle_pack_greedy` +#[pyfunction] +#[pyo3(name = "circle_pack_greedy", signature = (region, radii, rng, attempts))] +pub fn pyfn_circle_pack_greedy(region: crate::generated::types::PyPolygon2, radii: Vec, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>, attempts: usize) -> PyResult> { + let region = region.inner; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::packing::circle_pack_greedy(®ion, &radii, &mut rng.inner, attempts)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyCircle { inner: __x }).collect::>()) +} + +/// Hexagonal (densest) circle packing: circles of radius `r` whose +/// centers lie in the half-open region. +/// +/// Panics: +/// Panics unless `r > 0`. +/// +/// Rust: `patterns::packing::circle_pack_hex` +#[pyfunction] +#[pyo3(name = "circle_pack_hex", signature = (region, r))] +pub fn pyfn_circle_pack_hex(region: crate::generated::types::PyRect, r: f64) -> PyResult> { + let region = region.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::packing::circle_pack_hex(®ion, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyCircle { inner: __x }).collect::>()) +} + +/// Square-lattice circle packing (centers in the half-open region). +/// +/// Panics: +/// Panics unless `r > 0`. +/// +/// Rust: `patterns::packing::circle_pack_square` +#[pyfunction] +#[pyo3(name = "circle_pack_square", signature = (region, r))] +pub fn pyfn_circle_pack_square(region: crate::generated::types::PyRect, r: f64) -> PyResult> { + let region = region.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::packing::circle_pack_square(®ion, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyCircle { inner: __x }).collect::>()) +} + +/// Relaxes overlapping circles by symmetric push-apart steps, keeping +/// centers at least their radius away from the rectangle boundary. +/// +/// Rust: `patterns::packing::circle_pack_relax` +#[pyfunction] +#[pyo3(name = "circle_pack_relax", signature = (circles, region, iterations))] +pub fn pyfn_circle_pack_relax<'py>(circles: pyo3::Bound<'py, pyo3::PyAny>, region: crate::generated::types::PyRect, iterations: usize) -> PyResult<()> { + let mut circles__v: Vec = circles.extract::>()?.into_iter().map(|__e| __e.inner).collect(); + let region = region.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::packing::circle_pack_relax(&mut circles__v, ®ion, iterations)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + crate::runtime::coerce::write_back_objects(&circles, circles__v.into_iter().map(|__e| crate::generated::types::PyCircle { inner: __e }).collect::>())?; + Ok(()) +} + +/// Face-centered-cubic sphere packing (density π/(3√2) ≈ 0.7405), +/// centers in the half-open box. +/// +/// Panics: +/// Panics unless `r > 0`. +/// +/// Rust: `patterns::packing::sphere_pack_fcc` +#[pyfunction] +#[pyo3(name = "sphere_pack_fcc", signature = (region, r))] +pub fn pyfn_sphere_pack_fcc(region: crate::generated::types::PyAabb, r: f64) -> PyResult> { + let region = region.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::packing::sphere_pack_fcc(®ion, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PySphere { inner: __x }).collect::>()) +} + +/// Hexagonal-close-packed spheres (same density as FCC), ABAB layer +/// stacking along z; centers in the half-open box. +/// +/// Panics: +/// Panics unless `r > 0`. +/// +/// Rust: `patterns::packing::sphere_pack_hcp` +#[pyfunction] +#[pyo3(name = "sphere_pack_hcp", signature = (region, r))] +pub fn pyfn_sphere_pack_hcp(region: crate::generated::types::PyAabb, r: f64) -> PyResult> { + let region = region.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::packing::sphere_pack_hcp(®ion, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PySphere { inner: __x }).collect::>()) +} + +/// Body-centered-cubic spheres (density π√3/8 ≈ 0.6802), centers in +/// the half-open box. +/// +/// Panics: +/// Panics unless `r > 0`. +/// +/// Rust: `patterns::packing::sphere_pack_bcc` +#[pyfunction] +#[pyo3(name = "sphere_pack_bcc", signature = (region, r))] +pub fn pyfn_sphere_pack_bcc(region: crate::generated::types::PyAabb, r: f64) -> PyResult> { + let region = region.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::packing::sphere_pack_bcc(®ion, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PySphere { inner: __x }).collect::>()) +} + +/// Random sequential adsorption: spheres placed uniformly at random, +/// rejected on overlap, until `max_attempts` placements fail +/// (saturation density ≈ 0.38). +/// +/// Panics: +/// Panics unless `r > 0`. +/// +/// Rust: `patterns::packing::sphere_pack_random_sequential` +#[pyfunction] +#[pyo3(name = "sphere_pack_random_sequential", signature = (region, r, rng, max_attempts))] +pub fn pyfn_sphere_pack_random_sequential(region: crate::generated::types::PyAabb, r: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>, max_attempts: usize) -> PyResult> { + let region = region.inner; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::packing::sphere_pack_random_sequential(®ion, r, &mut rng.inner, max_attempts)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PySphere { inner: __x }).collect::>()) +} + +/// Fraction of the region area covered, counting each circle's full +/// area (consistent with the centers-in-region conventions above). +/// +/// Rust: `patterns::packing::packing_density_2d` +#[pyfunction] +#[pyo3(name = "packing_density_2d", signature = (circles, region))] +pub fn pyfn_packing_density_2d<'py>(py: Python<'py>, circles: Vec, region: crate::generated::types::PyRect) -> PyResult { + let circles = circles.into_iter().map(|__e| __e.inner).collect::>(); + let region = region.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::patterns::packing::packing_density_2d(&circles, ®ion))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Fraction of the box volume covered, counting each sphere's full +/// volume. +/// +/// Rust: `patterns::packing::packing_density_3d` +#[pyfunction] +#[pyo3(name = "packing_density_3d", signature = (spheres, region))] +pub fn pyfn_packing_density_3d<'py>(py: Python<'py>, spheres: Vec, region: crate::generated::types::PyAabb) -> PyResult { + let spheres = spheres.into_iter().map(|__e| __e.inner).collect::>(); + let region = region.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::patterns::packing::packing_density_3d(&spheres, ®ion))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Doyle spiral circle packing with `p` and `q` arms: each circle is +/// tangent to its neighbors along both spiral directions. The moduli +/// of the two spiral generators are solved numerically (Newton with +/// numeric Jacobian) so all three tangency ratios agree. +/// +/// Panics: +/// Panics unless `1 <= p < q` and the solver converges. +/// +/// Rust: `patterns::packing::doyle_spiral` +#[pyfunction] +#[pyo3(name = "doyle_spiral", signature = (p, q, count))] +pub fn pyfn_doyle_spiral(p: u32, q: u32, count: usize) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::packing::doyle_spiral(p, q, count)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyCircle { inner: __x }).collect::>()) +} + +/// Ford circles: for every reduced fraction p/q with +/// `q <= max_denominator` in [0, 1], the circle tangent to the x axis +/// at p/q with radius 1/(2q²). +/// +/// Panics: +/// Panics unless `max_denominator >= 1`. +/// +/// Rust: `patterns::packing::ford_circles` +#[pyfunction] +#[pyo3(name = "ford_circles", signature = (max_denominator))] +pub fn pyfn_ford_circles(max_denominator: u32) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::packing::ford_circles(max_denominator)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyCircle { inner: __x }).collect::>()) +} + +/// Steiner chain of `n` circles in the annular region between `inner` +/// and `outer` (inner strictly inside outer). Returns `None` when the +/// pair does not admit a closed chain of exactly `n` circles +/// (Steiner's porism: feasibility depends only on the inversive +/// distance). +/// +/// Panics: +/// Panics unless `n >= 3` and `inner` is strictly inside `outer`. +/// +/// Rust: `patterns::packing::steiner_chain` +#[pyfunction] +#[pyo3(name = "steiner_chain", signature = (outer, inner, n))] +pub fn pyfn_steiner_chain(outer: crate::generated::types::PyCircle, inner: crate::generated::types::PyCircle, n: usize) -> PyResult>> { + let outer = outer.inner; + let inner = inner.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::packing::steiner_chain(&outer, &inner, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x.into_iter().map(|__x| crate::generated::types::PyCircle { inner: __x }).collect::>())) +} + +/// The problem of Apollonius: circles tangent to three given circles +/// (up to 8 solutions, one per internal/external tangency sign +/// choice). Solved by reducing the tangency equations to a linear +/// system plus a quadratic in the radius. +/// +/// Rust: `patterns::packing::tangent_circles_to_three` +#[pyfunction] +#[pyo3(name = "tangent_circles_to_three", signature = (c1, c2, c3))] +pub fn pyfn_tangent_circles_to_three(c1: crate::generated::types::PyCircle, c2: crate::generated::types::PyCircle, c3: crate::generated::types::PyCircle) -> PyResult> { + let c1 = c1.inner; + let c2 = c2.inner; + let c3 = c3.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::packing::tangent_circles_to_three(&c1, &c2, &c3)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyCircle { inner: __x }).collect::>()) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_descartes_fourth_circle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_apollonian_gasket, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_apollonian_gasket_integral, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_circle_pack_greedy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_circle_pack_hex, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_circle_pack_square, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_circle_pack_relax, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sphere_pack_fcc, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sphere_pack_hcp, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sphere_pack_bcc, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sphere_pack_random_sequential, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_packing_density_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_packing_density_3d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_doyle_spiral, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ford_circles, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_steiner_chain, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tangent_circles_to_three, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_patterns__phyllotaxis.rs b/bindings/python/src/generated/m_patterns__phyllotaxis.rs new file mode 100644 index 0000000..ea0f551 --- /dev/null +++ b/bindings/python/src/generated/m_patterns__phyllotaxis.rs @@ -0,0 +1,306 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Vogel's sunflower model (Vogel 1979): floret i at radius +/// `scale · √i` and angle `i · GOLDEN_ANGLE`. +/// +/// Rust: `patterns::phyllotaxis::vogel_sunflower` +#[pyfunction] +#[pyo3(name = "vogel_sunflower", signature = (n, scale))] +pub fn pyfn_vogel_sunflower(n: usize, scale: f64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::phyllotaxis::vogel_sunflower(n, scale)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// Vogel model with an arbitrary divergence angle. +/// +/// Rust: `patterns::phyllotaxis::vogel_sunflower_angle` +#[pyfunction] +#[pyo3(name = "vogel_sunflower_angle", signature = (n, scale, angle))] +pub fn pyfn_vogel_sunflower_angle(n: usize, scale: f64, angle: f64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::phyllotaxis::vogel_sunflower_angle(n, scale, angle)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// Near-uniform points on the unit sphere: latitude strips of equal +/// area, longitude advanced by the golden angle. +/// +/// Panics: +/// Panics unless `n >= 1`. +/// +/// Rust: `patterns::phyllotaxis::fibonacci_sphere` +#[pyfunction] +#[pyo3(name = "fibonacci_sphere", signature = (n))] +pub fn pyfn_fibonacci_sphere(n: usize) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::phyllotaxis::fibonacci_sphere(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) +} + +/// Near-uniform points on the unit disk (Vogel pattern scaled so the +/// n-th floret reaches radius 1). +/// +/// Panics: +/// Panics unless `n >= 1`. +/// +/// Rust: `patterns::phyllotaxis::fibonacci_disk` +#[pyfunction] +#[pyo3(name = "fibonacci_disk", signature = (n))] +pub fn pyfn_fibonacci_disk(n: usize) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::phyllotaxis::fibonacci_disk(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// Near-uniform points on the upper (y > 0) unit hemisphere. +/// +/// Panics: +/// Panics unless `n >= 1`. +/// +/// Rust: `patterns::phyllotaxis::fibonacci_hemisphere` +#[pyfunction] +#[pyo3(name = "fibonacci_hemisphere", signature = (n))] +pub fn pyfn_fibonacci_hemisphere(n: usize) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::phyllotaxis::fibonacci_hemisphere(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) +} + +/// Golden spiral: logarithmic spiral growing by φ every quarter turn, +/// starting radius `a`. +/// +/// Panics: +/// Panics unless `turns > 0`, `points_per_turn >= 1`, `a > 0`. +/// +/// Rust: `patterns::phyllotaxis::golden_spiral` +#[pyfunction] +#[pyo3(name = "golden_spiral", signature = (turns, points_per_turn, a))] +pub fn pyfn_golden_spiral(turns: f64, points_per_turn: usize, a: f64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::phyllotaxis::golden_spiral(turns, points_per_turn, a)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// Archimedean spiral r = a + bθ sampled on `n` points over +/// θ ∈ [0, theta_max]. +/// +/// Panics: +/// Panics unless `n >= 2`. +/// +/// Rust: `patterns::phyllotaxis::archimedean_spiral` +#[pyfunction] +#[pyo3(name = "archimedean_spiral", signature = (a, b, theta_max, n))] +pub fn pyfn_archimedean_spiral(a: f64, b: f64, theta_max: f64, n: usize) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::phyllotaxis::archimedean_spiral(a, b, theta_max, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// Logarithmic spiral r = a e^{bθ}. +/// +/// Panics: +/// Panics unless `n >= 2`. +/// +/// Rust: `patterns::phyllotaxis::logarithmic_spiral` +#[pyfunction] +#[pyo3(name = "logarithmic_spiral", signature = (a, b, theta_max, n))] +pub fn pyfn_logarithmic_spiral(a: f64, b: f64, theta_max: f64, n: usize) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::phyllotaxis::logarithmic_spiral(a, b, theta_max, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// Fermat (parabolic) spiral r = a √θ. +/// +/// Panics: +/// Panics unless `n >= 2` and `theta_max >= 0`. +/// +/// Rust: `patterns::phyllotaxis::fermat_spiral` +#[pyfunction] +#[pyo3(name = "fermat_spiral", signature = (a, theta_max, n))] +pub fn pyfn_fermat_spiral(a: f64, theta_max: f64, n: usize) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::phyllotaxis::fermat_spiral(a, theta_max, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// Hyperbolic spiral r = a/θ over `theta_range` (which must exclude +/// 0). +/// +/// Panics: +/// Panics unless `n >= 2` and the range excludes zero. +/// +/// Rust: `patterns::phyllotaxis::hyperbolic_spiral` +#[pyfunction] +#[pyo3(name = "hyperbolic_spiral", signature = (a, theta_range, n))] +pub fn pyfn_hyperbolic_spiral(a: f64, theta_range: (f64, f64), n: usize) -> PyResult> { + let theta_range = (theta_range.0, theta_range.1); + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::phyllotaxis::hyperbolic_spiral(a, theta_range, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// Lituus r = a/√θ over `theta_range` (positive). +/// +/// Panics: +/// Panics unless `n >= 2` and the range is positive. +/// +/// Rust: `patterns::phyllotaxis::lituus` +#[pyfunction] +#[pyo3(name = "lituus", signature = (a, theta_range, n))] +pub fn pyfn_lituus(a: f64, theta_range: (f64, f64), n: usize) -> PyResult> { + let theta_range = (theta_range.0, theta_range.1); + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::phyllotaxis::lituus(a, theta_range, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// Euler spiral (clothoid): curvature grows linearly with arclength, +/// κ(s) = s. Points via composite-Simpson evaluation of the Fresnel +/// integrals x = ∫cos(t²/2)dt, y = ∫sin(t²/2)dt. +/// +/// Panics: +/// Panics unless `n >= 2` and `length > 0`. +/// +/// Rust: `patterns::phyllotaxis::euler_spiral` +#[pyfunction] +#[pyo3(name = "euler_spiral", signature = (length, n))] +pub fn pyfn_euler_spiral(length: f64, n: usize) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::phyllotaxis::euler_spiral(length, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// Spiral of Theodorus (square-root spiral): `n` right triangles with +/// unit legs; vertex k lies at radius √(k+1). +/// +/// Panics: +/// Panics unless `n >= 1`. +/// +/// Rust: `patterns::phyllotaxis::spiral_of_theodorus` +#[pyfunction] +#[pyo3(name = "spiral_of_theodorus", signature = (n))] +pub fn pyfn_spiral_of_theodorus(n: usize) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::phyllotaxis::spiral_of_theodorus(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// Conical spiral: radius `a + b t`, height `h t`, `turns` full turns +/// over t ∈ [0, 1], axis y. +/// +/// Panics: +/// Panics unless `n >= 2`. +/// +/// Rust: `patterns::phyllotaxis::conical_spiral` +#[pyfunction] +#[pyo3(name = "conical_spiral", signature = (a, b, h, turns, n))] +pub fn pyfn_conical_spiral(a: f64, b: f64, h: f64, turns: f64, n: usize) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::phyllotaxis::conical_spiral(a, b, h, turns, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) +} + +/// Spherical spiral on the unit sphere: polar angle sweeps 0..π while +/// the azimuth makes `turns` turns (axis y). +/// +/// Panics: +/// Panics unless `n >= 2`. +/// +/// Rust: `patterns::phyllotaxis::spherical_spiral` +#[pyfunction] +#[pyo3(name = "spherical_spiral", signature = (turns, n))] +pub fn pyfn_spherical_spiral(turns: f64, n: usize) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::phyllotaxis::spherical_spiral(turns, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) +} + +/// Detects the two dominant parastichy (visible spiral) families: +/// the two most common index differences between each floret and its +/// nearest neighbors, returned ascending. For golden-angle patterns +/// these are consecutive Fibonacci numbers. +/// +/// Panics: +/// Panics unless at least 8 points are given. +/// +/// Rust: `patterns::phyllotaxis::parastichy_counts` +#[pyfunction] +#[pyo3(name = "parastichy_counts", signature = (points))] +pub fn pyfn_parastichy_counts<'py>(py: Python<'py>, points: Vec) -> PyResult<(usize, usize)> { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::patterns::phyllotaxis::parastichy_counts(&points))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Helical (cylindrical) phyllotaxis: point i at height `i · rise` +/// and azimuth `i · angle` on a cylinder of the given radius. +/// +/// Rust: `patterns::phyllotaxis::cylinder_phyllotaxis` +#[pyfunction] +#[pyo3(name = "cylinder_phyllotaxis", signature = (n, rise, angle, radius))] +pub fn pyfn_cylinder_phyllotaxis(n: usize, rise: f64, angle: f64, radius: f64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::phyllotaxis::cylinder_phyllotaxis(n, rise, angle, radius)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) +} + +/// Logarithmic-spiral arc from `a` to `b` about the origin: `n` +/// points (inclusive) interpolating radius geometrically and angle +/// linearly (shortest way around). +/// +/// Panics: +/// Panics unless `n >= 2` and both points are away from the origin. +/// +/// Rust: `patterns::phyllotaxis::spiral_interpolate_sequence` +#[pyfunction] +#[pyo3(name = "spiral_interpolate_sequence", signature = (a, b, n))] +pub fn pyfn_spiral_interpolate_sequence(a: crate::generated::types::PyVec2Arg, b: crate::generated::types::PyVec2Arg, n: usize) -> PyResult> { + let a = a.0; + let b = b.0; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::phyllotaxis::spiral_interpolate_sequence(a, b, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_vogel_sunflower, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_vogel_sunflower_angle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fibonacci_sphere, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fibonacci_disk, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fibonacci_hemisphere, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_golden_spiral, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_archimedean_spiral, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_logarithmic_spiral, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fermat_spiral, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hyperbolic_spiral, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lituus, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_euler_spiral, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spiral_of_theodorus, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_conical_spiral, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spherical_spiral, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_parastichy_counts, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cylinder_phyllotaxis, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spiral_interpolate_sequence, m)?)?; + m.add("GOLDEN_ANGLE", rust_physics_engine::patterns::phyllotaxis::GOLDEN_ANGLE)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_patterns__polygon_ops.rs b/bindings/python/src/generated/m_patterns__polygon_ops.rs new file mode 100644 index 0000000..a4bd83e --- /dev/null +++ b/bindings/python/src/generated/m_patterns__polygon_ops.rs @@ -0,0 +1,529 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Ear-clipping triangulation of a simple polygon. Indices refer to +/// the polygon's own vertex order (clockwise input is handled). +/// +/// Errors: +/// `GeomError::InvalidArgument` for fewer than 3 vertices; +/// `GeomError::Degenerate` for zero area or self-intersecting input. +/// +/// Rust: `patterns::polygon_ops::triangulate_ear_clipping` +#[pyfunction] +#[pyo3(name = "triangulate_ear_clipping", signature = (poly))] +pub fn pyfn_triangulate_ear_clipping<'py>(py: Python<'py>, poly: crate::generated::types::PyPolygon2) -> PyResult>> { + let poly = poly.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::patterns::polygon_ops::triangulate_ear_clipping(&poly))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| __x.to_vec()).collect::>()) +} + +/// Triangulates a polygon with holes by bridging each hole to the +/// outer boundary (rightmost-vertex visibility bridge) and ear +/// clipping the result. Returns the combined vertex list (outer, then +/// holes in bridging order, with two duplicated bridge vertices per +/// hole) and triangles into it. +/// +/// Errors: +/// Propagates the failure modes of `triangulate_ear_clipping`; +/// holes must be strictly inside the outer polygon and disjoint. +/// +/// Rust: `patterns::polygon_ops::triangulate_with_holes` +#[pyfunction] +#[pyo3(name = "triangulate_with_holes", signature = (outer, holes))] +pub fn pyfn_triangulate_with_holes(outer: crate::generated::types::PyPolygon2, holes: Vec) -> PyResult<(Vec, Vec>)> { + let outer = outer.inner; + let holes = holes.into_iter().map(|__e| __e.inner).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polygon_ops::triangulate_with_holes(&outer, &holes)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>(), __v.1.into_iter().map(|__x| __x.to_vec()).collect::>())) +} + +/// Ramer-Douglas-Peucker polyline simplification: keeps points whose +/// deviation exceeds `epsilon`. Endpoints are always kept. +/// +/// Panics: +/// Panics unless `epsilon >= 0`. +/// +/// Rust: `patterns::polygon_ops::simplify_douglas_peucker` +#[pyfunction] +#[pyo3(name = "simplify_douglas_peucker", signature = (pts, epsilon))] +pub fn pyfn_simplify_douglas_peucker(pts: Vec, epsilon: f64) -> PyResult> { + let pts = pts.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polygon_ops::simplify_douglas_peucker(&pts, epsilon)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// Visvalingam-Whyatt simplification: repeatedly removes the interior +/// point spanning the smallest triangle until every remaining point +/// spans at least `min_area`. +/// +/// Panics: +/// Panics unless `min_area >= 0`. +/// +/// Rust: `patterns::polygon_ops::simplify_visvalingam` +#[pyfunction] +#[pyo3(name = "simplify_visvalingam", signature = (pts, min_area))] +pub fn pyfn_simplify_visvalingam(pts: Vec, min_area: f64) -> PyResult> { + let pts = pts.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polygon_ops::simplify_visvalingam(&pts, min_area)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// Offsets a simple polygon outward (`distance > 0`) or inward +/// (`distance < 0`), joining corners by `join`. Self-intersections of +/// the raw offset ring (spikes collapsing under inset, etc.) are +/// resolved by splitting into simple loops and keeping +/// counterclockwise ones; an inset larger than the inradius returns +/// an empty vector. Input orientation does not matter; outputs are +/// counterclockwise. +/// +/// Panics: +/// Panics unless the polygon has >= 3 vertices and `distance != 0`. +/// +/// Rust: `patterns::polygon_ops::offset_polygon` +#[pyfunction] +#[pyo3(name = "offset_polygon", signature = (poly, distance, join))] +pub fn pyfn_offset_polygon(poly: crate::generated::types::PyPolygon2, distance: f64, join: crate::generated::types::PyJoinStyle) -> PyResult> { + let poly = poly.inner; + let join = join.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polygon_ops::offset_polygon(&poly, distance, join)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyPolygon2 { inner: __x }).collect::>()) +} + +/// Minkowski sum of two convex polygons by the edge-merge +/// (convolution) construction; output is convex and counterclockwise. +/// +/// Panics: +/// Panics unless both polygons are convex with >= 3 vertices. +/// +/// Rust: `patterns::polygon_ops::minkowski_sum_convex` +#[pyfunction] +#[pyo3(name = "minkowski_sum_convex", signature = (a, b))] +pub fn pyfn_minkowski_sum_convex(a: crate::generated::types::PyPolygon2, b: crate::generated::types::PyPolygon2) -> PyResult { + let a = a.inner; + let b = b.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polygon_ops::minkowski_sum_convex(&a, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolygon2 { inner: __v }) +} + +/// Minkowski sum of two simple polygons via convex decomposition: +/// pairwise convex sums, unioned together. +/// +/// Rust: `patterns::polygon_ops::minkowski_sum` +#[pyfunction] +#[pyo3(name = "minkowski_sum", signature = (a, b))] +pub fn pyfn_minkowski_sum(a: crate::generated::types::PyPolygon2, b: crate::generated::types::PyPolygon2) -> PyResult> { + let a = a.inner; + let b = b.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polygon_ops::minkowski_sum(&a, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyPolygon2 { inner: __x }).collect::>()) +} + +/// Union of two simple polygons. Outer loops come out +/// counterclockwise; holes (e.g. two C shapes closing a ring) +/// clockwise. +/// +/// Rust: `patterns::polygon_ops::boolean_union` +#[pyfunction] +#[pyo3(name = "boolean_union", signature = (a, b))] +pub fn pyfn_boolean_union(a: crate::generated::types::PyPolygon2, b: crate::generated::types::PyPolygon2) -> PyResult> { + let a = a.inner; + let b = b.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polygon_ops::boolean_union(&a, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyPolygon2 { inner: __x }).collect::>()) +} + +/// Intersection of two simple polygons (possibly several pieces). +/// +/// Rust: `patterns::polygon_ops::boolean_intersection` +#[pyfunction] +#[pyo3(name = "boolean_intersection", signature = (a, b))] +pub fn pyfn_boolean_intersection(a: crate::generated::types::PyPolygon2, b: crate::generated::types::PyPolygon2) -> PyResult> { + let a = a.inner; + let b = b.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polygon_ops::boolean_intersection(&a, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyPolygon2 { inner: __x }).collect::>()) +} + +/// Difference a − b; a hole fully inside `a` is returned as a +/// clockwise loop. +/// +/// Rust: `patterns::polygon_ops::boolean_difference` +#[pyfunction] +#[pyo3(name = "boolean_difference", signature = (a, b))] +pub fn pyfn_boolean_difference(a: crate::generated::types::PyPolygon2, b: crate::generated::types::PyPolygon2) -> PyResult> { + let a = a.inner; + let b = b.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polygon_ops::boolean_difference(&a, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyPolygon2 { inner: __x }).collect::>()) +} + +/// Symmetric difference: (a − b) ∪ (b − a), returned as the two +/// difference loop sets concatenated. +/// +/// Rust: `patterns::polygon_ops::boolean_xor` +#[pyfunction] +#[pyo3(name = "boolean_xor", signature = (a, b))] +pub fn pyfn_boolean_xor(a: crate::generated::types::PyPolygon2, b: crate::generated::types::PyPolygon2) -> PyResult> { + let a = a.inner; + let b = b.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polygon_ops::boolean_xor(&a, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyPolygon2 { inner: __x }).collect::>()) +} + +/// Sutherland-Hodgman clipping of an arbitrary subject polygon +/// against a convex clip polygon. +/// +/// Panics: +/// Panics unless `clip` is convex with >= 3 vertices. +/// +/// Rust: `patterns::polygon_ops::clip_polygon_convex` +#[pyfunction] +#[pyo3(name = "clip_polygon_convex", signature = (subject, clip))] +pub fn pyfn_clip_polygon_convex(subject: crate::generated::types::PyPolygon2, clip: crate::generated::types::PyPolygon2) -> PyResult { + let subject = subject.inner; + let clip = clip.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polygon_ops::clip_polygon_convex(&subject, &clip)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolygon2 { inner: __v }) +} + +/// Clips a polygon to an axis-aligned rectangle +/// (Sutherland-Hodgman). +/// +/// Rust: `patterns::polygon_ops::clip_polygon_rect` +#[pyfunction] +#[pyo3(name = "clip_polygon_rect", signature = (subject, rect))] +pub fn pyfn_clip_polygon_rect(subject: crate::generated::types::PyPolygon2, rect: crate::generated::types::PyRect) -> PyResult { + let subject = subject.inner; + let rect = rect.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polygon_ops::clip_polygon_rect(&subject, &rect)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolygon2 { inner: __v }) +} + +/// Liang-Barsky segment clipping against a rectangle; `None` when the +/// segment misses it entirely. +/// +/// Rust: `patterns::polygon_ops::clip_line_rect` +#[pyfunction] +#[pyo3(name = "clip_line_rect", signature = (a, b, rect))] +pub fn pyfn_clip_line_rect(a: crate::generated::types::PyVec2Arg, b: crate::generated::types::PyVec2Arg, rect: crate::generated::types::PyRect) -> PyResult> { + let a = a.0; + let b = b.0; + let rect = rect.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polygon_ops::clip_line_rect(a, b, &rect)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| (crate::generated::types::PyVec2 { inner: __x.0 }, crate::generated::types::PyVec2 { inner: __x.1 }))) +} + +/// Hertel-Mehlhorn convex decomposition: triangulate, then greedily +/// remove inessential diagonals. At most 4x the optimal piece count. +/// +/// Panics: +/// Panics when the polygon cannot be triangulated (see +/// `triangulate_ear_clipping` for the failure modes). +/// +/// Rust: `patterns::polygon_ops::convex_decomposition` +#[pyfunction] +#[pyo3(name = "convex_decomposition", signature = (poly))] +pub fn pyfn_convex_decomposition(poly: crate::generated::types::PyPolygon2) -> PyResult> { + let poly = poly.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polygon_ops::convex_decomposition(&poly)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyPolygon2 { inner: __x }).collect::>()) +} + +/// Convex hull by Andrew's monotone chain, counterclockwise, minimal +/// vertex set (collinear points dropped). +/// +/// Panics: +/// Panics with fewer than 3 input points. +/// +/// Rust: `patterns::polygon_ops::convex_hull_2d` +#[pyfunction] +#[pyo3(name = "convex_hull_2d", signature = (points))] +pub fn pyfn_convex_hull_2d(points: Vec) -> PyResult { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polygon_ops::convex_hull_2d(&points)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolygon2 { inner: __v }) +} + +/// Convex hull of 3-D points as a triangle mesh (incremental hull, +/// outward-facing counterclockwise faces). +/// +/// Panics: +/// Panics with fewer than 4 points or fully coplanar input. +/// +/// Rust: `patterns::polygon_ops::convex_hull_3d` +#[pyfunction] +#[pyo3(name = "convex_hull_3d", signature = (points))] +pub fn pyfn_convex_hull_3d(points: Vec) -> PyResult { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polygon_ops::convex_hull_3d(&points)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMeshMesh { inner: __v }) +} + +/// Straight skeleton arcs of a simple polygon by the +/// shrinking-wavefront (roof) construction in the style of Felkel & +/// Obdržálek 1998, processing edge events (wavefront edges collapsing +/// as vertices meet). Split events of reflex vertices are not +/// resolved, so results are exact for convex polygons and approximate +/// for mildly non-convex ones. Each arc runs from a wavefront vertex +/// (original or intermediate) to the event point that consumed it. +/// +/// Panics: +/// Panics unless the polygon is simple with >= 3 vertices. +/// +/// Rust: `patterns::polygon_ops::straight_skeleton` +#[pyfunction] +#[pyo3(name = "straight_skeleton", signature = (poly))] +pub fn pyfn_straight_skeleton(poly: crate::generated::types::PyPolygon2) -> PyResult> { + let poly = poly.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polygon_ops::straight_skeleton(&poly)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyPrimitivesSegment2 { inner: __x }).collect::>()) +} + +/// Largest inscribed circle (pole of inaccessibility) by Mapbox's +/// polylabel quadtree refinement. +/// +/// Panics: +/// Panics unless the polygon has >= 3 vertices and `precision > 0` +/// would hold for the derived tolerance (bbox-scaled 1e-6). +/// +/// Rust: `patterns::polygon_ops::largest_inscribed_circle` +#[pyfunction] +#[pyo3(name = "largest_inscribed_circle", signature = (poly))] +pub fn pyfn_largest_inscribed_circle(poly: crate::generated::types::PyPolygon2) -> PyResult { + let poly = poly.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polygon_ops::largest_inscribed_circle(&poly)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCircle { inner: __v }) +} + +/// Smallest enclosing circle by Welzl's expected-linear incremental +/// algorithm (deterministically shuffled). +/// +/// Panics: +/// Panics on empty input. +/// +/// Rust: `patterns::polygon_ops::smallest_enclosing_circle` +#[pyfunction] +#[pyo3(name = "smallest_enclosing_circle", signature = (points))] +pub fn pyfn_smallest_enclosing_circle(points: Vec) -> PyResult { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polygon_ops::smallest_enclosing_circle(&points)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCircle { inner: __v }) +} + +/// Minimum-area oriented bounding rectangle by rotating calipers over +/// the convex hull: returns `(center, half_extents, angle)`, the +/// rectangle's local x axis rotated by `angle` from world x. +/// +/// Panics: +/// Panics with fewer than 3 points. +/// +/// Rust: `patterns::polygon_ops::minimum_bounding_rect` +#[pyfunction] +#[pyo3(name = "minimum_bounding_rect", signature = (points))] +pub fn pyfn_minimum_bounding_rect(points: Vec) -> PyResult<(crate::generated::types::PyVec2, crate::generated::types::PyVec2, f64)> { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polygon_ops::minimum_bounding_rect(&points)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((crate::generated::types::PyVec2 { inner: __v.0 }, crate::generated::types::PyVec2 { inner: __v.1 }, __v.2)) +} + +/// Farthest vertex pair (diameter) of a polygon: indices and +/// distance. +/// +/// Panics: +/// Panics with fewer than 2 vertices. +/// +/// Rust: `patterns::polygon_ops::polygon_diameter` +#[pyfunction] +#[pyo3(name = "polygon_diameter", signature = (poly))] +pub fn pyfn_polygon_diameter(poly: crate::generated::types::PyPolygon2) -> PyResult<(usize, usize, f64)> { + let poly = poly.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polygon_ops::polygon_diameter(&poly)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// Minimum width of the polygon: the smallest distance between +/// parallel supporting lines (over hull edge directions). +/// +/// Panics: +/// Panics with fewer than 3 vertices. +/// +/// Rust: `patterns::polygon_ops::polygon_width` +#[pyfunction] +#[pyo3(name = "polygon_width", signature = (poly))] +pub fn pyfn_polygon_width(poly: crate::generated::types::PyPolygon2) -> PyResult { + let poly = poly.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polygon_ops::polygon_width(&poly)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Resamples the polygon boundary into `n` equally spaced points +/// (by arclength) starting at vertex 0. +/// +/// Panics: +/// Panics unless `n >= 3` and the polygon has positive perimeter. +/// +/// Rust: `patterns::polygon_ops::resample_polygon` +#[pyfunction] +#[pyo3(name = "resample_polygon", signature = (poly, n))] +pub fn pyfn_resample_polygon(poly: crate::generated::types::PyPolygon2, n: usize) -> PyResult { + let poly = poly.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polygon_ops::resample_polygon(&poly, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolygon2 { inner: __v }) +} + +/// Chaikin corner cutting (closed polygon): each iteration replaces +/// every edge with its 1/4 and 3/4 points, converging to a smooth +/// quadratic B-spline. +/// +/// Rust: `patterns::polygon_ops::smooth_chaikin` +#[pyfunction] +#[pyo3(name = "smooth_chaikin", signature = (poly, iterations))] +pub fn pyfn_smooth_chaikin(poly: crate::generated::types::PyPolygon2, iterations: usize) -> PyResult { + let poly = poly.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polygon_ops::smooth_chaikin(&poly, iterations)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolygon2 { inner: __v }) +} + +/// Replaces each corner by a circular arc of the given radius +/// (clamped to half of the shorter adjacent edge), sampled with +/// `segments` points. +/// +/// Panics: +/// Panics unless `radius > 0` and `segments >= 1`. +/// +/// Rust: `patterns::polygon_ops::round_corners` +#[pyfunction] +#[pyo3(name = "round_corners", signature = (poly, radius, segments))] +pub fn pyfn_round_corners(poly: crate::generated::types::PyPolygon2, radius: f64, segments: usize) -> PyResult { + let poly = poly.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polygon_ops::round_corners(&poly, radius, segments)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolygon2 { inner: __v }) +} + +/// Parallel hatch lines filling the polygon: scanlines spaced by +/// `spacing`, rotated by `angle` radians from the x axis (even-odd +/// filled). +/// +/// Panics: +/// Panics unless `spacing > 0`. +/// +/// Rust: `patterns::polygon_ops::hatch_fill` +#[pyfunction] +#[pyo3(name = "hatch_fill", signature = (poly, spacing, angle))] +pub fn pyfn_hatch_fill(poly: crate::generated::types::PyPolygon2, spacing: f64, angle: f64) -> PyResult> { + let poly = poly.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polygon_ops::hatch_fill(&poly, spacing, angle)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyPrimitivesSegment2 { inner: __x }).collect::>()) +} + +/// Concentric fill: repeated inward offsets by `spacing` until the +/// polygon vanishes. +/// +/// Panics: +/// Panics unless `spacing > 0`. +/// +/// Rust: `patterns::polygon_ops::contour_fill` +#[pyfunction] +#[pyo3(name = "contour_fill", signature = (poly, spacing))] +pub fn pyfn_contour_fill(poly: crate::generated::types::PyPolygon2, spacing: f64) -> PyResult> { + let poly = poly.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polygon_ops::contour_fill(&poly, spacing)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyPolygon2 { inner: __x }).collect::>()) +} + +/// Triangulates a polygon (optionally with holes) into a flat mesh at +/// z = 0, facing +z. +/// +/// Panics: +/// Panics when triangulation fails (non-simple input). +/// +/// Rust: `patterns::polygon_ops::polygon_to_mesh_2d` +#[pyfunction] +#[pyo3(name = "polygon_to_mesh_2d", signature = (poly, holes))] +pub fn pyfn_polygon_to_mesh_2d(poly: crate::generated::types::PyPolygon2, holes: Vec) -> PyResult { + let poly = poly.inner; + let holes = holes.into_iter().map(|__e| __e.inner).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polygon_ops::polygon_to_mesh_2d(&poly, &holes)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMeshMesh { inner: __v }) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_triangulate_ear_clipping, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_triangulate_with_holes, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_simplify_douglas_peucker, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_simplify_visvalingam, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_offset_polygon, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_minkowski_sum_convex, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_minkowski_sum, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_boolean_union, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_boolean_intersection, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_boolean_difference, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_boolean_xor, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_clip_polygon_convex, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_clip_polygon_rect, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_clip_line_rect, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_convex_decomposition, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_convex_hull_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_convex_hull_3d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_straight_skeleton, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_largest_inscribed_circle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_smallest_enclosing_circle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_minimum_bounding_rect, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_polygon_diameter, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_polygon_width, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_resample_polygon, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_smooth_chaikin, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_round_corners, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hatch_fill, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_contour_fill, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_polygon_to_mesh_2d, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_patterns__polyhedra.rs b/bindings/python/src/generated/m_patterns__polyhedra.rs new file mode 100644 index 0000000..3fe0d08 --- /dev/null +++ b/bindings/python/src/generated/m_patterns__polyhedra.rs @@ -0,0 +1,475 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Conway dual (alias of `Polyhedron::dual`). +/// +/// Rust: `patterns::polyhedra::conway_dual` +#[pyfunction] +#[pyo3(name = "conway_dual", signature = (p))] +pub fn pyfn_conway_dual(p: crate::generated::types::PyPolyhedron) -> PyResult { + let p = p.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polyhedra::conway_dual(&p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyhedron { inner: __v }) +} + +/// Conway kis: a pyramid of the given apex height over every face. +/// +/// Rust: `patterns::polyhedra::kis` +#[pyfunction] +#[pyo3(name = "kis", signature = (p, apex_height))] +pub fn pyfn_kis(p: crate::generated::types::PyPolyhedron, apex_height: f64) -> PyResult { + let p = p.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polyhedra::kis(&p, apex_height)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyhedron { inner: __v }) +} + +/// Conway ambo (rectification): vertices at edge midpoints. +/// +/// Rust: `patterns::polyhedra::ambo` +#[pyfunction] +#[pyo3(name = "ambo", signature = (p))] +pub fn pyfn_ambo(p: crate::generated::types::PyPolyhedron) -> PyResult { + let p = p.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polyhedra::ambo(&p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyhedron { inner: __v }) +} + +/// Conway truncate: cuts each corner, moving `ratio` along every +/// edge (1/3 turns regular triangles into regular hexagons). +/// +/// Panics: +/// Panics unless `0 < ratio < 1/2`. +/// +/// Rust: `patterns::polyhedra::truncate` +#[pyfunction] +#[pyo3(name = "truncate", signature = (p, ratio))] +pub fn pyfn_truncate(p: crate::generated::types::PyPolyhedron, ratio: f64) -> PyResult { + let p = p.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polyhedra::truncate(&p, ratio)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyhedron { inner: __v }) +} + +/// Conway chamfer: shrinks faces in-plane by `ratio` and replaces +/// each edge with a hexagon (original vertices kept). +/// +/// Panics: +/// Panics unless `0 < ratio < 1`. +/// +/// Rust: `patterns::polyhedra::chamfer` +#[pyfunction] +#[pyo3(name = "chamfer", signature = (p, ratio))] +pub fn pyfn_chamfer(p: crate::generated::types::PyPolyhedron, ratio: f64) -> PyResult { + let p = p.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polyhedra::chamfer(&p, ratio)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyhedron { inner: __v }) +} + +/// Conway gyro: pentagonal faces, one per (face, edge) incidence. +/// +/// Rust: `patterns::polyhedra::gyro` +#[pyfunction] +#[pyo3(name = "gyro", signature = (p))] +pub fn pyfn_gyro(p: crate::generated::types::PyPolyhedron) -> PyResult { + let p = p.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polyhedra::gyro(&p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyhedron { inner: __v }) +} + +/// Conway propellor: each face spins off a smaller rotated copy +/// surrounded by quads. +/// +/// Rust: `patterns::polyhedra::propellor` +#[pyfunction] +#[pyo3(name = "propellor", signature = (p))] +pub fn pyfn_propellor(p: crate::generated::types::PyPolyhedron) -> PyResult { + let p = p.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polyhedra::propellor(&p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyhedron { inner: __v }) +} + +/// Conway whirl: hexagons spiral around shrunken rotated faces. +/// +/// Rust: `patterns::polyhedra::whirl` +#[pyfunction] +#[pyo3(name = "whirl", signature = (p))] +pub fn pyfn_whirl(p: crate::generated::types::PyPolyhedron) -> PyResult { + let p = p.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polyhedra::whirl(&p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyhedron { inner: __v }) +} + +/// Conway join = dual(ambo): rhombic faces over each original edge. +/// +/// Rust: `patterns::polyhedra::join` +#[pyfunction] +#[pyo3(name = "join", signature = (p))] +pub fn pyfn_join(p: crate::generated::types::PyPolyhedron) -> PyResult { + let p = p.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polyhedra::join(&p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyhedron { inner: __v }) +} + +/// Conway needle = kis(dual). +/// +/// Rust: `patterns::polyhedra::needle` +#[pyfunction] +#[pyo3(name = "needle", signature = (p))] +pub fn pyfn_needle(p: crate::generated::types::PyPolyhedron) -> PyResult { + let p = p.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polyhedra::needle(&p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyhedron { inner: __v }) +} + +/// Conway zip = dual(kis). +/// +/// Rust: `patterns::polyhedra::zip` +#[pyfunction] +#[pyo3(name = "zip", signature = (p))] +pub fn pyfn_zip(p: crate::generated::types::PyPolyhedron) -> PyResult { + let p = p.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polyhedra::zip(&p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyhedron { inner: __v }) +} + +/// Conway ortho = join(join). +/// +/// Rust: `patterns::polyhedra::ortho` +#[pyfunction] +#[pyo3(name = "ortho", signature = (p))] +pub fn pyfn_ortho(p: crate::generated::types::PyPolyhedron) -> PyResult { + let p = p.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polyhedra::ortho(&p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyhedron { inner: __v }) +} + +/// Conway expand = ambo(ambo). +/// +/// Rust: `patterns::polyhedra::expand` +#[pyfunction] +#[pyo3(name = "expand", signature = (p))] +pub fn pyfn_expand(p: crate::generated::types::PyPolyhedron) -> PyResult { + let p = p.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polyhedra::expand(&p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyhedron { inner: __v }) +} + +/// Conway bevel = truncate(ambo). +/// +/// Rust: `patterns::polyhedra::bevel` +#[pyfunction] +#[pyo3(name = "bevel", signature = (p))] +pub fn pyfn_bevel(p: crate::generated::types::PyPolyhedron) -> PyResult { + let p = p.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polyhedra::bevel(&p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyhedron { inner: __v }) +} + +/// Conway meta = kis(join). +/// +/// Rust: `patterns::polyhedra::meta` +#[pyfunction] +#[pyo3(name = "meta", signature = (p))] +pub fn pyfn_meta(p: crate::generated::types::PyPolyhedron) -> PyResult { + let p = p.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polyhedra::meta(&p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyhedron { inner: __v }) +} + +/// Conway snub = dual(gyro). +/// +/// Rust: `patterns::polyhedra::snub` +#[pyfunction] +#[pyo3(name = "snub", signature = (p))] +pub fn pyfn_snub(p: crate::generated::types::PyPolyhedron) -> PyResult { + let p = p.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polyhedra::snub(&p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyhedron { inner: __v }) +} + +/// Applies a Conway notation string, e.g. `"tkT"` or `"dsI"`: the +/// rightmost character may be a seed (T, C, O, D, I); otherwise the +/// operators apply to `p`. Operators: d a k t j n z o e b m s g p c w. +/// +/// Errors: +/// Returns `GeomError::InvalidArgument` for an unknown character. +/// +/// Rust: `patterns::polyhedra::conway_apply` +#[pyfunction] +#[pyo3(name = "conway_apply", signature = (p, notation))] +pub fn pyfn_conway_apply(p: crate::generated::types::PyPolyhedron, notation: String) -> PyResult { + let p = p.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polyhedra::conway_apply(&p, ¬ation)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyPolyhedron { inner: __v }) +} + +/// Regular tetrahedron (edge 2√2). +/// +/// Rust: `patterns::polyhedra::tetrahedron` +#[pyfunction] +#[pyo3(name = "tetrahedron", signature = ())] +pub fn pyfn_tetrahedron() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polyhedra::tetrahedron()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyhedron { inner: __v }) +} + +/// Cube (edge 2). +/// +/// Rust: `patterns::polyhedra::cube` +#[pyfunction] +#[pyo3(name = "cube", signature = ())] +pub fn pyfn_cube() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polyhedra::cube()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyhedron { inner: __v }) +} + +/// Regular octahedron (edge √2). +/// +/// Rust: `patterns::polyhedra::octahedron` +#[pyfunction] +#[pyo3(name = "octahedron", signature = ())] +pub fn pyfn_octahedron() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polyhedra::octahedron()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyhedron { inner: __v }) +} + +/// Regular icosahedron. +/// +/// Rust: `patterns::polyhedra::icosahedron` +#[pyfunction] +#[pyo3(name = "icosahedron", signature = ())] +pub fn pyfn_icosahedron() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polyhedra::icosahedron()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyhedron { inner: __v }) +} + +/// Regular dodecahedron (dual of the icosahedron). +/// +/// Rust: `patterns::polyhedra::dodecahedron` +#[pyfunction] +#[pyo3(name = "dodecahedron", signature = ())] +pub fn pyfn_dodecahedron() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polyhedra::dodecahedron()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyhedron { inner: __v }) +} + +/// Right prism over a regular n-gon (unit edge circumcircle scaled so +/// the polygon edge is 1), height `h`. +/// +/// Panics: +/// Panics unless `n >= 3` and `h > 0`. +/// +/// Rust: `patterns::polyhedra::prism` +#[pyfunction] +#[pyo3(name = "prism", signature = (n, h))] +pub fn pyfn_prism(n: usize, h: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polyhedra::prism(n, h)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyhedron { inner: __v }) +} + +/// Antiprism over a regular n-gon (unit polygon edge), height `h`. +/// +/// Panics: +/// Panics unless `n >= 3` and `h > 0`. +/// +/// Rust: `patterns::polyhedra::antiprism` +#[pyfunction] +#[pyo3(name = "antiprism", signature = (n, h))] +pub fn pyfn_antiprism(n: usize, h: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polyhedra::antiprism(n, h)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyhedron { inner: __v }) +} + +/// Pyramid over a regular n-gon (unit edge base), apex height `h`. +/// +/// Panics: +/// Panics unless `n >= 3` and `h > 0`. +/// +/// Rust: `patterns::polyhedra::pyramid` +#[pyfunction] +#[pyo3(name = "pyramid", signature = (n, h))] +pub fn pyfn_pyramid(n: usize, h: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polyhedra::pyramid(n, h)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyhedron { inner: __v }) +} + +/// Bipyramid over a regular n-gon (unit edge equator), apexes at ±h. +/// +/// Panics: +/// Panics unless `n >= 3` and `h > 0`. +/// +/// Rust: `patterns::polyhedra::bipyramid` +#[pyfunction] +#[pyo3(name = "bipyramid", signature = (n, h))] +pub fn pyfn_bipyramid(n: usize, h: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polyhedra::bipyramid(n, h)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyhedron { inner: __v }) +} + +/// Builds a polyhedron as the convex hull of a point set, merging +/// coplanar triangles into polygon faces. +/// +/// Panics: +/// Panics with fewer than 4 points or degenerate input. +/// +/// Rust: `patterns::polyhedra::from_convex_points` +#[pyfunction] +#[pyo3(name = "from_convex_points", signature = (points))] +pub fn pyfn_from_convex_points(points: Vec) -> PyResult { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polyhedra::from_convex_points(&points)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyhedron { inner: __v }) +} + +/// Constructs an Archimedean solid: exact coordinates or exact Conway +/// constructions everywhere except the snub dodecahedron, which is +/// built combinatorially by the snub operator and canonicalized (its +/// coordinates are then approximate). +/// +/// Rust: `patterns::polyhedra::archimedean` +#[pyfunction] +#[pyo3(name = "archimedean", signature = (kind))] +pub fn pyfn_archimedean(kind: crate::generated::types::PyArchimedeanSolid) -> PyResult { + let kind = kind.to_rust(); + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polyhedra::archimedean(kind)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyhedron { inner: __v }) +} + +/// Catalan solid: the dual of the corresponding Archimedean solid +/// (canonicalized so faces are planar and congruent). +/// +/// Rust: `patterns::polyhedra::catalan` +#[pyfunction] +#[pyo3(name = "catalan", signature = (kind))] +pub fn pyfn_catalan(kind: crate::generated::types::PyArchimedeanSolid) -> PyResult { + let kind = kind.to_rust(); + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polyhedra::catalan(kind)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyhedron { inner: __v }) +} + +/// The first 20 Johnson solids J1..J20 with unit edges; `None` for +/// n = 0 or n > 20. +/// +/// Rust: `patterns::polyhedra::johnson` +#[pyfunction] +#[pyo3(name = "johnson", signature = (n))] +pub fn pyfn_johnson(n: u8) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polyhedra::johnson(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyPolyhedron { inner: __x })) +} + +/// Class I geodesic sphere: each icosahedron face subdivided into +/// `frequency`² triangles, projected to the unit sphere. +/// +/// Panics: +/// Panics unless `frequency >= 1`. +/// +/// Rust: `patterns::polyhedra::geodesic_sphere` +#[pyfunction] +#[pyo3(name = "geodesic_sphere", signature = (frequency))] +pub fn pyfn_geodesic_sphere(frequency: u32) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polyhedra::geodesic_sphere(frequency)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyhedron { inner: __v }) +} + +/// Goldberg polyhedron GP(m, n): hexagons plus 12 pentagons. Class I +/// (n = 0) and class II (m = n) are supported (class II via a √3 +/// refinement of the class I triangulation); general class III is +/// not. +/// +/// Panics: +/// Panics unless `m >= 1` and (`n == 0` or `n == m`). +/// +/// Rust: `patterns::polyhedra::goldberg` +#[pyfunction] +#[pyo3(name = "goldberg", signature = (m, n))] +pub fn pyfn_goldberg(m: u32, n: u32) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::polyhedra::goldberg(m, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyhedron { inner: __v }) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_conway_dual, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kis, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ambo, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_truncate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_chamfer, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gyro, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_propellor, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_whirl, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_join, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_needle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_zip, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ortho, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_expand, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bevel, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_meta, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_snub, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_conway_apply, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tetrahedron, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cube, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_octahedron, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_icosahedron, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dodecahedron, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_prism, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_antiprism, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pyramid, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bipyramid, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_from_convex_points, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_archimedean, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_catalan, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_johnson, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_geodesic_sphere, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_goldberg, m)?)?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_patterns__sampling.rs b/bindings/python/src/generated/m_patterns__sampling.rs new file mode 100644 index 0000000..8d1894c --- /dev/null +++ b/bindings/python/src/generated/m_patterns__sampling.rs @@ -0,0 +1,472 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Bridson's Poisson disk sampling in a rectangle ("Fast Poisson Disk +/// Sampling in Arbitrary Dimensions", SIGGRAPH 2007): no two samples +/// closer than `min_dist`, maximal up to `k` candidate attempts per +/// active sample. +/// +/// Panics: +/// Panics unless `min_dist > 0` and `k >= 1`. +/// +/// Rust: `patterns::sampling::poisson_disk_2d` +#[pyfunction] +#[pyo3(name = "poisson_disk_2d", signature = (region, min_dist, k, rng))] +pub fn pyfn_poisson_disk_2d(region: crate::generated::types::PyRect, min_dist: f64, k: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let region = region.inner; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::sampling::poisson_disk_2d(®ion, min_dist, k, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// Bridson Poisson disk sampling in a box (3-D). +/// +/// Panics: +/// Panics unless `min_dist > 0` and `k >= 1`. +/// +/// Rust: `patterns::sampling::poisson_disk_3d` +#[pyfunction] +#[pyo3(name = "poisson_disk_3d", signature = (region, min_dist, k, rng))] +pub fn pyfn_poisson_disk_3d(region: crate::generated::types::PyAabb, min_dist: f64, k: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let region = region.inner; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::sampling::poisson_disk_3d(®ion, min_dist, k, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) +} + +/// Poisson disk sampling restricted to a polygon: Bridson over the +/// bounding rectangle, samples outside the polygon rejected. +/// +/// Panics: +/// Panics unless `min_dist > 0`, `k >= 1`, and the polygon has >= 3 +/// vertices. +/// +/// Rust: `patterns::sampling::poisson_disk_polygon` +#[pyfunction] +#[pyo3(name = "poisson_disk_polygon", signature = (poly, min_dist, k, rng))] +pub fn pyfn_poisson_disk_polygon(poly: crate::generated::types::PyPolygon2, min_dist: f64, k: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let poly = poly.inner; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::sampling::poisson_disk_polygon(&poly, min_dist, k, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// Variable-density Poisson disk sampling: `density` maps a point to +/// its local minimum distance (larger density value = larger +/// spacing). Dart throwing against a conflict grid keyed by the +/// smallest local radius. +/// +/// Panics: +/// Panics unless `k >= 1` and `density` returns positive values over +/// the region (sampled at the corners and center). +/// +/// Rust: `patterns::sampling::poisson_disk_variable` +#[pyfunction] +#[pyo3(name = "poisson_disk_variable", signature = (region, density, k, rng))] +pub fn pyfn_poisson_disk_variable(region: crate::generated::types::PyRect, density: pyo3::Py, k: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let region = region.inner; + let __cb_density = std::rc::Rc::new(crate::runtime::Callback::new(density)); + let density = { let __cb = __cb_density.clone(); move |__a0: rust_physics_engine::math::Vec2| -> f64 { __cb.call::<_, f64>((crate::generated::types::PyVec2 { inner: __a0 },), f64::NAN) } }; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::sampling::poisson_disk_variable(®ion, &density, k, &mut rng.inner)); + crate::runtime::callback::check(&[&__cb_density], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// Poisson disk sampling on a mesh surface by dart throwing over +/// area-weighted surface samples. +/// +/// Panics: +/// Panics unless `min_dist > 0` and the mesh has positive area. +/// +/// Rust: `patterns::sampling::poisson_disk_surface` +#[pyfunction] +#[pyo3(name = "poisson_disk_surface", signature = (mesh, min_dist, rng))] +pub fn pyfn_poisson_disk_surface(mesh: crate::generated::types::PyMeshMesh, min_dist: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mesh = mesh.inner; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::sampling::poisson_disk_surface(&mesh, min_dist, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) +} + +/// Blue-noise point ranking on a `w` x `h` grid by the void-and-cluster +/// method (Ulichney 1993, toroidal Gaussian energy): returns the `n` +/// best-spread grid cell centers. +/// +/// Panics: +/// Panics unless `n <= w * h / 2` and the grid is nonempty. +/// +/// Rust: `patterns::sampling::blue_noise_void_cluster` +#[pyfunction] +#[pyo3(name = "blue_noise_void_cluster", signature = (w, h, n))] +pub fn pyfn_blue_noise_void_cluster(w: usize, h: usize, n: usize) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::sampling::blue_noise_void_cluster(w, h, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// Stratified jittered samples on the unit square: one sample per +/// cell of an `nx` x `ny` grid, jittered by `jitter` in [0, 1]. +/// +/// Panics: +/// Panics unless `nx, ny >= 1` and `jitter` is in [0, 1]. +/// +/// Rust: `patterns::sampling::stratified_2d` +#[pyfunction] +#[pyo3(name = "stratified_2d", signature = (nx, ny, jitter, rng))] +pub fn pyfn_stratified_2d(nx: usize, ny: usize, jitter: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::sampling::stratified_2d(nx, ny, jitter, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// Uniform point in a 2-D triangle by the square-root warp. +/// +/// Rust: `patterns::sampling::uniform_in_triangle` +#[pyfunction] +#[pyo3(name = "uniform_in_triangle", signature = (t, rng))] +pub fn pyfn_uniform_in_triangle(t: crate::generated::types::PyTriangle2, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let t = t.inner; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::sampling::uniform_in_triangle(&t, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) +} + +/// Uniform point in a 3-D triangle. +/// +/// Rust: `patterns::sampling::uniform_in_triangle_3d` +#[pyfunction] +#[pyo3(name = "uniform_in_triangle_3d", signature = (t, rng))] +pub fn pyfn_uniform_in_triangle_3d(t: crate::generated::types::PyTriangle, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let t = t.inner; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::sampling::uniform_in_triangle_3d(&t, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Uniform point in a simple polygon: triangulate, pick a triangle by +/// area, sample it. +/// +/// Panics: +/// Panics when the polygon cannot be triangulated. +/// +/// Rust: `patterns::sampling::uniform_in_polygon` +#[pyfunction] +#[pyo3(name = "uniform_in_polygon", signature = (poly, rng))] +pub fn pyfn_uniform_in_polygon(poly: crate::generated::types::PyPolygon2, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let poly = poly.inner; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::sampling::uniform_in_polygon(&poly, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) +} + +/// Uniform point inside a circle. +/// +/// Rust: `patterns::sampling::uniform_in_circle` +#[pyfunction] +#[pyo3(name = "uniform_in_circle", signature = (c, rng))] +pub fn pyfn_uniform_in_circle(c: crate::generated::types::PyCircle, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let c = c.inner; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::sampling::uniform_in_circle(&c, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) +} + +/// Uniform point on a circle's boundary. +/// +/// Rust: `patterns::sampling::uniform_on_circle` +#[pyfunction] +#[pyo3(name = "uniform_on_circle", signature = (c, rng))] +pub fn pyfn_uniform_on_circle(c: crate::generated::types::PyCircle, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let c = c.inner; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::sampling::uniform_on_circle(&c, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) +} + +/// Uniform point inside a sphere (cube-root radial warp). +/// +/// Rust: `patterns::sampling::uniform_in_sphere` +#[pyfunction] +#[pyo3(name = "uniform_in_sphere", signature = (s, rng))] +pub fn pyfn_uniform_in_sphere(s: crate::generated::types::PySphere, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let s = s.inner; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::sampling::uniform_in_sphere(&s, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Uniform point on a sphere's surface. +/// +/// Rust: `patterns::sampling::uniform_on_sphere` +#[pyfunction] +#[pyo3(name = "uniform_on_sphere", signature = (s, rng))] +pub fn pyfn_uniform_on_sphere(s: crate::generated::types::PySphere, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let s = s.inner; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::sampling::uniform_on_sphere(&s, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Uniform direction on the unit hemisphere around `n`. +/// +/// Panics: +/// Panics when `n` is zero. +/// +/// Rust: `patterns::sampling::uniform_on_hemisphere` +#[pyfunction] +#[pyo3(name = "uniform_on_hemisphere", signature = (n, rng))] +pub fn pyfn_uniform_on_hemisphere(n: crate::generated::types::PyVec3Arg, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let n = n.0; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::sampling::uniform_on_hemisphere(n, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Cosine-weighted direction on the hemisphere around `n` (Malley's +/// method: uniform disk lifted to the sphere). +/// +/// Panics: +/// Panics when `n` is zero. +/// +/// Rust: `patterns::sampling::cosine_weighted_hemisphere` +#[pyfunction] +#[pyo3(name = "cosine_weighted_hemisphere", signature = (n, rng))] +pub fn pyfn_cosine_weighted_hemisphere(n: crate::generated::types::PyVec3Arg, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let n = n.0; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::sampling::cosine_weighted_hemisphere(n, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Uniform point inside an axis-aligned box. +/// +/// Rust: `patterns::sampling::uniform_in_aabb` +#[pyfunction] +#[pyo3(name = "uniform_in_aabb", signature = (b, rng))] +pub fn pyfn_uniform_in_aabb(b: crate::generated::types::PyAabb, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let b = b.inner; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::sampling::uniform_in_aabb(&b, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Uniform point inside an oriented box. +/// +/// Rust: `patterns::sampling::uniform_in_obb` +#[pyfunction] +#[pyo3(name = "uniform_in_obb", signature = (b, rng))] +pub fn pyfn_uniform_in_obb(b: crate::generated::types::PyObb, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let b = b.inner; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::sampling::uniform_in_obb(&b, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Uniform point on the surface of an axis-aligned box +/// (area-weighted face choice). +/// +/// Rust: `patterns::sampling::uniform_on_aabb_surface` +#[pyfunction] +#[pyo3(name = "uniform_on_aabb_surface", signature = (b, rng))] +pub fn pyfn_uniform_on_aabb_surface(b: crate::generated::types::PyAabb, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let b = b.inner; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::sampling::uniform_on_aabb_surface(&b, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Uniform point in the annulus between `r_in` and `r_out`. +/// +/// Panics: +/// Panics unless `0 <= r_in < r_out`. +/// +/// Rust: `patterns::sampling::uniform_in_annulus` +#[pyfunction] +#[pyo3(name = "uniform_in_annulus", signature = (c, r_in, r_out, rng))] +pub fn pyfn_uniform_in_annulus(c: crate::generated::types::PyVec2Arg, r_in: f64, r_out: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let c = c.0; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::sampling::uniform_in_annulus(c, r_in, r_out, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) +} + +/// Uniform direction within the cone of half-angle `angle` around +/// `axis` (solid-angle uniform). +/// +/// Panics: +/// Panics unless `axis` is nonzero and `angle` is in (0, π]. +/// +/// Rust: `patterns::sampling::uniform_in_cone` +#[pyfunction] +#[pyo3(name = "uniform_in_cone", signature = (axis, angle, rng))] +pub fn pyfn_uniform_in_cone(axis: crate::generated::types::PyVec3Arg, angle: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let axis = axis.0; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::sampling::uniform_in_cone(axis, angle, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Random convex polygon with `n` vertices by Valtr's algorithm +/// (uniform over convex polygons in the unit square), counterclockwise. +/// +/// Panics: +/// Panics unless `n >= 3`. +/// +/// Rust: `patterns::sampling::random_convex_polygon` +#[pyfunction] +#[pyo3(name = "random_convex_polygon", signature = (n, rng))] +pub fn pyfn_random_convex_polygon(n: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::sampling::random_convex_polygon(n, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolygon2 { inner: __v }) +} + +/// Random simple polygon: random points untangled by repeatedly +/// swapping crossing edges (2-opt), which strictly shortens the +/// perimeter and therefore terminates. +/// +/// Panics: +/// Panics unless `n >= 3`. +/// +/// Rust: `patterns::sampling::random_simple_polygon` +#[pyfunction] +#[pyo3(name = "random_simple_polygon", signature = (n, rng))] +pub fn pyfn_random_simple_polygon(n: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::sampling::random_simple_polygon(n, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolygon2 { inner: __v }) +} + +/// Uniform random rotation by Shoemake's subgroup algorithm (uniform +/// over SO(3)). +/// +/// Rust: `patterns::sampling::random_rotation` +#[pyfunction] +#[pyo3(name = "random_rotation", signature = (rng))] +pub fn pyfn_random_rotation(rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::sampling::random_rotation(&mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuaternion { inner: __v }) +} + +/// Uniform random unit vector (normalized Gaussian triple). +/// +/// Rust: `patterns::sampling::random_unit_vector` +#[pyfunction] +#[pyo3(name = "random_unit_vector", signature = (rng))] +pub fn pyfn_random_unit_vector(rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::sampling::random_unit_vector(&mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Lloyd relaxation toward a centroidal Voronoi arrangement: each +/// iteration moves every point to the centroid of its (grid-sampled) +/// Voronoi cell within `region`. +/// +/// Panics: +/// Panics when `points` is empty. +/// +/// Rust: `patterns::sampling::lloyd_relaxation` +#[pyfunction] +#[pyo3(name = "lloyd_relaxation", signature = (points, region, iterations))] +pub fn pyfn_lloyd_relaxation<'py>(points: pyo3::Bound<'py, pyo3::PyAny>, region: crate::generated::types::PyRect, iterations: usize) -> PyResult<()> { + let mut points__v: Vec = points.extract::>()?.into_iter().map(|__e| __e.0).collect(); + let region = region.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::sampling::lloyd_relaxation(&mut points__v, ®ion, iterations)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + crate::runtime::coerce::write_back_objects(&points, points__v.into_iter().map(|__e| crate::generated::types::PyVec2 { inner: __e }).collect::>())?; + Ok(()) +} + +/// Weighted stippling: `n` seed points relaxed by density-weighted +/// Lloyd iterations, so point density tracks `density`. +/// +/// Panics: +/// Panics unless `n >= 1` and `density` is nonnegative where sampled. +/// +/// Rust: `patterns::sampling::stipple` +#[pyfunction] +#[pyo3(name = "stipple", signature = (density, region, n, iterations, rng))] +pub fn pyfn_stipple(density: pyo3::Py, region: crate::generated::types::PyRect, n: usize, iterations: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let __cb_density = std::rc::Rc::new(crate::runtime::Callback::new(density)); + let density = { let __cb = __cb_density.clone(); move |__a0: rust_physics_engine::math::Vec2| -> f64 { __cb.call::<_, f64>((crate::generated::types::PyVec2 { inner: __a0 },), f64::NAN) } }; + let region = region.inner; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::sampling::stipple(&density, ®ion, n, iterations, &mut rng.inner)); + crate::runtime::callback::check(&[&__cb_density], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_poisson_disk_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_poisson_disk_3d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_poisson_disk_polygon, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_poisson_disk_variable, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_poisson_disk_surface, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_blue_noise_void_cluster, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_stratified_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_uniform_in_triangle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_uniform_in_triangle_3d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_uniform_in_polygon, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_uniform_in_circle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_uniform_on_circle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_uniform_in_sphere, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_uniform_on_sphere, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_uniform_on_hemisphere, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cosine_weighted_hemisphere, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_uniform_in_aabb, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_uniform_in_obb, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_uniform_on_aabb_surface, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_uniform_in_annulus, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_uniform_in_cone, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_random_convex_polygon, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_random_simple_polygon, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_random_rotation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_random_unit_vector, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lloyd_relaxation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_stipple, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_patterns__space_filling.rs b/bindings/python/src/generated/m_patterns__space_filling.rs new file mode 100644 index 0000000..ed531ef --- /dev/null +++ b/bindings/python/src/generated/m_patterns__space_filling.rs @@ -0,0 +1,323 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Hilbert curve index to grid coordinates on a `2^order` square grid +/// (Wikipedia's iterative rotate-and-flip formulation). +/// +/// Panics: +/// Panics unless `1 <= order <= 31` and `d < 4^order`. +/// +/// Rust: `patterns::space_filling::hilbert_d2xy` +#[pyfunction] +#[pyo3(name = "hilbert_d2xy", signature = (order, d))] +pub fn pyfn_hilbert_d2xy(order: u32, d: u64) -> PyResult<(u64, u64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::space_filling::hilbert_d2xy(order, d)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Grid coordinates to Hilbert index (inverse of `hilbert_d2xy`). +/// +/// Panics: +/// Panics unless `1 <= order <= 31` and both coordinates are below +/// `2^order`. +/// +/// Rust: `patterns::space_filling::hilbert_xy2d` +#[pyfunction] +#[pyo3(name = "hilbert_xy2d", signature = (order, x, y))] +pub fn pyfn_hilbert_xy2d(order: u32, x: u64, y: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::space_filling::hilbert_xy2d(order, x, y)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The full Hilbert curve as points in the unit square (cell +/// centers), in curve order. +/// +/// Panics: +/// Panics unless `1 <= order <= 10` (2^20 points at most). +/// +/// Rust: `patterns::space_filling::hilbert_curve_2d` +#[pyfunction] +#[pyo3(name = "hilbert_curve_2d", signature = (order))] +pub fn pyfn_hilbert_curve_2d(order: u32) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::space_filling::hilbert_curve_2d(order)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// 3-D Hilbert index to grid coordinates on a `2^order` cube. +/// +/// Panics: +/// Panics unless `1 <= order <= 21` and `d < 8^order`. +/// +/// Rust: `patterns::space_filling::hilbert_3d_d2xyz` +#[pyfunction] +#[pyo3(name = "hilbert_3d_d2xyz", signature = (order, d))] +pub fn pyfn_hilbert_3d_d2xyz(order: u32, d: u64) -> PyResult<(u64, u64, u64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::space_filling::hilbert_3d_d2xyz(order, d)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// 3-D grid coordinates to Hilbert index (inverse of +/// `hilbert_3d_d2xyz`). +/// +/// Panics: +/// Panics unless `1 <= order <= 21` and all coordinates are below +/// `2^order`. +/// +/// Rust: `patterns::space_filling::hilbert_3d_xyz2d` +#[pyfunction] +#[pyo3(name = "hilbert_3d_xyz2d", signature = (order, x, y, z))] +pub fn pyfn_hilbert_3d_xyz2d(order: u32, x: u64, y: u64, z: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::space_filling::hilbert_3d_xyz2d(order, x, y, z)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The 3-D Hilbert curve as points in the unit cube, in curve order. +/// +/// Panics: +/// Panics unless `1 <= order <= 6` (2^18 points at most). +/// +/// Rust: `patterns::space_filling::hilbert_curve_3d` +#[pyfunction] +#[pyo3(name = "hilbert_curve_3d", signature = (order))] +pub fn pyfn_hilbert_curve_3d(order: u32) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::space_filling::hilbert_curve_3d(order)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) +} + +/// Peano curve on a `3^order` grid via the ternary digit formula +/// (Peano 1890): points in the unit square in curve order. +/// +/// Panics: +/// Panics unless `1 <= order <= 6`. +/// +/// Rust: `patterns::space_filling::peano_curve` +#[pyfunction] +#[pyo3(name = "peano_curve", signature = (order))] +pub fn pyfn_peano_curve(order: u32) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::space_filling::peano_curve(order)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// Interleaves the bits of x (even positions) and y (odd positions). +/// +/// Rust: `patterns::space_filling::morton_encode_2d` +#[pyfunction] +#[pyo3(name = "morton_encode_2d", signature = (x, y))] +pub fn pyfn_morton_encode_2d(x: u32, y: u32) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::space_filling::morton_encode_2d(x, y)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Inverse of `morton_encode_2d`. +/// +/// Rust: `patterns::space_filling::morton_decode_2d` +#[pyfunction] +#[pyo3(name = "morton_decode_2d", signature = (m))] +pub fn pyfn_morton_decode_2d(m: u64) -> PyResult<(u32, u32)> { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::space_filling::morton_decode_2d(m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Interleaves 21 bits each of x, y, z. +/// +/// Panics: +/// Panics when any coordinate exceeds 21 bits. +/// +/// Rust: `patterns::space_filling::morton_encode_3d` +#[pyfunction] +#[pyo3(name = "morton_encode_3d", signature = (x, y, z))] +pub fn pyfn_morton_encode_3d(x: u32, y: u32, z: u32) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::space_filling::morton_encode_3d(x, y, z)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Inverse of `morton_encode_3d`. +/// +/// Rust: `patterns::space_filling::morton_decode_3d` +#[pyfunction] +#[pyo3(name = "morton_decode_3d", signature = (m))] +pub fn pyfn_morton_decode_3d(m: u64) -> PyResult<(u32, u32, u32)> { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::space_filling::morton_decode_3d(m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// The Z-order (Morton) traversal of a `2^order` grid as unit-square +/// points. +/// +/// Panics: +/// Panics unless `1 <= order <= 10`. +/// +/// Rust: `patterns::space_filling::z_order_curve` +#[pyfunction] +#[pyo3(name = "z_order_curve", signature = (order))] +pub fn pyfn_z_order_curve(order: u32) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::space_filling::z_order_curve(order)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// Binary reflected Gray code. +/// +/// Rust: `patterns::space_filling::gray_code` +#[pyfunction] +#[pyo3(name = "gray_code", signature = (n))] +pub fn pyfn_gray_code(n: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::space_filling::gray_code(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Inverse Gray code (prefix xor by doubling). +/// +/// Rust: `patterns::space_filling::gray_decode` +#[pyfunction] +#[pyo3(name = "gray_decode", signature = (g))] +pub fn pyfn_gray_decode(g: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::space_filling::gray_decode(g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Sierpiński arrowhead curve (traverses the Sierpiński triangle), +/// unit steps from the origin. +/// +/// Panics: +/// Panics unless `1 <= order <= 10`. +/// +/// Rust: `patterns::space_filling::sierpinski_curve` +#[pyfunction] +#[pyo3(name = "sierpinski_curve", signature = (order))] +pub fn pyfn_sierpinski_curve(order: u32) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::space_filling::sierpinski_curve(order)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// Moore curve: the closed variant of the Hilbert curve (last point +/// adjacent to the first), unit grid steps. +/// +/// Panics: +/// Panics unless `1 <= order <= 8`. +/// +/// Rust: `patterns::space_filling::moore_curve` +#[pyfunction] +#[pyo3(name = "moore_curve", signature = (order))] +pub fn pyfn_moore_curve(order: u32) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::space_filling::moore_curve(order)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// Gosper (flowsnake) curve, unit steps. +/// +/// Panics: +/// Panics unless `1 <= order <= 6`. +/// +/// Rust: `patterns::space_filling::gosper_curve` +#[pyfunction] +#[pyo3(name = "gosper_curve", signature = (order))] +pub fn pyfn_gosper_curve(order: u32) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::space_filling::gosper_curve(order)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// Sorts points by their Hilbert index on a `2^order` grid over the +/// bounding box. +/// +/// Panics: +/// Panics unless `1 <= order <= 31`. +/// +/// Rust: `patterns::space_filling::sort_by_hilbert` +#[pyfunction] +#[pyo3(name = "sort_by_hilbert", signature = (points, order))] +pub fn pyfn_sort_by_hilbert<'py>(points: pyo3::Bound<'py, pyo3::PyAny>, order: u32) -> PyResult<()> { + let mut points__v: Vec = points.extract::>()?.into_iter().map(|__e| __e.0).collect(); + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::space_filling::sort_by_hilbert(&mut points__v, order)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + crate::runtime::coerce::write_back_objects(&points, points__v.into_iter().map(|__e| crate::generated::types::PyVec2 { inner: __e }).collect::>())?; + Ok(()) +} + +/// Sorts 3-D points by Morton code (21 bits per axis over the +/// bounding box). +/// +/// Rust: `patterns::space_filling::sort_by_morton` +#[pyfunction] +#[pyo3(name = "sort_by_morton", signature = (points))] +pub fn pyfn_sort_by_morton<'py>(points: pyo3::Bound<'py, pyo3::PyAny>) -> PyResult<()> { + let mut points__v: Vec = points.extract::>()?.into_iter().map(|__e| __e.0).collect(); + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::space_filling::sort_by_morton(&mut points__v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + crate::runtime::coerce::write_back_objects(&points, points__v.into_iter().map(|__e| crate::generated::types::PyVec3 { inner: __e }).collect::>())?; + Ok(()) +} + +/// Locality measure of the Hilbert order: mean |index difference| +/// (normalized by the index range) divided by mean spatial distance +/// (normalized by the bounding-box diagonal) over all point pairs. +/// Lower means indices track spatial proximity better. +/// +/// Panics: +/// Panics unless `1 <= order <= 31` and at least 2 points are given. +/// +/// Rust: `patterns::space_filling::hilbert_locality_ratio` +#[pyfunction] +#[pyo3(name = "hilbert_locality_ratio", signature = (points, order))] +pub fn pyfn_hilbert_locality_ratio<'py>(py: Python<'py>, points: Vec, order: u32) -> PyResult { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::patterns::space_filling::hilbert_locality_ratio(&points, order))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_hilbert_d2xy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hilbert_xy2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hilbert_curve_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hilbert_3d_d2xyz, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hilbert_3d_xyz2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hilbert_curve_3d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_peano_curve, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_morton_encode_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_morton_decode_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_morton_encode_3d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_morton_decode_3d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_z_order_curve, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gray_code, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gray_decode, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sierpinski_curve, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_moore_curve, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gosper_curve, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sort_by_hilbert, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sort_by_morton, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hilbert_locality_ratio, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_patterns__symmetry.rs b/bindings/python/src/generated/m_patterns__symmetry.rs new file mode 100644 index 0000000..d382e68 --- /dev/null +++ b/bindings/python/src/generated/m_patterns__symmetry.rs @@ -0,0 +1,247 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// The coset representatives of the wallpaper group's operations in +/// unit-cell coordinates (closed under composition modulo unit +/// translations; the centered groups include their centering +/// translation). +/// +/// Rust: `patterns::symmetry::wallpaper_generators` +#[pyfunction] +#[pyo3(name = "wallpaper_generators", signature = (g))] +pub fn pyfn_wallpaper_generators(g: crate::generated::types::PyWallpaperGroup) -> PyResult> { + let g = g.to_rust(); + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::symmetry::wallpaper_generators(g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyAffine2 { inner: __x }).collect::>()) +} + +/// The order of the returned operation set. +/// +/// Rust: `patterns::symmetry::wallpaper_group_order` +#[pyfunction] +#[pyo3(name = "wallpaper_group_order", signature = (g))] +pub fn pyfn_wallpaper_group_order(g: crate::generated::types::PyWallpaperGroup) -> PyResult { + let g = g.to_rust(); + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::symmetry::wallpaper_group_order(g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// A natural lattice for the group at the given scale: square for the +/// tetragonal groups, hexagonal (120°) for the tri/hexagonal groups, +/// rectangular otherwise. +/// +/// Rust: `patterns::symmetry::wallpaper_lattice` +#[pyfunction] +#[pyo3(name = "wallpaper_lattice", signature = (g, scale))] +pub fn pyfn_wallpaper_lattice(g: crate::generated::types::PyWallpaperGroup, scale: f64) -> PyResult { + let g = g.to_rust(); + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::symmetry::wallpaper_lattice(g, scale)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLattice { inner: __v }) +} + +/// A fundamental domain in unit-cell coordinates with area 1/order of +/// the cell. For the rectangular-cell groups it is a genuine +/// fundamental domain; for the centered and hexagonal groups it is an +/// area-correct representative slab (one valid choice among many +/// shapes). +/// +/// Rust: `patterns::symmetry::wallpaper_fundamental_domain` +#[pyfunction] +#[pyo3(name = "wallpaper_fundamental_domain", signature = (g))] +pub fn pyfn_wallpaper_fundamental_domain(g: crate::generated::types::PyWallpaperGroup) -> PyResult { + let g = g.to_rust(); + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::symmetry::wallpaper_fundamental_domain(g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolygon2 { inner: __v }) +} + +/// Tiles a motif (given in unit-cell coordinates) by the group and +/// lattice over the extent: every group operation applied to every +/// motif polygon, replicated over the lattice translations whose cell +/// origin falls in the extent. +/// +/// Rust: `patterns::symmetry::tile_motif` +#[pyfunction] +#[pyo3(name = "tile_motif", signature = (g, motif, lattice, extent))] +pub fn pyfn_tile_motif(g: crate::generated::types::PyWallpaperGroup, motif: Vec, lattice: crate::generated::types::PyLattice, extent: crate::generated::types::PyRect) -> PyResult> { + let g = g.to_rust(); + let motif = motif.into_iter().map(|__e| __e.inner).collect::>(); + let lattice = lattice.inner; + let extent = extent.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::symmetry::tile_motif(g, &motif, &lattice, &extent)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyPolygon2 { inner: __x }).collect::>()) +} + +/// Tiles a point set (unit-cell coordinates) by the group and lattice +/// over the extent. +/// +/// Rust: `patterns::symmetry::tile_points` +#[pyfunction] +#[pyo3(name = "tile_points", signature = (g, points, lattice, extent))] +pub fn pyfn_tile_points(g: crate::generated::types::PyWallpaperGroup, points: Vec, lattice: crate::generated::types::PyLattice, extent: crate::generated::types::PyRect) -> PyResult> { + let g = g.to_rust(); + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let lattice = lattice.inner; + let extent = extent.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::symmetry::tile_points(g, &points, &lattice, &extent)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// Frieze group operations modulo the period translation, in world +/// coordinates with the frieze axis along x and period `period`. +/// +/// Rust: `patterns::symmetry::frieze_generators` +#[pyfunction] +#[pyo3(name = "frieze_generators", signature = (g, period))] +pub fn pyfn_frieze_generators(g: crate::generated::types::PyFriezeGroup, period: f64) -> PyResult> { + let g = g.to_rust(); + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::symmetry::frieze_generators(g, period)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyAffine2 { inner: __x }).collect::>()) +} + +/// Replicates a motif under the frieze group for `count` periods +/// (translations 0..count). +/// +/// Rust: `patterns::symmetry::frieze_motif` +#[pyfunction] +#[pyo3(name = "frieze_motif", signature = (g, motif, period, count))] +pub fn pyfn_frieze_motif(g: crate::generated::types::PyFriezeGroup, motif: Vec, period: f64, count: usize) -> PyResult> { + let g = g.to_rust(); + let motif = motif.into_iter().map(|__e| __e.inner).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::symmetry::frieze_motif(g, &motif, period, count)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyPolygon2 { inner: __x }).collect::>()) +} + +/// Rosette symmetry: the motif under the cyclic group C_n (rotations) +/// or dihedral D_n (`mirror` adds reflections), about the origin. +/// +/// Panics: +/// Panics unless `n >= 1`. +/// +/// Rust: `patterns::symmetry::rosette` +#[pyfunction] +#[pyo3(name = "rosette", signature = (motif, n, mirror))] +pub fn pyfn_rosette(motif: Vec, n: u32, mirror: bool) -> PyResult> { + let motif = motif.into_iter().map(|__e| __e.inner).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::symmetry::rosette(&motif, n, mirror)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyPolygon2 { inner: __x }).collect::>()) +} + +/// Detects rotations and reflections about the centroid that map the +/// point set to itself within `tol`. Checks rotation orders up to the +/// point count and reflection axes through point/midpoint directions. +/// +/// Rust: `patterns::symmetry::detect_symmetries_2d` +#[pyfunction] +#[pyo3(name = "detect_symmetries_2d", signature = (points, tol))] +pub fn pyfn_detect_symmetries_2d(points: Vec, tol: f64) -> PyResult> { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::symmetry::detect_symmetries_2d(&points, tol)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyAffine2 { inner: __x }).collect::>()) +} + +/// All proper rotations of the point group as quaternions (generated +/// by closure from the group's standard generators). +/// +/// Rust: `patterns::symmetry::point_group_rotations` +#[pyfunction] +#[pyo3(name = "point_group_rotations", signature = (g))] +pub fn pyfn_point_group_rotations(g: crate::generated::types::PyPointGroup3) -> PyResult> { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::symmetry::point_group_rotations(g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyQuaternion { inner: __x }).collect::>()) +} + +/// The full group order (including improper operations for the +/// mirror-bearing groups). +/// +/// Rust: `patterns::symmetry::point_group_order` +#[pyfunction] +#[pyo3(name = "point_group_order", signature = (g))] +pub fn pyfn_point_group_order(g: crate::generated::types::PyPointGroup3) -> PyResult { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::symmetry::point_group_order(g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Orbit of a point under the group's rotations (deduplicated within +/// 1e-9 of the point scale). +/// +/// Rust: `patterns::symmetry::point_group_orbit` +#[pyfunction] +#[pyo3(name = "point_group_orbit", signature = (g, p))] +pub fn pyfn_point_group_orbit(g: crate::generated::types::PyPointGroup3, p: crate::generated::types::PyVec3Arg) -> PyResult> { + let g = g.inner; + let p = p.0; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::symmetry::point_group_orbit(g, p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) +} + +/// Hankin's method for Islamic star patterns (after Kaplan): from two +/// points straddling each edge midpoint (offset `delta` along the +/// edge), rays leave into the polygon at `contact_angle` from the +/// edge; consecutive rays around the polygon are intersected to form +/// the strap segments. +/// +/// Panics: +/// Panics unless `0 < contact_angle < π/2` and `delta >= 0`. +/// +/// Rust: `patterns::symmetry::hankin_star_pattern` +#[pyfunction] +#[pyo3(name = "hankin_star_pattern", signature = (tiling, contact_angle, delta))] +pub fn pyfn_hankin_star_pattern(tiling: crate::generated::types::PyTiling, contact_angle: f64, delta: f64) -> PyResult> { + let tiling = tiling.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::symmetry::hankin_star_pattern(&tiling, contact_angle, delta)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyPrimitivesSegment2 { inner: __x }).collect::>()) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_wallpaper_generators, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wallpaper_group_order, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wallpaper_lattice, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wallpaper_fundamental_domain, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tile_motif, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tile_points, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_frieze_generators, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_frieze_motif, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rosette, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_detect_symmetries_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_point_group_rotations, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_point_group_order, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_point_group_orbit, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hankin_star_pattern, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_patterns__tilings.rs b/bindings/python/src/generated/m_patterns__tilings.rs new file mode 100644 index 0000000..86aeef9 --- /dev/null +++ b/bindings/python/src/generated/m_patterns__tilings.rs @@ -0,0 +1,169 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Square grid of `nx` x `ny` cells with the given cell size. +/// +/// Rust: `patterns::tilings::square_grid` +#[pyfunction] +#[pyo3(name = "square_grid", signature = (nx, ny, size))] +pub fn pyfn_square_grid(nx: usize, ny: usize, size: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::tilings::square_grid(nx, ny, size)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTiling { inner: __v }) +} + +/// Triangular grid: `nx` x `ny` rhombi split into unit triangles of +/// the given edge length. +/// +/// Rust: `patterns::tilings::triangular_grid` +#[pyfunction] +#[pyo3(name = "triangular_grid", signature = (nx, ny, size))] +pub fn pyfn_triangular_grid(nx: usize, ny: usize, size: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::tilings::triangular_grid(nx, ny, size)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTiling { inner: __v }) +} + +/// Hexagonal grid: `nx` x `ny` hexagons of circumradius `size`. +/// `pointy_top` orients a vertex upward; otherwise an edge is up. +/// +/// Rust: `patterns::tilings::hexagonal_grid` +#[pyfunction] +#[pyo3(name = "hexagonal_grid", signature = (nx, ny, size, pointy_top))] +pub fn pyfn_hexagonal_grid(nx: usize, ny: usize, size: f64, pointy_top: bool) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::tilings::hexagonal_grid(nx, ny, size, pointy_top)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTiling { inner: __v }) +} + +/// Archimedean (uniform) tiling of the given kind with edge length +/// `size`, covering `extent` (faces with centroid inside). +/// +/// Rust: `patterns::tilings::archimedean` +#[pyfunction] +#[pyo3(name = "archimedean", signature = (kind, extent, size))] +pub fn pyfn_archimedean(kind: crate::generated::types::PyArchimedean, extent: crate::generated::types::PyRect, size: f64) -> PyResult { + let kind = kind.to_rust(); + let extent = extent.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::tilings::archimedean(kind, &extent, size)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTiling { inner: __v }) +} + +/// Laves tiling: the dual of the corresponding Archimedean tiling. +/// +/// Rust: `patterns::tilings::laves` +#[pyfunction] +#[pyo3(name = "laves", signature = (kind, extent, size))] +pub fn pyfn_laves(kind: crate::generated::types::PyArchimedean, extent: crate::generated::types::PyRect, size: f64) -> PyResult { + let kind = kind.to_rust(); + let extent = extent.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::tilings::laves(kind, &extent, size)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTiling { inner: __v }) +} + +/// All hexes within `radius` of `center` (hex-distance ball). +/// +/// Panics: +/// Panics for negative radius. +/// +/// Rust: `patterns::tilings::hex_range` +#[pyfunction] +#[pyo3(name = "hex_range", signature = (center, radius))] +pub fn pyfn_hex_range(center: crate::generated::types::PyHex, radius: i32) -> PyResult> { + let center = center.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::tilings::hex_range(center, radius)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyHex { inner: __x }).collect::>()) +} + +/// Cairo pentagonal tiling: the dual of the snub square tiling. +/// +/// Rust: `patterns::tilings::cairo_pentagonal` +#[pyfunction] +#[pyo3(name = "cairo_pentagonal", signature = (extent, size))] +pub fn pyfn_cairo_pentagonal(extent: crate::generated::types::PyRect, size: f64) -> PyResult { + let extent = extent.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::tilings::cairo_pentagonal(&extent, size)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTiling { inner: __v }) +} + +/// Rhombille (tumbling blocks) tiling: the dual of the trihexagonal +/// tiling. +/// +/// Rust: `patterns::tilings::rhombille` +#[pyfunction] +#[pyo3(name = "rhombille", signature = (extent, size))] +pub fn pyfn_rhombille(extent: crate::generated::types::PyRect, size: f64) -> PyResult { + let extent = extent.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::tilings::rhombille(&extent, size)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTiling { inner: __v }) +} + +/// Running-bond brick pattern: rows of `w` x `h` bricks, each row +/// shifted by `offset` (in units of `w`). +/// +/// Panics: +/// Panics unless `w, h > 0`. +/// +/// Rust: `patterns::tilings::brick` +#[pyfunction] +#[pyo3(name = "brick", signature = (extent, w, h, offset))] +pub fn pyfn_brick(extent: crate::generated::types::PyRect, w: f64, h: f64, offset: f64) -> PyResult { + let extent = extent.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::tilings::brick(&extent, w, h, offset)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTiling { inner: __v }) +} + +/// Herringbone pattern of `w` x `h` bricks (alternating horizontal +/// and vertical along the diagonals). +/// +/// Panics: +/// Panics unless `0 < h < w`. +/// +/// Rust: `patterns::tilings::herringbone` +#[pyfunction] +#[pyo3(name = "herringbone", signature = (extent, w, h))] +pub fn pyfn_herringbone(extent: crate::generated::types::PyRect, w: f64, h: f64) -> PyResult { + let extent = extent.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::tilings::herringbone(&extent, w, h)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTiling { inner: __v }) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_square_grid, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_triangular_grid, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hexagonal_grid, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_archimedean, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_laves, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hex_range, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cairo_pentagonal, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rhombille, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_brick, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_herringbone, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_photonics.rs b/bindings/python/src/generated/m_photonics.rs new file mode 100644 index 0000000..e1d1d5a --- /dev/null +++ b/bindings/python/src/generated/m_photonics.rs @@ -0,0 +1,319 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Beam waist from far-field divergence: w₀ = λ/(π×θ) +/// +/// Rust: `photonics::beam_waist_from_divergence` +#[pyfunction] +#[pyo3(name = "beam_waist_from_divergence", signature = (wavelength, divergence_half_angle))] +pub fn pyfn_beam_waist_from_divergence(wavelength: f64, divergence_half_angle: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::photonics::beam_waist_from_divergence(wavelength, divergence_half_angle)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Rayleigh range: z_R = πw₀²/λ +/// +/// Rust: `photonics::rayleigh_range` +#[pyfunction] +#[pyo3(name = "rayleigh_range", signature = (waist, wavelength))] +pub fn pyfn_rayleigh_range(waist: f64, wavelength: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::photonics::rayleigh_range(waist, wavelength)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Beam radius at axial position z: w(z) = w₀√(1+(z/z_R)²) +/// +/// Rust: `photonics::beam_radius` +#[pyfunction] +#[pyo3(name = "beam_radius", signature = (waist, z, rayleigh))] +pub fn pyfn_beam_radius(waist: f64, z: f64, rayleigh: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::photonics::beam_radius(waist, z, rayleigh)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Far-field half-angle divergence: θ = λ/(πw₀) +/// +/// Rust: `photonics::beam_divergence` +#[pyfunction] +#[pyo3(name = "beam_divergence", signature = (waist, wavelength))] +pub fn pyfn_beam_divergence(waist: f64, wavelength: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::photonics::beam_divergence(waist, wavelength)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Radius of curvature of the wavefront: R(z) = z(1+(z_R/z)²) +/// +/// Rust: `photonics::beam_curvature` +#[pyfunction] +#[pyo3(name = "beam_curvature", signature = (z, rayleigh))] +pub fn pyfn_beam_curvature(z: f64, rayleigh: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::photonics::beam_curvature(z, rayleigh)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Gouy phase shift: ψ(z) = atan(z/z_R) +/// +/// Rust: `photonics::gouy_phase` +#[pyfunction] +#[pyo3(name = "gouy_phase", signature = (z, rayleigh))] +pub fn pyfn_gouy_phase(z: f64, rayleigh: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::photonics::gouy_phase(z, rayleigh)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Peak-normalized Gaussian beam intensity at radial offset r and axial position z: +/// I = (2P/(πw²)) exp(-2r²/w²), where w = w(z). +/// +/// Rust: `photonics::beam_intensity` +#[pyfunction] +#[pyo3(name = "beam_intensity", signature = (power, waist, r, z, rayleigh))] +pub fn pyfn_beam_intensity(power: f64, waist: f64, r: f64, z: f64, rayleigh: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::photonics::beam_intensity(power, waist, r, z, rayleigh)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Combined beam parameter: returns (w(z), R(z)) at axial position z. +/// +/// Rust: `photonics::beam_parameter` +#[pyfunction] +#[pyo3(name = "beam_parameter", signature = (waist, z, rayleigh))] +pub fn pyfn_beam_parameter(waist: f64, z: f64, rayleigh: f64) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::photonics::beam_parameter(waist, z, rayleigh)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Numerical aperture of a step-index fiber: NA = √(n_core² - n_clad²) +/// +/// Rust: `photonics::numerical_aperture` +#[pyfunction] +#[pyo3(name = "numerical_aperture", signature = (n_core, n_clad))] +pub fn pyfn_numerical_aperture(n_core: f64, n_clad: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::photonics::numerical_aperture(n_core, n_clad)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Maximum acceptance half-angle: θ_max = arcsin(NA) +/// +/// Rust: `photonics::acceptance_angle` +#[pyfunction] +#[pyo3(name = "acceptance_angle", signature = (na))] +pub fn pyfn_acceptance_angle(na: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::photonics::acceptance_angle(na)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Normalized frequency (V-number): V = 2πr×NA/λ +/// +/// Rust: `photonics::v_number` +#[pyfunction] +#[pyo3(name = "v_number", signature = (radius, na, wavelength))] +pub fn pyfn_v_number(radius: f64, na: f64, wavelength: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::photonics::v_number(radius, na, wavelength)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// True when the fiber supports only the fundamental mode (V < 2.405). +/// +/// Rust: `photonics::is_single_mode` +#[pyfunction] +#[pyo3(name = "is_single_mode", signature = (v_number))] +pub fn pyfn_is_single_mode(v_number: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::photonics::is_single_mode(v_number)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Approximate mode count for a step-index multimode fiber: M ≈ V²/2 +/// +/// Rust: `photonics::number_of_modes` +#[pyfunction] +#[pyo3(name = "number_of_modes", signature = (v_number))] +pub fn pyfn_number_of_modes(v_number: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::photonics::number_of_modes(v_number)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Output power after propagation through a lossy fiber: +/// P_out = P_in × 10^(-αL/10), where α is in dB/km and L in km. +/// +/// Rust: `photonics::fiber_attenuation` +#[pyfunction] +#[pyo3(name = "fiber_attenuation", signature = (input_power, attenuation_db_per_km, length_km))] +pub fn pyfn_fiber_attenuation(input_power: f64, attenuation_db_per_km: f64, length_km: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::photonics::fiber_attenuation(input_power, attenuation_db_per_km, length_km)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Chromatic dispersion pulse broadening: Δt = D × L × Δλ +/// D in ps/(nm·km), L in km, Δλ in nm → Δt in ps. +/// +/// Rust: `photonics::dispersion_broadening` +#[pyfunction] +#[pyo3(name = "dispersion_broadening", signature = (dispersion, length, spectral_width))] +pub fn pyfn_dispersion_broadening(dispersion: f64, length: f64, spectral_width: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::photonics::dispersion_broadening(dispersion, length, spectral_width)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Critical angle for total internal reflection inside the fiber core: +/// θc = arcsin(n_clad / n_core) +/// +/// Rust: `photonics::critical_angle_fiber` +#[pyfunction] +#[pyo3(name = "critical_angle_fiber", signature = (n_core, n_clad))] +pub fn pyfn_critical_angle_fiber(n_core: f64, n_clad: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::photonics::critical_angle_fiber(n_core, n_clad)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Thin lens matrix: [[1, 0], [-1/f, 1]] +/// +/// Rust: `photonics::thin_lens_matrix` +#[pyfunction] +#[pyo3(name = "thin_lens_matrix", signature = (focal_length))] +pub fn pyfn_thin_lens_matrix<'py>(py: Python<'py>, focal_length: f64) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::photonics::thin_lens_matrix(focal_length))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.to_vec()).collect::>()) +} + +/// Free-space propagation matrix: [[1, d], [0, 1]] +/// +/// Rust: `photonics::free_space_matrix` +#[pyfunction] +#[pyo3(name = "free_space_matrix", signature = (distance))] +pub fn pyfn_free_space_matrix<'py>(py: Python<'py>, distance: f64) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::photonics::free_space_matrix(distance))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.to_vec()).collect::>()) +} + +/// Image distance for a thick lens via ABCD matrix composition. +/// +/// Constructs the system matrix from: refraction at R1, propagation through +/// the lens of thickness `t` and index `n`, refraction at R2, then solves +/// for the image distance using the thin-lens-equivalent focal length. +/// +/// Rust: `photonics::image_distance_thick_lens` +#[pyfunction] +#[pyo3(name = "image_distance_thick_lens", signature = (n, r1, r2, thickness, object_dist))] +pub fn pyfn_image_distance_thick_lens(n: f64, r1: f64, r2: f64, thickness: f64, object_dist: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::photonics::image_distance_thick_lens(n, r1, r2, thickness, object_dist)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Temporal coherence length: L_c = λ²/Δλ +/// +/// Rust: `photonics::coherence_length` +#[pyfunction] +#[pyo3(name = "coherence_length", signature = (wavelength, bandwidth))] +pub fn pyfn_coherence_length(wavelength: f64, bandwidth: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::photonics::coherence_length(wavelength, bandwidth)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Coherence time from frequency bandwidth: τ_c = 1/Δf +/// +/// Rust: `photonics::coherence_time` +#[pyfunction] +#[pyo3(name = "coherence_time", signature = (bandwidth_hz))] +pub fn pyfn_coherence_time(bandwidth_hz: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::photonics::coherence_time(bandwidth_hz)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Fringe visibility (contrast): V = (I_max - I_min) / (I_max + I_min) +/// +/// Rust: `photonics::fringe_visibility` +#[pyfunction] +#[pyo3(name = "fringe_visibility", signature = (i_max, i_min))] +pub fn pyfn_fringe_visibility(i_max: f64, i_min: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::photonics::fringe_visibility(i_max, i_min)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Fabry-Perot etalon transmission (Airy function): +/// T = (1-R)² / ((1-R)² + 4R sin²(δ/2)) +/// +/// Rust: `photonics::fabry_perot_transmission` +#[pyfunction] +#[pyo3(name = "fabry_perot_transmission", signature = (reflectance, phase))] +pub fn pyfn_fabry_perot_transmission(reflectance: f64, phase: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::photonics::fabry_perot_transmission(reflectance, phase)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Free spectral range of a Fabry-Perot cavity: FSR = c/(2nL) in Hz. +/// +/// Rust: `photonics::free_spectral_range` +#[pyfunction] +#[pyo3(name = "free_spectral_range", signature = (cavity_length, n))] +pub fn pyfn_free_spectral_range(cavity_length: f64, n: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::photonics::free_spectral_range(cavity_length, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_beam_waist_from_divergence, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rayleigh_range, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_beam_radius, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_beam_divergence, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_beam_curvature, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gouy_phase, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_beam_intensity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_beam_parameter, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_numerical_aperture, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_acceptance_angle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_v_number, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_is_single_mode, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_number_of_modes, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fiber_attenuation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dispersion_broadening, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_critical_angle_fiber, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_thin_lens_matrix, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_free_space_matrix, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_image_distance_thick_lens, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_coherence_length, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_coherence_time, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fringe_visibility, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fabry_perot_transmission, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_free_spectral_range, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_plasma.rs b/bindings/python/src/generated/m_plasma.rs new file mode 100644 index 0000000..fc39857 --- /dev/null +++ b/bindings/python/src/generated/m_plasma.rs @@ -0,0 +1,214 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// λD = √(ε₀kT / (nq²)) +/// +/// Rust: `plasma::debye_length` +#[pyfunction] +#[pyo3(name = "debye_length", signature = (temperature, density, charge))] +pub fn pyfn_debye_length(temperature: f64, density: f64, charge: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::plasma::debye_length(temperature, density, charge)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// ωp = √(ne² / (mₑε₀)) +/// +/// Rust: `plasma::plasma_frequency_electron` +#[pyfunction] +#[pyo3(name = "plasma_frequency_electron", signature = (density))] +pub fn pyfn_plasma_frequency_electron(density: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::plasma::plasma_frequency_electron(density)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// ωp = √(ne² / (m_ion × ε₀)) +/// +/// Rust: `plasma::plasma_frequency_ion` +#[pyfunction] +#[pyo3(name = "plasma_frequency_ion", signature = (density, ion_mass))] +pub fn pyfn_plasma_frequency_ion(density: f64, ion_mass: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::plasma::plasma_frequency_ion(density, ion_mass)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// ωc = eB / mₑ +/// +/// Rust: `plasma::cyclotron_frequency_electron` +#[pyfunction] +#[pyo3(name = "cyclotron_frequency_electron", signature = (b_field))] +pub fn pyfn_cyclotron_frequency_electron(b_field: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::plasma::cyclotron_frequency_electron(b_field)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// ωc = qB / m +/// +/// Rust: `plasma::cyclotron_frequency_ion` +#[pyfunction] +#[pyo3(name = "cyclotron_frequency_ion", signature = (b_field, ion_mass, charge))] +pub fn pyfn_cyclotron_frequency_ion(b_field: f64, ion_mass: f64, charge: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::plasma::cyclotron_frequency_ion(b_field, ion_mass, charge)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// rL = mv⊥ / (|q|B) +/// +/// Rust: `plasma::larmor_radius` +#[pyfunction] +#[pyo3(name = "larmor_radius", signature = (velocity_perp, mass, charge, b_field))] +pub fn pyfn_larmor_radius(velocity_perp: f64, mass: f64, charge: f64, b_field: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::plasma::larmor_radius(velocity_perp, mass, charge, b_field)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// β = 2μ₀p / B² +/// +/// Rust: `plasma::plasma_beta` +#[pyfunction] +#[pyo3(name = "plasma_beta", signature = (pressure, b_field))] +pub fn pyfn_plasma_beta(pressure: f64, b_field: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::plasma::plasma_beta(pressure, b_field)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// vA = B / √(μ₀ρ) +/// +/// Rust: `plasma::alfven_speed` +#[pyfunction] +#[pyo3(name = "alfven_speed", signature = (b_field, density))] +pub fn pyfn_alfven_speed(b_field: f64, density: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::plasma::alfven_speed(b_field, density)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// cs = √(γkT / m) +/// +/// Rust: `plasma::sound_speed_plasma` +#[pyfunction] +#[pyo3(name = "sound_speed_plasma", signature = (gamma, temperature, ion_mass))] +pub fn pyfn_sound_speed_plasma(gamma: f64, temperature: f64, ion_mass: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::plasma::sound_speed_plasma(gamma, temperature, ion_mass)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// vth = √(2kT / m) +/// +/// Rust: `plasma::thermal_velocity` +#[pyfunction] +#[pyo3(name = "thermal_velocity", signature = (temperature, mass))] +pub fn pyfn_thermal_velocity(temperature: f64, mass: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::plasma::thermal_velocity(temperature, mass)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// ND = n × (4π/3)λD³ +/// +/// Rust: `plasma::debye_number` +#[pyfunction] +#[pyo3(name = "debye_number", signature = (density, debye_len))] +pub fn pyfn_debye_number(density: f64, debye_len: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::plasma::debye_number(density, debye_len)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// lnΛ = ln(12π × ND) +/// +/// Rust: `plasma::coulomb_logarithm` +#[pyfunction] +#[pyo3(name = "coulomb_logarithm", signature = (temperature, density))] +pub fn pyfn_coulomb_logarithm(temperature: f64, density: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::plasma::coulomb_logarithm(temperature, density)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Pm = B² / (2μ₀) +/// +/// Rust: `plasma::magnetic_pressure` +#[pyfunction] +#[pyo3(name = "magnetic_pressure", signature = (b_field))] +pub fn pyfn_magnetic_pressure(b_field: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::plasma::magnetic_pressure(b_field)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// vms = √(vA² + cs²) +/// +/// Rust: `plasma::magnetosonic_speed` +#[pyfunction] +#[pyo3(name = "magnetosonic_speed", signature = (alfven, sound))] +pub fn pyfn_magnetosonic_speed(alfven: f64, sound: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::plasma::magnetosonic_speed(alfven, sound)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// δ = c / ωp +/// +/// Rust: `plasma::skin_depth_plasma` +#[pyfunction] +#[pyo3(name = "skin_depth_plasma", signature = (plasma_freq))] +pub fn pyfn_skin_depth_plasma(plasma_freq: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::plasma::skin_depth_plasma(plasma_freq)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// ν = nq⁴lnΛ / (4πε₀²m²vth³) +/// +/// Rust: `plasma::collision_frequency` +#[pyfunction] +#[pyo3(name = "collision_frequency", signature = (density, temperature, coulomb_log, mass, charge))] +pub fn pyfn_collision_frequency(density: f64, temperature: f64, coulomb_log: f64, mass: f64, charge: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::plasma::collision_frequency(density, temperature, coulomb_log, mass, charge)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_debye_length, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_plasma_frequency_electron, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_plasma_frequency_ion, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cyclotron_frequency_electron, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cyclotron_frequency_ion, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_larmor_radius, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_plasma_beta, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_alfven_speed, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sound_speed_plasma, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_thermal_velocity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_debye_number, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_coulomb_logarithm, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_magnetic_pressure, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_magnetosonic_speed, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_skin_depth_plasma, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_collision_frequency, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_propulsion.rs b/bindings/python/src/generated/m_propulsion.rs new file mode 100644 index 0000000..f983baf --- /dev/null +++ b/bindings/python/src/generated/m_propulsion.rs @@ -0,0 +1,206 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Tsiolkovsky rocket equation: Δv = ve × ln(m0/mf) +/// +/// Rust: `propulsion::tsiolkovsky_delta_v` +#[pyfunction] +#[pyo3(name = "tsiolkovsky_delta_v", signature = (exhaust_velocity, mass_initial, mass_final))] +pub fn pyfn_tsiolkovsky_delta_v(exhaust_velocity: f64, mass_initial: f64, mass_final: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::propulsion::tsiolkovsky_delta_v(exhaust_velocity, mass_initial, mass_final)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Mass ratio from the rocket equation inverted: m0/mf = exp(Δv/ve) +/// +/// Rust: `propulsion::mass_ratio` +#[pyfunction] +#[pyo3(name = "mass_ratio", signature = (delta_v, exhaust_velocity))] +pub fn pyfn_mass_ratio(delta_v: f64, exhaust_velocity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::propulsion::mass_ratio(delta_v, exhaust_velocity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Specific impulse: Isp = F / (ṁ × g) +/// +/// Rust: `propulsion::specific_impulse` +#[pyfunction] +#[pyo3(name = "specific_impulse", signature = (thrust, mass_flow_rate, g))] +pub fn pyfn_specific_impulse(thrust: f64, mass_flow_rate: f64, g: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::propulsion::specific_impulse(thrust, mass_flow_rate, g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Effective exhaust velocity from specific impulse: ve = Isp × g +/// +/// Rust: `propulsion::exhaust_velocity_from_isp` +#[pyfunction] +#[pyo3(name = "exhaust_velocity_from_isp", signature = (isp, g))] +pub fn pyfn_exhaust_velocity_from_isp(isp: f64, g: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::propulsion::exhaust_velocity_from_isp(isp, g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Thrust from momentum: F = ṁ × ve +/// +/// Rust: `propulsion::thrust` +#[pyfunction] +#[pyo3(name = "thrust", signature = (mass_flow_rate, exhaust_velocity))] +pub fn pyfn_thrust(mass_flow_rate: f64, exhaust_velocity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::propulsion::thrust(mass_flow_rate, exhaust_velocity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Thrust including pressure term: F = ṁve + (Pe - Pa)Ae +/// +/// Rust: `propulsion::thrust_with_pressure` +#[pyfunction] +#[pyo3(name = "thrust_with_pressure", signature = (mass_flow_rate, exhaust_velocity, exit_pressure, ambient_pressure, exit_area))] +pub fn pyfn_thrust_with_pressure(mass_flow_rate: f64, exhaust_velocity: f64, exit_pressure: f64, ambient_pressure: f64, exit_area: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::propulsion::thrust_with_pressure(mass_flow_rate, exhaust_velocity, exit_pressure, ambient_pressure, exit_area)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Total Δv for a multi-stage rocket. Each element is (exhaust_velocity, mass_full, mass_empty). +/// +/// Rust: `propulsion::delta_v_staged` +#[pyfunction] +#[pyo3(name = "delta_v_staged", signature = (stages))] +pub fn pyfn_delta_v_staged<'py>(py: Python<'py>, stages: Vec<(f64, f64, f64)>) -> PyResult { + let stages = stages.into_iter().map(|__e| (__e.0, __e.1, __e.2)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::propulsion::delta_v_staged(&stages))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Hohmann transfer Δv values. Returns (Δv1, Δv2) for departure and arrival burns. +/// +/// Rust: `propulsion::hohmann_delta_v` +#[pyfunction] +#[pyo3(name = "hohmann_delta_v", signature = (mu, r1, r2))] +pub fn pyfn_hohmann_delta_v(mu: f64, r1: f64, r2: f64) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::propulsion::hohmann_delta_v(mu, r1, r2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Transfer time for a Hohmann orbit: t = π√((r1+r2)³ / (8μ)) +/// +/// Rust: `propulsion::hohmann_transfer_time` +#[pyfunction] +#[pyo3(name = "hohmann_transfer_time", signature = (mu, r1, r2))] +pub fn pyfn_hohmann_transfer_time(mu: f64, r1: f64, r2: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::propulsion::hohmann_transfer_time(mu, r1, r2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Gravity drag loss approximation: Δv_loss ≈ g × t +/// +/// Rust: `propulsion::gravity_turn_loss` +#[pyfunction] +#[pyo3(name = "gravity_turn_loss", signature = (g, burn_time))] +pub fn pyfn_gravity_turn_loss(g: f64, burn_time: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::propulsion::gravity_turn_loss(g, burn_time)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Plane change Δv: Δv = 2v × sin(θ/2) +/// +/// Rust: `propulsion::delta_v_plane_change` +#[pyfunction] +#[pyo3(name = "delta_v_plane_change", signature = (velocity, angle))] +pub fn pyfn_delta_v_plane_change(velocity: f64, angle: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::propulsion::delta_v_plane_change(velocity, angle)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Bi-elliptic transfer total Δv via three burns through an intermediate radius. +/// +/// Rust: `propulsion::bi_elliptic_delta_v` +#[pyfunction] +#[pyo3(name = "bi_elliptic_delta_v", signature = (mu, r1, r2, r_intermediate))] +pub fn pyfn_bi_elliptic_delta_v(mu: f64, r1: f64, r2: f64, r_intermediate: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::propulsion::bi_elliptic_delta_v(mu, r1, r2, r_intermediate)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Nozzle exit velocity from thermodynamic properties: +/// ve = √( 2γRT / (M(γ-1)) × (1 - (Pe/Pc)^((γ-1)/γ)) ) +/// +/// Rust: `propulsion::nozzle_exit_velocity` +#[pyfunction] +#[pyo3(name = "nozzle_exit_velocity", signature = (chamber_temp, molar_mass, gamma, pressure_ratio))] +pub fn pyfn_nozzle_exit_velocity(chamber_temp: f64, molar_mass: f64, gamma: f64, pressure_ratio: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::propulsion::nozzle_exit_velocity(chamber_temp, molar_mass, gamma, pressure_ratio)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Throat area required for a given mass flow: +/// A* = (ṁ / Pc) × √(R T / (γ M)) / (2/(γ+1))^((γ+1)/(2(γ-1))) +/// +/// Rust: `propulsion::throat_area` +#[pyfunction] +#[pyo3(name = "throat_area", signature = (mass_flow, chamber_pressure, chamber_temp, gamma, molar_mass))] +pub fn pyfn_throat_area(mass_flow: f64, chamber_pressure: f64, chamber_temp: f64, gamma: f64, molar_mass: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::propulsion::throat_area(mass_flow, chamber_pressure, chamber_temp, gamma, molar_mass)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Area ratio Ae/A* as a function of Mach number and heat capacity ratio: +/// Ae/A* = (1/M) × ((2/(γ+1)) × (1 + (γ-1)/2 × M²))^((γ+1)/(2(γ-1))) +/// +/// Rust: `propulsion::area_ratio_from_mach` +#[pyfunction] +#[pyo3(name = "area_ratio_from_mach", signature = (mach, gamma))] +pub fn pyfn_area_ratio_from_mach(mach: f64, gamma: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::propulsion::area_ratio_from_mach(mach, gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_tsiolkovsky_delta_v, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mass_ratio, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_specific_impulse, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_exhaust_velocity_from_isp, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_thrust, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_thrust_with_pressure, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_delta_v_staged, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hohmann_delta_v, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hohmann_transfer_time, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gravity_turn_loss, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_delta_v_plane_change, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bi_elliptic_delta_v, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_nozzle_exit_velocity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_throat_area, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_area_ratio_from_mach, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_quantum.rs b/bindings/python/src/generated/m_quantum.rs new file mode 100644 index 0000000..7cddc66 --- /dev/null +++ b/bindings/python/src/generated/m_quantum.rs @@ -0,0 +1,353 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// de Broglie wavelength: λ = h / p = h / (m * v) +/// +/// Rust: `quantum::de_broglie_wavelength` +#[pyfunction] +#[pyo3(name = "de_broglie_wavelength", signature = (mass, velocity))] +pub fn pyfn_de_broglie_wavelength(mass: f64, velocity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::de_broglie_wavelength(mass, velocity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// de Broglie wavelength from kinetic energy: λ = h / sqrt(2mE) +/// +/// Rust: `quantum::de_broglie_wavelength_from_energy` +#[pyfunction] +#[pyo3(name = "de_broglie_wavelength_from_energy", signature = (mass, kinetic_energy))] +pub fn pyfn_de_broglie_wavelength_from_energy(mass: f64, kinetic_energy: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::de_broglie_wavelength_from_energy(mass, kinetic_energy)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Photon momentum: p = h / λ = h*f / c +/// +/// Rust: `quantum::photon_momentum` +#[pyfunction] +#[pyo3(name = "photon_momentum", signature = (wavelength))] +pub fn pyfn_photon_momentum(wavelength: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::photon_momentum(wavelength)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Photon energy: E = h * f +/// +/// Rust: `quantum::photon_energy` +#[pyfunction] +#[pyo3(name = "photon_energy", signature = (frequency))] +pub fn pyfn_photon_energy(frequency: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::photon_energy(frequency)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Photon energy from wavelength: E = h * c / λ +/// +/// Rust: `quantum::photon_energy_from_wavelength` +#[pyfunction] +#[pyo3(name = "photon_energy_from_wavelength", signature = (wavelength))] +pub fn pyfn_photon_energy_from_wavelength(wavelength: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::photon_energy_from_wavelength(wavelength)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Photoelectric effect: KE_max = h*f - φ (work function) +/// Returns max kinetic energy of emitted electron. +/// +/// Rust: `quantum::photoelectric_ke` +#[pyfunction] +#[pyo3(name = "photoelectric_ke", signature = (frequency, work_function))] +pub fn pyfn_photoelectric_ke(frequency: f64, work_function: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::photoelectric_ke(frequency, work_function)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Threshold frequency: f_0 = φ / h +/// +/// Rust: `quantum::threshold_frequency` +#[pyfunction] +#[pyo3(name = "threshold_frequency", signature = (work_function))] +pub fn pyfn_threshold_frequency(work_function: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::threshold_frequency(work_function)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Threshold wavelength: λ_0 = h * c / φ +/// +/// Rust: `quantum::threshold_wavelength` +#[pyfunction] +#[pyo3(name = "threshold_wavelength", signature = (work_function))] +pub fn pyfn_threshold_wavelength(work_function: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::threshold_wavelength(work_function)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Stopping potential: V_s = KE_max / e +/// +/// Rust: `quantum::stopping_potential` +#[pyfunction] +#[pyo3(name = "stopping_potential", signature = (max_kinetic_energy))] +pub fn pyfn_stopping_potential(max_kinetic_energy: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::stopping_potential(max_kinetic_energy)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Heisenberg uncertainty principle (position-momentum): Δx * Δp ≥ ℏ/2 +/// Returns minimum uncertainty in momentum given position uncertainty. +/// +/// Rust: `quantum::min_momentum_uncertainty` +#[pyfunction] +#[pyo3(name = "min_momentum_uncertainty", signature = (position_uncertainty))] +pub fn pyfn_min_momentum_uncertainty(position_uncertainty: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::min_momentum_uncertainty(position_uncertainty)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Minimum position uncertainty given momentum uncertainty. +/// +/// Rust: `quantum::min_position_uncertainty` +#[pyfunction] +#[pyo3(name = "min_position_uncertainty", signature = (momentum_uncertainty))] +pub fn pyfn_min_position_uncertainty(momentum_uncertainty: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::min_position_uncertainty(momentum_uncertainty)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Energy-time uncertainty: ΔE * Δt ≥ ℏ/2 +/// +/// Rust: `quantum::min_energy_uncertainty` +#[pyfunction] +#[pyo3(name = "min_energy_uncertainty", signature = (time_uncertainty))] +pub fn pyfn_min_energy_uncertainty(time_uncertainty: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::min_energy_uncertainty(time_uncertainty)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Minimum time uncertainty given energy uncertainty: Δt ≥ ℏ / (2ΔE) +/// +/// Rust: `quantum::min_time_uncertainty` +#[pyfunction] +#[pyo3(name = "min_time_uncertainty", signature = (energy_uncertainty))] +pub fn pyfn_min_time_uncertainty(energy_uncertainty: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::min_time_uncertainty(energy_uncertainty)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Bohr radius: a_0 = ℏ^2 / (m_e * k_e * e^2) +/// +/// Rust: `quantum::bohr_radius` +#[pyfunction] +#[pyo3(name = "bohr_radius", signature = ())] +pub fn pyfn_bohr_radius() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::bohr_radius()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Energy levels of hydrogen atom: E_n = -13.6 eV / n^2 +/// Returns energy in Joules. +/// +/// Rust: `quantum::hydrogen_energy_level` +#[pyfunction] +#[pyo3(name = "hydrogen_energy_level", signature = (n))] +pub fn pyfn_hydrogen_energy_level(n: u32) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::hydrogen_energy_level(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Energy of photon emitted in hydrogen transition: E = 13.6 eV * (1/n_f^2 - 1/n_i^2) +/// Returns energy in Joules (positive for emission when n_i > n_f). +/// +/// Rust: `quantum::hydrogen_transition_energy` +#[pyfunction] +#[pyo3(name = "hydrogen_transition_energy", signature = (n_initial, n_final))] +pub fn pyfn_hydrogen_transition_energy(n_initial: u32, n_final: u32) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::hydrogen_transition_energy(n_initial, n_final)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Wavelength of photon from hydrogen transition (Rydberg formula): +/// 1/λ = R_H * (1/n_f^2 - 1/n_i^2) +/// +/// Rust: `quantum::hydrogen_transition_wavelength` +#[pyfunction] +#[pyo3(name = "hydrogen_transition_wavelength", signature = (n_initial, n_final))] +pub fn pyfn_hydrogen_transition_wavelength(n_initial: u32, n_final: u32) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::hydrogen_transition_wavelength(n_initial, n_final)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Orbital radius of nth level in hydrogen: r_n = n^2 * a_0 +/// +/// Rust: `quantum::hydrogen_orbital_radius` +#[pyfunction] +#[pyo3(name = "hydrogen_orbital_radius", signature = (n))] +pub fn pyfn_hydrogen_orbital_radius(n: u32) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::hydrogen_orbital_radius(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Orbital velocity in nth Bohr orbit: v_n = e^2 / (4πε_0 * n * ℏ) +/// +/// Rust: `quantum::hydrogen_orbital_velocity` +#[pyfunction] +#[pyo3(name = "hydrogen_orbital_velocity", signature = (n))] +pub fn pyfn_hydrogen_orbital_velocity(n: u32) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::hydrogen_orbital_velocity(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Transmission coefficient for a rectangular barrier (approximate, E < V): +/// T ≈ e^(-2κL) where κ = sqrt(2m(V-E)) / ℏ +/// +/// Rust: `quantum::tunneling_transmission` +#[pyfunction] +#[pyo3(name = "tunneling_transmission", signature = (mass, barrier_height, particle_energy, barrier_width))] +pub fn pyfn_tunneling_transmission(mass: f64, barrier_height: f64, particle_energy: f64, barrier_width: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::tunneling_transmission(mass, barrier_height, particle_energy, barrier_width)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Energy levels of a particle in a 1D infinite potential well: +/// E_n = n^2 * π^2 * ℏ^2 / (2 * m * L^2) +/// +/// Rust: `quantum::particle_in_box_energy` +#[pyfunction] +#[pyo3(name = "particle_in_box_energy", signature = (n, mass, box_length))] +pub fn pyfn_particle_in_box_energy(n: u32, mass: f64, box_length: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::particle_in_box_energy(n, mass, box_length)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Zero-point energy (ground state, n=1): +/// +/// Rust: `quantum::zero_point_energy` +#[pyfunction] +#[pyo3(name = "zero_point_energy", signature = (mass, box_length))] +pub fn pyfn_zero_point_energy(mass: f64, box_length: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::zero_point_energy(mass, box_length)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Compton wavelength shift: Δλ = (h / (m_e * c)) * (1 - cos(θ)) +/// +/// Rust: `quantum::compton_wavelength_shift` +#[pyfunction] +#[pyo3(name = "compton_wavelength_shift", signature = (scattering_angle_rad))] +pub fn pyfn_compton_wavelength_shift(scattering_angle_rad: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::compton_wavelength_shift(scattering_angle_rad)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Compton wavelength of the electron: λ_C = h / (m_e * c) +/// +/// Rust: `quantum::compton_wavelength_electron` +#[pyfunction] +#[pyo3(name = "compton_wavelength_electron", signature = ())] +pub fn pyfn_compton_wavelength_electron() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::compton_wavelength_electron()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Wien's displacement law: λ_max = b / T where b ≈ 2.898e-3 m·K +/// +/// Rust: `quantum::wien_peak_wavelength` +#[pyfunction] +#[pyo3(name = "wien_peak_wavelength", signature = (temperature))] +pub fn pyfn_wien_peak_wavelength(temperature: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::wien_peak_wavelength(temperature)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Stefan-Boltzmann law (total power): P = σ * A * T^4 +/// +/// Rust: `quantum::blackbody_power` +#[pyfunction] +#[pyo3(name = "blackbody_power", signature = (area, temperature))] +pub fn pyfn_blackbody_power(area: f64, temperature: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::blackbody_power(area, temperature)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Planck's law (spectral radiance): B(λ,T) = (2hc^2/λ^5) / (e^(hc/(λkT)) - 1) +/// +/// Rust: `quantum::planck_spectral_radiance` +#[pyfunction] +#[pyo3(name = "planck_spectral_radiance", signature = (wavelength, temperature))] +pub fn pyfn_planck_spectral_radiance(wavelength: f64, temperature: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::planck_spectral_radiance(wavelength, temperature)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_de_broglie_wavelength, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_de_broglie_wavelength_from_energy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_photon_momentum, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_photon_energy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_photon_energy_from_wavelength, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_photoelectric_ke, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_threshold_frequency, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_threshold_wavelength, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_stopping_potential, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_min_momentum_uncertainty, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_min_position_uncertainty, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_min_energy_uncertainty, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_min_time_uncertainty, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bohr_radius, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hydrogen_energy_level, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hydrogen_transition_energy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hydrogen_transition_wavelength, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hydrogen_orbital_radius, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hydrogen_orbital_velocity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tunneling_transmission, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_particle_in_box_energy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_zero_point_energy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_compton_wavelength_shift, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_compton_wavelength_electron, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wien_peak_wavelength, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_blackbody_power, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_planck_spectral_radiance, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_quantum__algorithms.rs b/bindings/python/src/generated/m_quantum__algorithms.rs new file mode 100644 index 0000000..cbe1aed --- /dev/null +++ b/bindings/python/src/generated/m_quantum__algorithms.rs @@ -0,0 +1,510 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// The quantum Fourier transform on `n` qubits. +/// +/// `O(n^2)` gates against the `O(n 2^n)` of the classical fast transform on +/// the same many amplitudes -- an exponential saving that is nonetheless not +/// directly useful, because the output is a superposition whose amplitudes +/// cannot be read out. What it is good for is exposing a *period*, which is +/// how Shor's algorithm uses it and why the QFT never appears alone. +/// +/// The controlled rotations shrink as `pi / 2^k`, so the far ones are almost +/// the identity; dropping them is the standard approximate QFT and costs +/// remarkably little. +/// +/// Errors: +/// Returns an error for a bad qubit count. +/// +/// Rust: `quantum::algorithms::qft_circuit` +#[pyfunction] +#[pyo3(name = "qft_circuit", signature = (n))] +pub fn pyfn_qft_circuit(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::algorithms::qft_circuit(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyCircuit { inner: __v }) +} + +/// The inverse quantum Fourier transform. +/// +/// Errors: +/// Returns an error for a bad qubit count. +/// +/// Rust: `quantum::algorithms::iqft` +#[pyfunction] +#[pyo3(name = "iqft", signature = (n))] +pub fn pyfn_iqft(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::algorithms::iqft(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyCircuit { inner: __v }) +} + +/// The largest discrepancy between the QFT circuit and the discrete Fourier +/// transform it is supposed to implement. +/// +/// Errors: +/// Returns an error for a bad qubit count or if the circuit cannot run. +/// +/// Rust: `quantum::algorithms::qft_check_vs_fft` +#[pyfunction] +#[pyo3(name = "qft_check_vs_fft", signature = (n))] +pub fn pyfn_qft_check_vs_fft(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::algorithms::qft_check_vs_fft(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Deutsch-Jozsa: decides whether a promised function is constant or +/// balanced in a single query. +/// +/// Returns true for constant. The classical worst case needs `2^(n-1) + 1` +/// queries, and the quantum algorithm needs exactly one -- the largest +/// separation there is, though it depends entirely on the promise. Without +/// it the problem is no easier quantumly. +/// +/// Errors: +/// Returns an error for a bad qubit count. +/// +/// Rust: `quantum::algorithms::deutsch_jozsa` +#[pyfunction] +#[pyo3(name = "deutsch_jozsa", signature = (f, n))] +pub fn pyfn_deutsch_jozsa(f: pyo3::Py, n: usize) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: u64| -> bool { __cb.call::<_, bool>((__a0,), false) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::algorithms::deutsch_jozsa(&f, n)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Bernstein-Vazirani: recovers a hidden bit string from one query to +/// `f(x) = s . x mod 2`. +/// +/// Classically it takes `n` queries, one per bit. The quantum algorithm gets +/// the whole string at once because the Hadamard transform maps the phase +/// pattern `(-1)^(s . x)` onto the single basis state `|s>` -- interference +/// doing in one step what `n` separate questions do classically. +/// +/// Errors: +/// Returns an error for a bad qubit count. +/// +/// Rust: `quantum::algorithms::bernstein_vazirani` +#[pyfunction] +#[pyo3(name = "bernstein_vazirani", signature = (secret, n))] +pub fn pyfn_bernstein_vazirani(secret: u64, n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::algorithms::bernstein_vazirani(secret, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Simon's problem: finds the hidden period of a two-to-one function +/// satisfying `f(x) = f(x ^ s)`. +/// +/// The quantum step returns a random string orthogonal to `s` under the +/// bitwise dot product; collecting `n - 1` independent ones and solving the +/// linear system classically gives `s`. This is the first problem with an +/// exponential separation for a decision task, and its structure -- a hidden +/// subgroup -- is exactly the structure Shor's algorithm exploits. +/// +/// Errors: +/// Returns an error for a bad qubit count or if the samples never become +/// independent. +/// +/// Rust: `quantum::algorithms::simon_lite` +#[pyfunction] +#[pyo3(name = "simon_lite", signature = (f, n, rng))] +pub fn pyfn_simon_lite(f: pyo3::Py, n: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: u64| -> u64 { __cb.call::<_, u64>((__a0,), 0) } }; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::algorithms::simon_lite(&f, n, &mut rng.inner)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The number of Grover iterations that maximises the success probability. +/// +/// `floor(pi / 4 sqrt(N / M))`. Overshooting *reduces* the success +/// probability -- the amplitude rotates past the target and back down -- so +/// more iterations are not better, which is the least intuitive feature of +/// the algorithm and the reason the marked count has to be known or +/// estimated. +/// +/// Errors: +/// Returns an error unless there is at least one item and at least one +/// marked, with no more marked than items. +/// +/// Rust: `quantum::algorithms::grover_optimal_iterations` +#[pyfunction] +#[pyo3(name = "grover_optimal_iterations", signature = (items, marked))] +pub fn pyfn_grover_optimal_iterations(items: usize, marked: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::algorithms::grover_optimal_iterations(items, marked)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Grover's search, returning the measured index and the success probability +/// it was drawn from. +/// +/// The oracle phase-flips the marked states and the diffusion operator +/// reflects about the uniform superposition; the pair is a rotation by a +/// fixed angle in the two-dimensional plane spanned by the marked and +/// unmarked subspaces, which is why the analysis is exactly trigonometry. +/// +/// Errors: +/// Returns an error for a bad qubit count or an empty marked set. +/// +/// Rust: `quantum::algorithms::grover` +#[pyfunction] +#[pyo3(name = "grover", signature = (marked, n, iterations, rng))] +pub fn pyfn_grover(marked: Vec, n: usize, iterations: Option, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(u64, f64)> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::algorithms::grover(&marked, n, iterations, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// Estimates how many items an oracle marks, without finding them. +/// +/// Amplitude estimation: the Grover operator rotates by an angle whose sine +/// squared is the marked fraction, so estimating that angle by phase +/// estimation counts the solutions. It is the same primitive that gives the +/// quadratic speedup for Monte Carlo estimation generally. +/// +/// Errors: +/// Returns an error for a bad qubit count. +/// +/// Rust: `quantum::algorithms::quantum_counting` +#[pyfunction] +#[pyo3(name = "quantum_counting", signature = (marked, n, precision))] +pub fn pyfn_quantum_counting<'py>(py: Python<'py>, marked: Vec, n: usize, precision: usize) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::quantum::algorithms::quantum_counting(&marked, n, precision))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Phase estimation for a one-qubit unitary and one of its eigenstates. +/// +/// Returns the estimated phase in `[0, 1)`, where the eigenvalue is +/// `exp(2 pi i phase)`. With `ancilla` counting qubits the answer is exact +/// whenever the phase is a multiple of `2^-ancilla`, and otherwise correct to +/// that resolution with high probability. Every algorithm with an exponential +/// speedup runs through this routine. +/// +/// Errors: +/// Returns an error for a bad ancilla count or a non-eigenstate. +/// +/// Rust: `quantum::algorithms::phase_estimation` +#[pyfunction] +#[pyo3(name = "phase_estimation", signature = (unitary, eigenstate, ancilla))] +pub fn pyfn_phase_estimation(unitary: crate::generated::types::PyGate, eigenstate: crate::generated::types::PyQState, ancilla: usize) -> PyResult { + let unitary = unitary.inner; + let eigenstate = eigenstate.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::algorithms::phase_estimation(&unitary, &eigenstate, ancilla)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The period of `a^x mod modulus`, by simulating the quantum subroutine. +/// +/// The modular exponentiation is a permutation of basis states, so it is +/// applied as one rather than compiled into gates -- the algorithm's +/// behaviour is identical and the simulation is `O(2^n)` instead of hopeless. +/// The counting register is transformed and measured, and the period is read +/// off by continued fractions, which is where the classical part of Shor's +/// algorithm begins. +/// +/// Errors: +/// Returns an error for a bad modulus, a base sharing a factor with it, or +/// too small a counting register. +/// +/// Rust: `quantum::algorithms::shor_period_finding_sim` +#[pyfunction] +#[pyo3(name = "shor_period_finding_sim", signature = (a, modulus, counting, rng))] +pub fn pyfn_shor_period_finding_sim(a: u64, modulus: u64, counting: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::algorithms::shor_period_finding_sim(a, modulus, counting, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.map(|__x| __x)) +} + +/// The classical half of Shor's algorithm: turns a period into factors. +/// +/// Works only when the period is even and `a^(r/2)` is not congruent to +/// `-1`; those conditions fail for a constant fraction of bases, which is +/// why the algorithm is randomised and retried rather than deterministic. +/// +/// Errors: +/// Returns an error for a bad modulus or period. +/// +/// Rust: `quantum::algorithms::shor_classical_post` +#[pyfunction] +#[pyo3(name = "shor_classical_post", signature = (a, r, modulus))] +pub fn pyfn_shor_classical_post(a: u64, r: u64, modulus: u64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::algorithms::shor_classical_post(a, r, modulus)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.map(|__x| (__x.0, __x.1))) +} + +/// The expectation of a Pauli-sum Hamiltonian in a state. +/// +/// Errors: +/// Returns an error if a term has the wrong width or an unknown symbol. +/// +/// Rust: `quantum::algorithms::pauli_sum_expectation` +#[pyfunction] +#[pyo3(name = "pauli_sum_expectation", signature = (terms, state))] +pub fn pyfn_pauli_sum_expectation<'py>(py: Python<'py>, terms: Vec<(String, f64)>, state: crate::generated::types::PyQState) -> PyResult { + let terms = terms.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let state = state.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::quantum::algorithms::pauli_sum_expectation(&terms, &state))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// A two-qubit model Hamiltonian for molecular hydrogen. +/// +/// This is *not* a table of ab initio coefficients. It is a two-qubit +/// operator constructed so that its ground eigenvalue follows the known H2 +/// potential curve -- a Morse form with a well depth of 0.1745 hartree at a +/// separation of 0.7414 angstrom, giving -1.1373 hartree at equilibrium and +/// dissociating to -1.0 -- while its excited states sit plausibly above. +/// The distinction matters: a real STO-3G calculation produces the +/// coefficients from integrals over basis functions, and inventing numbers +/// that merely look like published ones would be worse than useless. +/// +/// What it *is* good for is exercising a variational eigensolver against a +/// Hamiltonian whose exact ground energy is known in closed form, which is +/// what the tests below need. +/// +/// The construction: the `|00>` and `|11>` states form the bonding block, +/// coupled by the `XX` term, and their splitting is set to the desired gap; +/// the other two states are placed above both. +/// +/// Errors: +/// Returns an error for a non-positive bond length. +/// +/// Rust: `quantum::algorithms::h2_model_hamiltonian` +#[pyfunction] +#[pyo3(name = "h2_model_hamiltonian", signature = (bond_length))] +pub fn pyfn_h2_model_hamiltonian<'py>(py: Python<'py>, bond_length: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::quantum::algorithms::h2_model_hamiltonian(bond_length))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0.to_string(), __x.1)).collect::>()) +} + +/// The model H2 ground-state energy in hartree, as a Morse curve. +/// +/// The parameters are the measured ones: a dissociation energy of 0.1744 +/// hartree (4.75 electronvolts), an equilibrium separation of 0.7414 +/// angstrom, and the Morse width 1.9426 per angstrom. They are mutually +/// consistent by construction -- the curve dissociates to exactly -1.0 +/// hartree, two hydrogen atoms at -0.5 each -- which a minimum taken from a +/// small-basis calculation and a well depth taken from experiment would not +/// be. +/// +/// Rust: `quantum::algorithms::h2_ground_energy_model` +#[pyfunction] +#[pyo3(name = "h2_ground_energy_model", signature = (bond_length))] +pub fn pyfn_h2_ground_energy_model(bond_length: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::algorithms::h2_ground_energy_model(bond_length)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The exact lowest eigenvalue of a Pauli-sum Hamiltonian on a few qubits, +/// by building the matrix and diagonalising. +/// +/// The reference a variational result should be measured against. +/// +/// Errors: +/// Returns an error for a bad width or an eigensolver failure. +/// +/// Rust: `quantum::algorithms::pauli_sum_ground_energy` +#[pyfunction] +#[pyo3(name = "pauli_sum_ground_energy", signature = (terms, n))] +pub fn pyfn_pauli_sum_ground_energy<'py>(py: Python<'py>, terms: Vec<(String, f64)>, n: usize) -> PyResult { + let terms = terms.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::quantum::algorithms::pauli_sum_ground_energy(&terms, n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// QAOA for maximum cut on a small graph given by its edge list. +/// +/// Returns the best cut value found, the parameters, and the bit string. The +/// ansatz alternates a cost phase and a mixing rotation; at one layer it is +/// weak, and the interest is that the quality rises with the layer count -- +/// at infinitely many layers it becomes exact, since it approximates +/// adiabatic evolution. +/// +/// Errors: +/// Returns an error for a bad vertex count or an out-of-range edge. +/// +/// Rust: `quantum::algorithms::qaoa_maxcut` +#[pyfunction] +#[pyo3(name = "qaoa_maxcut", signature = (vertices, edges, layers))] +pub fn pyfn_qaoa_maxcut<'py>(py: Python<'py>, vertices: usize, edges: Vec<(usize, usize)>, layers: usize) -> PyResult<(f64, Vec, u64)> { + let edges = edges.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::quantum::algorithms::qaoa_maxcut(vertices, &edges, layers))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// A Trotterised circuit for `exp(-i H t)` with `H` a sum of Pauli terms. +/// +/// First order: each term is exponentiated in turn, which is exact only if +/// they commute. The error per step is the commutator, so it falls as +/// `t^2 / steps` -- and the whole point of Trotterisation is that a +/// Hamiltonian nobody can exponentiate is a sum of terms everybody can. +/// +/// Errors: +/// Returns an error for a bad width, zero steps, or an unknown symbol. +/// +/// Rust: `quantum::algorithms::trotter_evolution` +#[pyfunction] +#[pyo3(name = "trotter_evolution", signature = (terms, t, steps, n))] +pub fn pyfn_trotter_evolution(terms: Vec<(String, f64)>, t: f64, steps: usize, n: usize) -> PyResult { + let terms = terms.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::algorithms::trotter_evolution(&terms, t, steps, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyCircuit { inner: __v }) +} + +/// A discrete quantum walk on a line, returning the position distribution +/// after the given number of steps. +/// +/// The distribution spreads *linearly* in time rather than as its square +/// root, and it is bimodal with peaks at the edges rather than a bell curve +/// in the middle -- the opposite of a classical random walk in both respects, +/// and the reason quantum walks give speedups at all. +/// +/// Errors: +/// Returns an error for zero steps or a non-unitary coin. +/// +/// Rust: `quantum::algorithms::quantum_walk_line` +#[pyfunction] +#[pyo3(name = "quantum_walk_line", signature = (steps, coin))] +pub fn pyfn_quantum_walk_line<'py>(py: Python<'py>, steps: usize, coin: crate::generated::types::PyGate) -> PyResult> { + let coin = coin.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::quantum::algorithms::quantum_walk_line(steps, &coin))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The three-qubit bit-flip code, returning the logical and physical error +/// rates measured over the given number of trials. +/// +/// The code corrects any single bit flip, so the logical error is the chance +/// of two or three flips: `3 p^2 (1 - p) + p^3`. That beats `p` only below +/// `p = 1/2`, which is the threshold in its simplest form -- above it the +/// encoding makes things worse, and no amount of redundancy helps. +/// +/// Errors: +/// Returns an error unless `p` is a probability and the trial count is +/// positive. +/// +/// Rust: `quantum::algorithms::error_correction_3bit_flip_demo` +#[pyfunction] +#[pyo3(name = "error_correction_3bit_flip_demo", signature = (p, trials, rng))] +pub fn pyfn_error_correction_3bit_flip_demo(p: f64, trials: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(f64, f64)> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::algorithms::error_correction_3bit_flip_demo(p, trials, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// The exact logical error rate of the three-qubit code. +/// +/// Rust: `quantum::algorithms::three_bit_code_logical_error` +#[pyfunction] +#[pyo3(name = "three_bit_code_logical_error", signature = (p))] +pub fn pyfn_three_bit_code_logical_error(p: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::algorithms::three_bit_code_logical_error(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Randomised benchmarking: the surviving fidelity after a random Clifford +/// sequence and its inverse, at several depths. +/// +/// Returns `(depth, fidelity)` pairs. The decay is exponential in the depth +/// with a rate set by the average gate error, and -- this is the point of the +/// technique -- the rate is insensitive to errors in preparation and +/// measurement, which contaminate every direct fidelity estimate. +/// +/// Errors: +/// Returns an error for a bad noise level or an empty depth list. +/// +/// Rust: `quantum::algorithms::randomized_benchmarking_sim` +#[pyfunction] +#[pyo3(name = "randomized_benchmarking_sim", signature = (depths, noise, trials, rng))] +pub fn pyfn_randomized_benchmarking_sim(depths: Vec, noise: f64, trials: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::algorithms::randomized_benchmarking_sim(&depths, noise, trials, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_qft_circuit, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_iqft, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_qft_check_vs_fft, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_deutsch_jozsa, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bernstein_vazirani, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_simon_lite, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_grover_optimal_iterations, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_grover, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_quantum_counting, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_phase_estimation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_shor_period_finding_sim, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_shor_classical_post, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pauli_sum_expectation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_h2_model_hamiltonian, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_h2_ground_energy_model, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pauli_sum_ground_energy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_qaoa_maxcut, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_trotter_evolution, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_quantum_walk_line, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_error_correction_3bit_flip_demo, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_three_bit_code_logical_error, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_randomized_benchmarking_sim, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_quantum__circuit.rs b/bindings/python/src/generated/m_quantum__circuit.rs new file mode 100644 index 0000000..f9de2ed --- /dev/null +++ b/bindings/python/src/generated/m_quantum__circuit.rs @@ -0,0 +1,319 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// The depolarising channel: with probability `p`, replace the qubit by the +/// maximally mixed state. +/// +/// The one channel that treats every direction alike, so it shrinks the Bloch +/// vector uniformly toward the origin without rotating it. +/// +/// Errors: +/// Returns an error unless `p` is a probability. +/// +/// Rust: `quantum::circuit::depolarizing_channel` +#[pyfunction] +#[pyo3(name = "depolarizing_channel", signature = (p))] +pub fn pyfn_depolarizing_channel<'py>(py: Python<'py>, p: f64) -> PyResult>>>> { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::circuit::depolarizing_channel(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| __x.into_iter().map(|__x| __x.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()).collect::>()).collect::>()) +} + +/// Amplitude damping: a qubit decaying from `|1>` to `|0>` with probability +/// `gamma`. +/// +/// Models spontaneous emission, and unlike the symmetric channels it has a +/// fixed point that is not the maximally mixed state: everything ends up in +/// the ground state. That asymmetry is why `T_1` and `T_2` are different +/// numbers. +/// +/// Errors: +/// Returns an error unless `gamma` is a probability. +/// +/// Rust: `quantum::circuit::amplitude_damping` +#[pyfunction] +#[pyo3(name = "amplitude_damping", signature = (gamma))] +pub fn pyfn_amplitude_damping<'py>(py: Python<'py>, gamma: f64) -> PyResult>>>> { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::circuit::amplitude_damping(gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| __x.into_iter().map(|__x| __x.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()).collect::>()).collect::>()) +} + +/// Phase damping: coherence lost without any energy exchange. +/// +/// The off-diagonal terms shrink and the populations do not move at all, so +/// the Bloch vector flattens onto the `z` axis. It is the purely quantum kind +/// of noise -- there is no classical process it corresponds to. +/// +/// Errors: +/// Returns an error unless `gamma` is a probability. +/// +/// Rust: `quantum::circuit::phase_damping` +#[pyfunction] +#[pyo3(name = "phase_damping", signature = (gamma))] +pub fn pyfn_phase_damping<'py>(py: Python<'py>, gamma: f64) -> PyResult>>>> { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::circuit::phase_damping(gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| __x.into_iter().map(|__x| __x.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()).collect::>()).collect::>()) +} + +/// The bit-flip channel. +/// +/// Errors: +/// Returns an error unless `p` is a probability. +/// +/// Rust: `quantum::circuit::bit_flip` +#[pyfunction] +#[pyo3(name = "bit_flip", signature = (p))] +pub fn pyfn_bit_flip<'py>(py: Python<'py>, p: f64) -> PyResult>>>> { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::circuit::bit_flip(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| __x.into_iter().map(|__x| __x.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()).collect::>()).collect::>()) +} + +/// The phase-flip channel. +/// +/// Errors: +/// Returns an error unless `p` is a probability. +/// +/// Rust: `quantum::circuit::phase_flip` +#[pyfunction] +#[pyo3(name = "phase_flip", signature = (p))] +pub fn pyfn_phase_flip<'py>(py: Python<'py>, p: f64) -> PyResult>>>> { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::circuit::phase_flip(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| __x.into_iter().map(|__x| __x.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()).collect::>()).collect::>()) +} + +/// One of the four Bell states, indexed zero to three. +/// +/// Errors: +/// Returns an error for an index above three. +/// +/// Rust: `quantum::circuit::bell_state` +#[pyfunction] +#[pyo3(name = "bell_state", signature = (which))] +pub fn pyfn_bell_state(which: u8) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::circuit::bell_state(which)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyQState { inner: __v }) +} + +/// The `n`-qubit GHZ state. +/// +/// Maximally entangled and maximally fragile: losing one qubit leaves the +/// rest in a classical mixture with no entanglement at all, which is what +/// distinguishes it from the W state. +/// +/// Errors: +/// Returns an error for fewer than two qubits or more than the cap. +/// +/// Rust: `quantum::circuit::ghz` +#[pyfunction] +#[pyo3(name = "ghz", signature = (n))] +pub fn pyfn_ghz(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::circuit::ghz(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyQState { inner: __v }) +} + +/// The `n`-qubit W state: one excitation shared equally. +/// +/// The complement of GHZ. Losing a qubit leaves the others still entangled, +/// so the two are inequivalent under local operations -- there is no way to +/// turn one into the other without communication, even probabilistically. +/// +/// Errors: +/// Returns an error for fewer than two qubits or more than the cap. +/// +/// Rust: `quantum::circuit::w_state` +#[pyfunction] +#[pyo3(name = "w_state", signature = (n))] +pub fn pyfn_w_state(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::circuit::w_state(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyQState { inner: __v }) +} + +/// A Haar-random pure state. +/// +/// Built from independent complex Gaussians, which is the standard trick: +/// normalising a Gaussian vector gives the uniform measure on the sphere, so +/// this really is Haar random and not merely "random looking". +/// +/// Errors: +/// Returns an error for a bad qubit count. +/// +/// Rust: `quantum::circuit::random_state` +#[pyfunction] +#[pyo3(name = "random_state", signature = (n, rng))] +pub fn pyfn_random_state(n: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::circuit::random_state(n, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyQState { inner: __v }) +} + +/// The CHSH correlation for a two-qubit state at four measurement angles. +/// +/// `S = E(a, b) - E(a, b') + E(a', b) + E(a', b')`. Any local hidden variable +/// model obeys `|S| <= 2`; quantum mechanics reaches `2 sqrt 2` on a Bell +/// state, and no theory obeying no-signalling can exceed `4`. The gap between +/// two and `2 sqrt 2` is the whole experimental content of Bell's theorem. +/// +/// Errors: +/// Returns an error unless the state has two qubits. +/// +/// Rust: `quantum::circuit::chsh_value` +#[pyfunction] +#[pyo3(name = "chsh_value", signature = (state, angles))] +pub fn pyfn_chsh_value(state: crate::generated::types::PyQState, angles: (f64, f64, f64, f64)) -> PyResult { + let state = state.inner; + let angles = (angles.0, angles.1, angles.2, angles.3); + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::circuit::chsh_value(&state, angles)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The angles that maximise CHSH on a Bell state, as +/// `(a, a', b, b')` in radians. +/// +/// Rust: `quantum::circuit::chsh_optimal_angles` +#[pyfunction] +#[pyo3(name = "chsh_optimal_angles", signature = ())] +pub fn pyfn_chsh_optimal_angles() -> PyResult<(f64, f64, f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::circuit::chsh_optimal_angles()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2, __v.3)) +} + +/// Teleports a one-qubit state and returns the input and output Bloch +/// vectors. +/// +/// The protocol consumes one Bell pair and two classical bits, and it moves +/// the state exactly -- not a copy, since the sender's qubit is destroyed by +/// the measurement, which is what keeps no-cloning intact. Without the +/// classical bits the receiver holds the maximally mixed state, so nothing +/// travels faster than light either. +/// +/// Errors: +/// Returns an error if the simulation fails. +/// +/// Rust: `quantum::circuit::quantum_teleportation_demo` +#[pyfunction] +#[pyo3(name = "quantum_teleportation_demo", signature = (theta, phi, rng))] +pub fn pyfn_quantum_teleportation_demo(theta: f64, phi: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<((f64, f64, f64), (f64, f64, f64))> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::circuit::quantum_teleportation_demo(theta, phi, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(((__v.0.0, __v.0.1, __v.0.2), (__v.1.0, __v.1.1, __v.1.2))) +} + +/// Superdense coding: two classical bits carried by one qubit, given a +/// shared Bell pair. +/// +/// Returns the decoded bits, which must equal the encoded ones. The +/// bookkeeping is exact -- one qubit plus prior entanglement carries two +/// bits, and without the entanglement it carries one, which is Holevo's +/// bound. +/// +/// Errors: +/// Returns an error if the simulation fails. +/// +/// Rust: `quantum::circuit::superdense_coding_demo` +#[pyfunction] +#[pyo3(name = "superdense_coding_demo", signature = (bits))] +pub fn pyfn_superdense_coding_demo(bits: (bool, bool)) -> PyResult<(bool, bool)> { + let bits = (bits.0, bits.1); + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::circuit::superdense_coding_demo(bits)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// The best fidelity an approximate universal cloner can achieve: `5 / 6`. +/// +/// Exact cloning is impossible because it is not linear, and the optimal +/// approximation is bounded by this number, which is a theorem rather than an +/// engineering limit. +/// +/// Rust: `quantum::circuit::no_cloning_fidelity_bound` +#[pyfunction] +#[pyo3(name = "no_cloning_fidelity_bound", signature = ())] +pub fn pyfn_no_cloning_fidelity_bound() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::circuit::no_cloning_fidelity_bound()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Decomposes a Hermitian matrix on one or two qubits into Pauli terms. +/// +/// The Pauli strings form an orthogonal basis under the Hilbert-Schmidt inner +/// product, so each coefficient is just `tr(P H) / d` -- no linear solve +/// needed. That orthogonality is what makes measuring a Hamiltonian on +/// hardware possible at all. +/// +/// Errors: +/// Returns an error unless the matrix is square with side two or four. +/// +/// Rust: `quantum::circuit::pauli_decompose` +#[pyfunction] +#[pyo3(name = "pauli_decompose", signature = (h))] +pub fn pyfn_pauli_decompose<'py>(py: Python<'py>, h: Vec>) -> PyResult> { + let h = h.into_iter().map(|__e| __e.into_iter().map(|__e| __e.0).collect::>()).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::quantum::circuit::pauli_decompose(&h))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0.to_string(), __x.1)).collect::>()) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_depolarizing_channel, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_amplitude_damping, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_phase_damping, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bit_flip, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_phase_flip, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bell_state, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ghz, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_w_state, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_random_state, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_chsh_value, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_chsh_optimal_angles, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_quantum_teleportation_demo, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_superdense_coding_demo, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_no_cloning_fidelity_bound, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pauli_decompose, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_quantum__schrodinger.rs b/bindings/python/src/generated/m_quantum__schrodinger.rs new file mode 100644 index 0000000..276e6d6 --- /dev/null +++ b/bindings/python/src/generated/m_quantum__schrodinger.rs @@ -0,0 +1,655 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// The lowest `n_states` bound states on a grid, by second-order finite +/// differences with hard walls at the ends. +/// +/// Returns the energies in ascending order and the matching normalised +/// eigenvectors. The discrete Laplacian is tridiagonal and symmetric, so the +/// eigenproblem is solved directly rather than iteratively. +/// +/// The walls matter: this solves the problem on `[x_0, x_{n-1}]` with the +/// wavefunction pinned to zero just outside, so a state that has not decayed +/// by the edge of the grid is being confined by the box rather than by the +/// potential, and its energy is wrong. The error is `O(dx^2)` and one-sided: +/// the discrete Laplacian underestimates curvature, so the computed energies +/// sit below the true ones. +/// +/// Errors: +/// Returns an error for an empty potential, a non-positive spacing, mass or +/// `hbar`, or if the eigensolver fails. +/// +/// Rust: `quantum::schrodinger::tise_solve_fd` +#[pyfunction] +#[pyo3(name = "tise_solve_fd", signature = (v, dx, mass, hbar, n_states))] +pub fn pyfn_tise_solve_fd<'py>(py: Python<'py>, v: Vec, dx: f64, mass: f64, hbar: f64, n_states: usize) -> PyResult<(Vec, Vec>)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::quantum::schrodinger::tise_solve_fd(&v, dx, mass, hbar, n_states))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// Bound-state energies by Numerov shooting with node counting. +/// +/// Integrates from both ends toward a matching point and looks for the energy +/// at which the logarithmic derivatives agree. Node counting is what makes +/// the search reliable: the number of zeros of the solution is a monotone +/// function of the trial energy, so it says *which* state a bracket contains +/// and turns a search over a continuum into a bisection per state. +/// +/// Numerov itself is worth the extra terms: it integrates `y'' = f y` to +/// fourth order using only three points, because the equation's lack of a +/// first-derivative term lets the `O(h^4)` error be absorbed into the +/// coefficients. +/// +/// Returns `(energy, wavefunction)` for each of the lowest `n_states` levels +/// found inside `e_range`. +/// +/// Errors: +/// Returns an error for a degenerate grid or an inverted energy range. +/// +/// Rust: `quantum::schrodinger::tise_solve_numerov` +#[pyfunction] +#[pyo3(name = "tise_solve_numerov", signature = (v, x_range, n, e_range, mass, hbar, n_states))] +pub fn pyfn_tise_solve_numerov(v: pyo3::Py, x_range: (f64, f64), n: usize, e_range: (f64, f64), mass: f64, hbar: f64, n_states: usize) -> PyResult)>> { + let __cb_v = std::rc::Rc::new(crate::runtime::Callback::new(v)); + let v = { let __cb = __cb_v.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let x_range = (x_range.0, x_range.1); + let e_range = (e_range.0, e_range.1); + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::schrodinger::tise_solve_numerov(&v, x_range, n, e_range, mass, hbar, n_states)); + crate::runtime::callback::check(&[&__cb_v], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Bound states by expanding the Hamiltonian in a fixed basis and +/// diagonalising. +/// +/// Rayleigh-Ritz: the energies are upper bounds on the eigenvalues of the +/// same Hamiltonian, and they fall monotonically as the basis grows. The +/// bound is against the *discretised* operator -- the same tridiagonal +/// `tise_solve_fd` uses -- not against the continuum, since a truncated +/// basis cannot bound what the grid has already changed. +/// +/// The basis is orthonormalised on the grid before use, and that is not +/// tidiness. Sampling a basis at finitely many points and cutting it off at +/// the ends leaves it non-orthogonal, so `H c = E c` is the wrong problem; +/// the right one is `H c = E S c` with the overlap matrix `S`. Solving the +/// former with a non-orthonormal basis breaks the bound in the worst way -- +/// it returns energies *below* the true ones, which looks like a better +/// answer rather than a wrong one. +/// +/// Returns the energies in ascending order and the coefficient matrix in the +/// orthonormalised basis, whose column `i` holds the expansion of state `i`. +/// +/// Errors: +/// Returns an error for an empty basis, a degenerate grid, an eigensolver +/// failure, or a basis that collapses to nothing on this grid. +/// +/// Rust: `quantum::schrodinger::tise_solve_matrix_basis` +#[pyfunction] +#[pyo3(name = "tise_solve_matrix_basis", signature = (v, dx, x0, basis, n_basis, mass, hbar))] +pub fn pyfn_tise_solve_matrix_basis(v: Vec, dx: f64, x0: f64, basis: crate::generated::types::PyBasis, n_basis: usize, mass: f64, hbar: f64) -> PyResult<(Vec, crate::generated::types::PyMatrix)> { + let basis = basis.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::schrodinger::tise_solve_matrix_basis(&v, dx, x0, basis, n_basis, mass, hbar)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, crate::generated::types::PyMatrix { inner: __v.1 })) +} + +/// Advances a wavefunction by the split-operator method. +/// +/// Strang splitting: a half step of the potential, a full step of the kinetic +/// term in momentum space, and another half step of the potential. Each +/// factor is the exponential of a Hermitian operator and so is exactly +/// unitary, which is why the norm is conserved to rounding however large the +/// step is. What the step size controls is the *commutator* error between the +/// two -- second order for Strang against first for the naive ordering -- so +/// too large a step gives a wrong answer of exactly the right length. +/// +/// Errors: +/// Returns an error for a mismatched potential, a non-power-of-two grid, or a +/// non-positive mass. +/// +/// Rust: `quantum::schrodinger::tdse_split_operator` +#[pyfunction] +#[pyo3(name = "tdse_split_operator", signature = (psi, v, dt, steps, mass, hbar))] +pub fn pyfn_tdse_split_operator(psi: pyo3::PyRefMut<'_, crate::generated::types::PyWavefunction1D>, v: Vec, dt: f64, steps: usize, mass: f64, hbar: f64) -> PyResult<()> { + let mut psi = psi; + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::schrodinger::tdse_split_operator(&mut psi.inner, &v, dt, steps, mass, hbar)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(()) +} + +/// Advances a wavefunction by Crank-Nicolson. +/// +/// Applies `(1 + i H dt / 2 hbar)^{-1} (1 - i H dt / 2 hbar)`, the Cayley +/// transform of the Hamiltonian. For Hermitian `H` that is exactly unitary at +/// every step size -- not approximately, and not only in the small-step limit +/// -- which is the reason to prefer it to an explicit scheme here. An explicit +/// Euler step on the same equation has modulus strictly greater than one for +/// every non-zero step and blows up. +/// +/// Unlike the split-operator method this needs no FFT, so it works on any +/// grid length, and it imposes hard walls at the ends rather than periodicity. +/// +/// Errors: +/// Returns an error for a mismatched potential, a non-positive mass, or a +/// singular system. +/// +/// Rust: `quantum::schrodinger::tdse_crank_nicolson` +#[pyfunction] +#[pyo3(name = "tdse_crank_nicolson", signature = (psi, v, dt, steps, mass, hbar))] +pub fn pyfn_tdse_crank_nicolson(psi: pyo3::PyRefMut<'_, crate::generated::types::PyWavefunction1D>, v: Vec, dt: f64, steps: usize, mass: f64, hbar: f64) -> PyResult<()> { + let mut psi = psi; + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::schrodinger::tdse_crank_nicolson(&mut psi.inner, &v, dt, steps, mass, hbar)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(()) +} + +/// Adds an imaginary absorbing layer of the given width and strength to the +/// two ends of a complex potential. +/// +/// A wavepacket that reaches the edge of a periodic grid wraps around and +/// interferes with itself, which looks exactly like physics and is not. An +/// absorbing layer removes the outgoing amplitude instead. The profile has to +/// turn on smoothly -- a sudden absorber reflects, which is the problem it +/// was added to solve -- so the strength here rises quadratically. +/// +/// Returns the imaginary part to be subtracted from the Hamiltonian. +/// +/// Errors: +/// Returns an error if the two layers would overlap or the strength is +/// negative. +/// +/// Rust: `quantum::schrodinger::absorbing_boundary_cap` +#[pyfunction] +#[pyo3(name = "absorbing_boundary_cap", signature = (n, width, strength))] +pub fn pyfn_absorbing_boundary_cap<'py>(py: Python<'py>, n: usize, width: usize, strength: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::quantum::schrodinger::absorbing_boundary_cap(n, width, strength))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Applies one step of an absorbing layer to a wavefunction, damping the +/// amplitude by `exp(-cap dt / hbar)`. +/// +/// Errors: +/// Returns an error if the layer has the wrong length. +/// +/// Rust: `quantum::schrodinger::apply_absorber` +#[pyfunction] +#[pyo3(name = "apply_absorber", signature = (psi, cap, dt, hbar))] +pub fn pyfn_apply_absorber(psi: pyo3::PyRefMut<'_, crate::generated::types::PyWavefunction1D>, cap: Vec, dt: f64, hbar: f64) -> PyResult<()> { + let mut psi = psi; + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::schrodinger::apply_absorber(&mut psi.inner, &cap, dt, hbar)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(()) +} + +/// The transmission probability through an arbitrary piecewise-constant +/// barrier, by the transfer matrix method. +/// +/// Each slice contributes a two-by-two matrix relating the amplitudes on its +/// two sides, and the product of them all relates the incoming wave to the +/// outgoing one. The method is exact for a piecewise-constant potential, so +/// its only error is the piecewise-constant approximation itself -- which +/// means a smooth barrier converges as the slices are refined, and a genuinely +/// rectangular one is exact at any resolution. +/// +/// Below the barrier the wavenumber is imaginary and the same algebra +/// continues to work, which is where tunnelling comes from: the exponentially +/// decaying solution inside is not zero at the far side. +/// +/// Errors: +/// Returns an error for an empty barrier, a non-positive width, mass or +/// `hbar`, or a non-positive energy. +/// +/// Rust: `quantum::schrodinger::transmission_coefficient` +#[pyfunction] +#[pyo3(name = "transmission_coefficient", signature = (v, dx, energy, mass, hbar))] +pub fn pyfn_transmission_coefficient<'py>(py: Python<'py>, v: Vec, dx: f64, energy: f64, mass: f64, hbar: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::quantum::schrodinger::transmission_coefficient(&v, dx, energy, mass, hbar))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The exact transmission probability through a rectangular barrier. +/// +/// Three regimes in one formula. Below the barrier the transmission falls +/// exponentially with width, which is tunnelling; above it the transmission +/// oscillates and returns to one at the resonances where the barrier is a +/// whole number of half-wavelengths, which is the Ramsauer-Townsend effect +/// and has no classical counterpart at all -- classically, anything above the +/// barrier passes with certainty at every energy. +/// +/// Errors: +/// Returns an error for a non-positive width, mass, `hbar` or energy. +/// +/// Rust: `quantum::schrodinger::tunneling_rectangular_exact` +#[pyfunction] +#[pyo3(name = "tunneling_rectangular_exact", signature = (v0, width, energy, mass, hbar))] +pub fn pyfn_tunneling_rectangular_exact(v0: f64, width: f64, energy: f64, mass: f64, hbar: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::schrodinger::tunneling_rectangular_exact(v0, width, energy, mass, hbar)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The WKB tunnelling probability through a barrier between two turning +/// points. +/// +/// `exp(-2 integral kappa dx)` over the classically forbidden region. It is +/// the leading exponential only: the prefactor is missing, so it is accurate +/// for a thick barrier and wrong by a factor of order one for a thin one. It +/// also diverges from the truth near the barrier top, where the turning +/// points merge and the approximation's own assumption -- that the wavelength +/// varies slowly -- fails exactly where it matters. +/// +/// Errors: +/// Returns an error for an inverted interval or non-positive constants. +/// +/// Rust: `quantum::schrodinger::wkb_tunneling` +#[pyfunction] +#[pyo3(name = "wkb_tunneling", signature = (v, energy, turning_points, mass, hbar, samples))] +pub fn pyfn_wkb_tunneling(v: pyo3::Py, energy: f64, turning_points: (f64, f64), mass: f64, hbar: f64, samples: usize) -> PyResult { + let __cb_v = std::rc::Rc::new(crate::runtime::Callback::new(v)); + let v = { let __cb = __cb_v.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let turning_points = (turning_points.0, turning_points.1); + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::schrodinger::wkb_tunneling(&v, energy, turning_points, mass, hbar, samples)); + crate::runtime::callback::check(&[&__cb_v], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The Bohr-Sommerfeld energy of the `n`-th level: the energy at which the +/// action enclosed by the classical orbit is `(n + 1/2) 2 pi hbar`. +/// +/// The half is the Maslov correction, one quarter of a cycle for each of the +/// two turning points. Without it the harmonic oscillator comes out with no +/// zero-point energy; with it the WKB spectrum of the oscillator is *exact* +/// at every level, which is a coincidence of the quadratic potential and not +/// a general property. +/// +/// Errors: +/// Returns an error if no bracketing energy is found in `e_range`. +/// +/// Rust: `quantum::schrodinger::wkb_quantization` +#[pyfunction] +#[pyo3(name = "wkb_quantization", signature = (v, n, e_range, x_range, mass, hbar, samples))] +pub fn pyfn_wkb_quantization(v: pyo3::Py, n: usize, e_range: (f64, f64), x_range: (f64, f64), mass: f64, hbar: f64, samples: usize) -> PyResult { + let __cb_v = std::rc::Rc::new(crate::runtime::Callback::new(v)); + let v = { let __cb = __cb_v.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let e_range = (e_range.0, e_range.1); + let x_range = (x_range.0, x_range.1); + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::schrodinger::wkb_quantization(&v, n, e_range, x_range, mass, hbar, samples)); + crate::runtime::callback::check(&[&__cb_v], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The reflection probability at a potential step of height `v0`. +/// +/// Non-zero even when the particle has more than enough energy to pass, which +/// has no classical analogue: a classical particle rolling over a downward +/// step always continues. Reflection here comes from the impedance mismatch +/// between the two wavenumbers, exactly as for light at a glass surface. +/// +/// Errors: +/// Returns an error for a non-positive energy. +/// +/// Rust: `quantum::schrodinger::reflection_step_potential` +#[pyfunction] +#[pyo3(name = "reflection_step_potential", signature = (v0, energy))] +pub fn pyfn_reflection_step_potential(v0: f64, energy: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::schrodinger::reflection_step_potential(v0, energy)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The energy splitting of the lowest doublet in a symmetric double well. +/// +/// The two lowest states are the symmetric and antisymmetric combinations of +/// the states localised in each well, and their energies differ by an amount +/// exponentially small in the barrier. A particle prepared in one well +/// oscillates to the other with period `2 pi hbar / splitting`, so the +/// splitting *is* the tunnelling rate -- a static spectral quantity carrying +/// entirely dynamical information. +/// +/// Errors: +/// Returns an error if the finite-difference solve fails. +/// +/// Rust: `quantum::schrodinger::double_well_splitting` +#[pyfunction] +#[pyo3(name = "double_well_splitting", signature = (v, dx, mass, hbar))] +pub fn pyfn_double_well_splitting<'py>(py: Python<'py>, v: Vec, dx: f64, mass: f64, hbar: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::quantum::schrodinger::double_well_splitting(&v, dx, mass, hbar))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// First-order energy shifts: the expectation of the perturbation in each +/// unperturbed state. +/// +/// The whole of first order is a diagonal matrix element, which is why the +/// first-order shift of a state with a symmetry the perturbation breaks is so +/// often zero -- the integrand is odd. The Stark effect in hydrogen's ground +/// state is the standard case: no linear shift, because the ground state has +/// no permanent dipole. +/// +/// Errors: +/// Returns an error if a state has the wrong length. +/// +/// Rust: `quantum::schrodinger::perturbation_theory_1st` +#[pyfunction] +#[pyo3(name = "perturbation_theory_1st", signature = (states, perturbation, dx))] +pub fn pyfn_perturbation_theory_1st<'py>(py: Python<'py>, states: Vec>, perturbation: Vec, dx: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::quantum::schrodinger::perturbation_theory_1st(&states, &perturbation, dx))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Second-order energy shifts. +/// +/// A sum over the other states of `||^2 / (E_n - E_m)`. The sign is +/// forced for the ground state: every other state lies above it, so every +/// term is negative and the ground state is always pushed *down* by a +/// perturbation at second order, whatever the perturbation is. That is +/// level repulsion, and it is why avoided crossings avoid. +/// +/// Errors: +/// Returns an error on a length mismatch or degenerate levels, which +/// non-degenerate perturbation theory cannot treat. +/// +/// Rust: `quantum::schrodinger::perturbation_theory_2nd` +#[pyfunction] +#[pyo3(name = "perturbation_theory_2nd", signature = (states, energies, perturbation, dx))] +pub fn pyfn_perturbation_theory_2nd<'py>(py: Python<'py>, states: Vec>, energies: Vec, perturbation: Vec, dx: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::quantum::schrodinger::perturbation_theory_2nd(&states, &energies, &perturbation, dx))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The linear Stark shift of a hydrogen level in atomic units. +/// +/// Zero for `n = 1` and `3 n (n_1 - n_2) / 2` times the field for the excited +/// levels, whose degeneracy the field lifts. The ground state's vanishing +/// first-order shift is the general rule -- a non-degenerate state with +/// definite parity has no permanent dipole -- and hydrogen's excited levels +/// are the exception because their accidental degeneracy mixes opposite +/// parities. +/// +/// `parabolic_difference` is `n_1 - n_2` in the parabolic quantum numbers. +/// +/// Errors: +/// Returns an error for `n = 0` or an out-of-range parabolic difference. +/// +/// Rust: `quantum::schrodinger::stark_shift_perturbative` +#[pyfunction] +#[pyo3(name = "stark_shift_perturbative", signature = (field, n, parabolic_difference))] +pub fn pyfn_stark_shift_perturbative(field: f64, n: usize, parabolic_difference: i32) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::schrodinger::stark_shift_perturbative(field, n, parabolic_difference)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The variational ground state: minimises the expected energy of a trial +/// wavefunction over its parameters. +/// +/// The bound is one-sided and it is exact: `` over *any* normalisable +/// trial state is at least the true ground energy, because expanding the +/// trial state in eigenstates writes `` as a weighted average of +/// eigenvalues. So a variational calculation can never accidentally report +/// too low an energy, and the only way to be wrong is to be too high. +/// +/// Returns the minimised energy and the parameters that achieve it. +/// +/// Errors: +/// Returns an error for an empty grid or parameter vector. +/// +/// Rust: `quantum::schrodinger::variational_ground_state` +#[pyfunction] +#[pyo3(name = "variational_ground_state", signature = (v, dx, x0, trial, params0, mass, hbar))] +pub fn pyfn_variational_ground_state(v: Vec, dx: f64, x0: f64, trial: pyo3::Py, params0: Vec, mass: f64, hbar: f64) -> PyResult<(f64, Vec)> { + let __cb_trial = std::rc::Rc::new(crate::runtime::Callback::new(trial)); + let trial = { let __cb = __cb_trial.clone(); move |__a0: f64, __a1: &[f64]| -> f64 { __cb.call::<_, f64>((__a0, __a1.to_vec()), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::schrodinger::variational_ground_state(&v, dx, x0, &trial, ¶ms0, mass, hbar)); + crate::runtime::callback::check(&[&__cb_trial], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// The ground state by propagation in imaginary time. +/// +/// Replacing `t` with `-i tau` turns the oscillating phases `exp(-i E t)` +/// into decaying exponentials `exp(-E tau)`, so every excited component dies +/// faster than the ground state and what survives, renormalised, is the +/// ground state. The convergence rate is set by the gap `E_1 - E_0`, which +/// makes the method slow precisely for the nearly degenerate systems where +/// the answer is most delicate. +/// +/// Returns the ground energy and the normalised state. +/// +/// Errors: +/// Returns an error for a mismatched grid or non-positive constants. +/// +/// Rust: `quantum::schrodinger::imaginary_time_propagation` +#[pyfunction] +#[pyo3(name = "imaginary_time_propagation", signature = (v, dx, dtau, steps, mass, hbar))] +pub fn pyfn_imaginary_time_propagation<'py>(py: Python<'py>, v: Vec, dx: f64, dtau: f64, steps: usize, mass: f64, hbar: f64) -> PyResult<(f64, Vec)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::quantum::schrodinger::imaginary_time_propagation(&v, dx, dtau, steps, mass, hbar))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// The largest discrepancy in Ehrenfest's theorem along a trajectory. +/// +/// `d

/dt = -`: the expectations obey Newton's second law exactly, +/// with the force *averaged over the packet* rather than evaluated at its +/// centre. Those two differ as soon as the potential is not quadratic, which +/// is the precise sense in which a quantum particle is not a classical one -- +/// and the reason a wavepacket in a harmonic well follows the classical orbit +/// forever while one in any other well does not. +/// +/// Errors: +/// Returns an error for fewer than three snapshots or a mismatched potential. +/// +/// Rust: `quantum::schrodinger::ehrenfest_check` +#[pyfunction] +#[pyo3(name = "ehrenfest_check", signature = (snapshots, v, dt, hbar, mass))] +pub fn pyfn_ehrenfest_check<'py>(py: Python<'py>, snapshots: Vec, v: Vec, dt: f64, hbar: f64, mass: f64) -> PyResult { + let snapshots = snapshots.into_iter().map(|__e| __e.inner).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::quantum::schrodinger::ehrenfest_check(&snapshots, &v, dt, hbar, mass))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Scatters a wavepacket off a potential and returns the transmitted and +/// reflected probabilities. +/// +/// The packet carries a spread of momenta, so what comes back is the +/// transmission averaged over that spread rather than the value at the mean +/// momentum. A narrow packet in position is broad in momentum, so the sharper +/// the incident pulse the more the measured coefficient is smeared -- the +/// uncertainty relation showing up as an experimental resolution limit. +/// +/// Errors: +/// Returns an error for a mismatched grid or non-positive constants. +/// +/// Rust: `quantum::schrodinger::wavepacket_scattering` +#[pyfunction] +#[pyo3(name = "wavepacket_scattering", signature = (v, dx, x0, barrier_centre, k0, sigma, start, dt, steps, mass, hbar))] +pub fn pyfn_wavepacket_scattering<'py>(py: Python<'py>, v: Vec, dx: f64, x0: f64, barrier_centre: f64, k0: f64, sigma: f64, start: f64, dt: f64, steps: usize, mass: f64, hbar: f64) -> PyResult<(f64, f64)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::quantum::schrodinger::wavepacket_scattering(&v, dx, x0, barrier_centre, k0, sigma, start, dt, steps, mass, hbar))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// One-dimensional Gross-Pitaevskii evolution by split-step. +/// +/// The condensate's mean field adds a term `g |psi|^2` to the potential, so +/// the equation is nonlinear and superposition fails. With `g < 0` the +/// attraction can balance dispersion exactly and the result is a bright +/// soliton that propagates without spreading -- which a free packet never +/// does, and which is the clearest signature that the nonlinearity is really +/// there. +/// +/// Errors: +/// Returns an error for a mismatched grid or non-positive constants. +/// +/// Rust: `quantum::schrodinger::gross_pitaevskii_1d` +#[pyfunction] +#[pyo3(name = "gross_pitaevskii_1d", signature = (psi, v, g, dt, steps, mass, hbar))] +pub fn pyfn_gross_pitaevskii_1d(psi: pyo3::PyRefMut<'_, crate::generated::types::PyWavefunction1D>, v: Vec, g: f64, dt: f64, steps: usize, mass: f64, hbar: f64) -> PyResult<()> { + let mut psi = psi; + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::schrodinger::gross_pitaevskii_1d(&mut psi.inner, &v, g, dt, steps, mass, hbar)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(()) +} + +/// The exact bright soliton of the one-dimensional Gross-Pitaevskii equation +/// with `g < 0`, moving at speed `velocity`. +/// +/// `psi = sqrt(n0) sech((x - v t) / xi) exp(i(...))`. Its shape is preserved +/// exactly for all time, which is what "soliton" means and what distinguishes +/// it from an ordinary travelling wave. +/// +/// Panics: +/// Panics unless the amplitude and healing length are positive. +/// +/// Rust: `quantum::schrodinger::soliton_bright_exact` +#[pyfunction] +#[pyo3(name = "soliton_bright_exact", signature = (x, t, amplitude, width, velocity, mass, hbar))] +pub fn pyfn_soliton_bright_exact<'py>(py: Python<'py>, x: f64, t: f64, amplitude: f64, width: f64, velocity: f64, mass: f64, hbar: f64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::schrodinger::soliton_bright_exact(x, t, amplitude, width, velocity, mass, hbar)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::complex_out(py, __v)) +} + +/// The revival time of a particle in a box: the period after which every +/// phase returns to its start. +/// +/// The energies are `n^2` times a constant, so all the relative phases are +/// commensurate and the state reassembles exactly -- which is special to this +/// spectrum. At rational fractions of the revival time the state is a finite +/// superposition of displaced copies of itself, and plotting the density +/// against space and time produces the interference lattice known as a +/// quantum carpet. +/// +/// Panics: +/// Panics unless the width, mass and `hbar` are positive. +/// +/// Rust: `quantum::schrodinger::revival_time` +#[pyfunction] +#[pyo3(name = "revival_time", signature = (length, mass, hbar))] +pub fn pyfn_revival_time(length: f64, mass: f64, hbar: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::schrodinger::revival_time(length, mass, hbar)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The probability density of a box state at a sequence of times, one row per +/// time. +/// +/// `coefficients` gives the amplitude of each eigenstate, indexed from the +/// ground state. +/// +/// Errors: +/// Returns an error for an empty expansion or grid. +/// +/// Rust: `quantum::schrodinger::quantum_carpet` +#[pyfunction] +#[pyo3(name = "quantum_carpet", signature = (length, coefficients, times, points, mass, hbar))] +pub fn pyfn_quantum_carpet<'py>(py: Python<'py>, length: f64, coefficients: Vec, times: Vec, points: usize, mass: f64, hbar: f64) -> PyResult>> { + let coefficients = coefficients.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::quantum::schrodinger::quantum_carpet(length, &coefficients, ×, points, mass, hbar))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The survival probability of a state under repeated projective measurement. +/// +/// With `measurements` checks spread over a total time `t`, the survival +/// probability is `(1 - (t / measurements)^2 / tau^2)^measurements`, which +/// tends to one as the measurements are made more often. That is the quantum +/// Zeno effect, and it turns on the *quadratic* short-time behaviour of the +/// survival probability: an exponential decay law would give the same answer +/// however often it was interrupted. +/// +/// Errors: +/// Returns an error for a non-positive Zeno time or no measurements. +/// +/// Rust: `quantum::schrodinger::zeno_survival` +#[pyfunction] +#[pyo3(name = "zeno_survival", signature = (t, tau, measurements))] +pub fn pyfn_zeno_survival(t: f64, tau: f64, measurements: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::schrodinger::zeno_survival(t, tau, measurements)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_tise_solve_fd, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tise_solve_numerov, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tise_solve_matrix_basis, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tdse_split_operator, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tdse_crank_nicolson, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_absorbing_boundary_cap, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_apply_absorber, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_transmission_coefficient, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tunneling_rectangular_exact, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wkb_tunneling, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wkb_quantization, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_reflection_step_potential, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_double_well_splitting, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_perturbation_theory_1st, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_perturbation_theory_2nd, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_stark_shift_perturbative, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_variational_ground_state, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_imaginary_time_propagation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ehrenfest_check, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wavepacket_scattering, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gross_pitaevskii_1d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_soliton_bright_exact, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_revival_time, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_quantum_carpet, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_zeno_survival, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_quantum__solid_state.rs b/bindings/python/src/generated/m_quantum__solid_state.rs new file mode 100644 index 0000000..b78eb6f --- /dev/null +++ b/bindings/python/src/generated/m_quantum__solid_state.rs @@ -0,0 +1,752 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// A one-dimensional tight-binding chain, returning the energies ascending +/// and the matching eigenvectors as rows. +/// +/// `on_site` gives each site's energy and `t_hop` the nearest-neighbour +/// amplitude. The whole band structure of a simple metal is this model with +/// the on-site energies equal. +/// +/// Errors: +/// Returns an error for fewer than two sites, more than five hundred, or an +/// eigensolver failure. +/// +/// Rust: `quantum::solid_state::tight_binding_1d` +#[pyfunction] +#[pyo3(name = "tight_binding_1d", signature = (t_hop, on_site, periodic))] +pub fn pyfn_tight_binding_1d<'py>(py: Python<'py>, t_hop: f64, on_site: Vec, periodic: bool) -> PyResult<(Vec, Vec>)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::quantum::solid_state::tight_binding_1d(t_hop, &on_site, periodic))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// The tight-binding band of an infinite chain: `-2 t cos(k a)`. +/// +/// The bandwidth is `4 t` whatever the lattice constant, and the effective +/// mass at the band bottom is `hbar^2 / (2 t a^2)` -- so a narrow band means +/// a heavy electron, which is the whole of why transition metal oxides +/// behave as they do. +/// +/// Rust: `quantum::solid_state::tight_binding_band_1d` +#[pyfunction] +#[pyo3(name = "tight_binding_band_1d", signature = (k, t_hop, a))] +pub fn pyfn_tight_binding_band_1d(k: f64, t_hop: f64, a: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::solid_state::tight_binding_band_1d(k, t_hop, a)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Su-Schrieffer-Heeger model: a dimerised chain with alternating +/// hoppings. +/// +/// Returns the energies ascending and the eigenvectors as rows. The chain has +/// `2 n` sites, `n` unit cells of two. +/// +/// Errors: +/// Returns an error for a bad cell count or an eigensolver failure. +/// +/// Rust: `quantum::solid_state::ssh_model` +#[pyfunction] +#[pyo3(name = "ssh_model", signature = (cells, t1, t2))] +pub fn pyfn_ssh_model(cells: usize, t1: f64, t2: f64) -> PyResult<(Vec, Vec>)> { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::solid_state::ssh_model(cells, t1, t2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// The SSH winding number: one in the topological phase, zero otherwise. +/// +/// The invariant is a property of the *bulk* -- it is computed from the +/// Hamiltonian's winding in momentum space with no reference to any edge -- +/// and yet it predicts the number of protected edge states. That is the +/// bulk-boundary correspondence, and it is why topological states survive +/// disorder that would destroy an ordinary bound state. +/// +/// Rust: `quantum::solid_state::ssh_winding_number` +#[pyfunction] +#[pyo3(name = "ssh_winding_number", signature = (t1, t2))] +pub fn pyfn_ssh_winding_number(t1: f64, t2: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::solid_state::ssh_winding_number(t1, t2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The number of near-zero-energy edge states of a finite SSH chain. +/// +/// Errors: +/// Returns an error for a bad cell count. +/// +/// Rust: `quantum::solid_state::ssh_edge_states` +#[pyfunction] +#[pyo3(name = "ssh_edge_states", signature = (cells, t1, t2))] +pub fn pyfn_ssh_edge_states(cells: usize, t1: f64, t2: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::solid_state::ssh_edge_states(cells, t1, t2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The spectrum of a tight-binding square lattice with open boundaries. +/// +/// The eigenvalues are separable: `-2t(cos(k_x a) + cos(k_y a))` with the +/// allowed momenta set by the box, so no diagonalisation is needed. That +/// separability is exactly why the square lattice is the standard sanity +/// check for a lattice code. +/// +/// Errors: +/// Returns an error for a bad lattice size. +/// +/// Rust: `quantum::solid_state::tight_binding_square` +#[pyfunction] +#[pyo3(name = "tight_binding_square", signature = (nx, ny, t_hop))] +pub fn pyfn_tight_binding_square<'py>(py: Python<'py>, nx: usize, ny: usize, t_hop: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::quantum::solid_state::tight_binding_square(nx, ny, t_hop))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The two graphene bands at a point of the Brillouin zone, in units where +/// the lattice constant is one. +/// +/// The bands touch at the corners of the zone, and near them the dispersion +/// is *linear* rather than quadratic -- the electrons behave as massless +/// Dirac particles. Nothing about that requires relativity; it is a +/// consequence of the honeycomb's two-atom basis and its symmetry. +/// +/// Rust: `quantum::solid_state::graphene_dispersion` +#[pyfunction] +#[pyo3(name = "graphene_dispersion", signature = (kx, ky, t_hop))] +pub fn pyfn_graphene_dispersion(kx: f64, ky: f64, t_hop: f64) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::solid_state::graphene_dispersion(kx, ky, t_hop)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// The six Dirac points of graphene, in the same units. +/// +/// Rust: `quantum::solid_state::dirac_points_graphene` +#[pyfunction] +#[pyo3(name = "dirac_points_graphene", signature = ())] +pub fn pyfn_dirac_points_graphene<'py>(py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::quantum::solid_state::dirac_points_graphene())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// The Kronig-Penney dispersion function: the right-hand side of +/// `cos(k L) = f(E)`. +/// +/// Bands are where `|f| <= 1`, since only there does a real crystal momentum +/// exist. Where `|f| > 1` the momentum is complex and the states decay -- +/// that is a gap, and it is the whole mechanism by which a periodic potential +/// forbids energies. +/// +/// The well has width `a` and depth zero, the barrier width `b` and height +/// `v0`. +/// +/// Errors: +/// Returns an error for non-positive widths, mass, or `hbar`. +/// +/// Rust: `quantum::solid_state::kronig_penney` +#[pyfunction] +#[pyo3(name = "kronig_penney", signature = (v0, a, b, energy, mass, hbar))] +pub fn pyfn_kronig_penney(v0: f64, a: f64, b: f64, energy: f64, mass: f64, hbar: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::solid_state::kronig_penney(v0, a, b, energy, mass, hbar)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The allowed energy bands of a Kronig-Penney lattice, as intervals. +/// +/// Errors: +/// Returns an error for a bad range or sample count. +/// +/// Rust: `quantum::solid_state::kronig_penney_bands` +#[pyfunction] +#[pyo3(name = "kronig_penney_bands", signature = (v0, a, b, energy_range, samples, mass, hbar))] +pub fn pyfn_kronig_penney_bands<'py>(py: Python<'py>, v0: f64, a: f64, b: f64, energy_range: (f64, f64), samples: usize, mass: f64, hbar: f64) -> PyResult> { + let energy_range = (energy_range.0, energy_range.1); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::quantum::solid_state::kronig_penney_bands(v0, a, b, energy_range, samples, mass, hbar))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// The free-electron density of states per unit volume in one dimension. +/// +/// Spin degeneracy is included, as it is in the two- and three-dimensional +/// versions below: integrating any of them up to the Fermi energy gives the +/// electron density directly, with no further factor of two. +/// +/// Diverges as `1 / sqrt(E)` at the band bottom -- a van Hove singularity, +/// and the reason one-dimensional systems are so unstable to any interaction +/// at all. +/// +/// Errors: +/// Returns an error for a non-positive mass or `hbar`. +/// +/// Rust: `quantum::solid_state::density_of_states_1d_free` +#[pyfunction] +#[pyo3(name = "density_of_states_1d_free", signature = (energy, mass, hbar))] +pub fn pyfn_density_of_states_1d_free(energy: f64, mass: f64, hbar: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::solid_state::density_of_states_1d_free(energy, mass, hbar)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The free-electron density of states in two dimensions: a constant. +/// +/// Energy independent above the band bottom, which is what makes a +/// two-dimensional electron gas the clean setting for the quantum Hall +/// effect. +/// +/// Errors: +/// Returns an error for a non-positive mass or `hbar`. +/// +/// Rust: `quantum::solid_state::density_of_states_2d_free` +#[pyfunction] +#[pyo3(name = "density_of_states_2d_free", signature = (energy, mass, hbar))] +pub fn pyfn_density_of_states_2d_free(energy: f64, mass: f64, hbar: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::solid_state::density_of_states_2d_free(energy, mass, hbar)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The free-electron density of states in three dimensions, going as +/// `sqrt(E)`. +/// +/// Errors: +/// Returns an error for a non-positive mass or `hbar`. +/// +/// Rust: `quantum::solid_state::density_of_states_3d_free` +#[pyfunction] +#[pyo3(name = "density_of_states_3d_free", signature = (energy, mass, hbar))] +pub fn pyfn_density_of_states_3d_free(energy: f64, mass: f64, hbar: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::solid_state::density_of_states_3d_free(energy, mass, hbar)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// A density of states from a list of levels, broadened by a Gaussian. +/// +/// Errors: +/// Returns an error for an empty list, a non-positive width, or too few +/// points. +/// +/// Rust: `quantum::solid_state::dos_from_bands` +#[pyfunction] +#[pyo3(name = "dos_from_bands", signature = (levels, sigma, points))] +pub fn pyfn_dos_from_bands<'py>(py: Python<'py>, levels: Vec, sigma: f64, points: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::quantum::solid_state::dos_from_bands(&levels, sigma, points))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// The Fermi-Dirac occupation. +/// +/// Errors: +/// Returns an error for a negative temperature. +/// +/// Rust: `quantum::solid_state::fermi_dirac` +#[pyfunction] +#[pyo3(name = "fermi_dirac", signature = (energy, mu, temperature))] +pub fn pyfn_fermi_dirac(energy: f64, mu: f64, temperature: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::solid_state::fermi_dirac(energy, mu, temperature)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The Bose-Einstein occupation. +/// +/// Diverges as the energy approaches the chemical potential, which is +/// condensation: the ground state's occupation is not bounded by one, and in +/// three dimensions it takes a macroscopic share below a finite temperature. +/// +/// Errors: +/// Returns an error for a negative temperature or an energy at or below the +/// chemical potential. +/// +/// Rust: `quantum::solid_state::bose_einstein` +#[pyfunction] +#[pyo3(name = "bose_einstein", signature = (energy, mu, temperature))] +pub fn pyfn_bose_einstein(energy: f64, mu: f64, temperature: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::solid_state::bose_einstein(energy, mu, temperature)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The Fermi energy of a free electron gas at the given number density. +/// +/// Errors: +/// Returns an error for a non-positive density or mass. +/// +/// Rust: `quantum::solid_state::fermi_energy_free` +#[pyfunction] +#[pyo3(name = "fermi_energy_free", signature = (density, mass))] +pub fn pyfn_fermi_energy_free(density: f64, mass: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::solid_state::fermi_energy_free(density, mass)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The Sommerfeld electronic heat capacity per electron. +/// +/// Linear in temperature, and smaller than the classical `3k/2` by a factor +/// of order `T / T_F` -- which resolves the nineteenth-century puzzle of why +/// metals' electrons contribute almost nothing to the heat capacity despite +/// carrying the current. Only those within `kT` of the Fermi surface can +/// absorb energy at all. +/// +/// Errors: +/// Returns an error for a non-positive Fermi temperature. +/// +/// Rust: `quantum::solid_state::sommerfeld_heat_capacity` +#[pyfunction] +#[pyo3(name = "sommerfeld_heat_capacity", signature = (temperature, fermi_temperature))] +pub fn pyfn_sommerfeld_heat_capacity(temperature: f64, fermi_temperature: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::solid_state::sommerfeld_heat_capacity(temperature, fermi_temperature)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The Debye heat capacity per atom. +/// +/// Goes as `T^3` at low temperature and to the classical `3k` at high -- +/// Dulong and Petit's law. The cube is the count of phonon modes thermally +/// accessible, and it is one of the earliest quantitative successes of +/// quantum theory applied to solids. +/// +/// Errors: +/// Returns an error for a non-positive Debye temperature. +/// +/// Rust: `quantum::solid_state::debye_heat_capacity` +#[pyfunction] +#[pyo3(name = "debye_heat_capacity", signature = (temperature, debye_temperature))] +pub fn pyfn_debye_heat_capacity(temperature: f64, debye_temperature: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::solid_state::debye_heat_capacity(temperature, debye_temperature)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The Einstein heat capacity per atom, from a single vibrational frequency. +/// +/// Falls exponentially at low temperature rather than as `T^3`, which is +/// exactly where the model fails and Debye's succeeds: a single frequency +/// leaves no low-energy modes to excite, and a real solid has acoustic +/// phonons of arbitrarily low frequency. +/// +/// Errors: +/// Returns an error for a non-positive Einstein temperature. +/// +/// Rust: `quantum::solid_state::einstein_heat_capacity` +#[pyfunction] +#[pyo3(name = "einstein_heat_capacity", signature = (temperature, einstein_temperature))] +pub fn pyfn_einstein_heat_capacity(temperature: f64, einstein_temperature: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::solid_state::einstein_heat_capacity(temperature, einstein_temperature)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The phonon dispersion of a monatomic chain. +/// +/// Linear at long wavelength -- sound -- and flattening at the zone boundary, +/// where the group velocity vanishes and the mode becomes a standing wave. +/// +/// Panics: +/// Panics unless the spring constant and mass are positive. +/// +/// Rust: `quantum::solid_state::phonon_dispersion_1d_monatomic` +#[pyfunction] +#[pyo3(name = "phonon_dispersion_1d_monatomic", signature = (k, spring, mass, a))] +pub fn pyfn_phonon_dispersion_1d_monatomic(k: f64, spring: f64, mass: f64, a: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::solid_state::phonon_dispersion_1d_monatomic(k, spring, mass, a)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The two phonon branches of a diatomic chain, acoustic first. +/// +/// The gap between them at the zone boundary is the mass difference made +/// audible: a diatomic crystal has optical modes that a monatomic one does +/// not, and they are what infrared spectroscopy sees. +/// +/// Panics: +/// Panics unless the spring constant and both masses are positive. +/// +/// Rust: `quantum::solid_state::phonon_dispersion_1d_diatomic` +#[pyfunction] +#[pyo3(name = "phonon_dispersion_1d_diatomic", signature = (k, spring, m1, m2, a))] +pub fn pyfn_phonon_dispersion_1d_diatomic(k: f64, spring: f64, m1: f64, m2: f64, a: f64) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::solid_state::phonon_dispersion_1d_diatomic(k, spring, m1, m2, a)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// The Bloch oscillation period of an electron in a static field. +/// +/// An electron in a perfect crystal under a constant force does not +/// accelerate away: it traverses the Brillouin zone and comes back, so it +/// *oscillates*. Ordinary conductors never show this because scattering +/// intervenes long before a period completes; superlattices, with their much +/// smaller zones, do. +/// +/// Errors: +/// Returns an error for a non-positive field or lattice constant. +/// +/// Rust: `quantum::solid_state::bloch_oscillation_period` +#[pyfunction] +#[pyo3(name = "bloch_oscillation_period", signature = (field, a))] +pub fn pyfn_bloch_oscillation_period(field: f64, a: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::solid_state::bloch_oscillation_period(field, a)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The energy of the `n`-th Landau level. +/// +/// Equally spaced by `hbar omega_c`, with a zero-point half. The spacing +/// depends on the field and not on the level, which is what makes the +/// magneto-oscillations periodic in `1 / B` and lets a Fermi surface be +/// measured. +/// +/// Errors: +/// Returns an error for a non-positive field or mass. +/// +/// Rust: `quantum::solid_state::landau_levels` +#[pyfunction] +#[pyo3(name = "landau_levels", signature = (field, n, mass))] +pub fn pyfn_landau_levels(field: f64, n: usize, mass: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::solid_state::landau_levels(field, n, mass)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The Hofstadter spectrum: the energies of a square lattice at each rational +/// flux `p / q`, as `(flux, energy)` pairs. +/// +/// The famous butterfly. At flux `p / q` the magnetic unit cell holds `q` +/// sites, so the band splits into `q` sub-bands -- and because that count +/// depends on the *denominator*, the spectrum is discontinuous in the flux at +/// every rational. It is the first place a fractal appeared in a physical +/// spectrum. +/// +/// Errors: +/// Returns an error for a bad denominator bound or momentum sample count. +/// +/// Rust: `quantum::solid_state::hofstadter_butterfly` +#[pyfunction] +#[pyo3(name = "hofstadter_butterfly", signature = (q_max, k_samples))] +pub fn pyfn_hofstadter_butterfly<'py>(py: Python<'py>, q_max: usize, k_samples: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::quantum::solid_state::hofstadter_butterfly(q_max, k_samples))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// The Hall conductance of `n` filled Landau levels, in siemens. +/// +/// Quantised in units of `e^2 / h` to a part in a billion, in samples whose +/// disorder is uncontrolled and whose geometry is irregular. That the answer +/// depends on nothing but fundamental constants is why it defines the ohm. +/// +/// Rust: `quantum::solid_state::quantum_hall_conductance` +#[pyfunction] +#[pyo3(name = "quantum_hall_conductance", signature = (filled))] +pub fn pyfn_quantum_hall_conductance(filled: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::solid_state::quantum_hall_conductance(filled)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Drude conductivity. +/// +/// Errors: +/// Returns an error for a non-positive relaxation time or mass. +/// +/// Rust: `quantum::solid_state::drude_conductivity` +#[pyfunction] +#[pyo3(name = "drude_conductivity", signature = (density, tau, mass))] +pub fn pyfn_drude_conductivity(density: f64, tau: f64, mass: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::solid_state::drude_conductivity(density, tau, mass)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The Hall coefficient of a single-carrier conductor. +/// +/// Its *sign* is the useful part: positive for holes and negative for +/// electrons, so a Hall measurement says which carries the current -- a fact +/// no conductivity measurement can supply. +/// +/// Errors: +/// Returns an error for zero density. +/// +/// Rust: `quantum::solid_state::hall_coefficient` +#[pyfunction] +#[pyo3(name = "hall_coefficient", signature = (density, charge))] +pub fn pyfn_hall_coefficient(density: f64, charge: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::solid_state::hall_coefficient(density, charge)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The effective mass at a point of a band, from its curvature. +/// +/// `m* = hbar^2 / (d^2 E / dk^2)`, which can be negative near a band top -- +/// and a negative effective mass is precisely what a hole is. +/// +/// Errors: +/// Returns an error for a non-positive step or a flat band. +/// +/// Rust: `quantum::solid_state::effective_mass_from_band` +#[pyfunction] +#[pyo3(name = "effective_mass_from_band", signature = (band, k0, h))] +pub fn pyfn_effective_mass_from_band(band: pyo3::Py, k0: f64, h: f64) -> PyResult { + let __cb_band = std::rc::Rc::new(crate::runtime::Callback::new(band)); + let band = { let __cb = __cb_band.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::solid_state::effective_mass_from_band(&band, k0, h)); + crate::runtime::callback::check(&[&__cb_band], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The intrinsic carrier density of a semiconductor, per cubic metre. +/// +/// The exponential in half the gap is what makes semiconductor conductivity +/// so temperature sensitive: silicon's carrier density roughly doubles every +/// eight kelvin at room temperature. +/// +/// Errors: +/// Returns an error for a non-positive temperature or mass. +/// +/// Rust: `quantum::solid_state::semiconductor_carrier_density` +#[pyfunction] +#[pyo3(name = "semiconductor_carrier_density", signature = (gap_ev, temperature, m_electron, m_hole))] +pub fn pyfn_semiconductor_carrier_density(gap_ev: f64, temperature: f64, m_electron: f64, m_hole: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::solid_state::semiconductor_carrier_density(gap_ev, temperature, m_electron, m_hole)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The built-in potential of a p-n junction, in volts. +/// +/// Errors: +/// Returns an error for non-positive doping, intrinsic density, or +/// temperature. +/// +/// Rust: `quantum::solid_state::pn_junction_builtin` +#[pyfunction] +#[pyo3(name = "pn_junction_builtin", signature = (acceptors, donors, intrinsic, temperature))] +pub fn pyfn_pn_junction_builtin(acceptors: f64, donors: f64, intrinsic: f64, temperature: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::solid_state::pn_junction_builtin(acceptors, donors, intrinsic, temperature)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The depletion width of an abrupt p-n junction, in metres. +/// +/// Errors: +/// Returns an error for non-positive doping or permittivity. +/// +/// Rust: `quantum::solid_state::depletion_width` +#[pyfunction] +#[pyo3(name = "depletion_width", signature = (built_in, acceptors, donors, relative_permittivity))] +pub fn pyfn_depletion_width(built_in: f64, acceptors: f64, donors: f64, relative_permittivity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::solid_state::depletion_width(built_in, acceptors, donors, relative_permittivity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The BCS energy gap at temperature `t`, relative to its value at zero. +/// +/// Solved from the gap equation, which is self-consistent: the gap appears on +/// both sides, so it has the trivial solution zero above the critical +/// temperature and a non-zero one below. That the transition is continuous +/// and the gap opens as `sqrt(1 - T / Tc)` is a prediction of the theory, not +/// an input to it. +/// +/// Errors: +/// Returns an error for a non-positive critical temperature. +/// +/// Rust: `quantum::solid_state::bcs_gap_equation` +#[pyfunction] +#[pyo3(name = "bcs_gap_equation", signature = (temperature, critical_temperature))] +pub fn pyfn_bcs_gap_equation(temperature: f64, critical_temperature: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::solid_state::bcs_gap_equation(temperature, critical_temperature)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The BCS critical temperature from the coupling and the Debye frequency. +/// +/// `1.14 theta_D exp(-1 / lambda)`. The exponential in the reciprocal +/// coupling has no expansion about zero coupling, which is why +/// superconductivity could not be found by perturbation theory and took forty +/// years to explain. +/// +/// Errors: +/// Returns an error for a non-positive coupling or Debye temperature. +/// +/// Rust: `quantum::solid_state::bcs_tc_from_coupling` +#[pyfunction] +#[pyo3(name = "bcs_tc_from_coupling", signature = (coupling, debye_temperature))] +pub fn pyfn_bcs_tc_from_coupling(coupling: f64, debye_temperature: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::solid_state::bcs_tc_from_coupling(coupling, debye_temperature)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The DC Josephson current across a junction. +/// +/// A supercurrent flows with no voltage at all, set only by the phase +/// difference across the barrier. It is the most direct evidence that the +/// superconducting order parameter has a phase and that the phase is +/// physical. +/// +/// Rust: `quantum::solid_state::josephson_current` +#[pyfunction] +#[pyo3(name = "josephson_current", signature = (critical_current, phase))] +pub fn pyfn_josephson_current(critical_current: f64, phase: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::solid_state::josephson_current(critical_current, phase)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The AC Josephson frequency at a given voltage: `2 e V / h`. +/// +/// About 484 terahertz per volt, and known to a part in `10^10` -- which is +/// why the Josephson effect defines the volt. +/// +/// Rust: `quantum::solid_state::josephson_frequency` +#[pyfunction] +#[pyo3(name = "josephson_frequency", signature = (voltage))] +pub fn pyfn_josephson_frequency(voltage: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::solid_state::josephson_frequency(voltage)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The localisation length of a disordered one-dimensional chain, in lattice +/// sites. +/// +/// Every state in one dimension is localised for any disorder whatever, which +/// is the sharpest statement in the subject: there is no mobility edge and no +/// metallic phase, however weak the randomness. The length is extracted as +/// the reciprocal Lyapunov exponent of the transfer matrix product. +/// +/// Errors: +/// Returns an error for a bad chain length, disorder, or trial count. +/// +/// Rust: `quantum::solid_state::anderson_localization_1d` +#[pyfunction] +#[pyo3(name = "anderson_localization_1d", signature = (n, disorder, energy, trials, rng))] +pub fn pyfn_anderson_localization_1d(n: usize, disorder: f64, energy: f64, trials: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::solid_state::anderson_localization_1d(n, disorder, energy, trials, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The Landauer conductance of a set of transmission channels, in siemens. +/// +/// Conductance is transmission: a ballistic channel with perfect transmission +/// carries `2 e^2 / h` and no more, so even a perfect wire has a finite +/// resistance. That resistance is not dissipation in the wire -- it is the +/// cost of matching a few channels to the infinitely many in the leads. +/// +/// Errors: +/// Returns an error if a transmission is outside `[0, 1]`. +/// +/// Rust: `quantum::solid_state::conductance_landauer` +#[pyfunction] +#[pyo3(name = "conductance_landauer", signature = (transmissions))] +pub fn pyfn_conductance_landauer<'py>(py: Python<'py>, transmissions: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::quantum::solid_state::conductance_landauer(&transmissions))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_tight_binding_1d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tight_binding_band_1d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ssh_model, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ssh_winding_number, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ssh_edge_states, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tight_binding_square, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_graphene_dispersion, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dirac_points_graphene, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kronig_penney, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kronig_penney_bands, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_density_of_states_1d_free, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_density_of_states_2d_free, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_density_of_states_3d_free, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dos_from_bands, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fermi_dirac, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bose_einstein, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fermi_energy_free, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sommerfeld_heat_capacity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_debye_heat_capacity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_einstein_heat_capacity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_phonon_dispersion_1d_monatomic, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_phonon_dispersion_1d_diatomic, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bloch_oscillation_period, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_landau_levels, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hofstadter_butterfly, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_quantum_hall_conductance, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_drude_conductivity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hall_coefficient, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_effective_mass_from_band, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_semiconductor_carrier_density, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pn_junction_builtin, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_depletion_width, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bcs_gap_equation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bcs_tc_from_coupling, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_josephson_current, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_josephson_frequency, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_anderson_localization_1d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_conductance_landauer, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_quantum__spin.rs b/bindings/python/src/generated/m_quantum__spin.rs new file mode 100644 index 0000000..5f66ae0 --- /dev/null +++ b/bindings/python/src/generated/m_quantum__spin.rs @@ -0,0 +1,363 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// The three Pauli matrices, in the order `X`, `Y`, `Z`. +/// +/// Rust: `quantum::spin::pauli_matrices` +#[pyfunction] +#[pyo3(name = "pauli_matrices", signature = ())] +pub fn pyfn_pauli_matrices<'py>(py: Python<'py>) -> PyResult>>>> { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::spin::pauli_matrices()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.into_iter().map(|__x| __x.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()).collect::>()).collect::>()) +} + +/// The spin operators `(Sx, Sy, Sz)` for any spin `s`, as +/// `(2s + 1)`-dimensional matrices. +/// +/// Built from the ladder operators, whose matrix elements +/// `sqrt(s(s+1) - m(m+1))` are what make the representation finite: the +/// coefficient vanishes exactly at the top of the ladder, so raising the +/// highest state gives zero rather than escaping the space. That single fact +/// is why angular momentum is quantised. +/// +/// Errors: +/// Returns an error unless `2s` is a non-negative integer no larger than 20. +/// +/// Rust: `quantum::spin::spin_operators` +#[pyfunction] +#[pyo3(name = "spin_operators", signature = (s))] +pub fn pyfn_spin_operators<'py>(py: Python<'py>, s: f64) -> PyResult<(Vec>>, Vec>>, Vec>>)> { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::spin::spin_operators(s)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0.into_iter().map(|__x| __x.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()).collect::>(), __v.1.into_iter().map(|__x| __x.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()).collect::>(), __v.2.into_iter().map(|__x| __x.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()).collect::>())) +} + +/// A spin coherent state: the state pointing along `(theta, phi)`. +/// +/// The closest a spin gets to a classical arrow. Its uncertainty is the +/// minimum the algebra allows, and it becomes classical as `s` grows -- the +/// relative uncertainty falls as `1 / sqrt(s)`, which is why a macroscopic +/// magnet has a definite direction and a single electron does not. +/// +/// Errors: +/// Returns an error for an invalid spin. +/// +/// Rust: `quantum::spin::spin_coherent_state` +#[pyfunction] +#[pyo3(name = "spin_coherent_state", signature = (s, theta, phi))] +pub fn pyfn_spin_coherent_state<'py>(py: Python<'py>, s: f64, theta: f64, phi: f64) -> PyResult>> { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::spin::spin_coherent_state(s, theta, phi)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// The spectrum of two Heisenberg-coupled spin-1/2 particles: a singlet and a +/// triplet. +/// +/// `S1 . S2 = (S^2 - S1^2 - S2^2) / 2`, so the energy depends only on the +/// total spin: `-3/4` for the singlet and `+1/4` for the threefold triplet, +/// times the coupling. The whole of chemical bonding in a two-electron +/// molecule is this splitting. +/// +/// Rust: `quantum::spin::heisenberg_2site_exact` +#[pyfunction] +#[pyo3(name = "heisenberg_2site_exact", signature = (j))] +pub fn pyfn_heisenberg_2site_exact<'py>(py: Python<'py>, j: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::quantum::spin::heisenberg_2site_exact(j))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The transverse-field Ising chain as a dense matrix. +/// +/// `H = -sum_i sigma^z_i sigma^z_{i+1} - g sum_i sigma^x_i`, in Pauli +/// matrices rather than spin operators, which is the convention the exact +/// solution below uses. +/// +/// Errors: +/// Returns an error outside two to ten sites. +/// +/// Rust: `quantum::spin::ising_transverse_field_dense` +#[pyfunction] +#[pyo3(name = "ising_transverse_field_dense", signature = (n, g, periodic))] +pub fn pyfn_ising_transverse_field_dense(n: usize, g: f64, periodic: bool) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::spin::ising_transverse_field_dense(n, g, periodic)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) +} + +/// Applies the transverse-field Ising Hamiltonian to a state vector. +/// +/// Matrix free, so the cost is `O(n 2^n)` rather than the `O(4^n)` of forming +/// the matrix -- which at ten sites is the difference between a megabyte and +/// a gigabyte, and between a Jacobi diagonalisation that finishes and one +/// that does not. +/// +/// Errors: +/// Returns an error for a bad site count or vector length. +/// +/// Rust: `quantum::spin::ising_transverse_field_apply` +#[pyfunction] +#[pyo3(name = "ising_transverse_field_apply", signature = (n, g, periodic, v))] +pub fn pyfn_ising_transverse_field_apply<'py>(py: Python<'py>, n: usize, g: f64, periodic: bool, v: Vec) -> PyResult>> { + let v = v.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::spin::ising_transverse_field_apply(n, g, periodic, &v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// The exact ground energy of the periodic transverse-field Ising chain, from +/// the Jordan-Wigner solution. +/// +/// The chain maps to free fermions, so the ground energy is a sum of +/// single-particle energies: `-sum_k sqrt(1 + g^2 - 2 g cos k)` over the +/// antiperiodic momenta `(2m + 1) pi / n`. That the interacting spin model +/// is secretly free is what makes it the standard testbed for quantum phase +/// transitions -- the critical point at `g = 1` is exactly known. +/// +/// Errors: +/// Returns an error for fewer than two sites. +/// +/// Rust: `quantum::spin::ising_transverse_field_exact` +#[pyfunction] +#[pyo3(name = "ising_transverse_field_exact", signature = (n, g))] +pub fn pyfn_ising_transverse_field_exact(n: usize, g: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::spin::ising_transverse_field_exact(n, g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The critical transverse field of the Ising chain, where the gap closes. +/// +/// Rust: `quantum::spin::itf_critical_point` +#[pyfunction] +#[pyo3(name = "itf_critical_point", signature = ())] +pub fn pyfn_itf_critical_point() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::spin::itf_critical_point()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The magnon dispersion of a ferromagnetic Heisenberg chain. +/// +/// `2 j s (1 - cos(k a))`, which vanishes as `k^2` at long wavelength. The +/// quadratic -- rather than linear -- dispersion is the signature of a +/// ferromagnet's broken symmetry, and it is why a ferromagnet's low- +/// temperature heat capacity goes as `T^(3/2)` while an antiferromagnet's +/// goes as `T^3`. +/// +/// Rust: `quantum::spin::magnon_dispersion` +#[pyfunction] +#[pyo3(name = "magnon_dispersion", signature = (j, k, s, a))] +pub fn pyfn_magnon_dispersion(j: f64, k: f64, s: f64, a: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::spin::magnon_dispersion(j, k, s, a)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Larmor precession angle after a time `t` in a field of magnitude `b`. +/// +/// The precession rate depends on the field and the gyromagnetic ratio and +/// not at all on the angle, which is why a spin precesses at a fixed +/// frequency however it is tipped. +/// +/// Rust: `quantum::spin::larmor_frequency` +#[pyfunction] +#[pyo3(name = "larmor_frequency", signature = (b, gamma))] +pub fn pyfn_larmor_frequency(b: f64, gamma: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::spin::larmor_frequency(b, gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The magnetisation vector after Larmor precession about the `z` axis. +/// +/// The sense is the one the Bloch equation `dM/dt = gamma M x B` gives: for a +/// positive gyromagnetic ratio and a field along `+z`, the vector turns +/// *clockwise* seen from `+z`, so the angular velocity is `-gamma B`. Half +/// the sign conventions in the literature differ, and the two disagree on +/// everything that depends on the direction of a rotation. +/// +/// Rust: `quantum::spin::larmor_precession` +#[pyfunction] +#[pyo3(name = "larmor_precession", signature = (m0, b, gamma, t))] +pub fn pyfn_larmor_precession(m0: (f64, f64, f64), b: f64, gamma: f64, t: f64) -> PyResult<(f64, f64, f64)> { + let m0 = (m0.0, m0.1, m0.2); + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::spin::larmor_precession(m0, b, gamma, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// The excited-state probability of a driven two-level system: Rabi's +/// formula. +/// +/// `(omega^2 / Omega^2) sin^2(Omega t / 2)` with the generalised frequency +/// `Omega = sqrt(omega^2 + delta^2)`. Off resonance the oscillation is faster +/// and shallower, and the peak probability falls as the detuning grows -- +/// which is why a driven transition is a filter as well as a rotation. +/// +/// Errors: +/// Returns an error if the drive and detuning are both zero. +/// +/// Rust: `quantum::spin::rabi_oscillation` +#[pyfunction] +#[pyo3(name = "rabi_oscillation", signature = (rabi, detuning, t))] +pub fn pyfn_rabi_oscillation(rabi: f64, detuning: f64, t: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::spin::rabi_oscillation(rabi, detuning, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Ramsey fringes: the signal after two pulses separated by a free evolution. +/// +/// The fringe spacing measures the detuning, and the envelope's decay +/// measures `T2*` -- the *inhomogeneous* dephasing time, which includes +/// static field variations that a spin echo can undo. That distinction is the +/// point of the technique. +/// +/// Rust: `quantum::spin::ramsey_fringes` +#[pyfunction] +#[pyo3(name = "ramsey_fringes", signature = (detuning, free_time, t2_star))] +pub fn pyfn_ramsey_fringes(detuning: f64, free_time: f64, t2_star: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::spin::ramsey_fringes(detuning, free_time, t2_star)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The spin echo amplitude at time `t` after a refocusing pulse at `t / 2`. +/// +/// The echo removes static dephasing -- every spin that ran fast now runs +/// slow for an equal time -- so what survives decays at the true `T2` rather +/// than the much shorter `T2*`. The difference between them is entirely +/// reversible dephasing, which is why the echo can recover a signal that +/// looked lost. +/// +/// Rust: `quantum::spin::spin_echo_sim` +#[pyfunction] +#[pyo3(name = "spin_echo_sim", signature = (t, t2, t2_star))] +pub fn pyfn_spin_echo_sim(t: f64, t2: f64, t2_star: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::spin::spin_echo_sim(t, t2, t2_star)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Integrates the Bloch equations for a magnetisation in a time-dependent +/// field. +/// +/// `dM/dt = gamma M x B - (Mx, My) / T2 - (Mz - M0) / T1`. The two relaxation +/// times are independent parameters and `T2 <= 2 T1` always, since the +/// transverse components cannot survive the longitudinal decay. +/// +/// Errors: +/// Returns an error for non-positive times or steps. +/// +/// Rust: `quantum::spin::bloch_equations` +#[pyfunction] +#[pyo3(name = "bloch_equations", signature = (m0, field, gamma, t1, t2, equilibrium, t_end, dt))] +pub fn pyfn_bloch_equations(m0: (f64, f64, f64), field: pyo3::Py, gamma: f64, t1: f64, t2: f64, equilibrium: f64, t_end: f64, dt: f64) -> PyResult> { + let m0 = (m0.0, m0.1, m0.2); + let __cb_field = std::rc::Rc::new(crate::runtime::Callback::new(field)); + let field = { let __cb = __cb_field.clone(); move |__a0: f64| -> (f64, f64, f64) { __cb.call::<_, (f64, f64, f64)>((__a0,), (f64::NAN, f64::NAN, f64::NAN)) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::spin::bloch_equations(m0, &field, gamma, t1, t2, equilibrium, t_end, dt)); + crate::runtime::callback::check(&[&__cb_field], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, __x.2)).collect::>()) +} + +/// A free induction decay: the sum of decaying sinusoids one per chemical +/// environment, sampled at `rate`. +/// +/// The Fourier transform of this is the spectrum, which is how nuclear +/// magnetic resonance actually works: the signal is measured in time and the +/// chemistry is read in frequency. +/// +/// Errors: +/// Returns an error for mismatched lists or a non-positive rate. +/// +/// Rust: `quantum::spin::nmr_fid` +#[pyfunction] +#[pyo3(name = "nmr_fid", signature = (frequencies, decay_times, samples, rate))] +pub fn pyfn_nmr_fid<'py>(py: Python<'py>, frequencies: Vec, decay_times: Vec, samples: usize, rate: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::quantum::spin::nmr_fid(&frequencies, &decay_times, samples, rate))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The Zeeman energy shift of a level in a magnetic field. +/// +/// Panics: +/// Never; the arithmetic is a product. +/// +/// Rust: `quantum::spin::zeeman_splitting` +#[pyfunction] +#[pyo3(name = "zeeman_splitting", signature = (b, g_factor, m_j))] +pub fn pyfn_zeeman_splitting(b: f64, g_factor: f64, m_j: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::spin::zeeman_splitting(b, g_factor, m_j)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The hydrogen hyperfine transition frequency in hertz: the 21 centimetre +/// line. +/// +/// The transition is forbidden to first order and has a mean lifetime of some +/// ten million years, so no laboratory sample of hydrogen would ever show it. +/// The galaxy has enough hydrogen that it is the brightest line in radio +/// astronomy. +/// +/// Rust: `quantum::spin::hyperfine_hydrogen_21cm` +#[pyfunction] +#[pyo3(name = "hyperfine_hydrogen_21cm", signature = ())] +pub fn pyfn_hyperfine_hydrogen_21cm() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::spin::hyperfine_hydrogen_21cm()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_pauli_matrices, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spin_operators, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spin_coherent_state, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_heisenberg_2site_exact, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ising_transverse_field_dense, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ising_transverse_field_apply, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ising_transverse_field_exact, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_itf_critical_point, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_magnon_dispersion, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_larmor_frequency, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_larmor_precession, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rabi_oscillation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ramsey_fringes, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spin_echo_sim, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bloch_equations, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_nmr_fid, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_zeeman_splitting, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hyperfine_hydrogen_21cm, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_quantum__wavefunction.rs b/bindings/python/src/generated/m_quantum__wavefunction.rs new file mode 100644 index 0000000..8f74a9b --- /dev/null +++ b/bindings/python/src/generated/m_quantum__wavefunction.rs @@ -0,0 +1,270 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// The physicists' Hermite polynomial `H_n(x)`. +/// +/// Evaluated by the upward recurrence `H_{n+1} = 2x H_n - 2n H_{n-1}` rather +/// than from the explicit sum, whose alternating terms cancel catastrophically: +/// at `n = 20` and moderate `x` the largest term exceeds the answer by many +/// orders of magnitude, and a direct sum loses every significant digit. +/// +/// Rust: `quantum::wavefunction::hermite_polynomial` +#[pyfunction] +#[pyo3(name = "hermite_polynomial", signature = (n, x))] +pub fn pyfn_hermite_polynomial(n: usize, x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::wavefunction::hermite_polynomial(n, x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The associated Laguerre polynomial `L_n^k(x)`. +/// +/// Also by recurrence, and for the same reason. +/// +/// Rust: `quantum::wavefunction::laguerre_associated` +#[pyfunction] +#[pyo3(name = "laguerre_associated", signature = (n, k, x))] +pub fn pyfn_laguerre_associated(n: usize, k: f64, x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::wavefunction::laguerre_associated(n, k, x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The `n`-th harmonic oscillator eigenstate, normalised on the whole line. +/// +/// The normalisation `(m omega / pi hbar)^(1/4) / sqrt(2^n n!)` is folded in +/// through logarithms, since `2^n n!` overflows a double at `n = 170` while +/// the state itself stays perfectly ordinary. +/// +/// Panics: +/// Panics unless the mass, frequency and `hbar` are positive. +/// +/// Rust: `quantum::wavefunction::harmonic_oscillator_eigenstate` +#[pyfunction] +#[pyo3(name = "harmonic_oscillator_eigenstate", signature = (n, x, mass, omega, hbar))] +pub fn pyfn_harmonic_oscillator_eigenstate(n: usize, x: f64, mass: f64, omega: f64, hbar: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::wavefunction::harmonic_oscillator_eigenstate(n, x, mass, omega, hbar)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The energy of the `n`-th harmonic oscillator level: `(n + 1/2) hbar omega`. +/// +/// The half is the zero-point energy, and it is not a convention: the ground +/// state cannot sit at the bottom of the well without violating the +/// uncertainty relation, and `hbar omega / 2` is exactly what the relation +/// costs. +/// +/// Panics: +/// Panics unless `omega` and `hbar` are positive. +/// +/// Rust: `quantum::wavefunction::harmonic_oscillator_energy` +#[pyfunction] +#[pyo3(name = "harmonic_oscillator_energy", signature = (n, omega, hbar))] +pub fn pyfn_harmonic_oscillator_energy(n: usize, omega: f64, hbar: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::wavefunction::harmonic_oscillator_energy(n, omega, hbar)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The `n`-th eigenstate of an infinite square well of width `l`, indexed +/// from one, and zero outside the well. +/// +/// Panics: +/// Panics unless `n >= 1` and the width is positive. +/// +/// Rust: `quantum::wavefunction::infinite_well_eigenstate` +#[pyfunction] +#[pyo3(name = "infinite_well_eigenstate", signature = (n, x, l))] +pub fn pyfn_infinite_well_eigenstate(n: usize, x: f64, l: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::wavefunction::infinite_well_eigenstate(n, x, l)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The energy of the `n`-th infinite-well level. +/// +/// Panics: +/// Panics unless `n >= 1` and the width, mass and `hbar` are positive. +/// +/// Rust: `quantum::wavefunction::infinite_well_energy` +#[pyfunction] +#[pyo3(name = "infinite_well_energy", signature = (n, l, mass, hbar))] +pub fn pyfn_infinite_well_energy(n: usize, l: f64, mass: f64, hbar: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::wavefunction::infinite_well_energy(n, l, mass, hbar)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The hydrogen radial wavefunction `R_{n,l}(r)` in units of the Bohr radius +/// `a0`. +/// +/// Panics: +/// Panics unless `n >= 1`, `l < n` and `a0` is positive. +/// +/// Rust: `quantum::wavefunction::hydrogen_radial` +#[pyfunction] +#[pyo3(name = "hydrogen_radial", signature = (n, l, r, a0))] +pub fn pyfn_hydrogen_radial(n: usize, l: usize, r: f64, a0: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::wavefunction::hydrogen_radial(n, l, r, a0)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The hydrogen energy level in electronvolts: `-13.6 / n^2`. +/// +/// Panics: +/// Panics unless `n >= 1`. +/// +/// Rust: `quantum::wavefunction::hydrogen_energy` +#[pyfunction] +#[pyo3(name = "hydrogen_energy", signature = (n))] +pub fn pyfn_hydrogen_energy(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::wavefunction::hydrogen_energy(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The probability density of a real hydrogen orbital at a point in spherical +/// coordinates. +/// +/// Uses the real spherical harmonics, so `m` selects the real combinations +/// -- the `p_x`, `p_y`, `p_z` shapes rather than the complex `m` eigenstates. +/// The two bases span the same space and give the same total density in a +/// shell; they differ in the angular shape of an individual orbital, which is +/// exactly what chemistry draws. +/// +/// Panics: +/// Panics unless `n >= 1`, `l < n`, `|m| <= l` and `a0` is positive. +/// +/// Rust: `quantum::wavefunction::hydrogen_orbital_density` +#[pyfunction] +#[pyo3(name = "hydrogen_orbital_density", signature = (n, l, m, r, theta, phi, a0))] +pub fn pyfn_hydrogen_orbital_density(n: usize, l: usize, m: i32, r: f64, theta: f64, phi: f64, a0: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::wavefunction::hydrogen_orbital_density(n, l, m, r, theta, phi, a0)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Fock coefficients of a coherent state `|alpha>`, truncated at +/// `n_max` photons. +/// +/// A Poisson distribution over photon number with mean `|alpha|^2`. Coherent +/// states are the eigenstates of the annihilation operator, which is why +/// removing a photon from a laser beam leaves it unchanged, and why the +/// photon statistics of a laser are Poissonian rather than thermal. +/// +/// Errors: +/// Returns an error for an empty truncation. +/// +/// Rust: `quantum::wavefunction::coherent_state` +#[pyfunction] +#[pyo3(name = "coherent_state", signature = (alpha, n_max))] +pub fn pyfn_coherent_state<'py>(py: Python<'py>, alpha: crate::runtime::coerce::ComplexArg, n_max: usize) -> PyResult>> { + let alpha = alpha.0; + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::wavefunction::coherent_state(alpha, n_max)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// The Fock coefficients of a squeezed vacuum state, truncated at `n_max`. +/// +/// Only the even photon numbers are populated, because the squeezing operator +/// creates photons in pairs. That parity is the state's signature and is what +/// makes it useful: the noise removed from one quadrature has to go somewhere, +/// and it goes into the other. +/// +/// Errors: +/// Returns an error for an empty truncation. +/// +/// Rust: `quantum::wavefunction::squeezed_state` +#[pyfunction] +#[pyo3(name = "squeezed_state", signature = (r, phi, n_max))] +pub fn pyfn_squeezed_state<'py>(py: Python<'py>, r: f64, phi: f64, n_max: usize) -> PyResult>> { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::wavefunction::squeezed_state(r, phi, n_max)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// The Wigner function of a wavefunction at a point of phase space. +/// +/// `W(x, p) = (1 / pi hbar) integral psi*(x + y) psi(x - y) e^{2 i p y / hbar} dy`. +/// +/// The nearest thing quantum mechanics has to a phase-space probability +/// density: its marginals are the true position and momentum distributions. +/// It is not a probability density, because it takes negative values -- and +/// where it does is exactly where the state has no classical description, so +/// the negativity is the useful part rather than a defect of the definition. +/// +/// Errors: +/// Returns an error for a non-positive spacing or `hbar`. +/// +/// Rust: `quantum::wavefunction::wigner_function` +#[pyfunction] +#[pyo3(name = "wigner_function", signature = (psi, dx, x0, x, p, hbar))] +pub fn pyfn_wigner_function<'py>(py: Python<'py>, psi: Vec, dx: f64, x0: f64, x: f64, p: f64, hbar: f64) -> PyResult { + let psi = psi.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::quantum::wavefunction::wigner_function(&psi, dx, x0, x, p, hbar))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The Husimi Q function: the Wigner function smoothed by a coherent state of +/// width `sigma`. +/// +/// Smoothing over a phase-space cell of the minimum allowed area is exactly +/// enough to remove the negativity, so `Q` is a genuine probability density. +/// What it buys in interpretability it loses in resolution: the interference +/// fringes that make the Wigner function negative are precisely what the +/// smoothing erases. +/// +/// Errors: +/// Returns an error for a non-positive spacing, width, or `hbar`. +/// +/// Rust: `quantum::wavefunction::husimi_q` +#[pyfunction] +#[pyo3(name = "husimi_q", signature = (psi, dx, x0, x, p, sigma, hbar))] +pub fn pyfn_husimi_q<'py>(py: Python<'py>, psi: Vec, dx: f64, x0: f64, x: f64, p: f64, sigma: f64, hbar: f64) -> PyResult { + let psi = psi.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::quantum::wavefunction::husimi_q(&psi, dx, x0, x, p, sigma, hbar))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_hermite_polynomial, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_laguerre_associated, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_harmonic_oscillator_eigenstate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_harmonic_oscillator_energy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_infinite_well_eigenstate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_infinite_well_energy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hydrogen_radial, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hydrogen_energy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hydrogen_orbital_density, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_coherent_state, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_squeezed_state, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wigner_function, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_husimi_q, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_quaternion.rs b/bindings/python/src/generated/m_quaternion.rs new file mode 100644 index 0000000..83838e2 --- /dev/null +++ b/bindings/python/src/generated/m_quaternion.rs @@ -0,0 +1,51 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Spherical linear interpolation between two quaternions at parameter t in [0, 1]. +/// +/// Rust: `quaternion::slerp` +#[pyfunction] +#[pyo3(name = "slerp", signature = (q1, q2, t))] +pub fn pyfn_slerp(q1: crate::generated::types::PyQuaternionArg, q2: crate::generated::types::PyQuaternionArg, t: f64) -> PyResult { + let q1 = q1.0; + let q2 = q2.0; + let __r = crate::runtime::guard(|| rust_physics_engine::quaternion::slerp(&q1, &q2, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuaternion { inner: __v }) +} + +/// Normalized linear interpolation between two quaternions (cheaper than slerp). +/// +/// Rust: `quaternion::nlerp` +#[pyfunction] +#[pyo3(name = "nlerp", signature = (q1, q2, t))] +pub fn pyfn_nlerp(q1: crate::generated::types::PyQuaternionArg, q2: crate::generated::types::PyQuaternionArg, t: f64) -> PyResult { + let q1 = q1.0; + let q2 = q2.0; + let __r = crate::runtime::guard(|| rust_physics_engine::quaternion::nlerp(&q1, &q2, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuaternion { inner: __v }) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_slerp, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_nlerp, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_radiation.rs b/bindings/python/src/generated/m_radiation.rs new file mode 100644 index 0000000..72e0d74 --- /dev/null +++ b/bindings/python/src/generated/m_radiation.rs @@ -0,0 +1,199 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Total emissive power of a perfect blackbody (ε=1): E = σT⁴ +/// +/// Rust: `radiation::total_emissive_power` +#[pyfunction] +#[pyo3(name = "total_emissive_power", signature = (temperature))] +pub fn pyfn_total_emissive_power(temperature: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::radiation::total_emissive_power(temperature)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Wien's law in the frequency domain: f_max = 5.879×10¹⁰ × T +/// +/// Rust: `radiation::spectral_peak_frequency` +#[pyfunction] +#[pyo3(name = "spectral_peak_frequency", signature = (temperature))] +pub fn pyfn_spectral_peak_frequency(temperature: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::radiation::spectral_peak_frequency(temperature)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Inverse Wien's law: T = b / λ_max (color temperature from peak wavelength) +/// +/// Rust: `radiation::color_temperature` +#[pyfunction] +#[pyo3(name = "color_temperature", signature = (peak_wavelength))] +pub fn pyfn_color_temperature(peak_wavelength: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::radiation::color_temperature(peak_wavelength)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Brightness temperature via the Rayleigh-Jeans approximation: T_b = Ic² / (2kf²) +/// +/// Rust: `radiation::brightness_temperature` +#[pyfunction] +#[pyo3(name = "brightness_temperature", signature = (intensity, frequency))] +pub fn pyfn_brightness_temperature(intensity: f64, frequency: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::radiation::brightness_temperature(intensity, frequency)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Optical depth: τ = κ × s +/// +/// Rust: `radiation::optical_depth` +#[pyfunction] +#[pyo3(name = "optical_depth", signature = (absorption_coeff, path_length))] +pub fn pyfn_optical_depth(absorption_coeff: f64, path_length: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::radiation::optical_depth(absorption_coeff, path_length)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Beer-Lambert law: I = I₀ × e^(-κs) +/// +/// Rust: `radiation::beer_lambert` +#[pyfunction] +#[pyo3(name = "beer_lambert", signature = (initial_intensity, absorption_coeff, path_length))] +pub fn pyfn_beer_lambert(initial_intensity: f64, absorption_coeff: f64, path_length: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::radiation::beer_lambert(initial_intensity, absorption_coeff, path_length)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Photon mean free path: l = 1/κ +/// +/// Rust: `radiation::mean_free_path_photon` +#[pyfunction] +#[pyo3(name = "mean_free_path_photon", signature = (absorption_coeff))] +pub fn pyfn_mean_free_path_photon(absorption_coeff: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::radiation::mean_free_path_photon(absorption_coeff)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Radiation pressure for fully absorbed radiation: P = I/c +/// +/// Rust: `radiation::radiation_pressure` +#[pyfunction] +#[pyo3(name = "radiation_pressure", signature = (intensity))] +pub fn pyfn_radiation_pressure(intensity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::radiation::radiation_pressure(intensity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Radiation pressure for fully reflected radiation: P = 2I/c +/// +/// Rust: `radiation::radiation_pressure_reflected` +#[pyfunction] +#[pyo3(name = "radiation_pressure_reflected", signature = (intensity))] +pub fn pyfn_radiation_pressure_reflected(intensity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::radiation::radiation_pressure_reflected(intensity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// At thermal equilibrium, emissivity equals absorptivity: ε = α +/// +/// Rust: `radiation::emissivity_from_absorptivity` +#[pyfunction] +#[pyo3(name = "emissivity_from_absorptivity", signature = (absorptivity))] +pub fn pyfn_emissivity_from_absorptivity(absorptivity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::radiation::emissivity_from_absorptivity(absorptivity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// View factor for two identical, directly opposed, parallel rectangles of +/// width W and height H separated by distance D. +/// +/// Uses the exact analytical formula: +/// F = (2 / (πXY)) * [ ln(√((1+X²)(1+Y²)/(1+X²+Y²))) +/// + X√(1+Y²) atan(X/√(1+Y²)) +/// + Y√(1+X²) atan(Y/√(1+X²)) +/// - X atan(X) - Y atan(Y) ] +/// where X = W/D and Y = H/D. +/// +/// Rust: `radiation::view_factor_parallel_plates` +#[pyfunction] +#[pyo3(name = "view_factor_parallel_plates", signature = (width, height, separation))] +pub fn pyfn_view_factor_parallel_plates(width: f64, height: f64, separation: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::radiation::view_factor_parallel_plates(width, height, separation)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Radiative heat exchange between two infinite parallel gray surfaces: +/// Q = σA(T₁⁴ - T₂⁴) / (1/ε₁ + 1/ε₂ - 1) +/// +/// Rust: `radiation::radiative_exchange` +#[pyfunction] +#[pyo3(name = "radiative_exchange", signature = (emissivity1, emissivity2, area, t1, t2))] +pub fn pyfn_radiative_exchange(emissivity1: f64, emissivity2: f64, area: f64, t1: f64, t2: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::radiation::radiative_exchange(emissivity1, emissivity2, area, t1, t2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Intensity at distance from a point source: I = L / (4πd²) +/// +/// Rust: `radiation::intensity_at_distance` +#[pyfunction] +#[pyo3(name = "intensity_at_distance", signature = (luminosity, distance))] +pub fn pyfn_intensity_at_distance(luminosity: f64, distance: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::radiation::intensity_at_distance(luminosity, distance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Luminosity from measured intensity and distance: L = I × 4πd² +/// +/// Rust: `radiation::luminosity_from_intensity` +#[pyfunction] +#[pyo3(name = "luminosity_from_intensity", signature = (intensity, distance))] +pub fn pyfn_luminosity_from_intensity(intensity: f64, distance: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::radiation::luminosity_from_intensity(intensity, distance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_total_emissive_power, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spectral_peak_frequency, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_color_temperature, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_brightness_temperature, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_optical_depth, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_beer_lambert, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mean_free_path_photon, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_radiation_pressure, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_radiation_pressure_reflected, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_emissivity_from_absorptivity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_view_factor_parallel_plates, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_radiative_exchange, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_intensity_at_distance, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_luminosity_from_intensity, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_relativity.rs b/bindings/python/src/generated/m_relativity.rs new file mode 100644 index 0000000..0551b00 --- /dev/null +++ b/bindings/python/src/generated/m_relativity.rs @@ -0,0 +1,239 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Lorentz factor: γ = 1 / sqrt(1 - v^2/c^2) +/// +/// Rust: `relativity::lorentz_factor` +#[pyfunction] +#[pyo3(name = "lorentz_factor", signature = (velocity))] +pub fn pyfn_lorentz_factor(velocity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::relativity::lorentz_factor(velocity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Beta factor: β = v / c +/// +/// Rust: `relativity::beta` +#[pyfunction] +#[pyo3(name = "beta", signature = (velocity))] +pub fn pyfn_beta(velocity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::relativity::beta(velocity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Time dilation: Δt = γ * Δt_proper +/// +/// Rust: `relativity::time_dilation` +#[pyfunction] +#[pyo3(name = "time_dilation", signature = (proper_time, velocity))] +pub fn pyfn_time_dilation(proper_time: f64, velocity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::relativity::time_dilation(proper_time, velocity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Length contraction: L = L_proper / γ +/// +/// Rust: `relativity::length_contraction` +#[pyfunction] +#[pyo3(name = "length_contraction", signature = (proper_length, velocity))] +pub fn pyfn_length_contraction(proper_length: f64, velocity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::relativity::length_contraction(proper_length, velocity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Relativistic momentum: p = γ * m * v +/// +/// Rust: `relativity::relativistic_momentum` +#[pyfunction] +#[pyo3(name = "relativistic_momentum", signature = (mass, velocity))] +pub fn pyfn_relativistic_momentum(mass: f64, velocity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::relativity::relativistic_momentum(mass, velocity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Relativistic kinetic energy: KE = (γ - 1) * m * c^2 +/// +/// Rust: `relativity::relativistic_kinetic_energy` +#[pyfunction] +#[pyo3(name = "relativistic_kinetic_energy", signature = (mass, velocity))] +pub fn pyfn_relativistic_kinetic_energy(mass: f64, velocity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::relativity::relativistic_kinetic_energy(mass, velocity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Total relativistic energy: E = γ * m * c^2 +/// +/// Rust: `relativity::relativistic_total_energy` +#[pyfunction] +#[pyo3(name = "relativistic_total_energy", signature = (mass, velocity))] +pub fn pyfn_relativistic_total_energy(mass: f64, velocity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::relativity::relativistic_total_energy(mass, velocity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Rest energy: E = m * c^2 +/// +/// Rust: `relativity::rest_energy` +#[pyfunction] +#[pyo3(name = "rest_energy", signature = (mass))] +pub fn pyfn_rest_energy(mass: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::relativity::rest_energy(mass)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Energy-momentum relation: E^2 = (pc)^2 + (mc^2)^2 +/// Returns total energy given momentum and rest mass. +/// +/// Rust: `relativity::energy_from_momentum` +#[pyfunction] +#[pyo3(name = "energy_from_momentum", signature = (momentum, mass))] +pub fn pyfn_energy_from_momentum(momentum: f64, mass: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::relativity::energy_from_momentum(momentum, mass)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Relativistic velocity addition: u = (v + u') / (1 + v*u'/c^2) +/// +/// Rust: `relativity::velocity_addition` +#[pyfunction] +#[pyo3(name = "velocity_addition", signature = (v, u_prime))] +pub fn pyfn_velocity_addition(v: f64, u_prime: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::relativity::velocity_addition(v, u_prime)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Lorentz transformation of position: x' = γ * (x - v*t) +/// +/// Rust: `relativity::lorentz_transform_x` +#[pyfunction] +#[pyo3(name = "lorentz_transform_x", signature = (x, v, t))] +pub fn pyfn_lorentz_transform_x(x: f64, v: f64, t: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::relativity::lorentz_transform_x(x, v, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Lorentz transformation of time: t' = γ * (t - v*x/c^2) +/// +/// Rust: `relativity::lorentz_transform_t` +#[pyfunction] +#[pyo3(name = "lorentz_transform_t", signature = (t, v, x))] +pub fn pyfn_lorentz_transform_t(t: f64, v: f64, x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::relativity::lorentz_transform_t(t, v, x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Relativistic Doppler effect (approaching): f' = f * sqrt((1+β)/(1-β)) +/// +/// Rust: `relativity::relativistic_doppler_approaching` +#[pyfunction] +#[pyo3(name = "relativistic_doppler_approaching", signature = (frequency, velocity))] +pub fn pyfn_relativistic_doppler_approaching(frequency: f64, velocity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::relativity::relativistic_doppler_approaching(frequency, velocity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Relativistic Doppler effect (receding): f' = f * sqrt((1-β)/(1+β)) +/// +/// Rust: `relativity::relativistic_doppler_receding` +#[pyfunction] +#[pyo3(name = "relativistic_doppler_receding", signature = (frequency, velocity))] +pub fn pyfn_relativistic_doppler_receding(frequency: f64, velocity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::relativity::relativistic_doppler_receding(frequency, velocity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Gravitational redshift: f_obs = f_emit * sqrt(1 - 2GM/(rc^2)) +/// +/// Rust: `relativity::gravitational_redshift` +#[pyfunction] +#[pyo3(name = "gravitational_redshift", signature = (emitted_freq, mass, radius))] +pub fn pyfn_gravitational_redshift(emitted_freq: f64, mass: f64, radius: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::relativity::gravitational_redshift(emitted_freq, mass, radius)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Relativistic mass (apparent mass at velocity v): m_rel = γ * m_0 +/// +/// Rust: `relativity::relativistic_mass` +#[pyfunction] +#[pyo3(name = "relativistic_mass", signature = (rest_mass, velocity))] +pub fn pyfn_relativistic_mass(rest_mass: f64, velocity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::relativity::relativistic_mass(rest_mass, velocity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Proper time interval from coordinate time: Δτ = Δt / γ +/// +/// Rust: `relativity::proper_time` +#[pyfunction] +#[pyo3(name = "proper_time", signature = (coordinate_time, velocity))] +pub fn pyfn_proper_time(coordinate_time: f64, velocity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::relativity::proper_time(coordinate_time, velocity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Spacetime interval: s^2 = (cΔt)^2 - Δx^2 - Δy^2 - Δz^2 +/// +/// Rust: `relativity::spacetime_interval_squared` +#[pyfunction] +#[pyo3(name = "spacetime_interval_squared", signature = (dt, dx, dy, dz))] +pub fn pyfn_spacetime_interval_squared(dt: f64, dx: f64, dy: f64, dz: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::relativity::spacetime_interval_squared(dt, dx, dy, dz)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_lorentz_factor, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_beta, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_time_dilation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_length_contraction, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_relativistic_momentum, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_relativistic_kinetic_energy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_relativistic_total_energy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rest_energy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_energy_from_momentum, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_velocity_addition, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lorentz_transform_x, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lorentz_transform_t, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_relativistic_doppler_approaching, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_relativistic_doppler_receding, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gravitational_redshift, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_relativistic_mass, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_proper_time, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spacetime_interval_squared, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_resonance.rs b/bindings/python/src/generated/m_resonance.rs new file mode 100644 index 0000000..c02bbf8 --- /dev/null +++ b/bindings/python/src/generated/m_resonance.rs @@ -0,0 +1,22 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + Ok(()) +} diff --git a/bindings/python/src/generated/m_resonance__cavity.rs b/bindings/python/src/generated/m_resonance__cavity.rs new file mode 100644 index 0000000..f2cecb0 --- /dev/null +++ b/bindings/python/src/generated/m_resonance__cavity.rs @@ -0,0 +1,435 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Helmholtz resonance frequency (Hz) with a flanged-end correction of +/// 1.7·r added to the neck length. +/// +/// Rust: `resonance::cavity::helmholtz_resonator` +#[pyfunction] +#[pyo3(name = "helmholtz_resonator", signature = (volume, neck_area, neck_length, c))] +pub fn pyfn_helmholtz_resonator(volume: f64, neck_area: f64, neck_length: f64, c: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::cavity::helmholtz_resonator(volume, neck_area, neck_length, c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Radiation-limited quality factor of a Helmholtz resonator (flanged +/// baffle radiation resistance): Q = 2π·√(V·L_eff³/A³). +/// +/// Rust: `resonance::cavity::helmholtz_q` +#[pyfunction] +#[pyo3(name = "helmholtz_q", signature = (volume, neck_area, neck_length, c))] +pub fn pyfn_helmholtz_q(volume: f64, neck_area: f64, neck_length: f64, c: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::cavity::helmholtz_q(volume, neck_area, neck_length, c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Ideal string mode frequencies i·√(T/μ)/(2L), i = 1..=n. +/// +/// Rust: `resonance::cavity::string_modes` +#[pyfunction] +#[pyo3(name = "string_modes", signature = (length, tension, mu, n))] +pub fn pyfn_string_modes<'py>(py: Python<'py>, length: f64, tension: f64, mu: f64, n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::resonance::cavity::string_modes(length, tension, mu, n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Mode shape sin(nπx/L). +/// +/// Rust: `resonance::cavity::string_mode_shape` +#[pyfunction] +#[pyo3(name = "string_mode_shape", signature = (length, n, x))] +pub fn pyfn_string_mode_shape(length: f64, n: usize, x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::cavity::string_mode_shape(length, n, x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Stiff-string partials fₙ = n·f₁·√(1 + B·n²) with the piano +/// inharmonicity coefficient B (radius-based). +/// +/// Rust: `resonance::cavity::stiff_string_modes` +#[pyfunction] +#[pyo3(name = "stiff_string_modes", signature = (length, tension, mu, young, radius, n))] +pub fn pyfn_stiff_string_modes<'py>(py: Python<'py>, length: f64, tension: f64, mu: f64, young: f64, radius: f64, n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::resonance::cavity::stiff_string_modes(length, tension, mu, young, radius, n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Piano-string inharmonicity B = π³·E·r⁴/(4·T·L²). +/// +/// Rust: `resonance::cavity::inharmonicity_coefficient` +#[pyfunction] +#[pyo3(name = "inharmonicity_coefficient", signature = (young, radius, tension, length))] +pub fn pyfn_inharmonicity_coefficient(young: f64, radius: f64, tension: f64, length: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::cavity::inharmonicity_coefficient(young, radius, tension, length)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Air-column modes: open-open i·c/(2L); open-closed odd harmonics +/// (2i−1)·c/(4L). Pass the end-corrected length. +/// +/// Rust: `resonance::cavity::tube_modes` +#[pyfunction] +#[pyo3(name = "tube_modes", signature = (length, c, open_open, n))] +pub fn pyfn_tube_modes<'py>(py: Python<'py>, length: f64, c: f64, open_open: bool, n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::resonance::cavity::tube_modes(length, c, open_open, n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// End correction of an open tube end: 0.85·r flanged, 0.61·r unflanged. +/// +/// Rust: `resonance::cavity::tube_end_correction` +#[pyfunction] +#[pyo3(name = "tube_end_correction", signature = (radius, flanged))] +pub fn pyfn_tube_end_correction(radius: f64, flanged: bool) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::cavity::tube_end_correction(radius, flanged)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Complete-cone modes: like an open-open pipe, i·c/(2L). +/// +/// Rust: `resonance::cavity::conical_tube_modes` +#[pyfunction] +#[pyo3(name = "conical_tube_modes", signature = (length, c, n))] +pub fn pyfn_conical_tube_modes<'py>(py: Python<'py>, length: f64, c: f64, n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::resonance::cavity::conical_tube_modes(length, c, n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Rectangular membrane modes (m, n, f) sorted by frequency: +/// f = (c/2)·√((m/a)² + (n/b)²), c = √(T/σ). +/// +/// Rust: `resonance::cavity::rectangular_membrane_modes` +#[pyfunction] +#[pyo3(name = "rectangular_membrane_modes", signature = (a, b, tension, sigma, max_m, max_n))] +pub fn pyfn_rectangular_membrane_modes<'py>(py: Python<'py>, a: f64, b: f64, tension: f64, sigma: f64, max_m: usize, max_n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::resonance::cavity::rectangular_membrane_modes(a, b, tension, sigma, max_m, max_n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, __x.2)).collect::>()) +} + +/// Circular membrane modes (m angular, n radial, f) sorted by +/// frequency: f = α_mn·c/(2πR) with α_mn the n-th zero of J_m. +/// +/// Rust: `resonance::cavity::circular_membrane_modes` +#[pyfunction] +#[pyo3(name = "circular_membrane_modes", signature = (radius, tension, sigma, max_m, max_n))] +pub fn pyfn_circular_membrane_modes<'py>(py: Python<'py>, radius: f64, tension: f64, sigma: f64, max_m: usize, max_n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::resonance::cavity::circular_membrane_modes(radius, tension, sigma, max_m, max_n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, __x.2)).collect::>()) +} + +/// Circular membrane mode shape J_m(α_mn·r/R)·cos(mθ). +/// +/// Rust: `resonance::cavity::circular_membrane_shape` +#[pyfunction] +#[pyo3(name = "circular_membrane_shape", signature = (radius, m, n, r, theta))] +pub fn pyfn_circular_membrane_shape(radius: f64, m: usize, n: usize, r: f64, theta: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::cavity::circular_membrane_shape(radius, m, n, r, theta)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Thin rectangular plate modes (m, n, f Hz) sorted ascending. +/// Simply supported edges are exact; clamped edges use the separable +/// beam-function Rayleigh estimate (upper bound, a few % high). +/// +/// Rust: `resonance::cavity::rectangular_plate_modes` +#[pyfunction] +#[pyo3(name = "rectangular_plate_modes", signature = (a, b, h, young, nu, rho, bc, max_m, max_n))] +pub fn pyfn_rectangular_plate_modes<'py>(py: Python<'py>, a: f64, b: f64, h: f64, young: f64, nu: f64, rho: f64, bc: crate::generated::types::PyPlateBc, max_m: usize, max_n: usize) -> PyResult> { + let bc = bc.to_rust(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::resonance::cavity::rectangular_plate_modes(a, b, h, young, nu, rho, bc, max_m, max_n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, __x.2)).collect::>()) +} + +/// Chladni figure of a square-symmetric plate mode: the field +/// φ_mn + φ_nm with φ_mn = cos(mπx/a)cos(nπy/b) on a res×res grid +/// (nodal lines are the zero set). +/// +/// Rust: `resonance::cavity::chladni_pattern` +#[pyfunction] +#[pyo3(name = "chladni_pattern", signature = (a, b, m, n, res))] +pub fn pyfn_chladni_pattern(a: f64, b: f64, m: usize, n: usize, res: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::cavity::chladni_pattern(a, b, m, n, res)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyFieldsScalarField2 { inner: __v }) +} + +/// General superposition Σ cᵢ·cos(mᵢπx/a)cos(nᵢπy/b). +/// +/// Rust: `resonance::cavity::chladni_pattern_mixed` +#[pyfunction] +#[pyo3(name = "chladni_pattern_mixed", signature = (a, b, modes, res))] +pub fn pyfn_chladni_pattern_mixed(a: f64, b: f64, modes: Vec<(usize, usize, f64)>, res: usize) -> PyResult { + let modes = modes.into_iter().map(|__e| (__e.0, __e.1, __e.2)).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::cavity::chladni_pattern_mixed(a, b, &modes, res)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyFieldsScalarField2 { inner: __v }) +} + +/// Euler-Bernoulli beam natural frequencies (Hz): +/// f_i = λ_i²/(2πL²)·√(EI/(ρA)). +/// +/// Rust: `resonance::cavity::beam_modes` +#[pyfunction] +#[pyo3(name = "beam_modes", signature = (length, young, i_area, rho, area, bc, n))] +pub fn pyfn_beam_modes<'py>(py: Python<'py>, length: f64, young: f64, i_area: f64, rho: f64, area: f64, bc: crate::generated::types::PyBeamBc, n: usize) -> PyResult> { + let bc = bc.to_rust(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::resonance::cavity::beam_modes(length, young, i_area, rho, area, bc, n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Euler-Bernoulli beam mode shape at position x ∈ \[0, L\] +/// (unnormalized; standard clamped/simply-supported/free functions). +/// +/// Rust: `resonance::cavity::beam_mode_shape` +#[pyfunction] +#[pyo3(name = "beam_mode_shape", signature = (length, bc, n, x))] +pub fn pyfn_beam_mode_shape(length: f64, bc: crate::generated::types::PyBeamBc, n: usize, x: f64) -> PyResult { + let bc = bc.to_rust(); + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::cavity::beam_mode_shape(length, bc, n, x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Tuning fork prong frequency: cantilever first mode of a rectangular +/// prong, f = (1.875²/2π)·(t/L²)·√(E/(12ρ)). +/// +/// Rust: `resonance::cavity::tuning_fork_frequency` +#[pyfunction] +#[pyo3(name = "tuning_fork_frequency", signature = (length, thickness, young, rho))] +pub fn pyfn_tuning_fork_frequency(length: f64, thickness: f64, young: f64, rho: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::cavity::tuning_fork_frequency(length, thickness, young, rho)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Bell/ring flexural modes (thin-ring approximation), n = 2..: +/// f_n = n(n²−1)/√(n²+1) · (t/(2πR²))·√(E/(12ρ(1−ν²))). +/// +/// Rust: `resonance::cavity::bell_modes_approx` +#[pyfunction] +#[pyo3(name = "bell_modes_approx", signature = (radius, thickness, young, rho, nu, n))] +pub fn pyfn_bell_modes_approx<'py>(py: Python<'py>, radius: f64, thickness: f64, young: f64, rho: f64, nu: f64, n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::resonance::cavity::bell_modes_approx(radius, thickness, young, rho, nu, n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// All room modes (nx, ny, nz, f Hz) with indices up to max_n, sorted: +/// f = (c/2)·√((nx/lx)² + (ny/ly)² + (nz/lz)²). +/// +/// Rust: `resonance::cavity::room_modes` +#[pyfunction] +#[pyo3(name = "room_modes", signature = (lx, ly, lz, c, max_n))] +pub fn pyfn_room_modes<'py>(py: Python<'py>, lx: f64, ly: f64, lz: f64, c: f64, max_n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::resonance::cavity::room_modes(lx, ly, lz, c, max_n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, __x.2, __x.3)).collect::>()) +} + +/// Asymptotic modal density dN/df = 4πVf²/c³ + πSf/(2c²) + L/(8c). +/// +/// Rust: `resonance::cavity::room_mode_density` +#[pyfunction] +#[pyo3(name = "room_mode_density", signature = (lx, ly, lz, c, f))] +pub fn pyfn_room_mode_density(lx: f64, ly: f64, lz: f64, c: f64, f: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::cavity::room_mode_density(lx, ly, lz, c, f)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Schroeder crossover frequency 2000·√(RT60/V) (Hz). +/// +/// Rust: `resonance::cavity::schroeder_frequency` +#[pyfunction] +#[pyo3(name = "schroeder_frequency", signature = (rt60, volume))] +pub fn pyfn_schroeder_frequency(rt60: f64, volume: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::cavity::schroeder_frequency(rt60, volume)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Fabry-Perot (Airy) intensity transmission for mirror reflectance r: +/// T = (1−r)²/((1−r)² + 4r·sin²(δ/2)), δ = 4πnL/λ. +/// +/// Rust: `resonance::cavity::fabry_perot_transmission` +#[pyfunction] +#[pyo3(name = "fabry_perot_transmission", signature = (wavelength, length, r, n_index))] +pub fn pyfn_fabry_perot_transmission(wavelength: f64, length: f64, r: f64, n_index: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::cavity::fabry_perot_transmission(wavelength, length, r, n_index)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Free spectral range c/(2nL) (Hz). +/// +/// Rust: `resonance::cavity::fabry_perot_fsr` +#[pyfunction] +#[pyo3(name = "fabry_perot_fsr", signature = (length, n_index, c))] +pub fn pyfn_fabry_perot_fsr(length: f64, n_index: f64, c: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::cavity::fabry_perot_fsr(length, n_index, c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Finesse π√r/(1−r). +/// +/// Rust: `resonance::cavity::fabry_perot_finesse` +#[pyfunction] +#[pyo3(name = "fabry_perot_finesse", signature = (r))] +pub fn pyfn_fabry_perot_finesse(r: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::cavity::fabry_perot_finesse(r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Quality factor f/Δf. +/// +/// Rust: `resonance::cavity::cavity_q` +#[pyfunction] +#[pyo3(name = "cavity_q", signature = (frequency, fwhm))] +pub fn pyfn_cavity_q(frequency: f64, fwhm: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::cavity::cavity_q(frequency, fwhm)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Photon lifetime Q/(2πf) (s). +/// +/// Rust: `resonance::cavity::cavity_photon_lifetime` +#[pyfunction] +#[pyo3(name = "cavity_photon_lifetime", signature = (q, frequency))] +pub fn pyfn_cavity_photon_lifetime(q: f64, frequency: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::cavity::cavity_photon_lifetime(q, frequency)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Rectangular microwave cavity modes (label, f Hz) up to index max_n, +/// sorted: f = (c/2)·√((m/a)² + (n/b)² + (p/d)²) with the standard TE +/// (p ≥ 1, m+n ≥ 1) and TM (m, n ≥ 1, p ≥ 0) index rules. +/// +/// Rust: `resonance::cavity::microwave_cavity_modes_rect` +#[pyfunction] +#[pyo3(name = "microwave_cavity_modes_rect", signature = (a, b, d, c, max_n))] +pub fn pyfn_microwave_cavity_modes_rect<'py>(py: Python<'py>, a: f64, b: f64, d: f64, c: f64, max_n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::resonance::cavity::microwave_cavity_modes_rect(a, b, d, c, max_n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0.to_string(), __x.1)).collect::>()) +} + +/// Cylindrical cavity modes (label, f Hz), sorted: +/// TM_mnp uses J_m zeros (p ≥ 0), TE_mnp uses J′_m zeros (p ≥ 1); +/// f = (c/2π)·√((x/R)² + (pπ/H)²). +/// +/// Rust: `resonance::cavity::cylindrical_cavity_modes` +#[pyfunction] +#[pyo3(name = "cylindrical_cavity_modes", signature = (radius, height, c, max_n))] +pub fn pyfn_cylindrical_cavity_modes<'py>(py: Python<'py>, radius: f64, height: f64, c: f64, max_n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::resonance::cavity::cylindrical_cavity_modes(radius, height, c, max_n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0.to_string(), __x.1)).collect::>()) +} + +/// Quarter-wave resonator fundamental c/(4L). +/// +/// Rust: `resonance::cavity::quarter_wave_resonator` +#[pyfunction] +#[pyo3(name = "quarter_wave_resonator", signature = (length, c))] +pub fn pyfn_quarter_wave_resonator(length: f64, c: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::cavity::quarter_wave_resonator(length, c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Mode splitting of two identical coupled cavities with coupling +/// coefficient κ: (f₀√(1−κ), f₀√(1+κ)). +/// +/// Rust: `resonance::cavity::coupled_cavity_splitting` +#[pyfunction] +#[pyo3(name = "coupled_cavity_splitting", signature = (f0, coupling))] +pub fn pyfn_coupled_cavity_splitting(f0: f64, coupling: f64) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::cavity::coupled_cavity_splitting(f0, coupling)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Lorentzian overlap of two resonances (1 when co-tuned, → 0 when far +/// apart relative to their combined half-widths). +/// +/// Rust: `resonance::cavity::resonance_overlap` +#[pyfunction] +#[pyo3(name = "resonance_overlap", signature = (f1, q1, f2, q2))] +pub fn pyfn_resonance_overlap(f1: f64, q1: f64, f2: f64, q2: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::cavity::resonance_overlap(f1, q1, f2, q2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_helmholtz_resonator, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_helmholtz_q, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_string_modes, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_string_mode_shape, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_stiff_string_modes, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_inharmonicity_coefficient, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tube_modes, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tube_end_correction, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_conical_tube_modes, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rectangular_membrane_modes, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_circular_membrane_modes, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_circular_membrane_shape, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rectangular_plate_modes, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_chladni_pattern, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_chladni_pattern_mixed, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_beam_modes, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_beam_mode_shape, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tuning_fork_frequency, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bell_modes_approx, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_room_modes, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_room_mode_density, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_schroeder_frequency, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fabry_perot_transmission, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fabry_perot_fsr, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fabry_perot_finesse, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cavity_q, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cavity_photon_lifetime, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_microwave_cavity_modes_rect, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cylindrical_cavity_modes, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_quarter_wave_resonator, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_coupled_cavity_splitting, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_resonance_overlap, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_resonance__coupled.rs b/bindings/python/src/generated/m_resonance__coupled.rs new file mode 100644 index 0000000..d1e597c --- /dev/null +++ b/bindings/python/src/generated/m_resonance__coupled.rs @@ -0,0 +1,107 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Two pendulums (length l, mass m) coupled by a spring k: 2-dof +/// small-angle system in the displacement coordinates. +/// +/// Rust: `resonance::coupled::two_pendulums_coupled` +#[pyfunction] +#[pyo3(name = "two_pendulums_coupled", signature = (l, g, k, m))] +pub fn pyfn_two_pendulums_coupled(l: f64, g: f64, k: f64, m: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::coupled::two_pendulums_coupled(l, g, k, m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCoupledOscillators { inner: __v }) +} + +/// Wilberforce pendulum: vertical bounce (mass m, spring k) coupled to +/// torsion (inertia i, stiffness kappa) through the cross term eps. +/// +/// Rust: `resonance::coupled::wilberforce_pendulum` +#[pyfunction] +#[pyo3(name = "wilberforce_pendulum", signature = (m, k, i, kappa, eps))] +pub fn pyfn_wilberforce_pendulum(m: f64, k: f64, i: f64, kappa: f64, eps: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::coupled::wilberforce_pendulum(m, k, i, kappa, eps)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCoupledOscillators { inner: __v }) +} + +/// Two weakly coupled phase oscillators (Huygens' clocks abstraction): +/// φ̇₁ = ω₁ + κ·sin(φ₂−φ₁), φ̇₂ = ω₂ + κ·sin(φ₁−φ₂). Returns +/// (t, wrapped phase difference) per step. +/// +/// Rust: `resonance::coupled::huygens_sync_simulate` +#[pyfunction] +#[pyo3(name = "huygens_sync_simulate", signature = (omega1, omega2, coupling, t_end, dt))] +pub fn pyfn_huygens_sync_simulate<'py>(py: Python<'py>, omega1: f64, omega2: f64, coupling: f64, t_end: f64, dt: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::resonance::coupled::huygens_sync_simulate(omega1, omega2, coupling, t_end, dt))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Kuramoto model of n phase oscillators with global coupling K: +/// returns the phase history (per step) and the order parameter r(t). +/// +/// Panics: +/// Panics unless `omegas` and `theta0` both have length n. +/// +/// Rust: `resonance::coupled::kuramoto` +#[pyfunction] +#[pyo3(name = "kuramoto", signature = (n, k, omegas, theta0, t_end, dt))] +pub fn pyfn_kuramoto<'py>(py: Python<'py>, n: usize, k: f64, omegas: Vec, theta0: Vec, t_end: f64, dt: f64) -> PyResult<(Vec>, Vec)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::resonance::coupled::kuramoto(n, k, &omegas, &theta0, t_end, dt))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Critical coupling estimate K_c = 2/(π·g(0)) with the frequency +/// density at the center estimated by a Gaussian kernel (Silverman +/// bandwidth) around the mean frequency. +/// +/// Rust: `resonance::coupled::kuramoto_critical_coupling` +#[pyfunction] +#[pyo3(name = "kuramoto_critical_coupling", signature = (omegas))] +pub fn pyfn_kuramoto_critical_coupling<'py>(py: Python<'py>, omegas: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::resonance::coupled::kuramoto_critical_coupling(&omegas))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Den Hartog tuned-mass-damper design for an undamped primary +/// (m_primary, k_primary) and absorber mass ratio μ: returns the +/// absorber stiffness, damping, and optimal tuning ratio f = 1/(1+μ). +/// +/// Rust: `resonance::coupled::tuned_mass_damper_design` +#[pyfunction] +#[pyo3(name = "tuned_mass_damper_design", signature = (m_primary, k_primary, mass_ratio))] +pub fn pyfn_tuned_mass_damper_design(m_primary: f64, k_primary: f64, mass_ratio: f64) -> PyResult<(f64, f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::coupled::tuned_mass_damper_design(m_primary, k_primary, mass_ratio)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_two_pendulums_coupled, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wilberforce_pendulum, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_huygens_sync_simulate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kuramoto, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kuramoto_critical_coupling, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tuned_mass_damper_design, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_resonance__nonlinear.rs b/bindings/python/src/generated/m_resonance__nonlinear.rs new file mode 100644 index 0000000..81bbf4c --- /dev/null +++ b/bindings/python/src/generated/m_resonance__nonlinear.rs @@ -0,0 +1,336 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Steady single-harmonic response amplitudes of the Duffing oscillator +/// at drive frequency ω (harmonic balance): up to three coexisting +/// branches, ascending. +/// +/// Rust: `resonance::nonlinear::duffing_response_amplitude` +#[pyfunction] +#[pyo3(name = "duffing_response_amplitude", signature = (alpha, beta, delta, gamma, omega))] +pub fn pyfn_duffing_response_amplitude<'py>(py: Python<'py>, alpha: f64, beta: f64, delta: f64, gamma: f64, omega: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::resonance::nonlinear::duffing_response_amplitude(alpha, beta, delta, gamma, omega))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Backbone curve: free-vibration frequency at amplitude a, +/// ω = √(α + ¾βa²). +/// +/// Rust: `resonance::nonlinear::duffing_backbone` +#[pyfunction] +#[pyo3(name = "duffing_backbone", signature = (alpha, beta, amplitude))] +pub fn pyfn_duffing_backbone(alpha: f64, beta: f64, amplitude: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::nonlinear::duffing_backbone(alpha, beta, amplitude)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Jump (saddle-node) frequencies of the forced Duffing sweep: the ω +/// interval where three branches coexist, found by scanning the +/// harmonic-balance solution count. None when no bistability exists. +/// +/// Rust: `resonance::nonlinear::duffing_jump_frequencies` +#[pyfunction] +#[pyo3(name = "duffing_jump_frequencies", signature = (alpha, beta, delta, gamma))] +pub fn pyfn_duffing_jump_frequencies(alpha: f64, beta: f64, delta: f64, gamma: f64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::nonlinear::duffing_jump_frequencies(alpha, beta, delta, gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| (__x.0, __x.1))) +} + +/// RK4 trajectory (t, x, v) of the forced Duffing oscillator. +/// +/// Rust: `resonance::nonlinear::duffing_simulate` +#[pyfunction] +#[pyo3(name = "duffing_simulate", signature = (alpha, beta, delta, gamma, omega, x0, v0, t_end, dt))] +pub fn pyfn_duffing_simulate<'py>(py: Python<'py>, alpha: f64, beta: f64, delta: f64, gamma: f64, omega: f64, x0: f64, v0: f64, t_end: f64, dt: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::resonance::nonlinear::duffing_simulate(alpha, beta, delta, gamma, omega, x0, v0, t_end, dt))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, __x.2)).collect::>()) +} + +/// Poincaré section of the Duffing oscillator: (x, v) sampled once per +/// forcing period, after discarding 100 transient periods. +/// +/// Rust: `resonance::nonlinear::duffing_poincare` +#[pyfunction] +#[pyo3(name = "duffing_poincare", signature = (alpha, beta, delta, gamma, omega, x0, v0, n_points))] +pub fn pyfn_duffing_poincare<'py>(py: Python<'py>, alpha: f64, beta: f64, delta: f64, gamma: f64, omega: f64, x0: f64, v0: f64, n_points: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::resonance::nonlinear::duffing_poincare(alpha, beta, delta, gamma, omega, x0, v0, n_points))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Mathieu equation stability of x″ + (a − 2q·cos 2t)x = 0 by the +/// Floquet criterion |tr M| ≤ 2. +/// +/// Rust: `resonance::nonlinear::mathieu_stability` +#[pyfunction] +#[pyo3(name = "mathieu_stability", signature = (a, q))] +pub fn pyfn_mathieu_stability(a: f64, q: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::nonlinear::mathieu_stability(a, q)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Stability chart over a (rows) × q (columns) grids of n points each. +/// +/// Rust: `resonance::nonlinear::mathieu_stability_chart` +#[pyfunction] +#[pyo3(name = "mathieu_stability_chart", signature = (a_range, q_range, n))] +pub fn pyfn_mathieu_stability_chart<'py>(py: Python<'py>, a_range: (f64, f64), q_range: (f64, f64), n: usize) -> PyResult>> { + let a_range = (a_range.0, a_range.1); + let q_range = (q_range.0, q_range.1); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::resonance::nonlinear::mathieu_stability_chart(a_range, q_range, n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Pump-amplitude threshold h_c of the n-th parametric instability +/// tongue for x″ + 2λx′ + ω₀²(1 + h·cos(Ωt))x = 0 with Ω = 2ω₀/n, +/// found by bisecting the damped Floquet spectral radius. +/// +/// Panics: +/// Panics unless n ≥ 1 and the parameters are positive. +/// +/// Rust: `resonance::nonlinear::parametric_resonance_threshold` +#[pyfunction] +#[pyo3(name = "parametric_resonance_threshold", signature = (omega0, damping, n))] +pub fn pyfn_parametric_resonance_threshold(omega0: f64, damping: f64, n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::nonlinear::parametric_resonance_threshold(omega0, damping, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Kapitza inverted pendulum stability: a²ω² > 2·g·l. +/// +/// Rust: `resonance::nonlinear::kapitza_pendulum_stable` +#[pyfunction] +#[pyo3(name = "kapitza_pendulum_stable", signature = (l, g, a, omega))] +pub fn pyfn_kapitza_pendulum_stable(l: f64, g: f64, a: f64, omega: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::nonlinear::kapitza_pendulum_stable(l, g, a, omega)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// RK4 trajectory (t, x, v) of the van der Pol oscillator +/// x″ − μ(1 − x²)x′ + ω²x = 0. +/// +/// Rust: `resonance::nonlinear::van_der_pol_simulate` +#[pyfunction] +#[pyo3(name = "van_der_pol_simulate", signature = (mu, omega, x0, v0, t_end, dt))] +pub fn pyfn_van_der_pol_simulate<'py>(py: Python<'py>, mu: f64, omega: f64, x0: f64, v0: f64, t_end: f64, dt: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::resonance::nonlinear::van_der_pol_simulate(mu, omega, x0, v0, t_end, dt))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, __x.2)).collect::>()) +} + +/// Limit-cycle amplitude of the van der Pol oscillator (numerically +/// settled; → 2 as μ → 0). +/// +/// Rust: `resonance::nonlinear::van_der_pol_limit_cycle_amplitude` +#[pyfunction] +#[pyo3(name = "van_der_pol_limit_cycle_amplitude", signature = (mu))] +pub fn pyfn_van_der_pol_limit_cycle_amplitude(mu: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::nonlinear::van_der_pol_limit_cycle_amplitude(mu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Adler entrainment (lock-in) band of a weakly forced van der Pol +/// oscillator with unit natural frequency: ω ∈ 1 ± F/4 for weak +/// forcing F on the a = 2 limit cycle. +/// +/// Rust: `resonance::nonlinear::van_der_pol_entrainment_range` +#[pyfunction] +#[pyo3(name = "van_der_pol_entrainment_range", signature = (mu, forcing_amp))] +pub fn pyfn_van_der_pol_entrainment_range(mu: f64, forcing_amp: f64) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::nonlinear::van_der_pol_entrainment_range(mu, forcing_amp)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Fano lineshape (q + ε)²/(1 + ε²), ε = 2(ω − ω₀)/γ (background → 1). +/// +/// Rust: `resonance::nonlinear::fano_lineshape` +#[pyfunction] +#[pyo3(name = "fano_lineshape", signature = (omega, omega0, gamma, q))] +pub fn pyfn_fano_lineshape(omega: f64, omega0: f64, gamma: f64, q: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::nonlinear::fano_lineshape(omega, omega0, gamma, q)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Fit A·(q+ε)²/(1+ε²) to data; returns (ω₀, γ, q, A). +/// +/// Panics: +/// Panics if the fit fails or fewer than 5 points are supplied. +/// +/// Rust: `resonance::nonlinear::fano_fit` +#[pyfunction] +#[pyo3(name = "fano_fit", signature = (omega, y))] +pub fn pyfn_fano_fit<'py>(py: Python<'py>, omega: Vec, y: Vec) -> PyResult<(f64, f64, f64, f64)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::resonance::nonlinear::fano_fit(&omega, &y))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2, __v.3)) +} + +/// Autoresonance capture threshold for a swept-drive Duffing-type +/// oscillator: ε_c = 0.41·(dω/dt)^(3/4)/√|α_nl| (Fajans-Friedland +/// scaling law). +/// +/// Rust: `resonance::nonlinear::autoresonance_threshold` +#[pyfunction] +#[pyo3(name = "autoresonance_threshold", signature = (alpha, sweep_rate))] +pub fn pyfn_autoresonance_threshold(alpha: f64, sweep_rate: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::nonlinear::autoresonance_threshold(alpha, sweep_rate)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Resonator frequency pulling by a detuned load: +/// f = f₀·(1 + Δ/(2Q)). +/// +/// Rust: `resonance::nonlinear::frequency_pulling` +#[pyfunction] +#[pyo3(name = "frequency_pulling", signature = (f0, q, coupling_detuning))] +pub fn pyfn_frequency_pulling(f0: f64, q: f64, coupling_detuning: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::nonlinear::frequency_pulling(f0, q, coupling_detuning)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Adler injection-locking half-range Δf = f₀·ρ/(2Q) for injection +/// amplitude ratio ρ. +/// +/// Rust: `resonance::nonlinear::injection_locking_range` +#[pyfunction] +#[pyo3(name = "injection_locking_range", signature = (f0, q, injection_ratio))] +pub fn pyfn_injection_locking_range(f0: f64, q: f64, injection_ratio: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::nonlinear::injection_locking_range(f0, q, injection_ratio)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Weak-signal stochastic-resonance SNR of a bistable well +/// (McNamara-Wiesenfeld form): √2·(a·ΔU/D²)²·... reduced to the +/// standard shape SNR ∝ (a²ΔU²/D²)·e^(−ΔU/D), which is maximized at +/// D = ΔU/2. `omega` enters only beyond the adiabatic limit and is +/// ignored here. +/// +/// Rust: `resonance::nonlinear::stochastic_resonance_snr` +#[pyfunction] +#[pyo3(name = "stochastic_resonance_snr", signature = (a, d, noise, omega))] +pub fn pyfn_stochastic_resonance_snr(a: f64, d: f64, noise: f64, omega: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::nonlinear::stochastic_resonance_snr(a, d, noise, omega)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Enclosed area of a swept-response hysteresis loop (trapezoid of +/// up-sweep minus down-sweep). +/// +/// Panics: +/// Panics on mismatched lengths. +/// +/// Rust: `resonance::nonlinear::hysteresis_loop` +#[pyfunction] +#[pyo3(name = "hysteresis_loop", signature = (f_sweep, response_up, response_down))] +pub fn pyfn_hysteresis_loop<'py>(py: Python<'py>, f_sweep: Vec, response_up: Vec, response_down: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::resonance::nonlinear::hysteresis_loop(&f_sweep, &response_up, &response_down))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Generic harmonic balance for x″ + f(x, x′, t) = 0 with f +/// 2π/ω-periodic in t: Newton iteration on the truncated Fourier series +/// x(t) = c₀ + Σ_k \[a_k cos kωt + b_k sin kωt\], collocated at +/// 4·n_harmonics + 2 points. Returns coefficients c_k = a_k − j·b_k +/// (c₀ real) for k = 0..=n_harmonics. +/// +/// Panics: +/// Panics if the Newton solve fails to converge. +/// +/// Rust: `resonance::nonlinear::harmonic_balance` +#[pyfunction] +#[pyo3(name = "harmonic_balance", signature = (f, omega, n_harmonics, amplitude_guess))] +pub fn pyfn_harmonic_balance<'py>(py: Python<'py>, f: pyo3::Py, omega: f64, n_harmonics: usize, amplitude_guess: f64) -> PyResult>> { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64, __a1: f64, __a2: f64| -> f64 { __cb.call::<_, f64>((__a0, __a1, __a2), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::nonlinear::harmonic_balance(&f, omega, n_harmonics, amplitude_guess)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// Sinusoidal-input describing function of a static nonlinearity: +/// N(A) = (b₁ + j·a₁)/A from the first Fourier component of +/// f(A·sin θ). +/// +/// Rust: `resonance::nonlinear::describing_function` +#[pyfunction] +#[pyo3(name = "describing_function", signature = (nonlinearity, amplitude))] +pub fn pyfn_describing_function<'py>(py: Python<'py>, nonlinearity: pyo3::Py, amplitude: f64) -> PyResult> { + let __cb_nonlinearity = std::rc::Rc::new(crate::runtime::Callback::new(nonlinearity)); + let nonlinearity = { let __cb = __cb_nonlinearity.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::nonlinear::describing_function(&nonlinearity, amplitude)); + crate::runtime::callback::check(&[&__cb_nonlinearity], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::complex_out(py, __v)) +} + +/// Steady-state spectral amplitudes of the driven Duffing response at +/// \[ω/3, ω/2, ω, 2ω, 3ω\] — nonzero sub/superharmonic content flags +/// period-multiplied responses. +/// +/// Rust: `resonance::nonlinear::subharmonic_response` +#[pyfunction] +#[pyo3(name = "subharmonic_response", signature = (alpha, beta, delta, gamma, omega))] +pub fn pyfn_subharmonic_response<'py>(py: Python<'py>, alpha: f64, beta: f64, delta: f64, gamma: f64, omega: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::resonance::nonlinear::subharmonic_response(alpha, beta, delta, gamma, omega))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_duffing_response_amplitude, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_duffing_backbone, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_duffing_jump_frequencies, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_duffing_simulate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_duffing_poincare, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mathieu_stability, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mathieu_stability_chart, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_parametric_resonance_threshold, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kapitza_pendulum_stable, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_van_der_pol_simulate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_van_der_pol_limit_cycle_amplitude, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_van_der_pol_entrainment_range, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fano_lineshape, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fano_fit, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_autoresonance_threshold, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_frequency_pulling, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_injection_locking_range, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_stochastic_resonance_snr, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hysteresis_loop, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_harmonic_balance, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_describing_function, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_subharmonic_response, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_resonance__oscillator.rs b/bindings/python/src/generated/m_resonance__oscillator.rs new file mode 100644 index 0000000..ead15c5 --- /dev/null +++ b/bindings/python/src/generated/m_resonance__oscillator.rs @@ -0,0 +1,156 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Magnitude (dB) and phase (degrees) of a transfer function over a +/// frequency grid. +/// +/// Rust: `resonance::oscillator::bode_plot` +#[pyfunction] +#[pyo3(name = "bode_plot", signature = (tf, omega))] +pub fn pyfn_bode_plot(tf: pyo3::Py, omega: Vec) -> PyResult<(Vec, Vec)> { + let __cb_tf = std::rc::Rc::new(crate::runtime::Callback::new(tf)); + let tf = { let __cb = __cb_tf.clone(); move |__a0: rust_physics_engine::fractals::Complex| -> rust_physics_engine::fractals::Complex { { let __r = __cb.call::<_, crate::runtime::coerce::ComplexArg>((crate::runtime::coerce::Cx(__a0),), crate::runtime::coerce::ComplexArg(rust_physics_engine::fractals::Complex::new(f64::NAN, f64::NAN))); __r.0 } } }; + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::oscillator::bode_plot(&tf, &omega)); + crate::runtime::callback::check(&[&__cb_tf], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Nyquist locus H(jω) over a frequency grid. +/// +/// Rust: `resonance::oscillator::nyquist_plot` +#[pyfunction] +#[pyo3(name = "nyquist_plot", signature = (tf, omega))] +pub fn pyfn_nyquist_plot<'py>(py: Python<'py>, tf: pyo3::Py, omega: Vec) -> PyResult>> { + let __cb_tf = std::rc::Rc::new(crate::runtime::Callback::new(tf)); + let tf = { let __cb = __cb_tf.clone(); move |__a0: rust_physics_engine::fractals::Complex| -> rust_physics_engine::fractals::Complex { { let __r = __cb.call::<_, crate::runtime::coerce::ComplexArg>((crate::runtime::coerce::Cx(__a0),), crate::runtime::coerce::ComplexArg(rust_physics_engine::fractals::Complex::new(f64::NAN, f64::NAN))); __r.0 } } }; + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::oscillator::nyquist_plot(&tf, &omega)); + crate::runtime::callback::check(&[&__cb_tf], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// Area-normalized Lorentzian lineshape +/// (1/π)·(γ/2)/((ω−ω₀)² + (γ/2)²). +/// +/// Rust: `resonance::oscillator::lorentzian` +#[pyfunction] +#[pyo3(name = "lorentzian", signature = (omega, omega0, gamma))] +pub fn pyfn_lorentzian(omega: f64, omega0: f64, gamma: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::oscillator::lorentzian(omega, omega0, gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Fit y(ω) ≈ A·(γ/2)²/((ω−ω₀)² + (γ/2)²) (peak-amplitude Lorentzian) +/// by Levenberg-Marquardt; returns (ω₀, γ, A). +/// +/// Panics: +/// Panics if the fit fails to converge or fewer than 4 points are given. +/// +/// Rust: `resonance::oscillator::lorentzian_fit` +#[pyfunction] +#[pyo3(name = "lorentzian_fit", signature = (omega, y))] +pub fn pyfn_lorentzian_fit<'py>(py: Python<'py>, omega: Vec, y: Vec) -> PyResult<(f64, f64, f64)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::resonance::oscillator::lorentzian_fit(&omega, &y))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// Estimate (f₀ Hz, Q) from a free ring-down record: frequency from +/// interpolated zero crossings, decay rate from a log-linear fit to the +/// rectified peaks. +/// +/// Panics: +/// Panics if the record has fewer than 4 zero crossings. +/// +/// Rust: `resonance::oscillator::q_from_ringdown` +#[pyfunction] +#[pyo3(name = "q_from_ringdown", signature = (x, fs))] +pub fn pyfn_q_from_ringdown<'py>(py: Python<'py>, x: Vec, fs: f64) -> PyResult<(f64, f64)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::resonance::oscillator::q_from_ringdown(&x, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Estimate (f₀, Q) from a power spectrum by the −3 dB method with +/// linear interpolation of the half-power crossings. +/// +/// Panics: +/// Panics on an empty spectrum. +/// +/// Rust: `resonance::oscillator::q_from_spectrum` +#[pyfunction] +#[pyo3(name = "q_from_spectrum", signature = (f, psd))] +pub fn pyfn_q_from_spectrum<'py>(py: Python<'py>, f: Vec, psd: Vec) -> PyResult<(f64, f64)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::resonance::oscillator::q_from_spectrum(&f, &psd))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Steady-state amplitude of the oscillator (unit force) at each ω. +/// +/// Rust: `resonance::oscillator::resonance_curve` +#[pyfunction] +#[pyo3(name = "resonance_curve", signature = (osc, omega))] +pub fn pyfn_resonance_curve<'py>(py: Python<'py>, osc: crate::generated::types::PyDampedOscillatorArg, omega: Vec) -> PyResult> { + let osc = osc.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::resonance::oscillator::resonance_curve(&osc, &omega))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Base-excitation transmissibility at frequency ratio r = ω/ω₀: +/// √((1+(2ζr)²)/((1−r²)² + (2ζr)²)). +/// +/// Rust: `resonance::oscillator::transmissibility` +#[pyfunction] +#[pyo3(name = "transmissibility", signature = (omega_ratio, zeta))] +pub fn pyfn_transmissibility(omega_ratio: f64, zeta: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::oscillator::transmissibility(omega_ratio, zeta)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Combined quality factor of independent loss channels: +/// 1/Q = Σ 1/Qᵢ. +/// +/// Rust: `resonance::oscillator::quality_factor_combined` +#[pyfunction] +#[pyo3(name = "quality_factor_combined", signature = (qs))] +pub fn pyfn_quality_factor_combined<'py>(py: Python<'py>, qs: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::resonance::oscillator::quality_factor_combined(&qs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_bode_plot, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_nyquist_plot, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lorentzian, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lorentzian_fit, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_q_from_ringdown, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_q_from_spectrum, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_resonance_curve, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_transmissibility, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_quality_factor_combined, m)?)?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_resonance__structural.rs b/bindings/python/src/generated/m_resonance__structural.rs new file mode 100644 index 0000000..c02835a --- /dev/null +++ b/bindings/python/src/generated/m_resonance__structural.rs @@ -0,0 +1,113 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Peak-picking experimental modal analysis on a receptance FRF: +/// (natural frequency, damping ratio) per resolved peak via half-power +/// bandwidths. +/// +/// Rust: `resonance::structural::experimental_modal_peak_picking` +#[pyfunction] +#[pyo3(name = "experimental_modal_peak_picking", signature = (frf, freqs))] +pub fn pyfn_experimental_modal_peak_picking<'py>(py: Python<'py>, frf: Vec, freqs: Vec) -> PyResult> { + let frf = frf.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::resonance::structural::experimental_modal_peak_picking(&frf, &freqs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Half-power (−3 dB) bandwidth around the FRF magnitude peak at +/// `peak_idx`, with linear interpolation; 0 when a crossing is missing. +/// +/// Rust: `resonance::structural::half_power_bandwidth` +#[pyfunction] +#[pyo3(name = "half_power_bandwidth", signature = (frf_mag, freqs, peak_idx))] +pub fn pyfn_half_power_bandwidth<'py>(py: Python<'py>, frf_mag: Vec, freqs: Vec, peak_idx: usize) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::resonance::structural::half_power_bandwidth(&frf_mag, &freqs, peak_idx))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Kasa circle fit of an FRF arc in the Nyquist plane around a +/// resonance; returns (f₀, ζ) using the angular-sweep-rate maximum for +/// f₀ and the standard circle-fit damping formula. +/// +/// Panics: +/// Panics if fewer than 5 points are given. +/// +/// Rust: `resonance::structural::circle_fit` +#[pyfunction] +#[pyo3(name = "circle_fit", signature = (frf, freqs, window))] +pub fn pyfn_circle_fit<'py>(py: Python<'py>, frf: Vec, freqs: Vec, window: usize) -> PyResult<(f64, f64)> { + let frf = frf.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::resonance::structural::circle_fit(&frf, &freqs, window))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Operational deflection shape: the complex amplitude of every +/// measured channel at frequency ω (single-bin correlation at fs). +/// +/// Rust: `resonance::structural::operational_deflection_shape` +#[pyfunction] +#[pyo3(name = "operational_deflection_shape", signature = (responses, omega, fs))] +pub fn pyfn_operational_deflection_shape<'py>(py: Python<'py>, responses: Vec>, omega: f64, fs: f64) -> PyResult>> { + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::structural::operational_deflection_shape(&responses, omega, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// Covariance-driven stochastic subspace identification: output-only +/// modal frequencies and damping ratios from response channels sampled +/// at fs. `order` is the state dimension (≥ 2 per expected mode). +/// +/// Panics: +/// Panics if the SVD or eigen machinery fails, or the data is shorter +/// than 4·order. +/// +/// Rust: `resonance::structural::stochastic_subspace_identification` +#[pyfunction] +#[pyo3(name = "stochastic_subspace_identification", signature = (outputs, fs, order))] +pub fn pyfn_stochastic_subspace_identification<'py>(py: Python<'py>, outputs: Vec>, fs: f64, order: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::resonance::structural::stochastic_subspace_identification(&outputs, fs, order))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Shock response spectrum: peak absolute SDOF displacement response at +/// each requested natural frequency (Hz) for a base acceleration pulse. +/// +/// Rust: `resonance::structural::shock_response_spectrum` +#[pyfunction] +#[pyo3(name = "shock_response_spectrum", signature = (accel, dt, freqs, zeta))] +pub fn pyfn_shock_response_spectrum<'py>(py: Python<'py>, accel: Vec, dt: f64, freqs: Vec, zeta: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::resonance::structural::shock_response_spectrum(&accel, dt, &freqs, zeta))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_experimental_modal_peak_picking, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_half_power_bandwidth, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_circle_fit, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_operational_deflection_shape, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_stochastic_subspace_identification, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_shock_response_spectrum, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_rf.rs b/bindings/python/src/generated/m_rf.rs new file mode 100644 index 0000000..d8c93ed --- /dev/null +++ b/bindings/python/src/generated/m_rf.rs @@ -0,0 +1,358 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Wavelength to frequency: f = c / λ +/// +/// Rust: `rf::wavelength_to_frequency` +#[pyfunction] +#[pyo3(name = "wavelength_to_frequency", signature = (wavelength))] +pub fn pyfn_wavelength_to_frequency(wavelength: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::rf::wavelength_to_frequency(wavelength)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Frequency to wavelength: λ = c / f +/// +/// Rust: `rf::frequency_to_wavelength` +#[pyfunction] +#[pyo3(name = "frequency_to_wavelength", signature = (frequency))] +pub fn pyfn_frequency_to_wavelength(frequency: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::rf::frequency_to_wavelength(frequency)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Photon energy from frequency: E = hf +/// +/// Rust: `rf::frequency_to_energy` +#[pyfunction] +#[pyo3(name = "frequency_to_energy", signature = (frequency))] +pub fn pyfn_frequency_to_energy(frequency: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::rf::frequency_to_energy(frequency)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Free-space path loss in dB: FSPL = 20log₁₀(d) + 20log₁₀(f) + 20log₁₀(4π/c) +/// +/// Rust: `rf::free_space_path_loss` +#[pyfunction] +#[pyo3(name = "free_space_path_loss", signature = (distance, frequency))] +pub fn pyfn_free_space_path_loss(distance: f64, frequency: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::rf::free_space_path_loss(distance, frequency)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Friis transmission equation (linear): Pr = Pt × Gt × Gr × (λ/(4πd))² +/// +/// Rust: `rf::friis_received_power` +#[pyfunction] +#[pyo3(name = "friis_received_power", signature = (pt, gt, gr, wavelength, distance))] +pub fn pyfn_friis_received_power(pt: f64, gt: f64, gr: f64, wavelength: f64, distance: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::rf::friis_received_power(pt, gt, gr, wavelength, distance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Link budget in dB: Pr = Pt + Gt + Gr - PathLoss +/// +/// Rust: `rf::link_budget_db` +#[pyfunction] +#[pyo3(name = "link_budget_db", signature = (pt_dbm, gt_dbi, gr_dbi, path_loss_db))] +pub fn pyfn_link_budget_db(pt_dbm: f64, gt_dbi: f64, gr_dbi: f64, path_loss_db: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::rf::link_budget_db(pt_dbm, gt_dbi, gr_dbi, path_loss_db)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Skin depth in a conductor: δ = 1 / √(πfμσ) +/// +/// Rust: `rf::skin_depth_conductor` +#[pyfunction] +#[pyo3(name = "skin_depth_conductor", signature = (frequency, permeability, conductivity))] +pub fn pyfn_skin_depth_conductor(frequency: f64, permeability: f64, conductivity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::rf::skin_depth_conductor(frequency, permeability, conductivity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Fade margin: FM = received_dBm - sensitivity_dBm +/// +/// Rust: `rf::fade_margin_db` +#[pyfunction] +#[pyo3(name = "fade_margin_db", signature = (transmitted_dbm, received_dbm, sensitivity_dbm))] +pub fn pyfn_fade_margin_db(transmitted_dbm: f64, received_dbm: f64, sensitivity_dbm: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::rf::fade_margin_db(transmitted_dbm, received_dbm, sensitivity_dbm)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Antenna gain from effective area: G = 4πA_e / λ² +/// +/// Rust: `rf::antenna_gain_from_area` +#[pyfunction] +#[pyo3(name = "antenna_gain_from_area", signature = (effective_area, wavelength))] +pub fn pyfn_antenna_gain_from_area(effective_area: f64, wavelength: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::rf::antenna_gain_from_area(effective_area, wavelength)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Effective aperture from gain: A_e = Gλ² / (4π) +/// +/// Rust: `rf::effective_area_from_gain` +#[pyfunction] +#[pyo3(name = "effective_area_from_gain", signature = (gain, wavelength))] +pub fn pyfn_effective_area_from_gain(gain: f64, wavelength: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::rf::effective_area_from_gain(gain, wavelength)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Returns the half-wave dipole gain (linear): G ≈ 1.64 (2.15 dBi). +/// +/// Rust: `rf::half_wave_dipole_gain` +#[pyfunction] +#[pyo3(name = "half_wave_dipole_gain", signature = ())] +pub fn pyfn_half_wave_dipole_gain() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::rf::half_wave_dipole_gain()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Effective isotropic radiated power: EIRP = P × G +/// +/// Rust: `rf::eirp` +#[pyfunction] +#[pyo3(name = "eirp", signature = (power, gain))] +pub fn pyfn_eirp(power: f64, gain: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::rf::eirp(power, gain)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Approximate antenna beamwidth in degrees: θ ≈ 70λ / D. +/// +/// Rust: `rf::beamwidth_approximate` +#[pyfunction] +#[pyo3(name = "beamwidth_approximate", signature = (wavelength, aperture))] +pub fn pyfn_beamwidth_approximate(wavelength: f64, aperture: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::rf::beamwidth_approximate(wavelength, aperture)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Antenna directivity from gain and efficiency: D = G / η +/// +/// Rust: `rf::antenna_directivity` +#[pyfunction] +#[pyo3(name = "antenna_directivity", signature = (gain, efficiency))] +pub fn pyfn_antenna_directivity(gain: f64, efficiency: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::rf::antenna_directivity(gain, efficiency)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Characteristic impedance of coaxial cable: Z₀ = (138/√εr) × log₁₀(D/d). +/// +/// Rust: `rf::characteristic_impedance_coax` +#[pyfunction] +#[pyo3(name = "characteristic_impedance_coax", signature = (outer_radius, inner_radius, permittivity_rel))] +pub fn pyfn_characteristic_impedance_coax(outer_radius: f64, inner_radius: f64, permittivity_rel: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::rf::characteristic_impedance_coax(outer_radius, inner_radius, permittivity_rel)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Velocity factor: VF = 1 / √εr +/// +/// Rust: `rf::velocity_factor` +#[pyfunction] +#[pyo3(name = "velocity_factor", signature = (permittivity_rel))] +pub fn pyfn_velocity_factor(permittivity_rel: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::rf::velocity_factor(permittivity_rel)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Wavelength in a transmission line: λ_line = λ₀ × VF +/// +/// Rust: `rf::wavelength_in_line` +#[pyfunction] +#[pyo3(name = "wavelength_in_line", signature = (free_space_wavelength, velocity_factor))] +pub fn pyfn_wavelength_in_line(free_space_wavelength: f64, velocity_factor: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::rf::wavelength_in_line(free_space_wavelength, velocity_factor)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Voltage standing wave ratio: VSWR = (1 + |Γ|) / (1 - |Γ|) +/// +/// Rust: `rf::vswr` +#[pyfunction] +#[pyo3(name = "vswr", signature = (reflection_coeff))] +pub fn pyfn_vswr(reflection_coeff: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::rf::vswr(reflection_coeff)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Return loss in dB: RL = -20log₁₀(|Γ|) +/// +/// Rust: `rf::return_loss` +#[pyfunction] +#[pyo3(name = "return_loss", signature = (reflection_coeff))] +pub fn pyfn_return_loss(reflection_coeff: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::rf::return_loss(reflection_coeff)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Mismatch loss in dB: ML = -10log₁₀(1 - ((VSWR-1)/(VSWR+1))²) +/// +/// Rust: `rf::mismatch_loss` +#[pyfunction] +#[pyo3(name = "mismatch_loss", signature = (vswr))] +pub fn pyfn_mismatch_loss(vswr: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::rf::mismatch_loss(vswr)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert dBm to watts: P = 10^((dBm - 30) / 10) +/// +/// Rust: `rf::dbm_to_watts` +#[pyfunction] +#[pyo3(name = "dbm_to_watts", signature = (dbm))] +pub fn pyfn_dbm_to_watts(dbm: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::rf::dbm_to_watts(dbm)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert watts to dBm: dBm = 10log₁₀(P) + 30 +/// +/// Rust: `rf::watts_to_dbm` +#[pyfunction] +#[pyo3(name = "watts_to_dbm", signature = (watts))] +pub fn pyfn_watts_to_dbm(watts: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::rf::watts_to_dbm(watts)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert dB to linear ratio: ratio = 10^(dB / 10) +/// +/// Rust: `rf::db_to_ratio` +#[pyfunction] +#[pyo3(name = "db_to_ratio", signature = (db))] +pub fn pyfn_db_to_ratio(db: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::rf::db_to_ratio(db)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert linear ratio to dB: dB = 10log₁₀(ratio) +/// +/// Rust: `rf::ratio_to_db` +#[pyfunction] +#[pyo3(name = "ratio_to_db", signature = (ratio))] +pub fn pyfn_ratio_to_db(ratio: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::rf::ratio_to_db(ratio)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Thermal noise power: N = k_B × T × B +/// +/// Rust: `rf::noise_power` +#[pyfunction] +#[pyo3(name = "noise_power", signature = (bandwidth, temperature))] +pub fn pyfn_noise_power(bandwidth: f64, temperature: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::rf::noise_power(bandwidth, temperature)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Signal-to-noise ratio in dB: SNR = 10log₁₀(S / N) +/// +/// Rust: `rf::snr_db` +#[pyfunction] +#[pyo3(name = "snr_db", signature = (signal_power, noise_power))] +pub fn pyfn_snr_db(signal_power: f64, noise_power: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::rf::snr_db(signal_power, noise_power)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Thermal noise floor in dBm: 10log₁₀(k_B × T × B) + 30 +/// +/// Rust: `rf::thermal_noise_floor_dbm` +#[pyfunction] +#[pyo3(name = "thermal_noise_floor_dbm", signature = (bandwidth, temperature))] +pub fn pyfn_thermal_noise_floor_dbm(bandwidth: f64, temperature: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::rf::thermal_noise_floor_dbm(bandwidth, temperature)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Shannon-Hartley channel capacity: C = B × log₂(1 + SNR) +/// +/// Rust: `rf::shannon_capacity` +#[pyfunction] +#[pyo3(name = "shannon_capacity", signature = (bandwidth, snr_linear))] +pub fn pyfn_shannon_capacity(bandwidth: f64, snr_linear: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::rf::shannon_capacity(bandwidth, snr_linear)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_wavelength_to_frequency, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_frequency_to_wavelength, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_frequency_to_energy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_free_space_path_loss, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_friis_received_power, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_link_budget_db, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_skin_depth_conductor, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fade_margin_db, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_antenna_gain_from_area, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_effective_area_from_gain, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_half_wave_dipole_gain, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_eirp, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_beamwidth_approximate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_antenna_directivity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_characteristic_impedance_coax, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_velocity_factor, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wavelength_in_line, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_vswr, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_return_loss, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mismatch_loss, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dbm_to_watts, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_watts_to_dbm, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_db_to_ratio, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ratio_to_db, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_noise_power, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_snr_db, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_thermal_noise_floor_dbm, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_shannon_capacity, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_signal_processing.rs b/bindings/python/src/generated/m_signal_processing.rs new file mode 100644 index 0000000..4d53c48 --- /dev/null +++ b/bindings/python/src/generated/m_signal_processing.rs @@ -0,0 +1,216 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Linear convolution of signal with kernel: `y[n] = Σ s[i]·k[n-i]` +/// +/// Rust: `signal_processing::convolve` +#[pyfunction] +#[pyo3(name = "convolve", signature = (signal, kernel))] +pub fn pyfn_convolve<'py>(py: Python<'py>, signal: Vec, kernel: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::signal_processing::convolve(&signal, &kernel))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Cross-correlation of x and y via convolution with time-reversed y +/// +/// Rust: `signal_processing::cross_correlate` +#[pyfunction] +#[pyo3(name = "cross_correlate", signature = (x, y))] +pub fn pyfn_cross_correlate<'py>(py: Python<'py>, x: Vec, y: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::signal_processing::cross_correlate(&x, &y))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Auto-correlation of a signal: cross-correlation of the signal with itself +/// +/// Rust: `signal_processing::auto_correlate` +#[pyfunction] +#[pyo3(name = "auto_correlate", signature = (signal))] +pub fn pyfn_auto_correlate<'py>(py: Python<'py>, signal: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::signal_processing::auto_correlate(&signal))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Normalize signal amplitude to [-1, 1] by dividing by peak absolute value +/// +/// Rust: `signal_processing::normalize_signal` +#[pyfunction] +#[pyo3(name = "normalize_signal", signature = (signal))] +pub fn pyfn_normalize_signal<'py>(signal: pyo3::Bound<'py, pyo3::PyAny>) -> PyResult<()> { + let mut signal__v: Vec = signal.extract()?; + let __r = crate::runtime::guard(|| rust_physics_engine::signal_processing::normalize_signal(&mut signal__v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + crate::runtime::coerce::write_back(&signal, &signal__v)?; + Ok(()) +} + +/// Element-wise multiplication of signal by window coefficients +/// +/// Rust: `signal_processing::apply_window` +#[pyfunction] +#[pyo3(name = "apply_window", signature = (signal, window))] +pub fn pyfn_apply_window<'py>(py: Python<'py>, signal: Vec, window: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::signal_processing::apply_window(&signal, &window))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Simple moving average filter with specified window size +/// +/// Rust: `signal_processing::moving_average` +#[pyfunction] +#[pyo3(name = "moving_average", signature = (signal, window_size))] +pub fn pyfn_moving_average<'py>(py: Python<'py>, signal: Vec, window_size: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::signal_processing::moving_average(&signal, window_size))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Exponential moving average filter: `y[n] = α·x[n] + (1-α)·y[n-1]` +/// +/// Rust: `signal_processing::exponential_moving_average` +#[pyfunction] +#[pyo3(name = "exponential_moving_average", signature = (signal, alpha))] +pub fn pyfn_exponential_moving_average<'py>(py: Python<'py>, signal: Vec, alpha: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::signal_processing::exponential_moving_average(&signal, alpha))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Median filter for impulse noise removal with specified window size +/// +/// Rust: `signal_processing::median_filter` +#[pyfunction] +#[pyo3(name = "median_filter", signature = (signal, window_size))] +pub fn pyfn_median_filter<'py>(py: Python<'py>, signal: Vec, window_size: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::signal_processing::median_filter(&signal, window_size))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Generate a sine wave: `x[n] = A·sin(2πf·n/fs)` for n samples over given duration +/// +/// Rust: `signal_processing::sine_wave` +#[pyfunction] +#[pyo3(name = "sine_wave", signature = (frequency, sample_rate, duration, amplitude))] +pub fn pyfn_sine_wave<'py>(py: Python<'py>, frequency: f64, sample_rate: f64, duration: f64, amplitude: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::signal_processing::sine_wave(frequency, sample_rate, duration, amplitude))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Generate a square wave: +A for first half-period, -A for second half +/// +/// Rust: `signal_processing::square_wave` +#[pyfunction] +#[pyo3(name = "square_wave", signature = (frequency, sample_rate, duration, amplitude))] +pub fn pyfn_square_wave<'py>(py: Python<'py>, frequency: f64, sample_rate: f64, duration: f64, amplitude: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::signal_processing::square_wave(frequency, sample_rate, duration, amplitude))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Generate a sawtooth wave: linearly ramps from -A to +A each period +/// +/// Rust: `signal_processing::sawtooth_wave` +#[pyfunction] +#[pyo3(name = "sawtooth_wave", signature = (frequency, sample_rate, duration, amplitude))] +pub fn pyfn_sawtooth_wave<'py>(py: Python<'py>, frequency: f64, sample_rate: f64, duration: f64, amplitude: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::signal_processing::sawtooth_wave(frequency, sample_rate, duration, amplitude))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Generate deterministic pseudo-random white noise using a linear congruential generator +/// +/// Rust: `signal_processing::white_noise` +#[pyfunction] +#[pyo3(name = "white_noise", signature = (n, amplitude, seed))] +pub fn pyfn_white_noise<'py>(py: Python<'py>, n: usize, amplitude: f64, seed: u64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::signal_processing::white_noise(n, amplitude, seed))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Count the number of zero crossings (sign changes) in the signal +/// +/// Rust: `signal_processing::zero_crossings` +#[pyfunction] +#[pyo3(name = "zero_crossings", signature = (signal))] +pub fn pyfn_zero_crossings<'py>(py: Python<'py>, signal: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::signal_processing::zero_crossings(&signal))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Root mean square level: RMS = sqrt(Σx²/n) +/// +/// Rust: `signal_processing::rms_level` +#[pyfunction] +#[pyo3(name = "rms_level", signature = (signal))] +pub fn pyfn_rms_level<'py>(py: Python<'py>, signal: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::signal_processing::rms_level(&signal))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Peak-to-peak amplitude: max(x) - min(x) +/// +/// Rust: `signal_processing::peak_to_peak` +#[pyfunction] +#[pyo3(name = "peak_to_peak", signature = (signal))] +pub fn pyfn_peak_to_peak<'py>(py: Python<'py>, signal: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::signal_processing::peak_to_peak(&signal))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Crest factor: ratio of peak absolute value to RMS level +/// +/// Rust: `signal_processing::crest_factor` +#[pyfunction] +#[pyo3(name = "crest_factor", signature = (signal))] +pub fn pyfn_crest_factor<'py>(py: Python<'py>, signal: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::signal_processing::crest_factor(&signal))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_convolve, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cross_correlate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_auto_correlate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_normalize_signal, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_apply_window, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_moving_average, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_exponential_moving_average, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_median_filter, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sine_wave, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_square_wave, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sawtooth_wave, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_white_noise, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_zero_crossings, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rms_level, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_peak_to_peak, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_crest_factor, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_sim.rs b/bindings/python/src/generated/m_sim.rs new file mode 100644 index 0000000..c02bbf8 --- /dev/null +++ b/bindings/python/src/generated/m_sim.rs @@ -0,0 +1,22 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + Ok(()) +} diff --git a/bindings/python/src/generated/m_sim__cloth_sim.rs b/bindings/python/src/generated/m_sim__cloth_sim.rs new file mode 100644 index 0000000..e9de8cc --- /dev/null +++ b/bindings/python/src/generated/m_sim__cloth_sim.rs @@ -0,0 +1,57 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Creates a rectangular cloth grid with structural and shear springs. +/// +/// Particles are laid out in the XY plane. The top row (y = (height-1)*spacing) +/// is pinned. Gravity points in -Y. +/// +/// Structural springs: horizontal and vertical neighbors (rest_length = spacing). +/// Shear springs: diagonal neighbors (rest_length = spacing * sqrt(2)). +/// +/// Rust: `sim::cloth_sim::create_cloth_grid` +#[pyfunction] +#[pyo3(name = "create_cloth_grid", signature = (width, height, spacing, mass, stiffness, damping))] +pub fn pyfn_create_cloth_grid(width: usize, height: usize, spacing: f64, mass: f64, stiffness: f64, damping: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::sim::cloth_sim::create_cloth_grid(width, height, spacing, mass, stiffness, damping)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMassSpringSystem { inner: __v }) +} + +/// Creates a 1D rope (linear chain of particles connected by springs). +/// +/// The first particle is pinned. Gravity points in -Y. +/// +/// Rust: `sim::cloth_sim::create_rope` +#[pyfunction] +#[pyo3(name = "create_rope", signature = (n_particles, spacing, mass, stiffness, damping))] +pub fn pyfn_create_rope(n_particles: usize, spacing: f64, mass: f64, stiffness: f64, damping: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::sim::cloth_sim::create_rope(n_particles, spacing, mass, stiffness, damping)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMassSpringSystem { inner: __v }) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_create_cloth_grid, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_create_rope, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_sim__em_sim.rs b/bindings/python/src/generated/m_sim__em_sim.rs new file mode 100644 index 0000000..aebb9a7 --- /dev/null +++ b/bindings/python/src/generated/m_sim__em_sim.rs @@ -0,0 +1,24 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_sim__fluid_sim.rs b/bindings/python/src/generated/m_sim__fluid_sim.rs new file mode 100644 index 0000000..f96c95e --- /dev/null +++ b/bindings/python/src/generated/m_sim__fluid_sim.rs @@ -0,0 +1,25 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_sim__heat_sim.rs b/bindings/python/src/generated/m_sim__heat_sim.rs new file mode 100644 index 0000000..7e39a17 --- /dev/null +++ b/bindings/python/src/generated/m_sim__heat_sim.rs @@ -0,0 +1,25 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_sim__rigid_body.rs b/bindings/python/src/generated/m_sim__rigid_body.rs new file mode 100644 index 0000000..9777aba --- /dev/null +++ b/bindings/python/src/generated/m_sim__rigid_body.rs @@ -0,0 +1,51 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Detect sphere-sphere overlap, returning (contact_normal, penetration_depth) or None. +/// +/// Rust: `sim::rigid_body::sphere_sphere_collision` +#[pyfunction] +#[pyo3(name = "sphere_sphere_collision", signature = (a, radius_a, b, radius_b))] +pub fn pyfn_sphere_sphere_collision(a: pyo3::PyRef<'_, crate::generated::types::PyRigidBody>, radius_a: f64, b: pyo3::PyRef<'_, crate::generated::types::PyRigidBody>, radius_b: f64) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::sim::rigid_body::sphere_sphere_collision(&a.inner, radius_a, &b.inner, radius_b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| (crate::generated::types::PyVec3 { inner: __x.0 }, __x.1))) +} + +/// Resolve a collision between two rigid bodies using impulse-based response. +/// +/// Rust: `sim::rigid_body::resolve_collision` +#[pyfunction] +#[pyo3(name = "resolve_collision", signature = (a, b, normal, restitution))] +pub fn pyfn_resolve_collision(a: pyo3::PyRefMut<'_, crate::generated::types::PyRigidBody>, b: pyo3::PyRefMut<'_, crate::generated::types::PyRigidBody>, normal: crate::generated::types::PyVec3Arg, restitution: f64) -> PyResult<()> { + let mut a = a; + let mut b = b; + let normal = normal.0; + let __r = crate::runtime::guard(|| rust_physics_engine::sim::rigid_body::resolve_collision(&mut a.inner, &mut b.inner, normal, restitution)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_sphere_sphere_collision, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_resolve_collision, m)?)?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_sim__wave_sim.rs b/bindings/python/src/generated/m_sim__wave_sim.rs new file mode 100644 index 0000000..29075e2 --- /dev/null +++ b/bindings/python/src/generated/m_sim__wave_sim.rs @@ -0,0 +1,24 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_solid_mechanics.rs b/bindings/python/src/generated/m_solid_mechanics.rs new file mode 100644 index 0000000..8835ed6 --- /dev/null +++ b/bindings/python/src/generated/m_solid_mechanics.rs @@ -0,0 +1,298 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Tensile stress: σ = F/A +/// +/// Rust: `solid_mechanics::tensile_stress` +#[pyfunction] +#[pyo3(name = "tensile_stress", signature = (force, area))] +pub fn pyfn_tensile_stress(force: f64, area: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::solid_mechanics::tensile_stress(force, area)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Tensile strain: ε = ΔL/L₀ +/// +/// Rust: `solid_mechanics::tensile_strain` +#[pyfunction] +#[pyo3(name = "tensile_strain", signature = (delta_l, original_l))] +pub fn pyfn_tensile_strain(delta_l: f64, original_l: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::solid_mechanics::tensile_strain(delta_l, original_l)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Shear stress: τ = F/A +/// +/// Rust: `solid_mechanics::shear_stress` +#[pyfunction] +#[pyo3(name = "shear_stress", signature = (force, area))] +pub fn pyfn_shear_stress(force: f64, area: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::solid_mechanics::shear_stress(force, area)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Shear strain: γ = Δx/h +/// +/// Rust: `solid_mechanics::shear_strain` +#[pyfunction] +#[pyo3(name = "shear_strain", signature = (displacement, height))] +pub fn pyfn_shear_strain(displacement: f64, height: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::solid_mechanics::shear_strain(displacement, height)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Volumetric strain: εv = ΔV/V₀ +/// +/// Rust: `solid_mechanics::volumetric_strain` +#[pyfunction] +#[pyo3(name = "volumetric_strain", signature = (delta_v, original_v))] +pub fn pyfn_volumetric_strain(delta_v: f64, original_v: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::solid_mechanics::volumetric_strain(delta_v, original_v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// True stress from engineering values: σ_true = σ_eng(1 + ε_eng) +/// +/// Rust: `solid_mechanics::true_stress` +#[pyfunction] +#[pyo3(name = "true_stress", signature = (engineering_stress, engineering_strain))] +pub fn pyfn_true_stress(engineering_stress: f64, engineering_strain: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::solid_mechanics::true_stress(engineering_stress, engineering_strain)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// True strain from engineering strain: ε_true = ln(1 + ε_eng) +/// +/// Rust: `solid_mechanics::true_strain` +#[pyfunction] +#[pyo3(name = "true_strain", signature = (engineering_strain))] +pub fn pyfn_true_strain(engineering_strain: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::solid_mechanics::true_strain(engineering_strain)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Young's modulus (elastic modulus): E = σ/ε +/// +/// Rust: `solid_mechanics::youngs_modulus` +#[pyfunction] +#[pyo3(name = "youngs_modulus", signature = (stress, strain))] +pub fn pyfn_youngs_modulus(stress: f64, strain: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::solid_mechanics::youngs_modulus(stress, strain)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Shear modulus: G = τ/γ +/// +/// Rust: `solid_mechanics::shear_modulus` +#[pyfunction] +#[pyo3(name = "shear_modulus", signature = (shear_stress, shear_strain))] +pub fn pyfn_shear_modulus(shear_stress: f64, shear_strain: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::solid_mechanics::shear_modulus(shear_stress, shear_strain)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Bulk modulus: K = -P/εv +/// +/// Rust: `solid_mechanics::bulk_modulus` +#[pyfunction] +#[pyo3(name = "bulk_modulus", signature = (pressure, volumetric_strain))] +pub fn pyfn_bulk_modulus(pressure: f64, volumetric_strain: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::solid_mechanics::bulk_modulus(pressure, volumetric_strain)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Poisson's ratio from elastic moduli: ν = E/(2G) - 1 +/// +/// Rust: `solid_mechanics::poisson_ratio_from_moduli` +#[pyfunction] +#[pyo3(name = "poisson_ratio_from_moduli", signature = (e, g))] +pub fn pyfn_poisson_ratio_from_moduli(e: f64, g: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::solid_mechanics::poisson_ratio_from_moduli(e, g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Young's modulus from bulk and shear moduli: E = 9KG/(3K + G) +/// +/// Rust: `solid_mechanics::e_from_k_and_g` +#[pyfunction] +#[pyo3(name = "e_from_k_and_g", signature = (bulk, shear))] +pub fn pyfn_e_from_k_and_g(bulk: f64, shear: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::solid_mechanics::e_from_k_and_g(bulk, shear)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Bulk modulus from Young's modulus and Poisson's ratio: K = E/(3(1 - 2ν)) +/// +/// Rust: `solid_mechanics::bulk_from_e_and_nu` +#[pyfunction] +#[pyo3(name = "bulk_from_e_and_nu", signature = (e, nu))] +pub fn pyfn_bulk_from_e_and_nu(e: f64, nu: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::solid_mechanics::bulk_from_e_and_nu(e, nu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Shear modulus from Young's modulus and Poisson's ratio: G = E/(2(1 + ν)) +/// +/// Rust: `solid_mechanics::shear_from_e_and_nu` +#[pyfunction] +#[pyo3(name = "shear_from_e_and_nu", signature = (e, nu))] +pub fn pyfn_shear_from_e_and_nu(e: f64, nu: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::solid_mechanics::shear_from_e_and_nu(e, nu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Cantilever beam tip deflection under point load: δ = FL³/(3EI) +/// +/// Rust: `solid_mechanics::beam_deflection_cantilever_point` +#[pyfunction] +#[pyo3(name = "beam_deflection_cantilever_point", signature = (force, length, e, i))] +pub fn pyfn_beam_deflection_cantilever_point(force: f64, length: f64, e: f64, i: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::solid_mechanics::beam_deflection_cantilever_point(force, length, e, i)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Simply supported beam center deflection under point load: δ = FL³/(48EI) +/// +/// Rust: `solid_mechanics::beam_deflection_simply_supported_center` +#[pyfunction] +#[pyo3(name = "beam_deflection_simply_supported_center", signature = (force, length, e, i))] +pub fn pyfn_beam_deflection_simply_supported_center(force: f64, length: f64, e: f64, i: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::solid_mechanics::beam_deflection_simply_supported_center(force, length, e, i)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Bending moment: M = F·d +/// +/// Rust: `solid_mechanics::bending_moment` +#[pyfunction] +#[pyo3(name = "bending_moment", signature = (force, distance))] +pub fn pyfn_bending_moment(force: f64, distance: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::solid_mechanics::bending_moment(force, distance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Bending stress at distance y from neutral axis: σ = My/I +/// +/// Rust: `solid_mechanics::bending_stress` +#[pyfunction] +#[pyo3(name = "bending_stress", signature = (moment, y, i))] +pub fn pyfn_bending_stress(moment: f64, y: f64, i: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::solid_mechanics::bending_stress(moment, y, i)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Second moment of area for a rectangle: I = bh³/12 +/// +/// Rust: `solid_mechanics::second_moment_rectangle` +#[pyfunction] +#[pyo3(name = "second_moment_rectangle", signature = (width, height))] +pub fn pyfn_second_moment_rectangle(width: f64, height: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::solid_mechanics::second_moment_rectangle(width, height)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Second moment of area for a circle: I = πr⁴/4 +/// +/// Rust: `solid_mechanics::second_moment_circle` +#[pyfunction] +#[pyo3(name = "second_moment_circle", signature = (radius))] +pub fn pyfn_second_moment_circle(radius: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::solid_mechanics::second_moment_circle(radius)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Von Mises equivalent stress: σ_vm = √(((σ₁-σ₂)² + (σ₂-σ₃)² + (σ₃-σ₁)²)/2) +/// +/// Rust: `solid_mechanics::von_mises_stress` +#[pyfunction] +#[pyo3(name = "von_mises_stress", signature = (s1, s2, s3))] +pub fn pyfn_von_mises_stress(s1: f64, s2: f64, s3: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::solid_mechanics::von_mises_stress(s1, s2, s3)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Safety factor: n = σ_yield/σ_applied +/// +/// Rust: `solid_mechanics::safety_factor` +#[pyfunction] +#[pyo3(name = "safety_factor", signature = (yield_strength, applied_stress))] +pub fn pyfn_safety_factor(yield_strength: f64, applied_stress: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::solid_mechanics::safety_factor(yield_strength, applied_stress)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Elastic strain energy density: u = σε/2 +/// +/// Rust: `solid_mechanics::strain_energy_density` +#[pyfunction] +#[pyo3(name = "strain_energy_density", signature = (stress, strain))] +pub fn pyfn_strain_energy_density(stress: f64, strain: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::solid_mechanics::strain_energy_density(stress, strain)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_tensile_stress, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tensile_strain, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_shear_stress, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_shear_strain, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_volumetric_strain, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_true_stress, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_true_strain, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_youngs_modulus, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_shear_modulus, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bulk_modulus, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_poisson_ratio_from_moduli, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_e_from_k_and_g, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bulk_from_e_and_nu, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_shear_from_e_and_nu, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_beam_deflection_cantilever_point, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_beam_deflection_simply_supported_center, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bending_moment, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bending_stress, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_second_moment_rectangle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_second_moment_circle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_von_mises_stress, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_safety_factor, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_strain_energy_density, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_spatial.rs b/bindings/python/src/generated/m_spatial.rs new file mode 100644 index 0000000..c02bbf8 --- /dev/null +++ b/bindings/python/src/generated/m_spatial.rs @@ -0,0 +1,22 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + Ok(()) +} diff --git a/bindings/python/src/generated/m_spatial__bvh.rs b/bindings/python/src/generated/m_spatial__bvh.rs new file mode 100644 index 0000000..5a40343 --- /dev/null +++ b/bindings/python/src/generated/m_spatial__bvh.rs @@ -0,0 +1,23 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_spatial__contain.rs b/bindings/python/src/generated/m_spatial__contain.rs new file mode 100644 index 0000000..10753cd --- /dev/null +++ b/bindings/python/src/generated/m_spatial__contain.rs @@ -0,0 +1,329 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Twice the signed area of (a, b, c): positive for a CCW turn. +/// +/// Rust: `spatial::contain::orient2d` +#[pyfunction] +#[pyo3(name = "orient2d", signature = (a, b, c))] +pub fn pyfn_orient2d(a: crate::generated::types::PyVec2Arg, b: crate::generated::types::PyVec2Arg, c: crate::generated::types::PyVec2Arg) -> PyResult { + let a = a.0; + let b = b.0; + let c = c.0; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::contain::orient2d(a, b, c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Six times the signed volume of tetrahedron (a, b, c, d): positive +/// when d lies on the positive side of the CCW plane (a, b, c). +/// +/// Rust: `spatial::contain::orient3d` +#[pyfunction] +#[pyo3(name = "orient3d", signature = (a, b, c, d))] +pub fn pyfn_orient3d(a: crate::generated::types::PyVec3Arg, b: crate::generated::types::PyVec3Arg, c: crate::generated::types::PyVec3Arg, d: crate::generated::types::PyVec3Arg) -> PyResult { + let a = a.0; + let b = b.0; + let c = c.0; + let d = d.0; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::contain::orient3d(a, b, c, d)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// In-circle predicate: > 0 iff d lies inside the circumcircle of the +/// CCW triangle (a, b, c) (4×4 determinant form). +/// +/// Rust: `spatial::contain::in_circle` +#[pyfunction] +#[pyo3(name = "in_circle", signature = (a, b, c, d))] +pub fn pyfn_in_circle(a: crate::generated::types::PyVec2Arg, b: crate::generated::types::PyVec2Arg, c: crate::generated::types::PyVec2Arg, d: crate::generated::types::PyVec2Arg) -> PyResult { + let a = a.0; + let b = b.0; + let c = c.0; + let d = d.0; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::contain::in_circle(a, b, c, d)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// In-sphere predicate: > 0 iff e lies inside the circumsphere of the +/// positively oriented tetrahedron (a, b, c, d). +/// +/// Rust: `spatial::contain::in_sphere` +#[pyfunction] +#[pyo3(name = "in_sphere", signature = (a, b, c, d, e))] +pub fn pyfn_in_sphere(a: crate::generated::types::PyVec3Arg, b: crate::generated::types::PyVec3Arg, c: crate::generated::types::PyVec3Arg, d: crate::generated::types::PyVec3Arg, e: crate::generated::types::PyVec3Arg) -> PyResult { + let a = a.0; + let b = b.0; + let c = c.0; + let d = d.0; + let e = e.0; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::contain::in_sphere(a, b, c, d, e)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Exact sign of orient2d: −1, 0, or 1, never wrong. +/// +/// A Shewchuk-style floating-point filter answers the easy cases; the +/// hard ones are decided by exact expansion evaluation of the 6-term +/// determinant ax·by − ax·cy + bx·cy − bx·ay + cx·ay − cx·by. +/// +/// Rust: `spatial::contain::orient2d_exact` +#[pyfunction] +#[pyo3(name = "orient2d_exact", signature = (a, b, c))] +pub fn pyfn_orient2d_exact(a: crate::generated::types::PyVec2Arg, b: crate::generated::types::PyVec2Arg, c: crate::generated::types::PyVec2Arg) -> PyResult { + let a = a.0; + let b = b.0; + let c = c.0; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::contain::orient2d_exact(a, b, c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Point in 2-D triangle (boundary counts as inside), robust to either +/// winding. +/// +/// Rust: `spatial::contain::point_in_triangle_2d` +#[pyfunction] +#[pyo3(name = "point_in_triangle_2d", signature = (p, t))] +pub fn pyfn_point_in_triangle_2d(p: crate::generated::types::PyVec2Arg, t: crate::generated::types::PyTriangle2) -> PyResult { + let p = p.0; + let t = t.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::contain::point_in_triangle_2d(p, &t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Point in 3-D triangle: within `tol` of the plane and barycentrics +/// in [0, 1]. +/// +/// Rust: `spatial::contain::point_in_triangle` +#[pyfunction] +#[pyo3(name = "point_in_triangle", signature = (p, t, tol))] +pub fn pyfn_point_in_triangle(p: crate::generated::types::PyVec3Arg, t: crate::generated::types::PyTriangle, tol: f64) -> PyResult { + let p = p.0; + let t = t.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::contain::point_in_triangle(p, &t, tol)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Even-odd (crossing number) point-in-polygon test. +/// +/// Rust: `spatial::contain::point_in_polygon_2d` +#[pyfunction] +#[pyo3(name = "point_in_polygon_2d", signature = (p, poly))] +pub fn pyfn_point_in_polygon_2d(p: crate::generated::types::PyVec2Arg, poly: crate::generated::types::PyPolygon2) -> PyResult { + let p = p.0; + let poly = poly.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::contain::point_in_polygon_2d(p, &poly)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Winding number of a polygon about p (0 for outside points of simple +/// polygons; ±1 inside depending on orientation). +/// +/// Rust: `spatial::contain::winding_number_2d` +#[pyfunction] +#[pyo3(name = "winding_number_2d", signature = (p, poly))] +pub fn pyfn_winding_number_2d(p: crate::generated::types::PyVec2Arg, poly: crate::generated::types::PyPolygon2) -> PyResult { + let p = p.0; + let poly = poly.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::contain::winding_number_2d(p, &poly)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// O(log n) point-in-convex-polygon by binary search on the fan from +/// vertex 0 (polygon must be convex and CCW). +/// +/// Rust: `spatial::contain::point_in_convex_polygon_2d` +#[pyfunction] +#[pyo3(name = "point_in_convex_polygon_2d", signature = (p, poly))] +pub fn pyfn_point_in_convex_polygon_2d(p: crate::generated::types::PyVec2Arg, poly: crate::generated::types::PyPolygon2) -> PyResult { + let p = p.0; + let poly = poly.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::contain::point_in_convex_polygon_2d(p, &poly)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Point inside a convex hull given as outward-oriented triangles: on +/// or behind every face plane. +/// +/// Rust: `spatial::contain::point_in_convex_hull_3d` +#[pyfunction] +#[pyo3(name = "point_in_convex_hull_3d", signature = (p, hull_tris))] +pub fn pyfn_point_in_convex_hull_3d<'py>(py: Python<'py>, p: crate::generated::types::PyVec3Arg, hull_tris: Vec) -> PyResult { + let p = p.0; + let hull_tris = hull_tris.into_iter().map(|__e| __e.inner).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::spatial::contain::point_in_convex_hull_3d(p, &hull_tris))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Generalized winding number test for closed (possibly non-convex) +/// triangle meshes: the summed signed solid angle is ±4π inside and ~0 +/// outside (van Oosterom & Strackee 1983 per-triangle solid angle). +/// +/// Rust: `spatial::contain::point_in_mesh` +#[pyfunction] +#[pyo3(name = "point_in_mesh", signature = (p, tris))] +pub fn pyfn_point_in_mesh<'py>(py: Python<'py>, p: crate::generated::types::PyVec3Arg, tris: Vec) -> PyResult { + let p = p.0; + let tris = tris.into_iter().map(|__e| __e.inner).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::spatial::contain::point_in_mesh(p, &tris))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Point in AABB (closed). +/// +/// Rust: `spatial::contain::point_in_aabb` +#[pyfunction] +#[pyo3(name = "point_in_aabb", signature = (p, b))] +pub fn pyfn_point_in_aabb(p: crate::generated::types::PyVec3Arg, b: crate::generated::types::PyAabb) -> PyResult { + let p = p.0; + let b = b.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::contain::point_in_aabb(p, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Point in OBB: local coordinates within the half extents. +/// +/// Rust: `spatial::contain::point_in_obb` +#[pyfunction] +#[pyo3(name = "point_in_obb", signature = (p, b))] +pub fn pyfn_point_in_obb(p: crate::generated::types::PyVec3Arg, b: crate::generated::types::PyObb) -> PyResult { + let p = p.0; + let b = b.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::contain::point_in_obb(p, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Point in sphere (closed). +/// +/// Rust: `spatial::contain::point_in_sphere` +#[pyfunction] +#[pyo3(name = "point_in_sphere", signature = (p, s))] +pub fn pyfn_point_in_sphere(p: crate::generated::types::PyVec3Arg, s: crate::generated::types::PySphere) -> PyResult { + let p = p.0; + let s = s.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::contain::point_in_sphere(p, &s)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Point in capsule: within radius of the core segment. +/// +/// Rust: `spatial::contain::point_in_capsule` +#[pyfunction] +#[pyo3(name = "point_in_capsule", signature = (p, c))] +pub fn pyfn_point_in_capsule(p: crate::generated::types::PyVec3Arg, c: crate::generated::types::PyCapsule) -> PyResult { + let p = p.0; + let c = c.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::contain::point_in_capsule(p, &c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Point in finite cylinder: axial span and radial distance. +/// +/// Rust: `spatial::contain::point_in_cylinder` +#[pyfunction] +#[pyo3(name = "point_in_cylinder", signature = (p, c))] +pub fn pyfn_point_in_cylinder(p: crate::generated::types::PyVec3Arg, c: crate::generated::types::PyCylinder) -> PyResult { + let p = p.0; + let c = c.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::contain::point_in_cylinder(p, &c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Point in tetrahedron: consistent orientation with respect to all +/// four faces. +/// +/// Rust: `spatial::contain::point_in_tetrahedron` +#[pyfunction] +#[pyo3(name = "point_in_tetrahedron", signature = (p, a, b, c, d))] +pub fn pyfn_point_in_tetrahedron(p: crate::generated::types::PyVec3Arg, a: crate::generated::types::PyVec3Arg, b: crate::generated::types::PyVec3Arg, c: crate::generated::types::PyVec3Arg, d: crate::generated::types::PyVec3Arg) -> PyResult { + let p = p.0; + let a = a.0; + let b = b.0; + let c = c.0; + let d = d.0; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::contain::point_in_tetrahedron(p, a, b, c, d)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Full containment of one AABB in another (closed). +/// +/// Rust: `spatial::contain::aabb_contains_aabb` +#[pyfunction] +#[pyo3(name = "aabb_contains_aabb", signature = (outer, inner))] +pub fn pyfn_aabb_contains_aabb(outer: crate::generated::types::PyAabb, inner: crate::generated::types::PyAabb) -> PyResult { + let outer = outer.inner; + let inner = inner.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::contain::aabb_contains_aabb(&outer, &inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Sphere containing an entire AABB (all corners inside). +/// +/// Rust: `spatial::contain::sphere_contains_aabb` +#[pyfunction] +#[pyo3(name = "sphere_contains_aabb", signature = (s, b))] +pub fn pyfn_sphere_contains_aabb(s: crate::generated::types::PySphere, b: crate::generated::types::PyAabb) -> PyResult { + let s = s.inner; + let b = b.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::contain::sphere_contains_aabb(&s, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_orient2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_orient3d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_in_circle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_in_sphere, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_orient2d_exact, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_point_in_triangle_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_point_in_triangle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_point_in_polygon_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_winding_number_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_point_in_convex_polygon_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_point_in_convex_hull_3d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_point_in_mesh, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_point_in_aabb, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_point_in_obb, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_point_in_sphere, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_point_in_capsule, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_point_in_cylinder, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_point_in_tetrahedron, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_aabb_contains_aabb, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sphere_contains_aabb, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_spatial__distance.rs b/bindings/python/src/generated/m_spatial__distance.rs new file mode 100644 index 0000000..52abb2c --- /dev/null +++ b/bindings/python/src/generated/m_spatial__distance.rs @@ -0,0 +1,293 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Closest point on a segment and its parameter t ∈ [0, 1]. +/// +/// Rust: `spatial::distance::closest_point_segment` +#[pyfunction] +#[pyo3(name = "closest_point_segment", signature = (p, s))] +pub fn pyfn_closest_point_segment(p: crate::generated::types::PyVec3Arg, s: crate::generated::types::PySegment) -> PyResult<(crate::generated::types::PyVec3, f64)> { + let p = p.0; + let s = s.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::distance::closest_point_segment(p, &s)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((crate::generated::types::PyVec3 { inner: __v.0 }, __v.1)) +} + +/// 2-D closest point on a segment and its parameter. +/// +/// Rust: `spatial::distance::closest_point_segment_2d` +#[pyfunction] +#[pyo3(name = "closest_point_segment_2d", signature = (p, s))] +pub fn pyfn_closest_point_segment_2d(p: crate::generated::types::PyVec2Arg, s: crate::generated::types::PyPrimitivesSegment2) -> PyResult<(crate::generated::types::PyVec2, f64)> { + let p = p.0; + let s = s.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::distance::closest_point_segment_2d(p, &s)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((crate::generated::types::PyVec2 { inner: __v.0 }, __v.1)) +} + +/// Closest point on a triangle (Ericson RTCD §5.1.5 Voronoi-region +/// walk). +/// +/// Rust: `spatial::distance::closest_point_triangle` +#[pyfunction] +#[pyo3(name = "closest_point_triangle", signature = (p, t))] +pub fn pyfn_closest_point_triangle(p: crate::generated::types::PyVec3Arg, t: crate::generated::types::PyTriangle) -> PyResult { + let p = p.0; + let t = t.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::distance::closest_point_triangle(p, &t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Orthogonal projection onto a plane. +/// +/// Rust: `spatial::distance::closest_point_plane` +#[pyfunction] +#[pyo3(name = "closest_point_plane", signature = (p, pl))] +pub fn pyfn_closest_point_plane(p: crate::generated::types::PyVec3Arg, pl: crate::generated::types::PyPrimitivesPlane) -> PyResult { + let p = p.0; + let pl = pl.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::distance::closest_point_plane(p, &pl)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Componentwise clamp onto an AABB. +/// +/// Rust: `spatial::distance::closest_point_aabb` +#[pyfunction] +#[pyo3(name = "closest_point_aabb", signature = (p, b))] +pub fn pyfn_closest_point_aabb(p: crate::generated::types::PyVec3Arg, b: crate::generated::types::PyAabb) -> PyResult { + let p = p.0; + let b = b.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::distance::closest_point_aabb(p, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Clamp in the box's local frame (RTCD §5.1.4). +/// +/// Rust: `spatial::distance::closest_point_obb` +#[pyfunction] +#[pyo3(name = "closest_point_obb", signature = (p, b))] +pub fn pyfn_closest_point_obb(p: crate::generated::types::PyVec3Arg, b: crate::generated::types::PyObb) -> PyResult { + let p = p.0; + let b = b.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::distance::closest_point_obb(p, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Closest point on a sphere's surface (center maps to +radius·x̂). +/// +/// Rust: `spatial::distance::closest_point_sphere` +#[pyfunction] +#[pyo3(name = "closest_point_sphere", signature = (p, s))] +pub fn pyfn_closest_point_sphere(p: crate::generated::types::PyVec3Arg, s: crate::generated::types::PySphere) -> PyResult { + let p = p.0; + let s = s.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::distance::closest_point_sphere(p, &s)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Closest points between two segments and their distance +/// (RTCD §5.1.9). +/// +/// Rust: `spatial::distance::closest_points_segments` +#[pyfunction] +#[pyo3(name = "closest_points_segments", signature = (s1, s2))] +pub fn pyfn_closest_points_segments(s1: crate::generated::types::PySegment, s2: crate::generated::types::PySegment) -> PyResult<(crate::generated::types::PyVec3, crate::generated::types::PyVec3, f64)> { + let s1 = s1.inner; + let s2 = s2.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::distance::closest_points_segments(&s1, &s2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((crate::generated::types::PyVec3 { inner: __v.0 }, crate::generated::types::PyVec3 { inner: __v.1 }, __v.2)) +} + +/// Closest points between two infinite lines; `None` when parallel. +/// +/// Rust: `spatial::distance::closest_points_lines` +#[pyfunction] +#[pyo3(name = "closest_points_lines", signature = (p1, d1, p2, d2))] +pub fn pyfn_closest_points_lines(p1: crate::generated::types::PyVec3Arg, d1: crate::generated::types::PyVec3Arg, p2: crate::generated::types::PyVec3Arg, d2: crate::generated::types::PyVec3Arg) -> PyResult> { + let p1 = p1.0; + let d1 = d1.0; + let p2 = p2.0; + let d2 = d2.0; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::distance::closest_points_lines(p1, d1, p2, d2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| (crate::generated::types::PyVec3 { inner: __x.0 }, crate::generated::types::PyVec3 { inner: __x.1 }))) +} + +/// Distance from a point to a segment. +/// +/// Rust: `spatial::distance::distance_point_segment` +#[pyfunction] +#[pyo3(name = "distance_point_segment", signature = (p, s))] +pub fn pyfn_distance_point_segment(p: crate::generated::types::PyVec3Arg, s: crate::generated::types::PySegment) -> PyResult { + let p = p.0; + let s = s.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::distance::distance_point_segment(p, &s)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Distance from a point to a triangle. +/// +/// Rust: `spatial::distance::distance_point_triangle` +#[pyfunction] +#[pyo3(name = "distance_point_triangle", signature = (p, t))] +pub fn pyfn_distance_point_triangle(p: crate::generated::types::PyVec3Arg, t: crate::generated::types::PyTriangle) -> PyResult { + let p = p.0; + let t = t.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::distance::distance_point_triangle(p, &t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Distance from a point to a polyline, with the segment index and +/// parameter of the closest point. +/// +/// Panics: +/// Panics on a polyline with no segments. +/// +/// Rust: `spatial::distance::distance_point_polyline` +#[pyfunction] +#[pyo3(name = "distance_point_polyline", signature = (p, pl))] +pub fn pyfn_distance_point_polyline(p: crate::generated::types::PyVec3Arg, pl: crate::generated::types::PyPolyline) -> PyResult<(f64, usize, f64)> { + let p = p.0; + let pl = pl.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::distance::distance_point_polyline(p, &pl)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// Unsigned distance from a point to the boundary of a polygon. +/// +/// Rust: `spatial::distance::distance_point_polygon_2d` +#[pyfunction] +#[pyo3(name = "distance_point_polygon_2d", signature = (p, poly))] +pub fn pyfn_distance_point_polygon_2d(p: crate::generated::types::PyVec2Arg, poly: crate::generated::types::PyPolygon2) -> PyResult { + let p = p.0; + let poly = poly.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::distance::distance_point_polygon_2d(p, &poly)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Distance between a segment and a triangle: minimum over segment vs +/// the three edges and the endpoints vs the face. +/// +/// Rust: `spatial::distance::distance_segment_triangle` +#[pyfunction] +#[pyo3(name = "distance_segment_triangle", signature = (s, t))] +pub fn pyfn_distance_segment_triangle(s: crate::generated::types::PySegment, t: crate::generated::types::PyTriangle) -> PyResult { + let s = s.inner; + let t = t.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::distance::distance_segment_triangle(&s, &t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Distance between two AABBs (0 when overlapping). +/// +/// Rust: `spatial::distance::distance_aabb_aabb` +#[pyfunction] +#[pyo3(name = "distance_aabb_aabb", signature = (a, b))] +pub fn pyfn_distance_aabb_aabb(a: crate::generated::types::PyAabb, b: crate::generated::types::PyAabb) -> PyResult { + let a = a.inner; + let b = b.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::distance::distance_aabb_aabb(&a, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Symmetric Hausdorff distance between two 3-D point sets. +/// +/// Panics: +/// Panics when either set is empty. +/// +/// Rust: `spatial::distance::hausdorff_distance` +#[pyfunction] +#[pyo3(name = "hausdorff_distance", signature = (a, b))] +pub fn pyfn_hausdorff_distance<'py>(py: Python<'py>, a: Vec, b: Vec) -> PyResult { + let a = a.into_iter().map(|__e| __e.0).collect::>(); + let b = b.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::spatial::distance::hausdorff_distance(&a, &b))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Symmetric Hausdorff distance between two 2-D point sets. +/// +/// Panics: +/// Panics when either set is empty. +/// +/// Rust: `spatial::distance::hausdorff_distance_2d` +#[pyfunction] +#[pyo3(name = "hausdorff_distance_2d", signature = (a, b))] +pub fn pyfn_hausdorff_distance_2d<'py>(py: Python<'py>, a: Vec, b: Vec) -> PyResult { + let a = a.into_iter().map(|__e| __e.0).collect::>(); + let b = b.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::spatial::distance::hausdorff_distance_2d(&a, &b))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Discrete Fréchet distance between two 2-D polygonal curves +/// (Eiter & Mannila dynamic program). +/// +/// Panics: +/// Panics when either curve is empty. +/// +/// Rust: `spatial::distance::frechet_distance_2d` +#[pyfunction] +#[pyo3(name = "frechet_distance_2d", signature = (a, b))] +pub fn pyfn_frechet_distance_2d<'py>(py: Python<'py>, a: Vec, b: Vec) -> PyResult { + let a = a.into_iter().map(|__e| __e.0).collect::>(); + let b = b.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::spatial::distance::frechet_distance_2d(&a, &b))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_closest_point_segment, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_closest_point_segment_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_closest_point_triangle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_closest_point_plane, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_closest_point_aabb, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_closest_point_obb, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_closest_point_sphere, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_closest_points_segments, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_closest_points_lines, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_distance_point_segment, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_distance_point_triangle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_distance_point_polyline, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_distance_point_polygon_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_distance_segment_triangle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_distance_aabb_aabb, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hausdorff_distance, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hausdorff_distance_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_frechet_distance_2d, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_spatial__frame.rs b/bindings/python/src/generated/m_spatial__frame.rs new file mode 100644 index 0000000..203c0bd --- /dev/null +++ b/bindings/python/src/generated/m_spatial__frame.rs @@ -0,0 +1,23 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_spatial__intersect.rs b/bindings/python/src/generated/m_spatial__intersect.rs new file mode 100644 index 0000000..d7129d7 --- /dev/null +++ b/bindings/python/src/generated/m_spatial__intersect.rs @@ -0,0 +1,362 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Ray vs sphere: nearest hit with t ≥ 0 (RTCD §5.3.2). +/// +/// Rust: `spatial::intersect::ray_sphere` +#[pyfunction] +#[pyo3(name = "ray_sphere", signature = (r, s))] +pub fn pyfn_ray_sphere(r: crate::generated::types::PyRay, s: crate::generated::types::PySphere) -> PyResult> { + let r = r.inner; + let s = s.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::intersect::ray_sphere(&r, &s)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyIntersectRayHit { inner: __x })) +} + +/// Ray vs plane: `None` when parallel or hitting behind the origin. +/// +/// Rust: `spatial::intersect::ray_plane` +#[pyfunction] +#[pyo3(name = "ray_plane", signature = (r, p))] +pub fn pyfn_ray_plane(r: crate::generated::types::PyRay, p: crate::generated::types::PyPrimitivesPlane) -> PyResult> { + let r = r.inner; + let p = p.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::intersect::ray_plane(&r, &p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyIntersectRayHit { inner: __x })) +} + +/// Möller-Trumbore ray-triangle intersection; also returns the +/// barycentric coordinates (u, v, w) of the hit with respect to +/// (a, b, c). With `cull_backface`, only front faces (CCW seen from +/// the ray origin) hit. +/// +/// Rust: `spatial::intersect::ray_triangle` +#[pyfunction] +#[pyo3(name = "ray_triangle", signature = (r, t, cull_backface))] +pub fn pyfn_ray_triangle(r: crate::generated::types::PyRay, t: crate::generated::types::PyTriangle, cull_backface: bool) -> PyResult> { + let r = r.inner; + let t = t.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::intersect::ray_triangle(&r, &t, cull_backface)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| (crate::generated::types::PyIntersectRayHit { inner: __x.0 }, (__x.1.0, __x.1.1, __x.1.2)))) +} + +/// Slab-method ray vs AABB: (t_enter, t_exit) of the overlap with +/// t ≥ 0, `None` on a miss (RTCD §5.3.3). +/// +/// Rust: `spatial::intersect::ray_aabb` +#[pyfunction] +#[pyo3(name = "ray_aabb", signature = (r, b))] +pub fn pyfn_ray_aabb(r: crate::generated::types::PyRay, b: crate::generated::types::PyAabb) -> PyResult> { + let r = r.inner; + let b = b.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::intersect::ray_aabb(&r, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| (__x.0, __x.1))) +} + +/// Ray vs OBB: the slab test in the box's local frame. +/// +/// Rust: `spatial::intersect::ray_obb` +#[pyfunction] +#[pyo3(name = "ray_obb", signature = (r, b))] +pub fn pyfn_ray_obb(r: crate::generated::types::PyRay, b: crate::generated::types::PyObb) -> PyResult> { + let r = r.inner; + let b = b.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::intersect::ray_obb(&r, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| (__x.0, __x.1))) +} + +/// Ray vs capsule: cylinder body plus spherical caps, nearest hit. +/// +/// Rust: `spatial::intersect::ray_capsule` +#[pyfunction] +#[pyo3(name = "ray_capsule", signature = (r, c))] +pub fn pyfn_ray_capsule(r: crate::generated::types::PyRay, c: crate::generated::types::PyCapsule) -> PyResult> { + let r = r.inner; + let c = c.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::intersect::ray_capsule(&r, &c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyIntersectRayHit { inner: __x })) +} + +/// Ray vs finite cylinder: lateral surface and both cap disks. +/// +/// Rust: `spatial::intersect::ray_cylinder` +#[pyfunction] +#[pyo3(name = "ray_cylinder", signature = (r, c))] +pub fn pyfn_ray_cylinder(r: crate::generated::types::PyRay, c: crate::generated::types::PyCylinder) -> PyResult> { + let r = r.inner; + let c = c.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::intersect::ray_cylinder(&r, &c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyIntersectRayHit { inner: __x })) +} + +/// Proper 2-D segment intersection (interiors cross): the point, or +/// `None` for disjoint, touching, or collinear segments. +/// +/// Rust: `spatial::intersect::segment_segment_2d` +#[pyfunction] +#[pyo3(name = "segment_segment_2d", signature = (s1, s2))] +pub fn pyfn_segment_segment_2d(s1: crate::generated::types::PyPrimitivesSegment2, s2: crate::generated::types::PyPrimitivesSegment2) -> PyResult> { + let s1 = s1.inner; + let s2 = s2.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::intersect::segment_segment_2d(&s1, &s2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyVec2 { inner: __x })) +} + +/// Parameters (t, u) with s1(t) = s2(u), both in [0, 1] (endpoints +/// included); `None` for parallel/collinear or non-intersecting pairs. +/// +/// Rust: `spatial::intersect::segment_segment_2d_params` +#[pyfunction] +#[pyo3(name = "segment_segment_2d_params", signature = (s1, s2))] +pub fn pyfn_segment_segment_2d_params(s1: crate::generated::types::PyPrimitivesSegment2, s2: crate::generated::types::PyPrimitivesSegment2) -> PyResult> { + let s1 = s1.inner; + let s2 = s2.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::intersect::segment_segment_2d_params(&s1, &s2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| (__x.0, __x.1))) +} + +/// Infinite line (point + direction) vs circle: the two intersection +/// points ordered along the direction (equal at tangency). +/// +/// Rust: `spatial::intersect::line_circle` +#[pyfunction] +#[pyo3(name = "line_circle", signature = (p, dir, c))] +pub fn pyfn_line_circle(p: crate::generated::types::PyVec2Arg, dir: crate::generated::types::PyVec2Arg, c: crate::generated::types::PyCircle) -> PyResult> { + let p = p.0; + let dir = dir.0; + let c = c.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::intersect::line_circle(p, dir, &c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| (crate::generated::types::PyVec2 { inner: __x.0 }, crate::generated::types::PyVec2 { inner: __x.1 }))) +} + +/// Circle-circle intersection points; `None` when separate, nested, or +/// coincident. +/// +/// Rust: `spatial::intersect::circle_circle` +#[pyfunction] +#[pyo3(name = "circle_circle", signature = (a, b))] +pub fn pyfn_circle_circle(a: crate::generated::types::PyCircle, b: crate::generated::types::PyCircle) -> PyResult> { + let a = a.inner; + let b = b.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::intersect::circle_circle(&a, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| (crate::generated::types::PyVec2 { inner: __x.0 }, crate::generated::types::PyVec2 { inner: __x.1 }))) +} + +/// Sphere overlap test. +/// +/// Rust: `spatial::intersect::sphere_sphere` +#[pyfunction] +#[pyo3(name = "sphere_sphere", signature = (a, b))] +pub fn pyfn_sphere_sphere(a: crate::generated::types::PySphere, b: crate::generated::types::PySphere) -> PyResult { + let a = a.inner; + let b = b.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::intersect::sphere_sphere(&a, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Sphere contact: unit normal from a toward b and penetration depth, +/// `Some` only when overlapping. +/// +/// Rust: `spatial::intersect::sphere_sphere_contact` +#[pyfunction] +#[pyo3(name = "sphere_sphere_contact", signature = (a, b))] +pub fn pyfn_sphere_sphere_contact(a: crate::generated::types::PySphere, b: crate::generated::types::PySphere) -> PyResult> { + let a = a.inner; + let b = b.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::intersect::sphere_sphere_contact(&a, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| (crate::generated::types::PyVec3 { inner: __x.0 }, __x.1))) +} + +/// AABB overlap test (closed). +/// +/// Rust: `spatial::intersect::aabb_aabb` +#[pyfunction] +#[pyo3(name = "aabb_aabb", signature = (a, b))] +pub fn pyfn_aabb_aabb(a: crate::generated::types::PyAabb, b: crate::generated::types::PyAabb) -> PyResult { + let a = a.inner; + let b = b.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::intersect::aabb_aabb(&a, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Rectangle overlap test (closed). +/// +/// Rust: `spatial::intersect::rect_rect` +#[pyfunction] +#[pyo3(name = "rect_rect", signature = (a, b))] +pub fn pyfn_rect_rect(a: crate::generated::types::PyRect, b: crate::generated::types::PyRect) -> PyResult { + let a = a.inner; + let b = b.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::intersect::rect_rect(&a, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Sphere vs AABB (closest-point distance). +/// +/// Rust: `spatial::intersect::sphere_aabb` +#[pyfunction] +#[pyo3(name = "sphere_aabb", signature = (s, b))] +pub fn pyfn_sphere_aabb(s: crate::generated::types::PySphere, b: crate::generated::types::PyAabb) -> PyResult { + let s = s.inner; + let b = b.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::intersect::sphere_aabb(&s, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Sphere vs triangle: contact point on the triangle and penetration +/// depth when overlapping. +/// +/// Rust: `spatial::intersect::sphere_triangle` +#[pyfunction] +#[pyo3(name = "sphere_triangle", signature = (s, t))] +pub fn pyfn_sphere_triangle(s: crate::generated::types::PySphere, t: crate::generated::types::PyTriangle) -> PyResult> { + let s = s.inner; + let t = t.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::intersect::sphere_triangle(&s, &t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| (crate::generated::types::PyVec3 { inner: __x.0 }, __x.1))) +} + +/// OBB-OBB overlap by the separating axis theorem over the 15 +/// candidate axes (RTCD §4.4.1). +/// +/// Rust: `spatial::intersect::obb_obb` +#[pyfunction] +#[pyo3(name = "obb_obb", signature = (a, b))] +pub fn pyfn_obb_obb(a: crate::generated::types::PyObb, b: crate::generated::types::PyObb) -> PyResult { + let a = a.inner; + let b = b.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::intersect::obb_obb(&a, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Triangle-triangle overlap by SAT over both normals and the nine +/// edge-pair cross products, with a coplanar 2-D SAT fallback +/// (equivalent to the interval tests of Möller 1997). +/// +/// Rust: `spatial::intersect::triangle_triangle` +#[pyfunction] +#[pyo3(name = "triangle_triangle", signature = (a, b))] +pub fn pyfn_triangle_triangle(a: crate::generated::types::PyTriangle, b: crate::generated::types::PyTriangle) -> PyResult { + let a = a.inner; + let b = b.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::intersect::triangle_triangle(&a, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Plane-plane intersection line; `None` for (near-)parallel planes. +/// +/// Rust: `spatial::intersect::plane_plane` +#[pyfunction] +#[pyo3(name = "plane_plane", signature = (a, b))] +pub fn pyfn_plane_plane(a: crate::generated::types::PyPrimitivesPlane, b: crate::generated::types::PyPrimitivesPlane) -> PyResult> { + let a = a.inner; + let b = b.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::intersect::plane_plane(&a, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyRay { inner: __x })) +} + +/// Common point of three planes; `None` when any pair is parallel or +/// the normals are linearly dependent. +/// +/// Rust: `spatial::intersect::three_planes` +#[pyfunction] +#[pyo3(name = "three_planes", signature = (a, b, c))] +pub fn pyfn_three_planes(a: crate::generated::types::PyPrimitivesPlane, b: crate::generated::types::PyPrimitivesPlane, c: crate::generated::types::PyPrimitivesPlane) -> PyResult> { + let a = a.inner; + let b = b.inner; + let c = c.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::intersect::three_planes(&a, &b, &c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyVec3 { inner: __x })) +} + +/// Triangle vs AABB by the 13-axis SAT of Akenine-Möller. +/// +/// Rust: `spatial::intersect::triangle_aabb` +#[pyfunction] +#[pyo3(name = "triangle_aabb", signature = (t, b))] +pub fn pyfn_triangle_aabb(t: crate::generated::types::PyTriangle, b: crate::generated::types::PyAabb) -> PyResult { + let t = t.inner; + let b = b.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::intersect::triangle_aabb(&t, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Polygon overlap: SAT when both are convex, otherwise any edge +/// crossing or mutual containment. +/// +/// Rust: `spatial::intersect::polygon_polygon_2d` +#[pyfunction] +#[pyo3(name = "polygon_polygon_2d", signature = (a, b))] +pub fn pyfn_polygon_polygon_2d(a: crate::generated::types::PyPolygon2, b: crate::generated::types::PyPolygon2) -> PyResult { + let a = a.inner; + let b = b.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::intersect::polygon_polygon_2d(&a, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_ray_sphere, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ray_plane, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ray_triangle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ray_aabb, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ray_obb, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ray_capsule, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ray_cylinder, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_segment_segment_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_segment_segment_2d_params, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_line_circle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_circle_circle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sphere_sphere, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sphere_sphere_contact, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_aabb_aabb, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rect_rect, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sphere_aabb, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sphere_triangle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_obb_obb, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_triangle_triangle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_plane_plane, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_three_planes, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_triangle_aabb, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_polygon_polygon_2d, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_spatial__kdtree.rs b/bindings/python/src/generated/m_spatial__kdtree.rs new file mode 100644 index 0000000..f39c50a --- /dev/null +++ b/bindings/python/src/generated/m_spatial__kdtree.rs @@ -0,0 +1,25 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_spatial__mat4.rs b/bindings/python/src/generated/m_spatial__mat4.rs new file mode 100644 index 0000000..6d3beda --- /dev/null +++ b/bindings/python/src/generated/m_spatial__mat4.rs @@ -0,0 +1,23 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_spatial__octree.rs b/bindings/python/src/generated/m_spatial__octree.rs new file mode 100644 index 0000000..5e9f184 --- /dev/null +++ b/bindings/python/src/generated/m_spatial__octree.rs @@ -0,0 +1,38 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Computes accelerations for all bodies, using direct summation below the crossover threshold or Barnes-Hut above it. +/// +/// Rust: `spatial::octree::compute_all_accelerations` +#[pyfunction] +#[pyo3(name = "compute_all_accelerations", signature = (bodies, theta, softening))] +pub fn pyfn_compute_all_accelerations(bodies: Vec, theta: f64, softening: f64) -> PyResult> { + let bodies = bodies.into_iter().map(|__e| __e.inner).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::octree::compute_all_accelerations(&bodies, theta, softening)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_compute_all_accelerations, m)?)?; + m.add("BH_THETA", rust_physics_engine::spatial::octree::BH_THETA)?; + m.add("BH_CROSSOVER", rust_physics_engine::spatial::octree::BH_CROSSOVER)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_spatial__primitives.rs b/bindings/python/src/generated/m_spatial__primitives.rs new file mode 100644 index 0000000..5e25cc3 --- /dev/null +++ b/bindings/python/src/generated/m_spatial__primitives.rs @@ -0,0 +1,37 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_spatial__projective.rs b/bindings/python/src/generated/m_spatial__projective.rs new file mode 100644 index 0000000..a00784e --- /dev/null +++ b/bindings/python/src/generated/m_spatial__projective.rs @@ -0,0 +1,140 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Lifts a Euclidean point to homogeneous coordinates (w = 1). +/// +/// Rust: `spatial::projective::point_h` +#[pyfunction] +#[pyo3(name = "point_h", signature = (p))] +pub fn pyfn_point_h<'py>(py: Python<'py>, p: crate::generated::types::PyVec2Arg) -> PyResult> { + let p = p.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::spatial::projective::point_h(p))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_vec()) +} + +/// Projects back to Euclidean coordinates; `None` for points at +/// infinity (w ≈ 0). +/// +/// Rust: `spatial::projective::dehomogenize` +#[pyfunction] +#[pyo3(name = "dehomogenize", signature = (h))] +pub fn pyfn_dehomogenize(h: Vec) -> PyResult> { + let h = <[f64; 3]>::try_from(h).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 3 values, got {}", __v.len())))?; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::projective::dehomogenize(h)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyVec2 { inner: __x })) +} + +/// The line through two points: l = a × b. +/// +/// Rust: `spatial::projective::line_through` +#[pyfunction] +#[pyo3(name = "line_through", signature = (a, b))] +pub fn pyfn_line_through<'py>(py: Python<'py>, a: crate::generated::types::PyVec2Arg, b: crate::generated::types::PyVec2Arg) -> PyResult> { + let a = a.0; + let b = b.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::spatial::projective::line_through(a, b))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_vec()) +} + +/// Intersection of two lines: p = l₁ × l₂ (possibly at infinity for +/// parallel lines). +/// +/// Rust: `spatial::projective::lines_intersect` +#[pyfunction] +#[pyo3(name = "lines_intersect", signature = (l1, l2))] +pub fn pyfn_lines_intersect<'py>(py: Python<'py>, l1: Vec, l2: Vec) -> PyResult> { + let l1 = <[f64; 3]>::try_from(l1).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 3 values, got {}", __v.len())))?; + let l2 = <[f64; 3]>::try_from(l2).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 3 values, got {}", __v.len())))?; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::spatial::projective::lines_intersect(l1, l2))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_vec()) +} + +/// Incidence test: p·l = 0 within tol (both scale-normalized). +/// +/// Rust: `spatial::projective::point_on_line` +#[pyfunction] +#[pyo3(name = "point_on_line", signature = (p, l, tol))] +pub fn pyfn_point_on_line<'py>(py: Python<'py>, p: Vec, l: Vec, tol: f64) -> PyResult { + let p = <[f64; 3]>::try_from(p).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 3 values, got {}", __v.len())))?; + let l = <[f64; 3]>::try_from(l).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 3 values, got {}", __v.len())))?; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::spatial::projective::point_on_line(p, l, tol))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Collinearity of three Euclidean points (twice the triangle area +/// below tol, scale-normalized). +/// +/// Rust: `spatial::projective::are_collinear` +#[pyfunction] +#[pyo3(name = "are_collinear", signature = (a, b, c, tol))] +pub fn pyfn_are_collinear(a: crate::generated::types::PyVec2Arg, b: crate::generated::types::PyVec2Arg, c: crate::generated::types::PyVec2Arg, tol: f64) -> PyResult { + let a = a.0; + let b = b.0; + let c = c.0; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::projective::are_collinear(a, b, c, tol)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Cross ratio of four collinear parameters: +/// (a, b; c, d) = ((a−c)(b−d)) / ((a−d)(b−c)). +/// +/// Panics: +/// Panics when the denominator vanishes (repeated points). +/// +/// Rust: `spatial::projective::cross_ratio` +#[pyfunction] +#[pyo3(name = "cross_ratio", signature = (a, b, c, d))] +pub fn pyfn_cross_ratio(a: f64, b: f64, c: f64, d: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::projective::cross_ratio(a, b, c, d)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Homography mapping an arbitrary quad (CCW or CW consistent order) +/// onto the axis-aligned rectangle [0, w]×[0, h] with corner order +/// (0,0), (w,0), (w,h), (0,h). +/// +/// Rust: `spatial::projective::rectify_quad_to_rect` +#[pyfunction] +#[pyo3(name = "rectify_quad_to_rect", signature = (quad, width, height))] +pub fn pyfn_rectify_quad_to_rect(quad: Vec, width: f64, height: f64) -> PyResult> { + let quad = <[rust_physics_engine::math::Vec2; 4]>::try_from(quad.into_iter().map(|__e| __e.0).collect::>()).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 4 values, got {}", __v.len())))?; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::projective::rectify_quad_to_rect(quad, width, height)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyHomography { inner: __x })) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_point_h, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dehomogenize, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_line_through, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lines_intersect, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_point_on_line, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_are_collinear, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cross_ratio, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rectify_quad_to_rect, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_spatial__sdf.rs b/bindings/python/src/generated/m_spatial__sdf.rs new file mode 100644 index 0000000..37f70cb --- /dev/null +++ b/bindings/python/src/generated/m_spatial__sdf.rs @@ -0,0 +1,550 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Sphere of radius r at the origin: |p| − r. +/// +/// Rust: `spatial::sdf::sd_sphere` +#[pyfunction] +#[pyo3(name = "sd_sphere", signature = (p, r))] +pub fn pyfn_sd_sphere(p: crate::generated::types::PyVec3Arg, r: f64) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::sdf::sd_sphere(p, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Axis-aligned box with the given half extents. +/// +/// Rust: `spatial::sdf::sd_box` +#[pyfunction] +#[pyo3(name = "sd_box", signature = (p, half))] +pub fn pyfn_sd_box(p: crate::generated::types::PyVec3Arg, half: crate::generated::types::PyVec3Arg) -> PyResult { + let p = p.0; + let half = half.0; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::sdf::sd_box(p, half)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Box with edges rounded by radius r. +/// +/// Rust: `spatial::sdf::sd_rounded_box` +#[pyfunction] +#[pyo3(name = "sd_rounded_box", signature = (p, half, r))] +pub fn pyfn_sd_rounded_box(p: crate::generated::types::PyVec3Arg, half: crate::generated::types::PyVec3Arg, r: f64) -> PyResult { + let p = p.0; + let half = half.0; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::sdf::sd_rounded_box(p, half, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Torus in the xz-plane: major radius to the tube center, minor tube +/// radius. +/// +/// Rust: `spatial::sdf::sd_torus` +#[pyfunction] +#[pyo3(name = "sd_torus", signature = (p, major, minor))] +pub fn pyfn_sd_torus(p: crate::generated::types::PyVec3Arg, major: f64, minor: f64) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::sdf::sd_torus(p, major, minor)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Capsule between a and b with radius r. +/// +/// Rust: `spatial::sdf::sd_capsule` +#[pyfunction] +#[pyo3(name = "sd_capsule", signature = (p, a, b, r))] +pub fn pyfn_sd_capsule(p: crate::generated::types::PyVec3Arg, a: crate::generated::types::PyVec3Arg, b: crate::generated::types::PyVec3Arg, r: f64) -> PyResult { + let p = p.0; + let a = a.0; + let b = b.0; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::sdf::sd_capsule(p, a, b, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Finite capped cylinder between a and b with radius r (exact). +/// +/// Rust: `spatial::sdf::sd_cylinder` +#[pyfunction] +#[pyo3(name = "sd_cylinder", signature = (p, a, b, r))] +pub fn pyfn_sd_cylinder(p: crate::generated::types::PyVec3Arg, a: crate::generated::types::PyVec3Arg, b: crate::generated::types::PyVec3Arg, r: f64) -> PyResult { + let p = p.0; + let a = a.0; + let b = b.0; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::sdf::sd_cylinder(p, a, b, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Infinite-precision capped cone with apex at the origin opening +/// along −y: half-angle `angle`, height h (IQ's sdCone, exact). +/// +/// Rust: `spatial::sdf::sd_cone` +#[pyfunction] +#[pyo3(name = "sd_cone", signature = (p, angle, h))] +pub fn pyfn_sd_cone(p: crate::generated::types::PyVec3Arg, angle: f64, h: f64) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::sdf::sd_cone(p, angle, h)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Half-space n·p + d = 0 (n need not be unit; it is normalized). +/// +/// Rust: `spatial::sdf::sd_plane` +#[pyfunction] +#[pyo3(name = "sd_plane", signature = (p, n, d))] +pub fn pyfn_sd_plane(p: crate::generated::types::PyVec3Arg, n: crate::generated::types::PyVec3Arg, d: f64) -> PyResult { + let p = p.0; + let n = n.0; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::sdf::sd_plane(p, n, d)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Ellipsoid with the given semi-axes (IQ's bound-improved +/// approximation; not exact away from the axes). +/// +/// Rust: `spatial::sdf::sd_ellipsoid` +#[pyfunction] +#[pyo3(name = "sd_ellipsoid", signature = (p, radii))] +pub fn pyfn_sd_ellipsoid(p: crate::generated::types::PyVec3Arg, radii: crate::generated::types::PyVec3Arg) -> PyResult { + let p = p.0; + let radii = radii.0; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::sdf::sd_ellipsoid(p, radii)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Regular octahedron with "radius" s (exact). +/// +/// Rust: `spatial::sdf::sd_octahedron` +#[pyfunction] +#[pyo3(name = "sd_octahedron", signature = (p, s))] +pub fn pyfn_sd_octahedron(p: crate::generated::types::PyVec3Arg, s: f64) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::sdf::sd_octahedron(p, s)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Circle of radius r at the origin. +/// +/// Rust: `spatial::sdf::sd_circle` +#[pyfunction] +#[pyo3(name = "sd_circle", signature = (p, r))] +pub fn pyfn_sd_circle(p: crate::generated::types::PyVec2Arg, r: f64) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::sdf::sd_circle(p, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Axis-aligned rectangle with the given half extents. +/// +/// Rust: `spatial::sdf::sd_rect` +#[pyfunction] +#[pyo3(name = "sd_rect", signature = (p, half))] +pub fn pyfn_sd_rect(p: crate::generated::types::PyVec2Arg, half: crate::generated::types::PyVec2Arg) -> PyResult { + let p = p.0; + let half = half.0; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::sdf::sd_rect(p, half)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Unsigned distance to a 2-D segment minus nothing (a "line" SDF). +/// +/// Rust: `spatial::sdf::sd_segment_2d` +#[pyfunction] +#[pyo3(name = "sd_segment_2d", signature = (p, a, b))] +pub fn pyfn_sd_segment_2d(p: crate::generated::types::PyVec2Arg, a: crate::generated::types::PyVec2Arg, b: crate::generated::types::PyVec2Arg) -> PyResult { + let p = p.0; + let a = a.0; + let b = b.0; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::sdf::sd_segment_2d(p, a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Signed distance to a simple polygon (negative inside). +/// +/// Rust: `spatial::sdf::sd_polygon_2d` +#[pyfunction] +#[pyo3(name = "sd_polygon_2d", signature = (p, poly))] +pub fn pyfn_sd_polygon_2d(p: crate::generated::types::PyVec2Arg, poly: crate::generated::types::PyPolygon2) -> PyResult { + let p = p.0; + let poly = poly.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::sdf::sd_polygon_2d(p, &poly)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Regular hexagon with circumscribed radius derived from apothem r +/// (IQ's sdHexagon: r is the apothem / inradius). +/// +/// Rust: `spatial::sdf::sd_hexagon` +#[pyfunction] +#[pyo3(name = "sd_hexagon", signature = (p, r))] +pub fn pyfn_sd_hexagon(p: crate::generated::types::PyVec2Arg, r: f64) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::sdf::sd_hexagon(p, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// n-pointed star with outer radius r and inner-radius factor set by m +/// (IQ's sdStar; m between 2 and n controls pointiness). +/// +/// Rust: `spatial::sdf::sd_star` +#[pyfunction] +#[pyo3(name = "sd_star", signature = (p, r, n, m))] +pub fn pyfn_sd_star(p: crate::generated::types::PyVec2Arg, r: f64, n: u32, m: f64) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::sdf::sd_star(p, r, n, m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Union: min(a, b). +/// +/// Rust: `spatial::sdf::op_union` +#[pyfunction] +#[pyo3(name = "op_union", signature = (a, b))] +pub fn pyfn_op_union(a: f64, b: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::sdf::op_union(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Subtraction (a minus b): max(a, −b). +/// +/// Rust: `spatial::sdf::op_subtract` +#[pyfunction] +#[pyo3(name = "op_subtract", signature = (a, b))] +pub fn pyfn_op_subtract(a: f64, b: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::sdf::op_subtract(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Intersection: max(a, b). +/// +/// Rust: `spatial::sdf::op_intersect` +#[pyfunction] +#[pyo3(name = "op_intersect", signature = (a, b))] +pub fn pyfn_op_intersect(a: f64, b: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::sdf::op_intersect(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Polynomial smooth union with blending radius k. +/// +/// Rust: `spatial::sdf::op_smooth_union` +#[pyfunction] +#[pyo3(name = "op_smooth_union", signature = (a, b, k))] +pub fn pyfn_op_smooth_union(a: f64, b: f64, k: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::sdf::op_smooth_union(a, b, k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Smooth subtraction. +/// +/// Rust: `spatial::sdf::op_smooth_subtract` +#[pyfunction] +#[pyo3(name = "op_smooth_subtract", signature = (a, b, k))] +pub fn pyfn_op_smooth_subtract(a: f64, b: f64, k: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::sdf::op_smooth_subtract(a, b, k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Smooth intersection. +/// +/// Rust: `spatial::sdf::op_smooth_intersect` +#[pyfunction] +#[pyo3(name = "op_smooth_intersect", signature = (a, b, k))] +pub fn pyfn_op_smooth_intersect(a: f64, b: f64, k: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::sdf::op_smooth_intersect(a, b, k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Rounds a shape outward by r. +/// +/// Rust: `spatial::sdf::op_round` +#[pyfunction] +#[pyo3(name = "op_round", signature = (d, r))] +pub fn pyfn_op_round(d: f64, r: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::sdf::op_round(d, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Hollows a shape into a shell of the given thickness. +/// +/// Rust: `spatial::sdf::op_onion` +#[pyfunction] +#[pyo3(name = "op_onion", signature = (d, thickness))] +pub fn pyfn_op_onion(d: f64, thickness: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::sdf::op_onion(d, thickness)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Infinite domain repetition with the given period per axis +/// (returns the point folded into the central cell). +/// +/// Rust: `spatial::sdf::op_repeat` +#[pyfunction] +#[pyo3(name = "op_repeat", signature = (p, period))] +pub fn pyfn_op_repeat(p: crate::generated::types::PyVec3Arg, period: crate::generated::types::PyVec3Arg) -> PyResult { + let p = p.0; + let period = period.0; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::sdf::op_repeat(p, period)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Limited repetition: at most `count` cells either side per axis. +/// +/// Rust: `spatial::sdf::op_repeat_limited` +#[pyfunction] +#[pyo3(name = "op_repeat_limited", signature = (p, period, count))] +pub fn pyfn_op_repeat_limited(p: crate::generated::types::PyVec3Arg, period: crate::generated::types::PyVec3Arg, count: Vec) -> PyResult { + let p = p.0; + let period = period.0; + let count = <[i32; 3]>::try_from(count).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 3 values, got {}", __v.len())))?; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::sdf::op_repeat_limited(p, period, count)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Mirror the chosen axes (|x| fold). +/// +/// Rust: `spatial::sdf::op_mirror` +#[pyfunction] +#[pyo3(name = "op_mirror", signature = (p, axes))] +pub fn pyfn_op_mirror(p: crate::generated::types::PyVec3Arg, axes: Vec) -> PyResult { + let p = p.0; + let axes = <[bool; 3]>::try_from(axes).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 3 values, got {}", __v.len())))?; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::sdf::op_mirror(p, axes)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Twist about the y axis by k radians per unit height. +/// +/// Rust: `spatial::sdf::op_twist` +#[pyfunction] +#[pyo3(name = "op_twist", signature = (p, k))] +pub fn pyfn_op_twist(p: crate::generated::types::PyVec3Arg, k: f64) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::sdf::op_twist(p, k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Bend about the z axis with curvature k. +/// +/// Rust: `spatial::sdf::op_bend` +#[pyfunction] +#[pyo3(name = "op_bend", signature = (p, k))] +pub fn pyfn_op_bend(p: crate::generated::types::PyVec3Arg, k: f64) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::sdf::op_bend(p, k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Elongation: stretches the shape by clamping the sample point. +/// +/// Rust: `spatial::sdf::op_elongate` +#[pyfunction] +#[pyo3(name = "op_elongate", signature = (p, h))] +pub fn pyfn_op_elongate(p: crate::generated::types::PyVec3Arg, h: crate::generated::types::PyVec3Arg) -> PyResult { + let p = p.0; + let h = h.0; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::sdf::op_elongate(p, h)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Polar repetition: folds the plane into one of n angular sectors. +/// +/// Rust: `spatial::sdf::op_polar_repeat_2d` +#[pyfunction] +#[pyo3(name = "op_polar_repeat_2d", signature = (p, n))] +pub fn pyfn_op_polar_repeat_2d(p: crate::generated::types::PyVec2Arg, n: u32) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::sdf::op_polar_repeat_2d(p, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) +} + +/// Central-difference gradient normalized to a surface normal. +/// +/// Rust: `spatial::sdf::sdf_normal` +#[pyfunction] +#[pyo3(name = "sdf_normal", signature = (f, p, eps))] +pub fn pyfn_sdf_normal(f: pyo3::Py, p: crate::generated::types::PyVec3Arg, eps: f64) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: rust_physics_engine::math::Vec3| -> f64 { __cb.call::<_, f64>((crate::generated::types::PyVec3 { inner: __a0 },), f64::NAN) } }; + let p = p.0; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::sdf::sdf_normal(&f, p, eps)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) +} + +/// Sphere tracing: march the ray by the SDF value until |f| < eps. +/// +/// Rust: `spatial::sdf::sdf_raymarch` +#[pyfunction] +#[pyo3(name = "sdf_raymarch", signature = (f, r, max_dist, eps, max_steps))] +pub fn pyfn_sdf_raymarch(f: pyo3::Py, r: crate::generated::types::PyRay, max_dist: f64, eps: f64, max_steps: usize) -> PyResult> { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: rust_physics_engine::math::Vec3| -> f64 { __cb.call::<_, f64>((crate::generated::types::PyVec3 { inner: __a0 },), f64::NAN) } }; + let r = r.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::sdf::sdf_raymarch(&f, &r, max_dist, eps, max_steps)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyIntersectRayHit { inner: __x })) +} + +/// Samples the SDF on a regular grid (x-fastest order: +/// `data[k*ny*nx + j*nx + i]`), suitable for marching cubes. +/// +/// Panics: +/// Panics if any resolution is < 2. +/// +/// Rust: `spatial::sdf::sdf_to_grid` +#[pyfunction] +#[pyo3(name = "sdf_to_grid", signature = (f, bounds, res))] +pub fn pyfn_sdf_to_grid(f: pyo3::Py, bounds: crate::generated::types::PyAabb, res: Vec) -> PyResult> { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: rust_physics_engine::math::Vec3| -> f64 { __cb.call::<_, f64>((crate::generated::types::PyVec3 { inner: __a0 },), f64::NAN) } }; + let bounds = bounds.inner; + let res = <[usize; 3]>::try_from(res).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 3 values, got {}", __v.len())))?; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::sdf::sdf_to_grid(&f, &bounds, res)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// 2-D grid sampling (row-major, `data[j*nx + i]`). +/// +/// Panics: +/// Panics if any resolution is < 2. +/// +/// Rust: `spatial::sdf::sdf_to_grid_2d` +#[pyfunction] +#[pyo3(name = "sdf_to_grid_2d", signature = (f, bounds, res))] +pub fn pyfn_sdf_to_grid_2d(f: pyo3::Py, bounds: crate::generated::types::PyRect, res: Vec) -> PyResult> { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: rust_physics_engine::math::Vec2| -> f64 { __cb.call::<_, f64>((crate::generated::types::PyVec2 { inner: __a0 },), f64::NAN) } }; + let bounds = bounds.inner; + let res = <[usize; 2]>::try_from(res).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 2 values, got {}", __v.len())))?; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::sdf::sdf_to_grid_2d(&f, &bounds, res)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Screen-space-style ambient occlusion: samples the SDF along the +/// normal; 1 = fully open, 0 = fully occluded. +/// +/// Rust: `spatial::sdf::sdf_ambient_occlusion` +#[pyfunction] +#[pyo3(name = "sdf_ambient_occlusion", signature = (f, p, n, steps, step_size))] +pub fn pyfn_sdf_ambient_occlusion(f: pyo3::Py, p: crate::generated::types::PyVec3Arg, n: crate::generated::types::PyVec3Arg, steps: usize, step_size: f64) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: rust_physics_engine::math::Vec3| -> f64 { __cb.call::<_, f64>((crate::generated::types::PyVec3 { inner: __a0 },), f64::NAN) } }; + let p = p.0; + let n = n.0; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::sdf::sdf_ambient_occlusion(&f, p, n, steps, step_size)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// IQ soft shadow: marches from `origin` along `dir` and darkens by the +/// closest approach scaled by k (larger k = harder shadow). Returns a +/// factor in [0, 1]. +/// +/// Rust: `spatial::sdf::sdf_soft_shadow` +#[pyfunction] +#[pyo3(name = "sdf_soft_shadow", signature = (f, origin, dir, k))] +pub fn pyfn_sdf_soft_shadow(f: pyo3::Py, origin: crate::generated::types::PyVec3Arg, dir: crate::generated::types::PyVec3Arg, k: f64) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: rust_physics_engine::math::Vec3| -> f64 { __cb.call::<_, f64>((crate::generated::types::PyVec3 { inner: __a0 },), f64::NAN) } }; + let origin = origin.0; + let dir = dir.0; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::sdf::sdf_soft_shadow(&f, origin, dir, k)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_sd_sphere, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sd_box, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sd_rounded_box, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sd_torus, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sd_capsule, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sd_cylinder, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sd_cone, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sd_plane, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sd_ellipsoid, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sd_octahedron, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sd_circle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sd_rect, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sd_segment_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sd_polygon_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sd_hexagon, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sd_star, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_op_union, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_op_subtract, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_op_intersect, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_op_smooth_union, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_op_smooth_subtract, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_op_smooth_intersect, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_op_round, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_op_onion, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_op_repeat, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_op_repeat_limited, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_op_mirror, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_op_twist, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_op_bend, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_op_elongate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_op_polar_repeat_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sdf_normal, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sdf_raymarch, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sdf_to_grid, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sdf_to_grid_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sdf_ambient_occlusion, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sdf_soft_shadow, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_spatial__transform2d.rs b/bindings/python/src/generated/m_spatial__transform2d.rs new file mode 100644 index 0000000..339e59c --- /dev/null +++ b/bindings/python/src/generated/m_spatial__transform2d.rs @@ -0,0 +1,23 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_special.rs b/bindings/python/src/generated/m_special.rs new file mode 100644 index 0000000..c02bbf8 --- /dev/null +++ b/bindings/python/src/generated/m_special.rs @@ -0,0 +1,22 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + Ok(()) +} diff --git a/bindings/python/src/generated/m_special__bessel.rs b/bindings/python/src/generated/m_special__bessel.rs new file mode 100644 index 0000000..d1a9157 --- /dev/null +++ b/bindings/python/src/generated/m_special__bessel.rs @@ -0,0 +1,187 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Bessel function of the first kind, order 0: J₀(x). +/// +/// Rust: `special::bessel::bessel_j0` +#[pyfunction] +#[pyo3(name = "bessel_j0", signature = (x))] +pub fn pyfn_bessel_j0(x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::special::bessel::bessel_j0(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Bessel function of the first kind, order 1: J₁(x). +/// +/// Rust: `special::bessel::bessel_j1` +#[pyfunction] +#[pyo3(name = "bessel_j1", signature = (x))] +pub fn pyfn_bessel_j1(x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::special::bessel::bessel_j1(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Bessel function of the first kind, integer order n: Jₙ(x). +/// Upward recurrence for x > n; Miller's downward algorithm otherwise. +/// +/// Rust: `special::bessel::bessel_jn` +#[pyfunction] +#[pyo3(name = "bessel_jn", signature = (n, x))] +pub fn pyfn_bessel_jn(n: u32, x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::special::bessel::bessel_jn(n, x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Bessel function of the second kind, order 0: Y₀(x). +/// +/// Panics: +/// Panics unless x > 0. +/// +/// Rust: `special::bessel::bessel_y0` +#[pyfunction] +#[pyo3(name = "bessel_y0", signature = (x))] +pub fn pyfn_bessel_y0(x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::special::bessel::bessel_y0(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Bessel function of the second kind, order 1: Y₁(x). +/// +/// Panics: +/// Panics unless x > 0. +/// +/// Rust: `special::bessel::bessel_y1` +#[pyfunction] +#[pyo3(name = "bessel_y1", signature = (x))] +pub fn pyfn_bessel_y1(x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::special::bessel::bessel_y1(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Bessel function of the second kind, integer order n: Yₙ(x), by +/// stable upward recurrence. +/// +/// Panics: +/// Panics unless x > 0. +/// +/// Rust: `special::bessel::bessel_yn` +#[pyfunction] +#[pyo3(name = "bessel_yn", signature = (n, x))] +pub fn pyfn_bessel_yn(n: u32, x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::special::bessel::bessel_yn(n, x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Modified Bessel function of the first kind, order 0: I₀(x). +/// +/// Rust: `special::bessel::bessel_i0` +#[pyfunction] +#[pyo3(name = "bessel_i0", signature = (x))] +pub fn pyfn_bessel_i0(x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::special::bessel::bessel_i0(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Modified Bessel function of the first kind, order 1: I₁(x). +/// +/// Rust: `special::bessel::bessel_i1` +#[pyfunction] +#[pyo3(name = "bessel_i1", signature = (x))] +pub fn pyfn_bessel_i1(x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::special::bessel::bessel_i1(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Modified Bessel function of the second kind, order 0: K₀(x). +/// +/// Panics: +/// Panics unless x > 0. +/// +/// Rust: `special::bessel::bessel_k0` +#[pyfunction] +#[pyo3(name = "bessel_k0", signature = (x))] +pub fn pyfn_bessel_k0(x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::special::bessel::bessel_k0(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Modified Bessel function of the second kind, order 1: K₁(x). +/// +/// Panics: +/// Panics unless x > 0. +/// +/// Rust: `special::bessel::bessel_k1` +#[pyfunction] +#[pyo3(name = "bessel_k1", signature = (x))] +pub fn pyfn_bessel_k1(x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::special::bessel::bessel_k1(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// First `count` positive zeros of Jₙ, found by scanning for sign +/// changes (step π/4 starting past x = n) and refining each bracket +/// with Brent's method. +/// +/// Rust: `special::bessel::bessel_j_zeros` +#[pyfunction] +#[pyo3(name = "bessel_j_zeros", signature = (n, count))] +pub fn pyfn_bessel_j_zeros<'py>(py: Python<'py>, n: u32, count: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::special::bessel::bessel_j_zeros(n, count))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convenience alias family used by some references: Jₙ zeros with a +/// `Result` wrapper for invalid counts. +/// +/// Rust: `special::bessel::bessel_j_zeros_checked` +#[pyfunction] +#[pyo3(name = "bessel_j_zeros_checked", signature = (n, count))] +pub fn pyfn_bessel_j_zeros_checked<'py>(py: Python<'py>, n: u32, count: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::special::bessel::bessel_j_zeros_checked(n, count))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_bessel_j0, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bessel_j1, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bessel_jn, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bessel_y0, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bessel_y1, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bessel_yn, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bessel_i0, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bessel_i1, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bessel_k0, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bessel_k1, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bessel_j_zeros, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bessel_j_zeros_checked, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_special__beta.rs b/bindings/python/src/generated/m_special__beta.rs new file mode 100644 index 0000000..8c71188 --- /dev/null +++ b/bindings/python/src/generated/m_special__beta.rs @@ -0,0 +1,53 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Complete beta function B(a,b) = Γ(a)Γ(b)/Γ(a+b). +/// +/// Panics: +/// Panics unless a > 0 and b > 0. +/// +/// Rust: `special::beta::beta` +#[pyfunction] +#[pyo3(name = "beta", signature = (a, b))] +pub fn pyfn_beta(a: f64, b: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::special::beta::beta(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Regularized incomplete beta function I_x(a,b), the CDF of the Beta +/// distribution. +/// +/// Panics: +/// Panics unless a > 0, b > 0, and x ∈ [0, 1]. +/// +/// Rust: `special::beta::beta_inc` +#[pyfunction] +#[pyo3(name = "beta_inc", signature = (a, b, x))] +pub fn pyfn_beta_inc(a: f64, b: f64, x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::special::beta::beta_inc(a, b, x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_beta, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_beta_inc, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_special__elliptic.rs b/bindings/python/src/generated/m_special__elliptic.rs new file mode 100644 index 0000000..eaa3fd1 --- /dev/null +++ b/bindings/python/src/generated/m_special__elliptic.rs @@ -0,0 +1,138 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Complete elliptic integral of the first kind K(m), parameter m = k²: +/// K(m) = ∫₀^{π/2} dθ/√(1 − m·sin²θ) = π / (2·AGM(1, √(1−m))). +/// +/// Panics: +/// Panics unless 0 ≤ m < 1. +/// +/// Rust: `special::elliptic::elliptic_k` +#[pyfunction] +#[pyo3(name = "elliptic_k", signature = (m))] +pub fn pyfn_elliptic_k(m: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::special::elliptic::elliptic_k(m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Complete elliptic integral of the second kind E(m), parameter m = k²: +/// E(m) = ∫₀^{π/2} √(1 − m·sin²θ) dθ, via the AGM with the +/// c²-correction sum: E = K·(1 − Σ 2^{n−1}·cₙ²). +/// +/// Panics: +/// Panics unless 0 ≤ m ≤ 1 (E(1) = 1 exactly). +/// +/// Rust: `special::elliptic::elliptic_e` +#[pyfunction] +#[pyo3(name = "elliptic_e", signature = (m))] +pub fn pyfn_elliptic_e(m: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::special::elliptic::elliptic_e(m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Incomplete elliptic integral of the first kind F(φ | m) via Carlson +/// R_F: F = sinφ·R_F(cos²φ, 1 − m·sin²φ, 1). +/// +/// Panics: +/// Panics unless 0 ≤ φ ≤ π/2 and m·sin²φ < 1. +/// +/// Rust: `special::elliptic::elliptic_f` +#[pyfunction] +#[pyo3(name = "elliptic_f", signature = (phi, m))] +pub fn pyfn_elliptic_f(phi: f64, m: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::special::elliptic::elliptic_f(phi, m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Incomplete elliptic integral of the second kind E(φ | m) via Carlson +/// forms: E = sinφ·R_F − (m/3)·sin³φ·R_D. +/// +/// Panics: +/// Panics unless 0 ≤ φ ≤ π/2 and m·sin²φ < 1. +/// +/// Rust: `special::elliptic::elliptic_e_inc` +#[pyfunction] +#[pyo3(name = "elliptic_e_inc", signature = (phi, m))] +pub fn pyfn_elliptic_e_inc(phi: f64, m: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::special::elliptic::elliptic_e_inc(phi, m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Exact large-amplitude pendulum period: +/// T = 4·√(L/g)·K(sin²(θ₀/2)). +/// +/// Reduces to 2π√(L/g) as the amplitude → 0. Fails with +/// `InvalidArgument` for non-positive length/gravity or amplitude +/// outside [0, π). +/// +/// Rust: `special::elliptic::pendulum_period_exact` +#[pyfunction] +#[pyo3(name = "pendulum_period_exact", signature = (length, g, amplitude_rad))] +pub fn pyfn_pendulum_period_exact(length: f64, g: f64, amplitude_rad: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::special::elliptic::pendulum_period_exact(length, g, amplitude_rad)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) +} + +/// Exact ellipse perimeter: P = 4·a·E(m) with m = 1 − (b/a)² for +/// a ≥ b (arguments may be given in either order). +/// +/// Panics: +/// Panics unless both semi-axes are positive. +/// +/// Rust: `special::elliptic::ellipse_perimeter_exact` +#[pyfunction] +#[pyo3(name = "ellipse_perimeter_exact", signature = (a, b))] +pub fn pyfn_ellipse_perimeter_exact(a: f64, b: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::special::elliptic::ellipse_perimeter_exact(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Jacobi elliptic functions (sn, cn, dn) of real argument u with +/// parameter m = k², by the descending Gauss/AGM transformation +/// (Abramowitz & Stegun §16.4). +/// +/// Panics: +/// Panics unless 0 ≤ m ≤ 1. +/// +/// Rust: `special::elliptic::jacobi_elliptic` +#[pyfunction] +#[pyo3(name = "jacobi_elliptic", signature = (u, m))] +pub fn pyfn_jacobi_elliptic(u: f64, m: f64) -> PyResult<(f64, f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::special::elliptic::jacobi_elliptic(u, m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_elliptic_k, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_elliptic_e, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_elliptic_f, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_elliptic_e_inc, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pendulum_period_exact, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ellipse_perimeter_exact, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_jacobi_elliptic, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_special__erf.rs b/bindings/python/src/generated/m_special__erf.rs new file mode 100644 index 0000000..5ba7a0c --- /dev/null +++ b/bindings/python/src/generated/m_special__erf.rs @@ -0,0 +1,61 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Error function erf(x) = (2/√π)·∫₀ˣ e^(−t²) dt, full double precision. +/// +/// Rust: `special::erf::erf` +#[pyfunction] +#[pyo3(name = "erf", signature = (x))] +pub fn pyfn_erf(x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::special::erf::erf(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Complementary error function erfc(x) = 1 − erf(x), computed without +/// cancellation for large positive x. +/// +/// Rust: `special::erf::erfc` +#[pyfunction] +#[pyo3(name = "erfc", signature = (x))] +pub fn pyfn_erfc(x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::special::erf::erfc(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Inverse error function: erfinv(erf(x)) = x for p ∈ (−1, 1). +/// +/// Returns ±∞ at p = ±1 and NaN outside [−1, 1]. +/// +/// Rust: `special::erf::erfinv` +#[pyfunction] +#[pyo3(name = "erfinv", signature = (p))] +pub fn pyfn_erfinv(p: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::special::erf::erfinv(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_erf, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_erfc, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_erfinv, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_special__expint.rs b/bindings/python/src/generated/m_special__expint.rs new file mode 100644 index 0000000..04be921 --- /dev/null +++ b/bindings/python/src/generated/m_special__expint.rs @@ -0,0 +1,51 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Exponential integral E1(x) = ∫_x^∞ e^{-t}/t dt for x > 0. +/// +/// Power series for x ≤ 1, continued fraction (modified Lentz) for x > 1. +/// Returns infinity at x = 0 and NaN for x < 0. +/// +/// Rust: `special::expint::e1` +#[pyfunction] +#[pyo3(name = "e1", signature = (x))] +pub fn pyfn_e1(x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::special::expint::e1(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Exponential integral Ei(x) (Cauchy principal value for x > 0). +/// +/// For x < 0, Ei(x) = -E1(-x). Returns -infinity at x = 0. +/// +/// Rust: `special::expint::exponential_integral` +#[pyfunction] +#[pyo3(name = "exponential_integral", signature = (x))] +pub fn pyfn_exponential_integral(x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::special::expint::exponential_integral(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_e1, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_exponential_integral, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_special__gamma.rs b/bindings/python/src/generated/m_special__gamma.rs new file mode 100644 index 0000000..afb80b3 --- /dev/null +++ b/bindings/python/src/generated/m_special__gamma.rs @@ -0,0 +1,83 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Gamma function Γ(z) via the Lanczos approximation, with the +/// reflection formula for z < 0.5. +/// +/// Rust: `special::gamma::gamma` +#[pyfunction] +#[pyo3(name = "gamma", signature = (z))] +pub fn pyfn_gamma(z: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::special::gamma::gamma(z)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Natural log of |Γ(z)|, computed in log space so it stays finite up to +/// z ≈ 1e300 (e.g. `lgamma(1e6)` is exact to ~1e-13 relative). +/// +/// Panics: +/// Panics for z ≤ 0 (poles and the reflection region are out of scope +/// for the real-valued solvers this supports). +/// +/// Rust: `special::gamma::lgamma` +#[pyfunction] +#[pyo3(name = "lgamma", signature = (z))] +pub fn pyfn_lgamma(z: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::special::gamma::lgamma(z)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Regularized lower incomplete gamma P(a,x) = γ(a,x)/Γ(a). +/// +/// Panics: +/// Panics unless a > 0 and x ≥ 0. +/// +/// Rust: `special::gamma::gamma_p` +#[pyfunction] +#[pyo3(name = "gamma_p", signature = (a, x))] +pub fn pyfn_gamma_p(a: f64, x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::special::gamma::gamma_p(a, x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Regularized upper incomplete gamma Q(a,x) = 1 − P(a,x), computed by +/// continued fraction for x > a + 1 so large-x values keep precision. +/// +/// Panics: +/// Panics unless a > 0 and x ≥ 0. +/// +/// Rust: `special::gamma::gamma_q` +#[pyfunction] +#[pyo3(name = "gamma_q", signature = (a, x))] +pub fn pyfn_gamma_q(a: f64, x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::special::gamma::gamma_q(a, x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_gamma, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lgamma, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gamma_p, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gamma_q, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_special__legendre.rs b/bindings/python/src/generated/m_special__legendre.rs new file mode 100644 index 0000000..27e4797 --- /dev/null +++ b/bindings/python/src/generated/m_special__legendre.rs @@ -0,0 +1,85 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Legendre polynomial Pₙ(x) by the Bonnet recurrence +/// (n+1)·P_{n+1} = (2n+1)·x·Pₙ − n·P_{n−1}. +/// +/// Rust: `special::legendre::legendre_p` +#[pyfunction] +#[pyo3(name = "legendre_p", signature = (n, x))] +pub fn pyfn_legendre_p(n: u32, x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::special::legendre::legendre_p(n, x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Associated Legendre function Pₗᵐ(x) with the Condon-Shortley phase, +/// for |x| ≤ 1. Negative m uses +/// Pₗ^{−m} = (−1)ᵐ (l−m)!/(l+m)! Pₗᵐ. +/// +/// Panics: +/// Panics unless |m| ≤ l and |x| ≤ 1. +/// +/// Rust: `special::legendre::legendre_p_assoc` +#[pyfunction] +#[pyo3(name = "legendre_p_assoc", signature = (l, m, x))] +pub fn pyfn_legendre_p_assoc(l: u32, m: i32, x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::special::legendre::legendre_p_assoc(l, m, x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Real spherical harmonic Yₗₘ(θ, φ) (orthonormal on the sphere): +/// m > 0 pairs with cos(mφ), m < 0 with sin(|m|φ). +/// +/// Panics: +/// Panics unless |m| ≤ l. +/// +/// Rust: `special::legendre::spherical_harmonic_real` +#[pyfunction] +#[pyo3(name = "spherical_harmonic_real", signature = (l, m, theta, phi))] +pub fn pyfn_spherical_harmonic_real(l: u32, m: i32, theta: f64, phi: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::special::legendre::spherical_harmonic_real(l, m, theta, phi)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Nodes and weights of the n-point Gauss-Legendre rule on [−1, 1] +/// (NR `gauleg`: Newton iteration on Pₙ from the Chebyshev-like initial +/// guess). Integrates polynomials up to degree 2n−1 exactly. +/// +/// Panics: +/// Panics if n = 0. +/// +/// Rust: `special::legendre::gauss_legendre_nodes` +#[pyfunction] +#[pyo3(name = "gauss_legendre_nodes", signature = (n))] +pub fn pyfn_gauss_legendre_nodes(n: usize) -> PyResult<(Vec, Vec)> { + let __r = crate::runtime::guard(|| rust_physics_engine::special::legendre::gauss_legendre_nodes(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_legendre_p, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_legendre_p_assoc, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spherical_harmonic_real, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gauss_legendre_nodes, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_statistical_mechanics.rs b/bindings/python/src/generated/m_statistical_mechanics.rs new file mode 100644 index 0000000..892a6c5 --- /dev/null +++ b/bindings/python/src/generated/m_statistical_mechanics.rs @@ -0,0 +1,279 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Stokes-Einstein diffusion coefficient: D = k_B × T / (6π × μ × r) +/// +/// Rust: `statistical_mechanics::einstein_diffusion` +#[pyfunction] +#[pyo3(name = "einstein_diffusion", signature = (temperature, dynamic_viscosity, particle_radius))] +pub fn pyfn_einstein_diffusion(temperature: f64, dynamic_viscosity: f64, particle_radius: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::einstein_diffusion(temperature, dynamic_viscosity, particle_radius)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Mean square displacement: ⟨r²⟩ = 2nDt where n = number of spatial dimensions +/// +/// Rust: `statistical_mechanics::mean_square_displacement` +#[pyfunction] +#[pyo3(name = "mean_square_displacement", signature = (diffusion_coeff, time, dimensions))] +pub fn pyfn_mean_square_displacement(diffusion_coeff: f64, time: f64, dimensions: u32) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::mean_square_displacement(diffusion_coeff, time, dimensions)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Root-mean-square displacement: √(⟨r²⟩) +/// +/// Rust: `statistical_mechanics::rms_displacement` +#[pyfunction] +#[pyo3(name = "rms_displacement", signature = (diffusion_coeff, time, dimensions))] +pub fn pyfn_rms_displacement(diffusion_coeff: f64, time: f64, dimensions: u32) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::rms_displacement(diffusion_coeff, time, dimensions)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Fick's first law: J = -D × (dc/dx) +/// +/// Rust: `statistical_mechanics::fick_first_law` +#[pyfunction] +#[pyo3(name = "fick_first_law", signature = (diffusion_coeff, concentration_gradient))] +pub fn pyfn_fick_first_law(diffusion_coeff: f64, concentration_gradient: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::fick_first_law(diffusion_coeff, concentration_gradient)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Fick's second law via explicit finite-difference step in 1D. +/// Updates `concentrations` in place. Boundary cells (first and last) are held fixed. +/// +/// Rust: `statistical_mechanics::fick_second_law_step_1d` +#[pyfunction] +#[pyo3(name = "fick_second_law_step_1d", signature = (concentrations, dx, dt, diffusion_coeff))] +pub fn pyfn_fick_second_law_step_1d<'py>(concentrations: pyo3::Bound<'py, pyo3::PyAny>, dx: f64, dt: f64, diffusion_coeff: f64) -> PyResult<()> { + let mut concentrations__v: Vec = concentrations.extract()?; + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::fick_second_law_step_1d(&mut concentrations__v, dx, dt, diffusion_coeff)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + crate::runtime::coerce::write_back(&concentrations, &concentrations__v)?; + Ok(()) +} + +/// Characteristic diffusion length: L = √(2Dt) +/// +/// Rust: `statistical_mechanics::diffusion_length` +#[pyfunction] +#[pyo3(name = "diffusion_length", signature = (diffusion_coeff, time))] +pub fn pyfn_diffusion_length(diffusion_coeff: f64, time: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::diffusion_length(diffusion_coeff, time)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Time required to diffuse a given length: t = L² / (2D) +/// +/// Rust: `statistical_mechanics::diffusion_time` +#[pyfunction] +#[pyo3(name = "diffusion_time", signature = (diffusion_coeff, length))] +pub fn pyfn_diffusion_time(diffusion_coeff: f64, length: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::diffusion_time(diffusion_coeff, length)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Maxwell speed distribution: f(v) = 4π × (m/(2πk_BT))^(3/2) × v² × exp(-mv²/(2k_BT)) +/// +/// Rust: `statistical_mechanics::maxwell_speed_distribution` +#[pyfunction] +#[pyo3(name = "maxwell_speed_distribution", signature = (mass, temperature, speed))] +pub fn pyfn_maxwell_speed_distribution(mass: f64, temperature: f64, speed: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::maxwell_speed_distribution(mass, temperature, speed)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Most probable speed: v_p = √(2k_BT / m) +/// +/// Rust: `statistical_mechanics::most_probable_speed` +#[pyfunction] +#[pyo3(name = "most_probable_speed", signature = (mass, temperature))] +pub fn pyfn_most_probable_speed(mass: f64, temperature: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::most_probable_speed(mass, temperature)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Mean speed: v̄ = √(8k_BT / (πm)) +/// +/// Rust: `statistical_mechanics::mean_speed` +#[pyfunction] +#[pyo3(name = "mean_speed", signature = (mass, temperature))] +pub fn pyfn_mean_speed(mass: f64, temperature: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::mean_speed(mass, temperature)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// RMS speed from Maxwell-Boltzmann: v_rms = √(3k_BT / m) +/// +/// Rust: `statistical_mechanics::rms_speed_maxwell` +#[pyfunction] +#[pyo3(name = "rms_speed_maxwell", signature = (mass, temperature))] +pub fn pyfn_rms_speed_maxwell(mass: f64, temperature: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::rms_speed_maxwell(mass, temperature)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Equipartition energy: E = (f/2) × k_B × T +/// +/// Rust: `statistical_mechanics::equipartition_energy` +#[pyfunction] +#[pyo3(name = "equipartition_energy", signature = (degrees_of_freedom, temperature))] +pub fn pyfn_equipartition_energy(degrees_of_freedom: u32, temperature: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::equipartition_energy(degrees_of_freedom, temperature)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Equipartition heat capacity per particle: Cv = (f/2) × k_B +/// +/// Rust: `statistical_mechanics::equipartition_heat_capacity` +#[pyfunction] +#[pyo3(name = "equipartition_heat_capacity", signature = (degrees_of_freedom))] +pub fn pyfn_equipartition_heat_capacity(degrees_of_freedom: u32) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::equipartition_heat_capacity(degrees_of_freedom)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Boltzmann factor: exp(-E / (k_B × T)) +/// +/// Rust: `statistical_mechanics::boltzmann_factor` +#[pyfunction] +#[pyo3(name = "boltzmann_factor", signature = (energy, temperature))] +pub fn pyfn_boltzmann_factor(energy: f64, temperature: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::boltzmann_factor(energy, temperature)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Boltzmann probability: P = exp(-E/(k_BT)) / Z +/// +/// Rust: `statistical_mechanics::boltzmann_probability` +#[pyfunction] +#[pyo3(name = "boltzmann_probability", signature = (energy, temperature, partition_function))] +pub fn pyfn_boltzmann_probability(energy: f64, temperature: f64, partition_function: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::boltzmann_probability(energy, temperature, partition_function)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Partition function for a quantum harmonic oscillator: Z = 1 / (1 - exp(-hf/(k_BT))) +/// +/// Rust: `statistical_mechanics::partition_function_harmonic` +#[pyfunction] +#[pyo3(name = "partition_function_harmonic", signature = (temperature, frequency))] +pub fn pyfn_partition_function_harmonic(temperature: f64, frequency: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::partition_function_harmonic(temperature, frequency)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Mean energy of a quantum harmonic oscillator (includes zero-point energy): +/// ⟨E⟩ = hf / (exp(hf/(k_BT)) - 1) + hf/2 +/// +/// Rust: `statistical_mechanics::mean_energy_harmonic` +#[pyfunction] +#[pyo3(name = "mean_energy_harmonic", signature = (temperature, frequency))] +pub fn pyfn_mean_energy_harmonic(temperature: f64, frequency: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::mean_energy_harmonic(temperature, frequency)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Debye temperature: Θ_D = h × f_max / k_B +/// +/// Rust: `statistical_mechanics::debye_temperature` +#[pyfunction] +#[pyo3(name = "debye_temperature", signature = (max_frequency))] +pub fn pyfn_debye_temperature(max_frequency: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::debye_temperature(max_frequency)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Dulong-Petit limit (high-T Debye heat capacity): Cv = 3Nk_B +/// +/// Rust: `statistical_mechanics::debye_heat_capacity_high_t` +#[pyfunction] +#[pyo3(name = "debye_heat_capacity_high_t", signature = (n_atoms))] +pub fn pyfn_debye_heat_capacity_high_t(n_atoms: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::debye_heat_capacity_high_t(n_atoms)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Low-temperature Debye heat capacity: Cv = (12/5)π⁴Nk_B(T/Θ_D)³ +/// +/// Rust: `statistical_mechanics::debye_heat_capacity_low_t` +#[pyfunction] +#[pyo3(name = "debye_heat_capacity_low_t", signature = (n_atoms, temperature, debye_temp))] +pub fn pyfn_debye_heat_capacity_low_t(n_atoms: f64, temperature: f64, debye_temp: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::debye_heat_capacity_low_t(n_atoms, temperature, debye_temp)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Einstein model heat capacity: +/// Cv = 3Nk_B × (Θ_E/T)² × exp(Θ_E/T) / (exp(Θ_E/T) - 1)² +/// +/// Rust: `statistical_mechanics::einstein_heat_capacity` +#[pyfunction] +#[pyo3(name = "einstein_heat_capacity", signature = (n_atoms, temperature, einstein_temp))] +pub fn pyfn_einstein_heat_capacity(n_atoms: f64, temperature: f64, einstein_temp: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::einstein_heat_capacity(n_atoms, temperature, einstein_temp)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_einstein_diffusion, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mean_square_displacement, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rms_displacement, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fick_first_law, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fick_second_law_step_1d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_diffusion_length, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_diffusion_time, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_maxwell_speed_distribution, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_most_probable_speed, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mean_speed, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rms_speed_maxwell, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_equipartition_energy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_equipartition_heat_capacity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_boltzmann_factor, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_boltzmann_probability, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_partition_function_harmonic, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mean_energy_harmonic, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_debye_temperature, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_debye_heat_capacity_high_t, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_debye_heat_capacity_low_t, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_einstein_heat_capacity, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_statistical_mechanics__ising.rs b/bindings/python/src/generated/m_statistical_mechanics__ising.rs new file mode 100644 index 0000000..059f557 --- /dev/null +++ b/bindings/python/src/generated/m_statistical_mechanics__ising.rs @@ -0,0 +1,306 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// The exact critical temperature of the two-dimensional Ising model: +/// `2 / ln(1 + sqrt 2)`. +/// +/// About 2.269. Kramers and Wannier found it from a duality argument years +/// before Onsager solved the model, without ever computing the free energy -- +/// the self-dual point has to be the transition if there is only one. +/// +/// Rust: `statistical_mechanics::ising::ising_tc_exact` +#[pyfunction] +#[pyo3(name = "ising_tc_exact", signature = ())] +pub fn pyfn_ising_tc_exact() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::ising::ising_tc_exact()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Onsager's spontaneous magnetisation, zero above the critical temperature. +/// +/// `(1 - sinh^-4(2 beta j))^(1/8)`. The exponent one eighth is the critical +/// exponent beta, and its being a simple fraction rather than the one half +/// that mean-field theory predicts is the whole reason the exact solution +/// mattered. +/// +/// Errors: +/// Returns an error for a non-positive coupling or inverse temperature. +/// +/// Rust: `statistical_mechanics::ising::onsager_magnetization` +#[pyfunction] +#[pyo3(name = "onsager_magnetization", signature = (beta, j))] +pub fn pyfn_onsager_magnetization(beta: f64, j: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::ising::onsager_magnetization(beta, j)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Onsager's energy per site of the infinite lattice. +/// +/// Involves a complete elliptic integral, which is where the logarithmic +/// divergence of the heat capacity at the critical point comes from: the +/// integral's derivative diverges exactly at the self-dual point. +/// +/// Errors: +/// Returns an error for a non-positive coupling or inverse temperature. +/// +/// Rust: `statistical_mechanics::ising::onsager_energy` +#[pyfunction] +#[pyo3(name = "onsager_energy", signature = (beta, j))] +pub fn pyfn_onsager_energy(beta: f64, j: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::ising::onsager_energy(beta, j)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The one-dimensional Ising chain by transfer matrix, returning the free +/// energy per site and the magnetisation per site. +/// +/// The chain has no transition at any positive temperature, which is Ising's +/// own result and the reason he thought the model uninteresting. The transfer +/// matrix shows why: the free energy is the logarithm of the larger +/// eigenvalue of a two-by-two matrix with strictly positive entries, and such +/// an eigenvalue is analytic in the temperature. +/// +/// Errors: +/// Returns an error for a non-positive inverse temperature. +/// +/// Rust: `statistical_mechanics::ising::ising_1d_exact` +#[pyfunction] +#[pyo3(name = "ising_1d_exact", signature = (beta, j, h))] +pub fn pyfn_ising_1d_exact(beta: f64, j: f64, h: f64) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::ising::ising_1d_exact(beta, j, h)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// The partition function of a small system by direct enumeration. +/// +/// Exponential in the site count, so it stops at about twenty-four spins -- +/// but within that range it is exact, which makes it the reference every +/// sampler here is checked against. +/// +/// Errors: +/// Returns an error above twenty-four sites or for a non-positive beta. +/// +/// Rust: `statistical_mechanics::ising::partition_function_exact_small` +#[pyfunction] +#[pyo3(name = "partition_function_exact_small", signature = (energy, sites, beta))] +pub fn pyfn_partition_function_exact_small(energy: pyo3::Py, sites: usize, beta: f64) -> PyResult { + let __cb_energy = std::rc::Rc::new(crate::runtime::Callback::new(energy)); + let energy = { let __cb = __cb_energy.clone(); move |__a0: u64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::ising::partition_function_exact_small(&energy, sites, beta)); + crate::runtime::callback::check(&[&__cb_energy], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The free energy from a partition function. +/// +/// Errors: +/// Returns an error for a non-positive partition function or beta. +/// +/// Rust: `statistical_mechanics::ising::free_energy_from_z` +#[pyfunction] +#[pyo3(name = "free_energy_from_z", signature = (z, beta))] +pub fn pyfn_free_energy_from_z(z: f64, beta: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::ising::free_energy_from_z(z, beta)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The mean energy and entropy of a small system by enumeration. +/// +/// Errors: +/// Returns an error on the same conditions as +/// `partition_function_exact_small`. +/// +/// Rust: `statistical_mechanics::ising::thermodynamics_exact_small` +#[pyfunction] +#[pyo3(name = "thermodynamics_exact_small", signature = (energy, sites, beta))] +pub fn pyfn_thermodynamics_exact_small(energy: pyo3::Py, sites: usize, beta: f64) -> PyResult<(f64, f64)> { + let __cb_energy = std::rc::Rc::new(crate::runtime::Callback::new(energy)); + let energy = { let __cb = __cb_energy.clone(); move |__a0: u64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::ising::thermodynamics_exact_small(&energy, sites, beta)); + crate::runtime::callback::check(&[&__cb_energy], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// The exact critical temperature of the `q`-state Potts model in two +/// dimensions: `1 / ln(1 + sqrt q)`. +/// +/// Reduces to the Ising value at `q = 2`, as it must. +/// +/// Errors: +/// Returns an error for fewer than two states. +/// +/// Rust: `statistical_mechanics::ising::potts_tc_exact` +#[pyfunction] +#[pyo3(name = "potts_tc_exact", signature = (q))] +pub fn pyfn_potts_tc_exact(q: u8) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::ising::potts_tc_exact(q)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Wang-Landau sampling: the density of states as a function of energy. +/// +/// Rather than sampling the Boltzmann distribution at one temperature, this +/// performs a random walk in *energy* with acceptance `min(1, g(E_old) / +/// g(E_new))`, refining the estimate `g` as it goes so that the walk flattens +/// its own histogram. The result gives every temperature at once, which is +/// what a canonical simulation cannot do: it converges on the *entropy*, not +/// on an average. +/// +/// Returns the logarithm of the density of states, indexed by the energy +/// level offset from the minimum. +/// +/// Errors: +/// Returns an error for bad parameters or an energy range that does not fit. +/// +/// Rust: `statistical_mechanics::ising::wang_landau` +#[pyfunction] +#[pyo3(name = "wang_landau", signature = (energy, sites, flatness, final_modification, max_steps, rng))] +pub fn pyfn_wang_landau(energy: pyo3::Py, sites: usize, flatness: f64, final_modification: f64, max_steps: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let __cb_energy = std::rc::Rc::new(crate::runtime::Callback::new(energy)); + let energy = { let __cb = __cb_energy.clone(); move |__a0: u64| -> i64 { __cb.call::<_, i64>((__a0,), 0) } }; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::ising::wang_landau(&energy, sites, flatness, final_modification, max_steps, &mut rng.inner)); + crate::runtime::callback::check(&[&__cb_energy], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Canonical averages reconstructed from a density of states. +/// +/// The whole point of Wang-Landau: one run gives every temperature. Returns +/// the mean energy and the heat capacity at the given inverse temperature. +/// +/// Errors: +/// Returns an error for an empty density or a non-positive beta. +/// +/// Rust: `statistical_mechanics::ising::canonical_from_dos` +#[pyfunction] +#[pyo3(name = "canonical_from_dos", signature = (log_g, lowest_energy, step, beta))] +pub fn pyfn_canonical_from_dos<'py>(py: Python<'py>, log_g: Vec, lowest_energy: f64, step: f64, beta: f64) -> PyResult<(f64, f64)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistical_mechanics::ising::canonical_from_dos(&log_g, lowest_energy, step, beta))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// Parallel tempering: several replicas at different temperatures, with +/// neighbouring pairs occasionally swapped. +/// +/// The swap acceptance `min(1, exp((beta_i - beta_j)(E_i - E_j)))` preserves +/// each replica's own equilibrium distribution while letting a cold replica +/// escape a local minimum by wandering up to a hot temperature and back. It +/// is the standard answer to a rugged landscape, and it costs nothing in +/// correctness -- the swaps satisfy detailed balance on the joint system. +/// +/// Returns the statistics for each temperature and the swap acceptance rate. +/// +/// Errors: +/// Returns an error for fewer than two temperatures or bad sweep counts. +/// +/// Rust: `statistical_mechanics::ising::parallel_tempering_ising` +#[pyfunction] +#[pyo3(name = "parallel_tempering_ising", signature = (n, j, betas, sweeps, thermalize, rng))] +pub fn pyfn_parallel_tempering_ising(n: usize, j: f64, betas: Vec, sweeps: usize, thermalize: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(Vec, f64)> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::ising::parallel_tempering_ising(n, j, &betas, sweeps, thermalize, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0.into_iter().map(|__x| crate::generated::types::PyIsingStats { inner: __x }).collect::>(), __v.1)) +} + +/// The Binder crossing estimate of the critical temperature. +/// +/// The Binder cumulant is dimensionless, so its finite-size corrections +/// cancel at the critical point and curves for different lattice sizes cross +/// there. That makes it far more accurate than looking for a peak in the +/// susceptibility, whose position drifts with the size. +/// +/// `curves[i]` is the cumulant of lattice `sizes[i]` at each of the given +/// temperatures. +/// +/// Errors: +/// Returns an error for mismatched lengths or fewer than two sizes. +/// +/// Rust: `statistical_mechanics::ising::binder_crossing` +#[pyfunction] +#[pyo3(name = "binder_crossing", signature = (temperatures, curves))] +pub fn pyfn_binder_crossing<'py>(py: Python<'py>, temperatures: Vec, curves: Vec>) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistical_mechanics::ising::binder_crossing(&temperatures, &curves))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The fluctuation-dissipation check: the heat capacity computed from the +/// energy variance against the same quantity differentiated numerically. +/// +/// Returns the relative discrepancy. The identity `C = beta^2 Var(E)` is not +/// a modelling assumption but a consequence of the Boltzmann distribution, so +/// a sampler that violates it is not sampling that distribution. +/// +/// Errors: +/// Returns an error for a non-positive beta or a zero heat capacity. +/// +/// Rust: `statistical_mechanics::ising::fluctuation_dissipation_check` +#[pyfunction] +#[pyo3(name = "fluctuation_dissipation_check", signature = (stats, beta, sites))] +pub fn pyfn_fluctuation_dissipation_check(stats: crate::generated::types::PyIsingStats, beta: f64, sites: usize) -> PyResult { + let stats = stats.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::ising::fluctuation_dissipation_check(&stats, beta, sites)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_ising_tc_exact, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_onsager_magnetization, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_onsager_energy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ising_1d_exact, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_partition_function_exact_small, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_free_energy_from_z, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_thermodynamics_exact_small, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_potts_tc_exact, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wang_landau, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_canonical_from_dos, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_parallel_tempering_ising, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_binder_crossing, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fluctuation_dissipation_check, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_statistical_mechanics__kinetics.rs b/bindings/python/src/generated/m_statistical_mechanics__kinetics.rs new file mode 100644 index 0000000..97fe4e1 --- /dev/null +++ b/bindings/python/src/generated/m_statistical_mechanics__kinetics.rs @@ -0,0 +1,861 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// The stoichiometry matrix: species by reaction, each entry the net change +/// in that species when that reaction fires once. +/// +/// Errors: +/// Returns an error for no reactions, no species, or a species index outside +/// the declared count. +/// +/// Rust: `statistical_mechanics::kinetics::stoichiometry_matrix` +#[pyfunction] +#[pyo3(name = "stoichiometry_matrix", signature = (reactions, species))] +pub fn pyfn_stoichiometry_matrix(reactions: Vec, species: usize) -> PyResult { + let reactions = reactions.into_iter().map(|__e| __e.inner).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::kinetics::stoichiometry_matrix(&reactions, species)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) +} + +/// The deterministic mass-action rate of each reaction at a composition. +/// +/// `v_j = k_j prod_i c_i^m_ij`. Note the contrast with the stochastic +/// propensity in `gillespie_ssa`, which uses a falling factorial rather +/// than a power: a bimolecular reaction of a species with itself has rate +/// `k c^2` in the continuum and `k x (x - 1) / 2` in molecule counts, and +/// the two agree only when the count is large. Conflating them is the +/// classic way to get a stochastic simulation that quietly disagrees with +/// its own rate equations. +/// +/// Errors: +/// Returns an error for a rate constant per reaction mismatch, a negative +/// rate constant, or a species index outside the composition. +/// +/// Rust: `statistical_mechanics::kinetics::mass_action_rates` +#[pyfunction] +#[pyo3(name = "mass_action_rates", signature = (reactions, k, concentrations))] +pub fn pyfn_mass_action_rates<'py>(py: Python<'py>, reactions: Vec, k: Vec, concentrations: Vec) -> PyResult> { + let reactions = reactions.into_iter().map(|__e| __e.inner).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistical_mechanics::kinetics::mass_action_rates(&reactions, &k, &concentrations))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Integrates a reaction network in time with an adaptive implicit method. +/// +/// Chemical networks are almost always stiff -- a fast pre-equilibrium +/// alongside a slow overall conversion means the fastest and slowest +/// timescales differ by orders of magnitude -- and an explicit integrator is +/// then limited by the *fastest* one long after it has ceased to matter. +/// The step here is backward Euler -- A-stable, and L-stable, so a mode far +/// faster than the step is damped rather than merely bounded -- taken once +/// at the full step and twice at half. The difference is the local error +/// estimate, and their Richardson combination `2 y_half - y_full` is the +/// second-order value actually kept. +/// +/// A multistep formula would be the conventional choice and is the wrong one +/// here: BDF2 assumes a uniform step, and an adaptive controller varies it +/// every step, so the history it is handed is at the wrong spacing and the +/// resulting inconsistency dominates the error estimate. A one-step method +/// with Richardson has no history to get wrong. +/// +/// The step is limited by the solution's own timescale as well as by the +/// error estimate, and that second limit is not redundant. On an +/// *oscillatory* system step doubling can be fooled outright: an L-stable +/// method damps hard at a step much longer than the period, so the coarse +/// and fine solutions both collapse toward the fixed point, agree closely +/// with each other, and report a small error -- whereupon the controller +/// grows the step further. A run can end up stepping clean over whole +/// oscillations while its error estimate reports success. Bounding the step +/// by `|c| / |dc/dt|` prevents that, because it looks at the dynamics rather +/// than at the difference between two equally wrong answers. +/// +/// Returns `(time, composition)` at each accepted step. +/// +/// Errors: +/// Returns an error for a mismatched initial composition, a non-positive +/// end time or tolerance, or if the Newton iteration inside a step fails to +/// converge even at the smallest permitted step. +/// +/// Rust: `statistical_mechanics::kinetics::rate_equations` +#[pyfunction] +#[pyo3(name = "rate_equations", signature = (stoich, rates, c0, t_end, rtol))] +pub fn pyfn_rate_equations(stoich: crate::generated::types::PyMatrixArg, rates: pyo3::Py, c0: Vec, t_end: f64, rtol: f64) -> PyResult)>> { + let stoich = stoich.0; + let __cb_rates = std::rc::Rc::new(crate::runtime::Callback::new(rates)); + let rates = { let __cb = __cb_rates.clone(); move |__a0: &[f64]| -> Vec { __cb.call::<_, Vec>((__a0.to_vec(),), Vec::new()) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::kinetics::rate_equations(&stoich, &rates, &c0, t_end, rtol)); + crate::runtime::callback::check(&[&__cb_rates], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Gillespie's direct method: an exact realisation of the chemical master +/// equation. +/// +/// Exact in a strong sense -- the trajectory is drawn from the true +/// distribution of the jump process, with no time discretisation at all. +/// The waiting time to the next event is exponential with rate equal to the +/// total propensity, and which reaction fires is chosen in proportion to +/// its own. Returns `(time, counts)` after each event, including the +/// initial state. +/// +/// Errors: +/// Returns an error for a malformed network or a non-positive end time. +/// +/// Rust: `statistical_mechanics::kinetics::gillespie_ssa` +#[pyfunction] +#[pyo3(name = "gillespie_ssa", signature = (reactions, k, x0, t_end, max_events, rng))] +pub fn pyfn_gillespie_ssa(reactions: Vec, k: Vec, x0: Vec, t_end: f64, max_events: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult)>> { + let reactions = reactions.into_iter().map(|__e| __e.inner).collect::>(); + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::kinetics::gillespie_ssa(&reactions, &k, &x0, t_end, max_events, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Explicit tau-leaping: many reaction events per step, each count drawn +/// from a Poisson distribution. +/// +/// Trades exactness for speed. Over a leap of `tau` the propensities are +/// held fixed, so the number of firings of reaction `j` is Poisson with +/// mean `a_j tau` -- correct only while `tau` is short enough that the +/// propensities really do not change much, which is the whole art of the +/// method. Too long a leap drives species negative; this implementation +/// rejects a leap that would and retries it at half the length rather than +/// clamping, since clamping silently changes the reaction network. +/// +/// Errors: +/// Returns an error for a malformed network, a non-positive end time or +/// leap. +/// +/// Rust: `statistical_mechanics::kinetics::tau_leaping` +#[pyfunction] +#[pyo3(name = "tau_leaping", signature = (reactions, k, x0, t_end, tau, rng))] +pub fn pyfn_tau_leaping(reactions: Vec, k: Vec, x0: Vec, t_end: f64, tau: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult)>> { + let reactions = reactions.into_iter().map(|__e| __e.inner).collect::>(); + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::kinetics::tau_leaping(&reactions, &k, &x0, t_end, tau, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// The Michaelis-Menten rate `v = vmax s / (km + s)`. +/// +/// Rust: `statistical_mechanics::kinetics::michaelis_menten` +#[pyfunction] +#[pyo3(name = "michaelis_menten", signature = (s, vmax, km))] +pub fn pyfn_michaelis_menten(s: f64, vmax: f64, km: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::kinetics::michaelis_menten(s, vmax, km)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Hill rate `v = vmax s^n / (k^n + s^n)`. +/// +/// The exponent is a measure of cooperativity, not a molecularity: a Hill +/// coefficient of 2.8 for haemoglobin does not mean 2.8 oxygen molecules +/// bind at once, it means four sites bind with positive cooperativity and +/// the two-state fit lands there. +/// +/// Rust: `statistical_mechanics::kinetics::hill_equation` +#[pyfunction] +#[pyo3(name = "hill_equation", signature = (s, vmax, k, n))] +pub fn pyfn_hill_equation(s: f64, vmax: f64, k: f64, n: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::kinetics::hill_equation(s, vmax, k, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Fits `vmax` and `km` to saturation data by least squares on the +/// *residuals of the rate itself*, by Gauss-Newton. +/// +/// Deliberately not the Lineweaver-Burk fit. Inverting the data transforms +/// the error along with it, so the points at the lowest substrate -- where +/// the relative error is largest -- become the ones with the largest +/// leverage, and the fitted `vmax` is biased. The double-reciprocal plot +/// remains useful for *seeing* the mechanism, which is what +/// `lineweaver_burk` is for; it is not the way to get the numbers. +/// +/// Errors: +/// Returns an error for fewer than three points, mismatched lengths, +/// negative concentrations or rates, or a fit that does not converge. +/// +/// Rust: `statistical_mechanics::kinetics::mm_fit` +#[pyfunction] +#[pyo3(name = "mm_fit", signature = (s, v))] +pub fn pyfn_mm_fit<'py>(py: Python<'py>, s: Vec, v: Vec) -> PyResult<(f64, f64)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistical_mechanics::kinetics::mm_fit(&s, &v))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// The double-reciprocal transform: `(1/s, 1/v)` for each point, plus the +/// straight line through them as `(slope, intercept)`. +/// +/// The line has slope `km / vmax` and intercept `1 / vmax`. Useful for +/// reading a mechanism off a plot -- competitive, uncompetitive and +/// non-competitive inhibition give visibly different families of lines -- +/// and a poor way to extract the constants; see `mm_fit`. +/// +/// Errors: +/// Returns an error for fewer than two points, mismatched lengths, or a +/// non-positive concentration or rate, which the transform cannot represent. +/// +/// Rust: `statistical_mechanics::kinetics::lineweaver_burk` +#[pyfunction] +#[pyo3(name = "lineweaver_burk", signature = (s, v))] +pub fn pyfn_lineweaver_burk<'py>(py: Python<'py>, s: Vec, v: Vec) -> PyResult<(Vec<(f64, f64)>, f64, f64)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistical_mechanics::kinetics::lineweaver_burk(&s, &v))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0.into_iter().map(|__x| (__x.0, __x.1)).collect::>(), __v.1, __v.2)) +} + +/// Fits the Hill parameters `(vmax, k, n)` by Gauss-Newton. +/// +/// Errors: +/// Returns an error for fewer than four points, mismatched lengths, or +/// non-positive data. +/// +/// Rust: `statistical_mechanics::kinetics::hill_fit` +#[pyfunction] +#[pyo3(name = "hill_fit", signature = (s, v))] +pub fn pyfn_hill_fit<'py>(py: Python<'py>, s: Vec, v: Vec) -> PyResult<(f64, f64, f64)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistical_mechanics::kinetics::hill_fit(&s, &v))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// The inhibited Michaelis-Menten rate. +/// +/// The three mechanisms are distinguished by *which* constant moves, not by +/// how much the rate falls -- which is why a single rate measurement can +/// never identify the mechanism and a substrate series can. +/// +/// Errors: +/// Returns an error for a non-positive `km` or inhibition constant, or a +/// negative concentration. +/// +/// Rust: `statistical_mechanics::kinetics::enzyme_inhibition` +#[pyfunction] +#[pyo3(name = "enzyme_inhibition", signature = (s, i, vmax, km, ki, kind))] +pub fn pyfn_enzyme_inhibition(s: f64, i: f64, vmax: f64, km: f64, ki: f64, kind: crate::generated::types::PyInhibition) -> PyResult { + let kind = kind.to_rust(); + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::kinetics::enzyme_inhibition(s, i, vmax, km, ki, kind)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// How far a mechanism is from its steady-state approximation, as the +/// largest relative difference in the intermediate's concentration. +/// +/// The approximation holds when the intermediate is consumed as fast as it +/// is made, which for Michaelis-Menten means the enzyme is scarce beside the +/// substrate. Returns the discrepancy so the caller can see *whether* it +/// holds rather than assuming it. +/// +/// Errors: +/// Returns an error for non-positive rate constants or concentrations. +/// +/// Rust: `statistical_mechanics::kinetics::steady_state_approx_check` +#[pyfunction] +#[pyo3(name = "steady_state_approx_check", signature = (e0, s0, k1, k_minus1, k2, t_end))] +pub fn pyfn_steady_state_approx_check(e0: f64, s0: f64, k1: f64, k_minus1: f64, k2: f64, t_end: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::kinetics::steady_state_approx_check(e0, s0, k1, k_minus1, k2, t_end)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The equilibrium composition of a set of reactions with known constants, +/// found by minimising the total residual of the mass-action and +/// conservation conditions. +/// +/// Each reaction contributes `prod c^nu = k_eq` and each conserved element +/// contributes a total. Solved by Newton on the logarithms of the +/// concentrations, which keeps every one positive without a constraint -- +/// a composition can approach zero but never reach or cross it, which is +/// what the physical problem requires and what an unconstrained solve on the +/// concentrations themselves does not respect. +/// +/// `totals` is one row per conserved quantity, giving each species' content +/// and the total amount. +/// +/// Errors: +/// Returns an error for mismatched shapes, a non-positive constant or total, +/// or a system that does not converge. +/// +/// Rust: `statistical_mechanics::kinetics::equilibrium_composition` +#[pyfunction] +#[pyo3(name = "equilibrium_composition", signature = (stoich, k_eq, totals))] +pub fn pyfn_equilibrium_composition<'py>(py: Python<'py>, stoich: crate::generated::types::PyMatrixArg, k_eq: Vec, totals: Vec<(Vec, f64)>) -> PyResult> { + let stoich = stoich.0; + let totals = totals.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistical_mechanics::kinetics::equilibrium_composition(&stoich, &k_eq, &totals))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The Brusselator, integrated in time. +/// +/// `A -> X`, `2X + Y -> 3X`, `B + X -> Y + D`, `X -> E`, with `A` and `B` +/// held fixed. The steady state `(a, b/a)` loses stability in a Hopf +/// bifurcation exactly at `b = 1 + a^2`, and above it the system settles +/// onto a limit cycle whose amplitude does not depend on where it started. +/// That sharp threshold is what makes it the standard test of an oscillating +/// mechanism: the transition is a property of the equations, not of the +/// integrator. +/// +/// Errors: +/// Returns an error for non-positive parameters or a bad initial state. +/// +/// Rust: `statistical_mechanics::kinetics::oscillating_brusselator` +#[pyfunction] +#[pyo3(name = "oscillating_brusselator", signature = (a, b, c0, t_end))] +pub fn pyfn_oscillating_brusselator<'py>(py: Python<'py>, a: f64, b: f64, c0: (f64, f64), t_end: f64) -> PyResult)>> { + let c0 = (c0.0, c0.1); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistical_mechanics::kinetics::oscillating_brusselator(a, b, c0, t_end))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Whether the Brusselator oscillates at these parameters: `b > 1 + a^2`. +/// +/// Rust: `statistical_mechanics::kinetics::brusselator_oscillates` +#[pyfunction] +#[pyo3(name = "brusselator_oscillates", signature = (a, b))] +pub fn pyfn_brusselator_oscillates(a: f64, b: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::kinetics::brusselator_oscillates(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Oregonator, the Field-Noyes reduction of the Belousov-Zhabotinsky +/// reaction, in its scaled form. +/// +/// Genuinely stiff: `epsilon` and `delta` are of order `10^-2` and `10^-4`, +/// so the three variables move on timescales four orders of magnitude +/// apart, and an explicit integrator would be pinned to the fastest one for +/// the whole run. This is the case the implicit solver in +/// `rate_equations` exists for. +/// +/// Errors: +/// Returns an error for non-positive parameters or a bad initial state. +/// +/// Rust: `statistical_mechanics::kinetics::oregonator` +#[pyfunction] +#[pyo3(name = "oregonator", signature = (epsilon, delta, q, f, c0, t_end))] +pub fn pyfn_oregonator<'py>(py: Python<'py>, epsilon: f64, delta: f64, q: f64, f: f64, c0: (f64, f64, f64), t_end: f64) -> PyResult)>> { + let c0 = (c0.0, c0.1, c0.2); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistical_mechanics::kinetics::oregonator(epsilon, delta, q, f, c0, t_end))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// The chemical Lotka-Volterra mechanism: `A + X -> 2X`, `X + Y -> 2Y`, +/// `Y -> B`, with `A` held fixed. +/// +/// Returns the trajectory together with the conserved quantity +/// `V = k2 x + k2 y - k3 ln x - k1 a ln y`, which is constant along every +/// orbit. That constant is the reason the orbits are closed curves rather +/// than a limit cycle: the system is conservative, and unlike the +/// Brusselator its amplitude *does* depend on where it started. Reporting +/// it lets a caller see the integrator's drift directly. +/// +/// Errors: +/// Returns an error for non-positive parameters or a non-positive initial +/// state, for which the conserved quantity is undefined. +/// +/// Rust: `statistical_mechanics::kinetics::lotka_volterra_chemical` +#[pyfunction] +#[pyo3(name = "lotka_volterra_chemical", signature = (a, k1, k2, k3, c0, t_end))] +pub fn pyfn_lotka_volterra_chemical(a: f64, k1: f64, k2: f64, k3: f64, c0: (f64, f64), t_end: f64) -> PyResult<(Vec<(f64, Vec)>, Vec)> { + let c0 = (c0.0, c0.1); + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::kinetics::lotka_volterra_chemical(a, k1, k2, k3, c0, t_end)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0.into_iter().map(|__x| (__x.0, __x.1)).collect::>(), __v.1)) +} + +/// The ignition time of an autocatalytic reaction `A + B -> 2B`, defined as +/// the moment the product passes half its final amount. +/// +/// The closed form is the logistic inflection: with `a0 + b0` conserved, +/// `t = ln(a0 / b0) / (k (a0 + b0))`. The induction period is set by how +/// *little* product there is at the start, which is why an autocatalytic +/// reaction can sit apparently inert for a long time and then go over in a +/// moment. +/// +/// Errors: +/// Returns an error for a non-positive rate constant or a non-positive +/// initial amount of either species. +/// +/// Rust: `statistical_mechanics::kinetics::autocatalysis_ignition` +#[pyfunction] +#[pyo3(name = "autocatalysis_ignition", signature = (a0, b0, k))] +pub fn pyfn_autocatalysis_ignition(a0: f64, b0: f64, k: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::kinetics::autocatalysis_ignition(a0, b0, k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Whether a branching chain reaction runs away, and by how much: the +/// branching ratio `k_branch / k_term`. +/// +/// Above one the chain carriers multiply and the reaction accelerates +/// without bound; below one it dies out. The threshold is exactly one and +/// nothing continuous separates the two behaviours, which is why an +/// explosion limit is a sharp line in pressure and temperature rather than +/// a gradual onset. +/// +/// Errors: +/// Returns an error for a non-positive termination rate. +/// +/// Rust: `statistical_mechanics::kinetics::chain_reaction_criticality` +#[pyfunction] +#[pyo3(name = "chain_reaction_criticality", signature = (k_branch, k_term))] +pub fn pyfn_chain_reaction_criticality(k_branch: f64, k_term: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::kinetics::chain_reaction_criticality(k_branch, k_term)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The Eyring rate `(k_B T / h) exp(dS/R) exp(-dH/RT)`. +/// +/// Differs from Arrhenius in what the prefactor means: here it is +/// `k_B T / h`, a universal frequency of about `6 x 10^12` per second at +/// room temperature, and all the chemistry sits in the entropy of +/// activation. The two forms fit the same data equally well and disagree +/// about why. +/// +/// Errors: +/// Returns an error for a non-positive temperature. +/// +/// Rust: `statistical_mechanics::kinetics::eyring` +#[pyfunction] +#[pyo3(name = "eyring", signature = (delta_h, delta_s, t))] +pub fn pyfn_eyring(delta_h: f64, delta_s: f64, t: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::kinetics::eyring(delta_h, delta_s, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Transition-state theory with a transmission coefficient. +/// +/// `k = kappa (k_B T / h) exp(-dG/RT)`. The coefficient is the fraction of +/// trajectories that cross the barrier and *stay* crossed; transition-state +/// theory assumes it is one, which makes the theory an upper bound on the +/// true rate rather than an estimate of it. +/// +/// Errors: +/// Returns an error for a non-positive temperature or a coefficient outside +/// zero to one. +/// +/// Rust: `statistical_mechanics::kinetics::transition_state_theory_rate` +#[pyfunction] +#[pyo3(name = "transition_state_theory_rate", signature = (delta_g, t, transmission))] +pub fn pyfn_transition_state_theory_rate(delta_g: f64, t: f64, transmission: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::kinetics::transition_state_theory_rate(delta_g, t, transmission)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The Kramers rate in the moderate-to-high friction regime, relative to the +/// transition-state result. +/// +/// `k / k_TST = sqrt(1 + (gamma / 2 omega_b)^2) - gamma / (2 omega_b)`, +/// which is at most one and falls toward `omega_b / gamma` as the friction +/// grows: a solvent that couples strongly to the reaction coordinate makes +/// recrossing likely, and every recrossing is a barrier passage that did not +/// produce a product. This is the transmission coefficient that +/// `transition_state_theory_rate` takes on faith. +/// +/// Barrier frequency and friction are in the same units; the ratio is what +/// matters. +/// +/// Errors: +/// Returns an error for a non-positive barrier frequency or a negative +/// friction. +/// +/// Rust: `statistical_mechanics::kinetics::kramers_rate_check` +#[pyfunction] +#[pyo3(name = "kramers_rate_check", signature = (gamma, barrier_frequency))] +pub fn pyfn_kramers_rate_check(gamma: f64, barrier_frequency: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::kinetics::kramers_rate_check(gamma, barrier_frequency)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The semiclassical kinetic isotope effect from the change in zero-point +/// energy alone. +/// +/// `k_light / k_heavy = exp(h (nu_light - nu_heavy) / (2 k_B T))`. The +/// hydrogen-deuterium maximum near seven at room temperature comes out of +/// this and nothing else; a measured ratio well above it is evidence of +/// tunnelling, which this estimate deliberately omits so that the excess is +/// visible rather than absorbed into a fitted parameter. +/// +/// Frequencies are in reciprocal centimetres. +/// +/// Errors: +/// Returns an error for a non-positive temperature or frequency. +/// +/// Rust: `statistical_mechanics::kinetics::kinetic_isotope_effect_estimate` +#[pyfunction] +#[pyo3(name = "kinetic_isotope_effect_estimate", signature = (nu_light, nu_heavy, t))] +pub fn pyfn_kinetic_isotope_effect_estimate(nu_light: f64, nu_heavy: f64, t: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::kinetics::kinetic_isotope_effect_estimate(nu_light, nu_heavy, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The relaxation time of a reaction perturbed from equilibrium by a +/// temperature jump. +/// +/// For `A <-> B` the relaxation is a single exponential with rate +/// `k_forward + k_reverse` -- the *sum*, not either one. That is what makes +/// the technique work: a single measured relaxation gives the sum, the +/// equilibrium constant gives the ratio, and together they give both rate +/// constants, which no steady-state measurement can separate. +/// +/// Errors: +/// Returns an error if both rate constants are zero or either is negative. +/// +/// Rust: `statistical_mechanics::kinetics::temperature_jump_relaxation` +#[pyfunction] +#[pyo3(name = "temperature_jump_relaxation", signature = (k_forward, k_reverse))] +pub fn pyfn_temperature_jump_relaxation(k_forward: f64, k_reverse: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::kinetics::temperature_jump_relaxation(k_forward, k_reverse)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The classical nucleation rate `J = A exp(-dG* / k_B T)`. +/// +/// The exponent is enormous and its argument is a cube over a square, so +/// the rate spans dozens of orders of magnitude over a small change in +/// supersaturation. That extreme sensitivity is the physics, not a defect of +/// the model: it is why nucleation appears to have a threshold. +/// +/// Errors: +/// Returns an error for a non-positive temperature or prefactor. +/// +/// Rust: `statistical_mechanics::kinetics::nucleation_rate_cnt` +#[pyfunction] +#[pyo3(name = "nucleation_rate_cnt", signature = (barrier, prefactor, t))] +pub fn pyfn_nucleation_rate_cnt(barrier: f64, prefactor: f64, t: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::kinetics::nucleation_rate_cnt(barrier, prefactor, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The classical nucleation barrier for a spherical nucleus: +/// `16 pi sigma^3 / (3 (n dmu)^2)`. +/// +/// Errors: +/// Returns an error for a non-positive surface tension, density or driving +/// force. +/// +/// Rust: `statistical_mechanics::kinetics::nucleation_barrier` +#[pyfunction] +#[pyo3(name = "nucleation_barrier", signature = (surface_tension, number_density, driving_force))] +pub fn pyfn_nucleation_barrier(surface_tension: f64, number_density: f64, driving_force: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::kinetics::nucleation_barrier(surface_tension, number_density, driving_force)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The Johnson-Mehl-Avrami-Kolmogorov transformed fraction +/// `1 - exp(-(k t)^n)`. +/// +/// The exponent carries the mechanism: roughly 4 for three-dimensional +/// growth from a constant nucleation rate, 3 when all sites nucleate at +/// once, and lower for growth confined to a plane or a line. The point of +/// fitting it is to read the dimensionality off the kinetics. +/// +/// Rust: `statistical_mechanics::kinetics::jmak_avrami` +#[pyfunction] +#[pyo3(name = "jmak_avrami", signature = (t, k, n))] +pub fn pyfn_jmak_avrami(t: f64, k: f64, n: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::kinetics::jmak_avrami(t, k, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Fits `(k, n)` to transformed-fraction data. +/// +/// The double logarithm `ln(-ln(1 - x)) = n ln t + n ln k` makes the fit +/// linear and exact, which is the one case where a transform of the data is +/// the right thing to do: the relation is exactly linear in the transformed +/// variables, so no error is being reshaped, only re-expressed. +/// +/// Errors: +/// Returns an error for fewer than two usable points -- a fraction of zero +/// or one carries no information, since the transform sends it to infinity. +/// +/// Rust: `statistical_mechanics::kinetics::avrami_fit` +#[pyfunction] +#[pyo3(name = "avrami_fit", signature = (times, fraction))] +pub fn pyfn_avrami_fit<'py>(py: Python<'py>, times: Vec, fraction: Vec) -> PyResult<(f64, f64)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistical_mechanics::kinetics::avrami_fit(×, &fraction))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// The quantum yield: molecules transformed per photon absorbed. +/// +/// A yield above one is not an error -- a chain reaction initiated by one +/// photon can transform thousands of molecules -- so no upper bound is +/// imposed. +/// +/// Errors: +/// Returns an error for a non-positive photon count or a negative product +/// count. +/// +/// Rust: `statistical_mechanics::kinetics::photochemistry_quantum_yield` +#[pyfunction] +#[pyo3(name = "photochemistry_quantum_yield", signature = (molecules, photons_absorbed))] +pub fn pyfn_photochemistry_quantum_yield(molecules: f64, photons_absorbed: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::kinetics::photochemistry_quantum_yield(molecules, photons_absorbed)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The pH of a solution of one or more acids, by solving the full charge +/// balance rather than any approximation. +/// +/// Each acid is `(pKa, total concentration)`; `base_conc` is added strong +/// base. The equation solved is +/// `[H+] + [base] = K_w/[H+] + sum_a C_a K_a / (K_a + [H+])`, +/// which includes the water autoprotolysis and the depletion of the acid as +/// it dissociates. Neither can be dropped in general: the usual +/// `sqrt(K_a C)` shortcut assumes both, and it fails for a dilute acid +/// (where water dominates) and for a strong one (where the acid is nearly +/// all dissociated and the depletion is the whole story). Solved by +/// bisection on `pH`, which cannot diverge because the balance is monotone +/// in `[H+]`. +/// +/// Errors: +/// Returns an error for a negative concentration or an empty system with no +/// base. +/// +/// Rust: `statistical_mechanics::kinetics::ph_from_equilibria` +#[pyfunction] +#[pyo3(name = "ph_from_equilibria", signature = (acids, base_conc))] +pub fn pyfn_ph_from_equilibria<'py>(py: Python<'py>, acids: Vec<(f64, f64)>, base_conc: f64) -> PyResult { + let acids = acids.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistical_mechanics::kinetics::ph_from_equilibria(&acids, base_conc))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// A titration curve: pH against the volume of strong base added. +/// +/// Returns `(volume added, pH)` at each of `points` steps up to +/// `volume_max`. Dilution is accounted for -- both the acid and the base +/// are diluted by the growing total volume -- which is what puts the +/// equivalence point of a weak acid above pH 7 rather than at it. +/// +/// Errors: +/// Returns an error for a non-positive volume, concentration or point +/// count. +/// +/// Rust: `statistical_mechanics::kinetics::titration_curve` +#[pyfunction] +#[pyo3(name = "titration_curve", signature = (acid_pka, acid_conc, acid_volume, base_conc, volume_max, points))] +pub fn pyfn_titration_curve<'py>(py: Python<'py>, acid_pka: f64, acid_conc: f64, acid_volume: f64, base_conc: f64, volume_max: f64, points: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistical_mechanics::kinetics::titration_curve(acid_pka, acid_conc, acid_volume, base_conc, volume_max, points))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Henderson-Hasselbalch: `pH = pKa + log10(base / acid)`. +/// +/// An approximation, and one whose failure is predictable: it assumes the +/// dissociation does not appreciably change either concentration, so it is +/// accurate within about a unit of the pKa and wrong outside that. Compare +/// against `ph_from_equilibria`, which makes no such assumption. +/// +/// Errors: +/// Returns an error for a non-positive ratio. +/// +/// Rust: `statistical_mechanics::kinetics::buffer_henderson_hasselbalch` +#[pyfunction] +#[pyo3(name = "buffer_henderson_hasselbalch", signature = (pka, ratio))] +pub fn pyfn_buffer_henderson_hasselbalch(pka: f64, ratio: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::kinetics::buffer_henderson_hasselbalch(pka, ratio)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The Debye-Huckel activity coefficient of an ion. +/// +/// The extended law `log10 gamma = -A z^2 sqrt(I) / (1 + sqrt I)`, with +/// `A = 0.509` for water at 25 degrees. The limiting law without the +/// denominator is only good below about `I = 0.01`; the extended form holds +/// to roughly `I = 0.1`, and above that no simple expression does. +/// +/// Errors: +/// Returns an error for a negative ionic strength. +/// +/// Rust: `statistical_mechanics::kinetics::debye_huckel_activity` +#[pyfunction] +#[pyo3(name = "debye_huckel_activity", signature = (z, ionic_strength))] +pub fn pyfn_debye_huckel_activity(z: f64, ionic_strength: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::kinetics::debye_huckel_activity(z, ionic_strength)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The Nernst potential from a concentration ratio. +/// +/// A thin wrapper on `chemistry::nernst_potential` in the form the +/// kinetics literature uses. At 25 degrees and one electron the slope is +/// 59.16 mV per decade, which is the number every ion-selective electrode +/// is calibrated against. +/// +/// Errors: +/// Returns an error for a non-positive temperature, electron count or +/// ratio. +/// +/// Rust: `statistical_mechanics::kinetics::nernst` +#[pyfunction] +#[pyo3(name = "nernst", signature = (e0, z, ratio, t))] +pub fn pyfn_nernst(e0: f64, z: f64, ratio: f64, t: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::kinetics::nernst(e0, z, ratio, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The Butler-Volmer current density +/// `i0 (exp(alpha z F eta / RT) - exp(-(1 - alpha) z F eta / RT))`. +/// +/// At small overpotential the two exponentials cancel to leading order and +/// the current is *linear* in `eta` with a slope `i0 z F / RT` -- the +/// charge-transfer resistance. At large overpotential one term dominates +/// and the relation becomes the logarithmic Tafel law. Both limits come out +/// of the same expression, which is why fitting a Tafel slope to +/// near-equilibrium data gives a meaningless exchange current. +/// +/// Errors: +/// Returns an error for a non-positive temperature or exchange current, an +/// asymmetry outside zero to one, or a non-positive electron count. +/// +/// Rust: `statistical_mechanics::kinetics::butler_volmer` +#[pyfunction] +#[pyo3(name = "butler_volmer", signature = (i0, alpha, eta, z, t))] +pub fn pyfn_butler_volmer(i0: f64, alpha: f64, eta: f64, z: f64, t: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::kinetics::butler_volmer(i0, alpha, eta, z, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The Cottrell current `z F A c sqrt(D / (pi t))` for a diffusion-limited +/// electrode. +/// +/// Falls as the inverse square root of time, not exponentially: the +/// depletion layer grows as `sqrt(D t)`, so the gradient that drives the +/// current thins in proportion. The same square root governs every +/// semi-infinite diffusion problem. +/// +/// Errors: +/// Returns an error for a non-positive time, area, diffusion coefficient, +/// concentration or electron count. +/// +/// Rust: `statistical_mechanics::kinetics::cottrell_current` +#[pyfunction] +#[pyo3(name = "cottrell_current", signature = (z, area, concentration, diffusivity, t))] +pub fn pyfn_cottrell_current(z: f64, area: f64, concentration: f64, diffusivity: f64, t: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::kinetics::cottrell_current(z, area, concentration, diffusivity, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_stoichiometry_matrix, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mass_action_rates, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rate_equations, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gillespie_ssa, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tau_leaping, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_michaelis_menten, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hill_equation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mm_fit, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lineweaver_burk, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hill_fit, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_enzyme_inhibition, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_steady_state_approx_check, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_equilibrium_composition, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_oscillating_brusselator, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_brusselator_oscillates, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_oregonator, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lotka_volterra_chemical, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_autocatalysis_ignition, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_chain_reaction_criticality, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_eyring, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_transition_state_theory_rate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kramers_rate_check, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kinetic_isotope_effect_estimate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_temperature_jump_relaxation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_nucleation_rate_cnt, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_nucleation_barrier, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_jmak_avrami, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_avrami_fit, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_photochemistry_quantum_yield, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ph_from_equilibria, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_titration_curve, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_buffer_henderson_hasselbalch, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_debye_huckel_activity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_nernst, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_butler_volmer, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cottrell_current, m)?)?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_statistical_mechanics__lattice_models.rs b/bindings/python/src/generated/m_statistical_mechanics__lattice_models.rs new file mode 100644 index 0000000..2eee9ad --- /dev/null +++ b/bindings/python/src/generated/m_statistical_mechanics__lattice_models.rs @@ -0,0 +1,393 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Site percolation on a square lattice: occupy each site with probability +/// `p` and report whether an occupied cluster spans top to bottom. +/// +/// Returns the grid and whether it spans. The transition is sharp only in the +/// infinite lattice; on a finite one the spanning probability rises smoothly +/// through the threshold over a width that shrinks as the lattice grows, +/// which is finite-size scaling in its simplest visible form. +/// +/// Errors: +/// Returns an error for a bad lattice size or a probability outside `[0, 1]`. +/// +/// Rust: `statistical_mechanics::lattice_models::percolation_site` +#[pyfunction] +#[pyo3(name = "percolation_site", signature = (n, p, rng))] +pub fn pyfn_percolation_site(n: usize, p: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(Vec, bool)> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::lattice_models::percolation_site(n, p, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// Bond percolation on a square lattice: open each bond with probability `p` +/// and report whether the lattice spans. +/// +/// The bond threshold in two dimensions is exactly one half, by a duality +/// argument -- the dual of an open bond is a closed one, so the model is +/// self-dual at `p = 1/2` and the transition can be nowhere else. The site +/// threshold has no such argument and is only known numerically. +/// +/// Errors: +/// Returns an error for a bad lattice size or probability. +/// +/// Rust: `statistical_mechanics::lattice_models::percolation_bond` +#[pyfunction] +#[pyo3(name = "percolation_bond", signature = (n, p, rng))] +pub fn pyfn_percolation_bond(n: usize, p: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::lattice_models::percolation_bond(n, p, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Estimates the site percolation threshold by bisection on the spanning +/// probability. +/// +/// The true two-dimensional value is about 0.592746, and unlike the bond +/// threshold it has no closed form. A finite lattice puts the half-spanning +/// point slightly off it, and the offset shrinks as the lattice grows. +/// +/// Errors: +/// Returns an error for a bad lattice size or trial count. +/// +/// Rust: `statistical_mechanics::lattice_models::percolation_threshold_binary_search` +#[pyfunction] +#[pyo3(name = "percolation_threshold_binary_search", signature = (n, trials, rng))] +pub fn pyfn_percolation_threshold_binary_search(n: usize, trials: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::lattice_models::percolation_threshold_binary_search(n, trials, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The sizes of every occupied cluster, descending. +/// +/// Errors: +/// Returns an error if the grid is not square. +/// +/// Rust: `statistical_mechanics::lattice_models::cluster_size_distribution` +#[pyfunction] +#[pyo3(name = "cluster_size_distribution", signature = (grid, n))] +pub fn pyfn_cluster_size_distribution<'py>(py: Python<'py>, grid: Vec, n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistical_mechanics::lattice_models::cluster_size_distribution(&grid, n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The number of self-avoiding walks of `n` steps from the origin on the +/// square lattice. +/// +/// Counted by exhaustive backtracking, so it is exact and exponential -- +/// which is the state of the art: no formula is known, and the published +/// counts come from much cleverer enumerations of the same kind. +/// +/// Errors: +/// Returns an error above eighteen steps, where the count exceeds what this +/// enumeration will finish. +/// +/// Rust: `statistical_mechanics::lattice_models::self_avoiding_walk_count` +#[pyfunction] +#[pyo3(name = "self_avoiding_walk_count", signature = (n))] +pub fn pyfn_self_avoiding_walk_count(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::lattice_models::self_avoiding_walk_count(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// One self-avoiding walk sampled by the Rosenbluth method, with its weight. +/// +/// Growing a walk step by step and refusing to revisit gives a *biased* +/// sample: walks that had few choices are over-represented. The Rosenbluth +/// weight -- the product of the available choices at each step -- corrects +/// exactly for that, so weighted averages are unbiased. The method's known +/// weakness is that the weights become very unequal for long walks, so the +/// effective sample size collapses even though the estimator stays unbiased. +/// +/// Returns the path and its weight; a walk that traps itself returns a weight +/// of zero. +/// +/// Errors: +/// Returns an error for an excessive step count. +/// +/// Rust: `statistical_mechanics::lattice_models::saw_sample_rosenbluth` +#[pyfunction] +#[pyo3(name = "saw_sample_rosenbluth", signature = (n, rng))] +pub fn pyfn_saw_sample_rosenbluth(n: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(Vec<(i64, i64)>, f64)> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::lattice_models::saw_sample_rosenbluth(n, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0.into_iter().map(|__x| (__x.0, __x.1)).collect::>(), __v.1)) +} + +/// An estimate of the connective constant from exact walk counts. +/// +/// `mu = lim c_n^(1/n)`, about 2.63816 on the square lattice. +/// +/// Two corrections have to be removed and they are removed differently. The +/// counts alternate with parity -- `c_n / c_(n-1)` oscillates between about +/// 2.694 and 2.702 at these lengths -- so the ratio is taken two steps at a +/// time, `sqrt(c_n / c_(n-2))`, which averages the parity out rather than +/// amplifying it. What remains behaves as `mu (1 + (gamma - 1) / n)` because +/// `c_n ~ A mu^n n^(gamma - 1)`, and one Richardson step on `1 / n` cancels +/// it whatever the unknown coefficient. Applying Richardson to the raw +/// consecutive ratios instead makes matters *worse*, since it differences two +/// numbers of opposite parity and doubles the oscillation. +/// +/// Errors: +/// Returns an error for fewer than five counts, or a zero count. +/// +/// Rust: `statistical_mechanics::lattice_models::connective_constant_estimate` +#[pyfunction] +#[pyo3(name = "connective_constant_estimate", signature = (counts))] +pub fn pyfn_connective_constant_estimate<'py>(py: Python<'py>, counts: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistical_mechanics::lattice_models::connective_constant_estimate(&counts))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// A simple random walk on the `d`-dimensional cubic lattice. +/// +/// Errors: +/// Returns an error for zero dimensions or an excessive step count. +/// +/// Rust: `statistical_mechanics::lattice_models::random_walk_lattice` +#[pyfunction] +#[pyo3(name = "random_walk_lattice", signature = (steps, dimensions, rng))] +pub fn pyfn_random_walk_lattice(steps: usize, dimensions: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult>> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::lattice_models::random_walk_lattice(steps, dimensions, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Polya's return probability for a simple random walk in `d` dimensions. +/// +/// One in one and two dimensions and less than one from three up. The +/// dimension at which a walk stops returning is not a matter of degree: in +/// two dimensions the walker returns with certainty and in three it escapes +/// with probability about 0.66, and nothing continuous separates them. +/// +/// Errors: +/// Returns an error for zero dimensions or above eight. +/// +/// Rust: `statistical_mechanics::lattice_models::return_probability` +#[pyfunction] +#[pyo3(name = "return_probability", signature = (dimensions))] +pub fn pyfn_return_probability(dimensions: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::lattice_models::return_probability(dimensions)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The mean squared end-to-end distance of a set of weighted walks. +/// +/// Errors: +/// Returns an error for an empty sample or zero total weight. +/// +/// Rust: `statistical_mechanics::lattice_models::polymer_end_to_end` +#[pyfunction] +#[pyo3(name = "polymer_end_to_end", signature = (samples))] +pub fn pyfn_polymer_end_to_end<'py>(py: Python<'py>, samples: Vec<(Vec<(i64, i64)>, f64)>) -> PyResult { + let samples = samples.into_iter().map(|__e| (__e.0.into_iter().map(|__e| (__e.0, __e.1)).collect::>(), __e.1)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistical_mechanics::lattice_models::polymer_end_to_end(&samples))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The Flory exponent fitted from end-to-end distances at several lengths. +/// +/// ` ~ n^(2 nu)` with `nu = 3/4` exactly in two dimensions -- a result +/// of Nienhuis, and one that Flory's own mean-field argument happens to get +/// right in this dimension and wrong in three. +/// +/// Errors: +/// Returns an error for fewer than two lengths or a non-positive distance. +/// +/// Rust: `statistical_mechanics::lattice_models::flory_exponent_estimate` +#[pyfunction] +#[pyo3(name = "flory_exponent_estimate", signature = (lengths, squared))] +pub fn pyfn_flory_exponent_estimate<'py>(py: Python<'py>, lengths: Vec, squared: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistical_mechanics::lattice_models::flory_exponent_estimate(&lengths, &squared))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The number of perfect matchings of an `m` by `n` grid, by Kasteleyn's +/// formula. +/// +/// `prod_{j,k} (4 cos^2(pi j / (m+1)) + 4 cos^2(pi k / (n+1)))^(1/4)`. The +/// remarkable part is that a counting problem which is `#P`-complete on a +/// general graph is *polynomial* on a planar one, because the count becomes a +/// Pfaffian once the edges are oriented correctly. +/// +/// Returned as a float, since the count outgrows a `u64` by about the twelve +/// by twelve grid; it is exact to rounding and the caller can round it. +/// +/// Errors: +/// Returns an error for a zero dimension or an odd number of cells, which +/// admits no perfect matching at all. +/// +/// Rust: `statistical_mechanics::lattice_models::dimer_count_kasteleyn` +#[pyfunction] +#[pyo3(name = "dimer_count_kasteleyn", signature = (m, n))] +pub fn pyfn_dimer_count_kasteleyn(m: usize, n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::lattice_models::dimer_count_kasteleyn(m, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Ballistic deposition on a line, returning the final interface heights. +/// +/// A particle falls on a random column and sticks at the first point where it +/// touches the deposit, which may be the side of a neighbouring column rather +/// than the top of its own. That sideways sticking is the whole model: without +/// it the interface stays flat, and with it the interface roughens with the +/// Kardar-Parisi-Zhang exponents. +/// +/// Errors: +/// Returns an error for a bad width or an excessive time. +/// +/// Rust: `statistical_mechanics::lattice_models::kpz_growth_ballistic` +#[pyfunction] +#[pyo3(name = "kpz_growth_ballistic", signature = (width, depositions, rng))] +pub fn pyfn_kpz_growth_ballistic(width: usize, depositions: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::lattice_models::kpz_growth_ballistic(width, depositions, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The width of an interface: the standard deviation of its heights. +/// +/// Errors: +/// Returns an error for an empty interface. +/// +/// Rust: `statistical_mechanics::lattice_models::interface_width` +#[pyfunction] +#[pyo3(name = "interface_width", signature = (heights))] +pub fn pyfn_interface_width<'py>(py: Python<'py>, heights: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistical_mechanics::lattice_models::interface_width(&heights))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The growth exponent `beta`, fitted from the width against time. +/// +/// `W ~ t^beta` before the width saturates, with `beta = 1/3` in the +/// one-dimensional KPZ class. The fit must stay inside the growth regime: +/// once the correlation length reaches the system size the width stops +/// growing altogether, and including saturated points drags the exponent +/// toward zero. +/// +/// Errors: +/// Returns an error for fewer than three points or a non-positive width. +/// +/// Rust: `statistical_mechanics::lattice_models::growth_exponent_estimate` +#[pyfunction] +#[pyo3(name = "growth_exponent_estimate", signature = (times, widths))] +pub fn pyfn_growth_exponent_estimate<'py>(py: Python<'py>, times: Vec, widths: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistical_mechanics::lattice_models::growth_exponent_estimate(×, &widths))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The Abelian sandpile: drop grains at random sites and record the size of +/// each avalanche. +/// +/// The pile organises itself to the critical state without any parameter +/// being tuned, which is what "self-organised criticality" means: the +/// avalanche sizes come out power-law distributed whatever the initial +/// condition, with no temperature or field set by hand. +/// +/// Errors: +/// Returns an error for a bad lattice size or drop count. +/// +/// Rust: `statistical_mechanics::lattice_models::sandpile_avalanche_distribution` +#[pyfunction] +#[pyo3(name = "sandpile_avalanche_distribution", signature = (n, drops, rng))] +pub fn pyfn_sandpile_avalanche_distribution(n: usize, drops: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::lattice_models::sandpile_avalanche_distribution(n, drops, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Clauset's maximum-likelihood power-law fit above a cutoff, with the +/// Kolmogorov-Smirnov distance to the fitted law. +/// +/// Fitting a straight line to a log-log histogram is the traditional method +/// and it is badly biased: the bins in the tail hold few points, and least +/// squares weights them as heavily as the bins that hold thousands. The +/// maximum-likelihood estimator has a closed form for a continuous power law +/// and no such problem. +/// +/// Returns `(alpha, ks_distance)`. +/// +/// Errors: +/// Returns an error for a non-positive cutoff or too few points above it. +/// +/// Rust: `statistical_mechanics::lattice_models::power_law_fit_clauset` +#[pyfunction] +#[pyo3(name = "power_law_fit_clauset", signature = (data, x_min))] +pub fn pyfn_power_law_fit_clauset<'py>(py: Python<'py>, data: Vec, x_min: f64) -> PyResult<(f64, f64)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistical_mechanics::lattice_models::power_law_fit_clauset(&data, x_min))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_percolation_site, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_percolation_bond, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_percolation_threshold_binary_search, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cluster_size_distribution, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_self_avoiding_walk_count, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_saw_sample_rosenbluth, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_connective_constant_estimate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_random_walk_lattice, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_return_probability, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_polymer_end_to_end, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_flory_exponent_estimate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dimer_count_kasteleyn, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kpz_growth_ballistic, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_interface_width, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_growth_exponent_estimate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sandpile_avalanche_distribution, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_power_law_fit_clauset, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_statistical_mechanics__md.rs b/bindings/python/src/generated/m_statistical_mechanics__md.rs new file mode 100644 index 0000000..a7f817f --- /dev/null +++ b/bindings/python/src/generated/m_statistical_mechanics__md.rs @@ -0,0 +1,302 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// The secular drift of the total energy over a record, relative to its mean. +/// +/// This is the slope of a least-squares line through the total energy, +/// multiplied by the elapsed time -- not the spread. A symplectic +/// integrator's energy *oscillates* with an amplitude set by the step size +/// and does not go anywhere; reporting that oscillation as drift would +/// condemn a correct integrator, and reporting the maximum deviation would +/// do the same. What distinguishes a good integrator from a bad one is +/// whether the oscillation has a trend under it. +/// +/// Errors: +/// Returns an error for fewer than three samples or a zero time span. +/// +/// Rust: `statistical_mechanics::md::energy_drift` +#[pyfunction] +#[pyo3(name = "energy_drift", signature = (samples))] +pub fn pyfn_energy_drift<'py>(py: Python<'py>, samples: Vec) -> PyResult { + let samples = samples.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistical_mechanics::md::energy_drift(&samples))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// What the reduced units in this module mean. +/// +/// Rust: `statistical_mechanics::md::lj_reduced_units_note` +#[pyfunction] +#[pyo3(name = "lj_reduced_units_note", signature = ())] +pub fn pyfn_lj_reduced_units_note() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::md::lj_reduced_units_note()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_string()) +} + +/// A rough phase from the Lennard-Jones phase diagram. +/// +/// Boundaries taken from the accepted triple point near `(T* = 0.69, +/// rho* = 0.84)` and critical point near `(T* = 1.32, rho* = 0.31)`. It is a +/// classification, not an equation of state, and near a boundary it should +/// not be trusted over an actual measurement. +/// +/// Rust: `statistical_mechanics::md::lj_phase_point` +#[pyfunction] +#[pyo3(name = "lj_phase_point", signature = (t_star, rho_star))] +pub fn pyfn_lj_phase_point(t_star: f64, rho_star: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::md::lj_phase_point(t_star, rho_star)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_string()) +} + +/// The Ewald energy of a set of point charges in a periodic box. +/// +/// A charged system cannot be truncated: the Coulomb sum is only +/// conditionally convergent, so its value depends on the order of +/// summation and a spherical cutoff gives a different -- wrong -- answer. +/// Ewald splits the sum with a Gaussian screen into a real-space part that +/// converges quickly and a reciprocal-space part that does the same, plus +/// the self-energy of the screens. +/// +/// Errors: +/// Returns an error for mismatched lengths, a non-positive box or splitting +/// parameter, an empty system, or a net charge, for which the sum is not +/// defined without a neutralising background. +/// +/// Rust: `statistical_mechanics::md::ewald_sum_energy_lite` +#[pyfunction] +#[pyo3(name = "ewald_sum_energy_lite", signature = (charges, pos, box_l, alpha, k_max))] +pub fn pyfn_ewald_sum_energy_lite<'py>(py: Python<'py>, charges: Vec, pos: Vec, box_l: f64, alpha: f64, k_max: usize) -> PyResult { + let pos = pos.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistical_mechanics::md::ewald_sum_energy_lite(&charges, &pos, box_l, alpha, k_max))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The heat capacity per particle of a system from its energy fluctuations, +/// in units of Boltzmann's constant. +/// +/// `C_v = Var(E) / (k T^2)`. A classical harmonic crystal must return three: +/// each particle has three quadratic kinetic and three quadratic potential +/// degrees of freedom, and equipartition gives `k/2` to each. That is the +/// Dulong-Petit law, and it is the check this function exists for -- a +/// simulation that reports anything else at a temperature well above the +/// Debye temperature has a bug, not a discovery. +/// +/// Errors: +/// Returns an error for fewer than two energies, no particles, or a +/// non-positive temperature. +/// +/// Rust: `statistical_mechanics::md::harmonic_crystal_heat_capacity_check` +#[pyfunction] +#[pyo3(name = "harmonic_crystal_heat_capacity_check", signature = (energies, temperature, particles))] +pub fn pyfn_harmonic_crystal_heat_capacity_check<'py>(py: Python<'py>, energies: Vec, temperature: f64, particles: usize) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistical_mechanics::md::harmonic_crystal_heat_capacity_check(&energies, temperature, particles))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The second virial coefficient by numerical integration of the Mayer +/// function. +/// +/// `B2(T) = -2 pi int_0^rmax (exp(-u(r)/T) - 1) r^2 dr`. It changes sign at +/// the Boyle temperature, where attraction and repulsion cancel and the gas +/// is ideal to first order in the density -- about `T* = 3.418` for +/// Lennard-Jones. +/// +/// Errors: +/// Returns an error for a non-positive temperature or range, or an odd or +/// too-small interval count. +/// +/// Rust: `statistical_mechanics::md::virial_coefficient_b2` +#[pyfunction] +#[pyo3(name = "virial_coefficient_b2", signature = (potential, t, r_max, n))] +pub fn pyfn_virial_coefficient_b2(potential: crate::generated::types::PyPotential, t: f64, r_max: f64, n: usize) -> PyResult { + let potential = potential.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::md::virial_coefficient_b2(&potential, t, r_max, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The mean free path `1 / (sqrt 2 n sigma)`, with `sigma` the collision +/// cross-section. +/// +/// The `sqrt 2` is not decoration: it accounts for the *relative* motion of +/// the two colliding particles, and dropping it overestimates the path by +/// forty per cent. +/// +/// Errors: +/// Returns an error for a non-positive density or cross-section. +/// +/// Rust: `statistical_mechanics::md::mean_free_path` +#[pyfunction] +#[pyo3(name = "mean_free_path", signature = (density, sigma))] +pub fn pyfn_mean_free_path(density: f64, sigma: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::md::mean_free_path(density, sigma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The collision rate per particle, `sqrt 2 n sigma v_mean`. +/// +/// Errors: +/// Returns an error for a non-positive density, cross-section or speed. +/// +/// Rust: `statistical_mechanics::md::collision_rate` +#[pyfunction] +#[pyo3(name = "collision_rate", signature = (density, sigma, mean_speed))] +pub fn pyfn_collision_rate(density: f64, sigma: f64, mean_speed: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::md::collision_rate(density, sigma, mean_speed)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The shear viscosity from a Green-Kubo integral of the off-diagonal +/// stress autocorrelation. +/// +/// `eta = V / (k T) int_0^inf dt`, integrated up to the +/// first lag at which the estimated correlation stops being positive. +/// +/// That truncation is not an optimisation. Past a few correlation times the +/// estimate of `` is noise of a size set by the sample count, and +/// integrating thousands of such lags accumulates a random walk whose spread +/// is comparable to the whole integral -- for an exponential correlation +/// with a thirty-sample time and thirty thousand samples, the tail +/// contributes as much scatter as the signal contains. Integrating to the +/// end of the record therefore returns a number that is mostly noise, which +/// looks like a plausible viscosity and is not one. +/// +/// The cost of truncating is a known one: stopping at the first zero +/// crossing loses the part of the tail already below the noise floor, so the +/// result is a few per cent low. That is the accepted trade, and it is the +/// direction of the remaining error -- a short record still *underestimates* +/// rather than scattering, because the tail it cannot see carries real +/// weight. +/// +/// Errors: +/// Returns an error for fewer than two samples or a non-positive step, +/// volume or temperature. +/// +/// Rust: `statistical_mechanics::md::green_kubo_viscosity_lite` +#[pyfunction] +#[pyo3(name = "green_kubo_viscosity_lite", signature = (stress_xy, dt, volume, temperature))] +pub fn pyfn_green_kubo_viscosity_lite<'py>(py: Python<'py>, stress_xy: Vec, dt: f64, volume: f64, temperature: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistical_mechanics::md::green_kubo_viscosity_lite(&stress_xy, dt, volume, temperature))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// A potential of mean force from umbrella-sampling histograms, by +/// self-consistent WHAM. +/// +/// Each window is biased by `k (x - centre)^2 / 2`, and the windows have to +/// be combined by solving for one free-energy offset per window: simply +/// unbiasing each histogram and averaging leaves the offsets arbitrary, and +/// the resulting curve has a step at every window boundary. +/// +/// `histograms[w][b]` is the count in bin `b` of window `w`; bin `b` is +/// centred at `bin_lo + (b + 0.5) * bin_width`. +/// +/// Errors: +/// Returns an error for no windows, mismatched lengths, a non-positive bin +/// width, force constant or temperature, or if the iteration does not +/// converge. +/// +/// Rust: `statistical_mechanics::md::umbrella_sampling_pmf` +#[pyfunction] +#[pyo3(name = "umbrella_sampling_pmf", signature = (histograms, centers, k, bin_lo, bin_width, temperature))] +pub fn pyfn_umbrella_sampling_pmf<'py>(py: Python<'py>, histograms: Vec>, centers: Vec, k: f64, bin_lo: f64, bin_width: f64, temperature: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistical_mechanics::md::umbrella_sampling_pmf(&histograms, ¢ers, k, bin_lo, bin_width, temperature))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// A steered-molecular-dynamics pull: a harmonic restraint whose centre +/// moves at constant speed, returning the accumulated work at each step. +/// +/// The work is *not* the free-energy difference. It exceeds it by the +/// dissipation, and only in the reversible limit do the two coincide -- +/// which is what Jarzynski's equality repairs, by averaging `exp(-W/kT)` +/// over repeated pulls rather than averaging the work itself. +/// +/// Errors: +/// Returns an error for a non-positive step, force constant or step count. +/// +/// Rust: `statistical_mechanics::md::steered_pull` +#[pyfunction] +#[pyo3(name = "steered_pull", signature = (force_along, start, speed, k, dt, steps))] +pub fn pyfn_steered_pull(force_along: pyo3::Py, start: f64, speed: f64, k: f64, dt: f64, steps: usize) -> PyResult> { + let __cb_force_along = std::rc::Rc::new(crate::runtime::Callback::new(force_along)); + let force_along = { let __cb = __cb_force_along.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::md::steered_pull(&force_along, start, speed, k, dt, steps)); + crate::runtime::callback::check(&[&__cb_force_along], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Jarzynski's estimate of the free-energy difference from a set of +/// non-equilibrium work values. +/// +/// `exp(-dF/kT) = `. The average is dominated by the rare +/// trajectories with the *smallest* work, which is why the estimator is +/// notoriously hard to converge: the trajectories that matter most are the +/// ones sampled least. +/// +/// Errors: +/// Returns an error for no work values or a non-positive temperature. +/// +/// Rust: `statistical_mechanics::md::jarzynski_free_energy` +#[pyfunction] +#[pyo3(name = "jarzynski_free_energy", signature = (work, temperature))] +pub fn pyfn_jarzynski_free_energy<'py>(py: Python<'py>, work: Vec, temperature: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistical_mechanics::md::jarzynski_free_energy(&work, temperature))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_energy_drift, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lj_reduced_units_note, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lj_phase_point, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ewald_sum_energy_lite, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_harmonic_crystal_heat_capacity_check, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_virial_coefficient_b2, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mean_free_path, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_collision_rate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_green_kubo_viscosity_lite, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_umbrella_sampling_pmf, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_steered_pull, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_jarzynski_free_energy, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_statistics.rs b/bindings/python/src/generated/m_statistics.rs new file mode 100644 index 0000000..1a594f3 --- /dev/null +++ b/bindings/python/src/generated/m_statistics.rs @@ -0,0 +1,48 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Compute factorial of n: n! = 1 × 2 × ... × n +/// +/// Rust: `statistics::factorial` +#[pyfunction] +#[pyo3(name = "factorial", signature = (n))] +pub fn pyfn_factorial(n: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistics::factorial(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Compute the gamma function via Lanczos approximation: Γ(z). +/// Thin wrapper over `special::gamma::gamma`, kept for +/// backwards compatibility. +/// +/// Rust: `statistics::gamma_lanczos` +#[pyfunction] +#[pyo3(name = "gamma_lanczos", signature = (z))] +pub fn pyfn_gamma_lanczos(z: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistics::gamma_lanczos(z)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_factorial, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gamma_lanczos, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_statistics__descriptive.rs b/bindings/python/src/generated/m_statistics__descriptive.rs new file mode 100644 index 0000000..f10e323 --- /dev/null +++ b/bindings/python/src/generated/m_statistics__descriptive.rs @@ -0,0 +1,170 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Arithmetic mean of a data set: μ = (Σxᵢ) / n +/// The sum is computed with Neumaier compensated summation. +/// +/// Rust: `statistics::descriptive::mean` +#[pyfunction] +#[pyo3(name = "mean", signature = (data))] +pub fn pyfn_mean<'py>(py: Python<'py>, data: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistics::descriptive::mean(&data))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Population variance: σ² = Σ(xᵢ - μ)² / n +/// The sum of squared deviations is computed with Neumaier compensated summation. +/// +/// Rust: `statistics::descriptive::variance` +#[pyfunction] +#[pyo3(name = "variance", signature = (data))] +pub fn pyfn_variance<'py>(py: Python<'py>, data: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistics::descriptive::variance(&data))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Population standard deviation: σ = sqrt(σ²) +/// +/// Rust: `statistics::descriptive::std_deviation` +#[pyfunction] +#[pyo3(name = "std_deviation", signature = (data))] +pub fn pyfn_std_deviation<'py>(py: Python<'py>, data: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistics::descriptive::std_deviation(&data))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Sample variance with Bessel's correction: s² = Σ(xᵢ - x̄)² / (n - 1) +/// +/// Rust: `statistics::descriptive::sample_variance` +#[pyfunction] +#[pyo3(name = "sample_variance", signature = (data))] +pub fn pyfn_sample_variance<'py>(py: Python<'py>, data: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistics::descriptive::sample_variance(&data))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Sample standard deviation: s = sqrt(s²) +/// +/// Rust: `statistics::descriptive::sample_std_deviation` +#[pyfunction] +#[pyo3(name = "sample_std_deviation", signature = (data))] +pub fn pyfn_sample_std_deviation<'py>(py: Python<'py>, data: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistics::descriptive::sample_std_deviation(&data))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Median of a data set (sorts the slice in place) +/// +/// Rust: `statistics::descriptive::median` +#[pyfunction] +#[pyo3(name = "median", signature = (data))] +pub fn pyfn_median<'py>(data: pyo3::Bound<'py, pyo3::PyAny>) -> PyResult { + let mut data__v: Vec = data.extract()?; + let __r = crate::runtime::guard(|| rust_physics_engine::statistics::descriptive::median(&mut data__v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + crate::runtime::coerce::write_back(&data, &data__v)?; + Ok(__v) +} + +/// Population covariance of two data sets: cov(X,Y) = Σ(xᵢ - μₓ)(yᵢ - μᵧ) / n +/// +/// Rust: `statistics::descriptive::covariance` +#[pyfunction] +#[pyo3(name = "covariance", signature = (x, y))] +pub fn pyfn_covariance<'py>(py: Python<'py>, x: Vec, y: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistics::descriptive::covariance(&x, &y))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Pearson correlation coefficient: r = cov(X,Y) / (σₓ · σᵧ) +/// +/// Rust: `statistics::descriptive::correlation` +#[pyfunction] +#[pyo3(name = "correlation", signature = (x, y))] +pub fn pyfn_correlation<'py>(py: Python<'py>, x: Vec, y: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistics::descriptive::correlation(&x, &y))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Error propagation for sums: δ_total = sqrt(Σδᵢ²) +/// +/// Rust: `statistics::descriptive::error_propagation_sum` +#[pyfunction] +#[pyo3(name = "error_propagation_sum", signature = (errors))] +pub fn pyfn_error_propagation_sum<'py>(py: Python<'py>, errors: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistics::descriptive::error_propagation_sum(&errors))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Error propagation for products using relative errors: δ_rel = sqrt(Σ(δᵢ/vᵢ)²) +/// +/// Rust: `statistics::descriptive::error_propagation_product` +#[pyfunction] +#[pyo3(name = "error_propagation_product", signature = (values, relative_errors))] +pub fn pyfn_error_propagation_product<'py>(py: Python<'py>, values: Vec, relative_errors: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistics::descriptive::error_propagation_product(&values, &relative_errors))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Weighted mean: x̄_w = Σ(wᵢ·xᵢ) / Σwᵢ +/// +/// Rust: `statistics::descriptive::weighted_mean` +#[pyfunction] +#[pyo3(name = "weighted_mean", signature = (values, weights))] +pub fn pyfn_weighted_mean<'py>(py: Python<'py>, values: Vec, weights: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistics::descriptive::weighted_mean(&values, &weights))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Weighted mean uncertainty: δ = 1 / sqrt(Σwᵢ) +/// +/// Rust: `statistics::descriptive::weighted_mean_error` +#[pyfunction] +#[pyo3(name = "weighted_mean_error", signature = (weights))] +pub fn pyfn_weighted_mean_error<'py>(py: Python<'py>, weights: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistics::descriptive::weighted_mean_error(&weights))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_mean, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_variance, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_std_deviation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sample_variance, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sample_std_deviation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_median, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_covariance, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_correlation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_error_propagation_sum, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_error_propagation_product, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_weighted_mean, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_weighted_mean_error, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_statistics__distributions.rs b/bindings/python/src/generated/m_statistics__distributions.rs new file mode 100644 index 0000000..8a17b06 --- /dev/null +++ b/bindings/python/src/generated/m_statistics__distributions.rs @@ -0,0 +1,118 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Gaussian probability density function: f(x) = (1/(σ√(2π))) · exp(-½((x-μ)/σ)²) +/// +/// Rust: `statistics::distributions::gaussian` +#[pyfunction] +#[pyo3(name = "gaussian", signature = (x, mu, sigma))] +pub fn pyfn_gaussian(x: f64, mu: f64, sigma: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistics::distributions::gaussian(x, mu, sigma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Gaussian CDF: Φ(x) = ½·erfc(−(x−μ)/(σ√2)), full double precision. +/// +/// Rust: `statistics::distributions::gaussian_cdf` +#[pyfunction] +#[pyo3(name = "gaussian_cdf", signature = (x, mu, sigma))] +pub fn pyfn_gaussian_cdf(x: f64, mu: f64, sigma: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistics::distributions::gaussian_cdf(x, mu, sigma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Deprecated alias of `gaussian_cdf`; the historical Abramowitz & +/// Stegun approximation has been replaced by the exact erfc form. +/// +/// Rust: `statistics::distributions::gaussian_cdf_approx` +#[pyfunction] +#[pyo3(name = "gaussian_cdf_approx", signature = (x, mu, sigma))] +pub fn pyfn_gaussian_cdf_approx(x: f64, mu: f64, sigma: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistics::distributions::gaussian_cdf_approx(x, mu, sigma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Poisson probability mass function: P(k;λ) = λᵏ · e⁻λ / k! +/// +/// Rust: `statistics::distributions::poisson_pmf` +#[pyfunction] +#[pyo3(name = "poisson_pmf", signature = (k, lambda_))] +pub fn pyfn_poisson_pmf(k: u64, lambda_: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistics::distributions::poisson_pmf(k, lambda_)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Exponential probability density function: f(x;λ) = λ · e⁻ˡˣ for x ≥ 0 +/// +/// Rust: `statistics::distributions::exponential_pdf` +#[pyfunction] +#[pyo3(name = "exponential_pdf", signature = (x, lambda_))] +pub fn pyfn_exponential_pdf(x: f64, lambda_: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistics::distributions::exponential_pdf(x, lambda_)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Exponential cumulative distribution function: F(x;λ) = 1 - e⁻ˡˣ for x ≥ 0 +/// +/// Rust: `statistics::distributions::exponential_cdf` +#[pyfunction] +#[pyo3(name = "exponential_cdf", signature = (x, lambda_))] +pub fn pyfn_exponential_cdf(x: f64, lambda_: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistics::distributions::exponential_cdf(x, lambda_)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Chi-squared PDF: f(x;k) = x^(k/2-1)·e^(-x/2) / (2^(k/2)·Γ(k/2)) +/// +/// Rust: `statistics::distributions::chi_squared_pdf` +#[pyfunction] +#[pyo3(name = "chi_squared_pdf", signature = (x, k))] +pub fn pyfn_chi_squared_pdf(x: f64, k: u32) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistics::distributions::chi_squared_pdf(x, k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_gaussian, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gaussian_cdf, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gaussian_cdf_approx, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_poisson_pmf, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_exponential_pdf, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_exponential_cdf, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_chi_squared_pdf, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_statistics__fourier.rs b/bindings/python/src/generated/m_statistics__fourier.rs new file mode 100644 index 0000000..204fb3f --- /dev/null +++ b/bindings/python/src/generated/m_statistics__fourier.rs @@ -0,0 +1,74 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Discrete Fourier Transform: `X[k] = Σ x[n]·e^(-j2πkn/N)`, returns (real, imag) pairs +/// +/// Rust: `statistics::fourier::dft` +#[pyfunction] +#[pyo3(name = "dft", signature = (signal))] +pub fn pyfn_dft<'py>(py: Python<'py>, signal: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistics::fourier::dft(&signal))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Inverse DFT: `x[n] = (1/N)·Σ X[k]·e^(j2πkn/N)` +/// +/// Rust: `statistics::fourier::inverse_dft` +#[pyfunction] +#[pyo3(name = "inverse_dft", signature = (spectrum))] +pub fn pyfn_inverse_dft<'py>(py: Python<'py>, spectrum: Vec<(f64, f64)>) -> PyResult> { + let spectrum = spectrum.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistics::fourier::inverse_dft(&spectrum))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Power spectrum: `|X[k]|² = Re² + Im²` for each frequency bin. +/// +/// Uses the real FFT and reconstructs the upper half from conjugate +/// symmetry. Output length always equals `signal.len()`. +/// +/// Rust: `statistics::fourier::power_spectrum` +#[pyfunction] +#[pyo3(name = "power_spectrum", signature = (signal))] +pub fn pyfn_power_spectrum<'py>(py: Python<'py>, signal: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistics::fourier::power_spectrum(&signal))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Find the dominant frequency in a signal: f_peak = k_max · f_s / N +/// +/// Rust: `statistics::fourier::dominant_frequency` +#[pyfunction] +#[pyo3(name = "dominant_frequency", signature = (signal, sample_rate))] +pub fn pyfn_dominant_frequency<'py>(py: Python<'py>, signal: Vec, sample_rate: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistics::fourier::dominant_frequency(&signal, sample_rate))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_dft, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_inverse_dft, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_power_spectrum, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dominant_frequency, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_statistics__inference.rs b/bindings/python/src/generated/m_statistics__inference.rs new file mode 100644 index 0000000..008d4df --- /dev/null +++ b/bindings/python/src/generated/m_statistics__inference.rs @@ -0,0 +1,205 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// One-sample t test of H₀: μ = mu0. +/// +/// R cross-check: `t.test(c(1,2,3,4,5), mu=2)` gives +/// t = 1.4142, df = 4, p-value = 0.2302. +/// +/// Panics: +/// Panics unless x has at least 2 elements. +/// +/// Rust: `statistics::inference::t_test_one_sample` +#[pyfunction] +#[pyo3(name = "t_test_one_sample", signature = (x, mu0))] +pub fn pyfn_t_test_one_sample(x: Vec, mu0: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistics::inference::t_test_one_sample(&x, mu0)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTestResult { inner: __v }) +} + +/// Two-sample t test of H₀: μₓ = μᵧ. `equal_var` selects the pooled +/// test; otherwise Welch's test with Welch-Satterthwaite df. +/// +/// Analytic cross-check: x = 1..5 vs y = 2,4,…,10 with equal variances +/// gives t = −3/√2.5 = −1.897367 on 8 df (p ≈ 0.094); Welch df is +/// exactly 6.25/1.0625 = 5.882353. +/// +/// Panics: +/// Panics unless both samples have at least 2 elements. +/// +/// Rust: `statistics::inference::t_test_two_sample` +#[pyfunction] +#[pyo3(name = "t_test_two_sample", signature = (x, y, equal_var))] +pub fn pyfn_t_test_two_sample(x: Vec, y: Vec, equal_var: bool) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistics::inference::t_test_two_sample(&x, &y, equal_var)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTestResult { inner: __v }) +} + +/// Paired t test: one-sample test on the pairwise differences. +/// +/// Panics: +/// Panics unless the samples are the same length with n ≥ 2. +/// +/// Rust: `statistics::inference::t_test_paired` +#[pyfunction] +#[pyo3(name = "t_test_paired", signature = (x, y))] +pub fn pyfn_t_test_paired(x: Vec, y: Vec) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistics::inference::t_test_paired(&x, &y)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTestResult { inner: __v }) +} + +/// Chi-squared goodness-of-fit test: Σ (O − E)²/E with k − 1 df. +/// +/// R cross-check: `chisq.test(c(10,20,30,40), p=rep(0.25,4))` gives +/// X-squared = 20, df = 3, p-value = 0.0001697. +/// +/// Panics: +/// Panics unless the slices match in length (≥ 2) and all expected +/// counts are positive. +/// +/// Rust: `statistics::inference::chi_squared_gof` +#[pyfunction] +#[pyo3(name = "chi_squared_gof", signature = (observed, expected))] +pub fn pyfn_chi_squared_gof(observed: Vec, expected: Vec) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistics::inference::chi_squared_gof(&observed, &expected)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTestResult { inner: __v }) +} + +/// Chi-squared test of independence on an r×c contingency table; +/// expected counts from the margins, (r−1)(c−1) df (reported as `df`). +/// +/// Panics: +/// Panics unless the table is at least 2×2 with non-negative entries +/// and positive margins. +/// +/// Rust: `statistics::inference::chi_squared_independence` +#[pyfunction] +#[pyo3(name = "chi_squared_independence", signature = (table))] +pub fn pyfn_chi_squared_independence(table: crate::generated::types::PyMatrixArg) -> PyResult { + let table = table.0; + let __r = crate::runtime::guard(|| rust_physics_engine::statistics::inference::chi_squared_independence(&table)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTestResult { inner: __v }) +} + +/// One-sample Kolmogorov-Smirnov test of x against a continuous CDF. +/// `statistic` is Dₙ; `df` reports the sample size; p uses the NR +/// asymptotic correction λ = (√n + 0.12 + 0.11/√n)·D. +/// +/// Panics: +/// Panics if x is empty. +/// +/// Rust: `statistics::inference::ks_test_one_sample` +#[pyfunction] +#[pyo3(name = "ks_test_one_sample", signature = (x, cdf))] +pub fn pyfn_ks_test_one_sample(x: Vec, cdf: pyo3::Py) -> PyResult { + let __cb_cdf = std::rc::Rc::new(crate::runtime::Callback::new(cdf)); + let cdf = { let __cb = __cb_cdf.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::statistics::inference::ks_test_one_sample(&x, &cdf)); + crate::runtime::callback::check(&[&__cb_cdf], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTestResult { inner: __v }) +} + +/// Two-sample Kolmogorov-Smirnov test. `df` reports the effective +/// sample size n₁n₂/(n₁+n₂). +/// +/// Panics: +/// Panics if either sample is empty. +/// +/// Rust: `statistics::inference::ks_test_two_sample` +#[pyfunction] +#[pyo3(name = "ks_test_two_sample", signature = (x, y))] +pub fn pyfn_ks_test_two_sample(x: Vec, y: Vec) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistics::inference::ks_test_two_sample(&x, &y)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTestResult { inner: __v }) +} + +/// One-way ANOVA. `statistic` is F; `df` reports the between-groups +/// (numerator) df k − 1; the p-value uses F(k − 1, N − k). +/// +/// Analytic cross-check: groups (1,2,3), (2,3,4), (5,6,7) give +/// SS_between = 26, SS_within = 6, F = 13 on (2, 6) df. +/// +/// Panics: +/// Panics unless there are ≥ 2 groups, each non-empty, with more total +/// observations than groups. +/// +/// Rust: `statistics::inference::anova_one_way` +#[pyfunction] +#[pyo3(name = "anova_one_way", signature = (groups))] +pub fn pyfn_anova_one_way(groups: Vec>) -> PyResult { + let groups__b: Vec<&[f64]> = groups.iter().map(|__b| (*__b).as_slice()).collect(); + let __r = crate::runtime::guard(|| rust_physics_engine::statistics::inference::anova_one_way(&groups__b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTestResult { inner: __v }) +} + +/// Two-sided confidence interval for the mean at the given level +/// (e.g. 0.95): x̄ ± t·s/√n. +/// +/// Panics: +/// Panics unless n ≥ 2 and level ∈ (0, 1). +/// +/// Rust: `statistics::inference::confidence_interval_mean` +#[pyfunction] +#[pyo3(name = "confidence_interval_mean", signature = (x, level))] +pub fn pyfn_confidence_interval_mean<'py>(py: Python<'py>, x: Vec, level: f64) -> PyResult<(f64, f64)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistics::inference::confidence_interval_mean(&x, level))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Test of H₀: ρ = 0 from the Pearson correlation: +/// t = r·√((n−2)/(1−r²)) with n − 2 df. +/// +/// R cross-check: `cor.test(c(1,2,3,4,5), c(2,1,4,3,5))` gives +/// r = 0.8, t = 2.3094, df = 3, p-value = 0.1041. +/// +/// Panics: +/// Panics unless both slices have equal length n ≥ 3. +/// +/// Rust: `statistics::inference::pearson_test` +#[pyfunction] +#[pyo3(name = "pearson_test", signature = (x, y))] +pub fn pyfn_pearson_test(x: Vec, y: Vec) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistics::inference::pearson_test(&x, &y)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTestResult { inner: __v }) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_t_test_one_sample, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_t_test_two_sample, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_t_test_paired, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_chi_squared_gof, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_chi_squared_independence, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ks_test_one_sample, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ks_test_two_sample, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_anova_one_way, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_confidence_interval_mean, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pearson_test, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_statistics__resampling.rs b/bindings/python/src/generated/m_statistics__resampling.rs new file mode 100644 index 0000000..295736f --- /dev/null +++ b/bindings/python/src/generated/m_statistics__resampling.rs @@ -0,0 +1,107 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Percentile bootstrap for an arbitrary statistic at the given +/// confidence level (e.g. 0.95). +/// +/// Panics: +/// Panics unless data is non-empty, n_resamples ≥ 2, and +/// level ∈ (0, 1). +/// +/// Rust: `statistics::resampling::bootstrap` +#[pyfunction] +#[pyo3(name = "bootstrap", signature = (data, statistic, n_resamples, level, rng))] +pub fn pyfn_bootstrap(data: Vec, statistic: pyo3::Py, n_resamples: usize, level: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let __cb_statistic = std::rc::Rc::new(crate::runtime::Callback::new(statistic)); + let statistic = { let __cb = __cb_statistic.clone(); move |__a0: &[f64]| -> f64 { __cb.call::<_, f64>((__a0.to_vec(),), f64::NAN) } }; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::statistics::resampling::bootstrap(&data, &statistic, n_resamples, level, &mut rng.inner)); + crate::runtime::callback::check(&[&__cb_statistic], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBootstrapResult { inner: __v }) +} + +/// Bias-corrected and accelerated (BCa) bootstrap: percentile interval +/// with the bias correction z₀ from the replicate distribution and the +/// acceleration a from the jackknife influence values. +/// +/// Panics: +/// Panics unless data has ≥ 2 points, n_resamples ≥ 2, and +/// level ∈ (0, 1). +/// +/// Rust: `statistics::resampling::bootstrap_bca` +#[pyfunction] +#[pyo3(name = "bootstrap_bca", signature = (data, statistic, n_resamples, level, rng))] +pub fn pyfn_bootstrap_bca(data: Vec, statistic: pyo3::Py, n_resamples: usize, level: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let __cb_statistic = std::rc::Rc::new(crate::runtime::Callback::new(statistic)); + let statistic = { let __cb = __cb_statistic.clone(); move |__a0: &[f64]| -> f64 { __cb.call::<_, f64>((__a0.to_vec(),), f64::NAN) } }; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::statistics::resampling::bootstrap_bca(&data, &statistic, n_resamples, level, &mut rng.inner)); + crate::runtime::callback::check(&[&__cb_statistic], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBootstrapResult { inner: __v }) +} + +/// Two-sample permutation test. `statistic` maps (x, y) to a test +/// statistic (e.g. difference of means); the returned p-value is the +/// fraction of label permutations with |T*| ≥ |T| (with the +1 +/// continuity correction). +/// +/// Panics: +/// Panics unless both samples are non-empty and n_perm ≥ 1. +/// +/// Rust: `statistics::resampling::permutation_test` +#[pyfunction] +#[pyo3(name = "permutation_test", signature = (x, y, statistic, n_perm, rng))] +pub fn pyfn_permutation_test(x: Vec, y: Vec, statistic: pyo3::Py, n_perm: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let __cb_statistic = std::rc::Rc::new(crate::runtime::Callback::new(statistic)); + let statistic = { let __cb = __cb_statistic.clone(); move |__a0: &[f64], __a1: &[f64]| -> f64 { __cb.call::<_, f64>((__a0.to_vec(), __a1.to_vec()), f64::NAN) } }; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::statistics::resampling::permutation_test(&x, &y, &statistic, n_perm, &mut rng.inner)); + crate::runtime::callback::check(&[&__cb_statistic], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Jackknife estimate and standard error of a statistic: +/// SE² = (n−1)/n · Σ (θ₍ᵢ₎ − θ̄)². +/// +/// Panics: +/// Panics unless data has at least 2 points. +/// +/// Rust: `statistics::resampling::jackknife` +#[pyfunction] +#[pyo3(name = "jackknife", signature = (data, statistic))] +pub fn pyfn_jackknife(data: Vec, statistic: pyo3::Py) -> PyResult<(f64, f64)> { + let __cb_statistic = std::rc::Rc::new(crate::runtime::Callback::new(statistic)); + let statistic = { let __cb = __cb_statistic.clone(); move |__a0: &[f64]| -> f64 { __cb.call::<_, f64>((__a0.to_vec(),), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::statistics::resampling::jackknife(&data, &statistic)); + crate::runtime::callback::check(&[&__cb_statistic], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_bootstrap, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bootstrap_bca, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_permutation_test, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_jackknife, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_stochastic.rs b/bindings/python/src/generated/m_stochastic.rs new file mode 100644 index 0000000..c02bbf8 --- /dev/null +++ b/bindings/python/src/generated/m_stochastic.rs @@ -0,0 +1,22 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + Ok(()) +} diff --git a/bindings/python/src/generated/m_stochastic__extreme.rs b/bindings/python/src/generated/m_stochastic__extreme.rs new file mode 100644 index 0000000..6e66585 --- /dev/null +++ b/bindings/python/src/generated/m_stochastic__extreme.rs @@ -0,0 +1,607 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// The generalised extreme value density. +/// +/// Outside the support -- above the upper endpoint when `xi < 0`, below the +/// lower one when `xi > 0` -- the density is zero. +/// +/// Panics: +/// Panics unless `sigma` is positive. +/// +/// Rust: `stochastic::extreme::gev_pdf` +#[pyfunction] +#[pyo3(name = "gev_pdf", signature = (x, mu, sigma, xi))] +pub fn pyfn_gev_pdf(x: f64, mu: f64, sigma: f64, xi: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::extreme::gev_pdf(x, mu, sigma, xi)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The generalised extreme value distribution function, +/// `exp(-[1 + xi (x - mu)/sigma]^(-1/xi))`. +/// +/// Panics: +/// Panics unless `sigma` is positive. +/// +/// Rust: `stochastic::extreme::gev_cdf` +#[pyfunction] +#[pyo3(name = "gev_cdf", signature = (x, mu, sigma, xi))] +pub fn pyfn_gev_cdf(x: f64, mu: f64, sigma: f64, xi: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::extreme::gev_cdf(x, mu, sigma, xi)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The GEV quantile at probability `p`. +/// +/// `mu + (sigma / xi) [(-ln p)^(-xi) - 1]`, or the Gumbel form +/// `mu - sigma ln(-ln p)` when the shape vanishes. +/// +/// Panics: +/// Panics unless `sigma` is positive and `p` lies strictly in `(0, 1)`. +/// +/// Rust: `stochastic::extreme::gev_quantile` +#[pyfunction] +#[pyo3(name = "gev_quantile", signature = (p, mu, sigma, xi))] +pub fn pyfn_gev_quantile(p: f64, mu: f64, sigma: f64, xi: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::extreme::gev_quantile(p, mu, sigma, xi)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Fits a GEV to block maxima by maximum likelihood, returning +/// `(location, scale, shape)`. +/// +/// The scale is optimised on the log scale so it cannot go negative, and the +/// likelihood is infinite wherever an observation would fall outside the +/// support, which keeps the search inside the feasible region without an +/// explicit constraint. Started from the moment-matched Gumbel fit, which is +/// the shape-zero member of the family and a reliable neighbourhood to +/// descend from. +/// +/// Errors: +/// Returns an error for fewer than ten observations, or if no feasible +/// parameter set is found. +/// +/// Rust: `stochastic::extreme::gev_fit` +#[pyfunction] +#[pyo3(name = "gev_fit", signature = (maxima))] +pub fn pyfn_gev_fit<'py>(py: Python<'py>, maxima: Vec) -> PyResult<(f64, f64, f64)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::extreme::gev_fit(&maxima))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// Fits a Gumbel distribution -- the GEV with shape fixed at zero -- by +/// maximum likelihood, returning `(location, scale)`. +/// +/// Worth fitting separately rather than reading off a GEV fit: with the shape +/// pinned, the two remaining parameters are far better determined, and the +/// difference in log-likelihood against the free-shape fit is the natural +/// test of whether the tail is exponential. +/// +/// Errors: +/// Returns an error for fewer than five observations or a constant sample. +/// +/// Rust: `stochastic::extreme::gumbel_fit` +#[pyfunction] +#[pyo3(name = "gumbel_fit", signature = (maxima))] +pub fn pyfn_gumbel_fit<'py>(py: Python<'py>, maxima: Vec) -> PyResult<(f64, f64)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::extreme::gumbel_fit(&maxima))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// The generalised Pareto density for an exceedance `y > 0`. +/// +/// Panics: +/// Panics unless `sigma` is positive. +/// +/// Rust: `stochastic::extreme::gpd_pdf` +#[pyfunction] +#[pyo3(name = "gpd_pdf", signature = (y, sigma, xi))] +pub fn pyfn_gpd_pdf(y: f64, sigma: f64, xi: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::extreme::gpd_pdf(y, sigma, xi)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The generalised Pareto distribution function, +/// `1 - (1 + xi y / sigma)^(-1/xi)`. +/// +/// Panics: +/// Panics unless `sigma` is positive. +/// +/// Rust: `stochastic::extreme::gpd_cdf` +#[pyfunction] +#[pyo3(name = "gpd_cdf", signature = (y, sigma, xi))] +pub fn pyfn_gpd_cdf(y: f64, sigma: f64, xi: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::extreme::gpd_cdf(y, sigma, xi)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The generalised Pareto quantile at probability `p`. +/// +/// Panics: +/// Panics unless `sigma` is positive and `p` lies in `[0, 1)`. +/// +/// Rust: `stochastic::extreme::gpd_quantile` +#[pyfunction] +#[pyo3(name = "gpd_quantile", signature = (p, sigma, xi))] +pub fn pyfn_gpd_quantile(p: f64, sigma: f64, xi: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::extreme::gpd_quantile(p, sigma, xi)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Fits a generalised Pareto distribution to threshold exceedances by +/// maximum likelihood, returning `(scale, shape)`. +/// +/// The exceedances must already be measured from the threshold, so they are +/// all positive. This is the peaks-over-threshold half of the theory: by +/// Pickands-Balkema-de Haan the shape here is the same shape a GEV fit to +/// block maxima of the same data would find, but estimated from every large +/// observation rather than one per block. +/// +/// Errors: +/// Returns an error for fewer than ten exceedances, a non-positive +/// exceedance, or a failure to find feasible parameters. +/// +/// Rust: `stochastic::extreme::gpd_fit` +#[pyfunction] +#[pyo3(name = "gpd_fit", signature = (exceedances))] +pub fn pyfn_gpd_fit<'py>(py: Python<'py>, exceedances: Vec) -> PyResult<(f64, f64)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::extreme::gpd_fit(&exceedances))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// The mean excess over each threshold: the average of `x - u` across the +/// observations that exceed `u`. +/// +/// The standard threshold-selection diagnostic. If the exceedances over some +/// `u` follow a generalised Pareto, the mean excess above any higher +/// threshold is `(sigma + xi u) / (1 - xi)` -- *linear* in the threshold. So +/// the point above which the plot straightens is the point above which the +/// asymptotic theory has taken hold, and a slope of zero means an +/// exponential tail. +/// +/// A threshold exceeded by nothing yields NaN, which is reported rather than +/// silently dropped. +/// +/// Rust: `stochastic::extreme::mean_residual_life` +#[pyfunction] +#[pyo3(name = "mean_residual_life", signature = (x, thresholds))] +pub fn pyfn_mean_residual_life<'py>(py: Python<'py>, x: Vec, thresholds: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::extreme::mean_residual_life(&x, &thresholds))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Hill estimator of the tail index from the `k` largest observations. +/// +/// `(1/k) sum_{i=1}^{k} ln X_(i) - ln X_(k+1)`, where `X_(1)` is the largest. +/// Estimates `xi` for a heavy tail, and only for a heavy one: the derivation +/// assumes a regularly varying tail, so a negative or zero shape is outside +/// its scope and the estimator will still return a positive number there. +/// +/// Choosing `k` is the usual bias-variance trade: too small and the estimate +/// is noisy, too large and observations from the body contaminate it. +/// +/// Panics: +/// Panics unless `1 <= k < n` and all of the top `k + 1` observations are +/// positive. +/// +/// Rust: `stochastic::extreme::hill_estimator` +#[pyfunction] +#[pyo3(name = "hill_estimator", signature = (x, k))] +pub fn pyfn_hill_estimator<'py>(py: Python<'py>, x: Vec, k: usize) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::extreme::hill_estimator(&x, k))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The level exceeded on average once every `period` blocks, under a GEV fit. +/// +/// The quantile at `1 - 1/period`. A hundred-year level is not the largest +/// value seen in a century; it is the level with a one-in-a-hundred chance of +/// being exceeded in any given year. +/// +/// Panics: +/// Panics unless `sigma` is positive and `period` exceeds one. +/// +/// Rust: `stochastic::extreme::return_level` +#[pyfunction] +#[pyo3(name = "return_level", signature = (mu, sigma, xi, period))] +pub fn pyfn_return_level(mu: f64, sigma: f64, xi: f64, period: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::extreme::return_level(mu, sigma, xi, period)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The average number of blocks between exceedances of `level`, the exact +/// inverse of `return_level`. +/// +/// Infinite for a level at or above the finite upper endpoint of a bounded +/// tail, which is the honest answer: such a level is never exceeded. +/// +/// Panics: +/// Panics unless `sigma` is positive. +/// +/// Rust: `stochastic::extreme::return_period` +#[pyfunction] +#[pyo3(name = "return_period", signature = (mu, sigma, xi, level))] +pub fn pyfn_return_period(mu: f64, sigma: f64, xi: f64, level: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::extreme::return_period(mu, sigma, xi, level)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The maximum of each consecutive block of `block` observations. +/// +/// A trailing partial block is dropped: its maximum is drawn from fewer +/// observations and is not comparable with the rest, and including it biases +/// the fit downward. +/// +/// Panics: +/// Panics if `block` is zero. +/// +/// Rust: `stochastic::extreme::block_maxima` +#[pyfunction] +#[pyo3(name = "block_maxima", signature = (x, block))] +pub fn pyfn_block_maxima<'py>(py: Python<'py>, x: Vec, block: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::extreme::block_maxima(&x, block))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The extremal index by the Ferro-Segers intervals estimator. +/// +/// Roughly the reciprocal of the mean cluster size: 1 when exceedances arrive +/// independently, below 1 when they arrive in bursts. It matters because +/// clustering does not change *how many* exceedances there are but does +/// change how many *distinct events* they represent, and a return period +/// computed as though every exceedance were its own event overstates the +/// frequency by exactly this factor. +/// +/// The intervals estimator works from the gaps between exceedances rather +/// than from a declustering rule, so it needs no run length chosen. +/// +/// Returns 1 when there are too few exceedances to say anything. +/// +/// Panics: +/// Panics if `x` is empty. +/// +/// Rust: `stochastic::extreme::extremal_index` +#[pyfunction] +#[pyo3(name = "extremal_index", signature = (x, threshold))] +pub fn pyfn_extremal_index<'py>(py: Python<'py>, x: Vec, threshold: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::extreme::extremal_index(&x, threshold))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Kendall's tau: the probability of concordance minus the probability of +/// discordance, estimated over all pairs. +/// +/// Ties in either coordinate contribute nothing to either count. Unlike +/// Pearson correlation this depends only on the ranks, so it is invariant +/// under any increasing transformation of either variable -- which is exactly +/// what makes it a property of the copula rather than of the margins, and +/// what lets a copula parameter be recovered from it. +/// +/// Panics: +/// Panics unless the series have equal length and at least two points. +/// +/// Rust: `stochastic::extreme::kendall_tau` +#[pyfunction] +#[pyo3(name = "kendall_tau", signature = (x, y))] +pub fn pyfn_kendall_tau<'py>(py: Python<'py>, x: Vec, y: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::extreme::kendall_tau(&x, &y))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Spearman's rho: Pearson correlation applied to the ranks. +/// +/// Like Kendall's tau it is a function of the copula alone, but it weights +/// the whole distribution more evenly, so the two disagree in a way that is +/// itself informative about the shape of the dependence. +/// +/// Panics: +/// Panics unless the series have equal length and at least two points. +/// +/// Rust: `stochastic::extreme::spearman_rho` +#[pyfunction] +#[pyo3(name = "spearman_rho", signature = (x, y))] +pub fn pyfn_spearman_rho<'py>(py: Python<'py>, x: Vec, y: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::extreme::spearman_rho(&x, &y))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Samples `n` points from a Gaussian copula with the given correlation +/// matrix. +/// +/// Draws from a multivariate normal by a Cholesky factor and maps each margin +/// through the standard normal distribution function, which is what leaves +/// uniform margins and keeps only the dependence. +/// +/// Errors: +/// Returns an error if the matrix is not a valid correlation matrix -- not +/// square, not symmetric, or not positive definite. +/// +/// Rust: `stochastic::extreme::copula_gaussian_sample` +#[pyfunction] +#[pyo3(name = "copula_gaussian_sample", signature = (corr, n, rng))] +pub fn pyfn_copula_gaussian_sample(corr: crate::generated::types::PyMatrixArg, n: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult>> { + let corr = corr.0; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::extreme::copula_gaussian_sample(&corr, n, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Samples `n` points from a `t` copula with `df` degrees of freedom. +/// +/// The same construction as the Gaussian copula but with a shared chi-squared +/// scaling across all coordinates. That single shared factor is what creates +/// tail dependence: occasionally it is small, every coordinate is inflated at +/// once, and the sample lands in a corner. The Gaussian copula has no such +/// mechanism, which is why its tail dependence is exactly zero. +/// +/// Errors: +/// Returns an error for an invalid correlation matrix or `df` below one. +/// +/// Rust: `stochastic::extreme::copula_t_sample` +#[pyfunction] +#[pyo3(name = "copula_t_sample", signature = (corr, df, n, rng))] +pub fn pyfn_copula_t_sample(corr: crate::generated::types::PyMatrixArg, df: f64, n: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult>> { + let corr = corr.0; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::extreme::copula_t_sample(&corr, df, n, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Samples `n` pairs from a bivariate Clayton copula by conditional +/// inversion. +/// +/// `theta > 0`. Clayton concentrates its dependence in the *lower* tail: its +/// coefficient of lower tail dependence is `2^(-1/theta)`, while the upper is +/// zero. That asymmetry is the reason to reach for it -- joint crashes +/// without joint booms. +/// +/// Panics: +/// Panics unless `theta` is positive. +/// +/// Rust: `stochastic::extreme::copula_clayton` +#[pyfunction] +#[pyo3(name = "copula_clayton", signature = (theta, n, rng))] +pub fn pyfn_copula_clayton(theta: f64, n: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult>> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::extreme::copula_clayton(theta, n, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Samples `n` pairs from a bivariate Gumbel copula. +/// +/// `theta >= 1`. The mirror image of Clayton: upper tail dependence +/// `2 - 2^(1/theta)` and none in the lower tail. +/// +/// The conditional distribution has no closed-form inverse, so this uses the +/// Marshall-Olkin frailty construction instead. The Gumbel generator is the +/// Laplace transform of a positive stable law, so drawing one such variate +/// and dividing two independent exponentials by it produces the copula +/// directly. The stable variate comes from Kanter's algorithm. +/// +/// Panics: +/// Panics unless `theta >= 1`. +/// +/// Rust: `stochastic::extreme::copula_gumbel` +#[pyfunction] +#[pyo3(name = "copula_gumbel", signature = (theta, n, rng))] +pub fn pyfn_copula_gumbel(theta: f64, n: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult>> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::extreme::copula_gumbel(theta, n, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Samples `n` pairs from a bivariate Frank copula by conditional inversion. +/// +/// `theta` may be any non-zero real: positive for positive dependence, +/// negative for negative. Frank is the symmetric Archimedean copula, with no +/// tail dependence in either direction -- useful precisely when dependence in +/// the body should not imply dependence in the extremes. +/// +/// Panics: +/// Panics if `theta` is zero, where the family degenerates to independence. +/// +/// Rust: `stochastic::extreme::copula_frank` +#[pyfunction] +#[pyo3(name = "copula_frank", signature = (theta, n, rng))] +pub fn pyfn_copula_frank(theta: f64, n: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult>> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::extreme::copula_frank(theta, n, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Kendall's tau implied by a copula family at parameter `theta`. +/// +/// Each family has a closed-form relation, which is what makes inversion +/// possible: `2 arcsin(rho) / pi` for the Gaussian, `theta / (theta + 2)` for +/// Clayton, `1 - 1/theta` for Gumbel, and for Frank +/// `1 - 4 (1 - D_1(theta)) / theta` with `D_1` the Debye function. +/// +/// Rust: `stochastic::extreme::copula_tau` +#[pyfunction] +#[pyo3(name = "copula_tau", signature = (family, theta))] +pub fn pyfn_copula_tau(family: crate::generated::types::PyCopulaFamily, theta: f64) -> PyResult { + let family = family.to_rust(); + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::extreme::copula_tau(family, theta)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Fits a copula parameter by inverting Kendall's tau. +/// +/// The method of moments applied to a rank statistic: measure tau from the +/// data, then solve the family's tau-theta relation for theta. It needs no +/// likelihood and no numerical optimisation for three of the four families, +/// and because tau depends only on the ranks the answer is unaffected by +/// whatever the margins happen to be -- which is the entire point of +/// separating a copula from its margins. +/// +/// `data` holds one row per observation with two columns. +/// +/// Errors: +/// Returns an error for the wrong shape, or for a sample tau outside the +/// range the family can represent -- Clayton and Gumbel model only positive +/// dependence, so a negative tau has no solution. +/// +/// Rust: `stochastic::extreme::copula_fit_tau` +#[pyfunction] +#[pyo3(name = "copula_fit_tau", signature = (data, family))] +pub fn pyfn_copula_fit_tau<'py>(py: Python<'py>, data: Vec>, family: crate::generated::types::PyCopulaFamily) -> PyResult { + let family = family.to_rust(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::extreme::copula_fit_tau(&data, family))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The pseudo-observations of a sample: each column replaced by its ranks +/// divided by `n + 1`. +/// +/// This is the empirical copula transform. Dividing by `n + 1` rather than +/// `n` keeps every value strictly inside `(0, 1)`, which matters because the +/// copula densities and tail statistics below take logarithms of them. +/// Whatever the marginal distributions were, the result has approximately +/// uniform margins and retains exactly the original dependence. +/// +/// Errors: +/// Returns an error for empty or ragged input. +/// +/// Rust: `stochastic::extreme::empirical_copula` +#[pyfunction] +#[pyo3(name = "empirical_copula", signature = (data))] +pub fn pyfn_empirical_copula<'py>(py: Python<'py>, data: Vec>) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::extreme::empirical_copula(&data))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Empirical coefficients of `(lower, upper)` tail dependence at quantile +/// level `q`. +/// +/// The lower coefficient estimates `P(V <= q | U <= q)` and the upper +/// `P(V > q | U > q)`, both computed on pseudo-observations so the margins +/// are irrelevant. Only one of the pair is informative at any given `q`: read +/// the lower coefficient at a small `q` and the upper at a `q` near one. At +/// `q = 0.01` the upper coefficient is the probability both variables exceed +/// their first percentile, which is close to one for any sample and says +/// nothing about the tail. As `q` approaches its limit these tend to the theoretical +/// coefficients: `2^(-1/theta)` and 0 for Clayton, 0 and `2 - 2^(1/theta)` +/// for Gumbel, and 0 for both under any Gaussian copula with correlation +/// below one. +/// +/// The last of those is the practically important one. Two variables can have +/// a correlation of 0.9 and still, under a Gaussian copula, become +/// independent in the limit of extreme events. +/// +/// Errors: +/// Returns an error for the wrong shape or a `q` outside `(0, 1)`. +/// +/// Rust: `stochastic::extreme::tail_dependence_coefficient` +#[pyfunction] +#[pyo3(name = "tail_dependence_coefficient", signature = (data, q))] +pub fn pyfn_tail_dependence_coefficient<'py>(py: Python<'py>, data: Vec>, q: f64) -> PyResult<(f64, f64)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::extreme::tail_dependence_coefficient(&data, q))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) +} + +/// The Pickands dependence function estimated at `t`, for a bivariate +/// extreme-value copula. +/// +/// An extreme-value copula is determined entirely by a convex function `A` on +/// `[0, 1]` satisfying `max(t, 1-t) <= A(t) <= 1`. The two bounds are the two +/// extremes of dependence: `A == 1` is independence, and `A(t) = max(t, 1-t)` +/// is perfect dependence. Everything in between is a real dependence +/// structure, and `A` is the whole of it. +/// +/// Estimated by Pickands' original construction, the reciprocal of the mean +/// of `min(xi/(1-t), eta/t)` over the transformed data. +/// +/// Errors: +/// Returns an error for the wrong shape or a `t` outside `(0, 1)`. +/// +/// Rust: `stochastic::extreme::pickands_dependence` +#[pyfunction] +#[pyo3(name = "pickands_dependence", signature = (data, t))] +pub fn pyfn_pickands_dependence<'py>(py: Python<'py>, data: Vec>, t: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::extreme::pickands_dependence(&data, t))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_gev_pdf, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gev_cdf, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gev_quantile, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gev_fit, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gumbel_fit, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gpd_pdf, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gpd_cdf, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gpd_quantile, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gpd_fit, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mean_residual_life, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hill_estimator, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_return_level, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_return_period, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_block_maxima, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_extremal_index, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kendall_tau, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spearman_rho, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_copula_gaussian_sample, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_copula_t_sample, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_copula_clayton, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_copula_gumbel, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_copula_frank, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_copula_tau, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_copula_fit_tau, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_empirical_copula, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tail_dependence_coefficient, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pickands_dependence, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_stochastic__hmm.rs b/bindings/python/src/generated/m_stochastic__hmm.rs new file mode 100644 index 0000000..af68ced --- /dev/null +++ b/bindings/python/src/generated/m_stochastic__hmm.rs @@ -0,0 +1,133 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Runs a Kalman filter over a sequence of measurements, keeping every +/// intermediate so a smoother can walk back through them. +/// +/// Errors: +/// Returns an error if any linear solve fails. +/// +/// Rust: `stochastic::hmm::kalman_filter_sequence` +#[pyfunction] +#[pyo3(name = "kalman_filter_sequence", signature = (kf, measurements))] +pub fn pyfn_kalman_filter_sequence(kf: crate::generated::types::PyKalmanFilter, measurements: Vec>) -> PyResult> { + let kf = kf.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::hmm::kalman_filter_sequence(&kf, &measurements)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyFilterStep { inner: __x }).collect::>()) +} + +/// The Rauch-Tung-Striebel smoother: the best estimate of each state given +/// *all* the data, not just the data up to that point. +/// +/// A backward pass over the filter's output. Each smoothed estimate is the +/// filtered one corrected by how much the next step's smoothed estimate +/// disagreed with what the filter predicted, weighted by the gain +/// `P F' Ppred^-1`. Because it conditions on strictly more information than +/// the filter does, the smoothed covariance is never larger -- which is the +/// property the tests check, and the reason to run it at all. +/// +/// Errors: +/// Returns an error if any linear solve fails. +/// +/// Panics: +/// Panics on an empty sequence. +/// +/// Rust: `stochastic::hmm::rts_smooth` +#[pyfunction] +#[pyo3(name = "rts_smooth", signature = (kf, steps))] +pub fn pyfn_rts_smooth(kf: crate::generated::types::PyKalmanFilter, steps: Vec) -> PyResult<(Vec>, Vec)> { + let kf = kf.inner; + let steps = steps.into_iter().map(|__e| __e.inner).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::hmm::rts_smooth(&kf, &steps)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok((__v.0, __v.1.into_iter().map(|__x| crate::generated::types::PyMatrix { inner: __x }).collect::>())) +} + +/// The lag-one smoothed cross-covariances, which the +/// expectation-maximisation step needs and the plain smoother does not +/// return. +/// +/// `lag[k]` is the smoothed covariance between the state at `k` and the one +/// at `k - 1`, with `lag[0]` unused. Without it the process-noise estimate +/// has no way to know how correlated consecutive smoothed states are, and +/// treating them as independent inflates the residual it is built from. +/// +/// Errors: +/// Returns an error if any linear solve fails. +/// +/// Rust: `stochastic::hmm::rts_lag_one_covariances` +#[pyfunction] +#[pyo3(name = "rts_lag_one_covariances", signature = (kf, steps, smoothed_cov))] +pub fn pyfn_rts_lag_one_covariances(kf: crate::generated::types::PyKalmanFilter, steps: Vec, smoothed_cov: Vec) -> PyResult> { + let kf = kf.inner; + let steps = steps.into_iter().map(|__e| __e.inner).collect::>(); + let smoothed_cov = smoothed_cov.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::hmm::rts_lag_one_covariances(&kf, &steps, &smoothed_cov)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyMatrix { inner: __x }).collect::>()) +} + +/// Learns a Kalman filter's process and measurement noise from data, by +/// expectation-maximisation. +/// +/// The smoother gives the expected states and their covariances; those give +/// the noise covariances in closed form; those give a better smoother. As +/// with Baum-Welch, each round cannot lower the likelihood and the answer +/// depends on where it started. The dynamics and observation matrices are +/// taken as known, which is the usual situation -- they are physics, while +/// the noise is a fudge factor nobody knows. +/// +/// The covariance terms in the maximisation are not optional. The residual +/// of the smoothed states against the dynamics understates the process noise +/// on its own, because the smoothed states are shrunk towards each other; +/// the smoothed covariances are what put back the uncertainty that shrinkage +/// hid. +/// +/// Errors: +/// Returns an error if any linear solve fails. +/// +/// Panics: +/// Panics on an empty measurement sequence. +/// +/// Rust: `stochastic::hmm::em_kalman` +#[pyfunction] +#[pyo3(name = "em_kalman", signature = (initial, measurements, iters))] +pub fn pyfn_em_kalman(initial: crate::generated::types::PyKalmanFilter, measurements: Vec>, iters: usize) -> PyResult { + let initial = initial.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::hmm::em_kalman(&initial, &measurements, iters)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(crate::generated::types::PyKalmanFilter { inner: __v }) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_kalman_filter_sequence, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rts_smooth, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rts_lag_one_covariances, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_em_kalman, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_stochastic__markov.rs b/bindings/python/src/generated/m_stochastic__markov.rs new file mode 100644 index 0000000..e337088 --- /dev/null +++ b/bindings/python/src/generated/m_stochastic__markov.rs @@ -0,0 +1,25 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_stochastic__point_process.rs b/bindings/python/src/generated/m_stochastic__point_process.rs new file mode 100644 index 0000000..6b790ca --- /dev/null +++ b/bindings/python/src/generated/m_stochastic__point_process.rs @@ -0,0 +1,496 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Event times of a homogeneous Poisson process on `[0, t_end]`. +/// +/// Generated from exponential waiting times, which is the process's own +/// definition rather than a device: the memorylessness of the exponential is +/// exactly the memorylessness of the process. +/// +/// Panics: +/// Panics unless the rate is non-negative and `t_end` is positive. +/// +/// Rust: `stochastic::point_process::poisson_process` +#[pyfunction] +#[pyo3(name = "poisson_process", signature = (rate, t_end, rng))] +pub fn pyfn_poisson_process(rate: f64, t_end: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::point_process::poisson_process(rate, t_end, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Event times of a Poisson process whose rate varies with time, by thinning. +/// +/// Generate a homogeneous process at the maximum rate, then keep each point +/// with probability equal to the ratio of the true rate there to the +/// maximum. Lewis and Shedler's construction, and it is exact rather than an +/// approximation: the retained points have precisely the right intensity, +/// whatever shape the rate function has. +/// +/// Panics: +/// Panics unless `rate_max` is positive, `t_end` is positive, or if the rate +/// function exceeds the stated maximum, which would make the thinning wrong +/// rather than merely inefficient. +/// +/// Rust: `stochastic::point_process::poisson_inhomogeneous` +#[pyfunction] +#[pyo3(name = "poisson_inhomogeneous", signature = (rate_fn, rate_max, t_end, rng))] +pub fn pyfn_poisson_inhomogeneous(rate_fn: pyo3::Py, rate_max: f64, t_end: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let __cb_rate_fn = std::rc::Rc::new(crate::runtime::Callback::new(rate_fn)); + let rate_fn = { let __cb = __cb_rate_fn.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::point_process::poisson_inhomogeneous(&rate_fn, rate_max, t_end, &mut rng.inner)); + crate::runtime::callback::check(&[&__cb_rate_fn], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// A Poisson point pattern in a rectangle. +/// +/// The count is Poisson with mean `rate` times the area, and given the count +/// the points are independent and uniform -- which is the cleanest statement +/// of what complete spatial randomness means. +/// +/// Panics: +/// Panics unless the rate is non-negative and the rectangle has positive +/// area. +/// +/// Rust: `stochastic::point_process::poisson_2d` +#[pyfunction] +#[pyo3(name = "poisson_2d", signature = (rate, region, rng))] +pub fn pyfn_poisson_2d(rate: f64, region: crate::generated::types::PyRect, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let region = region.inner; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::point_process::poisson_2d(rate, ®ion, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// A Poisson point pattern in a box. +/// +/// Panics: +/// Panics unless the rate is non-negative and every side is positive. +/// +/// Rust: `stochastic::point_process::poisson_3d` +#[pyfunction] +#[pyo3(name = "poisson_3d", signature = (rate, min, max, rng))] +pub fn pyfn_poisson_3d(rate: f64, min: (f64, f64, f64), max: (f64, f64, f64), rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let min = (min.0, min.1, min.2); + let max = (max.0, max.1, max.2); + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::point_process::poisson_3d(rate, min, max, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, __x.2)).collect::>()) +} + +/// The conditional intensity of a Hawkes process with an exponential kernel. +/// +/// `mu + sum over past events of alpha exp(-beta (t - t_i))`. Each event +/// raises the chance of the next, and the excitation decays; the process is +/// its own trigger, which is what makes it a model for earthquakes and for +/// order flow alike. +/// +/// Rust: `stochastic::point_process::hawkes_intensity` +#[pyfunction] +#[pyo3(name = "hawkes_intensity", signature = (events, mu, alpha, beta, t))] +pub fn pyfn_hawkes_intensity<'py>(py: Python<'py>, events: Vec, mu: f64, alpha: f64, beta: f64, t: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::point_process::hawkes_intensity(&events, mu, alpha, beta, t))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The branching ratio `alpha / beta`: the expected number of events each +/// event directly triggers. +/// +/// Below one the process is stationary; at or above one it explodes, because +/// each generation of offspring is at least as large as the last. It is the +/// mean of a Galton-Watson offspring distribution wearing different clothes. +/// +/// Rust: `stochastic::point_process::hawkes_branching_ratio` +#[pyfunction] +#[pyo3(name = "hawkes_branching_ratio", signature = (alpha, beta))] +pub fn pyfn_hawkes_branching_ratio(alpha: f64, beta: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::point_process::hawkes_branching_ratio(alpha, beta)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// A Hawkes process with an exponential kernel, by Ogata's thinning. +/// +/// The intensity only ever falls between events, so it can be bounded by its +/// value just after the last one; propose from a homogeneous process at that +/// bound and accept in proportion. Rebounding after each event is what keeps +/// the acceptance rate high. +/// +/// Panics: +/// Panics unless `mu` and `beta` are positive, `alpha` is non-negative, and +/// the branching ratio is below one -- above it the process explodes and no +/// simulation terminates. +/// +/// Rust: `stochastic::point_process::hawkes_process` +#[pyfunction] +#[pyo3(name = "hawkes_process", signature = (mu, alpha, beta, t_end, rng))] +pub fn pyfn_hawkes_process(mu: f64, alpha: f64, beta: f64, t_end: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::point_process::hawkes_process(mu, alpha, beta, t_end, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The log-likelihood of a Hawkes process with an exponential kernel. +/// +/// `sum log lambda(t_i) - integral lambda`. The integral has a closed form +/// for this kernel, and the sum can be accumulated in one pass by the same +/// recursion, so the whole thing is linear in the event count rather than +/// quadratic. +/// +/// Rust: `stochastic::point_process::hawkes_log_likelihood` +#[pyfunction] +#[pyo3(name = "hawkes_log_likelihood", signature = (events, t_end, mu, alpha, beta))] +pub fn pyfn_hawkes_log_likelihood<'py>(py: Python<'py>, events: Vec, t_end: f64, mu: f64, alpha: f64, beta: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::point_process::hawkes_log_likelihood(&events, t_end, mu, alpha, beta))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Maximum likelihood estimates of a Hawkes process's parameters. +/// +/// Returns `(mu, alpha, beta)`, found by a coordinate search over the +/// log-likelihood. The likelihood is not concave in these coordinates, so +/// this is a local optimiser started from moment-based guesses rather than a +/// guarantee. +/// +/// Panics: +/// Panics unless there are at least two events and `t_end` is positive. +/// +/// Rust: `stochastic::point_process::hawkes_fit_mle` +#[pyfunction] +#[pyo3(name = "hawkes_fit_mle", signature = (events, t_end))] +pub fn pyfn_hawkes_fit_mle<'py>(py: Python<'py>, events: Vec, t_end: f64) -> PyResult<(f64, f64, f64)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::point_process::hawkes_fit_mle(&events, t_end))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// A Matern cluster process: Poisson parents, each surrounded by a Poisson +/// number of daughters uniformly inside a disc. +/// +/// Only the daughters are returned. Parents outside the region still throw +/// daughters into it, so they are generated over a margin as wide as the +/// cluster radius; omitting that margin would thin the pattern near the +/// edges and is the standard way a clustered simulation comes out wrong. +/// +/// Panics: +/// Panics unless the rates and the radius are positive and the region has +/// positive area. +/// +/// Rust: `stochastic::point_process::matern_cluster_process` +#[pyfunction] +#[pyo3(name = "matern_cluster_process", signature = (parent_rate, cluster_radius, daughter_mean, region, rng))] +pub fn pyfn_matern_cluster_process(parent_rate: f64, cluster_radius: f64, daughter_mean: f64, region: crate::generated::types::PyRect, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let region = region.inner; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::point_process::matern_cluster_process(parent_rate, cluster_radius, daughter_mean, ®ion, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// A Thomas process: the same as Matern, with daughters scattered by a +/// Gaussian instead of uniformly in a disc. +/// +/// The Gaussian has no hard edge, so the clusters blend rather than ending +/// abruptly; the margin is taken at four standard deviations, past which the +/// contribution is negligible. +/// +/// Panics: +/// Panics unless the rates and the spread are positive. +/// +/// Rust: `stochastic::point_process::thomas_process` +#[pyfunction] +#[pyo3(name = "thomas_process", signature = (parent_rate, spread, daughter_mean, region, rng))] +pub fn pyfn_thomas_process(parent_rate: f64, spread: f64, daughter_mean: f64, region: crate::generated::types::PyRect, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let region = region.inner; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::point_process::thomas_process(parent_rate, spread, daughter_mean, ®ion, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) +} + +/// Ripley's `K` function: the expected number of further points within `r` of +/// a typical point, divided by the intensity. +/// +/// For complete spatial randomness it is `pi r^2` at every distance, because +/// the expected count in a disc is the intensity times its area and the +/// division cancels the intensity. Above that means clustering and below +/// means regularity, so the whole diagnostic is a comparison against a +/// parabola. +/// +/// Edge effects are handled by Ripley's isotropic correction: a point near +/// the boundary sees only part of its own circle, so each neighbour is +/// weighted by the reciprocal of the fraction of that circle lying inside +/// the region. Without it every pattern looks regular near the edges. +/// +/// The correction is trustworthy only while the radius stays well inside the +/// window -- a quarter of the shorter side is the usual limit. Beyond that a +/// point near a corner has most of its circle outside, the weight it earns is +/// large, and the estimate becomes both noisy and biased upward. +/// +/// Panics: +/// Panics unless the region has positive area and the radii are positive. +/// +/// Rust: `stochastic::point_process::ripley_k` +#[pyfunction] +#[pyo3(name = "ripley_k", signature = (points, region, r_values))] +pub fn pyfn_ripley_k<'py>(py: Python<'py>, points: Vec, region: crate::generated::types::PyRect, r_values: Vec) -> PyResult> { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let region = region.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::point_process::ripley_k(&points, ®ion, &r_values))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Besag's `L` function: `sqrt(K / pi)`, which is `r` itself under complete +/// spatial randomness. +/// +/// The point of the transformation is that a straight line is far easier to +/// read a departure from than a parabola, and it stabilises the variance +/// along the way. +/// +/// Panics: +/// Panics under the same conditions as `ripley_k`. +/// +/// Rust: `stochastic::point_process::l_function` +#[pyfunction] +#[pyo3(name = "l_function", signature = (points, region, r_values))] +pub fn pyfn_l_function<'py>(py: Python<'py>, points: Vec, region: crate::generated::types::PyRect, r_values: Vec) -> PyResult> { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let region = region.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::point_process::l_function(&points, ®ion, &r_values))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The pair correlation function: the density of points at distance `r` from +/// a typical point, relative to the intensity. +/// +/// One everywhere under complete spatial randomness. Where `K` accumulates +/// everything within `r` and so smears features together, this looks at a +/// shell of width `dr` and shows the distance at which clustering actually +/// happens. +/// +/// Panics: +/// Panics unless the region has positive area and `r` and `dr` are positive +/// with `dr` below `r`. +/// +/// Rust: `stochastic::point_process::pair_correlation` +#[pyfunction] +#[pyo3(name = "pair_correlation", signature = (points, region, r, dr))] +pub fn pyfn_pair_correlation<'py>(py: Python<'py>, points: Vec, region: crate::generated::types::PyRect, r: f64, dr: f64) -> PyResult { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let region = region.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::point_process::pair_correlation(&points, ®ion, r, dr))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Clark-Evans nearest neighbour index: the mean nearest-neighbour +/// distance divided by what a Poisson pattern of the same intensity would +/// give. +/// +/// One for complete spatial randomness, below one for clustering, above for +/// regularity. The expected distance under randomness is +/// `1 / (2 sqrt(intensity))`, which follows from the void probability: the +/// chance that the nearest neighbour is beyond `r` is the chance a disc of +/// radius `r` is empty. +/// +/// Panics: +/// Panics unless the region has positive area and there are at least two +/// points. +/// +/// Rust: `stochastic::point_process::nearest_neighbor_index` +#[pyfunction] +#[pyo3(name = "nearest_neighbor_index", signature = (points, region))] +pub fn pyfn_nearest_neighbor_index<'py>(py: Python<'py>, points: Vec, region: crate::generated::types::PyRect) -> PyResult { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let region = region.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::point_process::nearest_neighbor_index(&points, ®ion))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The quadrat test: divide the region into cells and test whether the counts +/// look Poisson. +/// +/// Under complete spatial randomness every cell has the same expected count, +/// so a chi-squared goodness-of-fit against a flat expectation is the test. +/// It sees departures in the *variance* of the counts and is blind to +/// anything at a scale finer than a cell, which is why it is a first look +/// rather than a conclusion. +/// +/// Errors: +/// Returns an error unless the grid is at least two by two and there are at +/// least as many points as cells. +/// +/// Rust: `stochastic::point_process::quadrat_test` +#[pyfunction] +#[pyo3(name = "quadrat_test", signature = (points, region, nx, ny))] +pub fn pyfn_quadrat_test(points: Vec, region: crate::generated::types::PyRect, nx: usize, ny: usize) -> PyResult { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let region = region.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::point_process::quadrat_test(&points, ®ion, nx, ny)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyTestResult { inner: __v }) +} + +/// Tests whether the gaps between events look exponential, which is what a +/// Poisson process requires. +/// +/// A Kolmogorov-Smirnov test against the exponential distribution with the +/// observed mean. A small p-value says the process is not Poisson; a large +/// one says only that this particular test did not notice. +/// +/// Errors: +/// Returns an error unless there are at least three events with positive +/// gaps. +/// +/// Rust: `stochastic::point_process::ks_test_exponential_interarrivals` +#[pyfunction] +#[pyo3(name = "ks_test_exponential_interarrivals", signature = (events))] +pub fn pyfn_ks_test_exponential_interarrivals(events: Vec) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::point_process::ks_test_exponential_interarrivals(&events)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyTestResult { inner: __v }) +} + +/// A Galton-Watson branching process: the population size at each +/// generation. +/// +/// Every individual independently has a random number of offspring from the +/// same distribution. The population dies out with probability one when the +/// mean offspring count is at most one -- including exactly one, which is the +/// surprise: a population that replaces itself on average still goes extinct +/// unless the count is deterministic. +/// +/// A supercritical population is held once it passes two thousand: beyond +/// that its extinction probability is smaller than any double can represent, +/// so the remaining generations carry no information and every one of them +/// would cost time proportional to the population. +/// +/// Panics: +/// Panics unless the offspring distribution is a probability vector. +/// +/// Rust: `stochastic::point_process::branching_process_gw` +#[pyfunction] +#[pyo3(name = "branching_process_gw", signature = (offspring_pmf, generations, rng))] +pub fn pyfn_branching_process_gw(offspring_pmf: Vec, generations: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::point_process::branching_process_gw(&offspring_pmf, generations, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The extinction probability of a branching process: the smallest fixed +/// point of the offspring generating function in `[0, 1]`. +/// +/// One when the mean offspring count is at most one, and strictly below one +/// above it. The fixed point equation says that a lineage dies out exactly +/// when every one of its founder's children's lineages does, which is the +/// whole argument in one line. +/// +/// Panics: +/// Panics unless the coefficients are a probability vector. +/// +/// Rust: `stochastic::point_process::extinction_probability` +#[pyfunction] +#[pyo3(name = "extinction_probability", signature = (offspring_pgf_coeffs))] +pub fn pyfn_extinction_probability<'py>(py: Python<'py>, offspring_pgf_coeffs: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::point_process::extinction_probability(&offspring_pgf_coeffs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// A Yule process: pure birth, each individual splitting at a constant rate. +/// +/// Returns the times at which the population grew. The population at time `t` +/// is geometric with mean `exp(birth_rate t)`, which is the continuous-time +/// analogue of a branching process that never dies. +/// +/// Panics: +/// Panics unless the rate and the horizon are positive. +/// +/// Rust: `stochastic::point_process::yule_process` +#[pyfunction] +#[pyo3(name = "yule_process", signature = (birth_rate, t_end, rng))] +pub fn pyfn_yule_process(birth_rate: f64, t_end: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::point_process::yule_process(birth_rate, t_end, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// A linear birth-death process, by Gillespie's direct method. +/// +/// Returns `(time, population)` after each event. The population dies out +/// with probability one when the death rate is at least the birth rate, and +/// with probability `(death / birth)^n0` when it is not -- which is the +/// branching process's extinction probability again, in continuous time. +/// +/// A population past a thousand is held, for the reason +/// `branching_process_gw` gives. +/// +/// Panics: +/// Panics unless the rates are non-negative and the horizon is positive. +/// +/// Rust: `stochastic::point_process::birth_death_simulate` +#[pyfunction] +#[pyo3(name = "birth_death_simulate", signature = (birth, death, n0, t_end, rng))] +pub fn pyfn_birth_death_simulate(birth: f64, death: f64, n0: u64, t_end: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::point_process::birth_death_simulate(birth, death, n0, t_end, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_poisson_process, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_poisson_inhomogeneous, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_poisson_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_poisson_3d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hawkes_intensity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hawkes_branching_ratio, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hawkes_process, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hawkes_log_likelihood, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hawkes_fit_mle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_matern_cluster_process, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_thomas_process, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ripley_k, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_l_function, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pair_correlation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_nearest_neighbor_index, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_quadrat_test, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ks_test_exponential_interarrivals, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_branching_process_gw, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_extinction_probability, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_yule_process, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_birth_death_simulate, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_stochastic__queueing.rs b/bindings/python/src/generated/m_stochastic__queueing.rs new file mode 100644 index 0000000..5dce5ff --- /dev/null +++ b/bindings/python/src/generated/m_stochastic__queueing.rs @@ -0,0 +1,331 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// A single-server queue with Poisson arrivals and exponential service. +/// +/// The stationary distribution is geometric, `p_n = (1 - rho) rho^n`, which +/// gives `L = rho / (1 - rho)` directly. +/// +/// Panics: +/// Panics unless `lambda` and `mu` are positive. +/// +/// Rust: `stochastic::queueing::mm1` +#[pyfunction] +#[pyo3(name = "mm1", signature = (lambda_, mu))] +pub fn pyfn_mm1(lambda_: f64, mu: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::queueing::mm1(lambda_, mu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQueueMetrics { inner: __v }) +} + +/// `c` parallel servers, Poisson arrivals, exponential service, no limit on +/// the queue. The probability an arrival has to wait is Erlang C. +/// +/// Unstable loads (`lambda >= c mu`) return infinite means with `rho >= 1`; +/// the queue really does grow without bound there, so that is the answer +/// rather than an error. +/// +/// Panics: +/// Panics unless `lambda` and `mu` are positive and `c >= 1`. +/// +/// Rust: `stochastic::queueing::mmc` +#[pyfunction] +#[pyo3(name = "mmc", signature = (lambda_, mu, c))] +pub fn pyfn_mmc(lambda_: f64, mu: f64, c: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::queueing::mmc(lambda_, mu, c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQueueMetrics { inner: __v }) +} + +/// A single server with room for `k` customers in total. Arrivals that find +/// the system full are lost, so the effective arrival rate is `lambda (1 - p_k)` +/// and the queue is stable at any load. +/// +/// Panics: +/// Panics unless `lambda` and `mu` are positive and `k >= 1`. +/// +/// Rust: `stochastic::queueing::mm1k` +#[pyfunction] +#[pyo3(name = "mm1k", signature = (lambda_, mu, k))] +pub fn pyfn_mm1k(lambda_: f64, mu: f64, k: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::queueing::mm1k(lambda_, mu, k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQueueMetrics { inner: __v }) +} + +/// `c` servers with room for `k` in total, `k >= c`. Arrivals finding the +/// system full are lost. +/// +/// Panics: +/// Panics unless `lambda` and `mu` are positive and `c <= k`. +/// +/// Rust: `stochastic::queueing::mmck` +#[pyfunction] +#[pyo3(name = "mmck", signature = (lambda_, mu, c, k))] +pub fn pyfn_mmck(lambda_: f64, mu: f64, c: usize, k: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::queueing::mmck(lambda_, mu, c, k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQueueMetrics { inner: __v }) +} + +/// Unlimited servers: every arrival enters service at once. The number in +/// system is Poisson with mean `lambda / mu`, so nobody ever waits. +/// +/// Panics: +/// Panics unless `lambda` and `mu` are positive. +/// +/// Rust: `stochastic::queueing::mm_inf` +#[pyfunction] +#[pyo3(name = "mm_inf", signature = (lambda_, mu))] +pub fn pyfn_mm_inf(lambda_: f64, mu: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::queueing::mm_inf(lambda_, mu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQueueMetrics { inner: __v }) +} + +/// Erlang's loss formula: the fraction of calls blocked by `c` trunks under +/// an offered load of `a` erlangs. +/// +/// Computed by the recursion `B_c = a B_{c-1} / (c + a B_{c-1})` rather than +/// the ratio of factorial sums. The two agree exactly in real arithmetic, but +/// the direct form overflows near `c = 170` while the recursion stays in +/// `[0, 1]` at every step and is accurate for any `c`. +/// +/// Panics: +/// Panics if `a` is negative. +/// +/// Rust: `stochastic::queueing::erlang_b` +#[pyfunction] +#[pyo3(name = "erlang_b", signature = (offered_load, c))] +pub fn pyfn_erlang_b(offered_load: f64, c: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::queueing::erlang_b(offered_load, c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Erlang's delay formula: the probability an arrival to an `M/M/c` queue +/// finds every server busy and has to wait. +/// +/// Returns 1 for a saturated system. Related to the loss formula by +/// `C = B / (1 - rho (1 - B))`, which is how it is evaluated here. +/// +/// Panics: +/// Panics if `load` is negative or `c` is zero. +/// +/// Rust: `stochastic::queueing::erlang_c` +#[pyfunction] +#[pyo3(name = "erlang_c", signature = (load, c))] +pub fn pyfn_erlang_c(load: f64, c: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::queueing::erlang_c(load, c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The smallest number of trunks that holds blocking at or below +/// `blocking_target` for the given offered load. +/// +/// Steps the Erlang B recursion upward, which is monotone decreasing in `c`, +/// so the first `c` that clears the target is the smallest one. +/// +/// Panics: +/// Panics unless the target is in `(0, 1]` and the load is non-negative. +/// +/// Rust: `stochastic::queueing::erlang_b_inverse_capacity` +#[pyfunction] +#[pyo3(name = "erlang_b_inverse_capacity", signature = (load, blocking_target))] +pub fn pyfn_erlang_b_inverse_capacity(load: f64, blocking_target: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::queueing::erlang_b_inverse_capacity(load, blocking_target)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Pollaczek-Khinchine mean-value formula for a single server with +/// Poisson arrivals and a general service distribution. +/// +/// `Lq = lambda^2 (var + mean^2) / (2 (1 - rho))`. The service distribution +/// enters only through its first two moments: exponential service has +/// `var = mean^2` and recovers M/M/1, while deterministic service has +/// `var = 0` and halves the queue. +/// +/// Panics: +/// Panics unless `lambda` and `service_mean` are positive and the variance is +/// non-negative. +/// +/// Rust: `stochastic::queueing::mg1_pollaczek_khinchine` +#[pyfunction] +#[pyo3(name = "mg1_pollaczek_khinchine", signature = (lambda_, service_mean, service_var))] +pub fn pyfn_mg1_pollaczek_khinchine(lambda_: f64, service_mean: f64, service_var: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::queueing::mg1_pollaczek_khinchine(lambda_, service_mean, service_var)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQueueMetrics { inner: __v }) +} + +/// Kingman's diffusion approximation for the mean wait in a G/G/1 queue, +/// given the squared coefficients of variation of the interarrival and +/// service times. +/// +/// `Wq ~ (rho / (1 - rho)) ((ca2 + cs2) / 2) (1 / mu)`. It is exact for +/// M/M/1, where both coefficients are one and the middle factor drops out, +/// and is asymptotically exact as `rho -> 1` for any distribution. +/// +/// Panics: +/// Panics unless the rates are positive and the coefficients non-negative. +/// +/// Rust: `stochastic::queueing::gg1_kingman_approx` +#[pyfunction] +#[pyo3(name = "gg1_kingman_approx", signature = (lambda_, mu, ca2, cs2))] +pub fn pyfn_gg1_kingman_approx(lambda_: f64, mu: f64, ca2: f64, cs2: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::queueing::gg1_kingman_approx(lambda_, mu, ca2, cs2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The residual `L - lambda W`, which any consistent set of steady-state +/// numbers must drive to zero. +/// +/// Little's law is a pathwise identity, not a distributional one, so this is +/// a genuine check on measured or simulated quantities rather than an +/// assumption about the model. +/// +/// Rust: `stochastic::queueing::littles_law_check` +#[pyfunction] +#[pyo3(name = "littles_law_check", signature = (l, lambda_, w))] +pub fn pyfn_littles_law_check(l: f64, lambda_: f64, w: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::queueing::littles_law_check(l, lambda_, w)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// An open Jackson network of `M/M/c` nodes. +/// +/// `routing[i][j]` is the probability a customer leaving node `i` goes to +/// node `j`; whatever is left over departs the network. Total arrival rates +/// solve the traffic equations `lambda_j = external_j + sum_i lambda_i r_ij`, +/// after which Jackson's theorem says each node behaves in steady state +/// exactly like an isolated `M/M/c_j` queue at its own total rate -- even +/// though the internal arrival streams are not Poisson. +/// +/// Errors: +/// Returns `GeomError::InvalidArgument` if the shapes disagree, if a +/// routing row sums past one, or if the traffic equations are singular. +/// +/// Rust: `stochastic::queueing::jackson_network` +#[pyfunction] +#[pyo3(name = "jackson_network", signature = (routing, external, service, servers))] +pub fn pyfn_jackson_network(routing: crate::generated::types::PyMatrixArg, external: Vec, service: Vec, servers: Vec) -> PyResult> { + let routing = routing.0; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::queueing::jackson_network(&routing, &external, &service, &servers)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyQueueMetrics { inner: __x }).collect::>()) +} + +/// A non-preemptive priority queue with `c` servers and one exponential +/// class per entry of `lambdas`. +/// +/// Class 0 has the highest priority. A waiting customer of a higher class is +/// always taken next, but a job already in service runs to completion. +/// Returns one result per class. +/// +/// The discipline is work-conserving, so Kleinrock's conservation law applies: +/// `sum_k rho_k Wq_k` is the same here as under plain FIFO, however the +/// priorities are arranged. Only the split between classes changes. +/// +/// Panics: +/// Panics unless the rate vectors match in length, are positive, and +/// `c >= 1`. +/// +/// Rust: `stochastic::queueing::priority_queue_simulate` +#[pyfunction] +#[pyo3(name = "priority_queue_simulate", signature = (lambdas, mus, c, t_end, rng))] +pub fn pyfn_priority_queue_simulate(lambdas: Vec, mus: Vec, c: usize, t_end: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::queueing::priority_queue_simulate(&lambdas, &mus, c, t_end, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyQueueSimResult { inner: __x }).collect::>()) +} + +/// Transient distribution of a continuous-time chain by uniformization. +/// +/// Writes `P(t) = exp(Qt)` as a Poisson mixture of powers of a discrete +/// chain: pick a rate `L` at least as large as every exit rate, set +/// `P = I + Q/L`, and then `p(t) = sum_k e^{-Lt} (Lt)^k / k! * p0 P^k`. Every +/// term is a probability vector and every weight is positive, so unlike a +/// truncated matrix exponential the partial sums never go negative, however +/// stiff the generator. +/// +/// The sum is truncated when the remaining Poisson mass falls below `eps`. +/// +/// Errors: +/// Returns `GeomError::InvalidArgument` if `p0` is the wrong length, is not +/// a distribution, or if `t` or `eps` are not positive. +/// +/// Rust: `stochastic::queueing::uniformization` +#[pyfunction] +#[pyo3(name = "uniformization", signature = (q_matrix, p0, t, eps))] +pub fn pyfn_uniformization<'py>(py: Python<'py>, q_matrix: crate::generated::types::PyMatrixArg, p0: Vec, t: f64, eps: f64) -> PyResult> { + let q_matrix = q_matrix.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::queueing::uniformization(&q_matrix, &p0, t, eps))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The distribution of the number in an M/M/1 queue at time `t`, starting +/// from exactly `n0` customers. +/// +/// The state space is truncated well above the point where the stationary +/// geometric tail is negligible, then run through `uniformization`. Returns +/// the probability of each state from 0 up to the truncation point. +/// +/// Errors: +/// Returns an error if the rates are not positive or the transient solve fails. +/// +/// Rust: `stochastic::queueing::queue_transient_mm1` +#[pyfunction] +#[pyo3(name = "queue_transient_mm1", signature = (lambda_, mu, n0, t))] +pub fn pyfn_queue_transient_mm1<'py>(py: Python<'py>, lambda_: f64, mu: f64, n0: usize, t: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::queueing::queue_transient_mm1(lambda_, mu, n0, t))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_mm1, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mmc, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mm1k, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mmck, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mm_inf, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_erlang_b, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_erlang_c, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_erlang_b_inverse_capacity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mg1_pollaczek_khinchine, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gg1_kingman_approx, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_littles_law_check, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_jackson_network, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_priority_queue_simulate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_uniformization, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_queue_transient_mm1, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_stochastic__rmt.rs b/bindings/python/src/generated/m_stochastic__rmt.rs new file mode 100644 index 0000000..f72d40f --- /dev/null +++ b/bindings/python/src/generated/m_stochastic__rmt.rs @@ -0,0 +1,421 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// A sample from the Gaussian orthogonal ensemble: a symmetric matrix whose +/// entries are Gaussian, independent up to the symmetry constraint. +/// +/// Scaled so the spectrum fills `[-2, 2]` in the large-`n` limit: off-diagonal +/// entries have variance `1/n` and diagonal entries `2/n`. The factor of two +/// on the diagonal is not decorative -- it is what makes the distribution +/// invariant under orthogonal conjugation, which is the defining property of +/// the ensemble and the reason its spectral statistics are universal. +/// +/// Panics: +/// Panics if `n` is zero. +/// +/// Rust: `stochastic::rmt::goe_sample` +#[pyfunction] +#[pyo3(name = "goe_sample", signature = (n, rng))] +pub fn pyfn_goe_sample(n: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::rmt::goe_sample(n, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) +} + +/// A sample from the Gaussian unitary ensemble, returned as +/// `(real part, imaginary part)` of a Hermitian matrix. +/// +/// The real part is symmetric and the imaginary part antisymmetric with a +/// zero diagonal, which together is what "Hermitian" means for a matrix held +/// in two real halves. Scaled to the same `[-2, 2]` support as +/// `goe_sample`: each independent real degree of freedom carries variance +/// `1/(2n)`, so `E|H_ij|^2 = 1/n` off the diagonal. +/// +/// Panics: +/// Panics if `n` is zero. +/// +/// Rust: `stochastic::rmt::gue_sample` +#[pyfunction] +#[pyo3(name = "gue_sample", signature = (n, rng))] +pub fn pyfn_gue_sample(n: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(crate::generated::types::PyMatrix, crate::generated::types::PyMatrix)> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::rmt::gue_sample(n, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((crate::generated::types::PyMatrix { inner: __v.0 }, crate::generated::types::PyMatrix { inner: __v.1 })) +} + +/// A sample from the Ginibre ensemble: every entry independent Gaussian, with +/// no symmetry imposed at all. +/// +/// Its eigenvalues are complex and fill the unit disc rather than an +/// interval, which is the point of the ensemble -- non-normality changes the +/// spectral picture completely. +/// +/// Panics: +/// Panics if `n` is zero. +/// +/// Rust: `stochastic::rmt::ginibre_sample` +#[pyfunction] +#[pyo3(name = "ginibre_sample", signature = (n, rng))] +pub fn pyfn_ginibre_sample(n: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::rmt::ginibre_sample(n, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) +} + +/// A sample covariance matrix built from `p` independent variables observed +/// `n` times, each observation standard Gaussian. +/// +/// Returns `X' X / n` where `X` is `n` by `p`, so the population covariance +/// is the identity and every departure from it in the sample is estimation +/// noise. That noise is exactly what `marchenko_pastur` describes. +/// +/// Panics: +/// Panics if either dimension is zero. +/// +/// Rust: `stochastic::rmt::wishart_sample` +#[pyfunction] +#[pyo3(name = "wishart_sample", signature = (n, p, rng))] +pub fn pyfn_wishart_sample(n: usize, p: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::rmt::wishart_sample(n, p, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) +} + +/// Wigner's semicircle density on `[-r, r]`. +/// +/// `f(x) = 2 sqrt(r^2 - x^2) / (pi r^2)`, zero outside. The limiting +/// eigenvalue density of a symmetric random matrix, whatever the entry +/// distribution, provided the entries are independent with finite variance -- +/// the first and simplest statement of universality in the subject. +/// +/// Panics: +/// Panics unless `r` is positive. +/// +/// Rust: `stochastic::rmt::wigner_semicircle` +#[pyfunction] +#[pyo3(name = "wigner_semicircle", signature = (x, r))] +pub fn pyfn_wigner_semicircle(x: f64, r: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::rmt::wigner_semicircle(x, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Marchenko-Pastur density for a sample covariance matrix. +/// +/// `ratio` is `p / n`, the number of variables over the number of +/// observations, and `sigma2` the population variance. Support is +/// `[sigma2 (1 -+ sqrt(ratio))^2]`; the density there is +/// `sqrt((b - x)(x - a)) / (2 pi ratio sigma2 x)`. +/// +/// This is the shape a covariance matrix of *independent* variables takes. +/// The width of the band is the whole point: at `ratio = 0.5` the sample +/// eigenvalues spread over roughly `[0.09, 2.9]` even though every population +/// eigenvalue is exactly 1. +/// +/// The point mass at zero when `ratio > 1` (more variables than +/// observations, so the matrix is singular) is not part of the density and is +/// not reported here. +/// +/// Panics: +/// Panics unless `ratio` and `sigma2` are positive. +/// +/// Rust: `stochastic::rmt::marchenko_pastur` +#[pyfunction] +#[pyo3(name = "marchenko_pastur", signature = (x, ratio, sigma2))] +pub fn pyfn_marchenko_pastur(x: f64, ratio: f64, sigma2: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::rmt::marchenko_pastur(x, ratio, sigma2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The two edges of the Marchenko-Pastur support, +/// `sigma2 (1 -+ sqrt(ratio))^2`. +/// +/// Any sample eigenvalue between these is consistent with pure noise. +/// +/// Panics: +/// Panics unless `ratio` and `sigma2` are positive. +/// +/// Rust: `stochastic::rmt::mp_edges` +#[pyfunction] +#[pyo3(name = "mp_edges", signature = (ratio, sigma2))] +pub fn pyfn_mp_edges(ratio: f64, sigma2: f64) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::rmt::mp_edges(ratio, sigma2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Gaps between consecutive eigenvalues after unfolding to unit mean density. +/// +/// Unfolding is not a cosmetic step. The raw gaps of a semicircular spectrum +/// are far tighter near zero than near the edges, so their distribution would +/// mostly reflect that varying density rather than the correlations between +/// levels. Mapping each eigenvalue through a smooth estimate of its own +/// cumulative count removes the density and leaves the local statistics, +/// which is what the surmises below describe. +/// +/// The smooth estimate here is the empirical staircase itself, smoothed by +/// averaging over a window that grows as the square root of the sample -- the +/// standard compromise between following the density and following the +/// fluctuations one is trying to measure. +/// +/// Returns `eigs.len() - 1` gaps with mean 1. An empty or single-element +/// input gives an empty result. +/// +/// Rust: `stochastic::rmt::eigenvalue_spacing_distribution` +#[pyfunction] +#[pyo3(name = "eigenvalue_spacing_distribution", signature = (eigs))] +pub fn pyfn_eigenvalue_spacing_distribution<'py>(py: Python<'py>, eigs: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::rmt::eigenvalue_spacing_distribution(&eigs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Wigner's surmise for the orthogonal class: +/// `(pi/2) s exp(-pi s^2 / 4)`. +/// +/// The spacing distribution of a two-by-two GOE matrix, which turns out to +/// approximate the large-`n` answer to within a percent. Its defining feature +/// is the linear vanishing at `s = 0`: eigenvalues of a real symmetric random +/// matrix repel, so exact degeneracies have probability zero and near ones +/// are rare. +/// +/// Rust: `stochastic::rmt::wigner_surmise_goe` +#[pyfunction] +#[pyo3(name = "wigner_surmise_goe", signature = (s))] +pub fn pyfn_wigner_surmise_goe(s: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::rmt::wigner_surmise_goe(s)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Wigner's surmise for the unitary class: +/// `(32 / pi^2) s^2 exp(-4 s^2 / pi)`. +/// +/// The repulsion is quadratic rather than linear -- a complex Hermitian +/// matrix has twice as many degrees of freedom to tune away from a +/// degeneracy, so near-degeneracies are suppressed harder than in the +/// orthogonal class. +/// +/// Rust: `stochastic::rmt::wigner_surmise_gue` +#[pyfunction] +#[pyo3(name = "wigner_surmise_gue", signature = (s))] +pub fn pyfn_wigner_surmise_gue(s: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::rmt::wigner_surmise_gue(s)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The spacing density of uncorrelated levels: `exp(-s)`. +/// +/// A Poisson process of points has no repulsion at all, so its density is +/// maximal at zero. This is the null the surmises above are contrasted +/// against, and the contrast at small `s` is the whole diagnostic. +/// +/// Rust: `stochastic::rmt::poisson_spacing` +#[pyfunction] +#[pyo3(name = "poisson_spacing", signature = (s))] +pub fn pyfn_poisson_spacing(s: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::rmt::poisson_spacing(s)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The spectral rigidity `Delta_3(L)`: the mean-square deviation of the +/// unfolded counting function from the best straight line over a window of +/// length `L`. +/// +/// Where the spacing distribution measures correlations between *neighbours*, +/// rigidity measures them over a stretch of `L` levels, and it is the more +/// discriminating of the two. Uncorrelated levels give `L / 15`, growing +/// linearly; a correlated spectrum gives roughly `ln(L) / pi^2`, growing so +/// slowly that at `L = 20` the two differ by an order of magnitude. +/// +/// Averaged over windows starting across the spectrum. +/// +/// Panics: +/// Panics unless `l` is positive. +/// +/// Rust: `stochastic::rmt::spectral_rigidity` +#[pyfunction] +#[pyo3(name = "spectral_rigidity", signature = (eigs, l))] +pub fn pyfn_spectral_rigidity<'py>(py: Python<'py>, eigs: Vec, l: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::rmt::spectral_rigidity(&eigs, l))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// An approximation to the Tracy-Widom distribution function for the +/// orthogonal class, the law of the largest eigenvalue after edge scaling. +/// +/// Represented as a shifted gamma matched to the first three cumulants of +/// `TW_1` (mean `-1.2065`, variance `1.6078`, skewness `0.2935`), which is the +/// standard closed-form stand-in: exact evaluation needs the Hastings-McLeod +/// solution of Painleve II. Accurate to a few parts in a thousand through the +/// body, degrading in the far tails, where the true law decays like +/// `exp(-|x|^3/24)` on the left and `exp(-(2/3) x^{3/2})` on the right. +/// +/// The distribution matters because the largest eigenvalue does not +/// fluctuate on the scale of the spectrum: it sits within `n^{-2/3}` of the +/// edge, so a spike only a little above the Marchenko-Pastur edge is still +/// strong evidence of real signal. +/// +/// Rust: `stochastic::rmt::tracy_widom_beta1_approx` +#[pyfunction] +#[pyo3(name = "tracy_widom_beta1_approx", signature = (x))] +pub fn pyfn_tracy_widom_beta1_approx(x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::rmt::tracy_widom_beta1_approx(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The inverse participation ratio of a vector: `sum v_i^4 / (sum v_i^2)^2`. +/// +/// A measure of how many components carry the weight. A vector concentrated +/// on one component scores 1; one spread evenly over `n` scores `1/n`. For +/// eigenvectors it separates localised states from extended ones, and a GOE +/// eigenvector -- uniform on the sphere -- sits at `3/n`, the extra factor +/// being the fourth moment of a Gaussian. +/// +/// Returns zero for a zero vector. +/// +/// Rust: `stochastic::rmt::participation_ratio` +#[pyfunction] +#[pyo3(name = "participation_ratio", signature = (vec))] +pub fn pyfn_participation_ratio<'py>(py: Python<'py>, vec: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::rmt::participation_ratio(&vec))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The mean ratio of consecutive level spacings, +/// ``. +/// +/// The great virtue of this statistic is that it needs no unfolding: a ratio +/// of adjacent gaps is insensitive to the local density, which cancels. That +/// removes the one genuinely arbitrary step in spacing analysis. The limiting +/// values are 0.5307 for the orthogonal class, 0.5996 for the unitary, and +/// `2 ln 2 - 1 = 0.3863` for uncorrelated levels. +/// +/// Returns zero for fewer than three eigenvalues. +/// +/// Rust: `stochastic::rmt::level_spacing_ratio` +#[pyfunction] +#[pyo3(name = "level_spacing_ratio", signature = (eigs))] +pub fn pyfn_level_spacing_ratio<'py>(py: Python<'py>, eigs: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::rmt::level_spacing_ratio(&eigs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Cleans a sample correlation matrix by replacing every eigenvalue inside +/// the Marchenko-Pastur band with their common average. +/// +/// `t_over_n` is the number of observations divided by the number of +/// variables, so the band is set by `ratio = 1 / t_over_n`. Eigenvalues below +/// the upper edge are indistinguishable from the noise a correlation matrix +/// of independent variables would produce, and estimating each of them +/// separately fits that noise. Replacing them by their mean keeps the trace +/// -- so the cleaned matrix still has unit diagonal on average and remains a +/// correlation matrix -- while discarding the structure that was not there. +/// +/// The eigenvalues above the edge, and their eigenvectors, are left alone. +/// +/// Errors: +/// Returns an error if the matrix is not square and symmetric, if `t_over_n` +/// is not positive, or if the eigen-decomposition fails to converge. +/// +/// Rust: `stochastic::rmt::correlation_matrix_denoise_mp` +#[pyfunction] +#[pyo3(name = "correlation_matrix_denoise_mp", signature = (corr, t_over_n))] +pub fn pyfn_correlation_matrix_denoise_mp(corr: crate::generated::types::PyMatrixArg, t_over_n: f64) -> PyResult { + let corr = corr.0; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::rmt::correlation_matrix_denoise_mp(&corr, t_over_n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) +} + +/// The eigenvalues of a symmetric matrix, sorted ascending. +/// +/// A convenience over `eigen_symmetric` for the spectral statistics above, +/// which never need the eigenvectors. +/// +/// Errors: +/// Returns an error if the matrix is not symmetric or the solver fails. +/// +/// Rust: `stochastic::rmt::symmetric_spectrum` +#[pyfunction] +#[pyo3(name = "symmetric_spectrum", signature = (a))] +pub fn pyfn_symmetric_spectrum<'py>(py: Python<'py>, a: crate::generated::types::PyMatrixArg) -> PyResult> { + let a = a.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::rmt::symmetric_spectrum(&a))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// The eigenvalues of a Hermitian matrix held as `(real, imaginary)` parts. +/// +/// Uses the standard real embedding: the `2n`-by-`2n` real symmetric matrix +/// `[[Re, -Im], [Im, Re]]` has exactly the eigenvalues of `H`, each appearing +/// twice. Returns the `n` distinct ones by taking every second value of the +/// sorted `2n`, which is what lets a real symmetric solver handle the unitary +/// ensemble without any complex arithmetic. +/// +/// Errors: +/// Returns an error if the two halves disagree in shape or the solver fails. +/// +/// Rust: `stochastic::rmt::hermitian_spectrum` +#[pyfunction] +#[pyo3(name = "hermitian_spectrum", signature = (re, im))] +pub fn pyfn_hermitian_spectrum<'py>(py: Python<'py>, re: crate::generated::types::PyMatrixArg, im: crate::generated::types::PyMatrixArg) -> PyResult> { + let re = re.0; + let im = im.0; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::rmt::hermitian_spectrum(&re, &im))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_goe_sample, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gue_sample, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ginibre_sample, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wishart_sample, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wigner_semicircle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_marchenko_pastur, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mp_edges, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_eigenvalue_spacing_distribution, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wigner_surmise_goe, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wigner_surmise_gue, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_poisson_spacing, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spectral_rigidity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tracy_widom_beta1_approx, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_participation_ratio, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_level_spacing_ratio, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_correlation_matrix_denoise_mp, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_symmetric_spectrum, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hermitian_spectrum, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_stochastic__sde.rs b/bindings/python/src/generated/m_stochastic__sde.rs new file mode 100644 index 0000000..a197543 --- /dev/null +++ b/bindings/python/src/generated/m_stochastic__sde.rs @@ -0,0 +1,745 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// A Brownian path of `n + 1` points at spacing `dt`, starting at zero. +/// +/// Increments are independent Gaussians of variance `dt`, which is the +/// definition. Everything else in the module is built on this or on the +/// same increments used differently. +/// +/// Panics: +/// Panics unless `dt` is positive. +/// +/// Rust: `stochastic::sde::brownian_motion` +#[pyfunction] +#[pyo3(name = "brownian_motion", signature = (n, dt, rng))] +pub fn pyfn_brownian_motion(n: usize, dt: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::sde::brownian_motion(n, dt, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// A Brownian bridge: a path pinned at both ends. +/// +/// Built by taking a free Brownian path and subtracting the linear +/// interpolation of its own endpoint error. The result has variance +/// `t (T - t) / T` -- zero at both ends and largest in the middle -- which is +/// what conditioning on the destination does to the uncertainty. +/// +/// Panics: +/// Panics unless `dt` is positive and `n` is at least one. +/// +/// Rust: `stochastic::sde::brownian_bridge` +#[pyfunction] +#[pyo3(name = "brownian_bridge", signature = (n, dt, x0, x1, rng))] +pub fn pyfn_brownian_bridge(n: usize, dt: f64, x0: f64, x1: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::sde::brownian_bridge(n, dt, x0, x1, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// A Brownian path in two dimensions, as independent coordinates. +/// +/// Panics: +/// Panics unless `dt` is positive. +/// +/// Rust: `stochastic::sde::brownian_2d` +#[pyfunction] +#[pyo3(name = "brownian_2d", signature = (n, dt, rng))] +pub fn pyfn_brownian_2d(n: usize, dt: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::sde::brownian_2d(n, dt, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// A Brownian path in three dimensions. +/// +/// Panics: +/// Panics unless `dt` is positive. +/// +/// Rust: `stochastic::sde::brownian_3d` +#[pyfunction] +#[pyo3(name = "brownian_3d", signature = (n, dt, rng))] +pub fn pyfn_brownian_3d(n: usize, dt: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::sde::brownian_3d(n, dt, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, __x.2)).collect::>()) +} + +/// Geometric Brownian motion, simulated by exact log-space steps. +/// +/// `dS = mu S dt + sigma S dW`. Its logarithm is Brownian with drift +/// `mu - sigma^2 / 2`, so the process can be stepped exactly rather than +/// approximated -- and the `- sigma^2 / 2` is Ito's correction, the +/// difference between the drift of the process and the drift of its +/// logarithm. Simulating in log space also guarantees the path stays +/// positive, which a naive Euler step does not. +/// +/// Panics: +/// Panics unless `dt` is positive and `x0` is positive. +/// +/// Rust: `stochastic::sde::geometric_brownian` +#[pyfunction] +#[pyo3(name = "geometric_brownian", signature = (x0, mu, sigma, n, dt, rng))] +pub fn pyfn_geometric_brownian(x0: f64, mu: f64, sigma: f64, n: usize, dt: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::sde::geometric_brownian(x0, mu, sigma, n, dt, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The exact solution of geometric Brownian motion at time `t`, given the +/// standard normal `z` that drives it. +/// +/// The closed form the schemes are measured against. Passing the driving +/// normal in rather than drawing it is what lets a numerical path and the +/// exact path share the same noise, which is what strong convergence means. +/// +/// Rust: `stochastic::sde::gbm_exact` +#[pyfunction] +#[pyo3(name = "gbm_exact", signature = (x0, mu, sigma, t, z))] +pub fn pyfn_gbm_exact(x0: f64, mu: f64, sigma: f64, t: f64, z: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::sde::gbm_exact(x0, mu, sigma, t, z)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// An Ornstein-Uhlenbeck path, stepped exactly. +/// +/// `dX = theta (mu - X) dt + sigma dW`: a Brownian particle pulled back +/// towards `mu` at a rate proportional to its distance. Unlike Brownian +/// motion it has a stationary distribution -- Gaussian with mean `mu` and +/// variance `sigma^2 / (2 theta)` -- because the restoring pull eventually +/// balances the noise. The transition density is Gaussian in closed form, so +/// this is exact at any step size. +/// +/// Panics: +/// Panics unless `dt` and `theta` are positive. +/// +/// Rust: `stochastic::sde::ornstein_uhlenbeck` +#[pyfunction] +#[pyo3(name = "ornstein_uhlenbeck", signature = (x0, theta, mu, sigma, n, dt, rng))] +pub fn pyfn_ornstein_uhlenbeck(x0: f64, theta: f64, mu: f64, sigma: f64, n: usize, dt: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::sde::ornstein_uhlenbeck(x0, theta, mu, sigma, n, dt, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// One exact Ornstein-Uhlenbeck step, given the standard normal driving it. +/// +/// Panics: +/// Panics unless `theta` and `dt` are positive. +/// +/// Rust: `stochastic::sde::ou_exact_step` +#[pyfunction] +#[pyo3(name = "ou_exact_step", signature = (x, theta, mu, sigma, dt, z))] +pub fn pyfn_ou_exact_step(x: f64, theta: f64, mu: f64, sigma: f64, dt: f64, z: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::sde::ou_exact_step(x, theta, mu, sigma, dt, z)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Euler-Maruyama scheme for a scalar equation. +/// +/// `X_{k+1} = X_k + mu dt + sigma sqrt(dt) Z`. The obvious discretisation, +/// and strong order one half rather than the order one Euler's method +/// achieves without noise -- because the neglected term involves +/// `(dW)^2`, which is of order `dt` rather than `dt^2`. +/// +/// Panics: +/// Panics unless `n` is positive and `t_end` is positive. +/// +/// Rust: `stochastic::sde::euler_maruyama` +#[pyfunction] +#[pyo3(name = "euler_maruyama", signature = (mu, sigma, x0, t_end, n, rng))] +pub fn pyfn_euler_maruyama(mu: pyo3::Py, sigma: pyo3::Py, x0: f64, t_end: f64, n: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let __cb_mu = std::rc::Rc::new(crate::runtime::Callback::new(mu)); + let mu = { let __cb = __cb_mu.clone(); move |__a0: f64, __a1: f64| -> f64 { __cb.call::<_, f64>((__a0, __a1), f64::NAN) } }; + let __cb_sigma = std::rc::Rc::new(crate::runtime::Callback::new(sigma)); + let sigma = { let __cb = __cb_sigma.clone(); move |__a0: f64, __a1: f64| -> f64 { __cb.call::<_, f64>((__a0, __a1), f64::NAN) } }; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::sde::euler_maruyama(&mu, &sigma, x0, t_end, n, &mut rng.inner)); + crate::runtime::callback::check(&[&__cb_mu, &__cb_sigma], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Milstein scheme, which restores strong order one. +/// +/// Adds `0.5 sigma sigma' ((dW)^2 - dt)` to the Euler step. That term is +/// exactly what Ito's lemma says the expansion of `sigma(X)` contributes at +/// first order and Euler-Maruyama drops; putting it back doubles the +/// convergence rate for the price of one derivative. +/// +/// Panics: +/// Panics unless `n` and `t_end` are positive. +/// +/// Rust: `stochastic::sde::milstein` +#[pyfunction] +#[pyo3(name = "milstein", signature = (mu, sigma, dsigma_dx, x0, t_end, n, rng))] +pub fn pyfn_milstein(mu: pyo3::Py, sigma: pyo3::Py, dsigma_dx: pyo3::Py, x0: f64, t_end: f64, n: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let __cb_mu = std::rc::Rc::new(crate::runtime::Callback::new(mu)); + let mu = { let __cb = __cb_mu.clone(); move |__a0: f64, __a1: f64| -> f64 { __cb.call::<_, f64>((__a0, __a1), f64::NAN) } }; + let __cb_sigma = std::rc::Rc::new(crate::runtime::Callback::new(sigma)); + let sigma = { let __cb = __cb_sigma.clone(); move |__a0: f64, __a1: f64| -> f64 { __cb.call::<_, f64>((__a0, __a1), f64::NAN) } }; + let __cb_dsigma_dx = std::rc::Rc::new(crate::runtime::Callback::new(dsigma_dx)); + let dsigma_dx = { let __cb = __cb_dsigma_dx.clone(); move |__a0: f64, __a1: f64| -> f64 { __cb.call::<_, f64>((__a0, __a1), f64::NAN) } }; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::sde::milstein(&mu, &sigma, &dsigma_dx, x0, t_end, n, &mut rng.inner)); + crate::runtime::callback::check(&[&__cb_mu, &__cb_sigma, &__cb_dsigma_dx], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The stochastic Heun scheme, which converges to the *Stratonovich* +/// solution. +/// +/// A predictor-corrector: step forward, evaluate the coefficients there too, +/// and average. In the deterministic case that is the trapezoidal rule; with +/// noise it changes which stochastic integral is being computed. The +/// Stratonovich integral evaluates the integrand at the midpoint of each +/// interval rather than the left end, which makes the ordinary chain rule +/// hold and Ito's correction vanish -- and makes the answer differ from the +/// Ito one by `0.5 sigma sigma'`. +/// +/// Panics: +/// Panics unless `n` and `t_end` are positive. +/// +/// Rust: `stochastic::sde::stochastic_heun` +#[pyfunction] +#[pyo3(name = "stochastic_heun", signature = (mu, sigma, x0, t_end, n, rng))] +pub fn pyfn_stochastic_heun(mu: pyo3::Py, sigma: pyo3::Py, x0: f64, t_end: f64, n: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let __cb_mu = std::rc::Rc::new(crate::runtime::Callback::new(mu)); + let mu = { let __cb = __cb_mu.clone(); move |__a0: f64, __a1: f64| -> f64 { __cb.call::<_, f64>((__a0, __a1), f64::NAN) } }; + let __cb_sigma = std::rc::Rc::new(crate::runtime::Callback::new(sigma)); + let sigma = { let __cb = __cb_sigma.clone(); move |__a0: f64, __a1: f64| -> f64 { __cb.call::<_, f64>((__a0, __a1), f64::NAN) } }; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::sde::stochastic_heun(&mu, &sigma, x0, t_end, n, &mut rng.inner)); + crate::runtime::callback::check(&[&__cb_mu, &__cb_sigma], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// A stochastic Runge-Kutta scheme of strong order one and a half for +/// additive noise. +/// +/// With `sigma` constant the double stochastic integrals that ordinarily +/// block high-order schemes reduce to two correlated Gaussians, which can be +/// drawn directly. Both are drawn here, so the extra half order is real +/// rather than a relabelled Milstein. +/// +/// Panics: +/// Panics unless `n` and `t_end` are positive. +/// +/// Rust: `stochastic::sde::srk_order_1_5` +#[pyfunction] +#[pyo3(name = "srk_order_1_5", signature = (mu, sigma, x0, t_end, n, rng))] +pub fn pyfn_srk_order_1_5(mu: pyo3::Py, sigma: f64, x0: f64, t_end: f64, n: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let __cb_mu = std::rc::Rc::new(crate::runtime::Callback::new(mu)); + let mu = { let __cb = __cb_mu.clone(); move |__a0: f64, __a1: f64| -> f64 { __cb.call::<_, f64>((__a0, __a1), f64::NAN) } }; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::sde::srk_order_1_5(&mu, sigma, x0, t_end, n, &mut rng.inner)); + crate::runtime::callback::check(&[&__cb_mu], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The measured strong convergence order of a scheme. +/// +/// `errors` are mean absolute path errors against the exact solution, one +/// per step size in `dts`. The order is the slope of the error against the +/// step on log axes, by least squares. Measuring it rather than assuming it +/// is the only way to notice that a scheme has been implemented at the wrong +/// order, which looks like nothing at all at a single step size. +/// +/// Panics: +/// Panics unless the two slices have the same length, at least two entries, +/// and all values are positive. +/// +/// Rust: `stochastic::sde::strong_convergence_order` +#[pyfunction] +#[pyo3(name = "strong_convergence_order", signature = (errors, dts))] +pub fn pyfn_strong_convergence_order<'py>(py: Python<'py>, errors: Vec, dts: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::sde::strong_convergence_order(&errors, &dts))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The measured weak convergence order, from errors in an expectation. +/// +/// The same regression on a different error. A scheme can be weak order one +/// while being strong order a half, which is not a contradiction: getting +/// the distribution right is easier than getting each path right. +/// +/// Panics: +/// Panics under the same conditions as `strong_convergence_order`. +/// +/// Rust: `stochastic::sde::weak_convergence_order` +#[pyfunction] +#[pyo3(name = "weak_convergence_order", signature = (errors, dts))] +pub fn pyfn_weak_convergence_order<'py>(py: Python<'py>, errors: Vec, dts: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::sde::weak_convergence_order(&errors, &dts))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// A Cox-Ingersoll-Ross path by the full truncation scheme. +/// +/// `dX = kappa (theta - X) dt + sigma sqrt(X) dW`. The square root makes the +/// noise vanish at zero, so the exact process never goes negative -- but a +/// discretisation can step below zero and then take the root of a negative +/// number. +/// +/// Full truncation lets the *internal* state go negative and applies +/// `max(X, 0)` only inside the coefficients, reporting the truncated value. +/// Clipping the state itself instead -- reflecting at zero -- is the obvious +/// alternative and a much worse one: every reflection injects probability +/// mass that the exact process does not have, and the bias grows rather than +/// shrinks as the step is refined, because a finer step visits the boundary +/// more often. Full truncation has the smallest measured bias of the +/// published variants, which is why it is the one in use. +/// +/// Panics: +/// Panics unless `dt`, `kappa` and `theta` are positive and `x0` is +/// non-negative. +/// +/// Rust: `stochastic::sde::cir_process` +#[pyfunction] +#[pyo3(name = "cir_process", signature = (x0, kappa, theta, sigma, n, dt, rng))] +pub fn pyfn_cir_process(x0: f64, kappa: f64, theta: f64, sigma: f64, n: usize, dt: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::sde::cir_process(x0, kappa, theta, sigma, n, dt, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Heston paths: an asset whose variance is itself a Cox-Ingersoll-Ross +/// process. +/// +/// The correlation between the two noises is what makes the model useful. +/// A negative `rho` means variance rises when the price falls, which +/// reproduces the skew that a constant-volatility model cannot. +/// +/// Panics: +/// Panics unless `dt` and `s0` are positive, `v0` is non-negative, and the +/// correlation lies in `[-1, 1]`. +/// +/// Rust: `stochastic::sde::heston_paths` +#[pyfunction] +#[pyo3(name = "heston_paths", signature = (s0, v0, params, n, dt, rng))] +pub fn pyfn_heston_paths(s0: f64, v0: f64, params: crate::generated::types::PyHestonParamsArg, n: usize, dt: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(Vec, Vec)> { + let params = params.0; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::sde::heston_paths(s0, v0, params, n, dt, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Merton's jump diffusion: geometric Brownian motion with Poisson jumps of +/// lognormal size. +/// +/// The jumps put weight in the tails that a diffusion cannot, which is what +/// the model exists for. Between jumps it is exactly geometric Brownian +/// motion, and the compensator `lambda (exp(jump_mu + jump_sigma^2/2) - 1)` +/// is subtracted from the drift so the expected return is `mu` whether or +/// not a jump lands. +/// +/// Panics: +/// Panics unless `dt` and `x0` are positive and `lambda` is non-negative. +/// +/// Rust: `stochastic::sde::jump_diffusion_merton` +#[pyfunction] +#[pyo3(name = "jump_diffusion_merton", signature = (x0, mu, sigma, lambda_, jump_mu, jump_sigma, n, dt, rng))] +pub fn pyfn_jump_diffusion_merton(x0: f64, mu: f64, sigma: f64, lambda_: f64, jump_mu: f64, jump_sigma: f64, n: usize, dt: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::sde::jump_diffusion_merton(x0, mu, sigma, lambda_, jump_mu, jump_sigma, n, dt, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// A draw from a stable distribution by the Chambers-Mallows-Stuck method. +/// +/// The stable laws are the only possible limits of normalised sums, and only +/// the Gaussian among them has finite variance. `alpha` is the tail index: +/// two gives a Gaussian, one with `beta` zero gives Cauchy, and anything +/// below two has infinite variance and a tail decaying like a power rather +/// than an exponential. `beta` is the skew. +/// +/// Panics: +/// Panics unless `alpha` is in `(0, 2]` and `beta` is in `[-1, 1]`. +/// +/// Rust: `stochastic::sde::levy_stable_sample` +#[pyfunction] +#[pyo3(name = "levy_stable_sample", signature = (alpha, beta, rng))] +pub fn pyfn_levy_stable_sample(alpha: f64, beta: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::sde::levy_stable_sample(alpha, beta, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Fractional Brownian motion with Hurst parameter `h`, by the Davies-Harte +/// method. +/// +/// Increments are correlated rather than independent: `h` above a half gives +/// a path that persists, below a half one that reverses, and exactly a half +/// gives ordinary Brownian motion. Davies and Harte's method embeds the +/// covariance into a circulant matrix, whose eigenvalues a Fourier transform +/// supplies, so an exact sample costs one transform instead of a Cholesky +/// factorisation. +/// +/// Panics: +/// Panics unless `h` is in `(0, 1)` and `n` is positive. Falls back to a +/// Cholesky construction if the circulant embedding is not non-negative +/// definite, which can happen near the ends of the range. +/// +/// Rust: `stochastic::sde::fractional_brownian` +#[pyfunction] +#[pyo3(name = "fractional_brownian", signature = (h, n, rng))] +pub fn pyfn_fractional_brownian(h: f64, n: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::sde::fractional_brownian(h, n, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Hurst exponent by rescaled range analysis. +/// +/// Split the series into blocks of several sizes, and for each measure the +/// range of the cumulative deviation from the block mean divided by the +/// block's standard deviation. That ratio grows like the block size to the +/// power `H`, and the slope on log axes is the estimate. Hurst found the +/// relation studying Nile flood records; the point is that it needs no model +/// of the process at all. +/// +/// Panics: +/// Panics unless the series has at least sixteen points. +/// +/// Rust: `stochastic::sde::hurst_exponent_rs` +#[pyfunction] +#[pyo3(name = "hurst_exponent_rs", signature = (x))] +pub fn pyfn_hurst_exponent_rs<'py>(py: Python<'py>, x: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::sde::hurst_exponent_rs(&x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Hurst exponent by detrended fluctuation analysis. +/// +/// Integrate the series, split it into windows, remove a linear trend from +/// each, and measure the residual fluctuation against the window size. The +/// detrending is what lets it work on data with a slow drift, which +/// rescaled range analysis mistakes for persistence. +/// +/// The input should be the *increments* -- fractional Gaussian noise, not +/// fractional Brownian motion. Feeding it an already-integrated series +/// returns `H + 1`, since the routine integrates once itself. +/// +/// Windows shorter than sixteen points are skipped. Removing a straight line +/// from eight points takes out a real part of the fluctuation along with the +/// trend, which biases the exponent up by several hundredths -- enough to +/// make white noise look persistent. +/// +/// Panics: +/// Panics unless the series has at least thirty-two points. +/// +/// Rust: `stochastic::sde::hurst_dfa` +#[pyfunction] +#[pyo3(name = "hurst_dfa", signature = (x))] +pub fn pyfn_hurst_dfa<'py>(py: Python<'py>, x: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::sde::hurst_dfa(&x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// First passage times of a drifting Brownian motion to a barrier, by +/// simulation. +/// +/// Returns one time per path that reached the barrier; paths that did not are +/// omitted, so a short horizon returns fewer times than paths. +/// +/// Panics: +/// Panics unless `dt`, `t_end` and `n_paths` are positive. +/// +/// Rust: `stochastic::sde::first_passage_time_sim` +#[pyfunction] +#[pyo3(name = "first_passage_time_sim", signature = (barrier, drift, diffusion, t_end, dt, n_paths, rng))] +pub fn pyfn_first_passage_time_sim(barrier: f64, drift: f64, diffusion: f64, t_end: f64, dt: f64, n_paths: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::sde::first_passage_time_sim(barrier, drift, diffusion, t_end, dt, n_paths, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The exact density of the first passage time of a drifting Brownian motion +/// to a barrier. +/// +/// The inverse Gaussian density. It has a closed form because the reflection +/// principle turns the question "did the path ever reach the barrier" into a +/// statement about where the reflected path ended, which is an ordinary +/// Gaussian probability. +/// +/// Panics: +/// Panics unless `t` and the barrier are positive. +/// +/// Rust: `stochastic::sde::first_passage_bm_exact` +#[pyfunction] +#[pyo3(name = "first_passage_bm_exact", signature = (barrier, drift, diffusion, t))] +pub fn pyfn_first_passage_bm_exact(barrier: f64, drift: f64, diffusion: f64, t: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::sde::first_passage_bm_exact(barrier, drift, diffusion, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The probability that a drifting Brownian motion has reached the barrier +/// by time `t`. +/// +/// `Phi((mu t - b) / (sigma sqrt t)) + exp(2 mu b / sigma^2) +/// Phi((-mu t - b) / (sigma sqrt t))`. The second term is the reflection +/// principle's contribution: paths that crossed and came back are counted by +/// reflecting them about the barrier, which maps them onto paths that ended +/// beyond it. With a non-positive drift the limit as `t` grows is +/// `exp(2 mu b / sigma^2)` rather than one, since such a path may never +/// arrive at all. +/// +/// Panics: +/// Panics unless `t` and the barrier are positive. +/// +/// Rust: `stochastic::sde::first_passage_bm_cdf` +#[pyfunction] +#[pyo3(name = "first_passage_bm_cdf", signature = (barrier, drift, diffusion, t))] +pub fn pyfn_first_passage_bm_cdf(barrier: f64, drift: f64, diffusion: f64, t: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::sde::first_passage_bm_cdf(barrier, drift, diffusion, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Checks the Feynman-Kac correspondence: the expectation of a payoff along +/// simulated paths against the solution of the matching partial differential +/// equation. +/// +/// Returns `(monte_carlo, closed_form)` for a European call under geometric +/// Brownian motion, where the closed form is Black-Scholes. That the two +/// agree is not a coincidence -- Feynman-Kac says the expectation of a +/// terminal payoff over the paths of a diffusion *is* the solution of the +/// backward equation, which is what turns an option price into a partial +/// differential equation and back. +/// +/// Panics: +/// Panics unless the parameters are positive. +/// +/// Rust: `stochastic::sde::feynman_kac_check` +#[pyfunction] +#[pyo3(name = "feynman_kac_check", signature = (s0, strike, rate, sigma, t, n_paths, rng))] +pub fn pyfn_feynman_kac_check(s0: f64, strike: f64, rate: f64, sigma: f64, t: f64, n_paths: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(f64, f64)> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::sde::feynman_kac_check(s0, strike, rate, sigma, t, n_paths, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Checks Ito's isometry: the variance of a stochastic integral equals the +/// integral of the squared integrand. +/// +/// Returns `(measured, expected)`. The isometry is what makes stochastic +/// integration work at all -- it says the map from integrands to integrals +/// preserves the `L^2` norm, so the integral can be defined for any +/// square-integrable integrand by taking limits. +/// +/// Panics: +/// Panics unless `t`, `n_paths` and `steps` are positive. +/// +/// Rust: `stochastic::sde::ito_isometry_check` +#[pyfunction] +#[pyo3(name = "ito_isometry_check", signature = (sigma, t, steps, n_paths, rng))] +pub fn pyfn_ito_isometry_check(sigma: pyo3::Py, t: f64, steps: usize, n_paths: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(f64, f64)> { + let __cb_sigma = std::rc::Rc::new(crate::runtime::Callback::new(sigma)); + let sigma = { let __cb = __cb_sigma.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::sde::ito_isometry_check(&sigma, t, steps, n_paths, &mut rng.inner)); + crate::runtime::callback::check(&[&__cb_sigma], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Underdamped Langevin dynamics by the BAOAB splitting. +/// +/// A particle in a force field with friction and thermal noise. The +/// integrator splits the dynamics into a drift, a kick and an +/// Ornstein-Uhlenbeck step on the velocity, and applies them in the +/// palindromic order B-A-O-A-B. The symmetry is what gives it the best known +/// accuracy for configurational averages: at any step size it samples +/// positions from very nearly the right distribution, even where the +/// velocities are visibly wrong. +/// +/// Returns position and velocity at each step. `temp` is in energy units, so +/// the equipartition result is ` = temp / mass`. +/// +/// Panics: +/// Panics unless `dt`, `mass` and `gamma` are positive and `temp` is +/// non-negative. +/// +/// Rust: `stochastic::sde::langevin_underdamped` +#[pyfunction] +#[pyo3(name = "langevin_underdamped", signature = (x0, v0, gamma, temp, mass, force, n, dt, rng))] +pub fn pyfn_langevin_underdamped(x0: f64, v0: f64, gamma: f64, temp: f64, mass: f64, force: pyo3::Py, n: usize, dt: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let __cb_force = std::rc::Rc::new(crate::runtime::Callback::new(force)); + let force = { let __cb = __cb_force.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::sde::langevin_underdamped(x0, v0, gamma, temp, mass, &force, n, dt, &mut rng.inner)); + crate::runtime::callback::check(&[&__cb_force], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// One step of the Fokker-Planck equation by the Chang-Cooper scheme. +/// +/// The density evolves as `dp/dt = -d(mu p)/dx + 0.5 d^2(sigma^2 p)/dx^2`. +/// Chang and Cooper's discretisation weights the drift term so that the +/// scheme's own stationary solution is the exact one -- an ordinary centred +/// difference relaxes to a slightly wrong density and stays there, which is +/// the failure this scheme exists to avoid. Zero-flux boundaries, so the +/// total probability is conserved exactly. +/// +/// Panics: +/// Panics unless `dx`, `dt` are positive and the density has at least three +/// points. +/// +/// Rust: `stochastic::sde::fokker_planck_1d` +#[pyfunction] +#[pyo3(name = "fokker_planck_1d", signature = (p0, mu, sigma, x_min, dx, dt, steps))] +pub fn pyfn_fokker_planck_1d(p0: Vec, mu: pyo3::Py, sigma: pyo3::Py, x_min: f64, dx: f64, dt: f64, steps: usize) -> PyResult> { + let __cb_mu = std::rc::Rc::new(crate::runtime::Callback::new(mu)); + let mu = { let __cb = __cb_mu.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __cb_sigma = std::rc::Rc::new(crate::runtime::Callback::new(sigma)); + let sigma = { let __cb = __cb_sigma.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::sde::fokker_planck_1d(&p0, &mu, &sigma, x_min, dx, dt, steps)); + crate::runtime::callback::check(&[&__cb_mu, &__cb_sigma], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The stationary density of a one-dimensional diffusion, in closed form. +/// +/// `p(x) proportional to exp(2 integral mu / sigma^2) / sigma^2`. It is the +/// zero-flux solution: the drift's tendency to push probability one way +/// exactly balances diffusion's tendency to spread it, at every point rather +/// than on average. Returned normalised over the grid. +/// +/// Panics: +/// Panics unless the range is increasing and `n` is at least two. +/// +/// Rust: `stochastic::sde::stationary_density_1d` +#[pyfunction] +#[pyo3(name = "stationary_density_1d", signature = (mu, sigma, x_range, n))] +pub fn pyfn_stationary_density_1d(mu: pyo3::Py, sigma: pyo3::Py, x_range: (f64, f64), n: usize) -> PyResult> { + let __cb_mu = std::rc::Rc::new(crate::runtime::Callback::new(mu)); + let mu = { let __cb = __cb_mu.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __cb_sigma = std::rc::Rc::new(crate::runtime::Callback::new(sigma)); + let sigma = { let __cb = __cb_sigma.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let x_range = (x_range.0, x_range.1); + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::sde::stationary_density_1d(&mu, &sigma, x_range, n)); + crate::runtime::callback::check(&[&__cb_mu, &__cb_sigma], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Kramers' escape rate from a potential well over a barrier. +/// +/// `(omega_well omega_barrier / (2 pi gamma)) exp(-barrier / temp)` in the +/// high-friction limit. The exponential is Arrhenius and is the part everyone +/// knows; Kramers' contribution was the prefactor, which says the rate falls +/// as friction rises, because a strongly damped particle takes longer to +/// diffuse across the barrier top even once it has the energy. +/// +/// Panics: +/// Panics unless the temperature, friction and both frequencies are +/// positive. +/// +/// Rust: `stochastic::sde::kramers_escape_rate` +#[pyfunction] +#[pyo3(name = "kramers_escape_rate", signature = (barrier_height, temp, omega_well, omega_barrier, gamma))] +pub fn pyfn_kramers_escape_rate(barrier_height: f64, temp: f64, omega_well: f64, omega_barrier: f64, gamma: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::sde::kramers_escape_rate(barrier_height, temp, omega_well, omega_barrier, gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Simulates a bistable system driven by a weak periodic force and noise, and +/// returns the path. +/// +/// Stochastic resonance is the phenomenon that adding noise can *improve* the +/// response to a signal too weak to drive the system on its own: the noise +/// supplies the energy to cross the barrier, and the signal decides when. The +/// effect is largest at an intermediate noise level, which is what a sweep +/// over `temp` shows. +/// +/// Panics: +/// Panics unless `dt` is positive and `temp` is non-negative. +/// +/// Rust: `stochastic::sde::stochastic_resonance_sim` +#[pyfunction] +#[pyo3(name = "stochastic_resonance_sim", signature = (x0, amplitude, frequency, temp, n, dt, rng))] +pub fn pyfn_stochastic_resonance_sim(x0: f64, amplitude: f64, frequency: f64, temp: f64, n: usize, dt: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::sde::stochastic_resonance_sim(x0, amplitude, frequency, temp, n, dt, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_brownian_motion, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_brownian_bridge, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_brownian_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_brownian_3d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_geometric_brownian, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gbm_exact, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ornstein_uhlenbeck, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ou_exact_step, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_euler_maruyama, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_milstein, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_stochastic_heun, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_srk_order_1_5, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_strong_convergence_order, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_weak_convergence_order, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cir_process, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_heston_paths, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_jump_diffusion_merton, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_levy_stable_sample, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fractional_brownian, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hurst_exponent_rs, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hurst_dfa, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_first_passage_time_sim, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_first_passage_bm_exact, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_first_passage_bm_cdf, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_feynman_kac_check, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ito_isometry_check, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_langevin_underdamped, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fokker_planck_1d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_stationary_density_1d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kramers_escape_rate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_stochastic_resonance_sim, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_stochastic__timeseries.rs b/bindings/python/src/generated/m_stochastic__timeseries.rs new file mode 100644 index 0000000..48e8fb8 --- /dev/null +++ b/bindings/python/src/generated/m_stochastic__timeseries.rs @@ -0,0 +1,657 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Sample autocorrelation at lags `0..=max_lag`. +/// +/// Uses the divide-by-`n` estimator rather than dividing each lag by its own +/// count. That biases individual lags toward zero, but it is the choice that +/// makes the resulting sequence positive semi-definite, which is what lets +/// `pacf` and the Yule-Walker equations be solved at all. The +/// divide-by-`n-k` version can produce a sequence no stationary process +/// possesses, and Durbin-Levinson then divides by a negative variance. +/// +/// Element 0 is 1 by construction. +/// +/// Panics: +/// Panics unless the series has at least two points and `max_lag < n`. +/// +/// Rust: `stochastic::timeseries::acf` +#[pyfunction] +#[pyo3(name = "acf", signature = (x, max_lag))] +pub fn pyfn_acf<'py>(py: Python<'py>, x: Vec, max_lag: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::timeseries::acf(&x, max_lag))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Sample partial autocorrelation at lags `0..=max_lag`, by the +/// Durbin-Levinson recursion. +/// +/// The partial autocorrelation at lag `k` is the correlation between `x_t` +/// and `x_{t-k}` once the intervening lags are projected out -- equivalently, +/// the last coefficient of the best linear predictor of order `k`. For an +/// AR(p) process it is exactly zero beyond lag `p`, which is what makes it +/// the tool for choosing `p`. +/// +/// Element 0 is 1, matching `acf`. +/// +/// Panics: +/// Panics under the same conditions as `acf`. +/// +/// Rust: `stochastic::timeseries::pacf` +#[pyfunction] +#[pyo3(name = "pacf", signature = (x, max_lag))] +pub fn pyfn_pacf<'py>(py: Python<'py>, x: Vec, max_lag: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::timeseries::pacf(&x, max_lag))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Cross-correlation of `x` and `y` at lags `-max_lag..=max_lag`. +/// +/// Element `max_lag + k` is the correlation between `x_t` and `y_{t+k}`, so a +/// peak at positive `k` means `x` leads `y` by `k` steps. +/// +/// Panics: +/// Panics unless both series have the same length, at least two points, and +/// `max_lag < n`. +/// +/// Rust: `stochastic::timeseries::cross_correlation_lags` +#[pyfunction] +#[pyo3(name = "cross_correlation_lags", signature = (x, y, max_lag))] +pub fn pyfn_cross_correlation_lags<'py>(py: Python<'py>, x: Vec, y: Vec, max_lag: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::timeseries::cross_correlation_lags(&x, &y, max_lag))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The Ljung-Box portmanteau test for autocorrelation up to lag `lags`. +/// +/// `Q = n(n+2) sum_{k=1}^{h} r_k^2 / (n-k)`, which is asymptotically +/// chi-squared on `h` degrees of freedom under the null that the series is +/// uncorrelated. A small p-value says the series has structure a white-noise +/// model would not produce. +/// +/// Panics: +/// Panics unless `lags >= 1` and `lags < n`. +/// +/// Rust: `stochastic::timeseries::ljung_box` +#[pyfunction] +#[pyo3(name = "ljung_box", signature = (x, lags))] +pub fn pyfn_ljung_box(x: Vec, lags: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::timeseries::ljung_box(&x, lags)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTestResult { inner: __v }) +} + +/// The `d`-th successive difference of `x`, shortening it by `d`. +/// +/// Panics: +/// Panics if `d >= x.len()`. +/// +/// Rust: `stochastic::timeseries::difference` +#[pyfunction] +#[pyo3(name = "difference", signature = (x, d))] +pub fn pyfn_difference<'py>(py: Python<'py>, x: Vec, d: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::timeseries::difference(&x, d))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The seasonal difference `x_t - x_{t-s}`, shortening the series by `s`. +/// +/// Panics: +/// Panics unless `1 <= s < x.len()`. +/// +/// Rust: `stochastic::timeseries::seasonal_difference` +#[pyfunction] +#[pyo3(name = "seasonal_difference", signature = (x, s))] +pub fn pyfn_seasonal_difference<'py>(py: Python<'py>, x: Vec, s: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::timeseries::seasonal_difference(&x, s))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Rebuilds a series from its differences. +/// +/// `initial` holds the first element of each successive difference, lowest +/// order first: `initial[j]` is `difference(x, j)[0]`, so `initial[0]` is +/// `x[0]`. Its length sets the differencing order being undone. Exactly +/// inverts `difference`. +/// +/// Panics: +/// Panics if `initial` is empty. +/// +/// Rust: `stochastic::timeseries::undifference` +#[pyfunction] +#[pyo3(name = "undifference", signature = (diffed, initial))] +pub fn pyfn_undifference<'py>(py: Python<'py>, diffed: Vec, initial: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::timeseries::undifference(&diffed, &initial))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The augmented Dickey-Fuller test for a unit root, with a constant and no +/// trend. +/// +/// Regresses `dy_t` on `y_{t-1}`, a constant, and `lags` lagged differences; +/// the statistic is the `t`-ratio on the `y_{t-1}` coefficient. The null is +/// that a unit root is present, so a *small* p-value is evidence the series +/// is stationary. `df` reports the residual degrees of freedom. +/// +/// The p-value is interpolated from the module's table of Dickey-Fuller +/// quantiles; see the note on that table for why a `t` distribution would be +/// the wrong reference. +/// +/// Errors: +/// Returns an error if the series is too short for the requested lag order or +/// the regression is rank deficient. +/// +/// Rust: `stochastic::timeseries::adf_test` +#[pyfunction] +#[pyo3(name = "adf_test", signature = (x, lags))] +pub fn pyfn_adf_test(x: Vec, lags: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::timeseries::adf_test(&x, lags)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyTestResult { inner: __v }) +} + +/// The KPSS test for level stationarity. +/// +/// The statistic is `sum_t S_t^2 / (n^2 s^2(l))`, where `S_t` is the partial +/// sum of deviations from the mean and `s^2(l)` is a Newey-West long-run +/// variance with the usual `l = floor(4 (n/100)^{1/4})` bandwidth. Here +/// stationarity is the *null*, so a small p-value is evidence against it -- +/// the opposite polarity to `adf_test`, which is the point of running both. +/// +/// `df` is reported as the bandwidth actually used. +/// +/// Errors: +/// Returns an error for a series shorter than four points or one with no +/// variation at all. +/// +/// Rust: `stochastic::timeseries::kpss_test` +#[pyfunction] +#[pyo3(name = "kpss_test", signature = (x))] +pub fn pyfn_kpss_test(x: Vec) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::timeseries::kpss_test(&x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyTestResult { inner: __v }) +} + +/// Selects `(p, d, q)` by minimising AIC over a grid, choosing `d` by +/// differencing until an augmented Dickey-Fuller test rejects a unit root. +/// +/// Differencing order is settled first and separately, because AIC cannot +/// compare across it: differencing changes the data the likelihood is +/// computed on, so the numbers are not on the same scale. +/// +/// Errors: +/// Returns an error if no candidate model in the grid can be fitted. +/// +/// Rust: `stochastic::timeseries::auto_arima` +#[pyfunction] +#[pyo3(name = "auto_arima", signature = (x, max_p, max_d, max_q))] +pub fn pyfn_auto_arima(x: Vec, max_p: usize, max_d: usize, max_q: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::timeseries::auto_arima(&x, max_p, max_d, max_q)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyArima { inner: __v }) +} + +/// Simple exponential smoothing: `s_t = alpha x_t + (1 - alpha) s_{t-1}`, +/// seeded at `x_0`. +/// +/// The smoothed value is a geometrically weighted average of the whole past, +/// and the weights sum to one, so a constant series is reproduced exactly at +/// any `alpha`. +/// +/// Panics: +/// Panics unless `x` is non-empty and `alpha` is in `[0, 1]`. +/// +/// Rust: `stochastic::timeseries::exponential_smoothing` +#[pyfunction] +#[pyo3(name = "exponential_smoothing", signature = (x, alpha))] +pub fn pyfn_exponential_smoothing<'py>(py: Python<'py>, x: Vec, alpha: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::timeseries::exponential_smoothing(&x, alpha))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Holt's linear method: a smoothed level and a smoothed slope. +/// +/// Element `t` of the result is the one-step-ahead prediction of `x[t]`, made +/// from the state after seeing `x[t-1]` -- the same convention as +/// `holt_winters`. Unlike simple smoothing this tracks a linear trend +/// without lagging behind it. +/// +/// The state is seeded one step *before* the data: the slope from the first +/// two points, and a level back-extrapolated so that `level + trend` equals +/// `x[0]`. Seeding the level at `x[0]` itself, as is often done, puts the +/// state half a step ahead of where the recursion expects it and leaves a +/// transient that takes tens of observations to decay -- on an exact straight +/// line, which the method should reproduce perfectly from the first step. +/// +/// Panics: +/// Panics unless `x` has at least two points and both parameters lie in +/// `[0, 1]`. +/// +/// Rust: `stochastic::timeseries::double_exponential` +#[pyfunction] +#[pyo3(name = "double_exponential", signature = (x, alpha, beta))] +pub fn pyfn_double_exponential<'py>(py: Python<'py>, x: Vec, alpha: f64, beta: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::timeseries::double_exponential(&x, alpha, beta))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Holt-Winters triple exponential smoothing. +/// +/// Tracks a level, a slope, and a set of seasonal factors, each updated by +/// its own smoothing constant. Returns the one-step-ahead fitted values +/// alongside the final state. +/// +/// The seasonal factors are initialised from the first complete season and, +/// in the additive case, centred so they sum to zero -- otherwise the level +/// and the seasonal component are not separately identified and the pair can +/// drift apart while their sum stays right. +/// +/// Panics: +/// Panics unless the series covers at least two full seasons, `season_len` is +/// at least 2, and all three parameters lie in `[0, 1]`. +/// +/// Rust: `stochastic::timeseries::holt_winters` +#[pyfunction] +#[pyo3(name = "holt_winters", signature = (x, alpha, beta, gamma, season_len, multiplicative))] +pub fn pyfn_holt_winters(x: Vec, alpha: f64, beta: f64, gamma: f64, season_len: usize, multiplicative: bool) -> PyResult<(Vec, crate::generated::types::PyHwState)> { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::timeseries::holt_winters(&x, alpha, beta, gamma, season_len, multiplicative)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, crate::generated::types::PyHwState { inner: __v.1 })) +} + +/// Chooses `(alpha, beta, gamma)` by minimising the one-step-ahead sum of +/// squared errors over a coarse grid followed by a local refinement. +/// +/// A grid rather than a gradient method: the Holt-Winters error surface is +/// not convex in the three constants and has flat regions near the corners of +/// the unit cube, where a local method started badly will simply stop. +/// +/// Panics: +/// Panics under the same conditions as `holt_winters`. +/// +/// Rust: `stochastic::timeseries::holt_winters_optimize` +#[pyfunction] +#[pyo3(name = "holt_winters_optimize", signature = (x, season_len))] +pub fn pyfn_holt_winters_optimize<'py>(py: Python<'py>, x: Vec, season_len: usize) -> PyResult<(f64, f64, f64)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::timeseries::holt_winters_optimize(&x, season_len))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// The RiskMetrics exponentially weighted variance, +/// `v_t = lambda v_{t-1} + (1 - lambda) r_{t-1}^2`. +/// +/// A GARCH(1,1) with `omega = 0` and unit persistence: no mean reversion, so +/// the variance wanders rather than settling. +/// +/// Panics: +/// Panics unless `returns` is non-empty and `lambda` lies in `[0, 1)`. +/// +/// Rust: `stochastic::timeseries::ewma_variance` +#[pyfunction] +#[pyo3(name = "ewma_variance", signature = (returns, lambda_))] +pub fn pyfn_ewma_variance<'py>(py: Python<'py>, returns: Vec, lambda_: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::timeseries::ewma_variance(&returns, lambda_))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Engle's ARCH LM test for conditional heteroskedasticity. +/// +/// Regresses squared returns on their own lags; the statistic `n R^2` is +/// asymptotically chi-squared on `lags` degrees of freedom under the null of +/// no ARCH effect. A small p-value says the size of a return predicts the +/// size of the next one, which is precisely what a GARCH model is for. +/// +/// Errors: +/// Returns an error if the series is too short or the regression is +/// degenerate. +/// +/// Rust: `stochastic::timeseries::arch_lm_test` +#[pyfunction] +#[pyo3(name = "arch_lm_test", signature = (returns, lags))] +pub fn pyfn_arch_lm_test(returns: Vec, lags: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::timeseries::arch_lm_test(&returns, lags)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyTestResult { inner: __v }) +} + +/// Tests whether `x` Granger-causes `y`: whether past `x` improves a forecast +/// of `y` that already uses past `y`. +/// +/// An `F` test of the restricted regression of `y` on its own lags against +/// the unrestricted one that adds the lags of `x`. The name is a term of art +/// -- it is predictive precedence, not causation, and a common driver of both +/// series will produce it. +/// +/// Errors: +/// Returns an error if the series differ in length, are too short, or either +/// regression is degenerate. +/// +/// Rust: `stochastic::timeseries::granger_causality` +#[pyfunction] +#[pyo3(name = "granger_causality", signature = (x, y, lags))] +pub fn pyfn_granger_causality(x: Vec, y: Vec, lags: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::timeseries::granger_causality(&x, &y, lags)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyTestResult { inner: __v }) +} + +/// The Engle-Granger two-step test for cointegration between `x` and `y`. +/// +/// Regresses `y` on `x` with an intercept, then tests the residual for a unit +/// root. Rejecting means some linear combination of two individually +/// non-stationary series is stationary -- they share a stochastic trend. +/// +/// The p-value comes from the module's Engle-Granger table rather than its +/// plain Dickey-Fuller one: the residual is fitted rather than observed, and the +/// regression has already worked to make it look stationary, so the null +/// distribution sits further left. Using the ordinary table here is a common +/// way to find cointegration that is not there. +/// +/// Errors: +/// Returns an error if the series differ in length, are too short, or the +/// first-stage regression is degenerate. +/// +/// Rust: `stochastic::timeseries::cointegration_engle_granger` +#[pyfunction] +#[pyo3(name = "cointegration_engle_granger", signature = (x, y))] +pub fn pyfn_cointegration_engle_granger(x: Vec, y: Vec) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::timeseries::cointegration_engle_granger(&x, &y)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyTestResult { inner: __v }) +} + +/// Additive seasonal decomposition into `(trend, seasonal, residual)`. +/// +/// The trend is a centred moving average over one full period; the seasonal +/// component is the average detrended value at each phase, centred to sum to +/// zero; the residual is whatever is left. Near the ends, where the moving +/// average has no window, the trend is held at the nearest value it does +/// have -- so the three components add back to the input exactly at every +/// index, which is the property that makes the decomposition usable rather +/// than merely indicative. +/// +/// Panics: +/// Panics unless `period >= 2` and the series covers at least two periods. +/// +/// Rust: `stochastic::timeseries::seasonal_decompose_stl_lite` +#[pyfunction] +#[pyo3(name = "seasonal_decompose_stl_lite", signature = (x, period))] +pub fn pyfn_seasonal_decompose_stl_lite<'py>(py: Python<'py>, x: Vec, period: usize) -> PyResult<(Vec, Vec, Vec)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::timeseries::seasonal_decompose_stl_lite(&x, period))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// Change-in-mean detection by PELT (pruned exact linear time). +/// +/// Finds the segmentation minimising the total within-segment sum of squares +/// plus `penalty` per changepoint. Unlike binary segmentation this is exact: +/// dynamic programming considers every segmentation, and the pruning step +/// discards only candidates that provably cannot start an optimal segment, +/// so the answer is the global optimum rather than a greedy approximation. +/// +/// Returns the interior changepoint indices, each the first index of a new +/// segment, in increasing order. +/// +/// Panics: +/// Panics if `penalty` is negative. +/// +/// Rust: `stochastic::timeseries::changepoint_pelt` +#[pyfunction] +#[pyo3(name = "changepoint_pelt", signature = (x, penalty))] +pub fn pyfn_changepoint_pelt<'py>(py: Python<'py>, x: Vec, penalty: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::timeseries::changepoint_pelt(&x, penalty))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Change-in-mean detection by recursive binary segmentation. +/// +/// Splits at the point giving the largest reduction in sum of squares, then +/// recurses into both halves, stopping at `max_k` changepoints. Greedy rather +/// than exact -- it can miss a pair of changes whose individual effects +/// cancel -- but it is fast and needs no penalty to be chosen. +/// +/// Returns changepoint indices in increasing order. +/// +/// Rust: `stochastic::timeseries::changepoint_binary_segmentation` +#[pyfunction] +#[pyo3(name = "changepoint_binary_segmentation", signature = (x, max_k))] +pub fn pyfn_changepoint_binary_segmentation<'py>(py: Python<'py>, x: Vec, max_k: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::timeseries::changepoint_binary_segmentation(&x, max_k))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Two-sided cumulative sum control statistics, `(upper, lower)`. +/// +/// `S+_t = max(0, S+_{t-1} + (x_t - target) - k)` and the mirror image for +/// the lower arm. The slack `k` is what stops the statistic drifting on +/// ordinary noise: with `k` set to half the shift worth detecting, the +/// statistic stays near zero while the process is on target and climbs +/// roughly linearly once it is not. +/// +/// Panics: +/// Panics if `k` is negative. +/// +/// Rust: `stochastic::timeseries::cusum` +#[pyfunction] +#[pyo3(name = "cusum", signature = (x, target, k))] +pub fn pyfn_cusum<'py>(py: Python<'py>, x: Vec, target: f64, k: f64) -> PyResult<(Vec, Vec)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::timeseries::cusum(&x, target, k))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// The matrix profile of `x` for subsequences of length `m`: +/// `(distance to the nearest other subsequence, its index)`. +/// +/// Distances are z-normalised Euclidean, so a match is about shape rather +/// than level or amplitude. Overlapping neighbours are excluded -- a +/// subsequence's closest match is always the one shifted by one sample, which +/// says nothing -- using the usual exclusion zone of half the window. +/// +/// The smallest entries locate the repeated motifs; the largest locates the +/// discord, the least-like-anything-else stretch. +/// +/// Panics: +/// Panics unless `m >= 2` and the series holds at least two non-overlapping +/// windows. +/// +/// Rust: `stochastic::timeseries::matrix_profile_lite` +#[pyfunction] +#[pyo3(name = "matrix_profile_lite", signature = (x, m))] +pub fn pyfn_matrix_profile_lite<'py>(py: Python<'py>, x: Vec, m: usize) -> PyResult<(Vec, Vec)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::timeseries::matrix_profile_lite(&x, m))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Sample entropy: the negative log probability that two sequences matching +/// for `m` points go on matching for `m + 1`. +/// +/// `tol` is given as a multiple of the series standard deviation. Unlike +/// `approximate_entropy` the self-match is excluded, which removes the bias +/// that otherwise makes a short series look more regular than it is. +/// +/// Returns infinity when no `m+1`-length match occurs at all, which is the +/// honest answer -- the estimator has run out of data rather than found zero +/// probability. +/// +/// Panics: +/// Panics unless `m >= 1`, `tol > 0`, and the series holds at least `m + 2` +/// points. +/// +/// Rust: `stochastic::timeseries::sample_entropy` +#[pyfunction] +#[pyo3(name = "sample_entropy", signature = (x, m, tol))] +pub fn pyfn_sample_entropy<'py>(py: Python<'py>, x: Vec, m: usize, tol: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::timeseries::sample_entropy(&x, m, tol))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Approximate entropy, the older cousin of `sample_entropy`. +/// +/// Includes the self-match, which guarantees the logarithm is defined but +/// biases the estimate toward regularity, the more so the shorter the series. +/// Kept because it is what a great deal of published work reports. +/// +/// Panics: +/// Panics under the same conditions as `sample_entropy`. +/// +/// Rust: `stochastic::timeseries::approximate_entropy` +#[pyfunction] +#[pyo3(name = "approximate_entropy", signature = (x, m, tol))] +pub fn pyfn_approximate_entropy<'py>(py: Python<'py>, x: Vec, m: usize, tol: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::timeseries::approximate_entropy(&x, m, tol))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Permutation entropy: the Shannon entropy of the ordinal patterns of length +/// `order` sampled at spacing `delay`, normalised to `[0, 1]`. +/// +/// Only the ranking within each window matters, so the measure is invariant +/// to any monotone transformation of the series and needs no tolerance +/// parameter. A monotone series visits one pattern and scores 0; independent +/// noise visits all `order!` patterns equally and scores 1. +/// +/// Panics: +/// Panics unless `order` is between 2 and 8, `delay >= 1`, and the series is +/// long enough to hold at least two windows. +/// +/// Rust: `stochastic::timeseries::permutation_entropy` +#[pyfunction] +#[pyo3(name = "permutation_entropy", signature = (x, order, delay))] +pub fn pyfn_permutation_entropy<'py>(py: Python<'py>, x: Vec, order: usize, delay: usize) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::timeseries::permutation_entropy(&x, order, delay))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// A surrogate-data test: how extreme `statistic(x)` is against the +/// distribution it takes on IAAFT surrogates of `x`. +/// +/// The surrogates share the series' amplitude distribution and power +/// spectrum, hence all of its linear structure. Rejecting therefore points at +/// something a linear Gaussian process could not produce -- nonlinearity -- +/// rather than merely at "not white noise", which is what a test against +/// shuffled data would show. +/// +/// Returns the two-sided rank p-value `(1 + #{|s_i - mean| >= |s_x - mean|}) / +/// (1 + n)`, which is exact for finite `n` rather than asymptotic. +/// +/// Panics: +/// Panics if `n_surrogates` is zero or the series is shorter than four points. +/// +/// Rust: `stochastic::timeseries::surrogate_test_iaaft` +#[pyfunction] +#[pyo3(name = "surrogate_test_iaaft", signature = (x, statistic, n_surrogates, rng))] +pub fn pyfn_surrogate_test_iaaft(x: Vec, statistic: pyo3::Py, n_surrogates: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let __cb_statistic = std::rc::Rc::new(crate::runtime::Callback::new(statistic)); + let statistic = { let __cb = __cb_statistic.clone(); move |__a0: &[f64]| -> f64 { __cb.call::<_, f64>((__a0.to_vec(),), f64::NAN) } }; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::timeseries::surrogate_test_iaaft(&x, &statistic, n_surrogates, &mut rng.inner)); + crate::runtime::callback::check(&[&__cb_statistic], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The local level model: `x_t = mu_t + e_t`, `mu_t = mu_{t-1} + n_t`. +/// +/// Returns `(smoothed level, signal variance, observation variance)`. The two +/// variances are estimated by maximising the Gaussian likelihood from the +/// Kalman filter; only their ratio -- the signal-to-noise ratio, or hyper- +/// parameter `q` -- affects the filtered path, so it is that ratio the +/// optimiser searches over, with the overall scale then available in closed +/// form. +/// +/// The model is the state-space form of simple exponential smoothing: the +/// steady-state Kalman gain *is* the smoothing constant, so an estimated `q` +/// and an estimated `alpha` carry the same information. +/// +/// Errors: +/// Returns an error for a series shorter than five points or with no +/// variation. +/// +/// Rust: `stochastic::timeseries::state_space_local_level` +#[pyfunction] +#[pyo3(name = "state_space_local_level", signature = (x))] +pub fn pyfn_state_space_local_level<'py>(py: Python<'py>, x: Vec) -> PyResult<(Vec, f64, f64)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::timeseries::state_space_local_level(&x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_acf, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pacf, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cross_correlation_lags, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ljung_box, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_difference, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_seasonal_difference, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_undifference, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_adf_test, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kpss_test, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_auto_arima, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_exponential_smoothing, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_double_exponential, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_holt_winters, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_holt_winters_optimize, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ewma_variance, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_arch_lm_test, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_granger_causality, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cointegration_engle_granger, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_seasonal_decompose_stl_lite, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_changepoint_pelt, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_changepoint_binary_segmentation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cusum, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_matrix_profile_lite, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sample_entropy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_approximate_entropy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_permutation_entropy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_surrogate_test_iaaft, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_state_space_local_level, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_thermodynamics.rs b/bindings/python/src/generated/m_thermodynamics.rs new file mode 100644 index 0000000..01555ff --- /dev/null +++ b/bindings/python/src/generated/m_thermodynamics.rs @@ -0,0 +1,651 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Ideal gas law: PV = nRT. Solve for pressure: P = nRT / V +/// +/// Rust: `thermodynamics::ideal_gas_pressure` +#[pyfunction] +#[pyo3(name = "ideal_gas_pressure", signature = (moles, temperature, volume))] +pub fn pyfn_ideal_gas_pressure(moles: f64, temperature: f64, volume: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::ideal_gas_pressure(moles, temperature, volume)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Solve for volume: V = nRT / P +/// +/// Rust: `thermodynamics::ideal_gas_volume` +#[pyfunction] +#[pyo3(name = "ideal_gas_volume", signature = (moles, temperature, pressure))] +pub fn pyfn_ideal_gas_volume(moles: f64, temperature: f64, pressure: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::ideal_gas_volume(moles, temperature, pressure)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Solve for temperature: T = PV / (nR) +/// +/// Rust: `thermodynamics::ideal_gas_temperature` +#[pyfunction] +#[pyo3(name = "ideal_gas_temperature", signature = (pressure, volume, moles))] +pub fn pyfn_ideal_gas_temperature(pressure: f64, volume: f64, moles: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::ideal_gas_temperature(pressure, volume, moles)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Number of moles: n = PV / (RT) +/// +/// Rust: `thermodynamics::ideal_gas_moles` +#[pyfunction] +#[pyo3(name = "ideal_gas_moles", signature = (pressure, volume, temperature))] +pub fn pyfn_ideal_gas_moles(pressure: f64, volume: f64, temperature: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::ideal_gas_moles(pressure, volume, temperature)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Average kinetic energy of a gas molecule: KE = (3/2) * k_B * T +/// +/// Rust: `thermodynamics::average_kinetic_energy` +#[pyfunction] +#[pyo3(name = "average_kinetic_energy", signature = (temperature))] +pub fn pyfn_average_kinetic_energy(temperature: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::average_kinetic_energy(temperature)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// RMS speed of gas molecules: v_rms = sqrt(3 * k_B * T / m) +/// +/// Rust: `thermodynamics::rms_speed` +#[pyfunction] +#[pyo3(name = "rms_speed", signature = (temperature, molecular_mass))] +pub fn pyfn_rms_speed(temperature: f64, molecular_mass: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::rms_speed(temperature, molecular_mass)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Mean free path: λ = 1 / (√2 * π * d^2 * n/V) +/// +/// Rust: `thermodynamics::mean_free_path` +#[pyfunction] +#[pyo3(name = "mean_free_path", signature = (molecular_diameter, number_density))] +pub fn pyfn_mean_free_path(molecular_diameter: f64, number_density: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::mean_free_path(molecular_diameter, number_density)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Heat transfer: Q = m * c * ΔT +/// +/// Rust: `thermodynamics::heat_transfer` +#[pyfunction] +#[pyo3(name = "heat_transfer", signature = (mass, specific_heat, delta_temp))] +pub fn pyfn_heat_transfer(mass: f64, specific_heat: f64, delta_temp: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::heat_transfer(mass, specific_heat, delta_temp)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Heat conduction (Fourier's law): Q/t = k * A * ΔT / d +/// +/// Rust: `thermodynamics::heat_conduction_rate` +#[pyfunction] +#[pyo3(name = "heat_conduction_rate", signature = (conductivity, area, delta_temp, thickness))] +pub fn pyfn_heat_conduction_rate(conductivity: f64, area: f64, delta_temp: f64, thickness: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::heat_conduction_rate(conductivity, area, delta_temp, thickness)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Heat radiation (Stefan-Boltzmann law): P = ε * σ * A * T^4 +/// +/// Rust: `thermodynamics::heat_radiation_power` +#[pyfunction] +#[pyo3(name = "heat_radiation_power", signature = (emissivity, area, temperature))] +pub fn pyfn_heat_radiation_power(emissivity: f64, area: f64, temperature: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::heat_radiation_power(emissivity, area, temperature)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Net radiative heat transfer: P = ε * σ * A * (T_hot^4 - T_cold^4) +/// +/// Rust: `thermodynamics::net_radiation_power` +#[pyfunction] +#[pyo3(name = "net_radiation_power", signature = (emissivity, area, t_hot, t_cold))] +pub fn pyfn_net_radiation_power(emissivity: f64, area: f64, t_hot: f64, t_cold: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::net_radiation_power(emissivity, area, t_hot, t_cold)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Newton's law of cooling: dT/dt = -k * (T - T_env) +/// Returns temperature at time t: T(t) = T_env + (T0 - T_env) * e^(-k*t) +/// +/// Rust: `thermodynamics::newton_cooling` +#[pyfunction] +#[pyo3(name = "newton_cooling", signature = (t_initial, t_environment, cooling_constant, time))] +pub fn pyfn_newton_cooling(t_initial: f64, t_environment: f64, cooling_constant: f64, time: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::newton_cooling(t_initial, t_environment, cooling_constant, time)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Work done by an ideal gas during isothermal expansion: W = nRT * ln(V2/V1) +/// +/// Rust: `thermodynamics::work_isothermal` +#[pyfunction] +#[pyo3(name = "work_isothermal", signature = (moles, temperature, v1, v2))] +pub fn pyfn_work_isothermal(moles: f64, temperature: f64, v1: f64, v2: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::work_isothermal(moles, temperature, v1, v2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Work done during isobaric (constant pressure) process: W = P * ΔV +/// +/// Rust: `thermodynamics::work_isobaric` +#[pyfunction] +#[pyo3(name = "work_isobaric", signature = (pressure, delta_v))] +pub fn pyfn_work_isobaric(pressure: f64, delta_v: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::work_isobaric(pressure, delta_v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Work done during adiabatic process: W = (P1*V1 - P2*V2) / (γ - 1) +/// +/// Rust: `thermodynamics::work_adiabatic` +#[pyfunction] +#[pyo3(name = "work_adiabatic", signature = (p1, v1, p2, v2, gamma))] +pub fn pyfn_work_adiabatic(p1: f64, v1: f64, p2: f64, v2: f64, gamma: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::work_adiabatic(p1, v1, p2, v2, gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Adiabatic relation: P1 * V1^γ = P2 * V2^γ → P2 = P1 * (V1/V2)^γ +/// +/// Rust: `thermodynamics::adiabatic_final_pressure` +#[pyfunction] +#[pyo3(name = "adiabatic_final_pressure", signature = (p1, v1, v2, gamma))] +pub fn pyfn_adiabatic_final_pressure(p1: f64, v1: f64, v2: f64, gamma: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::adiabatic_final_pressure(p1, v1, v2, gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Entropy change for heat transfer at constant temperature: ΔS = Q / T +/// +/// Rust: `thermodynamics::entropy_change_isothermal` +#[pyfunction] +#[pyo3(name = "entropy_change_isothermal", signature = (heat, temperature))] +pub fn pyfn_entropy_change_isothermal(heat: f64, temperature: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::entropy_change_isothermal(heat, temperature)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Entropy change for an ideal gas: ΔS = n*Cv*ln(T2/T1) + n*R*ln(V2/V1) +/// +/// Rust: `thermodynamics::entropy_change_ideal_gas` +#[pyfunction] +#[pyo3(name = "entropy_change_ideal_gas", signature = (moles, cv, t1, t2, v1, v2))] +pub fn pyfn_entropy_change_ideal_gas(moles: f64, cv: f64, t1: f64, t2: f64, v1: f64, v2: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::entropy_change_ideal_gas(moles, cv, t1, t2, v1, v2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Carnot efficiency: η = 1 - T_cold / T_hot +/// +/// Rust: `thermodynamics::carnot_efficiency` +#[pyfunction] +#[pyo3(name = "carnot_efficiency", signature = (t_cold, t_hot))] +pub fn pyfn_carnot_efficiency(t_cold: f64, t_hot: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::carnot_efficiency(t_cold, t_hot)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Thermal efficiency: η = W / Q_hot +/// +/// Rust: `thermodynamics::thermal_efficiency` +#[pyfunction] +#[pyo3(name = "thermal_efficiency", signature = (work, heat_input))] +pub fn pyfn_thermal_efficiency(work: f64, heat_input: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::thermal_efficiency(work, heat_input)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Coefficient of performance (refrigerator): COP = Q_cold / W +/// +/// Rust: `thermodynamics::cop_refrigerator` +#[pyfunction] +#[pyo3(name = "cop_refrigerator", signature = (heat_removed, work))] +pub fn pyfn_cop_refrigerator(heat_removed: f64, work: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::cop_refrigerator(heat_removed, work)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Coefficient of performance (heat pump): COP = Q_hot / W +/// +/// Rust: `thermodynamics::cop_heat_pump` +#[pyfunction] +#[pyo3(name = "cop_heat_pump", signature = (heat_delivered, work))] +pub fn pyfn_cop_heat_pump(heat_delivered: f64, work: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::cop_heat_pump(heat_delivered, work)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Heat for phase change: Q = m * L (latent heat) +/// +/// Rust: `thermodynamics::latent_heat` +#[pyfunction] +#[pyo3(name = "latent_heat", signature = (mass, specific_latent_heat))] +pub fn pyfn_latent_heat(mass: f64, specific_latent_heat: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::latent_heat(mass, specific_latent_heat)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Clausius-Clapeyron (approximate): ln(P2/P1) = (L/R) * (1/T1 - 1/T2) +/// Returns P2 given P1, T1, T2, and molar latent heat L. +/// +/// Rust: `thermodynamics::clausius_clapeyron` +#[pyfunction] +#[pyo3(name = "clausius_clapeyron", signature = (p1, t1, t2, molar_latent_heat))] +pub fn pyfn_clausius_clapeyron(p1: f64, t1: f64, t2: f64, molar_latent_heat: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::clausius_clapeyron(p1, t1, t2, molar_latent_heat)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Newton's law of convection: Q/t = h×A×ΔT +/// +/// Rust: `thermodynamics::convective_heat_rate` +#[pyfunction] +#[pyo3(name = "convective_heat_rate", signature = (h, area, delta_temp))] +pub fn pyfn_convective_heat_rate(h: f64, area: f64, delta_temp: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::convective_heat_rate(h, area, delta_temp)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Thermal diffusivity: α = k/(ρ×cₚ) +/// +/// Rust: `thermodynamics::thermal_diffusivity` +#[pyfunction] +#[pyo3(name = "thermal_diffusivity", signature = (conductivity, density, specific_heat))] +pub fn pyfn_thermal_diffusivity(conductivity: f64, density: f64, specific_heat: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::thermal_diffusivity(conductivity, density, specific_heat)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Grashof number: Gr = gβΔTL³/ν² +/// +/// Rust: `thermodynamics::grashof_number` +#[pyfunction] +#[pyo3(name = "grashof_number", signature = (g, beta, delta_temp, length, kinematic_viscosity))] +pub fn pyfn_grashof_number(g: f64, beta: f64, delta_temp: f64, length: f64, kinematic_viscosity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::grashof_number(g, beta, delta_temp, length, kinematic_viscosity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Rayleigh number: Ra = Gr × Pr +/// +/// Rust: `thermodynamics::rayleigh_number` +#[pyfunction] +#[pyo3(name = "rayleigh_number", signature = (grashof, prandtl))] +pub fn pyfn_rayleigh_number(grashof: f64, prandtl: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::rayleigh_number(grashof, prandtl)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Prandtl number: Pr = ν/α +/// +/// Rust: `thermodynamics::prandtl_number` +#[pyfunction] +#[pyo3(name = "prandtl_number", signature = (kinematic_viscosity, thermal_diffusivity))] +pub fn pyfn_prandtl_number(kinematic_viscosity: f64, thermal_diffusivity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::prandtl_number(kinematic_viscosity, thermal_diffusivity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Nusselt number: Nu = hL/k +/// +/// Rust: `thermodynamics::nusselt_number` +#[pyfunction] +#[pyo3(name = "nusselt_number", signature = (h, length, conductivity))] +pub fn pyfn_nusselt_number(h: f64, length: f64, conductivity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::nusselt_number(h, length, conductivity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Biot number: Bi = hL/k (external convection vs internal conduction) +/// +/// Rust: `thermodynamics::biot_number` +#[pyfunction] +#[pyo3(name = "biot_number", signature = (h, length, conductivity))] +pub fn pyfn_biot_number(h: f64, length: f64, conductivity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::biot_number(h, length, conductivity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Explicit finite difference: T_i^(n+1) = T_i^n + α×dt/dx² × (T_(i+1) - 2T_i + T_(i-1)) +/// Fixed boundary conditions (first and last elements unchanged). +/// +/// Rust: `thermodynamics::heat_equation_step_1d` +#[pyfunction] +#[pyo3(name = "heat_equation_step_1d", signature = (temperatures, dx, dt, diffusivity))] +pub fn pyfn_heat_equation_step_1d<'py>(temperatures: pyo3::Bound<'py, pyo3::PyAny>, dx: f64, dt: f64, diffusivity: f64) -> PyResult<()> { + let mut temperatures__v: Vec = temperatures.extract()?; + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::heat_equation_step_1d(&mut temperatures__v, dx, dt, diffusivity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + crate::runtime::coerce::write_back(&temperatures, &temperatures__v)?; + Ok(()) +} + +/// Maximum stable time step for explicit finite difference: dt_max = dx²/(2α) +/// +/// Rust: `thermodynamics::heat_equation_stability` +#[pyfunction] +#[pyo3(name = "heat_equation_stability", signature = (dx, diffusivity))] +pub fn pyfn_heat_equation_stability(dx: f64, diffusivity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::heat_equation_stability(dx, diffusivity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Wien's displacement law: λ_max = b/T where b = 2.898e-3 m·K +/// +/// Rust: `thermodynamics::wien_displacement` +#[pyfunction] +#[pyo3(name = "wien_displacement", signature = (temperature))] +pub fn pyfn_wien_displacement(temperature: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::wien_displacement(temperature)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Planck's law: M = (2πhc²/λ⁵) × 1/(e^(hc/λkT) - 1) +/// +/// Rust: `thermodynamics::spectral_exitance` +#[pyfunction] +#[pyo3(name = "spectral_exitance", signature = (wavelength, temperature))] +pub fn pyfn_spectral_exitance(wavelength: f64, temperature: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::spectral_exitance(wavelength, temperature)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Radiative equilibrium temperature: T = ((L(1-a))/(16πσd²))^(1/4) +/// +/// Rust: `thermodynamics::radiative_equilibrium_temperature` +#[pyfunction] +#[pyo3(name = "radiative_equilibrium_temperature", signature = (luminosity, distance, albedo))] +pub fn pyfn_radiative_equilibrium_temperature(luminosity: f64, distance: f64, albedo: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::radiative_equilibrium_temperature(luminosity, distance, albedo)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Celsius to Kelvin: K = C + 273.15 +/// +/// Rust: `thermodynamics::celsius_to_kelvin` +#[pyfunction] +#[pyo3(name = "celsius_to_kelvin", signature = (c))] +pub fn pyfn_celsius_to_kelvin(c: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::celsius_to_kelvin(c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Kelvin to Celsius: C = K - 273.15 +/// +/// Rust: `thermodynamics::kelvin_to_celsius` +#[pyfunction] +#[pyo3(name = "kelvin_to_celsius", signature = (k))] +pub fn pyfn_kelvin_to_celsius(k: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::kelvin_to_celsius(k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Celsius to Fahrenheit: F = C × 9/5 + 32 +/// +/// Rust: `thermodynamics::celsius_to_fahrenheit` +#[pyfunction] +#[pyo3(name = "celsius_to_fahrenheit", signature = (c))] +pub fn pyfn_celsius_to_fahrenheit(c: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::celsius_to_fahrenheit(c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Fahrenheit to Celsius: C = (F - 32) × 5/9 +/// +/// Rust: `thermodynamics::fahrenheit_to_celsius` +#[pyfunction] +#[pyo3(name = "fahrenheit_to_celsius", signature = (f))] +pub fn pyfn_fahrenheit_to_celsius(f: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::fahrenheit_to_celsius(f)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Fahrenheit to Kelvin via Celsius +/// +/// Rust: `thermodynamics::fahrenheit_to_kelvin` +#[pyfunction] +#[pyo3(name = "fahrenheit_to_kelvin", signature = (f))] +pub fn pyfn_fahrenheit_to_kelvin(f: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::fahrenheit_to_kelvin(f)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Kelvin to Fahrenheit via Celsius +/// +/// Rust: `thermodynamics::kelvin_to_fahrenheit` +#[pyfunction] +#[pyo3(name = "kelvin_to_fahrenheit", signature = (k))] +pub fn pyfn_kelvin_to_fahrenheit(k: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::kelvin_to_fahrenheit(k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Celsius to Rankine: R = (C + 273.15) × 9/5 +/// +/// Rust: `thermodynamics::celsius_to_rankine` +#[pyfunction] +#[pyo3(name = "celsius_to_rankine", signature = (c))] +pub fn pyfn_celsius_to_rankine(c: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::celsius_to_rankine(c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Rankine to Celsius: C = R × 5/9 - 273.15 +/// +/// Rust: `thermodynamics::rankine_to_celsius` +#[pyfunction] +#[pyo3(name = "rankine_to_celsius", signature = (r))] +pub fn pyfn_rankine_to_celsius(r: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::rankine_to_celsius(r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Boiling point elevation: ΔTb = Kb × m +/// +/// Rust: `thermodynamics::boiling_point_elevation` +#[pyfunction] +#[pyo3(name = "boiling_point_elevation", signature = (kb, molality))] +pub fn pyfn_boiling_point_elevation(kb: f64, molality: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::boiling_point_elevation(kb, molality)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Freezing point depression: ΔTf = Kf × m +/// +/// Rust: `thermodynamics::freezing_point_depression` +#[pyfunction] +#[pyo3(name = "freezing_point_depression", signature = (kf, molality))] +pub fn pyfn_freezing_point_depression(kf: f64, molality: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::freezing_point_depression(kf, molality)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Antoine equation: log10(P) = A - B/(C+T), returns P +/// +/// Rust: `thermodynamics::saturation_pressure` +#[pyfunction] +#[pyo3(name = "saturation_pressure", signature = (t, a, b, c))] +pub fn pyfn_saturation_pressure(t: f64, a: f64, b: f64, c: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::saturation_pressure(t, a, b, c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Trouton's rule: ΔHvap ≈ 88 × Tb (J/mol) +/// +/// Rust: `thermodynamics::heat_of_vaporization_trouton` +#[pyfunction] +#[pyo3(name = "heat_of_vaporization_trouton", signature = (boiling_point_k))] +pub fn pyfn_heat_of_vaporization_trouton(boiling_point_k: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::heat_of_vaporization_trouton(boiling_point_k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Degree of superheat: ΔT = T_actual - T_sat +/// +/// Rust: `thermodynamics::superheat_degree` +#[pyfunction] +#[pyo3(name = "superheat_degree", signature = (actual_temp, saturation_temp))] +pub fn pyfn_superheat_degree(actual_temp: f64, saturation_temp: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::superheat_degree(actual_temp, saturation_temp)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Degree of subcooling: ΔT = T_sat - T_actual +/// +/// Rust: `thermodynamics::subcool_degree` +#[pyfunction] +#[pyo3(name = "subcool_degree", signature = (saturation_temp, actual_temp))] +pub fn pyfn_subcool_degree(saturation_temp: f64, actual_temp: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::subcool_degree(saturation_temp, actual_temp)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Steam quality (dryness fraction): x = m_vapor / m_total +/// +/// Rust: `thermodynamics::quality` +#[pyfunction] +#[pyo3(name = "quality", signature = (mass_vapor, mass_total))] +pub fn pyfn_quality(mass_vapor: f64, mass_total: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::quality(mass_vapor, mass_total)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Specific enthalpy of wet steam: h = hf + x × hfg +/// +/// Rust: `thermodynamics::specific_enthalpy_wet` +#[pyfunction] +#[pyo3(name = "specific_enthalpy_wet", signature = (hf, hfg, quality))] +pub fn pyfn_specific_enthalpy_wet(hf: f64, hfg: f64, quality: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::thermodynamics::specific_enthalpy_wet(hf, hfg, quality)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_ideal_gas_pressure, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ideal_gas_volume, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ideal_gas_temperature, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ideal_gas_moles, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_average_kinetic_energy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rms_speed, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mean_free_path, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_heat_transfer, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_heat_conduction_rate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_heat_radiation_power, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_net_radiation_power, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_newton_cooling, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_work_isothermal, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_work_isobaric, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_work_adiabatic, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_adiabatic_final_pressure, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_entropy_change_isothermal, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_entropy_change_ideal_gas, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_carnot_efficiency, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_thermal_efficiency, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cop_refrigerator, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cop_heat_pump, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_latent_heat, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_clausius_clapeyron, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_convective_heat_rate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_thermal_diffusivity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_grashof_number, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rayleigh_number, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_prandtl_number, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_nusselt_number, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_biot_number, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_heat_equation_step_1d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_heat_equation_stability, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wien_displacement, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spectral_exitance, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_radiative_equilibrium_temperature, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_celsius_to_kelvin, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kelvin_to_celsius, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_celsius_to_fahrenheit, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fahrenheit_to_celsius, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fahrenheit_to_kelvin, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kelvin_to_fahrenheit, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_celsius_to_rankine, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rankine_to_celsius, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_boiling_point_elevation, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_freezing_point_depression, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_saturation_pressure, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_heat_of_vaporization_trouton, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_superheat_degree, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_subcool_degree, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_quality, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_specific_enthalpy_wet, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_transforms.rs b/bindings/python/src/generated/m_transforms.rs new file mode 100644 index 0000000..c02bbf8 --- /dev/null +++ b/bindings/python/src/generated/m_transforms.rs @@ -0,0 +1,22 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + Ok(()) +} diff --git a/bindings/python/src/generated/m_transforms__dct.rs b/bindings/python/src/generated/m_transforms__dct.rs new file mode 100644 index 0000000..3df596b --- /dev/null +++ b/bindings/python/src/generated/m_transforms__dct.rs @@ -0,0 +1,183 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// DCT-I of length N ≥ 2 (even symmetry about both endpoints). +/// +/// Panics: +/// Panics if `x.len() < 2`. +/// +/// Rust: `transforms::dct::dct_i` +#[pyfunction] +#[pyo3(name = "dct_i", signature = (x))] +pub fn pyfn_dct_i<'py>(py: Python<'py>, x: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::dct::dct_i(&x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// DCT-II (the "standard" DCT). +/// +/// Rust: `transforms::dct::dct_ii` +#[pyfunction] +#[pyo3(name = "dct_ii", signature = (x))] +pub fn pyfn_dct_ii<'py>(py: Python<'py>, x: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::dct::dct_ii(&x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// DCT-III (the unnormalized inverse of DCT-II). +/// +/// Rust: `transforms::dct::dct_iii` +#[pyfunction] +#[pyo3(name = "dct_iii", signature = (x))] +pub fn pyfn_dct_iii<'py>(py: Python<'py>, x: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::dct::dct_iii(&x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// DCT-IV (its own inverse up to a factor 2N). +/// +/// Rust: `transforms::dct::dct_iv` +#[pyfunction] +#[pyo3(name = "dct_iv", signature = (x))] +pub fn pyfn_dct_iv<'py>(py: Python<'py>, x: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::dct::dct_iv(&x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Inverse of `dct_ii`: x = dct_iii(y) / (2N). +/// +/// Rust: `transforms::dct::idct_ii` +#[pyfunction] +#[pyo3(name = "idct_ii", signature = (x))] +pub fn pyfn_idct_ii<'py>(py: Python<'py>, x: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::dct::idct_ii(&x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// DST-I (odd symmetry about both virtual endpoints); its own inverse up +/// to a factor 2(N+1). +/// +/// Rust: `transforms::dct::dst_i` +#[pyfunction] +#[pyo3(name = "dst_i", signature = (x))] +pub fn pyfn_dst_i<'py>(py: Python<'py>, x: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::dct::dst_i(&x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// DST-II, via the identity DST-II(x)\[k\] = DCT-II(x·(−1)^n)\[N−1−k\]. +/// +/// Rust: `transforms::dct::dst_ii` +#[pyfunction] +#[pyo3(name = "dst_ii", signature = (x))] +pub fn pyfn_dst_ii<'py>(py: Python<'py>, x: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::dct::dst_ii(&x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Separable 2D DCT-II of row-major data (index = y·w + x). +/// +/// Panics: +/// Panics unless `x.len() == w * h`. +/// +/// Rust: `transforms::dct::dct_2d` +#[pyfunction] +#[pyo3(name = "dct_2d", signature = (x, w, h))] +pub fn pyfn_dct_2d<'py>(py: Python<'py>, x: Vec, w: usize, h: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::dct::dct_2d(&x, w, h))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Inverse of `dct_2d`. +/// +/// Panics: +/// Panics unless `x.len() == w * h`. +/// +/// Rust: `transforms::dct::idct_2d` +#[pyfunction] +#[pyo3(name = "idct_2d", signature = (x, w, h))] +pub fn pyfn_idct_2d<'py>(py: Python<'py>, x: Vec, w: usize, h: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::dct::idct_2d(&x, w, h))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Discrete Hartley transform: H\[k\] = Σ x\[n\]·cas(2πkn/N) with +/// cas θ = cos θ + sin θ. Self-inverse up to a factor N. +/// +/// Rust: `transforms::dct::hartley` +#[pyfunction] +#[pyo3(name = "hartley", signature = (x))] +pub fn pyfn_hartley<'py>(py: Python<'py>, x: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::dct::hartley(&x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Lossy compression demo: keep the largest `keep_fraction` of DCT-II +/// coefficients (by magnitude), zero the rest, and reconstruct. +/// +/// Rust: `transforms::dct::dct_compress` +#[pyfunction] +#[pyo3(name = "dct_compress", signature = (x, keep_fraction))] +pub fn pyfn_dct_compress<'py>(py: Python<'py>, x: Vec, keep_fraction: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::dct::dct_compress(&x, keep_fraction))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Solve the 1D Poisson problem u'' = rhs on a uniform grid with +/// homogeneous boundary conditions, diagonalizing the discrete +/// three-point Laplacian with the DST-I (Dirichlet) or DCT-II (Neumann). +/// The discrete residual is at roundoff. +/// +/// Rust: `transforms::dct::dct_poisson_1d` +#[pyfunction] +#[pyo3(name = "dct_poisson_1d", signature = (rhs, dx, bc))] +pub fn pyfn_dct_poisson_1d<'py>(py: Python<'py>, rhs: Vec, dx: f64, bc: crate::generated::types::PyDctBc) -> PyResult> { + let bc = bc.to_rust(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::dct::dct_poisson_1d(&rhs, dx, bc))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_dct_i, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dct_ii, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dct_iii, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dct_iv, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_idct_ii, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dst_i, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dst_ii, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dct_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_idct_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hartley, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dct_compress, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dct_poisson_1d, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_transforms__fft.rs b/bindings/python/src/generated/m_transforms__fft.rs new file mode 100644 index 0000000..aaf1966 --- /dev/null +++ b/bindings/python/src/generated/m_transforms__fft.rs @@ -0,0 +1,338 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Smallest power of two ≥ n (returns 1 for n = 0). +/// +/// Rust: `transforms::fft::next_power_of_two` +#[pyfunction] +#[pyo3(name = "next_power_of_two", signature = (n))] +pub fn pyfn_next_power_of_two(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::transforms::fft::next_power_of_two(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Forward FFT: `X[k] = Σ x[n]·e^(−j2πkn/N)`. +/// +/// Panics: +/// Panics unless `input.len()` is a power of two. Use `fft_any` for +/// arbitrary lengths. +/// +/// Rust: `transforms::fft::fft` +#[pyfunction] +#[pyo3(name = "fft", signature = (input))] +pub fn pyfn_fft<'py>(py: Python<'py>, input: Vec) -> PyResult>> { + let input = input.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::transforms::fft::fft(&input)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// Inverse FFT: `x[n] = (1/N)·Σ X[k]·e^(j2πkn/N)`. +/// +/// Panics: +/// Panics unless `input.len()` is a power of two. Use `ifft_any` for +/// arbitrary lengths. +/// +/// Rust: `transforms::fft::ifft` +#[pyfunction] +#[pyo3(name = "ifft", signature = (input))] +pub fn pyfn_ifft<'py>(py: Python<'py>, input: Vec) -> PyResult>> { + let input = input.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::transforms::fft::ifft(&input)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// Forward DFT of any length: mixed radix 2/3/5 with a Bluestein +/// fallback for lengths containing other prime factors. O(n log n). +/// +/// Rust: `transforms::fft::fft_any` +#[pyfunction] +#[pyo3(name = "fft_any", signature = (x))] +pub fn pyfn_fft_any<'py>(py: Python<'py>, x: Vec) -> PyResult>> { + let x = x.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::transforms::fft::fft_any(&x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// Inverse DFT of any length (includes the 1/n scaling). +/// +/// Rust: `transforms::fft::ifft_any` +#[pyfunction] +#[pyo3(name = "ifft_any", signature = (x))] +pub fn pyfn_ifft_any<'py>(py: Python<'py>, x: Vec) -> PyResult>> { + let x = x.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::transforms::fft::ifft_any(&x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// FFT of a real signal, returning the n/2 + 1 non-redundant bins +/// (bins `k > n/2` satisfy `X[n−k] = X[k]*`). Any length. +/// +/// Rust: `transforms::fft::rfft` +#[pyfunction] +#[pyo3(name = "rfft", signature = (input))] +pub fn pyfn_rfft<'py>(py: Python<'py>, input: Vec) -> PyResult>> { + let __r = crate::runtime::guard(|| rust_physics_engine::transforms::fft::rfft(&input)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// Inverse of `rfft`: rebuilds the full conjugate-symmetric spectrum +/// and returns the length-n real signal. +/// +/// Panics: +/// Panics unless `x.len() == n / 2 + 1`. +/// +/// Rust: `transforms::fft::irfft` +#[pyfunction] +#[pyo3(name = "irfft", signature = (x, n))] +pub fn pyfn_irfft<'py>(py: Python<'py>, x: Vec, n: usize) -> PyResult> { + let x = x.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::fft::irfft(&x, n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// 2D FFT of row-major data (index = y·w + x): transform rows, then columns. +/// +/// Panics: +/// Panics unless `x.len() == w * h`. +/// +/// Rust: `transforms::fft::fft_2d` +#[pyfunction] +#[pyo3(name = "fft_2d", signature = (x, w, h))] +pub fn pyfn_fft_2d<'py>(py: Python<'py>, x: Vec, w: usize, h: usize) -> PyResult>> { + let x = x.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::transforms::fft::fft_2d(&x, w, h)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// Inverse 2D FFT (includes the 1/(w·h) scaling). +/// +/// Panics: +/// Panics unless `x.len() == w * h`. +/// +/// Rust: `transforms::fft::ifft_2d` +#[pyfunction] +#[pyo3(name = "ifft_2d", signature = (x, w, h))] +pub fn pyfn_ifft_2d<'py>(py: Python<'py>, x: Vec, w: usize, h: usize) -> PyResult>> { + let x = x.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::transforms::fft::ifft_2d(&x, w, h)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// 3D FFT of data indexed as `(z·ny + y)·nx + x`. +/// +/// Panics: +/// Panics unless `x.len() == nx * ny * nz`. +/// +/// Rust: `transforms::fft::fft_3d` +#[pyfunction] +#[pyo3(name = "fft_3d", signature = (x, nx, ny, nz))] +pub fn pyfn_fft_3d<'py>(py: Python<'py>, x: Vec, nx: usize, ny: usize, nz: usize) -> PyResult>> { + let x = x.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::transforms::fft::fft_3d(&x, nx, ny, nz)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// Inverse 3D FFT (includes the 1/(nx·ny·nz) scaling). +/// +/// Panics: +/// Panics unless `x.len() == nx * ny * nz`. +/// +/// Rust: `transforms::fft::ifft_3d` +#[pyfunction] +#[pyo3(name = "ifft_3d", signature = (x, nx, ny, nz))] +pub fn pyfn_ifft_3d<'py>(py: Python<'py>, x: Vec, nx: usize, ny: usize, nz: usize) -> PyResult>> { + let x = x.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::transforms::fft::ifft_3d(&x, nx, ny, nz)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// 2D FFT of real row-major data, keeping only the non-redundant half +/// along x: output is row-major with width `w/2 + 1` and height `h` +/// (full transform along y). +/// +/// Panics: +/// Panics unless `x.len() == w * h`. +/// +/// Rust: `transforms::fft::rfft_2d` +#[pyfunction] +#[pyo3(name = "rfft_2d", signature = (x, w, h))] +pub fn pyfn_rfft_2d<'py>(py: Python<'py>, x: Vec, w: usize, h: usize) -> PyResult>> { + let __r = crate::runtime::guard(|| rust_physics_engine::transforms::fft::rfft_2d(&x, w, h)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// Swap spectrum halves in place so the zero-frequency bin moves to the +/// center (numpy `fftshift`; for odd n the extra bin lands left of center). +/// +/// Rust: `transforms::fft::fft_shift` +#[pyfunction] +#[pyo3(name = "fft_shift", signature = (x))] +pub fn pyfn_fft_shift<'py>(x: pyo3::Bound<'py, pyo3::PyAny>) -> PyResult<()> { + let mut x__v: Vec = x.extract::>()?.into_iter().map(|__e| __e.0).collect(); + let __r = crate::runtime::guard(|| rust_physics_engine::transforms::fft::fft_shift(&mut x__v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + crate::runtime::coerce::write_back_objects(&x, x__v.into_iter().map(|__e| crate::runtime::coerce::Cx(__e)).collect::>())?; + Ok(()) +} + +/// Frequencies (Hz) of the DFT bins for sample spacing `dt`, in FFT +/// order: 0, 1/(n·dt), …, then the negative frequencies. +/// +/// Rust: `transforms::fft::fft_freqs` +#[pyfunction] +#[pyo3(name = "fft_freqs", signature = (n, dt))] +pub fn pyfn_fft_freqs<'py>(py: Python<'py>, n: usize, dt: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::fft::fft_freqs(n, dt))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Circular (periodic) 2D convolution of two w×h real fields via FFT. +/// +/// Panics: +/// Panics unless both inputs have `w * h` samples. +/// +/// Rust: `transforms::fft::fft_convolve_2d` +#[pyfunction] +#[pyo3(name = "fft_convolve_2d", signature = (a, b, w, h))] +pub fn pyfn_fft_convolve_2d<'py>(py: Python<'py>, a: Vec, b: Vec, w: usize, h: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::fft::fft_convolve_2d(&a, &b, w, h))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Linear convolution of two real signals via zero-padded FFT. +/// Matches `signal_processing::convolve` (output length a + b − 1). +/// +/// Rust: `transforms::fft::fft_convolve` +#[pyfunction] +#[pyo3(name = "fft_convolve", signature = (a, b))] +pub fn pyfn_fft_convolve<'py>(py: Python<'py>, a: Vec, b: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::fft::fft_convolve(&a, &b))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Cross-correlation via FFT; matches +/// `signal_processing::cross_correlate` (length a + b − 1). +/// +/// Rust: `transforms::fft::fft_correlate` +#[pyfunction] +#[pyo3(name = "fft_correlate", signature = (a, b))] +pub fn pyfn_fft_correlate<'py>(py: Python<'py>, a: Vec, b: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::fft::fft_correlate(&a, &b))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Band-limited (sinc) interpolation by an integer factor: zero-pad the +/// spectrum and inverse transform at length n·factor. +/// +/// Panics: +/// Panics if `factor == 0`. +/// +/// Rust: `transforms::fft::fft_interpolate` +#[pyfunction] +#[pyo3(name = "fft_interpolate", signature = (x, factor))] +pub fn pyfn_fft_interpolate<'py>(py: Python<'py>, x: Vec, factor: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::fft::fft_interpolate(&x, factor))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Spectral derivative of a periodic signal sampled at spacing `dt`: +/// multiply each bin by jω and transform back (Nyquist bin zeroed). +/// +/// Rust: `transforms::fft::fft_differentiate` +#[pyfunction] +#[pyo3(name = "fft_differentiate", signature = (x, dt))] +pub fn pyfn_fft_differentiate<'py>(py: Python<'py>, x: Vec, dt: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::fft::fft_differentiate(&x, dt))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Spectral antiderivative of a periodic signal: divide each nonzero bin +/// by jω; the DC bin is zeroed, so the result is the zero-mean periodic +/// antiderivative of the mean-removed input. +/// +/// Rust: `transforms::fft::fft_integrate` +#[pyfunction] +#[pyo3(name = "fft_integrate", signature = (x, dt))] +pub fn pyfn_fft_integrate<'py>(py: Python<'py>, x: Vec, dt: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::fft::fft_integrate(&x, dt))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Solve the periodic Poisson problem ∇²u = rhs on a w×h grid with +/// spacing `dx`, using the eigenvalues of the discrete 5-point Laplacian +/// so the discrete residual is at roundoff. The k=0 mode is set to zero +/// (the mean-free solution; a pure-Neumann/periodic problem only +/// determines u up to a constant, and requires a mean-free rhs). +/// +/// Panics: +/// Panics unless `rhs.len() == w * h`. +/// +/// Rust: `transforms::fft::fft_poisson_2d` +#[pyfunction] +#[pyo3(name = "fft_poisson_2d", signature = (rhs, w, h, dx))] +pub fn pyfn_fft_poisson_2d<'py>(py: Python<'py>, rhs: Vec, w: usize, h: usize, dx: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::fft::fft_poisson_2d(&rhs, w, h, dx))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_next_power_of_two, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fft, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ifft, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fft_any, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ifft_any, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rfft, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_irfft, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fft_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ifft_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fft_3d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ifft_3d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rfft_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fft_shift, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fft_freqs, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fft_convolve_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fft_convolve, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fft_correlate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fft_interpolate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fft_differentiate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fft_integrate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fft_poisson_2d, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_transforms__hilbert.rs b/bindings/python/src/generated/m_transforms__hilbert.rs new file mode 100644 index 0000000..163770e --- /dev/null +++ b/bindings/python/src/generated/m_transforms__hilbert.rs @@ -0,0 +1,195 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Analytic signal x + j·H(x) by the FFT method: double the positive +/// frequencies, zero the negative ones. +/// +/// Rust: `transforms::hilbert::analytic_signal` +#[pyfunction] +#[pyo3(name = "analytic_signal", signature = (x))] +pub fn pyfn_analytic_signal<'py>(py: Python<'py>, x: Vec) -> PyResult>> { + let __r = crate::runtime::guard(|| rust_physics_engine::transforms::hilbert::analytic_signal(&x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// Hilbert transform: the quadrature (imaginary) part of the analytic +/// signal — hilbert(cos ωt) = sin ωt. +/// +/// Rust: `transforms::hilbert::hilbert` +#[pyfunction] +#[pyo3(name = "hilbert", signature = (x))] +pub fn pyfn_hilbert<'py>(py: Python<'py>, x: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::hilbert::hilbert(&x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Instantaneous amplitude |x + jH(x)|. +/// +/// Rust: `transforms::hilbert::envelope` +#[pyfunction] +#[pyo3(name = "envelope", signature = (x))] +pub fn pyfn_envelope<'py>(py: Python<'py>, x: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::hilbert::envelope(&x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Unwrapped instantaneous phase of the analytic signal. +/// +/// Rust: `transforms::hilbert::instantaneous_phase` +#[pyfunction] +#[pyo3(name = "instantaneous_phase", signature = (x))] +pub fn pyfn_instantaneous_phase<'py>(py: Python<'py>, x: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::hilbert::instantaneous_phase(&x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Instantaneous frequency in Hz (central difference of the unwrapped +/// phase). +/// +/// Rust: `transforms::hilbert::instantaneous_frequency` +#[pyfunction] +#[pyo3(name = "instantaneous_frequency", signature = (x, fs))] +pub fn pyfn_instantaneous_frequency<'py>(py: Python<'py>, x: Vec, fs: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::hilbert::instantaneous_frequency(&x, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// FIR Hilbert transformer kernel (odd taps, antisymmetric, windowed). +/// +/// Panics: +/// Panics unless `n_taps` is odd. +/// +/// Rust: `transforms::hilbert::hilbert_fir` +#[pyfunction] +#[pyo3(name = "hilbert_fir", signature = (n_taps))] +pub fn pyfn_hilbert_fir<'py>(py: Python<'py>, n_taps: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::hilbert::hilbert_fir(n_taps))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Single-sideband modulation: upper sideband is x·cos − H(x)·sin, +/// lower is x·cos + H(x)·sin (carrier fc Hz at sample rate fs). +/// +/// Rust: `transforms::hilbert::ssb_modulate` +#[pyfunction] +#[pyo3(name = "ssb_modulate", signature = (x, fc, fs, upper))] +pub fn pyfn_ssb_modulate<'py>(py: Python<'py>, x: Vec, fc: f64, fs: f64, upper: bool) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::hilbert::ssb_modulate(&x, fc, fs, upper))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// AM envelope demodulation: the analytic-signal envelope (carrier plus +/// modulation; subtract the mean to recover the AC message). +/// +/// Rust: `transforms::hilbert::am_demodulate` +#[pyfunction] +#[pyo3(name = "am_demodulate", signature = (x))] +pub fn pyfn_am_demodulate<'py>(py: Python<'py>, x: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::hilbert::am_demodulate(&x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// FM demodulation: instantaneous frequency of the analytic signal (Hz). +/// +/// Rust: `transforms::hilbert::fm_demodulate` +#[pyfunction] +#[pyo3(name = "fm_demodulate", signature = (x, fs))] +pub fn pyfn_fm_demodulate<'py>(py: Python<'py>, x: Vec, fs: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::hilbert::fm_demodulate(&x, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Empirical mode decomposition (Huang sifting): returns the IMFs plus +/// the final residual as the last entry, so the components sum to x. +/// +/// Rust: `transforms::hilbert::empirical_mode_decomposition` +#[pyfunction] +#[pyo3(name = "empirical_mode_decomposition", signature = (x, max_imfs, sift_tol))] +pub fn pyfn_empirical_mode_decomposition<'py>(py: Python<'py>, x: Vec, max_imfs: usize, sift_tol: f64) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::hilbert::empirical_mode_decomposition(&x, max_imfs, sift_tol))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Hilbert-Huang spectrum: per IMF (excluding the residual), the +/// instantaneous frequency track and amplitude envelope. +/// +/// Rust: `transforms::hilbert::hilbert_huang_spectrum` +#[pyfunction] +#[pyo3(name = "hilbert_huang_spectrum", signature = (x, fs, max_imfs))] +pub fn pyfn_hilbert_huang_spectrum<'py>(py: Python<'py>, x: Vec, fs: f64, max_imfs: usize) -> PyResult, Vec)>> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::hilbert::hilbert_huang_spectrum(&x, fs, max_imfs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Kramers-Kronig relation: real part of a causal response from its +/// imaginary part sampled on `omega` (ω ≥ 0), by principal-value +/// trapezoid integration of (2/π)∫ ω′·Im(ω′)/(ω′² − ω²) dω′. +/// +/// Panics: +/// Panics if the lengths differ. +/// +/// Rust: `transforms::hilbert::kramers_kronig` +#[pyfunction] +#[pyo3(name = "kramers_kronig", signature = (im, omega))] +pub fn pyfn_kramers_kronig<'py>(py: Python<'py>, im: Vec, omega: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::hilbert::kramers_kronig(&im, &omega))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Minimum-phase spectrum with the given magnitude, by the real-cepstrum +/// method: fold the causal cepstrum and re-exponentiate. `mag` samples +/// |H| on the full FFT circle (length n). +/// +/// Rust: `transforms::hilbert::minimum_phase_from_magnitude` +#[pyfunction] +#[pyo3(name = "minimum_phase_from_magnitude", signature = (mag))] +pub fn pyfn_minimum_phase_from_magnitude<'py>(py: Python<'py>, mag: Vec) -> PyResult>> { + let __r = crate::runtime::guard(|| rust_physics_engine::transforms::hilbert::minimum_phase_from_magnitude(&mag)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_analytic_signal, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hilbert, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_envelope, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_instantaneous_phase, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_instantaneous_frequency, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hilbert_fir, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ssb_modulate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_am_demodulate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fm_demodulate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_empirical_mode_decomposition, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hilbert_huang_spectrum, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kramers_kronig, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_minimum_phase_from_magnitude, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_transforms__laplace.rs b/bindings/python/src/generated/m_transforms__laplace.rs new file mode 100644 index 0000000..e9609b7 --- /dev/null +++ b/bindings/python/src/generated/m_transforms__laplace.rs @@ -0,0 +1,156 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Fixed-Talbot inverse Laplace transform (Abate & Valkó 2004) with m +/// contour nodes: f(t) from F(s) for t > 0. +/// +/// Panics: +/// Panics if `t <= 0` or `m < 2`. +/// +/// Rust: `transforms::laplace::inverse_laplace_talbot` +#[pyfunction] +#[pyo3(name = "inverse_laplace_talbot", signature = (f, t, m))] +pub fn pyfn_inverse_laplace_talbot(f: pyo3::Py, t: f64, m: usize) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: rust_physics_engine::fractals::Complex| -> rust_physics_engine::fractals::Complex { { let __r = __cb.call::<_, crate::runtime::coerce::ComplexArg>((crate::runtime::coerce::Cx(__a0),), crate::runtime::coerce::ComplexArg(rust_physics_engine::fractals::Complex::new(f64::NAN, f64::NAN))); __r.0 } } }; + let __r = crate::runtime::guard(|| rust_physics_engine::transforms::laplace::inverse_laplace_talbot(&f, t, m)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Gaver-Stehfest inverse Laplace transform: needs only real F(s) +/// evaluations. `n` must be even (12-16 is typical; larger n needs more +/// precision than f64 can give). +/// +/// Panics: +/// Panics if `t <= 0`, n is odd, or n > 18. +/// +/// Rust: `transforms::laplace::inverse_laplace_stehfest` +#[pyfunction] +#[pyo3(name = "inverse_laplace_stehfest", signature = (f, t, n))] +pub fn pyfn_inverse_laplace_stehfest(f: pyo3::Py, t: f64, n: usize) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::transforms::laplace::inverse_laplace_stehfest(&f, t, n)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Forward Laplace transform F(s) = ∫₀^tmax f(t)e^(−st) dt by composite +/// Simpson quadrature (n panels, n rounded up to even). +/// +/// Rust: `transforms::laplace::laplace_numeric` +#[pyfunction] +#[pyo3(name = "laplace_numeric", signature = (f, s, t_max, n))] +pub fn pyfn_laplace_numeric(f: pyo3::Py, s: f64, t_max: f64, n: usize) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::transforms::laplace::laplace_numeric(&f, s, t_max, n)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Evaluate the (one-sided) z-transform X(z) = Σ x\[n\]·z^(−n). +/// +/// Rust: `transforms::laplace::z_transform_eval` +#[pyfunction] +#[pyo3(name = "z_transform_eval", signature = (x, z))] +pub fn pyfn_z_transform_eval<'py>(py: Python<'py>, x: Vec, z: crate::runtime::coerce::ComplexArg) -> PyResult> { + let z = z.0; + let __r = crate::runtime::guard(|| rust_physics_engine::transforms::laplace::z_transform_eval(&x, z)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::complex_out(py, __v)) +} + +/// Impulse response of the digital transfer function +/// H(z) = (num\[0\] + num\[1\]z⁻¹ + …)/(den\[0\] + den\[1\]z⁻¹ + …), +/// by running the difference equation for n samples. +/// +/// Panics: +/// Panics if `den` is empty or `den[0] == 0`. +/// +/// Rust: `transforms::laplace::impulse_response_from_tf` +#[pyfunction] +#[pyo3(name = "impulse_response_from_tf", signature = (num, den, n))] +pub fn pyfn_impulse_response_from_tf<'py>(py: Python<'py>, num: Vec, den: Vec, n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::laplace::impulse_response_from_tf(&num, &den, n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Continuous-time frequency response H(jω) of num(s)/den(s) +/// (highest-power-first coefficients) at each ω. +/// +/// Rust: `transforms::laplace::s_domain_freq_response` +#[pyfunction] +#[pyo3(name = "s_domain_freq_response", signature = (num, den, omega))] +pub fn pyfn_s_domain_freq_response<'py>(py: Python<'py>, num: Vec, den: Vec, omega: Vec) -> PyResult>> { + let __r = crate::runtime::guard(|| rust_physics_engine::transforms::laplace::s_domain_freq_response(&num, &den, &omega)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// Digital frequency response of b(z⁻¹)/a(z⁻¹) at `n_points` normalized +/// frequencies spanning [0, 0.5]; returns (frequencies, response). +/// +/// Panics: +/// Panics if `n_points < 2`. +/// +/// Rust: `transforms::laplace::digital_freq_response` +#[pyfunction] +#[pyo3(name = "digital_freq_response", signature = (b, a, n_points))] +pub fn pyfn_digital_freq_response<'py>(py: Python<'py>, b: Vec, a: Vec, n_points: usize) -> PyResult<(Vec, Vec>)> { + let __r = crate::runtime::guard(|| rust_physics_engine::transforms::laplace::digital_freq_response(&b, &a, n_points)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>())) +} + +/// Discrete fractional Fourier transform of angle `alpha` (α = π/2 is +/// the unitary DFT), via the eigendecomposition of the Candan-Kutay- +/// Ozaktas commuting matrix: F^α = Σ_k e^(−i·k·α)·u_k·(u_kᵀx). O(n²) +/// after an O(n³) eigen solve, intended for moderate n. +/// +/// Errors: +/// Returns an error if the eigen decomposition fails to converge. +/// +/// Rust: `transforms::laplace::fractional_fourier` +#[pyfunction] +#[pyo3(name = "fractional_fourier", signature = (x, alpha))] +pub fn pyfn_fractional_fourier<'py>(py: Python<'py>, x: Vec, alpha: f64) -> PyResult>> { + let x = x.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::transforms::laplace::fractional_fourier(&x, alpha)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_inverse_laplace_talbot, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_inverse_laplace_stehfest, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_laplace_numeric, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_z_transform_eval, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_impulse_response_from_tf, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_s_domain_freq_response, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_digital_freq_response, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fractional_fourier, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_transforms__radon.rs b/bindings/python/src/generated/m_transforms__radon.rs new file mode 100644 index 0000000..5582a9d --- /dev/null +++ b/bindings/python/src/generated/m_transforms__radon.rs @@ -0,0 +1,155 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Forward Radon transform: one projection (length `n_rays`) per angle +/// (radians). Ray offsets span the image diagonal; line integrals use +/// unit-pixel steps with bilinear interpolation. +/// +/// Panics: +/// Panics unless `img.len() == w * h`. +/// +/// Rust: `transforms::radon::radon` +#[pyfunction] +#[pyo3(name = "radon", signature = (img, w, h, angles, n_rays))] +pub fn pyfn_radon<'py>(py: Python<'py>, img: Vec, w: usize, h: usize, angles: Vec, n_rays: usize) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::radon::radon(&img, w, h, &angles, n_rays))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Filtered back-projection onto an `out`×`out` image (pixels outside +/// the inscribed circle are zero). `sino` is \[angle\]\[ray\] as produced +/// by `radon` on a square image of side `out`. +/// +/// Rust: `transforms::radon::inverse_radon_fbp` +#[pyfunction] +#[pyo3(name = "inverse_radon_fbp", signature = (sino, angles, out, filter))] +pub fn pyfn_inverse_radon_fbp<'py>(py: Python<'py>, sino: Vec>, angles: Vec, out: usize, filter: crate::generated::types::PyFbpFilter) -> PyResult> { + let filter = filter.to_rust(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::radon::inverse_radon_fbp(&sino, &angles, out, filter))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Simultaneous algebraic reconstruction (SART): iterate over angles, +/// forward-project the estimate, and back-project the normalized ray +/// residuals. +/// +/// Rust: `transforms::radon::inverse_radon_sart` +#[pyfunction] +#[pyo3(name = "inverse_radon_sart", signature = (sino, angles, out, iters))] +pub fn pyfn_inverse_radon_sart<'py>(py: Python<'py>, sino: Vec>, angles: Vec, out: usize, iters: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::radon::inverse_radon_sart(&sino, &angles, out, iters))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// The classic Shepp-Logan head phantom on an n×n grid (values in the +/// original low-contrast scale). +/// +/// Rust: `transforms::radon::shepp_logan_phantom` +#[pyfunction] +#[pyo3(name = "shepp_logan_phantom", signature = (n))] +pub fn pyfn_shepp_logan_phantom<'py>(py: Python<'py>, n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::radon::shepp_logan_phantom(n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Hankel transform of order `order`: ∫₀^rmax f(r)·J_ν(k·r)·r dr by +/// composite Simpson quadrature with n panels. +/// +/// Rust: `transforms::radon::hankel_transform` +#[pyfunction] +#[pyo3(name = "hankel_transform", signature = (f, k, order, r_max, n))] +pub fn pyfn_hankel_transform(f: pyo3::Py, k: f64, order: u32, r_max: f64, n: usize) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::transforms::radon::hankel_transform(&f, k, order, r_max, n)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Forward Abel transform F(y) = 2∫_y^rmax f(r)·r/√(r²−y²) dr, computed +/// singularity-free with the substitution r = √(y² + u²). +/// +/// Rust: `transforms::radon::abel_transform` +#[pyfunction] +#[pyo3(name = "abel_transform", signature = (f, y, r_max, n))] +pub fn pyfn_abel_transform(f: pyo3::Py, y: f64, r_max: f64, n: usize) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::transforms::radon::abel_transform(&f, y, r_max, n)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Inverse Abel transform of a projection sampled at y_i = i·dr: +/// f(r) = −(1/π)∫_r^R F′(y)/√(y²−r²) dy, with a central-difference F′ +/// and the same singularity-removing substitution. +/// +/// Rust: `transforms::radon::inverse_abel` +#[pyfunction] +#[pyo3(name = "inverse_abel", signature = (data, dr))] +pub fn pyfn_inverse_abel<'py>(py: Python<'py>, data: Vec, dr: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::radon::inverse_abel(&data, dr))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Hough line accumulator: votes\[θ\]\[ρ\] with θ over \[0, π) in +/// `n_theta` steps and ρ over \[−D, D\] (D = image diagonal) in `n_rho` +/// bins. +/// +/// Rust: `transforms::radon::hough_lines` +#[pyfunction] +#[pyo3(name = "hough_lines", signature = (edges, w, h, n_theta, n_rho))] +pub fn pyfn_hough_lines<'py>(py: Python<'py>, edges: Vec, w: usize, h: usize, n_theta: usize, n_rho: usize) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::radon::hough_lines(&edges, w, h, n_theta, n_rho))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Hough circle detection: returns candidate (cx, cy, r, votes) sorted +/// by votes, keeping local maxima with at least half the top vote. +/// +/// Rust: `transforms::radon::hough_circles` +#[pyfunction] +#[pyo3(name = "hough_circles", signature = (edges, w, h, r_min, r_max))] +pub fn pyfn_hough_circles<'py>(py: Python<'py>, edges: Vec, w: usize, h: usize, r_min: usize, r_max: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::radon::hough_circles(&edges, w, h, r_min, r_max))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, __x.2, __x.3)).collect::>()) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_radon, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_inverse_radon_fbp, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_inverse_radon_sart, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_shepp_logan_phantom, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hankel_transform, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_abel_transform, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_inverse_abel, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hough_lines, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_hough_circles, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_transforms__spectral.rs b/bindings/python/src/generated/m_transforms__spectral.rs new file mode 100644 index 0000000..5042399 --- /dev/null +++ b/bindings/python/src/generated/m_transforms__spectral.rs @@ -0,0 +1,281 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Windowed periodogram: (frequencies, one-sided PSD). +/// +/// Panics: +/// Panics on an empty signal. +/// +/// Rust: `transforms::spectral::periodogram` +#[pyfunction] +#[pyo3(name = "periodogram", signature = (x, fs, window_kind))] +pub fn pyfn_periodogram<'py>(py: Python<'py>, x: Vec, fs: f64, window_kind: crate::generated::types::PyWindowKind) -> PyResult<(Vec, Vec)> { + let window_kind = window_kind.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::spectral::periodogram(&x, fs, window_kind))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Welch's method: average windowed periodograms of overlapping +/// segments (detrended by segment mean removal). +/// +/// Panics: +/// Panics unless `0 < noverlap < nperseg <= x.len()`. +/// +/// Rust: `transforms::spectral::welch` +#[pyfunction] +#[pyo3(name = "welch", signature = (x, fs, nperseg, noverlap, window_kind))] +pub fn pyfn_welch<'py>(py: Python<'py>, x: Vec, fs: f64, nperseg: usize, noverlap: usize, window_kind: crate::generated::types::PyWindowKind) -> PyResult<(Vec, Vec)> { + let window_kind = window_kind.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::spectral::welch(&x, fs, nperseg, noverlap, window_kind))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Discrete prolate spheroidal (Slepian) sequences: the first k tapers +/// of length n at time-bandwidth product nw, from the tridiagonal +/// eigenproblem. Each taper has unit energy; sign convention: positive +/// mean (even tapers) / positive first lag (odd tapers). +/// +/// Panics: +/// Panics if `k == 0`, `k > n`, or the eigen solve fails. +/// +/// Rust: `transforms::spectral::dpss` +#[pyfunction] +#[pyo3(name = "dpss", signature = (n, nw, k))] +pub fn pyfn_dpss<'py>(py: Python<'py>, n: usize, nw: f64, k: usize) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::spectral::dpss(n, nw, k))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Thomson multitaper PSD estimate with k DPSS tapers. +/// +/// Rust: `transforms::spectral::multitaper` +#[pyfunction] +#[pyo3(name = "multitaper", signature = (x, fs, nw, k))] +pub fn pyfn_multitaper<'py>(py: Python<'py>, x: Vec, fs: f64, nw: f64, k: usize) -> PyResult<(Vec, Vec)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::spectral::multitaper(&x, fs, nw, k))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Burg's method AR(p) fit: returns (a, σ²) for the model +/// x\[n\] = Σ a\[k\]·x\[n−1−k\] + e\[n\] with prediction-error variance σ². +/// +/// Panics: +/// Panics unless `0 < order < x.len()`. +/// +/// Rust: `transforms::spectral::burg_ar` +#[pyfunction] +#[pyo3(name = "burg_ar", signature = (x, order))] +pub fn pyfn_burg_ar<'py>(py: Python<'py>, x: Vec, order: usize) -> PyResult<(Vec, f64)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::spectral::burg_ar(&x, order))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Yule-Walker AR(p) fit via the autocorrelation method (Levinson-style +/// dense solve): same conventions as `burg_ar`. +/// +/// Panics: +/// Panics unless `0 < order < x.len()` and the autocorrelation system is +/// nonsingular. +/// +/// Rust: `transforms::spectral::yule_walker_ar` +#[pyfunction] +#[pyo3(name = "yule_walker_ar", signature = (x, order))] +pub fn pyfn_yule_walker_ar<'py>(py: Python<'py>, x: Vec, order: usize) -> PyResult<(Vec, f64)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::spectral::yule_walker_ar(&x, order))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// One-sided PSD of an AR model (a, σ²) on n frequency points up to +/// Nyquist: σ²/(fs·|1 − Σ a\[k\] e^(−jω(k+1))|²), doubled off DC/Nyquist. +/// +/// Rust: `transforms::spectral::ar_psd` +#[pyfunction] +#[pyo3(name = "ar_psd", signature = (coeffs, sigma2, fs, n))] +pub fn pyfn_ar_psd<'py>(py: Python<'py>, coeffs: Vec, sigma2: f64, fs: f64, n: usize) -> PyResult<(Vec, Vec)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::spectral::ar_psd(&coeffs, sigma2, fs, n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// MUSIC pseudospectrum for real sinusoids: correlation matrix of +/// dimension `order`, signal subspace of dimension 2·n_sources, and the +/// noise-subspace projection evaluated at n frequencies up to Nyquist. +/// +/// Panics: +/// Panics unless `2*n_sources < order < x.len()`. +/// +/// Rust: `transforms::spectral::music` +#[pyfunction] +#[pyo3(name = "music", signature = (x, n_sources, order, fs, n))] +pub fn pyfn_music<'py>(py: Python<'py>, x: Vec, n_sources: usize, order: usize, fs: f64, n: usize) -> PyResult<(Vec, Vec)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::spectral::music(&x, n_sources, order, fs, n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Welch-averaged cross-spectral density S_xy(f) = E\[X*(f)·Y(f)\] +/// (Hann window, 50% overlap): (frequencies, complex CSD). +/// +/// Panics: +/// Panics unless both signals have at least `nperseg` samples. +/// +/// Rust: `transforms::spectral::cross_spectral_density` +#[pyfunction] +#[pyo3(name = "cross_spectral_density", signature = (x, y, fs, nperseg))] +pub fn pyfn_cross_spectral_density<'py>(py: Python<'py>, x: Vec, y: Vec, fs: f64, nperseg: usize) -> PyResult<(Vec, Vec>)> { + let __r = crate::runtime::guard(|| rust_physics_engine::transforms::spectral::cross_spectral_density(&x, &y, fs, nperseg)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>())) +} + +/// Magnitude-squared coherence |S_xy|²/(S_xx·S_yy) on the Welch grid. +/// +/// Rust: `transforms::spectral::coherence` +#[pyfunction] +#[pyo3(name = "coherence", signature = (x, y, fs, nperseg))] +pub fn pyfn_coherence<'py>(py: Python<'py>, x: Vec, y: Vec, fs: f64, nperseg: usize) -> PyResult<(Vec, Vec)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::spectral::coherence(&x, &y, fs, nperseg))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// H1 transfer-function estimate S_xy/S_xx from input to output. +/// +/// Rust: `transforms::spectral::transfer_function_estimate` +#[pyfunction] +#[pyo3(name = "transfer_function_estimate", signature = (input, output, fs, nperseg))] +pub fn pyfn_transfer_function_estimate<'py>(py: Python<'py>, input: Vec, output: Vec, fs: f64, nperseg: usize) -> PyResult<(Vec, Vec>)> { + let __r = crate::runtime::guard(|| rust_physics_engine::transforms::spectral::transfer_function_estimate(&input, &output, fs, nperseg)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>())) +} + +/// Real cepstrum: IFFT of log |X(f)| (real part). +/// +/// Rust: `transforms::spectral::cepstrum_real` +#[pyfunction] +#[pyo3(name = "cepstrum_real", signature = (x))] +pub fn pyfn_cepstrum_real<'py>(py: Python<'py>, x: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::spectral::cepstrum_real(&x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Power cepstrum: IFFT of log |X(f)|², i.e. twice the real cepstrum. +/// +/// Rust: `transforms::spectral::cepstrum_power` +#[pyfunction] +#[pyo3(name = "cepstrum_power", signature = (x))] +pub fn pyfn_cepstrum_power<'py>(py: Python<'py>, x: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::spectral::cepstrum_power(&x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Lomb-Scargle normalized periodogram for unevenly sampled data at the +/// requested frequencies (Hz). Values are in the classical normalization +/// (power / 2σ²). +/// +/// Panics: +/// Panics if `t` and `y` lengths differ or fewer than 2 samples. +/// +/// Rust: `transforms::spectral::lomb_scargle` +#[pyfunction] +#[pyo3(name = "lomb_scargle", signature = (t, y, freqs))] +pub fn pyfn_lomb_scargle<'py>(py: Python<'py>, t: Vec, y: Vec, freqs: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::spectral::lomb_scargle(&t, &y, &freqs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Spectral entropy of a PSD, normalized to \[0, 1\]. +/// +/// Rust: `transforms::spectral::spectral_entropy` +#[pyfunction] +#[pyo3(name = "spectral_entropy", signature = (psd))] +pub fn pyfn_spectral_entropy<'py>(py: Python<'py>, psd: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::spectral::spectral_entropy(&psd))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Spectral flatness (Wiener entropy): geometric over arithmetic mean. +/// +/// Rust: `transforms::spectral::spectral_flatness` +#[pyfunction] +#[pyo3(name = "spectral_flatness", signature = (psd))] +pub fn pyfn_spectral_flatness<'py>(py: Python<'py>, psd: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::spectral::spectral_flatness(&psd))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Remove a least-squares polynomial trend of the given order. +/// +/// Panics: +/// Panics if the fit system is singular (order too high for the data). +/// +/// Rust: `transforms::spectral::detrend` +#[pyfunction] +#[pyo3(name = "detrend", signature = (x, order))] +pub fn pyfn_detrend<'py>(py: Python<'py>, x: Vec, order: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::spectral::detrend(&x, order))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Fit PSD ≈ A·f^(−α) over \[f_min, f_max\] by log-log linear regression; +/// returns (α, A). +/// +/// Rust: `transforms::spectral::power_law_fit` +#[pyfunction] +#[pyo3(name = "power_law_fit", signature = (f, psd, f_min, f_max))] +pub fn pyfn_power_law_fit<'py>(py: Python<'py>, f: Vec, psd: Vec, f_min: f64, f_max: f64) -> PyResult<(f64, f64)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::spectral::power_law_fit(&f, &psd, f_min, f_max))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_periodogram, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_welch, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dpss, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_multitaper, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_burg_ar, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_yule_walker_ar, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ar_psd, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_music, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cross_spectral_density, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_coherence, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_transfer_function_estimate, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cepstrum_real, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cepstrum_power, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lomb_scargle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spectral_entropy, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spectral_flatness, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_detrend, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_power_law_fit, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_transforms__stft.rs b/bindings/python/src/generated/m_transforms__stft.rs new file mode 100644 index 0000000..64172f9 --- /dev/null +++ b/bindings/python/src/generated/m_transforms__stft.rs @@ -0,0 +1,159 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Power spectrogram: (frame times, bin frequencies, |X|² per frame). +/// +/// Rust: `transforms::stft::spectrogram` +#[pyfunction] +#[pyo3(name = "spectrogram", signature = (x, fs, n_fft, hop, window_kind))] +pub fn pyfn_spectrogram<'py>(py: Python<'py>, x: Vec, fs: f64, n_fft: usize, hop: usize, window_kind: crate::generated::types::PyWindowKind) -> PyResult<(Vec, Vec, Vec>)> { + let window_kind = window_kind.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::stft::spectrogram(&x, fs, n_fft, hop, window_kind))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// Triangular mel filterbank: `n_mels` rows of n_fft/2 + 1 weights. +/// +/// Panics: +/// Panics if the frequency range is empty or fmax exceeds Nyquist. +/// +/// Rust: `transforms::stft::mel_filterbank` +#[pyfunction] +#[pyo3(name = "mel_filterbank", signature = (n_fft, fs, n_mels, fmin, fmax))] +pub fn pyfn_mel_filterbank<'py>(py: Python<'py>, n_fft: usize, fs: f64, n_mels: usize, fmin: f64, fmax: f64) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::stft::mel_filterbank(n_fft, fs, n_mels, fmin, fmax))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Mel-scale power spectrogram: one vector of `n_mels` band energies per +/// frame (Hann window). +/// +/// Rust: `transforms::stft::mel_spectrogram` +#[pyfunction] +#[pyo3(name = "mel_spectrogram", signature = (x, fs, n_fft, hop, n_mels, fmin, fmax))] +pub fn pyfn_mel_spectrogram<'py>(py: Python<'py>, x: Vec, fs: f64, n_fft: usize, hop: usize, n_mels: usize, fmin: f64, fmax: f64) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::stft::mel_spectrogram(&x, fs, n_fft, hop, n_mels, fmin, fmax))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Goertzel single-bin DFT at an arbitrary frequency: returns the +/// (magnitude, phase) of Σ x\[n\]·e^(−jωn), ω = 2π·target/fs. +/// +/// Rust: `transforms::stft::goertzel` +#[pyfunction] +#[pyo3(name = "goertzel", signature = (x, target_freq, fs))] +pub fn pyfn_goertzel<'py>(py: Python<'py>, x: Vec, target_freq: f64, fs: f64) -> PyResult<(f64, f64)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::stft::goertzel(&x, target_freq, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Goertzel magnitudes for a set of frequencies. +/// +/// Rust: `transforms::stft::goertzel_bank` +#[pyfunction] +#[pyo3(name = "goertzel_bank", signature = (x, freqs, fs))] +pub fn pyfn_goertzel_bank<'py>(py: Python<'py>, x: Vec, freqs: Vec, fs: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::stft::goertzel_bank(&x, &freqs, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Decode one DTMF digit from a tone burst; None when no clear +/// row/column pair dominates. +/// +/// Rust: `transforms::stft::dtmf_decode` +#[pyfunction] +#[pyo3(name = "dtmf_decode", signature = (x, fs))] +pub fn pyfn_dtmf_decode<'py>(py: Python<'py>, x: Vec, fs: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::stft::dtmf_decode(&x, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// Chirp-z transform: X\[k\] = Σ x\[n\]·a^(−n)·w^(nk) for k = 0..m−1, +/// evaluated in O((n+m) log(n+m)) by Bluestein's substitution. +/// +/// Rust: `transforms::stft::chirp_z` +#[pyfunction] +#[pyo3(name = "chirp_z", signature = (x, m, w, a))] +pub fn pyfn_chirp_z<'py>(py: Python<'py>, x: Vec, m: usize, w: crate::runtime::coerce::ComplexArg, a: crate::runtime::coerce::ComplexArg) -> PyResult>> { + let x = x.into_iter().map(|__e| __e.0).collect::>(); + let w = w.0; + let a = a.0; + let __r = crate::runtime::guard(|| rust_physics_engine::transforms::stft::chirp_z(&x, m, w, a)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// Zoom FFT: m spectrum samples evenly spaced over [f_lo, f_hi] Hz. +/// +/// Rust: `transforms::stft::zoom_fft` +#[pyfunction] +#[pyo3(name = "zoom_fft", signature = (x, fs, f_lo, f_hi, m))] +pub fn pyfn_zoom_fft<'py>(py: Python<'py>, x: Vec, fs: f64, f_lo: f64, f_hi: f64, m: usize) -> PyResult>> { + let __r = crate::runtime::guard(|| rust_physics_engine::transforms::stft::zoom_fft(&x, fs, f_lo, f_hi, m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) +} + +/// Time-frequency reassigned spectrogram (Hann window): each bin's +/// energy is moved to its instantaneous time and frequency. Returns +/// (time s, frequency Hz, power) for every bin above −80 dB of the peak. +/// +/// Rust: `transforms::stft::reassigned_spectrogram` +#[pyfunction] +#[pyo3(name = "reassigned_spectrogram", signature = (x, fs, n_fft, hop))] +pub fn pyfn_reassigned_spectrogram<'py>(py: Python<'py>, x: Vec, fs: f64, n_fft: usize, hop: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::stft::reassigned_spectrogram(&x, fs, n_fft, hop))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, __x.2)).collect::>()) +} + +/// Constant-Q transform magnitudes: bins at fmin·2^(k/bins_per_octave), +/// each analyzed with its own Hann-windowed complex kernel whose length +/// keeps Q constant. Returns one vector of `n_bins` magnitudes per hop +/// of half the longest kernel. +/// +/// Rust: `transforms::stft::constant_q_transform` +#[pyfunction] +#[pyo3(name = "constant_q_transform", signature = (x, fs, fmin, bins_per_octave, n_bins))] +pub fn pyfn_constant_q_transform<'py>(py: Python<'py>, x: Vec, fs: f64, fmin: f64, bins_per_octave: usize, n_bins: usize) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::stft::constant_q_transform(&x, fs, fmin, bins_per_octave, n_bins))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_spectrogram, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mel_filterbank, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mel_spectrogram, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_goertzel, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_goertzel_bank, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dtmf_decode, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_chirp_z, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_zoom_fft, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_reassigned_spectrogram, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_constant_q_transform, m)?)?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_transforms__wavelet.rs b/bindings/python/src/generated/m_transforms__wavelet.rs new file mode 100644 index 0000000..13512c8 --- /dev/null +++ b/bindings/python/src/generated/m_transforms__wavelet.rs @@ -0,0 +1,272 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Decomposition and reconstruction filters (dec_lo, dec_hi, rec_lo, +/// rec_hi), quadrature-mirror related for the orthogonal families. +/// +/// Panics: +/// Panics for an unsupported order (Db/Sym > 20, Coif > 5, or a bior +/// pair outside the standard set). +/// +/// Rust: `transforms::wavelet::wavelet_filters` +#[pyfunction] +#[pyo3(name = "wavelet_filters", signature = (w))] +pub fn pyfn_wavelet_filters(w: crate::generated::types::PyWavelet) -> PyResult<(Vec, Vec, Vec, Vec)> { + let w = w.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::transforms::wavelet::wavelet_filters(w)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2, __v.3)) +} + +/// One-level DWT: (approximation, detail), each of length ⌊(n+L−1)/2⌋. +/// +/// Rust: `transforms::wavelet::dwt` +#[pyfunction] +#[pyo3(name = "dwt", signature = (x, w, mode))] +pub fn pyfn_dwt<'py>(py: Python<'py>, x: Vec, w: crate::generated::types::PyWavelet, mode: crate::generated::types::PyPadMode) -> PyResult<(Vec, Vec)> { + let w = w.inner; + let mode = mode.to_rust(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::wavelet::dwt(&x, w, mode))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// One-level inverse DWT; output length 2·len − L + 2. `mode` is +/// accepted for API symmetry (reconstruction itself needs no padding). +/// +/// Panics: +/// Panics if the approximation and detail lengths differ. +/// +/// Rust: `transforms::wavelet::idwt` +#[pyfunction] +#[pyo3(name = "idwt", signature = (a, d, w, mode))] +pub fn pyfn_idwt<'py>(py: Python<'py>, a: Vec, d: Vec, w: crate::generated::types::PyWavelet, mode: crate::generated::types::PyPadMode) -> PyResult> { + let w = w.inner; + let mode = mode.to_rust(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::wavelet::idwt(&a, &d, w, mode))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Multilevel decomposition: returns \[a_L, d_L, d_{L−1}, …, d_1\]. +/// +/// Rust: `transforms::wavelet::wavedec` +#[pyfunction] +#[pyo3(name = "wavedec", signature = (x, w, levels, mode))] +pub fn pyfn_wavedec<'py>(py: Python<'py>, x: Vec, w: crate::generated::types::PyWavelet, levels: usize, mode: crate::generated::types::PyPadMode) -> PyResult>> { + let w = w.inner; + let mode = mode.to_rust(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::wavelet::wavedec(&x, w, levels, mode))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Multilevel reconstruction (inverse of `wavedec`). +/// +/// Rust: `transforms::wavelet::waverec` +#[pyfunction] +#[pyo3(name = "waverec", signature = (coeffs, w, mode))] +pub fn pyfn_waverec<'py>(py: Python<'py>, coeffs: Vec>, w: crate::generated::types::PyWavelet, mode: crate::generated::types::PyPadMode) -> PyResult> { + let w = w.inner; + let mode = mode.to_rust(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::wavelet::waverec(&coeffs, w, mode))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// One-level separable 2D DWT with symmetric extension (rows along x +/// first, then columns): returns (LL, LH, HL, HH) where the first +/// letter is the x (row-direction) channel. Sub-band dims are +/// ⌊(w+L−1)/2⌋ × ⌊(h+L−1)/2⌋. +/// +/// Panics: +/// Panics unless `img.len() == w * h`. +/// +/// Rust: `transforms::wavelet::dwt_2d` +#[pyfunction] +#[pyo3(name = "dwt_2d", signature = (img, w, h, wavelet))] +pub fn pyfn_dwt_2d<'py>(py: Python<'py>, img: Vec, w: usize, h: usize, wavelet: crate::generated::types::PyWavelet) -> PyResult<(Vec, Vec, Vec, Vec)> { + let wavelet = wavelet.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::wavelet::dwt_2d(&img, w, h, wavelet))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2, __v.3)) +} + +/// Inverse of `dwt_2d`; `w` and `h` are the original image dimensions. +/// +/// Rust: `transforms::wavelet::idwt_2d` +#[pyfunction] +#[pyo3(name = "idwt_2d", signature = (ll, lh, hl, hh, w, h, wavelet))] +pub fn pyfn_idwt_2d<'py>(py: Python<'py>, ll: Vec, lh: Vec, hl: Vec, hh: Vec, w: usize, h: usize, wavelet: crate::generated::types::PyWavelet) -> PyResult> { + let wavelet = wavelet.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::wavelet::idwt_2d(&ll, &lh, &hl, &hh, w, h, wavelet))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Wavelet shrinkage denoising: decompose, threshold the detail bands, +/// reconstruct (trimmed to the input length). +/// +/// Rust: `transforms::wavelet::wavelet_denoise` +#[pyfunction] +#[pyo3(name = "wavelet_denoise", signature = (x, w, levels, t))] +pub fn pyfn_wavelet_denoise<'py>(py: Python<'py>, x: Vec, w: crate::generated::types::PyWavelet, levels: usize, t: crate::generated::types::PyThreshold) -> PyResult> { + let w = w.inner; + let t = t.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::wavelet::wavelet_denoise(&x, w, levels, t))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Keep the largest `keep_fraction` of all coefficients (approximation +/// always kept), zero the rest, and reconstruct. +/// +/// Rust: `transforms::wavelet::wavelet_compress` +#[pyfunction] +#[pyo3(name = "wavelet_compress", signature = (x, w, levels, keep_fraction))] +pub fn pyfn_wavelet_compress<'py>(py: Python<'py>, x: Vec, w: crate::generated::types::PyWavelet, levels: usize, keep_fraction: f64) -> PyResult> { + let w = w.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::wavelet::wavelet_compress(&x, w, levels, keep_fraction))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Continuous wavelet transform. `scales` are in samples; row s of the +/// output holds W(s, t) at every sample. Computed in the Fourier domain +/// (Torrence & Compo eq. 4) with unit-energy normalization √(2πs). +/// +/// Rust: `transforms::wavelet::cwt` +#[pyfunction] +#[pyo3(name = "cwt", signature = (x, scales, mother, fs))] +pub fn pyfn_cwt<'py>(py: Python<'py>, x: Vec, scales: Vec, mother: crate::generated::types::PyMother, fs: f64) -> PyResult>>> { + let mother = mother.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::transforms::wavelet::cwt(&x, &scales, mother, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()).collect::>()) +} + +/// |CWT|² per scale and sample. +/// +/// Rust: `transforms::wavelet::scalogram` +#[pyfunction] +#[pyo3(name = "scalogram", signature = (x, scales, mother, fs))] +pub fn pyfn_scalogram<'py>(py: Python<'py>, x: Vec, scales: Vec, mother: crate::generated::types::PyMother, fs: f64) -> PyResult>> { + let mother = mother.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::wavelet::scalogram(&x, &scales, mother, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Equivalent Fourier frequency (Hz) of a CWT scale in samples +/// (Torrence & Compo table 1). +/// +/// Rust: `transforms::wavelet::scale_to_frequency` +#[pyfunction] +#[pyo3(name = "scale_to_frequency", signature = (scale, mother, fs))] +pub fn pyfn_scale_to_frequency(scale: f64, mother: crate::generated::types::PyMother, fs: f64) -> PyResult { + let mother = mother.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::transforms::wavelet::scale_to_frequency(scale, mother, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Full wavelet-packet tree at the given depth: 2^levels leaves in +/// natural (frequency-ordered-by-index) order, symmetric extension. +/// +/// Rust: `transforms::wavelet::wavelet_packet_decompose` +#[pyfunction] +#[pyo3(name = "wavelet_packet_decompose", signature = (x, w, levels))] +pub fn pyfn_wavelet_packet_decompose<'py>(py: Python<'py>, x: Vec, w: crate::generated::types::PyWavelet, levels: usize) -> PyResult>> { + let w = w.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::wavelet::wavelet_packet_decompose(&x, w, levels))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Lossless integer 5/3 (LeGall) lifting DWT, in place: the first half +/// becomes the approximation, the second half the detail. +/// +/// Panics: +/// Panics unless the length is even and ≥ 2. +/// +/// Rust: `transforms::wavelet::lifting_dwt_53` +#[pyfunction] +#[pyo3(name = "lifting_dwt_53", signature = (x))] +pub fn pyfn_lifting_dwt_53<'py>(x: pyo3::Bound<'py, pyo3::PyAny>) -> PyResult<()> { + let mut x__v: Vec = x.extract()?; + let __r = crate::runtime::guard(|| rust_physics_engine::transforms::wavelet::lifting_dwt_53(&mut x__v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + crate::runtime::coerce::write_back(&x, &x__v)?; + Ok(()) +} + +/// Exact inverse of `lifting_dwt_53`. +/// +/// Panics: +/// Panics unless the length is even and ≥ 2. +/// +/// Rust: `transforms::wavelet::lifting_idwt_53` +#[pyfunction] +#[pyo3(name = "lifting_idwt_53", signature = (x))] +pub fn pyfn_lifting_idwt_53<'py>(x: pyo3::Bound<'py, pyo3::PyAny>) -> PyResult<()> { + let mut x__v: Vec = x.extract()?; + let __r = crate::runtime::guard(|| rust_physics_engine::transforms::wavelet::lifting_idwt_53(&mut x__v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + crate::runtime::coerce::write_back(&x, &x__v)?; + Ok(()) +} + +/// Multiresolution analysis: the input split into levels+1 additive +/// components (details from coarse to fine, then the approximation +/// first). Component 0 is the level-L approximation signal; component k +/// (k ≥ 1) is the detail at level L+1−k. The components sum to x. +/// +/// Rust: `transforms::wavelet::multiresolution_analysis` +#[pyfunction] +#[pyo3(name = "multiresolution_analysis", signature = (x, w, levels))] +pub fn pyfn_multiresolution_analysis<'py>(py: Python<'py>, x: Vec, w: crate::generated::types::PyWavelet, levels: usize) -> PyResult>> { + let w = w.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::wavelet::multiresolution_analysis(&x, w, levels))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_wavelet_filters, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dwt, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_idwt, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wavedec, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_waverec, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dwt_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_idwt_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wavelet_denoise, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wavelet_compress, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cwt, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_scalogram, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_scale_to_frequency, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wavelet_packet_decompose, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lifting_dwt_53, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lifting_idwt_53, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_multiresolution_analysis, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_trigonometry.rs b/bindings/python/src/generated/m_trigonometry.rs new file mode 100644 index 0000000..270eeca --- /dev/null +++ b/bindings/python/src/generated/m_trigonometry.rs @@ -0,0 +1,418 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Compute unknown side via law of cosines: c = sqrt(a² + b² - 2ab·cos(C)) +/// +/// Rust: `trigonometry::law_of_cosines_side` +#[pyfunction] +#[pyo3(name = "law_of_cosines_side", signature = (a, b, angle_c))] +pub fn pyfn_law_of_cosines_side(a: f64, b: f64, angle_c: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::trigonometry::law_of_cosines_side(a, b, angle_c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Compute unknown angle via law of cosines: C = arccos((a² + b² - c²) / (2ab)) +/// +/// Rust: `trigonometry::law_of_cosines_angle` +#[pyfunction] +#[pyo3(name = "law_of_cosines_angle", signature = (a, b, c))] +pub fn pyfn_law_of_cosines_angle(a: f64, b: f64, c: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::trigonometry::law_of_cosines_angle(a, b, c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Compute unknown side via law of sines: b = a·sin(B) / sin(A) +/// +/// Rust: `trigonometry::law_of_sines_side` +#[pyfunction] +#[pyo3(name = "law_of_sines_side", signature = (a, angle_a, angle_b))] +pub fn pyfn_law_of_sines_side(a: f64, angle_a: f64, angle_b: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::trigonometry::law_of_sines_side(a, angle_a, angle_b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Compute unknown angle via law of sines: B = arcsin(b·sin(A) / a) +/// +/// Rust: `trigonometry::law_of_sines_angle` +#[pyfunction] +#[pyo3(name = "law_of_sines_angle", signature = (a, b, angle_a))] +pub fn pyfn_law_of_sines_angle(a: f64, b: f64, angle_a: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::trigonometry::law_of_sines_angle(a, b, angle_a)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Triangle area using two sides and included angle: A = ½·a·b·sin(C) +/// +/// Rust: `trigonometry::triangle_area_sas` +#[pyfunction] +#[pyo3(name = "triangle_area_sas", signature = (a, b, angle_c))] +pub fn pyfn_triangle_area_sas(a: f64, b: f64, angle_c: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::trigonometry::triangle_area_sas(a, b, angle_c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Sine of sum identity: sin(a+b) = sin(a)cos(b) + cos(a)sin(b) +/// +/// Rust: `trigonometry::sin_sum` +#[pyfunction] +#[pyo3(name = "sin_sum", signature = (a, b))] +pub fn pyfn_sin_sum(a: f64, b: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::trigonometry::sin_sum(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Cosine of sum identity: cos(a+b) = cos(a)cos(b) - sin(a)sin(b) +/// +/// Rust: `trigonometry::cos_sum` +#[pyfunction] +#[pyo3(name = "cos_sum", signature = (a, b))] +pub fn pyfn_cos_sum(a: f64, b: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::trigonometry::cos_sum(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Sine of difference identity: sin(a-b) = sin(a)cos(b) - cos(a)sin(b) +/// +/// Rust: `trigonometry::sin_diff` +#[pyfunction] +#[pyo3(name = "sin_diff", signature = (a, b))] +pub fn pyfn_sin_diff(a: f64, b: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::trigonometry::sin_diff(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Cosine of difference identity: cos(a-b) = cos(a)cos(b) + sin(a)sin(b) +/// +/// Rust: `trigonometry::cos_diff` +#[pyfunction] +#[pyo3(name = "cos_diff", signature = (a, b))] +pub fn pyfn_cos_diff(a: f64, b: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::trigonometry::cos_diff(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Tangent of sum identity: tan(a+b) = (tan(a) + tan(b)) / (1 - tan(a)tan(b)) +/// +/// Rust: `trigonometry::tan_sum` +#[pyfunction] +#[pyo3(name = "tan_sum", signature = (a, b))] +pub fn pyfn_tan_sum(a: f64, b: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::trigonometry::tan_sum(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Double-angle sine identity: sin(2a) = 2·sin(a)·cos(a) +/// +/// Rust: `trigonometry::double_angle_sin` +#[pyfunction] +#[pyo3(name = "double_angle_sin", signature = (a))] +pub fn pyfn_double_angle_sin(a: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::trigonometry::double_angle_sin(a)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Double-angle cosine identity: cos(2a) = cos²(a) - sin²(a) +/// +/// Rust: `trigonometry::double_angle_cos` +#[pyfunction] +#[pyo3(name = "double_angle_cos", signature = (a))] +pub fn pyfn_double_angle_cos(a: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::trigonometry::double_angle_cos(a)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Half-angle sine identity: sin(a/2) = sqrt(|1 - cos(a)| / 2) +/// +/// Rust: `trigonometry::half_angle_sin` +#[pyfunction] +#[pyo3(name = "half_angle_sin", signature = (a))] +pub fn pyfn_half_angle_sin(a: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::trigonometry::half_angle_sin(a)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Half-angle cosine identity: cos(a/2) = sqrt(|1 + cos(a)| / 2) +/// +/// Rust: `trigonometry::half_angle_cos` +#[pyfunction] +#[pyo3(name = "half_angle_cos", signature = (a))] +pub fn pyfn_half_angle_cos(a: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::trigonometry::half_angle_cos(a)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Product-to-sum for sin·sin: sin(a)sin(b) = ½[cos(a-b) - cos(a+b)] +/// +/// Rust: `trigonometry::product_to_sum_sin_sin` +#[pyfunction] +#[pyo3(name = "product_to_sum_sin_sin", signature = (a, b))] +pub fn pyfn_product_to_sum_sin_sin(a: f64, b: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::trigonometry::product_to_sum_sin_sin(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Product-to-sum for cos·cos: cos(a)cos(b) = ½[cos(a-b) + cos(a+b)] +/// +/// Rust: `trigonometry::product_to_sum_cos_cos` +#[pyfunction] +#[pyo3(name = "product_to_sum_cos_cos", signature = (a, b))] +pub fn pyfn_product_to_sum_cos_cos(a: f64, b: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::trigonometry::product_to_sum_cos_cos(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Hyperbolic sine: sinh(x) = (eˣ - e⁻ˣ) / 2 +/// +/// Rust: `trigonometry::sinh` +#[pyfunction] +#[pyo3(name = "sinh", signature = (x))] +pub fn pyfn_sinh(x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::trigonometry::sinh(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Hyperbolic cosine: cosh(x) = (eˣ + e⁻ˣ) / 2 +/// +/// Rust: `trigonometry::cosh` +#[pyfunction] +#[pyo3(name = "cosh", signature = (x))] +pub fn pyfn_cosh(x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::trigonometry::cosh(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Hyperbolic tangent: tanh(x) = sinh(x) / cosh(x) +/// +/// Rust: `trigonometry::tanh` +#[pyfunction] +#[pyo3(name = "tanh", signature = (x))] +pub fn pyfn_tanh(x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::trigonometry::tanh(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Hyperbolic secant: sech(x) = 1 / cosh(x) +/// +/// Rust: `trigonometry::sech` +#[pyfunction] +#[pyo3(name = "sech", signature = (x))] +pub fn pyfn_sech(x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::trigonometry::sech(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Hyperbolic cosecant: csch(x) = 1 / sinh(x) +/// +/// Rust: `trigonometry::csch` +#[pyfunction] +#[pyo3(name = "csch", signature = (x))] +pub fn pyfn_csch(x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::trigonometry::csch(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Hyperbolic cotangent: coth(x) = cosh(x) / sinh(x) +/// +/// Rust: `trigonometry::coth` +#[pyfunction] +#[pyo3(name = "coth", signature = (x))] +pub fn pyfn_coth(x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::trigonometry::coth(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Inverse hyperbolic sine: asinh(x) = ln(x + sqrt(x² + 1)) +/// +/// Rust: `trigonometry::asinh` +#[pyfunction] +#[pyo3(name = "asinh", signature = (x))] +pub fn pyfn_asinh(x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::trigonometry::asinh(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Inverse hyperbolic cosine: acosh(x) = ln(x + sqrt(x² - 1)) +/// +/// Rust: `trigonometry::acosh` +#[pyfunction] +#[pyo3(name = "acosh", signature = (x))] +pub fn pyfn_acosh(x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::trigonometry::acosh(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Inverse hyperbolic tangent: atanh(x) = ½·ln((1+x) / (1-x)) +/// +/// Rust: `trigonometry::atanh` +#[pyfunction] +#[pyo3(name = "atanh", signature = (x))] +pub fn pyfn_atanh(x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::trigonometry::atanh(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Normalize angle to the range [0, 2π) +/// +/// Rust: `trigonometry::normalize_angle` +#[pyfunction] +#[pyo3(name = "normalize_angle", signature = (angle))] +pub fn pyfn_normalize_angle(angle: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::trigonometry::normalize_angle(angle)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Normalize angle to the range [-π, π) +/// +/// Rust: `trigonometry::normalize_angle_signed` +#[pyfunction] +#[pyo3(name = "normalize_angle_signed", signature = (angle))] +pub fn pyfn_normalize_angle_signed(angle: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::trigonometry::normalize_angle_signed(angle)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Signed shortest angular difference from a to b: normalize(b - a) in [-π, π) +/// +/// Rust: `trigonometry::angular_difference` +#[pyfunction] +#[pyo3(name = "angular_difference", signature = (a, b))] +pub fn pyfn_angular_difference(a: f64, b: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::trigonometry::angular_difference(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Check whether angle (in radians) is acute: 0 < angle < π/2 +/// +/// Rust: `trigonometry::is_acute` +#[pyfunction] +#[pyo3(name = "is_acute", signature = (angle))] +pub fn pyfn_is_acute(angle: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::trigonometry::is_acute(angle)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Check whether angle (in radians) is a right angle within tolerance: |angle - π/2| < tol +/// +/// Rust: `trigonometry::is_right` +#[pyfunction] +#[pyo3(name = "is_right", signature = (angle, tolerance))] +pub fn pyfn_is_right(angle: f64, tolerance: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::trigonometry::is_right(angle, tolerance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Check whether angle (in radians) is obtuse: π/2 < angle < π +/// +/// Rust: `trigonometry::is_obtuse` +#[pyfunction] +#[pyo3(name = "is_obtuse", signature = (angle))] +pub fn pyfn_is_obtuse(angle: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::trigonometry::is_obtuse(angle)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Complementary angle: π/2 - angle +/// +/// Rust: `trigonometry::complementary` +#[pyfunction] +#[pyo3(name = "complementary", signature = (angle))] +pub fn pyfn_complementary(angle: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::trigonometry::complementary(angle)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Supplementary angle: π - angle +/// +/// Rust: `trigonometry::supplementary` +#[pyfunction] +#[pyo3(name = "supplementary", signature = (angle))] +pub fn pyfn_supplementary(angle: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::trigonometry::supplementary(angle)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_law_of_cosines_side, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_law_of_cosines_angle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_law_of_sines_side, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_law_of_sines_angle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_triangle_area_sas, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sin_sum, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cos_sum, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sin_diff, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cos_diff, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tan_sum, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_double_angle_sin, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_double_angle_cos, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_half_angle_sin, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_half_angle_cos, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_product_to_sum_sin_sin, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_product_to_sum_cos_cos, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sinh, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_cosh, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tanh, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sech, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_csch, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_coth, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_asinh, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_acosh, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_atanh, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_normalize_angle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_normalize_angle_signed, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_angular_difference, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_is_acute, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_is_right, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_is_obtuse, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_complementary, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_supplementary, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_units.rs b/bindings/python/src/generated/m_units.rs new file mode 100644 index 0000000..c72e709 --- /dev/null +++ b/bindings/python/src/generated/m_units.rs @@ -0,0 +1,982 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Convert meters to feet: ft = m × 3.28084 +/// +/// Rust: `units::meters_to_feet` +#[pyfunction] +#[pyo3(name = "meters_to_feet", signature = (m))] +pub fn pyfn_meters_to_feet(m: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::meters_to_feet(m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert feet to meters: m = ft / 3.28084 +/// +/// Rust: `units::feet_to_meters` +#[pyfunction] +#[pyo3(name = "feet_to_meters", signature = (ft))] +pub fn pyfn_feet_to_meters(ft: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::feet_to_meters(ft)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert meters to inches: in = m × 39.3701 +/// +/// Rust: `units::meters_to_inches` +#[pyfunction] +#[pyo3(name = "meters_to_inches", signature = (m))] +pub fn pyfn_meters_to_inches(m: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::meters_to_inches(m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert inches to meters: m = in / 39.3701 +/// +/// Rust: `units::inches_to_meters` +#[pyfunction] +#[pyo3(name = "inches_to_meters", signature = (i))] +pub fn pyfn_inches_to_meters(i: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::inches_to_meters(i)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert kilometers to miles: mi = km × 0.621371 +/// +/// Rust: `units::km_to_miles` +#[pyfunction] +#[pyo3(name = "km_to_miles", signature = (km))] +pub fn pyfn_km_to_miles(km: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::km_to_miles(km)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert miles to kilometers: km = mi / 0.621371 +/// +/// Rust: `units::miles_to_km` +#[pyfunction] +#[pyo3(name = "miles_to_km", signature = (mi))] +pub fn pyfn_miles_to_km(mi: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::miles_to_km(mi)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert meters to astronomical units: AU = m / 1.496×10¹¹ +/// +/// Rust: `units::meters_to_au` +#[pyfunction] +#[pyo3(name = "meters_to_au", signature = (m))] +pub fn pyfn_meters_to_au(m: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::meters_to_au(m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert astronomical units to meters: m = AU × 1.496×10¹¹ +/// +/// Rust: `units::au_to_meters` +#[pyfunction] +#[pyo3(name = "au_to_meters", signature = (au))] +pub fn pyfn_au_to_meters(au: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::au_to_meters(au)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert meters to light-years: ly = m / 9.461×10¹⁵ +/// +/// Rust: `units::meters_to_light_years` +#[pyfunction] +#[pyo3(name = "meters_to_light_years", signature = (m))] +pub fn pyfn_meters_to_light_years(m: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::meters_to_light_years(m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert light-years to meters: m = ly × 9.461×10¹⁵ +/// +/// Rust: `units::light_years_to_meters` +#[pyfunction] +#[pyo3(name = "light_years_to_meters", signature = (ly))] +pub fn pyfn_light_years_to_meters(ly: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::light_years_to_meters(ly)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert meters to parsecs: pc = m / 3.086×10¹⁶ +/// +/// Rust: `units::meters_to_parsec` +#[pyfunction] +#[pyo3(name = "meters_to_parsec", signature = (m))] +pub fn pyfn_meters_to_parsec(m: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::meters_to_parsec(m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert parsecs to meters: m = pc × 3.086×10¹⁶ +/// +/// Rust: `units::parsec_to_meters` +#[pyfunction] +#[pyo3(name = "parsec_to_meters", signature = (pc))] +pub fn pyfn_parsec_to_meters(pc: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::parsec_to_meters(pc)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert angstroms to meters: m = Å × 10⁻¹⁰ +/// +/// Rust: `units::angstrom_to_meters` +#[pyfunction] +#[pyo3(name = "angstrom_to_meters", signature = (a))] +pub fn pyfn_angstrom_to_meters(a: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::angstrom_to_meters(a)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert meters to angstroms: Å = m / 10⁻¹⁰ +/// +/// Rust: `units::meters_to_angstrom` +#[pyfunction] +#[pyo3(name = "meters_to_angstrom", signature = (m))] +pub fn pyfn_meters_to_angstrom(m: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::meters_to_angstrom(m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert nautical miles to meters: m = nmi × 1852 +/// +/// Rust: `units::nautical_miles_to_meters` +#[pyfunction] +#[pyo3(name = "nautical_miles_to_meters", signature = (nm))] +pub fn pyfn_nautical_miles_to_meters(nm: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::nautical_miles_to_meters(nm)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert meters to nautical miles: nmi = m / 1852 +/// +/// Rust: `units::meters_to_nautical_miles` +#[pyfunction] +#[pyo3(name = "meters_to_nautical_miles", signature = (m))] +pub fn pyfn_meters_to_nautical_miles(m: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::meters_to_nautical_miles(m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert kilograms to pounds: lb = kg × 2.20462 +/// +/// Rust: `units::kg_to_lbs` +#[pyfunction] +#[pyo3(name = "kg_to_lbs", signature = (kg))] +pub fn pyfn_kg_to_lbs(kg: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::kg_to_lbs(kg)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert pounds to kilograms: kg = lb / 2.20462 +/// +/// Rust: `units::lbs_to_kg` +#[pyfunction] +#[pyo3(name = "lbs_to_kg", signature = (lbs))] +pub fn pyfn_lbs_to_kg(lbs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::lbs_to_kg(lbs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert kilograms to solar masses: M☉ = kg / 1.989×10³⁰ +/// +/// Rust: `units::kg_to_solar_masses` +#[pyfunction] +#[pyo3(name = "kg_to_solar_masses", signature = (kg))] +pub fn pyfn_kg_to_solar_masses(kg: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::kg_to_solar_masses(kg)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert solar masses to kilograms: kg = M☉ × 1.989×10³⁰ +/// +/// Rust: `units::solar_masses_to_kg` +#[pyfunction] +#[pyo3(name = "solar_masses_to_kg", signature = (sm))] +pub fn pyfn_solar_masses_to_kg(sm: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::solar_masses_to_kg(sm)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert atomic mass units to kilograms: kg = amu × 1.66054×10⁻²⁷ +/// +/// Rust: `units::amu_to_kg` +#[pyfunction] +#[pyo3(name = "amu_to_kg", signature = (amu))] +pub fn pyfn_amu_to_kg(amu: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::amu_to_kg(amu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert kilograms to atomic mass units: amu = kg / 1.66054×10⁻²⁷ +/// +/// Rust: `units::kg_to_amu` +#[pyfunction] +#[pyo3(name = "kg_to_amu", signature = (kg))] +pub fn pyfn_kg_to_amu(kg: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::kg_to_amu(kg)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert joules to electron-volts: eV = J / e +/// +/// Rust: `units::joules_to_ev` +#[pyfunction] +#[pyo3(name = "joules_to_ev", signature = (j))] +pub fn pyfn_joules_to_ev(j: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::joules_to_ev(j)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert electron-volts to joules: J = eV × e +/// +/// Rust: `units::ev_to_joules` +#[pyfunction] +#[pyo3(name = "ev_to_joules", signature = (ev))] +pub fn pyfn_ev_to_joules(ev: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::ev_to_joules(ev)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert joules to calories: cal = J / 4.184 +/// +/// Rust: `units::joules_to_calories` +#[pyfunction] +#[pyo3(name = "joules_to_calories", signature = (j))] +pub fn pyfn_joules_to_calories(j: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::joules_to_calories(j)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert calories to joules: J = cal × 4.184 +/// +/// Rust: `units::calories_to_joules` +#[pyfunction] +#[pyo3(name = "calories_to_joules", signature = (cal))] +pub fn pyfn_calories_to_joules(cal: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::calories_to_joules(cal)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert joules to kilowatt-hours: kWh = J / 3.6×10⁶ +/// +/// Rust: `units::joules_to_kwh` +#[pyfunction] +#[pyo3(name = "joules_to_kwh", signature = (j))] +pub fn pyfn_joules_to_kwh(j: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::joules_to_kwh(j)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert kilowatt-hours to joules: J = kWh × 3.6×10⁶ +/// +/// Rust: `units::kwh_to_joules` +#[pyfunction] +#[pyo3(name = "kwh_to_joules", signature = (kwh))] +pub fn pyfn_kwh_to_joules(kwh: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::kwh_to_joules(kwh)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert joules to British thermal units: BTU = J / 1055.06 +/// +/// Rust: `units::joules_to_btu` +#[pyfunction] +#[pyo3(name = "joules_to_btu", signature = (j))] +pub fn pyfn_joules_to_btu(j: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::joules_to_btu(j)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert British thermal units to joules: J = BTU × 1055.06 +/// +/// Rust: `units::btu_to_joules` +#[pyfunction] +#[pyo3(name = "btu_to_joules", signature = (btu))] +pub fn pyfn_btu_to_joules(btu: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::btu_to_joules(btu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Converts photon energy in eV to wavelength in meters via λ = hc/E. +/// +/// Rust: `units::ev_to_wavelength` +#[pyfunction] +#[pyo3(name = "ev_to_wavelength", signature = (ev))] +pub fn pyfn_ev_to_wavelength(ev: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::ev_to_wavelength(ev)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Converts photon wavelength in meters to energy in eV via E = hc/λ. +/// +/// Rust: `units::wavelength_to_ev` +#[pyfunction] +#[pyo3(name = "wavelength_to_ev", signature = (wavelength))] +pub fn pyfn_wavelength_to_ev(wavelength: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::wavelength_to_ev(wavelength)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert pascals to atmospheres: atm = Pa / 101325 +/// +/// Rust: `units::pa_to_atm` +#[pyfunction] +#[pyo3(name = "pa_to_atm", signature = (pa))] +pub fn pyfn_pa_to_atm(pa: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::pa_to_atm(pa)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert atmospheres to pascals: Pa = atm × 101325 +/// +/// Rust: `units::atm_to_pa` +#[pyfunction] +#[pyo3(name = "atm_to_pa", signature = (atm))] +pub fn pyfn_atm_to_pa(atm: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::atm_to_pa(atm)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert pascals to bar: bar = Pa / 10⁵ +/// +/// Rust: `units::pa_to_bar` +#[pyfunction] +#[pyo3(name = "pa_to_bar", signature = (pa))] +pub fn pyfn_pa_to_bar(pa: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::pa_to_bar(pa)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert bar to pascals: Pa = bar × 10⁵ +/// +/// Rust: `units::bar_to_pa` +#[pyfunction] +#[pyo3(name = "bar_to_pa", signature = (bar))] +pub fn pyfn_bar_to_pa(bar: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::bar_to_pa(bar)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert pascals to pounds per square inch: psi = Pa / 6894.76 +/// +/// Rust: `units::pa_to_psi` +#[pyfunction] +#[pyo3(name = "pa_to_psi", signature = (pa))] +pub fn pyfn_pa_to_psi(pa: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::pa_to_psi(pa)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert pounds per square inch to pascals: Pa = psi × 6894.76 +/// +/// Rust: `units::psi_to_pa` +#[pyfunction] +#[pyo3(name = "psi_to_pa", signature = (psi))] +pub fn pyfn_psi_to_pa(psi: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::psi_to_pa(psi)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert pascals to millimeters of mercury: mmHg = Pa / 133.322 +/// +/// Rust: `units::pa_to_mmhg` +#[pyfunction] +#[pyo3(name = "pa_to_mmhg", signature = (pa))] +pub fn pyfn_pa_to_mmhg(pa: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::pa_to_mmhg(pa)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert millimeters of mercury to pascals: Pa = mmHg × 133.322 +/// +/// Rust: `units::mmhg_to_pa` +#[pyfunction] +#[pyo3(name = "mmhg_to_pa", signature = (mmhg))] +pub fn pyfn_mmhg_to_pa(mmhg: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::mmhg_to_pa(mmhg)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert degrees to radians: rad = deg × π / 180 +/// +/// Rust: `units::degrees_to_radians` +#[pyfunction] +#[pyo3(name = "degrees_to_radians", signature = (deg))] +pub fn pyfn_degrees_to_radians(deg: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::degrees_to_radians(deg)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert radians to degrees: deg = rad × 180 / π +/// +/// Rust: `units::radians_to_degrees` +#[pyfunction] +#[pyo3(name = "radians_to_degrees", signature = (rad))] +pub fn pyfn_radians_to_degrees(rad: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::radians_to_degrees(rad)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert revolutions per minute to radians per second: ω = rpm × 2π / 60 +/// +/// Rust: `units::rpm_to_rad_per_sec` +#[pyfunction] +#[pyo3(name = "rpm_to_rad_per_sec", signature = (rpm))] +pub fn pyfn_rpm_to_rad_per_sec(rpm: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::rpm_to_rad_per_sec(rpm)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert radians per second to revolutions per minute: rpm = ω × 60 / (2π) +/// +/// Rust: `units::rad_per_sec_to_rpm` +#[pyfunction] +#[pyo3(name = "rad_per_sec_to_rpm", signature = (omega))] +pub fn pyfn_rad_per_sec_to_rpm(omega: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::rad_per_sec_to_rpm(omega)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert seconds to years: yr = s / 3.1557×10⁷ +/// +/// Rust: `units::seconds_to_years` +#[pyfunction] +#[pyo3(name = "seconds_to_years", signature = (s))] +pub fn pyfn_seconds_to_years(s: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::seconds_to_years(s)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert years to seconds: s = yr × 3.1557×10⁷ +/// +/// Rust: `units::years_to_seconds` +#[pyfunction] +#[pyo3(name = "years_to_seconds", signature = (yr))] +pub fn pyfn_years_to_seconds(yr: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::years_to_seconds(yr)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert meters per second to kilometers per hour: km/h = m/s × 3.6 +/// +/// Rust: `units::mps_to_kmh` +#[pyfunction] +#[pyo3(name = "mps_to_kmh", signature = (mps))] +pub fn pyfn_mps_to_kmh(mps: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::mps_to_kmh(mps)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert kilometers per hour to meters per second: m/s = km/h / 3.6 +/// +/// Rust: `units::kmh_to_mps` +#[pyfunction] +#[pyo3(name = "kmh_to_mps", signature = (kmh))] +pub fn pyfn_kmh_to_mps(kmh: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::kmh_to_mps(kmh)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert meters per second to miles per hour: mph = m/s × 2.23694 +/// +/// Rust: `units::mps_to_mph` +#[pyfunction] +#[pyo3(name = "mps_to_mph", signature = (mps))] +pub fn pyfn_mps_to_mph(mps: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::mps_to_mph(mps)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert miles per hour to meters per second: m/s = mph / 2.23694 +/// +/// Rust: `units::mph_to_mps` +#[pyfunction] +#[pyo3(name = "mph_to_mps", signature = (mph))] +pub fn pyfn_mph_to_mps(mph: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::mph_to_mps(mph)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert meters per second to knots: kt = m/s × 1.94384 +/// +/// Rust: `units::mps_to_knots` +#[pyfunction] +#[pyo3(name = "mps_to_knots", signature = (mps))] +pub fn pyfn_mps_to_knots(mps: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::mps_to_knots(mps)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert knots to meters per second: m/s = kt / 1.94384 +/// +/// Rust: `units::knots_to_mps` +#[pyfunction] +#[pyo3(name = "knots_to_mps", signature = (kt))] +pub fn pyfn_knots_to_mps(kt: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::knots_to_mps(kt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert meters per second to Mach number: M = v / v_sound +/// +/// Rust: `units::mps_to_mach` +#[pyfunction] +#[pyo3(name = "mps_to_mach", signature = (mps, speed_of_sound))] +pub fn pyfn_mps_to_mach(mps: f64, speed_of_sound: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::mps_to_mach(mps, speed_of_sound)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert Mach number to meters per second: v = M × v_sound +/// +/// Rust: `units::mach_to_mps` +#[pyfunction] +#[pyo3(name = "mach_to_mps", signature = (mach, speed_of_sound))] +pub fn pyfn_mach_to_mps(mach: f64, speed_of_sound: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::mach_to_mps(mach, speed_of_sound)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert watts to mechanical horsepower: hp = W / 745.7 +/// +/// Rust: `units::watts_to_horsepower` +#[pyfunction] +#[pyo3(name = "watts_to_horsepower", signature = (w))] +pub fn pyfn_watts_to_horsepower(w: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::watts_to_horsepower(w)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert mechanical horsepower to watts: W = hp × 745.7 +/// +/// Rust: `units::horsepower_to_watts` +#[pyfunction] +#[pyo3(name = "horsepower_to_watts", signature = (hp))] +pub fn pyfn_horsepower_to_watts(hp: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::horsepower_to_watts(hp)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert watts to BTU per hour: BTU/h = W / 0.293071 +/// +/// Rust: `units::watts_to_btu_per_hour` +#[pyfunction] +#[pyo3(name = "watts_to_btu_per_hour", signature = (w))] +pub fn pyfn_watts_to_btu_per_hour(w: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::watts_to_btu_per_hour(w)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert BTU per hour to watts: W = BTU/h × 0.293071 +/// +/// Rust: `units::btu_per_hour_to_watts` +#[pyfunction] +#[pyo3(name = "btu_per_hour_to_watts", signature = (btu_hr))] +pub fn pyfn_btu_per_hour_to_watts(btu_hr: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::btu_per_hour_to_watts(btu_hr)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert watts to tons of refrigeration: TR = W / 3516.85 +/// +/// Rust: `units::watts_to_tons_refrigeration` +#[pyfunction] +#[pyo3(name = "watts_to_tons_refrigeration", signature = (w))] +pub fn pyfn_watts_to_tons_refrigeration(w: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::watts_to_tons_refrigeration(w)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert tons of refrigeration to watts: W = TR × 3516.85 +/// +/// Rust: `units::tons_refrigeration_to_watts` +#[pyfunction] +#[pyo3(name = "tons_refrigeration_to_watts", signature = (tons))] +pub fn pyfn_tons_refrigeration_to_watts(tons: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::tons_refrigeration_to_watts(tons)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert watts to kilocalories per hour: kcal/h = W / 1.163 +/// +/// Rust: `units::watts_to_kcal_per_hour` +#[pyfunction] +#[pyo3(name = "watts_to_kcal_per_hour", signature = (w))] +pub fn pyfn_watts_to_kcal_per_hour(w: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::watts_to_kcal_per_hour(w)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert kilocalories per hour to watts: W = kcal/h × 1.163 +/// +/// Rust: `units::kcal_per_hour_to_watts` +#[pyfunction] +#[pyo3(name = "kcal_per_hour_to_watts", signature = (kcal_hr))] +pub fn pyfn_kcal_per_hour_to_watts(kcal_hr: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::kcal_per_hour_to_watts(kcal_hr)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert kilowatts to watts: W = kW × 10³ +/// +/// Rust: `units::kilowatts_to_watts` +#[pyfunction] +#[pyo3(name = "kilowatts_to_watts", signature = (kw))] +pub fn pyfn_kilowatts_to_watts(kw: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::kilowatts_to_watts(kw)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert watts to kilowatts: kW = W × 10⁻³ +/// +/// Rust: `units::watts_to_kilowatts` +#[pyfunction] +#[pyo3(name = "watts_to_kilowatts", signature = (w))] +pub fn pyfn_watts_to_kilowatts(w: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::watts_to_kilowatts(w)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert megawatts to watts: W = MW × 10⁶ +/// +/// Rust: `units::megawatts_to_watts` +#[pyfunction] +#[pyo3(name = "megawatts_to_watts", signature = (mw))] +pub fn pyfn_megawatts_to_watts(mw: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::megawatts_to_watts(mw)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert watts to megawatts: MW = W × 10⁻⁶ +/// +/// Rust: `units::watts_to_megawatts` +#[pyfunction] +#[pyo3(name = "watts_to_megawatts", signature = (w))] +pub fn pyfn_watts_to_megawatts(w: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::watts_to_megawatts(w)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert watt-hours to joules: J = Wh × 3600 +/// +/// Rust: `units::watt_hours_to_joules` +#[pyfunction] +#[pyo3(name = "watt_hours_to_joules", signature = (wh))] +pub fn pyfn_watt_hours_to_joules(wh: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::watt_hours_to_joules(wh)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert joules to watt-hours: Wh = J / 3600 +/// +/// Rust: `units::joules_to_watt_hours` +#[pyfunction] +#[pyo3(name = "joules_to_watt_hours", signature = (j))] +pub fn pyfn_joules_to_watt_hours(j: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::joules_to_watt_hours(j)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert ampere-hours to coulombs: C = Ah × 3600 +/// +/// Rust: `units::amp_hours_to_coulombs` +#[pyfunction] +#[pyo3(name = "amp_hours_to_coulombs", signature = (ah))] +pub fn pyfn_amp_hours_to_coulombs(ah: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::amp_hours_to_coulombs(ah)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert coulombs to ampere-hours: Ah = C / 3600 +/// +/// Rust: `units::coulombs_to_amp_hours` +#[pyfunction] +#[pyo3(name = "coulombs_to_amp_hours", signature = (c))] +pub fn pyfn_coulombs_to_amp_hours(c: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::coulombs_to_amp_hours(c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert kg of TNT equivalent to joules: J = kg × 4.184×10⁶ +/// +/// Rust: `units::kg_tnt_to_joules` +#[pyfunction] +#[pyo3(name = "kg_tnt_to_joules", signature = (kg))] +pub fn pyfn_kg_tnt_to_joules(kg: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::kg_tnt_to_joules(kg)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert joules to kg of TNT equivalent: kg = J / 4.184×10⁶ +/// +/// Rust: `units::joules_to_kg_tnt` +#[pyfunction] +#[pyo3(name = "joules_to_kg_tnt", signature = (j))] +pub fn pyfn_joules_to_kg_tnt(j: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::joules_to_kg_tnt(j)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert kg of coal equivalent to joules: J = kg × 2.9×10⁷ +/// +/// Rust: `units::kg_coal_to_joules` +#[pyfunction] +#[pyo3(name = "kg_coal_to_joules", signature = (kg))] +pub fn pyfn_kg_coal_to_joules(kg: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::kg_coal_to_joules(kg)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert joules to kg of coal equivalent: kg = J / 2.9×10⁷ +/// +/// Rust: `units::joules_to_kg_coal` +#[pyfunction] +#[pyo3(name = "joules_to_kg_coal", signature = (j))] +pub fn pyfn_joules_to_kg_coal(j: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::joules_to_kg_coal(j)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert kg of oil equivalent to joules: J = kg × 4.187×10⁷ +/// +/// Rust: `units::kg_oil_to_joules` +#[pyfunction] +#[pyo3(name = "kg_oil_to_joules", signature = (kg))] +pub fn pyfn_kg_oil_to_joules(kg: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::kg_oil_to_joules(kg)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert joules to kg of oil equivalent: kg = J / 4.187×10⁷ +/// +/// Rust: `units::joules_to_kg_oil` +#[pyfunction] +#[pyo3(name = "joules_to_kg_oil", signature = (j))] +pub fn pyfn_joules_to_kg_oil(j: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::joules_to_kg_oil(j)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert kg of hydrogen to joules: J = kg × 1.42×10⁸ +/// +/// Rust: `units::kg_hydrogen_to_joules` +#[pyfunction] +#[pyo3(name = "kg_hydrogen_to_joules", signature = (kg))] +pub fn pyfn_kg_hydrogen_to_joules(kg: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::kg_hydrogen_to_joules(kg)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert joules to kg of hydrogen equivalent: kg = J / 1.42×10⁸ +/// +/// Rust: `units::joules_to_kg_hydrogen` +#[pyfunction] +#[pyo3(name = "joules_to_kg_hydrogen", signature = (j))] +pub fn pyfn_joules_to_kg_hydrogen(j: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::joules_to_kg_hydrogen(j)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert liters of gasoline to joules: J = L × 3.4×10⁷ +/// +/// Rust: `units::liters_gasoline_to_joules` +#[pyfunction] +#[pyo3(name = "liters_gasoline_to_joules", signature = (liters))] +pub fn pyfn_liters_gasoline_to_joules(liters: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::liters_gasoline_to_joules(liters)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Convert joules to liters of gasoline equivalent: L = J / 3.4×10⁷ +/// +/// Rust: `units::joules_to_liters_gasoline` +#[pyfunction] +#[pyo3(name = "joules_to_liters_gasoline", signature = (j))] +pub fn pyfn_joules_to_liters_gasoline(j: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::joules_to_liters_gasoline(j)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_meters_to_feet, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_feet_to_meters, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_meters_to_inches, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_inches_to_meters, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_km_to_miles, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_miles_to_km, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_meters_to_au, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_au_to_meters, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_meters_to_light_years, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_light_years_to_meters, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_meters_to_parsec, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_parsec_to_meters, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_angstrom_to_meters, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_meters_to_angstrom, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_nautical_miles_to_meters, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_meters_to_nautical_miles, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kg_to_lbs, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_lbs_to_kg, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kg_to_solar_masses, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_solar_masses_to_kg, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_amu_to_kg, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kg_to_amu, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_joules_to_ev, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ev_to_joules, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_joules_to_calories, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_calories_to_joules, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_joules_to_kwh, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kwh_to_joules, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_joules_to_btu, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_btu_to_joules, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_ev_to_wavelength, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wavelength_to_ev, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pa_to_atm, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_atm_to_pa, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pa_to_bar, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_bar_to_pa, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pa_to_psi, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_psi_to_pa, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_pa_to_mmhg, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mmhg_to_pa, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_degrees_to_radians, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_radians_to_degrees, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rpm_to_rad_per_sec, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rad_per_sec_to_rpm, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_seconds_to_years, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_years_to_seconds, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mps_to_kmh, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kmh_to_mps, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mps_to_mph, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mph_to_mps, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mps_to_knots, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_knots_to_mps, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mps_to_mach, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mach_to_mps, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_watts_to_horsepower, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_horsepower_to_watts, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_watts_to_btu_per_hour, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_btu_per_hour_to_watts, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_watts_to_tons_refrigeration, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_tons_refrigeration_to_watts, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_watts_to_kcal_per_hour, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kcal_per_hour_to_watts, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kilowatts_to_watts, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_watts_to_kilowatts, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_megawatts_to_watts, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_watts_to_megawatts, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_watt_hours_to_joules, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_joules_to_watt_hours, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_amp_hours_to_coulombs, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_coulombs_to_amp_hours, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kg_tnt_to_joules, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_joules_to_kg_tnt, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kg_coal_to_joules, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_joules_to_kg_coal, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kg_oil_to_joules, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_joules_to_kg_oil, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_kg_hydrogen_to_joules, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_joules_to_kg_hydrogen, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_liters_gasoline_to_joules, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_joules_to_liters_gasoline, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_units__dimensional.rs b/bindings/python/src/generated/m_units__dimensional.rs new file mode 100644 index 0000000..88ccf01 --- /dev/null +++ b/bindings/python/src/generated/m_units__dimensional.rs @@ -0,0 +1,213 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// A basis for the dimensionless groups of a set of quantities. +/// +/// Returns exactly `n - rank` vectors of `n` exact rational exponents. +/// The product of the quantities raised to those exponents is +/// dimensionless, exactly. +/// +/// Any basis of the null space is a valid answer and this one is +/// whichever the elimination produces; see the module note on why that +/// is not the same as producing the groups anybody has named. +/// +/// Errors: +/// +/// `DimError::Malformed` if given no quantities. +/// +/// Rust: `units::dimensional::buckingham_pi` +#[pyfunction] +#[pyo3(name = "buckingham_pi", signature = (dims))] +pub fn pyfn_buckingham_pi<'py>(py: Python<'py>, dims: Vec) -> PyResult>>> { + let dims = dims.into_iter().map(|__e| __e.inner).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::units::dimensional::buckingham_pi(&dims)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::errors::map_dim)?; + Ok(__v.into_iter().map(|__x| -> PyResult>> { Ok(__x.into_iter().map(|__x| -> PyResult> { Ok(crate::runtime::coerce::rational_out(py, &__x)?) }).collect::>>()?) }).collect::>>()?) +} + +/// Checks that a vector of exponents really does cancel every dimension. +/// +/// Exact: the sum of each row is compared against zero, not against a +/// tolerance. +/// +/// Errors: +/// +/// `DimError::Malformed` if the lengths disagree. +/// +/// Rust: `units::dimensional::is_dimensionless_group` +#[pyfunction] +#[pyo3(name = "is_dimensionless_group", signature = (dims, exponents))] +pub fn pyfn_is_dimensionless_group<'py>(py: Python<'py>, dims: Vec, exponents: Vec) -> PyResult { + let dims = dims.into_iter().map(|__e| __e.inner).collect::>(); + let exponents = exponents.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::units::dimensional::is_dimensionless_group(&dims, &exponents))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::errors::map_dim)?; + Ok(__v) +} + +/// The dimensionless groups that have names, with their formulas and +/// what each compares. +/// +/// The formulas are the conventional ones. Each is *a* member of its +/// problem's null space rather than the only one -- see the module note. +/// +/// Rust: `units::dimensional::dimensionless_groups_named` +#[pyfunction] +#[pyo3(name = "dimensionless_groups_named", signature = ())] +pub fn pyfn_dimensionless_groups_named<'py>(py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::units::dimensional::dimensionless_groups_named())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0.to_string(), __x.1.to_string(), __x.2.to_string())).collect::>()) +} + +/// The power of energy a dimension corresponds to when `hbar = c = 1`. +/// +/// `[M] = [E]`, `[L] = [T] = [E]^-1`, so the power is +/// `kg - m - s`. +/// +/// Errors: +/// +/// `DimError::Mismatch` if the dimension involves amperes, kelvin, +/// moles or candela, which need further conventions to absorb and are +/// refused rather than guessed at. +/// +/// Rust: `units::dimensional::natural_units_power` +#[pyfunction] +#[pyo3(name = "natural_units_power", signature = (dim))] +pub fn pyfn_natural_units_power(dim: crate::generated::types::PyDim) -> PyResult { + let dim = dim.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::units::dimensional::natural_units_power(dim)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::errors::map_dim)?; + Ok(__v) +} + +/// Expresses an SI magnitude in electron volts to the power +/// `natural_units_power` gives. +/// +/// Errors: +/// +/// As `natural_units_power`. +/// +/// Rust: `units::dimensional::natural_units_convert` +#[pyfunction] +#[pyo3(name = "natural_units_convert", signature = (value, dim))] +pub fn pyfn_natural_units_convert(value: f64, dim: crate::generated::types::PyDim) -> PyResult { + let dim = dim.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::units::dimensional::natural_units_convert(value, dim)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::errors::map_dim)?; + Ok(__v) +} + +/// The Planck units, as `(name, value, unit)`. +/// +/// Each is built from `hbar`, `c` and `G` alone, which is the point: +/// they are the only combination of those three with the dimensions of a +/// length, a time, a mass and so on, so they are the scale at which +/// gravity and quantum mechanics are the same size. The defining +/// relations are checked in the tests against the CODATA values rather +/// than the numbers being copied in. +/// +/// Rust: `units::dimensional::planck_units` +#[pyfunction] +#[pyo3(name = "planck_units", signature = ())] +pub fn pyfn_planck_units<'py>(py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::units::dimensional::planck_units())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0.to_string(), __x.1, __x.2.to_string())).collect::>()) +} + +/// The dimension of a symbolic expression, given the dimension of every +/// variable in it. +/// +/// This is the check a physicist runs before believing an algebra step, +/// done mechanically. It is worth having as code because the two rules +/// it enforces are the ones a hand derivation drops: +/// +/// * **Every term of a sum has to have the same dimension.** A length +/// plus a time is not a longer length, it is a mistake, and it is the +/// mistake a dropped factor produces. +/// * **A transcendental function's argument has to be dimensionless.** +/// `sin`, `exp` and `ln` are defined by their power series, and a +/// series adds `x` to `x^3` to `x^5`, so `x` can only be a pure +/// number. `exp(-t/tau)` is meaningful and `exp(-t)` is not, and the +/// difference is the missing timescale. +/// +/// Neither rule can be checked by evaluating the formula: both sides of +/// `x + v` are finite floats. They are properties of the expression, and +/// this walks the expression. +/// +/// `var_dims` maps each variable name to its dimension; the first +/// matching entry wins. Numeric literals are dimensionless. +/// +/// Exponents: +/// +/// `Pow(b, e)` needs `e` to be a literal number, because the dimension +/// of `b^e` depends on the *value* of `e` and not on its dimension. +/// When the base is dimensionless the exponent may be anything +/// dimensionless -- `2^n` is a pure number whatever `n` is -- but when +/// the base carries dimensions the exponent must be a literal -- an +/// `Expr::Rat` or an `Expr::Const` -- and the base's exponents must +/// all be divisible by the literal's denominator. +/// +/// A `Const` is read as the dyadic rational it exactly is, which needs +/// no guessing: `0.5` is one half, so `Pow(x, 0.5)` is a square root +/// and behaves like one. `0.1` is not one tenth, it is the +/// power-of-two fraction the float holds, and no dimension is +/// divisible by that denominator, so `l^0.1` is reported as a root +/// that does not exist rather than quietly rounded into one that +/// does. +/// +/// Errors: +/// +/// `DimError::Mismatch` when the terms of a sum disagree or a +/// transcendental is handed something dimensioned; +/// `DimError::UnknownVar` for a variable missing from `var_dims`; +/// `DimError::NotAPerfectRoot` for a root that does not come out +/// exactly; `DimError::Malformed` for an exponent that is not a +/// literal; `DimError::Overflow` if an exponent leaves `i8`. +/// +/// Examples: +/// +/// Rust: `units::dimensional::dimensional_check_formula` +#[pyfunction] +#[pyo3(name = "dimensional_check_formula", signature = (expr, var_dims))] +pub fn pyfn_dimensional_check_formula(expr: crate::generated::types::PyExpr, var_dims: Vec<(String, crate::generated::types::PyDim)>) -> PyResult { + let expr = expr.inner; + let var_dims = var_dims.into_iter().map(|__e| (__e.0, __e.1.inner)).collect::>(); + let var_dims__b: Vec<(&str, rust_physics_engine::units::quantity::Dim)> = var_dims.iter().map(|__b| ((*__b).0.as_str(), (*__b).1.clone())).collect(); + let __r = crate::runtime::guard(|| rust_physics_engine::units::dimensional::dimensional_check_formula(&expr, &var_dims__b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::errors::map_dim)?; + Ok(crate::generated::types::PyDim { inner: __v }) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_buckingham_pi, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_is_dimensionless_group, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dimensionless_groups_named, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_natural_units_power, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_natural_units_convert, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_planck_units, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_dimensional_check_formula, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_units__quantity.rs b/bindings/python/src/generated/m_units__quantity.rs new file mode 100644 index 0000000..d9467e4 --- /dev/null +++ b/bindings/python/src/generated/m_units__quantity.rs @@ -0,0 +1,135 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Parses a unit expression such as `m/s^2`, `kg*m^2/s^3` or `J s`. +/// +/// Multiplication is written `*` or a space, and division `/`. A `/` +/// applies to the single term that follows it and nothing more, so +/// `J/mol/K` is joules per mole per kelvin. Parentheses are **not** +/// supported: `J/(mol K)` is rejected as an unknown unit rather than +/// quietly parsed as something else, which is the safer of the two ways +/// to not support them. +/// +/// Errors: +/// +/// `DimError::UnknownUnit` for an unrecognised name, or +/// `DimError::Malformed` for a broken exponent. +/// +/// Rust: `units::quantity::parse_unit` +#[pyfunction] +#[pyo3(name = "parse_unit", signature = (text))] +pub fn pyfn_parse_unit(text: String) -> PyResult<(f64, crate::generated::types::PyDim)> { + let __r = crate::runtime::guard(|| rust_physics_engine::units::quantity::parse_unit(&text)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::errors::map_dim)?; + Ok((__v.0, crate::generated::types::PyDim { inner: __v.1 })) +} + +/// Parses a quantity such as `"9.81 m/s^2"` or `"3 kWh"`. +/// +/// Errors: +/// +/// `DimError::Malformed` if there is no number, and whatever +/// `parse_unit` reports for the rest. +/// +/// Rust: `units::quantity::parse_quantity` +#[pyfunction] +#[pyo3(name = "parse_quantity", signature = (text))] +pub fn pyfn_parse_quantity(text: String) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::quantity::parse_quantity(&text)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::errors::map_dim)?; + Ok(crate::generated::types::PyQuantity { inner: __v }) +} + +/// Converts a value between two named units. +/// +/// Errors: +/// +/// `DimError::UnknownUnit` for an unrecognised name, or +/// `DimError::Mismatch` if the two measure different things -- which +/// is the whole point of the function rather than an edge case. +/// +/// Rust: `units::quantity::unit_convert` +#[pyfunction] +#[pyo3(name = "unit_convert", signature = (value, from_, to))] +pub fn pyfn_unit_convert(value: f64, from_: String, to: String) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::quantity::unit_convert(value, &from_, &to)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::errors::map_dim)?; + Ok(__v) +} + +/// Formats a number with the SI prefix that brings it into `[1, 1000)`. +/// +/// Returns the scaled number and the prefix, so that a caller can put +/// the unit after it. Zero and anything non-finite are returned with no +/// prefix, there being no sensible one. +/// +/// Rust: `units::quantity::si_prefixes_format` +#[pyfunction] +#[pyo3(name = "si_prefixes_format", signature = (value))] +pub fn pyfn_si_prefixes_format(value: f64) -> PyResult<(f64, String)> { + let __r = crate::runtime::guard(|| rust_physics_engine::units::quantity::si_prefixes_format(value)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1.to_string())) +} + +/// The 2022 CODATA constants, as `(name, value, unit)`. +/// +/// Seven of these are exact by definition rather than measured: the +/// 2019 revision of the SI fixed `c`, `h`, `e`, `k`, `N_A`, the +/// caesium hyperfine frequency and the luminous efficacy, and defined +/// the kilogram, ampere, kelvin, mole and candela in terms of them. The +/// gravitational constant is not among them and remains the worst known +/// of the fundamental constants by a wide margin -- about one part in +/// forty thousand, against one part in `1e10` for the fine-structure +/// constant. +/// +/// Rust: `units::quantity::constants_codata` +#[pyfunction] +#[pyo3(name = "constants_codata", signature = ())] +pub fn pyfn_constants_codata<'py>(py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::units::quantity::constants_codata())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0.to_string(), __x.1, __x.2.to_string())).collect::>()) +} + +/// Looks a CODATA constant up by name. +/// +/// Rust: `units::quantity::codata` +#[pyfunction] +#[pyo3(name = "codata", signature = (name))] +pub fn pyfn_codata(name: String) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::units::quantity::codata(&name)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_parse_unit, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_parse_quantity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_unit_convert, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_si_prefixes_format, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_constants_codata, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_codata, m)?)?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_vector_calculus.rs b/bindings/python/src/generated/m_vector_calculus.rs new file mode 100644 index 0000000..62b1c0a --- /dev/null +++ b/bindings/python/src/generated/m_vector_calculus.rs @@ -0,0 +1,239 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Compute the gradient of a scalar field on a uniform 3-D grid. +/// +/// `field` is row-major with index mapping `i*ny*nz + j*nz + k`. +/// Returns a `Vec` of `(∂f/∂x, ∂f/∂y, ∂f/∂z)` tuples, one per grid point. +/// +/// Rust: `vector_calculus::gradient_3d` +#[pyfunction] +#[pyo3(name = "gradient_3d", signature = (field, nx, ny, nz, dx, dy, dz))] +pub fn pyfn_gradient_3d<'py>(py: Python<'py>, field: Vec, nx: usize, ny: usize, nz: usize, dx: f64, dy: f64, dz: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::vector_calculus::gradient_3d(&field, nx, ny, nz, dx, dy, dz))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, __x.2)).collect::>()) +} + +/// Compute the Laplacian (∇²f) of a scalar field on a uniform 3-D grid. +/// +/// Rust: `vector_calculus::laplacian_3d` +#[pyfunction] +#[pyo3(name = "laplacian_3d", signature = (field, nx, ny, nz, dx, dy, dz))] +pub fn pyfn_laplacian_3d<'py>(py: Python<'py>, field: Vec, nx: usize, ny: usize, nz: usize, dx: f64, dy: f64, dz: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::vector_calculus::laplacian_3d(&field, nx, ny, nz, dx, dy, dz))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Divergence of a vector field on a uniform 3-D grid. +/// +/// Each component (`fx`, `fy`, `fz`) is a flat array with the same index mapping +/// as scalar fields. +/// +/// Rust: `vector_calculus::divergence_3d` +#[pyfunction] +#[pyo3(name = "divergence_3d", signature = (fx, fy, fz, nx, ny, nz, dx, dy, dz))] +pub fn pyfn_divergence_3d<'py>(py: Python<'py>, fx: Vec, fy: Vec, fz: Vec, nx: usize, ny: usize, nz: usize, dx: f64, dy: f64, dz: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::vector_calculus::divergence_3d(&fx, &fy, &fz, nx, ny, nz, dx, dy, dz))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Curl of a vector field on a uniform 3-D grid. +/// +/// Returns `(curl_x, curl_y, curl_z)` as separate flat arrays. +/// +/// Rust: `vector_calculus::curl_3d` +#[pyfunction] +#[pyo3(name = "curl_3d", signature = (fx, fy, fz, nx, ny, nz, dx, dy, dz))] +pub fn pyfn_curl_3d<'py>(py: Python<'py>, fx: Vec, fy: Vec, fz: Vec, nx: usize, ny: usize, nz: usize, dx: f64, dy: f64, dz: f64) -> PyResult<(Vec, Vec, Vec)> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::vector_calculus::curl_3d(&fx, &fy, &fz, nx, ny, nz, dx, dy, dz))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// Gradient of a scalar field on a uniform 2-D grid. +/// +/// Index mapping: `i*ny + j`. Returns `(∂f/∂x, ∂f/∂y)` per grid point. +/// +/// Rust: `vector_calculus::gradient_2d` +#[pyfunction] +#[pyo3(name = "gradient_2d", signature = (field, nx, ny, dx, dy))] +pub fn pyfn_gradient_2d<'py>(py: Python<'py>, field: Vec, nx: usize, ny: usize, dx: f64, dy: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::vector_calculus::gradient_2d(&field, nx, ny, dx, dy))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) +} + +/// Laplacian of a scalar field on a uniform 2-D grid. +/// +/// Rust: `vector_calculus::laplacian_2d` +#[pyfunction] +#[pyo3(name = "laplacian_2d", signature = (field, nx, ny, dx, dy))] +pub fn pyfn_laplacian_2d<'py>(py: Python<'py>, field: Vec, nx: usize, ny: usize, dx: f64, dy: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::vector_calculus::laplacian_2d(&field, nx, ny, dx, dy))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Divergence of a 2-D vector field. +/// +/// Rust: `vector_calculus::divergence_2d` +#[pyfunction] +#[pyo3(name = "divergence_2d", signature = (fx, fy, nx, ny, dx, dy))] +pub fn pyfn_divergence_2d<'py>(py: Python<'py>, fx: Vec, fy: Vec, nx: usize, ny: usize, dx: f64, dy: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::vector_calculus::divergence_2d(&fx, &fy, nx, ny, dx, dy))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Scalar curl of a 2-D vector field: ∂Fy/∂x - ∂Fx/∂y. +/// +/// Rust: `vector_calculus::curl_2d` +#[pyfunction] +#[pyo3(name = "curl_2d", signature = (fx, fy, nx, ny, dx, dy))] +pub fn pyfn_curl_2d<'py>(py: Python<'py>, fx: Vec, fy: Vec, nx: usize, ny: usize, dx: f64, dy: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::vector_calculus::curl_2d(&fx, &fy, nx, ny, dx, dy))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Solve ∇²φ = ρ on a 2-D grid with zero (Dirichlet) boundary +/// conditions. +/// +/// The interior unknowns are assembled into a sparse SPD system +/// (−∇²φ = −ρ, 5-point stencil) and solved with Jacobi-preconditioned +/// conjugate gradient (`linalg::sparse::pcg_jacobi`); if CG does not +/// reach `tol` within `max_iter` iterations, the classic point-Jacobi +/// sweep is used as a fallback so the historical behavior (best-effort +/// answer, never an error) is preserved. +/// +/// Rust: `vector_calculus::poisson_jacobi_2d` +#[pyfunction] +#[pyo3(name = "poisson_jacobi_2d", signature = (rhs, nx, ny, dx, dy, max_iter, tol))] +pub fn pyfn_poisson_jacobi_2d<'py>(py: Python<'py>, rhs: Vec, nx: usize, ny: usize, dx: f64, dy: f64, max_iter: usize, tol: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::vector_calculus::poisson_jacobi_2d(&rhs, nx, ny, dx, dy, max_iter, tol))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Numerical line integral ∫F·dr along a piecewise-linear path in 3-D. +/// +/// `f` returns the vector field value (Fx, Fy, Fz) at a given point. +/// `path` is an ordered list of waypoints. The integral is evaluated at the +/// midpoint of each segment. +/// +/// Rust: `vector_calculus::line_integral` +#[pyfunction] +#[pyo3(name = "line_integral", signature = (f, path))] +pub fn pyfn_line_integral(f: pyo3::Py, path: Vec<(f64, f64, f64)>) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64, __a1: f64, __a2: f64| -> (f64, f64, f64) { __cb.call::<_, (f64, f64, f64)>((__a0, __a1, __a2), (f64::NAN, f64::NAN, f64::NAN)) } }; + let path = path.into_iter().map(|__e| (__e.0, __e.1, __e.2)).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::vector_calculus::line_integral(&f, &path)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Flux integral ∫F·n̂ ds along a 2-D curve. +/// +/// The outward normal is computed by rotating each segment's tangent 90 degrees +/// clockwise: tangent (tx, ty) -> normal (ty, -tx). The integral is evaluated +/// at the midpoint of each segment. +/// +/// Rust: `vector_calculus::flux_integral_2d` +#[pyfunction] +#[pyo3(name = "flux_integral_2d", signature = (fn_field, path))] +pub fn pyfn_flux_integral_2d(fn_field: pyo3::Py, path: Vec<(f64, f64)>) -> PyResult { + let __cb_fn_field = std::rc::Rc::new(crate::runtime::Callback::new(fn_field)); + let fn_field = { let __cb = __cb_fn_field.clone(); move |__a0: f64, __a1: f64| -> (f64, f64) { __cb.call::<_, (f64, f64)>((__a0, __a1), (f64::NAN, f64::NAN)) } }; + let path = path.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::vector_calculus::flux_integral_2d(&fn_field, &path)); + crate::runtime::callback::check(&[&__cb_fn_field], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Finite-difference gradient of a scalar function at a point. +/// +/// Rust: `vector_calculus::numerical_gradient` +#[pyfunction] +#[pyo3(name = "numerical_gradient", signature = (f, x, y, z, h))] +pub fn pyfn_numerical_gradient(f: pyo3::Py, x: f64, y: f64, z: f64, h: f64) -> PyResult<(f64, f64, f64)> { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64, __a1: f64, __a2: f64| -> f64 { __cb.call::<_, f64>((__a0, __a1, __a2), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::vector_calculus::numerical_gradient(&f, x, y, z, h)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) +} + +/// Finite-difference Laplacian of a scalar function at a point. +/// +/// Rust: `vector_calculus::numerical_laplacian` +#[pyfunction] +#[pyo3(name = "numerical_laplacian", signature = (f, x, y, z, h))] +pub fn pyfn_numerical_laplacian(f: pyo3::Py, x: f64, y: f64, z: f64, h: f64) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64, __a1: f64, __a2: f64| -> f64 { __cb.call::<_, f64>((__a0, __a1, __a2), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::vector_calculus::numerical_laplacian(&f, x, y, z, h)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Finite-difference divergence of a vector field at a point. +/// +/// Each component of the field is given as a separate function. +/// +/// Rust: `vector_calculus::numerical_divergence` +#[pyfunction] +#[pyo3(name = "numerical_divergence", signature = (fx, fy, fz, x, y, z, h))] +pub fn pyfn_numerical_divergence(fx: pyo3::Py, fy: pyo3::Py, fz: pyo3::Py, x: f64, y: f64, z: f64, h: f64) -> PyResult { + let __cb_fx = std::rc::Rc::new(crate::runtime::Callback::new(fx)); + let fx = { let __cb = __cb_fx.clone(); move |__a0: f64, __a1: f64, __a2: f64| -> f64 { __cb.call::<_, f64>((__a0, __a1, __a2), f64::NAN) } }; + let __cb_fy = std::rc::Rc::new(crate::runtime::Callback::new(fy)); + let fy = { let __cb = __cb_fy.clone(); move |__a0: f64, __a1: f64, __a2: f64| -> f64 { __cb.call::<_, f64>((__a0, __a1, __a2), f64::NAN) } }; + let __cb_fz = std::rc::Rc::new(crate::runtime::Callback::new(fz)); + let fz = { let __cb = __cb_fz.clone(); move |__a0: f64, __a1: f64, __a2: f64| -> f64 { __cb.call::<_, f64>((__a0, __a1, __a2), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::vector_calculus::numerical_divergence(&fx, &fy, &fz, x, y, z, h)); + crate::runtime::callback::check(&[&__cb_fx, &__cb_fy, &__cb_fz], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_gradient_3d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_laplacian_3d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_divergence_3d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_curl_3d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_gradient_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_laplacian_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_divergence_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_curl_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_poisson_jacobi_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_line_integral, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_flux_integral_2d, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_numerical_gradient, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_numerical_laplacian, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_numerical_divergence, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/m_waves.rs b/bindings/python/src/generated/m_waves.rs new file mode 100644 index 0000000..d128656 --- /dev/null +++ b/bindings/python/src/generated/m_waves.rs @@ -0,0 +1,607 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; +use pyo3::types::PyModule; + + +/// Wave speed: v = f * λ +/// +/// Rust: `waves::wave_speed` +#[pyfunction] +#[pyo3(name = "wave_speed", signature = (frequency, wavelength))] +pub fn pyfn_wave_speed(frequency: f64, wavelength: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::wave_speed(frequency, wavelength)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Wavelength from speed and frequency: λ = v / f +/// +/// Rust: `waves::wavelength` +#[pyfunction] +#[pyo3(name = "wavelength", signature = (speed, frequency))] +pub fn pyfn_wavelength(speed: f64, frequency: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::wavelength(speed, frequency)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Frequency from speed and wavelength: f = v / λ +/// +/// Rust: `waves::frequency` +#[pyfunction] +#[pyo3(name = "frequency", signature = (speed, wavelength))] +pub fn pyfn_frequency(speed: f64, wavelength: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::frequency(speed, wavelength)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Period: T = 1 / f +/// +/// Rust: `waves::period` +#[pyfunction] +#[pyo3(name = "period", signature = (frequency))] +pub fn pyfn_period(frequency: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::period(frequency)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Angular frequency: ω = 2πf +/// +/// Rust: `waves::angular_frequency` +#[pyfunction] +#[pyo3(name = "angular_frequency", signature = (frequency))] +pub fn pyfn_angular_frequency(frequency: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::angular_frequency(frequency)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Wave number: k = 2π / λ +/// +/// Rust: `waves::wave_number` +#[pyfunction] +#[pyo3(name = "wave_number", signature = (wavelength))] +pub fn pyfn_wave_number(wavelength: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::wave_number(wavelength)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Transverse wave displacement: y(x,t) = A * sin(kx - ωt + φ) +/// +/// Rust: `waves::wave_displacement` +#[pyfunction] +#[pyo3(name = "wave_displacement", signature = (amplitude, wave_number, x, angular_freq, t, phase))] +pub fn pyfn_wave_displacement(amplitude: f64, wave_number: f64, x: f64, angular_freq: f64, t: f64, phase: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::wave_displacement(amplitude, wave_number, x, angular_freq, t, phase)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Energy of a wave (proportional): E ∝ A^2 * f^2 +/// Returns the energy for a given amplitude and frequency (with a constant factor). +/// +/// Rust: `waves::wave_energy_density` +#[pyfunction] +#[pyo3(name = "wave_energy_density", signature = (amplitude, frequency, linear_density))] +pub fn pyfn_wave_energy_density(amplitude: f64, frequency: f64, linear_density: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::wave_energy_density(amplitude, frequency, linear_density)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Intensity of a wave: I = P / A (power per unit area) +/// +/// Rust: `waves::wave_intensity` +#[pyfunction] +#[pyo3(name = "wave_intensity", signature = (power, area))] +pub fn pyfn_wave_intensity(power: f64, area: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::wave_intensity(power, area)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Intensity falls off with distance (spherical wave): I = P / (4πr^2) +/// +/// Rust: `waves::spherical_wave_intensity` +#[pyfunction] +#[pyo3(name = "spherical_wave_intensity", signature = (power, distance))] +pub fn pyfn_spherical_wave_intensity(power: f64, distance: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::spherical_wave_intensity(power, distance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Decibel level: β = 10 * log10(I / I_0) +/// +/// Rust: `waves::decibel_level` +#[pyfunction] +#[pyo3(name = "decibel_level", signature = (intensity, reference_intensity))] +pub fn pyfn_decibel_level(intensity: f64, reference_intensity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::decibel_level(intensity, reference_intensity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Intensity from decibel level: I = I_0 * 10^(β/10) +/// +/// Rust: `waves::intensity_from_decibels` +#[pyfunction] +#[pyo3(name = "intensity_from_decibels", signature = (decibels, reference_intensity))] +pub fn pyfn_intensity_from_decibels(decibels: f64, reference_intensity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::intensity_from_decibels(decibels, reference_intensity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Doppler effect (sound): f' = f * (v + v_observer) / (v + v_source) +/// Convention: positive v_observer = observer moving toward source, +/// positive v_source = source moving away from observer. +/// +/// Rust: `waves::doppler_frequency` +#[pyfunction] +#[pyo3(name = "doppler_frequency", signature = (source_freq, wave_speed, observer_velocity, source_velocity))] +pub fn pyfn_doppler_frequency(source_freq: f64, wave_speed: f64, observer_velocity: f64, source_velocity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::doppler_frequency(source_freq, wave_speed, observer_velocity, source_velocity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Relativistic Doppler effect: f' = f * sqrt((1 + β) / (1 - β)) +/// where β = v/c, positive β = approaching. +/// +/// Rust: `waves::relativistic_doppler` +#[pyfunction] +#[pyo3(name = "relativistic_doppler", signature = (source_freq, beta))] +pub fn pyfn_relativistic_doppler(source_freq: f64, beta: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::relativistic_doppler(source_freq, beta)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Mach cone half-angle: sin(θ) = v_sound / v_object = 1/M +/// +/// Rust: `waves::mach_cone_angle` +#[pyfunction] +#[pyo3(name = "mach_cone_angle", signature = (mach))] +pub fn pyfn_mach_cone_angle(mach: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::mach_cone_angle(mach)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Frequencies of standing waves on a string fixed at both ends: +/// f_n = n * v / (2L) +/// +/// Rust: `waves::standing_wave_frequency` +#[pyfunction] +#[pyo3(name = "standing_wave_frequency", signature = (harmonic, wave_speed, length))] +pub fn pyfn_standing_wave_frequency(harmonic: u32, wave_speed: f64, length: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::standing_wave_frequency(harmonic, wave_speed, length)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Fundamental frequency of a string: f = (1/(2L)) * sqrt(T/μ) +/// T = tension, μ = linear mass density +/// +/// Rust: `waves::string_fundamental` +#[pyfunction] +#[pyo3(name = "string_fundamental", signature = (length, tension, linear_density))] +pub fn pyfn_string_fundamental(length: f64, tension: f64, linear_density: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::string_fundamental(length, tension, linear_density)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Standing waves in an open pipe: f_n = n * v / (2L) (all harmonics) +/// +/// Rust: `waves::open_pipe_frequency` +#[pyfunction] +#[pyo3(name = "open_pipe_frequency", signature = (harmonic, sound_speed, length))] +pub fn pyfn_open_pipe_frequency(harmonic: u32, sound_speed: f64, length: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::open_pipe_frequency(harmonic, sound_speed, length)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Standing waves in a closed pipe: f_n = n * v / (4L) (odd harmonics only) +/// +/// Rust: `waves::closed_pipe_frequency` +#[pyfunction] +#[pyo3(name = "closed_pipe_frequency", signature = (odd_harmonic, sound_speed, length))] +pub fn pyfn_closed_pipe_frequency(odd_harmonic: u32, sound_speed: f64, length: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::closed_pipe_frequency(odd_harmonic, sound_speed, length)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Beat frequency: f_beat = |f1 - f2| +/// +/// Rust: `waves::beat_frequency` +#[pyfunction] +#[pyo3(name = "beat_frequency", signature = (f1, f2))] +pub fn pyfn_beat_frequency(f1: f64, f2: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::beat_frequency(f1, f2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Superposition of two waves at a point (same frequency): +/// A_resultant = sqrt(A1^2 + A2^2 + 2*A1*A2*cos(Δφ)) +/// +/// Rust: `waves::superposition_amplitude` +#[pyfunction] +#[pyo3(name = "superposition_amplitude", signature = (a1, a2, phase_diff))] +pub fn pyfn_superposition_amplitude(a1: f64, a2: f64, phase_diff: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::superposition_amplitude(a1, a2, phase_diff)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Speed of sound in an ideal gas: v = sqrt(γ * R * T / M) +/// γ = heat capacity ratio, M = molar mass +/// +/// Rust: `waves::speed_of_sound_gas` +#[pyfunction] +#[pyo3(name = "speed_of_sound_gas", signature = (gamma, temperature, molar_mass))] +pub fn pyfn_speed_of_sound_gas(gamma: f64, temperature: f64, molar_mass: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::speed_of_sound_gas(gamma, temperature, molar_mass)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Wave speed on a string: v = sqrt(T / μ) +/// +/// Rust: `waves::wave_speed_string` +#[pyfunction] +#[pyo3(name = "wave_speed_string", signature = (tension, linear_density))] +pub fn pyfn_wave_speed_string(tension: f64, linear_density: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::wave_speed_string(tension, linear_density)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Phase velocity: v_p = ω/k +/// +/// Rust: `waves::phase_velocity` +#[pyfunction] +#[pyo3(name = "phase_velocity", signature = (angular_freq, wave_number))] +pub fn pyfn_phase_velocity(angular_freq: f64, wave_number: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::phase_velocity(angular_freq, wave_number)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Group velocity: v_g = dω/dk +/// +/// Rust: `waves::group_velocity` +#[pyfunction] +#[pyo3(name = "group_velocity", signature = (d_omega, d_k))] +pub fn pyfn_group_velocity(d_omega: f64, d_k: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::group_velocity(d_omega, d_k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Acoustic impedance: Z = ρv +/// +/// Rust: `waves::wave_impedance` +#[pyfunction] +#[pyo3(name = "wave_impedance", signature = (density, wave_speed))] +pub fn pyfn_wave_impedance(density: f64, wave_speed: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::wave_impedance(density, wave_speed)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Amplitude reflection coefficient: R = (Z2 - Z1)/(Z2 + Z1) +/// +/// Rust: `waves::reflection_coefficient` +#[pyfunction] +#[pyo3(name = "reflection_coefficient", signature = (z1, z2))] +pub fn pyfn_reflection_coefficient(z1: f64, z2: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::reflection_coefficient(z1, z2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Amplitude transmission coefficient: T = 2Z2/(Z1 + Z2) +/// +/// Rust: `waves::transmission_coefficient` +#[pyfunction] +#[pyo3(name = "transmission_coefficient", signature = (z1, z2))] +pub fn pyfn_transmission_coefficient(z1: f64, z2: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::transmission_coefficient(z1, z2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Intensity reflection coefficient: R_I = ((Z2 - Z1)/(Z2 + Z1))² +/// +/// Rust: `waves::intensity_reflection` +#[pyfunction] +#[pyo3(name = "intensity_reflection", signature = (z1, z2))] +pub fn pyfn_intensity_reflection(z1: f64, z2: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::intensity_reflection(z1, z2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Intensity transmission coefficient: T_I = 4Z1Z2/(Z1 + Z2)² +/// +/// Rust: `waves::intensity_transmission` +#[pyfunction] +#[pyo3(name = "intensity_transmission", signature = (z1, z2))] +pub fn pyfn_intensity_transmission(z1: f64, z2: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::intensity_transmission(z1, z2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Attenuated amplitude: A = A₀ × e^(-αx) +/// +/// Rust: `waves::attenuated_amplitude` +#[pyfunction] +#[pyo3(name = "attenuated_amplitude", signature = (initial, attenuation_coeff, distance))] +pub fn pyfn_attenuated_amplitude(initial: f64, attenuation_coeff: f64, distance: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::attenuated_amplitude(initial, attenuation_coeff, distance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Absorption coefficient from dB/m: α = dB × ln(10)/20 +/// +/// Rust: `waves::absorption_coefficient_from_db` +#[pyfunction] +#[pyo3(name = "absorption_coefficient_from_db", signature = (db_per_meter))] +pub fn pyfn_absorption_coefficient_from_db(db_per_meter: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::absorption_coefficient_from_db(db_per_meter)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Penetration depth (skin depth): δ = 1/α +/// +/// Rust: `waves::penetration_depth` +#[pyfunction] +#[pyo3(name = "penetration_depth", signature = (attenuation_coeff))] +pub fn pyfn_penetration_depth(attenuation_coeff: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::penetration_depth(attenuation_coeff)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Sound pressure level: SPL = 20 × log10(p/p_ref) +/// +/// Rust: `waves::sound_pressure_level` +#[pyfunction] +#[pyo3(name = "sound_pressure_level", signature = (pressure, reference))] +pub fn pyfn_sound_pressure_level(pressure: f64, reference: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::sound_pressure_level(pressure, reference)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Acoustic power: P = p²A/Z +/// +/// Rust: `waves::acoustic_power` +#[pyfunction] +#[pyo3(name = "acoustic_power", signature = (pressure, area, impedance))] +pub fn pyfn_acoustic_power(pressure: f64, area: f64, impedance: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::acoustic_power(pressure, area, impedance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Resonant frequency of open tube: f = v/(2L) +/// +/// Rust: `waves::resonant_frequency_tube_open` +#[pyfunction] +#[pyo3(name = "resonant_frequency_tube_open", signature = (length, speed))] +pub fn pyfn_resonant_frequency_tube_open(length: f64, speed: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::resonant_frequency_tube_open(length, speed)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Resonant frequency of closed tube: f = v/(4L) +/// +/// Rust: `waves::resonant_frequency_tube_closed` +#[pyfunction] +#[pyo3(name = "resonant_frequency_tube_closed", signature = (length, speed))] +pub fn pyfn_resonant_frequency_tube_closed(length: f64, speed: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::resonant_frequency_tube_closed(length, speed)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Acoustic intensity from pressure: I = p²/Z +/// +/// Rust: `waves::acoustic_intensity_from_pressure` +#[pyfunction] +#[pyo3(name = "acoustic_intensity_from_pressure", signature = (pressure, impedance))] +pub fn pyfn_acoustic_intensity_from_pressure(pressure: f64, impedance: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::acoustic_intensity_from_pressure(pressure, impedance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Wavelength in a medium: λ = v/f +/// +/// Rust: `waves::wavelength_in_medium` +#[pyfunction] +#[pyo3(name = "wavelength_in_medium", signature = (frequency, speed_in_medium))] +pub fn pyfn_wavelength_in_medium(frequency: f64, speed_in_medium: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::wavelength_in_medium(frequency, speed_in_medium)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// P-wave speed: vp = √((K + 4G/3)/ρ) +/// +/// Rust: `waves::p_wave_speed` +#[pyfunction] +#[pyo3(name = "p_wave_speed", signature = (bulk_modulus, shear_modulus, density))] +pub fn pyfn_p_wave_speed(bulk_modulus: f64, shear_modulus: f64, density: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::p_wave_speed(bulk_modulus, shear_modulus, density)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// S-wave speed: vs = √(G/ρ) +/// +/// Rust: `waves::s_wave_speed` +#[pyfunction] +#[pyo3(name = "s_wave_speed", signature = (shear_modulus, density))] +pub fn pyfn_s_wave_speed(shear_modulus: f64, density: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::s_wave_speed(shear_modulus, density)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Rayleigh wave speed approximation: vR ≈ vs × (0.862 + 1.14ν)/(1 + ν) +/// +/// Rust: `waves::rayleigh_wave_speed` +#[pyfunction] +#[pyo3(name = "rayleigh_wave_speed", signature = (shear_speed, poisson_ratio))] +pub fn pyfn_rayleigh_wave_speed(shear_speed: f64, poisson_ratio: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::rayleigh_wave_speed(shear_speed, poisson_ratio)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Love wave speed range: between vs_layer and vs_halfspace +/// +/// Rust: `waves::love_wave_speed_range` +#[pyfunction] +#[pyo3(name = "love_wave_speed_range", signature = (shear_speed_layer, shear_speed_halfspace))] +pub fn pyfn_love_wave_speed_range(shear_speed_layer: f64, shear_speed_halfspace: f64) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::love_wave_speed_range(shear_speed_layer, shear_speed_halfspace)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) +} + +/// Constructive interference path difference: Δ = mλ +/// +/// Rust: `waves::path_difference_constructive` +#[pyfunction] +#[pyo3(name = "path_difference_constructive", signature = (order, wavelength))] +pub fn pyfn_path_difference_constructive(order: i32, wavelength: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::path_difference_constructive(order, wavelength)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Destructive interference path difference: Δ = (m + 0.5)λ +/// +/// Rust: `waves::path_difference_destructive` +#[pyfunction] +#[pyo3(name = "path_difference_destructive", signature = (order, wavelength))] +pub fn pyfn_path_difference_destructive(order: i32, wavelength: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::path_difference_destructive(order, wavelength)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Fraunhofer single-slit intensity: I/I₀ = (sin(β)/β)² where β = πa sin(θ)/λ +/// Returns 1.0 at θ = 0 (central maximum). +/// +/// Rust: `waves::fraunhofer_single_slit_intensity` +#[pyfunction] +#[pyo3(name = "fraunhofer_single_slit_intensity", signature = (angle, slit_width, wavelength))] +pub fn pyfn_fraunhofer_single_slit_intensity(angle: f64, slit_width: f64, wavelength: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::fraunhofer_single_slit_intensity(angle, slit_width, wavelength)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Airy disk radius: r = 1.22λf/D +/// +/// Rust: `waves::airy_disk_radius` +#[pyfunction] +#[pyo3(name = "airy_disk_radius", signature = (wavelength, focal_length, aperture))] +pub fn pyfn_airy_disk_radius(wavelength: f64, focal_length: f64, aperture: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::airy_disk_radius(wavelength, focal_length, aperture)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Fresnel number: F = a²/(λL) +/// +/// Rust: `waves::fresnel_number` +#[pyfunction] +#[pyo3(name = "fresnel_number", signature = (aperture, distance, wavelength))] +pub fn pyfn_fresnel_number(aperture: f64, distance: f64, wavelength: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::waves::fresnel_number(aperture, distance, wavelength)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) +} + +/// Registers this module's contents. +pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = (py, m); + m.add_function(wrap_pyfunction!(pyfn_wave_speed, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wavelength, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_frequency, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_period, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_angular_frequency, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wave_number, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wave_displacement, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wave_energy_density, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wave_intensity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_spherical_wave_intensity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_decibel_level, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_intensity_from_decibels, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_doppler_frequency, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_relativistic_doppler, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_mach_cone_angle, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_standing_wave_frequency, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_string_fundamental, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_open_pipe_frequency, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_closed_pipe_frequency, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_beat_frequency, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_superposition_amplitude, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_speed_of_sound_gas, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wave_speed_string, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_phase_velocity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_group_velocity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wave_impedance, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_reflection_coefficient, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_transmission_coefficient, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_intensity_reflection, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_intensity_transmission, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_attenuated_amplitude, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_absorption_coefficient_from_db, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_penetration_depth, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_sound_pressure_level, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_acoustic_power, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_resonant_frequency_tube_open, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_resonant_frequency_tube_closed, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_acoustic_intensity_from_pressure, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_wavelength_in_medium, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_p_wave_speed, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_s_wave_speed, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_rayleigh_wave_speed, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_love_wave_speed_range, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_path_difference_constructive, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_path_difference_destructive, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fraunhofer_single_slit_intensity, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_airy_disk_radius, m)?)?; + m.add_function(wrap_pyfunction!(pyfn_fresnel_number, m)?)?; + Ok(()) +} diff --git a/bindings/python/src/generated/mod.rs b/bindings/python/src/generated/mod.rs new file mode 100644 index 0000000..d708757 --- /dev/null +++ b/bindings/python/src/generated/mod.rs @@ -0,0 +1,3686 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + +use pyo3::prelude::*; +use pyo3::types::PyModule; +use std::collections::HashMap; + +pub mod types; +pub mod m_acoustics; +pub mod m_astrophysics; +pub mod m_atmosphere; +pub mod m_audio; +pub mod m_biophysics; +pub mod m_cfd; +pub mod m_chemistry; +pub mod m_classical; +pub mod m_codes; +pub mod m_color_science; +pub mod m_continuum_mechanics; +pub mod m_control_systems; +pub mod m_core; +pub mod m_curves; +pub mod m_discrete; +pub mod m_dsp; +pub mod m_electromagnetism; +pub mod m_electronics; +pub mod m_exact; +pub mod m_fem; +pub mod m_fields; +pub mod m_finance; +pub mod m_fluid_instabilities; +pub mod m_fluids; +pub mod m_fractals; +pub mod m_general_relativity; +pub mod m_geometry; +pub mod m_geophysics; +pub mod m_graph; +pub mod m_gravitation; +pub mod m_information_theory; +pub mod m_learn; +pub mod m_linalg; +pub mod m_magnetohydrodynamics; +pub mod m_manifold; +pub mod m_materials; +pub mod m_math; +pub mod m_mesh; +pub mod m_monte_carlo; +pub mod m_neutronics; +pub mod m_nonlinear; +pub mod m_nuclear; +pub mod m_numerical; +pub mod m_optics; +pub mod m_optimization; +pub mod m_particle_physics; +pub mod m_patterns; +pub mod m_photonics; +pub mod m_plasma; +pub mod m_propulsion; +pub mod m_quantum; +pub mod m_quaternion; +pub mod m_radiation; +pub mod m_relativity; +pub mod m_resonance; +pub mod m_rf; +pub mod m_signal_processing; +pub mod m_sim; +pub mod m_solid_mechanics; +pub mod m_spatial; +pub mod m_special; +pub mod m_statistical_mechanics; +pub mod m_statistics; +pub mod m_stochastic; +pub mod m_thermodynamics; +pub mod m_transforms; +pub mod m_trigonometry; +pub mod m_units; +pub mod m_vector_calculus; +pub mod m_waves; +pub mod m_astrophysics__collisions; +pub mod m_astrophysics__coords; +pub mod m_astrophysics__gravitational_waves; +pub mod m_astrophysics__habitable_zone; +pub mod m_astrophysics__kepler; +pub mod m_astrophysics__lagrange; +pub mod m_astrophysics__lambert; +pub mod m_astrophysics__magnetosphere; +pub mod m_astrophysics__maneuvers; +pub mod m_astrophysics__nbody; +pub mod m_astrophysics__orbital_elements; +pub mod m_astrophysics__tidal; +pub mod m_astrophysics__time_systems; +pub mod m_audio__analysis; +pub mod m_audio__effects; +pub mod m_audio__envelope; +pub mod m_audio__oscillators; +pub mod m_audio__physical; +pub mod m_audio__spatial; +pub mod m_audio__synthesis; +pub mod m_audio__tuning; +pub mod m_audio__vocoder; +pub mod m_audio__wav; +pub mod m_biophysics__epidemiology; +pub mod m_biophysics__neuro; +pub mod m_biophysics__phylo; +pub mod m_biophysics__population; +pub mod m_biophysics__seq_align; +pub mod m_cfd__advection; +pub mod m_cfd__boundary_layer; +pub mod m_cfd__grid; +pub mod m_cfd__lbm; +pub mod m_cfd__level_set; +pub mod m_cfd__multiphase; +pub mod m_cfd__porous; +pub mod m_cfd__potential_flow; +pub mod m_cfd__riemann; +pub mod m_cfd__shallow_water; +pub mod m_cfd__sph; +pub mod m_cfd__stable_fluids; +pub mod m_cfd__turbulence; +pub mod m_cfd__vortex; +pub mod m_codes__block; +pub mod m_codes__checksum; +pub mod m_codes__compression; +pub mod m_codes__convolutional; +pub mod m_codes__crypto_math; +pub mod m_codes__reed_solomon; +pub mod m_control_systems__kalman; +pub mod m_core__compensated; +pub mod m_core__dual; +pub mod m_core__interval; +pub mod m_discrete__combinatorics; +pub mod m_discrete__disjoint_set; +pub mod m_discrete__number_theory; +pub mod m_discrete__partitions; +pub mod m_discrete__primes; +pub mod m_discrete__sequences; +pub mod m_dsp__fir; +pub mod m_dsp__iir; +pub mod m_dsp__phase; +pub mod m_dsp__resample; +pub mod m_dsp__windows; +pub mod m_exact__bigfloat; +pub mod m_exact__bigint; +pub mod m_exact__contfrac; +pub mod m_exact__polynomial; +pub mod m_exact__rational; +pub mod m_exact__symbolic; +pub mod m_fem__fdtd; +pub mod m_fem__fem1d; +pub mod m_fem__fem2d; +pub mod m_fem__spectral_pde; +pub mod m_finance__options; +pub mod m_finance__portfolio; +pub mod m_finance__rates; +pub mod m_finance__risk; +pub mod m_fractals__attractors; +pub mod m_fractals__automata; +pub mod m_fractals__escape_time; +pub mod m_fractals__ifs; +pub mod m_fractals__lsystem; +pub mod m_fractals__noise; +pub mod m_geometry__delaunay; +pub mod m_geometry__geodesy; +pub mod m_geometry__hull; +pub mod m_geometry__mesh; +pub mod m_graph__coloring; +pub mod m_graph__core; +pub mod m_graph__flow; +pub mod m_graph__layout; +pub mod m_graph__matching; +pub mod m_graph__paths; +pub mod m_graph__spectral; +pub mod m_learn__cluster; +pub mod m_learn__gp; +pub mod m_learn__nn; +pub mod m_learn__tree; +pub mod m_linalg__cholesky; +pub mod m_linalg__eigen; +pub mod m_linalg__lu; +pub mod m_linalg__matrix; +pub mod m_linalg__qr; +pub mod m_linalg__sparse; +pub mod m_linalg__svd; +pub mod m_linalg__tridiagonal; +pub mod m_manifold__clifford; +pub mod m_manifold__dec; +pub mod m_manifold__embedding; +pub mod m_manifold__geodesic; +pub mod m_manifold__hyperbolic; +pub mod m_manifold__lie; +pub mod m_manifold__metric; +pub mod m_manifold__polytope4; +pub mod m_manifold__spacetime; +pub mod m_manifold__spherical; +pub mod m_manifold__vecn; +pub mod m_materials__common; +pub mod m_materials__elements; +pub mod m_materials__fluids; +pub mod m_materials__gases; +pub mod m_math__constants; +pub mod m_mesh__analyze; +pub mod m_mesh__generate; +pub mod m_mesh__isosurface; +pub mod m_mesh__parameterize; +pub mod m_mesh__subdivide; +pub mod m_mesh__surfaces; +pub mod m_monte_carlo__quasi; +pub mod m_numerical__bvp; +pub mod m_numerical__integrate; +pub mod m_numerical__interpolate; +pub mod m_numerical__ode; +pub mod m_numerical__roots; +pub mod m_optimization__convex; +pub mod m_optimization__game_theory; +pub mod m_optimization__integer; +pub mod m_optimization__least_squares; +pub mod m_optimization__lp; +pub mod m_optimization__metaheuristics; +pub mod m_optimization__network; +pub mod m_patterns__aperiodic; +pub mod m_patterns__knots; +pub mod m_patterns__packing; +pub mod m_patterns__phyllotaxis; +pub mod m_patterns__polygon_ops; +pub mod m_patterns__polyhedra; +pub mod m_patterns__sampling; +pub mod m_patterns__space_filling; +pub mod m_patterns__symmetry; +pub mod m_patterns__tilings; +pub mod m_quantum__algorithms; +pub mod m_quantum__circuit; +pub mod m_quantum__schrodinger; +pub mod m_quantum__solid_state; +pub mod m_quantum__spin; +pub mod m_quantum__wavefunction; +pub mod m_resonance__cavity; +pub mod m_resonance__coupled; +pub mod m_resonance__nonlinear; +pub mod m_resonance__oscillator; +pub mod m_resonance__structural; +pub mod m_sim__cloth_sim; +pub mod m_sim__em_sim; +pub mod m_sim__fluid_sim; +pub mod m_sim__heat_sim; +pub mod m_sim__rigid_body; +pub mod m_sim__wave_sim; +pub mod m_spatial__bvh; +pub mod m_spatial__contain; +pub mod m_spatial__distance; +pub mod m_spatial__frame; +pub mod m_spatial__intersect; +pub mod m_spatial__kdtree; +pub mod m_spatial__mat4; +pub mod m_spatial__octree; +pub mod m_spatial__primitives; +pub mod m_spatial__projective; +pub mod m_spatial__sdf; +pub mod m_spatial__transform2d; +pub mod m_special__bessel; +pub mod m_special__beta; +pub mod m_special__elliptic; +pub mod m_special__erf; +pub mod m_special__expint; +pub mod m_special__gamma; +pub mod m_special__legendre; +pub mod m_statistical_mechanics__ising; +pub mod m_statistical_mechanics__kinetics; +pub mod m_statistical_mechanics__lattice_models; +pub mod m_statistical_mechanics__md; +pub mod m_statistics__descriptive; +pub mod m_statistics__distributions; +pub mod m_statistics__fourier; +pub mod m_statistics__inference; +pub mod m_statistics__resampling; +pub mod m_stochastic__extreme; +pub mod m_stochastic__hmm; +pub mod m_stochastic__markov; +pub mod m_stochastic__point_process; +pub mod m_stochastic__queueing; +pub mod m_stochastic__rmt; +pub mod m_stochastic__sde; +pub mod m_stochastic__timeseries; +pub mod m_transforms__dct; +pub mod m_transforms__fft; +pub mod m_transforms__hilbert; +pub mod m_transforms__laplace; +pub mod m_transforms__radon; +pub mod m_transforms__spectral; +pub mod m_transforms__stft; +pub mod m_transforms__wavelet; +pub mod m_units__dimensional; +pub mod m_units__quantity; +pub mod m_fractals__attractors__presets; +pub mod m_fractals__automata__patterns; +pub mod m_fractals__ifs__presets; +pub mod m_fractals__lsystem__presets; +pub mod m_manifold__clifford__cga3; +pub mod m_manifold__clifford__cl3; +pub mod m_manifold__clifford__pga3; +pub mod m_manifold__clifford__sta; +pub mod m_numerical__ode__adaptive; +pub mod m_numerical__ode__explicit; +pub mod m_numerical__ode__implicit; +pub mod m_numerical__ode__symplectic; + +/// Builds the module tree under the extension module. +pub fn register<'py>(py: Python<'py>, root: &Bound<'py, PyModule>) -> PyResult<()> { + let mut mods: HashMap<&'static str, Bound<'py, PyModule>> = HashMap::new(); + { + let sub = PyModule::new(py, "numeria.acoustics")?; + sub.setattr("__doc__", "Room acoustics, psychoacoustic scales, and musical pitch. Reverberation by Sabine (`RT60 = 0.161 V / A`) and by Eyring, which differ in how they treat a very absorptive room: Sabine's formula is a diffuse-field approximation that never reaches zero however absorptive the surfaces, while Eyring's does. Room modes, critical distance and the mass-law transmission loss follow. The perceptual scales -- mel, bark, ERB, A-weighting, equal-loudness phon -- map physical frequency and level onto what a listener reports, and are fits to listening data rather than derivations. Musical pitch is here too: equal temperament, cents, and MIDI note conversion.")?; + m_acoustics::register(py, &sub)?; + root.add("acoustics", &sub)?; + mods.insert("acoustics", sub); + } + { + let sub = PyModule::new(py, "numeria.astrophysics")?; + sub.setattr("__doc__", "Astrodynamics and astrophysics. Orbits are the core. `kepler` solves Kepler's equation for elliptic, parabolic and hyperbolic orbits; `orbital_elements` converts between state vectors and Keplerian elements; `maneuvers` covers Hohmann and bi-elliptic transfers, plane changes, phasing and J2 secular rates; and `lambert` solves for the transfer orbit connecting two positions in a given time. `time_systems` and `coords` are the bookkeeping that makes those answers refer to anything real -- Julian dates, UT1/TAI/TT/TDB, sidereal time, and the equatorial, ecliptic, galactic, horizontal and ITRF frames with precession and nutation. Many-body gravity is handled by `nbody` with a leapfrog integrator and `octree` for Barnes-Hut O(N log N) forces. The remaining modules cover `tidal` forces and Roche limits, `lagrange` points, `gravitational_waves`, `magnetosphere` field-line tracing, `habitable_zone` boundaries, and `collisions` and impact cratering.")?; + m_astrophysics::register(py, &sub)?; + root.add("astrophysics", &sub)?; + mods.insert("astrophysics", sub); + } + { + let sub = PyModule::new(py, "numeria.atmosphere")?; + sub.setattr("__doc__", "The standard atmosphere, humidity, and near-surface wind. The barometric formula and the ISA lapse-rate model give pressure, temperature and density against altitude, plus the pressure and density altitudes an aircraft altimeter reports. Humidity is covered by the Magnus formulation for dew point and relative humidity. Wind includes the power-law shear profile, the wind power density that sets a turbine's available energy (`P/A = ½ρv³`, so a doubling of wind speed is eight times the power), the Beaufort scale, and the Coriolis parameter `f = 2Ω sin φ`.")?; + m_atmosphere::register(py, &sub)?; + root.add("atmosphere", &sub)?; + mods.insert("atmosphere", sub); + } + { + let sub = PyModule::new(py, "numeria.audio")?; + sub.setattr("__doc__", "Audio synthesis, analysis, effects, and I/O.")?; + m_audio::register(py, &sub)?; + root.add("audio", &sub)?; + mods.insert("audio", sub); + } + { + let sub = PyModule::new(py, "numeria.biophysics")?; + sub.setattr("__doc__", "Biophysics: the elementary membrane, transport and mechanics relations here, with the population-scale models in submodules. The roadmap calls this area `bio`; it lives under the existing `biophysics` module instead, so that there is one home for the subject rather than two.")?; + m_biophysics::register(py, &sub)?; + root.add("biophysics", &sub)?; + mods.insert("biophysics", sub); + } + { + let sub = PyModule::new(py, "numeria.cfd")?; + sub.setattr("__doc__", "Computational fluid dynamics: staggered grids, advection schemes, and (in later modules) incompressible solvers, shallow water, SPH, LBM, level sets, and turbulence models.")?; + m_cfd::register(py, &sub)?; + root.add("cfd", &sub)?; + mods.insert("cfd", sub); + } + { + let sub = PyModule::new(py, "numeria.chemistry")?; + sub.setattr("__doc__", "Reaction kinetics, chemical thermodynamics and electrochemistry. Rate laws for first- and second-order decay and the Arrhenius temperature dependence `k = A exp(−Eₐ/RT)`; the Gibbs free energy and its relation to the equilibrium constant, `ΔG° = −RT ln K`, with the van 't Hoff equation for how K moves with temperature; and Hess's law. Electrochemistry covers the Nernst equation, cell potentials and Faraday electrolysis. Solution chemistry covers pH and pOH, molarity, dilution and osmotic pressure.")?; + m_chemistry::register(py, &sub)?; + root.add("chemistry", &sub)?; + mods.insert("chemistry", sub); + } + { + let sub = PyModule::new(py, "numeria.classical")?; + sub.setattr("__doc__", "Newtonian mechanics: kinematics, dynamics, and the harmonic oscillator. Linear and rotational motion under constant acceleration, forces and momentum, work, energy and power, collisions in one dimension from perfectly elastic to perfectly inelastic, moments of inertia for the standard bodies, and circular motion. The oscillator section runs from the undamped period through the damped response -- damping ratio, logarithmic decrement, quality factor -- to the driven steady state and its resonance, and ends with the normal frequencies of two coupled oscillators. For the same problem solved numerically, or with more than two masses, see `resonance`.")?; + m_classical::register(py, &sub)?; + root.add("classical", &sub)?; + mods.insert("classical", sub); + } + { + let sub = PyModule::new(py, "numeria.codes")?; + sub.setattr("__doc__", "Error detection, error correction, compression, and the arithmetic cryptography is built on.")?; + m_codes::register(py, &sub)?; + root.add("codes", &sub)?; + mods.insert("codes", sub); + } + { + let sub = PyModule::new(py, "numeria.color_science")?; + sub.setattr("__doc__", "Colour: the standard spaces, the transforms between them, and perceptual measures. RGB to and from HSV, HSL and CIE XYZ, with the sRGB transfer function kept separate from the linear values -- the distinction that most colour bugs come from, since averaging or blending is only meaningful in linear light. Also spectral colour (wavelength to RGB), the Planckian locus (blackbody temperature to RGB, and the correlated colour temperature back), relative luminance and the WCAG contrast ratio.")?; + m_color_science::register(py, &sub)?; + root.add("color_science", &sub)?; + mods.insert("color_science", sub); + } + { + let sub = PyModule::new(py, "numeria.continuum_mechanics")?; + sub.setattr("__doc__", "Stress and strain as tensors, and the yield criteria built on them. The stress tensor with its invariants, principal stresses, and the split into hydrostatic and deviatoric parts -- the split that matters because metals yield on the deviatoric part alone, which is why the von Mises criterion ignores hydrostatic pressure entirely. Strain in both the small-strain and Green-Lagrange forms, the isotropic 3-D Hooke's law `σᵢⱼ = λ δᵢⱼ ε_kk + 2μ εᵢⱼ` and its compliance inverse, plane stress and plane strain, and the von Mises, Tresca, Mohr-Coulomb and Drucker-Prager yield criteria.")?; + m_continuum_mechanics::register(py, &sub)?; + root.add("continuum_mechanics", &sub)?; + mods.insert("continuum_mechanics", sub); + } + { + let sub = PyModule::new(py, "numeria.control_systems")?; + sub.setattr("__doc__", "Linear control: system response, stability margins and PID tuning. First- and second-order step and impulse responses in closed form, and the parameters that characterise them -- natural frequency, damping ratio, rise and settling time, percent overshoot, bandwidth. Stability is assessed through the gain and phase margins, which say how much extra gain or delay the loop tolerates before it oscillates. Steady-state error is given by system type. PID tuning uses the Ziegler-Nichols rules. They are a starting point rather than an answer: they were derived for a quarter-amplitude decay and typically give an aggressive loop that wants detuning.")?; + m_control_systems::register(py, &sub)?; + root.add("control_systems", &sub)?; + mods.insert("control_systems", sub); + } + { + let sub = PyModule::new(py, "numeria.core")?; + sub.setattr("__doc__", "Pure numeric building blocks: compensated summation, forward-mode automatic differentiation, and interval arithmetic.")?; + m_core::register(py, &sub)?; + root.add("core", &sub)?; + mods.insert("core", sub); + } + { + let sub = PyModule::new(py, "numeria.curves")?; + sub.setattr("__doc__", "Plane curves: conics, Bézier curves, and parametric families. The conic sections with their eccentricities, foci and the discriminant that classifies a general quadratic; quadratic and cubic Bézier curves in 2-D and 3-D; and parametric circles, ellipses, spirals, Lissajous figures, cycloids and helices. Arc length and signed curvature close the module. For subdivision surfaces and B-spline or NURBS patches see `mesh::surfaces`; for space curves with Frenet frames see `patterns::knots`.")?; + m_curves::register(py, &sub)?; + root.add("curves", &sub)?; + mods.insert("curves", sub); + } + { + let sub = PyModule::new(py, "numeria.discrete")?; + sub.setattr("__doc__", "Discrete mathematics: primes and factorization, elementary and analytic number theory, counting and enumeration, integer partitions, integer sequences, and union-find.")?; + m_discrete::register(py, &sub)?; + root.add("discrete", &sub)?; + mods.insert("discrete", sub); + } + { + let sub = PyModule::new(py, "numeria.dsp")?; + sub.setattr("__doc__", "Digital signal processing: window functions, FIR/IIR filter design, resampling, and phase utilities. The window generators and first-order RC filters that used to live in `signal_processing` moved here; the old paths re-export them.")?; + m_dsp::register(py, &sub)?; + root.add("dsp", &sub)?; + mods.insert("dsp", sub); + } + { + let sub = PyModule::new(py, "numeria.electromagnetism")?; + sub.setattr("__doc__", "Classical electromagnetism, from Coulomb's law to radiating dipoles. Electrostatics (Coulomb force and field, potential, Gauss flux, capacitance), magnetostatics (the force on a moving charge, the field of a wire, solenoid and toroid, dipole moments and torques), induction (Faraday and motional EMF, self and mutual inductance), and circuits from Ohm's law through RC transients to the AC steady state -- complex reactance, RLC impedance, resonance, quality factor and bandwidth, power factor, and transformer ratios. The wave section covers the free-space relations: propagation speed, the Poynting magnitude, energy density, the impedance of free space `Z₀ = μ₀c ≈ 376.73 Ω`, dipole radiation and the Larmor power.")?; + m_electromagnetism::register(py, &sub)?; + root.add("electromagnetism", &sub)?; + mods.insert("electromagnetism", sub); + } + { + let sub = PyModule::new(py, "numeria.electronics")?; + sub.setattr("__doc__", "Semiconductor device physics. Carrier statistics -- the intrinsic concentration, Fermi-Dirac occupancy, the thermal voltage `kT/q` -- and transport by drift and diffusion, linked by the Einstein relation `D/μ = kT/q`. Then the devices: the PN junction's built-in potential and depletion width, the Shockley diode equation, MOSFET drain current in the linear and saturation regimes, and solar cells through open-circuit voltage, fill factor and efficiency.")?; + m_electronics::register(py, &sub)?; + root.add("electronics", &sub)?; + mods.insert("electronics", sub); + } + { + let sub = PyModule::new(py, "numeria.exact")?; + sub.setattr("__doc__", "Exact arithmetic: arbitrary-precision integers, exact rationals, arbitrary-precision binary floating point, polynomials, and continued fractions.")?; + m_exact::register(py, &sub)?; + root.add("exact", &sub)?; + mods.insert("exact", sub); + } + { + let sub = PyModule::new(py, "numeria.fem")?; + sub.setattr("__doc__", "Finite elements, finite-difference time domain, and spectral methods. Three ways of turning a differential equation into a linear system, kept in one place because the interesting content is how they differ. A finite *difference* replaces the derivative with a difference quotient and asks the equation to hold at grid points. A finite *element* never differentiates the solution twice at all: it multiplies by a test function, integrates by parts, and asks the resulting integral identity to hold for every test function in a finite dimensional space. That change of question is what buys the method its two best properties -- it needs one less derivative of the solution to make sense, so a kink in the coefficient is admissible rather than fatal, and the answer it produces is the *best* approximation in the space with respect to the energy the operator defines. A spectral method is the same Galerkin idea with global smooth basis functions instead of local piecewise ones, which trades the sparsity of the matrix for a convergence rate limited only by the smoothness of the solution.")?; + m_fem::register(py, &sub)?; + root.add("fem", &sub)?; + mods.insert("fem", sub); + } + { + let sub = PyModule::new(py, "numeria.fields")?; + sub.setattr("__doc__", "Uniform-grid scalar fields. Minimal backfill of the Part 2 `ScalarField2`/`ScalarField3` types that later roadmap phases build on: row-major storage with grid spacing and bilinear/trilinear sampling.")?; + m_fields::register(py, &sub)?; + root.add("fields", &sub)?; + mods.insert("fields", sub); + } + { + let sub = PyModule::new(py, "numeria.finance")?; + sub.setattr("__doc__", "Quantitative finance: derivative pricing, interest rates, portfolio construction and risk measurement. # What the models are and are not Every pricing model here is a statement about a *hypothetical* market: continuous trading, no transaction costs, a known volatility, and a price process of a stated form. None of those is true. What the models buy is not a prediction of price but a consistent way to quote one instrument in terms of another -- which is why the quantity traders actually exchange is implied volatility, the number that makes the formula reproduce the market price, rather than the price itself. The tests in this module lean hard on that internal consistency. Put-call parity is a no-arbitrage identity independent of the model; a binomial tree must converge to Black-Scholes as its steps grow; Monte Carlo must agree with the closed form within its own standard error; and the Greeks must match finite differences of the price they are derivatives of. Those are checkable. Whether the model describes a real market is not, and nothing here claims it.")?; + m_finance::register(py, &sub)?; + root.add("finance", &sub)?; + mods.insert("finance", sub); + } + { + let sub = PyModule::new(py, "numeria.fluid_instabilities")?; + sub.setattr("__doc__", "When a fluid configuration stops being stable, and how fast it comes apart. Each entry here is a growth rate or a threshold. Rayleigh-Taylor for a heavy fluid over a light one, with the Atwood number and the most unstable wavelength; Kelvin-Helmholtz for a velocity shear; Rayleigh-Bénard convection through the Rayleigh number and its critical value; Plateau-Rayleigh for the breakup of a liquid column into drops; Richtmyer-Meshkov for a shock crossing an interface; and the Jeans criterion, which is the same instability applied to a self-gravitating gas cloud and so sets the mass at which a cloud collapses into a star. The Richardson number and its stability test cover stratified shear flow.")?; + m_fluid_instabilities::register(py, &sub)?; + root.add("fluid_instabilities", &sub)?; + mods.insert("fluid_instabilities", sub); + } + { + let sub = PyModule::new(py, "numeria.fluids")?; + sub.setattr("__doc__", "Fluid statics and single-phase flow. Statics: hydrostatic pressure, buoyancy and flotation, Pascal's principle. Inviscid flow: continuity, Bernoulli, Torricelli, the Venturi meter. Viscous flow: Stokes drag, the drag equation and terminal velocity, Poiseuille's law, the Darcy-Weisbach head loss. Compressible flow: Mach number, stagnation and isentropic ratios. Surface tension, capillary rise, vorticity, circulation and the Kutta-Joukowski lift round it out, along with the dimensionless groups that decide which regime you are in -- Reynolds, Froude, Weber, Bond, Peclet, Marangoni, Archimedes. These are the closed-form relations. For flow solved on a grid or with particles see `cfd`.")?; + m_fluids::register(py, &sub)?; + root.add("fluids", &sub)?; + mods.insert("fluids", sub); + } + { + let sub = PyModule::new(py, "numeria.fractals")?; + sub.setattr("__doc__", "Fractals: escape-time sets, attractors, automata and noise. The module root holds the classic escape-time sets computed directly -- Mandelbrot with smooth (continuous) iteration counts, Julia, burning ship, Newton fractals and the Sierpinski gasket. The submodules generalise each direction: `escape_time` for a generic iteration engine, `attractors` for chaotic flows and maps, `ifs` for iterated function systems and the chaos game, `lsystem` for Lindenmayer rewriting, `automata` for cellular automata and growth, and `noise` for Perlin, OpenSimplex2, Worley and fBm. For the dynamical-systems view -- Lyapunov exponents and bifurcation -- see `nonlinear`.")?; + m_fractals::register(py, &sub)?; + root.add("fractals", &sub)?; + mods.insert("fractals", sub); + } + { + let sub = PyModule::new(py, "numeria.general_relativity")?; + sub.setattr("__doc__", "General relativity: black holes and cosmology. The Schwarzschild solution -- the metric components, the horizon at `r_s = 2GM/c²`, proper time and gravitational redshift, the photon sphere at `1.5 r_s` and the innermost stable circular orbit at `3 r_s`, with the effective potential and the orbital energy and angular momentum that produce them. The Kerr solution adds rotation: the horizon, the ergosphere, the shifted ISCO, and the frame-dragging rate. Cosmology covers the Friedmann equation for the Hubble parameter, the critical density, redshift-distance relations, luminosity distance, lookback time, and the scale factor and CMB temperature at a given redshift. For four-vectors and curved-spacetime tensor machinery see `manifold::spacetime` and `manifold::metric`.")?; + m_general_relativity::register(py, &sub)?; + root.add("general_relativity", &sub)?; + mods.insert("general_relativity", sub); + } + { + let sub = PyModule::new(py, "numeria.geometry")?; + sub.setattr("__doc__", "Areas, volumes and surface areas of the standard shapes. Plane figures (circle, ellipse, triangle by base-height and by Heron's formula, regular polygon, sector, annulus) and solids (sphere, cylinder, cone, ellipsoid, torus, frustum, capsule), with perimeters and surface areas alongside. Closed-form mensuration only. For triangle solving see `trigonometry`, for curves and conics see `curves`, for polygon algorithms such as triangulation and offsetting see `patterns::polygon_ops`, and for meshes see `mesh`.")?; + m_geometry::register(py, &sub)?; + root.add("geometry", &sub)?; + mods.insert("geometry", sub); + } + { + let sub = PyModule::new(py, "numeria.geophysics")?; + sub.setattr("__doc__", "The solid Earth: gravity, seismology, and heat. Gravity surveying -- the latitude formula, the free-air and Bouguer corrections, the resulting anomaly, and Airy isostatic compensation. Seismology: P- and S-wave travel times, epicentral distance from the S−P lag, the Richter and moment magnitude scales, and seismic moment and energy. The moment magnitude is the one to use for large events, because Richter saturates. Heat flow: pressure and temperature with depth, the geothermal gradient, and geothermal power. Plate tectonics closes the module with Euler-pole plate velocities and the square-root-of-age law for seafloor depth.")?; + m_geophysics::register(py, &sub)?; + root.add("geophysics", &sub)?; + mods.insert("geophysics", sub); + } + { + let sub = PyModule::new(py, "numeria.graph")?; + sub.setattr("__doc__", "Graphs: representation and structure, shortest paths, network flow, matchings, spectral graph theory, colouring, and drawing.")?; + m_graph::register(py, &sub)?; + root.add("graph", &sub)?; + mods.insert("graph", sub); + } + { + let sub = PyModule::new(py, "numeria.gravitation")?; + sub.setattr("__doc__", "Newtonian gravity and two-body orbits. The inverse-square force and its potential energy, the field of a point mass, escape and circular orbital velocity, and Kepler's third law in both directions. The vis-viva equation `v² = μ(2/r − 1/a)` ties speed to position on any conic orbit, and the specific orbital energy fixes which conic it is. Also the Roche limit, the Hill sphere, the Schwarzschild radius and gravitational time dilation -- the last two are the points at which Newtonian gravity stops being enough; see `general_relativity`. For orbits propagated rather than characterised, and for transfers between them, see `astrophysics`.")?; + m_gravitation::register(py, &sub)?; + root.add("gravitation", &sub)?; + mods.insert("gravitation", sub); + } + { + let sub = PyModule::new(py, "numeria.information_theory")?; + sub.setattr("__doc__", "Shannon information: entropy, divergence, and channel capacity. Entropy in bits and in nats, the maximum-entropy bound for a given alphabet, and the entropy rate. Then the relations between two distributions: cross entropy, Kullback-Leibler divergence, and the Jensen-Shannon divergence -- which unlike KL is symmetric and bounded, which is why it is the one that behaves like a distance. Mutual information and conditional entropy connect the two, and the binary entropy function gives the capacity of a binary symmetric channel as `C = 1 − H₂(p)`. Fisher information and the Cramér-Rao bound cover the estimation side. For codes that approach these limits see `codes`.")?; + m_information_theory::register(py, &sub)?; + root.add("information_theory", &sub)?; + mods.insert("information_theory", sub); + } + { + let sub = PyModule::new(py, "numeria.learn")?; + sub.setattr("__doc__", "Learning algorithms, written to be read rather than to be fast. Every method here has a closed-form or exactly-checkable property attached to it, because that is what makes a learning algorithm testable at all. A network that trains to a plausible loss is not evidence of anything -- gradient descent will happily reduce the loss of a model whose gradients are wrong, just more slowly. What settles it is comparing the analytic gradient against a finite difference, comparing a linear model fitted by descent against the normal equations, or checking that a clustering agrees with itself under a relabelling.")?; + m_learn::register(py, &sub)?; + root.add("learn", &sub)?; + mods.insert("learn", sub); + } + { + let sub = PyModule::new(py, "numeria.linalg")?; + sub.setattr("__doc__", "Dense and sparse linear algebra. `Matrix` is the dense row-major `f64` type everything here operates on. The factorizations are chosen by what the matrix is: `lu` with partial pivoting for a general square solve, `cholesky` for symmetric positive-definite (half the work, and it fails cleanly if the matrix is not), `qr` by Householder reflections for least squares, `svd` by one-sided Jacobi for rank and pseudo-inverse, and `tridiagonal` for the Thomas algorithm in O(n). `eigen` provides the symmetric eigenproblem and general eigenvalues. `sparse` provides CSR storage with conjugate gradient and a Jacobi-preconditioned variant, for the large systems that the PDE solvers in `fem` produce. Note that `pcg_jacobi`'s tolerance is relative to the norm of the right-hand side, not absolute.")?; + m_linalg::register(py, &sub)?; + root.add("linalg", &sub)?; + mods.insert("linalg", sub); + } + { + let sub = PyModule::new(py, "numeria.magnetohydrodynamics")?; + sub.setattr("__doc__", "Magnetohydrodynamics: a conducting fluid and the field frozen into it. The dimensionless numbers first, because they decide the regime: magnetic Reynolds (advection against diffusion, and so whether the field is frozen in), Lundquist, Hartmann, and the plasma beta -- the ratio of thermal to magnetic pressure, which says whether the field or the gas is in charge. Wave speeds: Alfvén, and the slow and fast magnetosonic branches. Equilibria: pinch pressure balance, the Bennett condition, and the Grad-Shafranov beta limit. Reconnection is covered by the Sweet-Parker rate and the associated electric field.")?; + m_magnetohydrodynamics::register(py, &sub)?; + root.add("magnetohydrodynamics", &sub)?; + mods.insert("magnetohydrodynamics", sub); + } + { + let sub = PyModule::new(py, "numeria.manifold")?; + sub.setattr("__doc__", "Manifolds and higher-dimensional geometry: generic n-dimensional vectors and tensors, metric-driven curvature, and (in later modules) geodesics, Lie groups, constant-curvature spaces, polytopes, Clifford algebras, embeddings, discrete exterior calculus, and spacetimes.")?; + m_manifold::register(py, &sub)?; + root.add("manifold", &sub)?; + mods.insert("manifold", sub); + } + { + let sub = PyModule::new(py, "numeria.materials")?; + sub.setattr("__doc__", "Reference property tables. Lookup data rather than computation: `elements` carries all 118 elements with atomic mass, density, melting and boiling points and thermal and electrical conductivity; `common` carries engineering solids; `fluids` carries liquids with density, viscosity, surface tension and speed of sound; and `gases` carries molar mass, specific heat ratio and thermal conductivity. Values are room-temperature and one-atmosphere unless stated. They are reference figures for calculation, not a substitute for a datasheet on a specific alloy or grade.")?; + m_materials::register(py, &sub)?; + root.add("materials", &sub)?; + mods.insert("materials", sub); + } + { + let sub = PyModule::new(py, "numeria.math")?; + sub.setattr("__doc__", "Vectors and the crate's table of physical constants. `Vec2` and `Vec3` with the usual algebra -- addition, scaling, dot and cross products, norms, normalization, projection, reflection, rotation and interpolation. `constants` is the single table the rest of the crate refers back to, and it is deliberately one table: duplicate definitions elsewhere are re-exports of it, and a test enforces that they agree. The values fixed by the 2019 SI redefinition -- `C`, `H`, `HBAR`, `E_CHARGE`, `K_B`, `N_A` -- are exact by definition rather than measured. Constants that are products of others, such as `FARADAY = N_A · E_CHARGE`, are computed from their factors rather than transcribed, so they cannot disagree with them. For the 2022 CODATA set with units attached see `units::quantity::constants_codata`.")?; + m_math::register(py, &sub)?; + root.add("math", &sub)?; + mods.insert("math", sub); + } + { + let sub = PyModule::new(py, "numeria.mesh")?; + sub.setattr("__doc__", "Indexed triangle meshes: construction, mass properties, cleanup, spatial queries, and OBJ/STL interchange.")?; + m_mesh::register(py, &sub)?; + root.add("mesh", &sub)?; + mods.insert("mesh", sub); + } + { + let sub = PyModule::new(py, "numeria.monte_carlo")?; + sub.setattr("__doc__", "Monte Carlo methods and the random number generator behind them. Integration (plain, 2-D, and importance-sampled), random walks in one to three dimensions, the Wiener and Ornstein-Uhlenbeck processes, Langevin dynamics, and Metropolis-Hastings sampling with a worked Ising example. # A warning about `Rng` It is a linear congruential generator that returns its raw state, so the low bits have a short period: `next_u64() % m` for a power-of-two `m` cycles through a handful of values -- `% 2` gives 0,1,0,1 and `% 4` gives 0,3,2,1 forever. Use `Rng::below`, which takes the high bits, for any small-integer draw. It is adequate for simulation and testing and is not cryptographically secure.")?; + m_monte_carlo::register(py, &sub)?; + root.add("monte_carlo", &sub)?; + mods.insert("monte_carlo", sub); + } + { + let sub = PyModule::new(py, "numeria.neutronics")?; + sub.setattr("__doc__", "Reactor physics: criticality, neutron diffusion, and shielding. Criticality through the six-factor formula and `k_eff`, with reactivity and the reactor period. Neutron transport in diffusion theory: the diffusion coefficient and length, migration length, thermal utilization, and the flux in a slab. Cross sections and reaction rates convert between microscopic and macroscopic pictures, including the 1/v absorption law. Operations covers reactor power, burnup and decay heat. Shielding closes with attenuation, half- and tenth-value layers, and the buildup factor that corrects the exponential law for scattered photons -- the correction that matters, since ignoring it underestimates the dose behind a thick shield.")?; + m_neutronics::register(py, &sub)?; + root.add("neutronics", &sub)?; + mods.insert("neutronics", sub); + } + { + let sub = PyModule::new(py, "numeria.nonlinear")?; + sub.setattr("__doc__", "Chaos in low-dimensional systems. The logistic map and its period-doubling route to chaos, the Hénon map, and the Lorenz and Rössler flows given as derivative functions to hand to an integrator from `numerical`. Lyapunov exponents are the quantitative test: a positive exponent means nearby trajectories separate exponentially, which is what makes a system chaotic rather than merely complicated. Dimension estimators -- box counting and the correlation dimension -- measure the attractor that results. For strange attractors as drawable objects, escape-time fractals and cellular automata see `fractals`.")?; + m_nonlinear::register(py, &sub)?; + root.add("nonlinear", &sub)?; + mods.insert("nonlinear", sub); + } + { + let sub = PyModule::new(py, "numeria.nuclear")?; + sub.setattr("__doc__", "Radioactive decay, nuclear binding, and dosimetry. Exponential decay in its several parameterisations -- decay constant, half-life, mean lifetime -- and activity. Binding energy from the mass defect, binding energy per nucleon (the curve whose peak at iron-56 is why both fission and fusion release energy), and reaction Q-values. Nuclear size follows `R = R₀A^(1/3)`, giving a roughly constant nuclear density. Dosimetry covers absorbed and equivalent dose and the inverse-square falloff of intensity with distance. For reactor-scale neutron transport see `neutronics`.")?; + m_nuclear::register(py, &sub)?; + root.add("nuclear", &sub)?; + mods.insert("nuclear", sub); + } + { + let sub = PyModule::new(py, "numeria.numerical")?; + sub.setattr("__doc__", "Numerical methods: quadrature, root finding, ODE solvers, and interpolation. Submodules are re-exported so historical paths such as `numerical::trapezoid` keep working.")?; + m_numerical::register(py, &sub)?; + root.add("numerical", &sub)?; + mods.insert("numerical", sub); + } + { + let sub = PyModule::new(py, "numeria.optics")?; + sub.setattr("__doc__", "Geometric and wave optics. Refraction by Snell's law, the critical angle for total internal reflection, and the Brewster angle at which reflected light is fully polarized. Imaging through the thin-lens and mirror equations, magnification, lens power, combined focal lengths and the lensmaker's radius of curvature. Wave optics covers single-slit minima, double-slit maxima, the grating equation, thin-film interference, and the Rayleigh resolution criterion `θ = 1.22 λ/D`. Malus's law closes it. For Gaussian beams, fibre optics and ray transfer matrices see `photonics`.")?; + m_optics::register(py, &sub)?; + root.add("optics", &sub)?; + mods.insert("optics", sub); + } + { + let sub = PyModule::new(py, "numeria.optimization")?; + sub.setattr("__doc__", "Optimization: continuous, combinatorial, and strategic. The module root holds the scalar and unconstrained-gradient methods -- golden section and Brent for a bracketed minimum of one variable, then gradient descent with and without momentum, Adam, numerical gradients, and the regression and curve fitting built on them. The submodules take it further: `lp` for linear programming and duality, `integer` for branch-and-bound and dynamic programming, `network` for flows and scheduling, `convex` for L-BFGS, proximal methods and ADMM, `metaheuristics` for the derivative-free and population-based methods, `game_theory` for equilibria and cooperative solutions, and `least_squares` for Levenberg-Marquardt.")?; + m_optimization::register(py, &sub)?; + root.add("optimization", &sub)?; + mods.insert("optimization", sub); + } + { + let sub = PyModule::new(py, "numeria.particle_physics")?; + sub.setattr("__doc__", "Relativistic kinematics and scattering for particle collisions. Invariant mass -- the quantity every collider analysis is built on, because it is the same in every frame -- along with centre-of-mass energy for colliding and fixed-target geometries, and the Lorentz boost of energy and longitudinal momentum. The collider coordinates: rapidity, pseudorapidity and transverse momentum, chosen because rapidity differences are boost invariant along the beam. Scattering by the Rutherford cross section and the Breit-Wigner resonance shape, with the width-lifetime relation `Γτ = ħ` and branching ratios. Also the conservation-law checks -- charge, lepton number, baryon number -- that say whether a proposed reaction can happen at all.")?; + m_particle_physics::register(py, &sub)?; + root.add("particle_physics", &sub)?; + mods.insert("particle_physics", sub); + } + { + let sub = PyModule::new(py, "numeria.patterns")?; + sub.setattr("__doc__", "Geometric patterns: polygon algorithms, sampling distributions, phyllotaxis, tilings, symmetry groups, packings, space-filling curves, polyhedra, aperiodic tilings, and knots.")?; + m_patterns::register(py, &sub)?; + root.add("patterns", &sub)?; + mods.insert("patterns", sub); + } + { + let sub = PyModule::new(py, "numeria.photonics")?; + sub.setattr("__doc__", "Laser beams, optical fibre, and interferometry. Gaussian beam propagation: waist, Rayleigh range, radius and curvature against distance, divergence, the Gouy phase, and on-axis intensity. Fibre through the numerical aperture, acceptance angle, and the V-number that decides single- versus multi-mode operation, plus attenuation and dispersion broadening. Ray transfer (ABCD) matrices compose optical elements by matrix multiplication. Coherence length and time, fringe visibility, and the Fabry-Pérot transmission with its free spectral range close the module.")?; + m_photonics::register(py, &sub)?; + root.add("photonics", &sub)?; + mods.insert("photonics", sub); + } + { + let sub = PyModule::new(py, "numeria.plasma")?; + sub.setattr("__doc__", "Plasma parameters: the characteristic lengths, frequencies and speeds. The Debye length is where it starts -- the distance over which a plasma screens a charge, and therefore the scale below which \"plasma\" stops being the right description. The Debye number counts particles in that sphere, and a plasma is only collective if that number is large. Frequencies: electron and ion plasma frequencies, and the cyclotron frequencies in a magnetic field, with the associated Larmor radius. Speeds: thermal, ion-acoustic, Alfvén and magnetosonic. Plus magnetic pressure, plasma beta, the skin depth, the Coulomb logarithm and the collision frequency. For a conducting fluid treated as a continuum see `magnetohydrodynamics`.")?; + m_plasma::register(py, &sub)?; + root.add("plasma", &sub)?; + mods.insert("plasma", sub); + } + { + let sub = PyModule::new(py, "numeria.propulsion")?; + sub.setattr("__doc__", "Rocket propulsion and impulsive orbital transfers. The Tsiolkovsky equation `Δv = v_e ln(m₀/m_f)` and the specific impulse and mass ratio around it, thrust with and without the pressure-thrust term, staged Δv, and the gravity-turn loss that makes the ideal Δv an underestimate for a launch. Transfers: Hohmann Δv and time, the bi-elliptic alternative (which wins beyond a radius ratio of about 11.94), and plane changes. Nozzle design covers exit velocity, throat area and the area ratio for a given exit Mach number. For Lambert targeting, J2 effects and orbit propagation see `astrophysics`.")?; + m_propulsion::register(py, &sub)?; + root.add("propulsion", &sub)?; + mods.insert("propulsion", sub); + } + { + let sub = PyModule::new(py, "numeria.quantum")?; + sub.setattr("__doc__", "Quantum mechanics: the elementary relations here, with the wavefunction machinery and the Schrodinger solvers in submodules.")?; + m_quantum::register(py, &sub)?; + root.add("quantum", &sub)?; + mods.insert("quantum", sub); + } + { + let sub = PyModule::new(py, "numeria.quaternion")?; + sub.setattr("__doc__", "Unit quaternions for 3-D rotation. `Quaternion` with the full algebra -- Hamilton product, conjugate, inverse, norm and normalization -- and conversion to and from axis-angle, Euler angles and rotation matrices. Quaternions are used for orientation rather than Euler angles because they compose without gimbal lock and interpolate smoothly: `slerp` moves along the great circle at constant angular rate, and `nlerp` is the cheaper normalized-linear approximation to it. For the Lie-group view of the same object, and for rotations in four dimensions, see `manifold::lie`.")?; + m_quaternion::register(py, &sub)?; + root.add("quaternion", &sub)?; + mods.insert("quaternion", sub); + } + { + let sub = PyModule::new(py, "numeria.radiation")?; + sub.setattr("__doc__", "Thermal radiation and radiative transfer. The Stefan-Boltzmann law `j = σT⁴`, Wien's displacement of the spectral peak, and the colour and brightness temperatures that invert them. Transfer through an absorbing medium: optical depth, the Beer-Lambert law, and the photon mean free path. Radiation pressure for absorbing and reflecting surfaces. Surface exchange via Kirchhoff's law (emissivity equals absorptivity at equilibrium), view factors, and net radiative exchange between surfaces. For the Planck spectrum itself see `quantum`; for reactor and photon shielding see `neutronics`.")?; + m_radiation::register(py, &sub)?; + root.add("radiation", &sub)?; + mods.insert("radiation", sub); + } + { + let sub = PyModule::new(py, "numeria.relativity")?; + sub.setattr("__doc__", "Special relativity. The Lorentz factor and the kinematic consequences -- time dilation, length contraction, the velocity-addition law that keeps `c` a limit, and the Lorentz transformation of position and time. Dynamics: relativistic momentum and kinetic energy, total and rest energy, and the energy-momentum relation `E² = (pc)² + (mc²)²`. The relativistic Doppler shift for approaching and receding sources. Also proper time and the spacetime interval, whose sign classifies a separation as timelike, spacelike or null -- the invariant that replaces separate notions of distance and duration.")?; + m_relativity::register(py, &sub)?; + root.add("relativity", &sub)?; + mods.insert("relativity", sub); + } + { + let sub = PyModule::new(py, "numeria.resonance")?; + sub.setattr("__doc__", "Resonance and vibration: single and coupled oscillators, acoustic and electromagnetic cavities, nonlinear resonance, and structural dynamics.")?; + m_resonance::register(py, &sub)?; + root.add("resonance", &sub)?; + mods.insert("resonance", sub); + } + { + let sub = PyModule::new(py, "numeria.rf")?; + sub.setattr("__doc__", "Radio-frequency engineering: links, lines and noise. Link budgets built from free-space path loss, the Friis transmission equation, antenna gain and effective area, EIRP, beamwidth, directivity and fade margin. Transmission lines: characteristic impedance of coax, velocity factor, guide wavelength, and the mismatch quantities -- VSWR, return loss, mismatch loss. Conductors are covered by the skin depth `δ = √(2ρ/ωμ)`, which is why RF current flows in a thin surface layer. Noise and units: thermal noise power and floor in dBm, signal-to-noise ratio, the Shannon capacity of the resulting channel, and conversions between watts, dBm, ratios and decibels.")?; + m_rf::register(py, &sub)?; + root.add("rf", &sub)?; + mods.insert("rf", sub); + } + { + let sub = PyModule::new(py, "numeria.signal_processing")?; + sub.setattr("__doc__", "Time-domain signal operations and test waveforms. Convolution, cross- and autocorrelation, normalization, windowing, and the simple smoothers -- moving average, exponential moving average, and the median filter, which unlike the other two removes impulsive noise without smearing an edge. Waveform generators (sine, square, sawtooth, triangle, noise, chirp) provide test signals. This module is the elementary layer and re-exports the pieces of `transforms` and `dsp` most often wanted alongside it. For FFTs of any length go to `transforms::fft`; for filter *design* go to `dsp`.")?; + m_signal_processing::register(py, &sub)?; + root.add("signal_processing", &sub)?; + mods.insert("signal_processing", sub); + } + { + let sub = PyModule::new(py, "numeria.sim")?; + sub.setattr("__doc__", "Time-stepping simulation engines. Where the rest of the crate evaluates a relation, these advance a state forward in time: `rigid_body` for 3-D dynamics with quaternion orientation and Euler's equations, `fluid_sim` for shallow water and 2-D incompressible Euler, `heat_sim` for conduction and convection-diffusion, `wave_sim` for the wave equation with Mur absorbing boundaries, `em_sim` for FDTD electromagnetics, and `cloth_sim` for Verlet cloth and rope. These are compact, readable integrators intended for interactive use and for seeing the physics behave. For the research-grade schemes -- Riemann solvers, WENO, lattice Boltzmann, SPH -- see `cfd`; for finite elements and a Yee-grid FDTD with PML see `fem`.")?; + m_sim::register(py, &sub)?; + root.add("sim", &sub)?; + mods.insert("sim", sub); + } + { + let sub = PyModule::new(py, "numeria.solid_mechanics")?; + sub.setattr("__doc__", "Strength of materials: stress, strain, elastic constants and beams. Engineering and true stress and strain, and the elastic constants with the identities that connect them -- any two of `E`, `G`, `K` and `ν` determine the other two for an isotropic material, and the conversions are all here. Beam bending: cantilever and simply-supported deflections, bending moment and stress, and second moments of area for rectangular and circular sections. Design closes with the von Mises equivalent stress, the safety factor, and strain energy density. For the tensor formulation and yield surfaces see `continuum_mechanics`; for finite-element beams and modal analysis see `resonance::structural`.")?; + m_solid_mechanics::register(py, &sub)?; + root.add("solid_mechanics", &sub)?; + mods.insert("solid_mechanics", sub); + } + { + let sub = PyModule::new(py, "numeria.spatial")?; + sub.setattr("__doc__", "Spatial data structures, transforms, geometric primitives, and queries.")?; + m_spatial::register(py, &sub)?; + root.add("spatial", &sub)?; + mods.insert("spatial", sub); + } + { + let sub = PyModule::new(py, "numeria.special")?; + sub.setattr("__doc__", "Special functions: error function family, gamma family, and beta functions.")?; + m_special::register(py, &sub)?; + root.add("special", &sub)?; + mods.insert("special", sub); + } + { + let sub = PyModule::new(py, "numeria.statistical_mechanics")?; + sub.setattr("__doc__", "Statistical mechanics: the elementary relations here, with lattice models and Monte Carlo in submodules. The roadmap calls this area `statmech`; it lives under the existing `statistical_mechanics` module instead, so that there is one home for the subject rather than two.")?; + m_statistical_mechanics::register(py, &sub)?; + root.add("statistical_mechanics", &sub)?; + mods.insert("statistical_mechanics", sub); + } + { + let sub = PyModule::new(py, "numeria.statistics")?; + sub.setattr("__doc__", "Statistics: descriptive measures, probability distributions, and Fourier utilities. Submodules are re-exported so historical paths such as `statistics::mean` keep working.")?; + m_statistics::register(py, &sub)?; + root.add("statistics", &sub)?; + mods.insert("statistics", sub); + } + { + let sub = PyModule::new(py, "numeria.stochastic")?; + sub.setattr("__doc__", "Stochastic processes: Markov chains, Markov chain Monte Carlo, and hidden state models.")?; + m_stochastic::register(py, &sub)?; + root.add("stochastic", &sub)?; + mods.insert("stochastic", sub); + } + { + let sub = PyModule::new(py, "numeria.thermodynamics")?; + sub.setattr("__doc__", "Thermodynamics: gases, heat transfer, cycles and phase change. The ideal gas law in each of its four solved forms, and the kinetic picture behind it -- average kinetic energy, RMS speed, mean free path. Work and entropy change along isothermal, isobaric and adiabatic paths. Heat transfer by all three mechanisms: Fourier conduction (with an explicit 1-D stepper and its stability limit), Newton's law of cooling and convection, and radiative exchange. The dimensionless groups that classify convection -- Grashof, Rayleigh, Prandtl, Nusselt, Biot -- are here too. Cycles through the Carnot efficiency and the coefficients of performance for refrigerators and heat pumps; phase change through latent heat, Clausius-Clapeyron, boiling-point elevation, freezing-point depression, and wet-steam quality. Temperature scale conversions round it out.")?; + m_thermodynamics::register(py, &sub)?; + root.add("thermodynamics", &sub)?; + mods.insert("thermodynamics", sub); + } + { + let sub = PyModule::new(py, "numeria.transforms")?; + sub.setattr("__doc__", "Discrete transforms: FFT (any length), DCT/DST, STFT, wavelets, Hilbert, Laplace inversion, Radon, and spectral estimation. The radix-2 FFT that used to live in `signal_processing::fft` moved here; the old paths re-export everything so no caller changes.")?; + m_transforms::register(py, &sub)?; + root.add("transforms", &sub)?; + mods.insert("transforms", sub); + } + { + let sub = PyModule::new(py, "numeria.trigonometry")?; + sub.setattr("__doc__", "Triangle solving, trigonometric identities, and hyperbolic functions. The laws of sines and cosines in both directions -- side from angles and angle from sides -- and the SAS triangle area. The identities are provided as functions rather than left to the caller to expand: sum and difference, double and half angle, and product-to-sum. The hyperbolic family includes the reciprocals (`sech`, `csch`, `coth`) and inverses that `f64` does not provide directly. Angle utilities close the module: normalization to `[0, 2π)` or `(−π, π]`, the signed shortest difference between two angles, and classification as acute, right or obtuse.")?; + m_trigonometry::register(py, &sub)?; + root.add("trigonometry", &sub)?; + mods.insert("trigonometry", sub); + } + { + let sub = PyModule::new(py, "numeria.units")?; + sub.setattr("__doc__", "Unit conversions, dimensional analysis and the CODATA constants. The flat conversion functions below are the original contents of this module and are unchanged. What sits alongside them now is the typed machinery: `quantity` carries a value together with its seven SI exponents so that adding a length to a time is a compile-time-shaped error rather than a silent number, and `dimensional` does the analysis those exponents make possible -- Buckingham's theorem over exact rationals, the named dimensionless groups, natural units and the Planck scale.")?; + m_units::register(py, &sub)?; + root.add("units", &sub)?; + mods.insert("units", sub); + } + { + let sub = PyModule::new(py, "numeria.vector_calculus")?; + sub.setattr("__doc__", "Vector calculus operators and field theory for physics grids. Provides discrete differential operators (gradient, laplacian, divergence, curl) on 2D and 3D uniform grids, point-wise numerical differentiation via function pointers, line/surface integrals, and a Jacobi Poisson solver.")?; + m_vector_calculus::register(py, &sub)?; + root.add("vector_calculus", &sub)?; + mods.insert("vector_calculus", sub); + } + { + let sub = PyModule::new(py, "numeria.waves")?; + sub.setattr("__doc__", "Wave propagation: mechanical, acoustic and seismic. The kinematic relations (`v = fλ` and the wavenumber-frequency pair), displacement, energy density and intensity, and the inverse-square falloff of a spherical wave. The Doppler effect in both classical and relativistic forms, with the Mach cone angle for supersonic sources. Standing waves on strings and in open and closed pipes, beats, and superposition. Boundaries are handled by impedance: the reflection and transmission coefficients follow from the impedance mismatch, which is also why they carry a sign. Acoustics covers the speed of sound in a gas, sound pressure level and the decibel scale, and absorption and penetration depth. Seismology covers P-, S-, Rayleigh and Love wave speeds. Diffraction closes with the Fraunhofer single-slit pattern, the Airy disk radius and the Fresnel number.")?; + m_waves::register(py, &sub)?; + root.add("waves", &sub)?; + mods.insert("waves", sub); + } + { + let sub = PyModule::new(py, "numeria.astrophysics.collisions")?; + sub.setattr("__doc__", "Impacts, mergers, and collision probability. Impact geometry and speed (including the gravitational focusing that makes the impact speed at least the escape velocity, however slowly the bodies approach), perfectly inelastic merger of mass and momentum, and the energy released. Crater scaling and the collision probability for objects sharing a volume of space follow, along with the debris-flux relations used for orbital collision risk.")?; + m_astrophysics__collisions::register(py, &sub)?; + mods["astrophysics"].add("collisions", &sub)?; + mods.insert("astrophysics::collisions", sub); + } + { + let sub = PyModule::new(py, "numeria.astrophysics.coords")?; + sub.setattr("__doc__", "Astronomical coordinates, low-precision ephemerides and TLE parsing. # Four frames and what each is for *Equatorial* coordinates -- right ascension and declination -- are fixed to the stars, or nearly so, and are what a catalogue lists. *Horizontal* coordinates -- azimuth and altitude -- are what an observer sees, and depend on where and when they are looking. *Ecliptic* coordinates are referred to the Earth's orbital plane, which is the natural frame for anything in the solar system. And the *perifocal* and inertial frames of `astrophysics::kepler` are where orbits live. Converting between the first three is pure spherical trigonometry, and all of it is exactly invertible. Which is worth saying because the *ephemerides* here are not: they are truncated series good to a fraction of a degree, and their inverses do not exist in any useful sense. # What \"low precision\" means `sun_position_approx` is good to about a hundredth of a degree over a couple of centuries around J2000. `moon_position_approx` is good to a few tenths of a degree, because the Moon's motion has hundreds of terms of comparable size and this keeps a handful. `planet_position_low_precision` uses mean elements with linear rates and no perturbations at all, which is good to a fraction of a degree for the inner planets over a few centuries and steadily worse outward, where Jupiter and Saturn pull each other around by degrees. None of these is suitable for an occultation, a transit timing, or anything where arcseconds matter. They are for pointing a small telescope, checking whether a planet is up, and drawing a sky map.")?; + m_astrophysics__coords::register(py, &sub)?; + mods["astrophysics"].add("coords", &sub)?; + mods.insert("astrophysics::coords", sub); + } + { + let sub = PyModule::new(py, "numeria.astrophysics.gravitational_waves")?; + sub.setattr("__doc__", "Gravitational radiation from a compact binary. Quadrupole-formula results for an inspiralling binary: the emitted luminosity, the wave frequency (twice the orbital frequency), the strain amplitude at a given distance, and the time remaining to merger. The chirp mass `ℳ = (m₁m₂)^(3/5)/(m₁+m₂)^(1/5)` is the combination that governs all of them -- it is the parameter the inspiral waveform actually determines, which is why it is measured far better than either individual mass.")?; + m_astrophysics__gravitational_waves::register(py, &sub)?; + mods["astrophysics"].add("gravitational_waves", &sub)?; + mods.insert("astrophysics::gravitational_waves", sub); + } + { + let sub = PyModule::new(py, "numeria.astrophysics.habitable_zone")?; + sub.setattr("__doc__", "Habitable zone boundaries and tidal locking. Inner and outer edges scale as the square root of the stellar luminosity, with the conventional coefficients: 0.95 AU and 1.37 AU per square root of a solar luminosity. Also the mass-luminosity relation for main-sequence stars, equilibrium temperature for a given albedo, and the tidal locking timescale -- which matters here because low-mass stars have close-in habitable zones, so their habitable planets are likely to be locked.")?; + m_astrophysics__habitable_zone::register(py, &sub)?; + mods["astrophysics"].add("habitable_zone", &sub)?; + mods.insert("astrophysics::habitable_zone", sub); + } + { + let sub = PyModule::new(py, "numeria.astrophysics.kepler")?; + sub.setattr("__doc__", "Kepler's equation, anomaly conversions and two-body propagation. # Three anomalies and why there are three An orbit's position is described by an angle, and three different angles are useful for different things. *True anomaly* is the physical angle from periapsis to the body, seen from the focus -- it is what a telescope measures and what converts directly to a position. *Mean anomaly* advances uniformly in time, `M = n (t - t_p)`, so it is what a clock gives. *Eccentric anomaly* is the intermediate angle on the circumscribing circle that connects the two, and it exists because no closed form connects the other two directly. Kepler's equation `M = E - e sin E` is the link, and it is transcendental. Everything in orbital mechanics that looks like \"where will it be at time t\" bottoms out in solving it, which is why five centuries of work have gone into doing so quickly. # What is not here `astrophysics::orbital_elements` already provides the element set, the state-to-elements conversion and the geometric quantities read off an orbit; this module adds the time dependence and the inverse conversion, and does not repeat them.")?; + m_astrophysics__kepler::register(py, &sub)?; + mods["astrophysics"].add("kepler", &sub)?; + mods.insert("astrophysics::kepler", sub); + } + { + let sub = PyModule::new(py, "numeria.astrophysics.lagrange")?; + sub.setattr("__doc__", "The five Lagrange points of the circular restricted three-body problem. L1, L2 and L3 lie on the line through the two masses and are found by solving a quintic numerically; L4 and L5 sit at the vertices of equilateral triangles with the two masses and are exact. The collinear points are unstable saddles -- a spacecraft there needs station-keeping -- while L4 and L5 are stable for a mass ratio below about 1/24.96, which is why Jupiter's Trojan asteroids stay put. The Hill radius is here as well.")?; + m_astrophysics__lagrange::register(py, &sub)?; + mods["astrophysics"].add("lagrange", &sub)?; + mods.insert("astrophysics::lagrange", sub); + } + { + let sub = PyModule::new(py, "numeria.astrophysics.lambert")?; + sub.setattr("__doc__", "Lambert's problem: the orbit connecting two positions in a given time. # The problem and why it is hard Given where a spacecraft is, where it must be, and how long it has to get there, find the transfer orbit. Stated that way it sounds like `astrophysics::kepler::propagate_kepler` run backwards, but it is a genuinely different problem: propagation is an initial-value problem with one answer, and Lambert's is a *boundary*-value problem whose answer need not be unique. It is not, however, a problem of existence. Within a single revolution a transfer exists for every positive flight time: making the trip faster costs more energy without limit, and the minimum-energy transfer is a particular duration rather than a floor on one. What *does* fail is a degenerate geometry -- a transfer angle of zero or exactly `pi`, where the two radii do not determine a plane and infinitely many orbits connect the points. Only the zero-revolution solution is computed here, which is the one interplanetary trajectory design starts from. Multi-revolution transfers exist for longer flight times and are a separate search, with two branches per revolution count; they are not attempted rather than approximated. # The universal-variable formulation Every conic is covered by one iteration, on a variable `z` that is positive for an ellipse, negative for a hyperbola and zero for a parabola. The Stumpff functions `C(z)` and `S(z)` carry the difference, and their series expansions near zero are what keep the parabolic case from losing precision to cancellation -- the closed forms are `0/0` there.")?; + m_astrophysics__lambert::register(py, &sub)?; + mods["astrophysics"].add("lambert", &sub)?; + mods.insert("astrophysics::lambert", sub); + } + { + let sub = PyModule::new(py, "numeria.astrophysics.magnetosphere")?; + sub.setattr("__doc__", "Planetary dipole fields and the magnetopause. The magnetic dipole field in vector form, field-line tracing by integration along the field, and the magnetopause standoff distance -- where magnetic pressure balances the solar wind's dynamic pressure, which is what sets the size of a magnetosphere. Field strength falls as `1/r³`, so the standoff distance depends only weakly (as the sixth root) on the wind pressure.")?; + m_astrophysics__magnetosphere::register(py, &sub)?; + mods["astrophysics"].add("magnetosphere", &sub)?; + mods.insert("astrophysics::magnetosphere", sub); + } + { + let sub = PyModule::new(py, "numeria.astrophysics.maneuvers")?; + sub.setattr("__doc__", "Orbital manoeuvres: combined burns, patched conics, gravity assists and the perturbation that dominates low orbits. # What lives elsewhere The impulsive transfers themselves are already in `propulsion`: `hohmann_delta_v`, `hohmann_transfer_time`, `bi_elliptic_delta_v`, `delta_v_plane_change`, `tsiolkovsky_delta_v` and `delta_v_staged`. The Roche limit is in `astrophysics::tidal` and the Hill radius in `astrophysics::lagrange`. This module adds what those do not cover, and reuses rather than repeats them. # Why delta-v is the currency Every manoeuvre here is priced in velocity change rather than in fuel, because the conversion between them is exponential: Tsiolkovsky's equation says the mass ratio is `e^(dv/v_e)`, so a mission's delta-v budget is a linear quantity that adds up while its mass is not. Ten per cent more delta-v is not ten per cent more spacecraft. The other consequence is the Oberth effect. A burn's *energy* gain is `v dv`, proportional to the speed you already have, so the same delta-v spent deep in a gravity well buys far more energy than the same delta-v spent far from it. That is why escape burns are made at periapsis and why a flyby is worth planning around.")?; + m_astrophysics__maneuvers::register(py, &sub)?; + mods["astrophysics"].add("maneuvers", &sub)?; + mods.insert("astrophysics::maneuvers", sub); + } + { + let sub = PyModule::new(py, "numeria.astrophysics.nbody")?; + sub.setattr("__doc__", "Direct N-body gravitational simulation. Velocity Verlet integration, chosen because it is symplectic: it conserves a nearby \"shadow\" energy exactly rather than drifting, so orbits stay closed over long integrations where Runge-Kutta of the same order would spiral. Softening replaces `1/r²` with `1/(r² + ε²)` to keep close encounters from producing unbounded accelerations, at the cost of biasing the force at short range. Includes energy and momentum diagnostics, and system generators. Cost is O(N²) per step. For large N use `astrophysics::octree`.")?; + m_astrophysics__nbody::register(py, &sub)?; + mods["astrophysics"].add("nbody", &sub)?; + mods.insert("astrophysics::nbody", sub); + } + { + let sub = PyModule::new(py, "numeria.astrophysics.orbital_elements")?; + sub.setattr("__doc__", "Keplerian elements: conversion, propagation, and the anomalies. State vectors to elements and back -- semi-major axis, eccentricity, inclination, longitude of ascending node, argument of periapsis and true anomaly -- via the specific orbital energy, the angular momentum and the eccentricity vector. The three anomalies (true, eccentric and mean) and the conversions between them, with Kepler's equation solved by Newton iteration. Periapsis and apoapsis distances and speeds, orbital period, and propagation forward in time complete the module. For a solver that also handles parabolic and hyperbolic orbits and near e = 1, see `astrophysics::kepler`.")?; + m_astrophysics__orbital_elements::register(py, &sub)?; + mods["astrophysics"].add("orbital_elements", &sub)?; + mods.insert("astrophysics::orbital_elements", sub); + } + { + let sub = PyModule::new(py, "numeria.astrophysics.tidal")?; + sub.setattr("__doc__", "Tidal forces and the Roche limit. The tidal acceleration is the *difference* in gravitational pull across a body, so it falls as `1/r³` rather than `1/r²` -- which is why the Moon raises larger tides on Earth than the far more massive Sun does. The Roche limit is given in both the rigid and fluid forms; the fluid limit is the larger, because a fluid body deforms and so becomes easier to pull apart. Tidal heating, the locking timescale and the tidal tensor complete the module.")?; + m_astrophysics__tidal::register(py, &sub)?; + mods["astrophysics"].add("tidal", &sub)?; + mods.insert("astrophysics::tidal", sub); + } + { + let sub = PyModule::new(py, "numeria.astrophysics.time_systems")?; + sub.setattr("__doc__", "Astronomical time: Julian dates and sidereal time. # Why a day is not a day The Earth turns once on its axis in 23h 56m 04s -- a *sidereal* day -- and takes the extra four minutes to face the sun again, because it has moved along its orbit in the meantime. A solar day is therefore longer than a rotation, by almost exactly one part in 366. Everything about pointing a telescope, predicting a satellite pass or reading a ground track depends on keeping the two apart. Sidereal time is the hour angle of the vernal equinox, which is to say how far the Earth has turned relative to the stars. Greenwich mean sidereal time is that quantity at longitude zero, and adding the observer's longitude gives the local value. Right ascension is measured from the same origin, so an object is due south exactly when the local sidereal time equals its right ascension -- which is the whole reason the quantity exists. # What is approximated here `UT1` and `UTC` are treated as the same thing. They differ by up to 0.9 seconds, which is 0.0037 degrees of rotation -- irrelevant for anything in this module and decisive for geodesy. The `TT`/`UTC` offset from leap seconds is likewise ignored; the sun and planet positions here are low-precision approximations for which it does not matter.")?; + m_astrophysics__time_systems::register(py, &sub)?; + mods["astrophysics"].add("time_systems", &sub)?; + mods.insert("astrophysics::time_systems", sub); + } + { + let sub = PyModule::new(py, "numeria.audio.analysis")?; + sub.setattr("__doc__", "Audio analysis: pitch detection (YIN, autocorrelation, cepstral, HPS, McLeod), onset/tempo/beat tracking, MFCCs, LPC and formants, LSPs, spectral descriptors, chroma/key/chord estimation, psychoacoustic approximations, distortion metrics, room-acoustics measures from impulse responses, DTW, and constellation fingerprinting.")?; + m_audio__analysis::register(py, &sub)?; + mods["audio"].add("analysis", &sub)?; + mods.insert("audio::analysis", sub); + } + { + let sub = PyModule::new(py, "numeria.audio.effects")?; + sub.setattr("__doc__", "Audio effects: delays, reverbs (Schroeder, Freeverb, FDN), convolution, modulation effects, dynamics, distortion, EQ, imaging, loudness (ITU-R BS.1770), and dithering.")?; + m_audio__effects::register(py, &sub)?; + mods["audio"].add("effects", &sub)?; + mods.insert("audio::effects", sub); + } + { + let sub = PyModule::new(py, "numeria.audio.envelope")?; + sub.setattr("__doc__", "Envelopes, LFOs, followers, fades, and glides.")?; + m_audio__envelope::register(py, &sub)?; + mods["audio"].add("envelope", &sub)?; + mods.insert("audio::envelope", sub); + } + { + let sub = PyModule::new(py, "numeria.audio.oscillators")?; + sub.setattr("__doc__", "Audio-rate oscillators and test signals: PolyBLEP anti-aliased classics, additive resynthesis, mipmapped wavetables, colored noise, chirps, and measurement sweeps.")?; + m_audio__oscillators::register(py, &sub)?; + mods["audio"].add("oscillators", &sub)?; + mods.insert("audio::oscillators", sub); + } + { + let sub = PyModule::new(py, "numeria.audio.physical")?; + sub.setattr("__doc__", "Physical modeling synthesis: digital waveguides (plucked/struck/bowed strings, clarinet and flute bores), modal synthesis (bars, membranes, plates, bells, glasses), finite-difference membranes and Kirchhoff plates, a brute-force mass-spring string for validation, the Kelly-Lochbaum vocal tract, and glottal source models.")?; + m_audio__physical::register(py, &sub)?; + mods["audio"].add("physical", &sub)?; + mods.insert("audio::physical", sub); + } + { + let sub = PyModule::new(py, "numeria.audio.spatial")?; + sub.setattr("__doc__", "Spatial audio: panning laws, VBAP, ambisonics, simple binaural cues, Doppler, distance/air attenuation, geometric room acoustics (image source and ray tracing), microphone arrays (beamforming, TDOA localization), sonar, and loudspeaker system responses.")?; + m_audio__spatial::register(py, &sub)?; + mods["audio"].add("spatial", &sub)?; + mods.insert("audio::spatial", sub); + } + { + let sub = PyModule::new(py, "numeria.audio.synthesis")?; + sub.setattr("__doc__", "Sound synthesis: additive, FM (DX7-style operator routing), Karplus-Strong, subtractive, granular, formant, waveshaping, drums, and note/sequence rendering.")?; + m_audio__synthesis::register(py, &sub)?; + mods["audio"].add("synthesis", &sub)?; + mods.insert("audio::synthesis", sub); + } + { + let sub = PyModule::new(py, "numeria.audio.tuning")?; + sub.setattr("__doc__", "Musical tuning: temperaments, interval math, Scala parsing, consonance models, stretch tuning, and pitch-class utilities.")?; + m_audio__tuning::register(py, &sub)?; + mods["audio"].add("tuning", &sub)?; + mods.insert("audio::tuning", sub); + } + { + let sub = PyModule::new(py, "numeria.audio.vocoder")?; + sub.setattr("__doc__", "Phase vocoder and related voice/spectral processors: time stretching, pitch shifting, robotization, channel and LPC vocoders, WSOLA, PSOLA, spectral morphing, cross synthesis, harmonizing, and autotune.")?; + m_audio__vocoder::register(py, &sub)?; + mods["audio"].add("vocoder", &sub)?; + mods.insert("audio::vocoder", sub); + } + { + let sub = PyModule::new(py, "numeria.audio.wav")?; + sub.setattr("__doc__", "WAV (RIFF) reading and writing: PCM 8/16/24/32-bit, IEEE float 32/64-bit, and WAVE_FORMAT_EXTENSIBLE containers. Samples are normalized to −1..1 per channel.")?; + m_audio__wav::register(py, &sub)?; + mods["audio"].add("wav", &sub)?; + mods.insert("audio::wav", sub); + } + { + let sub = PyModule::new(py, "numeria.biophysics.epidemiology")?; + sub.setattr("__doc__", "Compartment models of epidemics, their stochastic counterparts, and the quantities estimated from case data. # Units and conventions Compartments are *fractions* of the population and sum to one, so a model is independent of the population size and the numbers can be read as probabilities. The stochastic models work in whole individuals instead, because the questions they answer -- will this outbreak die out, how long until it does -- are questions about integers and have no meaning in a continuum. Rates are per unit time in whatever unit the caller uses for `t_end`; the recovery rate `gamma` is the reciprocal of the mean infectious period, so a two-week illness with time in days is `gamma = 1/14`. # What the basic reproduction number is and is not `R0 = beta / gamma` is the expected number of secondary cases from one case in a *wholly susceptible* population. It is a property of the pathogen and the contact structure together, not of the pathogen alone, and it stops describing the epidemic the moment susceptibles are depleted -- which is what the effective reproduction number is for. Two populations with the same `R0` and different contact heterogeneity do not have the same epidemic; see `epidemic_threshold_network`, where the threshold is set by the largest eigenvalue of the contact graph rather than by any average.")?; + m_biophysics__epidemiology::register(py, &sub)?; + mods["biophysics"].add("epidemiology", &sub)?; + mods.insert("biophysics::epidemiology", sub); + } + { + let sub = PyModule::new(py, "numeria.biophysics.neuro")?; + sub.setattr("__doc__", "Computational neuroscience: single neurons, spike trains, synapses and the small networks built from them. # Units The conductance-based models use the squid axon's units throughout: millivolts, milliseconds, microfarads and microamps per square centimetre, and millisiemens per square centimetre. A rate is therefore a count per millisecond unless a function says otherwise, and the spike frequencies reported by the F-I curves are converted to hertz where that is the useful number. The reduced models -- FitzHugh-Nagumo and the drift-diffusion process -- carry no units at all. # What a spike is here Every model that fires does so by one of two mechanisms, and the difference decides what can be asked of it. Hodgkin-Huxley, Morris-Lecar and FitzHugh-Nagumo generate the spike from their own dynamics: the upstroke is a solution of the equations and the threshold is not a parameter but an emergent property of the vector field. The integrate-and-fire family -- LIF, Izhikevich, AdEx -- *stipulates* the spike: the equations describe only the approach, and a rule replaces the voltage when it crosses a number. The second kind is far cheaper and reproduces firing statistics well; it has no answer to questions about the spike's shape, because the shape was never computed. Spikes are detected in a trace by an upward crossing of a fixed level, which is the right test for a model whose spikes are tall and brief. # Equilibrium potentials `biophysics::nernst_potential` and `biophysics::goldman_potential` already provide the reversal potentials these models take as constants, and are not repeated here.")?; + m_biophysics__neuro::register(py, &sub)?; + mods["biophysics"].add("neuro", &sub)?; + mods.insert("biophysics::neuro", sub); + } + { + let sub = PyModule::new(py, "numeria.biophysics.phylo")?; + sub.setattr("__doc__", "Phylogenetics: trees, the distance and character methods that build them, and the statistics read off them. # What a tree is here `PhyloTree` stores a parent index and a branch length per node, with leaves first and internal nodes after. That representation makes the two operations everything else needs -- walking to the root, and finding a common ancestor -- direct, at the cost of making \"children of\" a search. Trees in this module are rooted; an unrooted method such as neighbour joining produces a tree whose root is an artefact of the construction and carries no meaning, which is noted where it matters. # Distances are not times A branch length is a number of substitutions per site, not an elapsed time, and converting between them needs a rate that no method here estimates. UPGMA is the exception and it is an *assumption* rather than an inference: it produces an ultrametric tree, in which every leaf is equidistant from the root, which is true only under a strict molecular clock. Neighbour joining makes no such assumption, and the difference shows immediately on data where rates vary between lineages.")?; + m_biophysics__phylo::register(py, &sub)?; + mods["biophysics"].add("phylo", &sub)?; + mods.insert("biophysics::phylo", sub); + } + { + let sub = PyModule::new(py, "numeria.biophysics.population")?; + sub.setattr("__doc__", "Population dynamics and population genetics: growth laws, interacting species, age-structured projection, discrete maps, and the drift, selection and coalescent theory that describes gene frequencies. # Two kinds of model, and why they disagree The deterministic models here describe a population large enough that averages are the whole story. The genetic models mostly do not: drift is the *variance* introduced by finite sampling, and it vanishes from any model that tracks only the mean. A Wright-Fisher population's expected allele frequency never changes at all, and yet every such population eventually fixes one allele or the other -- so the mean is not merely an approximation here, it is silent about the outcome. Where a function reports an expectation, it says so. # Units Times are in whatever unit the caller uses for rates. Genetic models work in generations, and `n` is the number of *diploid* individuals unless a function says otherwise, so a population of `n` carries `2n` gene copies -- the factor that makes heterozygosity decay as `1 - 1/(2n)` rather than `1 - 1/n`.")?; + m_biophysics__population::register(py, &sub)?; + mods["biophysics"].add("population", &sub)?; + mods.insert("biophysics::population", sub); + } + { + let sub = PyModule::new(py, "numeria.biophysics.seq_align")?; + sub.setattr("__doc__", "Sequence alignment and the elementary sequence analysis around it. # What an alignment score means Every function here returns a score under an explicit `Scoring`, and the score is only comparable between alignments computed under the *same* one. That is not pedantry: a gap penalty is a free parameter, and the choice of it decides whether two sequences align as one long homology with an insertion or as two short unrelated fragments. Where a function returns an alignment as well as a score, the score is always the score of that alignment under that scoring -- which the tests check directly, since a dynamic program that reports a maximum it did not achieve is the commonest way for one of these to be wrong. # Global, local and affine The three classical algorithms differ in one line of the recurrence each, and the differences matter more than the similarity suggests. Needleman-Wunsch aligns the sequences end to end; Smith-Waterman clamps the score at zero so a poor prefix cannot drag a good local match below the surface; Gotoh separates opening a gap from extending one, which is what lets a single long insertion cost less than many short ones.")?; + m_biophysics__seq_align::register(py, &sub)?; + mods["biophysics"].add("seq_align", &sub)?; + mods.insert("biophysics::seq_align", sub); + } + { + let sub = PyModule::new(py, "numeria.cfd.advection")?; + sub.setattr("__doc__", "Advection schemes: classic 1D finite-volume methods (upwind, Lax-Wendroff, MUSCL with slope limiters, WENO5), 2D semi-Lagrangian transport with BFECC/MacCormack error compensation, SSP-RK3, Burgers solvers, and the Cole-Hopf exact solution.")?; + m_cfd__advection::register(py, &sub)?; + mods["cfd"].add("advection", &sub)?; + mods.insert("cfd::advection", sub); + } + { + let sub = PyModule::new(py, "numeria.cfd.boundary_layer")?; + sub.setattr("__doc__", "Boundary layers: Blasius and Falkner-Skan similarity solutions (shooting), Thwaites and Head integral methods, turbulent wall laws, transition and separation criteria, rotating and oscillating layers, and flat-plate heat transfer.")?; + m_cfd__boundary_layer::register(py, &sub)?; + mods["cfd"].add("boundary_layer", &sub)?; + mods.insert("cfd::boundary_layer", sub); + } + { + let sub = PyModule::new(py, "numeria.cfd.grid")?; + sub.setattr("__doc__", "Staggered (MAC) grids and cell-centered scalar fields for incompressible flow solvers.")?; + m_cfd__grid::register(py, &sub)?; + mods["cfd"].add("grid", &sub)?; + mods.insert("cfd::grid", sub); + } + { + let sub = PyModule::new(py, "numeria.cfd.lbm")?; + sub.setattr("__doc__", "Lattice Boltzmann method: D2Q9 with BGK/TRT/MRT/cumulant-style collisions, bounce-back solids, Zou-He open boundaries, Guo forcing, D3Q19 and D3Q27 lattices, and classic benchmarks.")?; + m_cfd__lbm::register(py, &sub)?; + mods["cfd"].add("lbm", &sub)?; + mods.insert("cfd::lbm", sub); + } + { + let sub = PyModule::new(py, "numeria.cfd.level_set")?; + sub.setattr("__doc__", "Interface capturing: level sets (upwind/WENO advection, Sussman reinitialization, fast marching, marching squares/tetrahedra), volume of fluid with PLIC, a simple free-surface fluid, and bubble/droplet physics relations.")?; + m_cfd__level_set::register(py, &sub)?; + mods["cfd"].add("level_set", &sub)?; + mods.insert("cfd::level_set", sub); + } + { + let sub = PyModule::new(py, "numeria.cfd.multiphase")?; + sub.setattr("__doc__", "Multiphase flow correlations: mixture properties, drift-flux and void fraction models, two-phase pressure drop, flow-pattern maps, bubble and droplet dynamics, population balance, boiling and condensation, sprays, and dispersed-particle transport.")?; + m_cfd__multiphase::register(py, &sub)?; + mods["cfd"].add("multiphase", &sub)?; + mods.insert("cfd::multiphase", sub); + } + { + let sub = PyModule::new(py, "numeria.cfd.porous")?; + sub.setattr("__doc__", "Porous-media flow: Darcy's law and extensions, unsaturated flow (Richards equation with Van Genuchten retention), well hydraulics, solute transport, and two-phase relations (Leverett, Corey, Buckley-Leverett).")?; + m_cfd__porous::register(py, &sub)?; + mods["cfd"].add("porous", &sub)?; + mods.insert("cfd::porous", sub); + } + { + let sub = PyModule::new(py, "numeria.cfd.potential_flow")?; + sub.setattr("__doc__", "Incompressible potential flow: elementary singularities, complex potentials, Joukowski and Karman-Trefftz airfoils, NACA sections, the Hess-Smith panel method, thin-airfoil and lifting-line theory, a simple vortex lattice, and added-mass results.")?; + m_cfd__potential_flow::register(py, &sub)?; + mods["cfd"].add("potential_flow", &sub)?; + mods.insert("cfd::potential_flow", sub); + } + { + let sub = PyModule::new(py, "numeria.cfd.riemann")?; + sub.setattr("__doc__", "1D/2D compressible Euler equations: exact and approximate Riemann solvers (HLL, HLLC, Roe with entropy fix, Rusanov, AUSM+), MUSCL finite-volume drivers, classic shock-tube problems, and gas-dynamic shock/expansion relations.")?; + m_cfd__riemann::register(py, &sub)?; + mods["cfd"].add("riemann", &sub)?; + mods.insert("cfd::riemann", sub); + } + { + let sub = PyModule::new(py, "numeria.cfd.shallow_water")?; + sub.setattr("__doc__", "Shallow water equations: well-balanced HLL finite volumes with hydrostatic reconstruction and wet/dry handling (1D and 2D), the Stoker dam-break solution, water-wave dispersion relations, ocean spectra, and Gerstner waves.")?; + m_cfd__shallow_water::register(py, &sub)?; + mods["cfd"].add("shallow_water", &sub)?; + mods.insert("cfd::shallow_water", sub); + } + { + let sub = PyModule::new(py, "numeria.cfd.sph")?; + sub.setattr("__doc__", "Smoothed-particle hydrodynamics: standard kernel family, spatial hashing, weakly compressible (WCSPH) and predictive-corrective solvers with boundary particles, and classic free-surface benchmarks.")?; + m_cfd__sph::register(py, &sub)?; + mods["cfd"].add("sph", &sub)?; + mods.insert("cfd::sph", sub); + } + { + let sub = PyModule::new(py, "numeria.cfd.stable_fluids")?; + sub.setattr("__doc__", "Stable-fluids incompressible solver on a MAC grid: MacCormack advection, implicit viscosity, buoyancy, vorticity confinement, and a pressure projection with a choice of Poisson solvers, plus classic benchmark configurations.")?; + m_cfd__stable_fluids::register(py, &sub)?; + mods["cfd"].add("stable_fluids", &sub)?; + mods.insert("cfd::stable_fluids", sub); + } + { + let sub = PyModule::new(py, "numeria.cfd.turbulence")?; + sub.setattr("__doc__", "Turbulence modelling and statistics. Kolmogorov scaling, energy spectra, LES subgrid models (Smagorinsky, dynamic Smagorinsky, WALE, Vreman), vortex identification criteria, RANS models (k-epsilon, k-omega SST, Spalart-Allmaras), synthetic turbulence generation, and canonical spectra (von Karman, Pao).")?; + m_cfd__turbulence::register(py, &sub)?; + mods["cfd"].add("turbulence", &sub)?; + mods.insert("cfd::turbulence", sub); + } + { + let sub = PyModule::new(py, "numeria.cfd.vortex")?; + sub.setattr("__doc__", "Vortex methods: regularized Biot-Savart particle methods in 2D and 3D, classical vortex solutions (Lamb-Oseen, Rankine, Burgers, Hill), point vortex dynamics with a symplectic integrator, and vortex phenomenology (shedding, Crow instability, tip vortices).")?; + m_cfd__vortex::register(py, &sub)?; + mods["cfd"].add("vortex", &sub)?; + mods.insert("cfd::vortex", sub); + } + { + let sub = PyModule::new(py, "numeria.codes.block")?; + sub.setattr("__doc__", "Binary linear block codes. A linear code of length `n` and dimension `k` is a `k`-dimensional subspace of `GF(2)^n`. Everything follows from that one sentence. The subspace is described either by a basis -- the rows of a generator matrix `G` -- or by the equations that cut it out -- the rows of a parity check matrix `H`, with `C = { x : H x' = 0 }`. Encoding is a matrix product. Decoding is the observation that `H (c + e)' = H e'`, so the syndrome depends only on the error and not on what was sent: correcting is choosing the lightest error pattern with the observed syndrome. Linearity is also what makes the minimum distance computable at all. The distance between two codewords is the weight of their difference, which is another codeword, so the minimum distance over all `2^k (2^k - 1) / 2` pairs is just the minimum weight over the `2^k - 1` non-zero codewords. The `_small` routines enumerate the whole code and are exponential in `k` by construction; they are for the classical codes, which are small.")?; + m_codes__block::register(py, &sub)?; + mods["codes"].add("block", &sub)?; + mods.insert("codes::block", sub); + } + { + let sub = PyModule::new(py, "numeria.codes.checksum")?; + sub.setattr("__doc__", "Checksums and check digits: cheap ways to notice that data changed. None of these corrects anything, and none of them resists an adversary. What they do is turn a class of likely accidents into a mismatch, and the useful question about each is which class. A single parity bit catches any odd number of flipped bits and nothing else. A Fletcher or Adler sum catches reordering, which a plain sum does not, because the second accumulator weights each byte by its position. A CRC of width `w` catches every burst of `w` bits or fewer, every odd number of bit errors when the polynomial has `x + 1` as a factor, and all but `2^-w` of everything else. The decimal check digits catch every single-digit error and, except for Luhn, every transposition of adjacent digits. For an adversary, none of this is relevant: all of it is linear or nearly so, and a forger can adjust the data to hit any checksum they like.")?; + m_codes__checksum::register(py, &sub)?; + mods["codes"].add("checksum", &sub)?; + mods.insert("codes::checksum", sub); + } + { + let sub = PyModule::new(py, "numeria.codes.compression")?; + sub.setattr("__doc__", "Lossless compression, and the string machinery it is built on. Every method here is one of two ideas. *Entropy coding* -- Huffman, Shannon-Fano, arithmetic -- assumes the symbols are drawn independently from a known distribution and spends about `-log2 p` bits on a symbol of probability `p`. It cannot beat the entropy, and Shannon's theorem says nothing can. *Modelling* -- run lengths, LZ77, LZW, the Burrows-Wheeler transform -- changes what the symbols are, so that a stream with obvious structure and high byte entropy becomes one with low entropy that an entropy coder can then finish off. Real compressors are a modelling stage followed by an entropy stage, and the two halves are here separately. The suffix array and its longest-common-prefix array sit underneath: they are what makes the Burrows-Wheeler transform computable in near-linear time, and they answer questions about repetition in their own right.")?; + m_codes__compression::register(py, &sub)?; + mods["codes"].add("compression", &sub)?; + mods.insert("codes::compression", sub); + } + { + let sub = PyModule::new(py, "numeria.codes.convolutional")?; + sub.setattr("__doc__", "Convolutional and turbo codes, and the channels they run over. A convolutional code has no block length. The encoder is a shift register: each input bit is combined with the last few, and the output depends on a sliding window rather than on a partition of the message. That makes the code a walk through a *trellis* -- a graph whose vertices are the register states and whose edges are the possible inputs -- and decoding the problem of finding the walk that best matches what arrived. Viterbi's algorithm is dynamic programming on that graph, and it is optimal: it returns the maximum-likelihood sequence, not an approximation to it. Turbo codes take two such encoders, feed the second an interleaved copy of the message, and decode by having the two halves exchange opinions. What each passes the other is *extrinsic* information -- what it concluded about a bit from everything except that bit's own channel evidence -- and keeping the exchange extrinsic is the whole trick. Feeding back a decoder's full opinion would let it hear its own guess reflected as independent confirmation, and the iteration would converge confidently to nonsense. The capacity functions at the end say where the limits are. A rate-`1/2` binary code cannot work below about `0.187` decibels of `Eb/N0`, whatever it is; turbo codes reached within a few tenths of that, which is why they ended a thirty-year search.")?; + m_codes__convolutional::register(py, &sub)?; + mods["codes"].add("convolutional", &sub)?; + mods.insert("codes::convolutional", sub); + } + { + let sub = PyModule::new(py, "numeria.codes.crypto_math")?; + sub.setattr("__doc__", "The arithmetic underneath public-key cryptography, for study rather than for use. **None of this is safe to deploy.** Every routine here branches and indexes on secret values, so the time it takes and the memory it touches leak what it is working on; a modular exponentiation that skips a squaring when a bit is zero tells anyone timing it how many bits are set. Real implementations are written to take the same time and the same path whatever the key, use blinding to break the correlation between input and timing, and are audited for the dozen further side channels that remain. Nothing here does any of that, and the key sizes the tests use are small enough to factor over lunch. What it is for is seeing why the constructions work. RSA rests on the fact that exponentiating by `e` and then by `d` returns you to where you started whenever `ed = 1` modulo the group order -- so anyone who can compute the group order can find `d`, and the security assumption is exactly that factoring `n` is hard. Diffie-Hellman and elliptic curve Diffie-Hellman rest on the same shape in a different group. Shamir's scheme rests on a polynomial of degree `k - 1` being determined by `k` points and by no fewer. Each of those is a theorem, and the tests here check the theorem rather than the ciphertext.")?; + m_codes__crypto_math::register(py, &sub)?; + mods["codes"].add("crypto_math", &sub)?; + mods.insert("codes::crypto_math", sub); + } + { + let sub = PyModule::new(py, "numeria.codes.reed_solomon")?; + sub.setattr("__doc__", "Reed-Solomon and BCH codes over finite fields. Reed-Solomon works on symbols rather than bits, which is why it appears wherever errors arrive in clumps: a scratch on a disc, a fading burst on a radio link, a smudge across a printed barcode. A byte is wrong whether one bit of it flipped or all eight, so a burst that would defeat a bit-level code costs an `RS(255, 223)` codeword at most one of its sixteen correctable symbols per byte touched. The construction is one idea. Fix a field, treat the message as the coefficients of a polynomial, and multiply by a generator whose roots are consecutive powers of a primitive element. A codeword is then exactly a polynomial vanishing at those `n - k` points, so evaluating the received word there gives zero if nothing went wrong and, if something did, a set of *syndromes* that depend only on the errors. Berlekamp-Massey turns those syndromes into a polynomial whose roots say where the errors are, Chien search finds the roots, and Forney's formula says how large each error was. Every step is field arithmetic; none of it looks at the message. Because the generator has exactly `n - k` roots, the code meets the Singleton bound with equality -- `d = n - k + 1`. Reed-Solomon codes are the standard example of a maximum distance separable code, and there is no slack anywhere in the parameters.")?; + m_codes__reed_solomon::register(py, &sub)?; + mods["codes"].add("reed_solomon", &sub)?; + mods.insert("codes::reed_solomon", sub); + } + { + let sub = PyModule::new(py, "numeria.control_systems.kalman")?; + sub.setattr("__doc__", "Kalman filtering. Linear filter: predict x ← F·x, P ← F·P·Fᵀ + Q; update with gain K = P·Hᵀ·S⁻¹, S = H·P·Hᵀ + R, using the Joseph-form covariance update P ← (I−KH)·P·(I−KH)ᵀ + K·R·Kᵀ so P stays symmetric PSD. The gain solve goes through `linalg::lu`. Reference: Bar-Shalom, Li & Kirubarajan, *Estimation with Applications to Tracking*.")?; + m_control_systems__kalman::register(py, &sub)?; + mods["control_systems"].add("kalman", &sub)?; + mods.insert("control_systems::kalman", sub); + } + { + let sub = PyModule::new(py, "numeria.core.compensated")?; + sub.setattr("__doc__", "Compensated (error-free-transformation) summation. Formulas: Neumaier's improved Kahan-Babuska summation (A. Neumaier, \"Rundungsfehleranalyse einiger Verfahren zur Summation endlicher Summen\", ZAMM 54, 1974) and recursive pairwise summation (Higham, *Accuracy and Stability of Numerical Algorithms*, ch. 4).")?; + m_core__compensated::register(py, &sub)?; + mods["core"].add("compensated", &sub)?; + mods.insert("core::compensated", sub); + } + { + let sub = PyModule::new(py, "numeria.core.dual")?; + sub.setattr("__doc__", "Forward-mode automatic differentiation with dual numbers. A dual number x = re + ε·eps with ε² = 0 propagates exact first derivatives through arithmetic: f(a + ε·a') = f(a) + ε·f'(a)·a'.")?; + m_core__dual::register(py, &sub)?; + mods["core"].add("dual", &sub)?; + mods.insert("core::dual", sub); + } + { + let sub = PyModule::new(py, "numeria.core.interval")?; + sub.setattr("__doc__", "Rigorous interval arithmetic with outward rounding. Every operation returns an interval guaranteed to contain the true real result for all inputs in the operand intervals: computed bounds are widened outward with `f64::next_down`/`next_up` (plus a small ulp margin for transcendental functions whose libm error is ≤ 1 ulp but unproven). Reference: Moore, Kearfott & Cloud, *Introduction to Interval Analysis* (SIAM, 2009).")?; + m_core__interval::register(py, &sub)?; + mods["core"].add("interval", &sub)?; + mods.insert("core::interval", sub); + } + { + let sub = PyModule::new(py, "numeria.discrete.combinatorics")?; + sub.setattr("__doc__", "Counting, enumeration, and the permutation group. Three kinds of function live here. Counting functions return a `BigInt` whenever the value outgrows 64 bits, which is almost immediately -- the Bell numbers pass `u64::MAX` at n = 25 and the Catalan numbers at n = 33. Enumeration functions return iterators that generate one object at a time rather than materialising the whole family. The permutation functions treat a `&[usize]` as the one-line form of a bijection on `0..n`, so `p[i]` is the image of `i`.")?; + m_discrete__combinatorics::register(py, &sub)?; + mods["discrete"].add("combinatorics", &sub)?; + mods.insert("discrete::combinatorics", sub); + } + { + let sub = PyModule::new(py, "numeria.discrete.disjoint_set")?; + sub.setattr("__doc__", "Union-find over `0..n` with path compression and union by size. Shared infrastructure: graph minimum spanning trees, percolation cluster labelling, and single-linkage clustering all reduce to the same \"merge these two, are these two together\" question.")?; + m_discrete__disjoint_set::register(py, &sub)?; + mods["discrete"].add("disjoint_set", &sub)?; + mods.insert("discrete::disjoint_set", sub); + } + { + let sub = PyModule::new(py, "numeria.discrete.number_theory")?; + sub.setattr("__doc__", "Elementary and analytic number theory. Divisibility and the Euclidean algorithm, modular arithmetic and the Chinese remainder theorem, the classical arithmetic functions (`phi`, `mu`, `sigma_k`, Carmichael's `lambda`) together with their sieves, multiplicative order, discrete logarithms, quadratic residues, and a collection of Diophantine and digit problems. Factorization comes from `discrete::primes`; nothing here re-implements it.")?; + m_discrete__number_theory::register(py, &sub)?; + mods["discrete"].add("number_theory", &sub)?; + mods.insert("discrete::number_theory", sub); + } + { + let sub = PyModule::new(py, "numeria.discrete.partitions")?; + sub.setattr("__doc__", "Integer partitions, Young diagrams, and the RSK correspondence. A partition of `n` is a weakly decreasing list of positive integers summing to `n`. It is stored as `Vec` in that order, so `p[0]` is the largest part.")?; + m_discrete__partitions::register(py, &sub)?; + mods["discrete"].add("partitions", &sub)?; + mods.insert("discrete::partitions", sub); + } + { + let sub = PyModule::new(py, "numeria.discrete.primes")?; + sub.setattr("__doc__", "Primes: sieves, primality testing, factorization, and prime counting.")?; + m_discrete__primes::register(py, &sub)?; + mods["discrete"].add("primes", &sub)?; + mods.insert("discrete::primes", sub); + } + { + let sub = PyModule::new(py, "numeria.discrete.sequences")?; + sub.setattr("__doc__", "Integer sequences, linear recurrences, and generating functions. Two halves. The first recovers a sequence from an analytic or algebraic description: Taylor coefficients from a function by Cauchy's integral, and the minimal linear recurrence from a prefix by Berlekamp-Massey. The second is the named sequences themselves.")?; + m_discrete__sequences::register(py, &sub)?; + mods["discrete"].add("sequences", &sub)?; + mods.insert("discrete::sequences", sub); + } + { + let sub = PyModule::new(py, "numeria.dsp.fir")?; + sub.setattr("__doc__", "FIR filter design and application. All frequencies are normalized to the sample rate (cycles/sample), so cutoffs live in (0, 0.5). Designs are linear-phase; windowed-sinc designs follow Oppenheim & Schafer §7.5, the equiripple design is the Parks-McClellan / Remez exchange (type I), and Savitzky-Golay follows the least-squares polynomial derivation.")?; + m_dsp__fir::register(py, &sub)?; + mods["dsp"].add("fir", &sub)?; + mods.insert("dsp::fir", sub); + } + { + let sub = PyModule::new(py, "numeria.dsp.iir")?; + sub.setattr("__doc__", "Infinite impulse response filters: RBJ biquads, second-order-section cascades, and classical designs (Butterworth, Chebyshev I/II, elliptic, Bessel) via analog prototypes, frequency transformation, and the bilinear transform. Frequencies are in Hz against an explicit sample rate. The elliptic prototype follows Orfanidis' lecture notes (the same construction as scipy's `ellipap`); Chebyshev and Butterworth prototypes are the textbook pole formulas. The pre-Part-3 first-order RC filters remain here unchanged.")?; + m_dsp__iir::register(py, &sub)?; + mods["dsp"].add("iir", &sub)?; + mods.insert("dsp::iir", sub); + } + { + let sub = PyModule::new(py, "numeria.dsp.phase")?; + sub.setattr("__doc__", "Phase utilities: unwrapping (1D and Itoh 2D), phase-locked loops, interpolated zero crossings, and phase measurement against a reference tone.")?; + m_dsp__phase::register(py, &sub)?; + mods["dsp"].add("phase", &sub)?; + mods.insert("dsp::phase", sub); + } + { + let sub = PyModule::new(py, "numeria.dsp.resample")?; + sub.setattr("__doc__", "Sample-rate conversion: integer up/down sampling, polyphase rational resampling, windowed-sinc/linear/cubic interpolation, CIC decimation, and half-band filters. Anti-aliasing and interpolation kernels are symmetric windowed-sinc filters applied centered (\"same\" alignment), so resampled signals keep zero net delay.")?; + m_dsp__resample::register(py, &sub)?; + mods["dsp"].add("resample", &sub)?; + mods.insert("dsp::resample", sub); + } + { + let sub = PyModule::new(py, "numeria.dsp.windows")?; + sub.setattr("__doc__", "Window functions for spectral analysis and FIR design. `window` generates any of the standard windows in symmetric form (filter design; endpoints at k = 0 and k = n−1) or periodic form (spectral analysis; the implied period is n). `window_metrics` measures the figures of merit from Harris (1978), *On the Use of Windows for Harmonic Analysis with the DFT*. The pre-Part-3 generators (`hann_window`, …) are kept and wrap `window` with their original symmetric convention.")?; + m_dsp__windows::register(py, &sub)?; + mods["dsp"].add("windows", &sub)?; + mods.insert("dsp::windows", sub); + } + { + let sub = PyModule::new(py, "numeria.exact.bigfloat")?; + sub.setattr("__doc__", "Arbitrary-precision binary floating point. A `BigFloat` is the exact dyadic rational `mantissa * 2^exponent` together with a working `precision` measured in bits. # Canonical form Every value produced by this module is normalized: either the mantissa is zero (and the exponent is zero), or the mantissa's magnitude has *exactly* `precision` significant bits. Normalization is applied on construction and after every operation, so `precision` is the true working precision rather than an upper bound, and the leading bit of the mantissa is always set. # Rounding All rounding is **round-to-nearest, ties-to-even** — the IEEE-754 default — applied exactly once per operation. `add`, `sub`, `mul`, `div` and `sqrt` form the exact result (or an exact result plus a sticky low bit that cannot change the rounding decision) and round it once, so they are correctly rounded: the returned value is the closest `precision`-bit dyadic to the true mathematical result. As a consequence they reproduce IEEE-754 `f64` arithmetic bit for bit when used at `precision = 53` on operands in the normal range. The transcendental functions (`BigFloat::exp`, `BigFloat::ln`, `BigFloat::sin`, `BigFloat::cos`, `BigFloat::atan`, `BigFloat::pow`) and the constants (`BigFloat::pi`, `BigFloat::e`, `BigFloat::ln2`) evaluate their series at 64 guard bits above the target precision and round once at the end. They are not *proved* correctly rounded (that would need the table-maker's dilemma resolved), but the guard digits put the error far below one ulp of the requested precision. Formulas: Gauss-Legendre AGM iteration for π (Brent 1976, Salamin 1976), Machin's `π/4 = 4·atan(1/5) − atan(1/239)` as an independent cross-check, `ln 2 = 2·atanh(1/3)`, exponential and circular Taylor series after range reduction, and `ln x = 2^s · 2·atanh((m−1)/(m+1))` after repeated square roots.")?; + m_exact__bigfloat::register(py, &sub)?; + mods["exact"].add("bigfloat", &sub)?; + mods.insert("exact::bigfloat", sub); + } + { + let sub = PyModule::new(py, "numeria.exact.bigint")?; + sub.setattr("__doc__", "Arbitrary-precision signed integers. Magnitudes are little-endian vectors of `u64` limbs in base 2^64, held in a canonical form: no trailing zero limbs, and the limb vector is empty exactly when the value is zero. Every operation restores that form, so equality is structural and `is_zero` is a length check.")?; + m_exact__bigint::register(py, &sub)?; + mods["exact"].add("bigint", &sub)?; + mods.insert("exact::bigint", sub); + } + { + let sub = PyModule::new(py, "numeria.exact.contfrac")?; + sub.setattr("__doc__", "Continued fractions: expansions, convergents, the periodic expansion of a square root, Pell's equation, generalized continued fractions by the modified Lentz algorithm, and the Gauss-map statistics.")?; + m_exact__contfrac::register(py, &sub)?; + mods["exact"].add("contfrac", &sub)?; + mods.insert("exact::contfrac", sub); + } + { + let sub = PyModule::new(py, "numeria.exact.polynomial")?; + sub.setattr("__doc__", "Dense univariate polynomials with `f64` coefficients (`Poly`) and with exact rational coefficients (`PolyQ`). Both types store coefficients from lowest to highest degree, so `c[i]` multiplies `x^i`, and both keep that vector trimmed: the last entry of a non-empty coefficient vector is never zero. The zero polynomial is the empty vector, and `Poly::degree` reports `0` for it (use `Poly::is_zero` to tell the zero polynomial from a non-zero constant). `Poly` carries the numerical machinery -- root finding, Sturm sequences, Chebyshev fitting, Pade approximants -- while `PolyQ` carries the exact machinery: subresultant GCDs, content and primitive parts, rational root factoring, and Eisenstein's criterion.")?; + m_exact__polynomial::register(py, &sub)?; + mods["exact"].add("polynomial", &sub)?; + mods.insert("exact::polynomial", sub); + } + { + let sub = PyModule::new(py, "numeria.exact.rational")?; + sub.setattr("__doc__", "Exact rational arithmetic over `BigInt`. Every `Rational` is kept in lowest terms with a strictly positive denominator, so equality is structural and there is exactly one representation of each value. Zero is `0/1`.")?; + m_exact__rational::register(py, &sub)?; + mods["exact"].add("rational", &sub)?; + mods.insert("exact::rational", sub); + } + { + let sub = PyModule::new(py, "numeria.exact.symbolic")?; + sub.setattr("__doc__", "A small computer algebra system over expression trees. Expressions are built from constants, exact rationals, named variables, n-ary sums and products, powers, and the usual elementary functions. The design is numeric-first: everything can be evaluated, differentiated exactly, simplified enough to make cancellation visible, and compiled to a stack machine for repeated evaluation.")?; + m_exact__symbolic::register(py, &sub)?; + mods["exact"].add("symbolic", &sub)?; + mods.insert("exact::symbolic", sub); + } + { + let sub = PyModule::new(py, "numeria.fem.fdtd")?; + sub.setattr("__doc__", "Finite-difference time domain: Maxwell's equations on a Yee grid. # Why the grid is staggered Maxwell's curl equations couple the two fields' time derivatives to each other's spatial derivatives. Yee's arrangement puts `E` and `H` half a cell apart in space *and* half a step apart in time, so that every derivative in the scheme is a centred difference straddling the point it is evaluated at. Nothing is interpolated and nothing is averaged: the update is second-order accurate while using the narrowest possible stencil, and it is explicit, so a step costs one pass over the arrays. The arrangement also makes the discrete divergence of `B` exactly conserved -- the update adds a discrete curl, and the discrete divergence of a discrete curl is identically zero on this grid. A collocated scheme has to enforce that separately or watch it drift. # The Courant limit is not a guideline With `S = c dt / dx`, the scheme's numerical dispersion relation admits a real wavenumber for every real frequency only while `S <= 1` in one dimension, or `S <= 1/sqrt(d)` in `d` dimensions. Past that the scheme has a mode that grows geometrically, and it grows from rounding noise if nothing else. This is not accuracy degrading gently; it is a hard threshold, and `fdtd_courant_check` reports which side of it a set of parameters falls on. # The magic time step At exactly `S = 1` in one dimension the numerical dispersion relation becomes the exact one, and the update degenerates into a shift: a pulse moves one cell per step with its shape unchanged, to machine precision, forever. One dimension is the only place this happens -- in two or three the dispersion error depends on the propagation angle and cannot be cancelled at all angles at once, which is why a two-dimensional simulation is run at a Courant number safely below the limit rather than at it. # Fields are normalised The updates here track `E` and `eta_0 H` rather than `E` and `H`, which removes the free-space impedance from every line of the update and leaves the Courant number as the only coefficient. It also makes the two fields comparable in magnitude, which matters because the conserved energy adds their squares -- in unnormalised units one term would be `1e5` times the other and the sum would be numerical nonsense.")?; + m_fem__fdtd::register(py, &sub)?; + mods["fem"].add("fdtd", &sub)?; + mods.insert("fem::fdtd", sub); + } + { + let sub = PyModule::new(py, "numeria.fem.fem1d")?; + sub.setattr("__doc__", "One-dimensional finite elements for `-(p u')' + q u = f`. # The weak form The strong form asks for a function whose second derivative satisfies the equation pointwise. Multiplying by a test function `v` that vanishes wherever `u` is prescribed, integrating over the interval and integrating the second-derivative term by parts gives ```text a(u, v) = integral p u' v' + q u v dx = integral f v dx = L(v) ``` for every admissible `v`. Two things happened in that line. The solution now needs only one derivative rather than two, so a discontinuous `p` -- a layered material -- is admissible instead of fatal. And the boundary term `[p u' v]` that integration by parts produced is where flux conditions enter: prescribe nothing and the method silently imposes zero flux, which is why Neumann conditions are called *natural* and Dirichlet conditions, which have to be built into the space, are called *essential*. # Why the answer is the best one available Galerkin's method asks for the identity to hold not for every `v` but for every `v` in a finite dimensional subspace, and looks for `u_h` in that same subspace. Subtracting the two statements gives Galerkin orthogonality, `a(u - u_h, v_h) = 0` for every `v_h` in the space: the error is `a`-orthogonal to everything representable. When `a` is symmetric and positive definite it is an inner product, orthogonality of the error is exactly the characterisation of an orthogonal projection, and so ```text ||u - u_h||_a <= ||u - v_h||_a for every v_h in the space ``` with a constant of one. The finite element solution is not merely a good approximation in the energy norm; it is *the* best one. Nothing in a finite difference scheme corresponds to this. It is checked directly against the nodal interpolant in the property tests. Equivalently, `u_h` minimises the energy `J(v) = a(v,v)/2 - L(v)` over the space -- the Ritz view -- which is why refining a mesh can only lower the computed energy: the coarse space sits inside the fine one. # A variable coefficient is averaged, not sampled Linear elements have a constant derivative on each element, so the quadrature in the stiffness term integrates `p` against a constant and reproduces its element *average* exactly. That has a consequence worth knowing: the discrete bilinear form still agrees with the true one on the element space itself, so `u_h` is the exact `a`-orthogonal projection of the true solution rather than an approximation of one, and the Pythagoras identity ```text ||u - v_h||_a^2 = ||u - u_h||_a^2 + ||u_h - v_h||_a^2 ``` holds to rounding for every `v_h` in the space. It is not an accident of a smooth `p`: a `p` that jumps *within* an element is averaged the same way, which is the sense in which a finite element method handles a discontinuous coefficient gracefully rather than exactly. # Nodal exactness, and its limits For the pure Poisson problem `-u'' = f` with Dirichlet data, the linear element solution is exact *at the nodes*, to machine precision, on any mesh. The Green's function of `-d^2/dx^2` is piecewise linear with its kink at the source point, so for a mesh node it lies in the element space itself; pairing it against the orthogonal error gives `(u - u_h)(x_i) = 0`. This is a property of the operator, not a lucky cancellation, and it fails the moment either ingredient goes: - a variable `p` makes the Green's function piecewise `int dx/p`, which is not piecewise linear, and nodal exactness disappears; - a reaction term `q` does the same; - for quadratic elements the piecewise linear Green's function of a *vertex* is still in the space, so vertices stay exact, but the one belonging to a midside node kinks in the middle of an element and is not. Quadratic elements are exact at element vertices and merely third-order accurate at the midsides. Nodal exactness also needs the load `integral f phi_i` integrated exactly. Assembly here uses five-point Gauss-Legendre per element, exact through degree nine, so it holds to rounding for polynomial data and to quadrature error otherwise. # Sign conventions Flux conditions are stated with the *outward* normal, so the same `Bc::Neumann` value means the same physical thing at both ends: `p du/dn = g`, which is `-p u'(a) = g` on the left and `p u'(b) = g` on the right. `Bc::Robin` is `p du/dn + alpha u = g` in the same convention, and keeps the stiffness matrix symmetric.")?; + m_fem__fem1d::register(py, &sub)?; + mods["fem"].add("fem1d", &sub)?; + mods.insert("fem::fem1d", sub); + } + { + let sub = PyModule::new(py, "numeria.fem.fem2d")?; + sub.setattr("__doc__", "Triangular finite elements in the plane. # The linear triangle On a triangle the three barycentric coordinates are themselves the linear shape functions, and their gradients are constant. That single fact does most of the work: the stiffness integral `integral grad(phi_i) . grad(phi_j)` has a constant integrand, so it is the gradient product times the triangle's area, with no quadrature involved and no error introduced. The whole element matrix for the Laplacian is ```text K_ij = (b_i b_j + c_i c_j) / (4 A) ``` where `b` and `c` are the edge-opposite coordinate differences and `A` is the signed area. The two-dimensional method inherits everything the one-dimensional one has -- Galerkin orthogonality, energy minimisation, best approximation in the energy norm -- because none of those arguments mentions the dimension. # What the mesh has to guarantee Two conditions matter and they are different in kind. *Conformity* is structural: two triangles meet along a whole shared edge or at a single shared vertex, never at a vertex hanging in the middle of a neighbour's edge. Without it the assembled function is not continuous and the space is not a subspace of `H1`, so the theory does not apply at all. It is checked here by counting: every edge belongs to one triangle or two, never more. *Shape* is quantitative. The interpolation error carries a factor of `1/sin(theta_min)`, so a mesh of slivers converges at the same rate with a much worse constant. `FemMesh2::quality_min_angle` reports the worst angle in the mesh, and uniform refinement leaves it exactly unchanged -- the four children of a triangle are all similar to their parent, which is the property that makes repeated refinement safe and that a red-green or longest-edge scheme has to work to recover. # Delaunay and the maximum principle The off-diagonal stiffness entry for an interior edge is `-(cot alpha + cot beta)/2`, the two angles opposite the edge in the triangles sharing it. It is nonpositive exactly when those angles sum to no more than `pi` -- which is the Delaunay condition. So a Delaunay triangulation gives an M-matrix, and an M-matrix gives a discrete maximum principle: a nonnegative load produces a nonnegative solution, and a harmonic one attains its extremes on the boundary. On a badly shaped non-Delaunay mesh the discrete solution can overshoot its own boundary data while still converging, which is exactly the kind of defect a plausibility check on a picture would miss.")?; + m_fem__fem2d::register(py, &sub)?; + mods["fem"].add("fem2d", &sub)?; + mods.insert("fem::fem2d", sub); + } + { + let sub = PyModule::new(py, "numeria.fem.spectral_pde")?; + sub.setattr("__doc__", "Spectral methods: global basis functions instead of local ones. # What changes when the basis stops being local A finite element expands the solution in functions that are nonzero on one or two cells. The matrix is sparse, and the accuracy is whatever the polynomial degree gives -- `h^2`, `h^3`, a fixed power of the mesh size no matter how smooth the answer is. A spectral method expands in functions that are nonzero everywhere and smooth: complex exponentials on a periodic domain, Chebyshev polynomials on an interval. The matrix becomes dense, and in exchange the error stops obeying any fixed power of `N` at all. For an analytic function it falls geometrically -- adding a few points multiplies the error by a constant factor rather than reducing it by a fixed order -- and for a function with `k` continuous derivatives it falls as `N^-k`. The method is only as good as the solution is smooth, and it is *exactly* as good as that. Both halves are measured in the tests rather than asserted. # Two Poisson solvers that are not the same solver `transforms::fft::fft_poisson_2d` already solves the periodic Poisson problem with an FFT, but it is not a spectral method. It divides by the eigenvalue of the *five-point* Laplacian, `(2 cos kx + 2 cos ky - 4)/h^2`, which makes the discrete residual vanish to rounding -- exactly what a pressure projection in a fluid solver wants, since there the finite-difference divergence is the thing that must be zero. Against the continuum it is second-order accurate and no better. `spectral_poisson_periodic` divides by the true symbol `-k^2`. Its discrete residual is not zero, and its error against the continuum solution is nil for anything the grid can represent and geometrically small otherwise. The two answers differ by `O(h^2)`, and which one is wanted depends on whether the discrete operator or the differential one is the thing being solved. # Chebyshev points cluster, and they have to Interpolating at equally spaced points on an interval diverges as the degree grows, even for functions as tame as `1/(1+25x^2)` -- Runge's phenomenon, and it is not a rounding problem but a property of the Lebesgue constant, which grows like `2^N/(N log N)`. The Chebyshev points `cos(j pi / N)` cluster towards the ends at a density that makes the Lebesgue constant grow only logarithmically, which is what makes high-degree interpolation usable at all.")?; + m_fem__spectral_pde::register(py, &sub)?; + mods["fem"].add("spectral_pde", &sub)?; + mods.insert("fem::spectral_pde", sub); + } + { + let sub = PyModule::new(py, "numeria.finance.options")?; + sub.setattr("__doc__", "Option pricing: closed forms, lattices, Monte Carlo and a PDE solver. # Conventions Rates and volatilities are continuously compounded and annualised; time is in years. `q` is a continuous dividend yield, which also serves as a foreign interest rate for a currency option and as a convenience yield for a commodity. A `call: bool` argument names the payoff: `max(S - K, 0)` when true and `max(K - S, 0)` when false. # Why there are so many methods for one number They price different things, and where they overlap they check each other. `black_scholes` is exact but only for a European payoff on a lognormal process. A lattice (`binomial_crr`, `trinomial`) handles early exercise, at the cost of converging to the closed form only in the limit -- and it converges by oscillating around the answer, not by approaching it from one side. Monte Carlo (`monte_carlo_european` and the path-dependent payoffs) handles anything you can simulate, and pays for that with an error that falls like the square root of the path count, which is why the variance reduction here is not an optimisation but the difference between usable and not. # The volatility argument is the whole problem Black-Scholes takes one volatility for all strikes. Real option prices do not admit one: the implied volatilities of options on the same underlying and expiry form a smile, and a model with a single sigma cannot produce it. That is not a defect in the arithmetic, it is the lognormal assumption failing. `merton_jump_price` and the Heston model add mechanisms that generate a smile, and `volatility_smile_svi` simply parameterises one without a mechanism.")?; + m_finance__options::register(py, &sub)?; + mods["finance"].add("options", &sub)?; + mods.insert("finance::options", sub); + } + { + let sub = PyModule::new(py, "numeria.finance.portfolio")?; + sub.setattr("__doc__", "Portfolio construction and performance measurement. # What mean-variance optimisation actually does Markowitz's problem is: given expected returns and a covariance matrix, find the weights minimising variance at each level of expected return. It has a closed form, and that is both its appeal and its trap. The optimiser is an *error maximiser*: it puts weight where the estimated return is highest relative to the estimated risk, which is exactly where the estimates are most likely to be wrong. Expected returns estimated from a decade of monthly data carry standard errors of the same order as the differences between assets, so the \"optimal\" portfolio is often a leveraged bet on estimation noise. Nothing here shrinks, regularises or constrains, because the roadmap's signatures do not. `min_variance_weights` uses only the covariance matrix, which is estimated far more reliably than the mean, and is for that reason the one output here that survives contact with real data. # Returns compound, and that decides which average to use `returns_from_prices` gives simple returns, whose *arithmetic* mean is the expected one-period return. `log_returns` gives continuously compounded returns, which add across periods, so their *sum* is the total log return. Mixing them up produces the standard error of quoting an arithmetic mean as though it were achievable: a series that gains 50% then loses 50% has an arithmetic mean return of zero and has lost a quarter of its value.")?; + m_finance__portfolio::register(py, &sub)?; + mods["finance"].add("portfolio", &sub)?; + mods.insert("finance::portfolio", sub); + } + { + let sub = PyModule::new(py, "numeria.finance.rates")?; + sub.setattr("__doc__", "Interest rates: discounting, bonds, curves and short-rate models. # Two things a \"rate\" can mean A quoted rate is meaningless without its compounding convention. 10% compounded annually, semi-annually and continuously produce growth factors of 1.1, 1.1025 and 1.10517 over a year -- differences that are small over one period and decisive over thirty. `Compounding` makes the convention explicit at every call site rather than leaving it to a comment, and `equivalent_rate` converts between them. The second distinction is between a *zero rate*, which discounts a single payment at one maturity, and a *yield*, which is the single rate that reproduces a whole bond's price. They coincide only for a zero-coupon bond. A coupon bond's yield is a weighted average of the zero rates along its life, weighted by the discounted cashflows -- so two bonds of the same maturity and different coupons have different yields off the same curve, which is what makes a yield a property of the instrument rather than of the market. # What is solved and what is assumed `irr`, `ytm_solve` and `bootstrap_zero_curve` invert a price to find a rate, and each has a uniqueness condition that the documentation states and the code checks where it can. A yield always exists and is unique for a bond with positive cashflows; an internal rate of return need not be either, and the sign-change test is the only cheap guarantee available.")?; + m_finance__rates::register(py, &sub)?; + mods["finance"].add("rates", &sub)?; + mods.insert("finance::rates", sub); + } + { + let sub = PyModule::new(py, "numeria.finance.risk")?; + sub.setattr("__doc__", "Risk measurement: value at risk, expected shortfall, backtesting. # What value at risk does and does not tell you VaR at confidence `1 - alpha` is a *quantile*: the loss that will be exceeded on a fraction `alpha` of days. It says nothing whatever about how much worse things get beyond it, and that is not a subtlety but the central objection to the measure. Two portfolios with identical VaR can have completely different tails, and the one with the fatter tail is the one that ends the firm. `cvar_historical` -- expected shortfall -- answers the question VaR ducks: the *average* loss given that VaR is exceeded. It is also *coherent* where VaR is not: VaR can penalise diversification, saying a combined portfolio is riskier than the sum of its parts, because a quantile is not subadditive. Expected shortfall cannot do that. Since Basel III, expected shortfall is the regulatory measure and VaR is the one everyone still quotes. # Sign convention Every function here returns a **positive number for a loss**. A VaR of 0.023 means a 2.3% loss. This is the industry convention and it is the opposite of the return series' own sign, which is a standing source of confusion; the tests pin it down explicitly.")?; + m_finance__risk::register(py, &sub)?; + mods["finance"].add("risk", &sub)?; + mods.insert("finance::risk", sub); + } + { + let sub = PyModule::new(py, "numeria.fractals.attractors")?; + sub.setattr("__doc__", "Strange attractors: 3-D chaotic flows and 2-D chaotic maps with trajectory integration, Lyapunov spectra (Benettin renormalization), Kaplan-Yorke dimension, Poincaré sections, bifurcation diagrams, and dimension estimators (Grassberger-Procaccia correlation dimension, box counting, Rosenstein's largest-Lyapunov method, Feigenbaum ratios).")?; + m_fractals__attractors::register(py, &sub)?; + mods["fractals"].add("attractors", &sub)?; + mods.insert("fractals::attractors", sub); + } + { + let sub = PyModule::new(py, "numeria.fractals.automata")?; + sub.setattr("__doc__", "Cellular automata and growth models: elementary 1-D rules, life-like 2-D automata with pattern/RLE placement, cyclic CA, Langton's ant and turmites, Brian's Brain, Wireworld, 3-D life-like rules, SmoothLife and Lenia (direct convolution), abelian sandpiles, stochastic lattice models (forest fire, Greenberg-Hastings, majority/voter dynamics, Schelling segregation), reaction-diffusion systems (Gray-Scott, Gierer-Meinhardt, FitzHugh-Nagumo, Oregonator, Brusselator), and aggregation/percolation (DLA, Eden growth, invasion percolation).")?; + m_fractals__automata::register(py, &sub)?; + mods["fractals"].add("automata", &sub)?; + mods.insert("fractals::automata", sub); + } + { + let sub = PyModule::new(py, "numeria.fractals.escape_time")?; + sub.setattr("__doc__", "Escape-time fractals: a generic iteration engine with smooth coloring, orbit traps, and distance estimation, the classic quadratic families (Mandelbrot, Julia, tricorn, burning ship), Newton/nova and magnet fractals, Lyapunov fractals, Buddhabrot accumulation, and perturbation iteration for deep zooms.")?; + m_fractals__escape_time::register(py, &sub)?; + mods["fractals"].add("escape_time", &sub)?; + mods.insert("fractals::escape_time", sub); + } + { + let sub = PyModule::new(py, "numeria.fractals.ifs")?; + sub.setattr("__doc__", "Iterated function systems: the chaos game and deterministic attractor construction (Barnsley, \"Fractals Everywhere\", 1988), Moran similarity dimension, collage error, a library of classic IFS presets in 2-D and 3-D, and Draves-style fractal flames.")?; + m_fractals__ifs::register(py, &sub)?; + mods["fractals"].add("ifs", &sub)?; + mods.insert("fractals::ifs", sub); + } + { + let sub = PyModule::new(py, "numeria.fractals.lsystem")?; + sub.setattr("__doc__", "Lindenmayer systems: parallel string rewriting with simple, stochastic, and context-sensitive rules, 2-D and 3-D turtle interpretation of the ABOP alphabet (Prusinkiewicz & Lindenmayer, \"The Algorithmic Beauty of Plants\", 1990), and a library of classic presets. Parametric modules are out of scope: the rule set here covers character rewriting only.")?; + m_fractals__lsystem::register(py, &sub)?; + mods["fractals"].add("lsystem", &sub)?; + mods.insert("fractals::lsystem", sub); + } + { + let sub = PyModule::new(py, "numeria.fractals.noise")?; + sub.setattr("__doc__", "Coherent noise: Perlin gradient noise (Perlin 2002), OpenSimplex2 (ported from K.jpg's reference implementation), value noise, Worley cellular noise, fractal combinators (fBm, turbulence, ridged and hybrid multifractals, domain warping, curl noise), and terrain synthesis (diamond-square, spectral synthesis, thermal and hydraulic erosion, void-and-cluster blue noise).")?; + m_fractals__noise::register(py, &sub)?; + mods["fractals"].add("noise", &sub)?; + mods.insert("fractals::noise", sub); + } + { + let sub = PyModule::new(py, "numeria.geometry.delaunay")?; + sub.setattr("__doc__", "Delaunay triangulation and Voronoi diagrams in the plane. Triangulation: Bowyer-Watson incremental insertion with a super-triangle. Voronoi cells: half-plane intersection of the perpendicular bisectors (the dual definition), clipped to the bounding box of the sites — robust for boundary cells.")?; + m_geometry__delaunay::register(py, &sub)?; + mods["geometry"].add("delaunay", &sub)?; + mods.insert("geometry::delaunay", sub); + } + { + let sub = PyModule::new(py, "numeria.geometry.geodesy")?; + sub.setattr("__doc__", "Geodesy on a reference ellipsoid. Vincenty's inverse and direct formulae (Vincenty, \"Direct and inverse solutions of geodesics on the ellipsoid\", Survey Review 1975) plus geodetic ↔ ECEF ↔ ENU coordinate conversions. Angles are radians; distances and heights are meters.")?; + m_geometry__geodesy::register(py, &sub)?; + mods["geometry"].add("geodesy", &sub)?; + mods.insert("geometry::geodesy", sub); + } + { + let sub = PyModule::new(py, "numeria.geometry.hull")?; + sub.setattr("__doc__", "Convex hulls and polygon predicates. 2-D hull: Andrew's monotone chain (O(n log n)), CCW output. 3-D hull: incremental visible-face (quickhull-style) algorithm returning triangle index triples with outward-facing orientation.")?; + m_geometry__hull::register(py, &sub)?; + mods["geometry"].add("hull", &sub)?; + mods.insert("geometry::hull", sub); + } + { + let sub = PyModule::new(py, "numeria.geometry.mesh")?; + sub.setattr("__doc__", "Minimal indexed triangle mesh with ray intersection, backfilling the Part 2 `Mesh` type consumed by acoustics ray tracing and display helpers.")?; + m_geometry__mesh::register(py, &sub)?; + mods["geometry"].add("mesh", &sub)?; + mods.insert("geometry::mesh", sub); + } + { + let sub = PyModule::new(py, "numeria.graph.coloring")?; + sub.setattr("__doc__", "Colouring, cliques, independent sets, and covers. Almost everything here is NP-hard in general, so the module is split deliberately between two kinds of routine. The heuristics -- greedy colouring, Welsh-Powell, the two-approximation for vertex cover, the greedy dominating set -- run on any graph and come with a stated guarantee, usually a bound relative to a structural parameter rather than to the optimum. The exact routines carry `_small` or `_exact` in their names and are honest about the size they can take: they enumerate, and the cost is exponential. The exception is Vizing's edge colouring, which is exact-ish for free: the theorem says `Delta` or `Delta + 1` colours always suffice, and the Misra-Gries construction reaches `Delta + 1` in polynomial time. Which of the two a given graph needs is itself NP-hard to decide.")?; + m_graph__coloring::register(py, &sub)?; + mods["graph"].add("coloring", &sub)?; + mods.insert("graph::coloring", sub); + } + { + let sub = PyModule::new(py, "numeria.graph.core")?; + sub.setattr("__doc__", "Graphs: representation, structural queries, generators, and products. A `Graph` is an adjacency list of weighted arcs over the vertices `0..n`. An undirected graph stores each edge in both directions, so degree, traversal and neighbour iteration need no special case; `Graph::edges` reports each undirected edge once. Weights are `f64` and default to one. Structural queries here ignore them; the shortest-path and flow modules use them.")?; + m_graph__core::register(py, &sub)?; + mods["graph"].add("core", &sub)?; + mods.insert("graph::core", sub); + } + { + let sub = PyModule::new(py, "numeria.graph.flow")?; + sub.setattr("__doc__", "Network flow: maximum flow, minimum cut, and the problems that reduce to them. A flow network is a `Graph` whose weights are capacities. An undirected edge is treated as a pair of arcs, each with the full capacity, which is the usual convention: flow may run either way but not both at once. Capacities must be finite and non-negative. The residual graph is built internally as an arc list with paired indices, so the reverse arc of arc `i` is arc `i ^ 1`.")?; + m_graph__flow::register(py, &sub)?; + mods["graph"].add("flow", &sub)?; + mods.insert("graph::flow", sub); + } + { + let sub = PyModule::new(py, "numeria.graph.layout")?; + sub.setattr("__doc__", "Graph drawing: where to put the vertices. Two families. The *metric* layouts -- Kamada-Kawai, stress majorization, Fruchterman-Reingold, spectral -- treat drawing as optimisation: pick a target distance for every pair, usually the number of edges between them, and place the points so the drawn distances match. What they optimise is stated exactly, so what they achieve can be measured, which is why `layout_stress` is public. The *structural* layouts -- circular, shell, Reingold-Tilford, Sugiyama -- draw a shape the graph already has. They are not approximating anything, and their output satisfies exact statements: a tree drawn by Reingold-Tilford has every parent centred over its children and no two subtrees overlapping, and a layered drawing of an acyclic graph has every arc pointing downward. Planarity sits apart from both: `planarity_test` answers whether a crossing-free drawing exists at all, and `planar_embedding_small` produces the combinatorial structure of one.")?; + m_graph__layout::register(py, &sub)?; + mods["graph"].add("layout", &sub)?; + mods.insert("graph::layout", sub); + } + { + let sub = PyModule::new(py, "numeria.graph.matching")?; + sub.setattr("__doc__", "Matchings: bipartite, general, weighted, and stable. A matching is a set of edges no two of which share a vertex. It is returned as a partner array: `m[v]` is the vertex matched to `v`, or `None` when `v` is unmatched. That form is symmetric by construction, so `m[m[v]] == v` whenever `m[v]` is `Some`.")?; + m_graph__matching::register(py, &sub)?; + mods["graph"].add("matching", &sub)?; + mods.insert("graph::matching", sub); + } + { + let sub = PyModule::new(py, "numeria.graph.paths")?; + sub.setattr("__doc__", "Shortest paths, spanning trees, and tours. Distances are `f64` and an unreachable vertex is `f64::INFINITY`, so the results compose without an `Option` at every step. Predecessor arrays use `None` for the source and for unreachable vertices alike; the distance distinguishes the two.")?; + m_graph__paths::register(py, &sub)?; + mods["graph"].add("paths", &sub)?; + mods.insert("graph::paths", sub); + } + { + let sub = PyModule::new(py, "numeria.graph.spectral")?; + sub.setattr("__doc__", "Spectral graph theory: Laplacians, centralities, resistances, and community detection. The Laplacian `L = D - A` is the object almost everything here rests on. It is symmetric positive semi-definite for an undirected graph, its smallest eigenvalue is always zero with the all-ones eigenvector, and the multiplicity of that zero is the number of connected components. The second-smallest eigenvalue -- the algebraic connectivity -- measures how hard the graph is to cut, and its eigenvector orders the vertices in a way that separates the graph well. Weights are treated as edge multiplicities where that makes sense (Laplacian, resistance, random walks) and ignored where it does not (the combinatorial centralities, which count edges).")?; + m_graph__spectral::register(py, &sub)?; + mods["graph"].add("spectral", &sub)?; + mods.insert("graph::spectral", sub); + } + { + let sub = PyModule::new(py, "numeria.learn.cluster")?; + sub.setattr("__doc__", "Clustering, mixture models and nearest neighbours. # Clustering has no ground truth, so the tests need invariants Nothing here has a right answer to compare against. What it has instead is a supply of exact statements, and those are what the tests use: *Lloyd's algorithm cannot go uphill.* Each half of a k-means iteration -- reassigning points to their nearest centre, then moving each centre to its cluster's mean -- minimises the same objective over one of its two arguments, so the inertia is non-increasing and the algorithm terminates in finitely many steps. There are finitely many assignments and none repeats. *Expectation-maximisation cannot go downhill.* The same argument in the other direction: each step maximises a lower bound that touches the log-likelihood at the current parameters, so the likelihood climbs monotonically. Both are asserted step by step rather than end to end, because a monotone sequence is a much sharper claim than an improved endpoint. *A label is not a name.* Cluster indices are arbitrary, so every comparison between two clusterings has to be invariant under relabelling either of them. `adjusted_rand_index` is, exactly, and it is corrected for chance so that two independent random partitions score about zero rather than about a half. # Where the guarantees stop, and why that is worth saying Single and complete linkage produce merge heights that never decrease, so their dendrograms can be drawn without crossings. *Centroid linkage does not.* Merging two clusters moves their centre to somewhere between them, which can be closer to a third cluster than either original was, and the dendrogram then contains an inversion. That is a property of the method, not a bug in it, and `Linkage::Centroid` is documented and tested as inverting rather than quietly producing dendrograms nobody should draw. DBSCAN's core points are determined by the data alone and do not depend on the order it arrives in. Its *border* points can: a point within reach of two clusters joins whichever claimed it first. That asymmetry is in the algorithm as Ester and colleagues defined it, and pretending otherwise would mean inventing a tie-break and calling it DBSCAN.")?; + m_learn__cluster::register(py, &sub)?; + mods["learn"].add("cluster", &sub)?; + mods.insert("learn::cluster", sub); + } + { + let sub = PyModule::new(py, "numeria.learn.gp")?; + sub.setattr("__doc__", "Gaussian process regression. # A distribution over functions, conditioned A Gaussian process says that any finite set of function values is jointly normal, with a covariance given by the kernel. Regression is then not fitting but conditioning: the posterior over an unobserved point is the conditional of a multivariate normal, and that has a closed form. There is no optimisation anywhere in `Gp::fit` -- it is one Cholesky factorisation, and the answer is exact given the kernel. Two consequences are worth stating because they surprise people and because they are exactly testable. *The posterior variance does not depend on what was observed.* It is `k(x,x) - k_*^T K^-1 k_*`, and `y` does not appear. Uncertainty in a Gaussian process is a statement about where the data *is*, not about what it said. Doubling every observation doubles the mean and leaves every error bar alone. *With no noise the mean interpolates exactly and the variance vanishes at the data.* The conditional of a normal on one of its own coordinates is a point mass. Adding noise is what turns interpolation into smoothing, and the residual at the data grows from zero in proportion to it. # Which kernel is a modelling choice, not a detail The kernel *is* the prior. A squared exponential asserts that the function is infinitely differentiable, which is a very strong claim and the reason its posterior can look implausibly smooth between widely spaced points. The Matern family asserts a finite number of derivatives -- `3/2` gives one, `5/2` gives two -- and is usually the better default for anything physical. A periodic kernel asserts exact periodicity, and `KernelFn::Periodic` satisfies `k(x, x + p) = k(x, x)` to rounding rather than approximately. Kernels are closed under addition and multiplication, which is what `KernelFn::Sum` and `KernelFn::Product` are for: a sum models additive structure (a trend plus a wiggle), a product models interaction (a periodicity whose amplitude decays). # The marginal likelihood balances fit against complexity on its own `log p(y | X)` splits into a data-fit term `-y^T K^-1 y / 2` and a complexity penalty `-log|K| / 2`. Making the kernel more flexible improves the first and costs the second, and the trade is not a hyperparameter anyone chose -- it falls out of the normalisation of a probability distribution. That is why hyperparameters can be tuned by maximising it without a validation set.")?; + m_learn__gp::register(py, &sub)?; + mods["learn"].add("gp", &sub)?; + mods.insert("learn::gp", sub); + } + { + let sub = PyModule::new(py, "numeria.learn.nn")?; + sub.setattr("__doc__", "Feed-forward networks, trained by backpropagation. # Backpropagation is the chain rule with the products reassociated The derivative of the loss with respect to an early weight is a product of Jacobians, one per layer. Multiplying them left to right costs a matrix-matrix product per layer; multiplying right to left, starting from the scalar loss, costs a matrix-*vector* product per layer. Backpropagation is the second association, and that is the whole of it. It is not an approximation and it is not specific to neural networks -- it is reverse-mode differentiation, and the cost of one gradient is a small multiple of the cost of one forward pass however many parameters there are. Which is why `Mlp::numerical_grad_check` is the test that matters. Descent will reduce a loss using wrong gradients, just more slowly and towards somewhere else, so a falling training curve is no evidence at all. A central difference agreeing with the analytic gradient to eight digits is. # Softmax and cross-entropy belong together Taken separately, softmax has a Jacobian and cross-entropy has a gradient, and composing them involves a matrix. Taken together the product collapses: the gradient of cross-entropy with respect to the *logits* is exactly `p - y`, the predicted distribution minus the target. That cancellation is worth having for accuracy as well as speed -- computing the two separately loses precision exactly where the network is confident and the softmax output is near zero or one. The two are therefore fused here, and `Loss::CrossEntropy` requires `Act::Softmax` on the output layer. # Initialisation is not cosmetic Weights start from a scaled normal draw -- the He scaling `sqrt(2/fan_in)` for rectifiers, the Xavier scaling `sqrt(1/fan_in)` otherwise. Initialising everything to zero makes every hidden unit in a layer compute the same thing and receive the same gradient forever, so the layer has one effective unit no matter how wide it is; initialising too large saturates the sigmoid and tanh, whose derivative is then near zero and whose gradient therefore vanishes.")?; + m_learn__nn::register(py, &sub)?; + mods["learn"].add("nn", &sub)?; + mods.insert("learn::nn", sub); + } + { + let sub = PyModule::new(py, "numeria.learn.tree")?; + sub.setattr("__doc__", "Decision trees, random forests and gradient boosting. # What a tree does that a linear model cannot A decision tree asks a sequence of threshold questions about single features. Three consequences follow, and they are what the method is for rather than incidental to it. *Scale does not matter.* A threshold on a feature is decided by the order of its values, not their magnitudes, so multiplying a column by a thousand changes the thresholds and nothing else -- the tree computes the same function and the predictions are identical. Nothing that measures a distance can say that: k-nearest-neighbours, k-means and a Gaussian process all change their answers entirely under the same rescaling. This is asserted directly. *Interactions come free.* A split below a split conditions on the first, so a tree represents `x > a AND y > b` without anyone writing the product term. *Nothing is extrapolated.* Every prediction is a leaf's summary of the training points that reached it, so a tree's output outside the training range is flat. That is honest and it is also useless for trend extrapolation, which is the usual reason to reach for something else. # The impurity decrease is never negative A split is chosen to minimise the weighted impurity of its two children, and refusing to split is always available, so the decrease recorded at every node is at least zero. Feature importances are sums of those decreases, weighted by how many samples passed through, so they are nonnegative and they sum to exactly the total impurity the tree removed. Both are checked rather than assumed. # A single tree overfits by construction Grown without limit, a tree separates every training point that can be separated, and its training error reaches zero. That number is therefore worthless as evidence of anything, in the same way a 1-nearest-neighbour training error is. What the ensembles do about it differs: - a **random forest** grows many deep trees on bootstrap samples with a random subset of features considered at each split, and averages them. The trees are individually overfitted and their errors are decorrelated, so averaging cancels the variance without adding bias. - **gradient boosting** grows shallow trees in sequence, each fitted to what the previous ones got wrong. The trees are individually underfitted and the bias comes down step by step, which is why the learning rate matters and why the round count is what has to be stopped early. The two are opposite strategies and neither is a variant of the other.")?; + m_learn__tree::register(py, &sub)?; + mods["learn"].add("tree", &sub)?; + mods.insert("learn::tree", sub); + } + { + let sub = PyModule::new(py, "numeria.linalg.cholesky")?; + sub.setattr("__doc__", "Cholesky factorization of symmetric positive-definite matrices. Reference: Golub & Van Loan, *Matrix Computations*, §4.2: A = L·Lᵀ with L lower triangular and positive diagonal.")?; + m_linalg__cholesky::register(py, &sub)?; + mods["linalg"].add("cholesky", &sub)?; + mods.insert("linalg::cholesky", sub); + } + { + let sub = PyModule::new(py, "numeria.linalg.eigen")?; + sub.setattr("__doc__", "Eigenvalue solvers. Symmetric matrices use the cyclic Jacobi rotation method (Golub & Van Loan §8.5), which is unconditionally convergent. General real matrices are reduced to upper Hessenberg form by Gaussian similarity transformations and their eigenvalues extracted with the Francis-shift QR iteration (Wilkinson, *The Algebraic Eigenvalue Problem*; the classic `hqr` algorithm).")?; + m_linalg__eigen::register(py, &sub)?; + mods["linalg"].add("eigen", &sub)?; + mods.insert("linalg::eigen", sub); + } + { + let sub = PyModule::new(py, "numeria.linalg.lu")?; + sub.setattr("__doc__", "LU decomposition with partial pivoting (Doolittle form). Reference: Golub & Van Loan, *Matrix Computations*, §3.4.")?; + m_linalg__lu::register(py, &sub)?; + mods["linalg"].add("lu", &sub)?; + mods.insert("linalg::lu", sub); + } + { + let sub = PyModule::new(py, "numeria.linalg.matrix")?; + sub.setattr("__doc__", "Dense row-major matrix of `f64`.")?; + m_linalg__matrix::register(py, &sub)?; + mods["linalg"].add("matrix", &sub)?; + mods.insert("linalg::matrix", sub); + } + { + let sub = PyModule::new(py, "numeria.linalg.qr")?; + sub.setattr("__doc__", "QR decomposition by Householder reflections and least-squares solve. Reference: Golub & Van Loan, *Matrix Computations*, §5.2: A = Q·R with Q orthogonal (m×m) and R upper trapezoidal (m×n).")?; + m_linalg__qr::register(py, &sub)?; + mods["linalg"].add("qr", &sub)?; + mods.insert("linalg::qr", sub); + } + { + let sub = PyModule::new(py, "numeria.linalg.sparse")?; + sub.setattr("__doc__", "Compressed sparse row (CSR) matrices and conjugate-gradient solvers. Reference: Golub & Van Loan §11.5 (CG), Saad, *Iterative Methods for Sparse Linear Systems* §9.2 (Jacobi-preconditioned CG).")?; + m_linalg__sparse::register(py, &sub)?; + mods["linalg"].add("sparse", &sub)?; + mods.insert("linalg::sparse", sub); + } + { + let sub = PyModule::new(py, "numeria.linalg.svd")?; + sub.setattr("__doc__", "Singular value decomposition by one-sided Jacobi rotations. Reference: Golub & Van Loan §8.6 / Demmel & Veselić, \"Jacobi's method is more accurate than QR\". Produces the thin decomposition A = U·Σ·Vᵀ with U m×n (orthonormal columns where σ > 0), Σ the non-negative singular values in descending order, and Vᵀ n×n.")?; + m_linalg__svd::register(py, &sub)?; + mods["linalg"].add("svd", &sub)?; + mods.insert("linalg::svd", sub); + } + { + let sub = PyModule::new(py, "numeria.linalg.tridiagonal")?; + sub.setattr("__doc__", "Tridiagonal linear solve (Thomas algorithm). Reference: Press et al., *Numerical Recipes*, §2.4. Solves `sub[i-1]·x[i-1] + diag[i]·x[i] + sup[i]·x[i+1] = rhs[i]` in O(n).")?; + m_linalg__tridiagonal::register(py, &sub)?; + mods["linalg"].add("tridiagonal", &sub)?; + mods.insert("linalg::tridiagonal", sub); + } + { + let sub = PyModule::new(py, "numeria.manifold.clifford")?; + sub.setattr("__doc__", "Clifford (geometric) algebras Cl(p, q, r): a dense multivector type over any signature, with the geometric/outer/inner products, versors and rotors, and specialized models — Euclidean `cl3`, projective `pga3`, conformal `cga3`, and spacetime `sta` geometric algebra.")?; + m_manifold__clifford::register(py, &sub)?; + mods["manifold"].add("clifford", &sub)?; + mods.insert("manifold::clifford", sub); + } + { + let sub = PyModule::new(py, "numeria.manifold.dec")?; + sub.setattr("__doc__", "Discrete exterior calculus on triangle meshes: exterior derivatives, diagonal Hodge stars, Laplacians, Hodge decomposition, harmonic forms and Betti numbers, heat and Poisson solves, spectral shape analysis, curvature flows, and persistent homology.")?; + m_manifold__dec::register(py, &sub)?; + mods["manifold"].add("dec", &sub)?; + mods.insert("manifold::dec", sub); + } + { + let sub = PyModule::new(py, "numeria.manifold.embedding")?; + sub.setattr("__doc__", "Manifold learning and dimensionality reduction: spectral embeddings (MDS, Isomap, LLE, Laplacian eigenmaps, diffusion maps), PCA and kernel PCA, stochastic neighbor embeddings, intrinsic-dimension estimators, embedding quality metrics, benchmark datasets, and optimization on matrix manifolds.")?; + m_manifold__embedding::register(py, &sub)?; + mods["manifold"].add("embedding", &sub)?; + mods.insert("manifold::embedding", sub); + } + { + let sub = PyModule::new(py, "numeria.manifold.geodesic")?; + sub.setattr("__doc__", "Geodesics, parallel transport, Jacobi fields, and relativistic orbits, all driven by the finite-difference `Metric` machinery.")?; + m_manifold__geodesic::register(py, &sub)?; + mods["manifold"].add("geodesic", &sub)?; + mods.insert("manifold::geodesic", sub); + } + { + let sub = PyModule::new(py, "numeria.manifold.hyperbolic")?; + sub.setattr("__doc__", "Hyperbolic geometry across the standard models: Poincare disk/ball, upper half-plane/space, Klein disk, and the hyperboloid, with isometries, trigonometry, tilings, and low-distortion embeddings.")?; + m_manifold__hyperbolic::register(py, &sub)?; + mods["manifold"].add("hyperbolic", &sub)?; + mods.insert("manifold::hyperbolic", sub); + } + { + let sub = PyModule::new(py, "numeria.manifold.lie")?; + sub.setattr("__doc__", "Lie groups and algebras: rotation and rigid-motion groups in 2/3/4 dimensions, SU(2) and SL(2) groups, matrix exponentials and logarithms, representation-theory helpers (Wigner d, Clebsch-Gordan), and estimation algorithms on these manifolds (pose graphs, hand-eye, Umeyama).")?; + m_manifold__lie::register(py, &sub)?; + mods["manifold"].add("lie", &sub)?; + mods.insert("manifold::lie", sub); + } + { + let sub = PyModule::new(py, "numeria.manifold.metric")?; + sub.setattr("__doc__", "Metric geometry on n-dimensional manifolds: a metric is a function from coordinates to a matrix g_ij, and everything else — Christoffel symbols, Riemann/Ricci/Weyl curvature, covariant derivatives, geodesic machinery inputs — is derived from it by finite differences.")?; + m_manifold__metric::register(py, &sub)?; + mods["manifold"].add("metric", &sub)?; + mods.insert("manifold::metric", sub); + } + { + let sub = PyModule::new(py, "numeria.manifold.polytope4")?; + sub.setattr("__doc__", "Four-dimensional polytopes: the six regular 4-polytopes with their full combinatorics, prisms and products, projections and cross-sections, duals, Coxeter-plane pictures, exceptional root systems and lattices, and curse-of-dimensionality demonstrations.")?; + m_manifold__polytope4::register(py, &sub)?; + mods["manifold"].add("polytope4", &sub)?; + mods.insert("manifold::polytope4", sub); + } + { + let sub = PyModule::new(py, "numeria.manifold.spacetime")?; + sub.setattr("__doc__", "Special and general relativity: four-vectors and Lorentz transforms, Rindler and Kruskal coordinates, Schwarzschild and Kerr geodesics, gravitational lensing, black hole thermodynamics, cosmological distances, inspiral waveforms, and Kaluza-Klein reduction. Kinematics and geometry use geometric units (G = c = 1) with the mostly-minus signature (+, -, -, -) unless stated otherwise; the thermodynamic and cosmological helpers use SI units.")?; + m_manifold__spacetime::register(py, &sub)?; + mods["manifold"].add("spacetime", &sub)?; + mods.insert("manifold::spacetime", sub); + } + { + let sub = PyModule::new(py, "numeria.manifold.spherical")?; + sub.setattr("__doc__", "Spherical geometry: n-sphere maps, spherical trigonometry, map projections, the Hopf fibration, spherical harmonics and their transforms, sky pixelizations, point distributions, and directional statistics.")?; + m_manifold__spherical::register(py, &sub)?; + mods["manifold"].add("spherical", &sub)?; + mods.insert("manifold::spherical", sub); + } + { + let sub = PyModule::new(py, "numeria.manifold.vecn")?; + sub.setattr("__doc__", "n-dimensional vectors and arbitrary-rank tensors: the generic machinery behind the metric-driven differential geometry in this module tree.")?; + m_manifold__vecn::register(py, &sub)?; + mods["manifold"].add("vecn", &sub)?; + mods.insert("manifold::vecn", sub); + } + { + let sub = PyModule::new(py, "numeria.materials.common")?; + sub.setattr("__doc__", "Engineering solids: metals, alloys, polymers and ceramics. Density, Young's modulus, yield and tensile strength, Poisson's ratio, thermal conductivity and expansion, and specific heat. Room-temperature values; a specific alloy, temper or grade will differ, sometimes substantially.")?; + m_materials__common::register(py, &sub)?; + mods["materials"].add("common", &sub)?; + mods.insert("materials::common", sub); + } + { + let sub = PyModule::new(py, "numeria.materials.elements")?; + sub.setattr("__doc__", "The 118 chemical elements. Atomic number, symbol, name, atomic mass, density, melting and boiling points, and thermal and electrical conductivity, with lookup by atomic number, symbol or name. Densities are for the standard state at room temperature, so gases are quoted at STP. Where an element has no stable isotope the atomic mass is that of the longest-lived one, and properties that have never been measured are absent rather than guessed.")?; + m_materials__elements::register(py, &sub)?; + mods["materials"].add("elements", &sub)?; + mods.insert("materials::elements", sub); + } + { + let sub = PyModule::new(py, "numeria.materials.fluids")?; + sub.setattr("__doc__", "Common liquids. Density, dynamic and kinematic viscosity, surface tension, speed of sound, specific heat, and boiling and freezing points, at room temperature and one atmosphere. Viscosity is the strongly temperature-dependent one: it can change by a factor of several over a few tens of degrees, so a single figure is only a starting point.")?; + m_materials__fluids::register(py, &sub)?; + mods["materials"].add("fluids", &sub)?; + mods.insert("materials::fluids", sub); + } + { + let sub = PyModule::new(py, "numeria.materials.gases")?; + sub.setattr("__doc__", "Common gases. Molar mass, density at STP, specific heat at constant pressure and the specific heat ratio `γ`, thermal conductivity, viscosity and the speed of sound. `γ` is the entry most often needed: it fixes the adiabatic relations and the speed of sound `c = √(γRT/M)`, and it follows the molecular structure -- about 5/3 for a monatomic gas, 7/5 for a diatomic one.")?; + m_materials__gases::register(py, &sub)?; + mods["materials"].add("gases", &sub)?; + mods.insert("materials::gases", sub); + } + { + let sub = PyModule::new(py, "numeria.math.constants")?; + sub.setattr("__doc__", "Physical and mathematical constants (NIST CODATA 2018 / 2019 SI redefinition).")?; + m_math__constants::register(py, &sub)?; + mods["math"].add("constants", &sub)?; + mods.insert("math::constants", sub); + } + { + let sub = PyModule::new(py, "numeria.mesh.analyze")?; + sub.setattr("__doc__", "Mesh analysis: topology (manifoldness, orientation, boundary, components, genus), quality statistics, QEM decimation, discrete curvatures, and geodesic distances.")?; + m_mesh__analyze::register(py, &sub)?; + mods["mesh"].add("analyze", &sub)?; + mods.insert("mesh::analyze", sub); + } + { + let sub = PyModule::new(py, "numeria.mesh.generate")?; + sub.setattr("__doc__", "Procedural mesh generators. Closed shapes are watertight (shared seam vertices, no duplicates) with outward-facing counterclockwise winding.")?; + m_mesh__generate::register(py, &sub)?; + mods["mesh"].add("generate", &sub)?; + mods.insert("mesh::generate", sub); + } + { + let sub = PyModule::new(py, "numeria.mesh.isosurface")?; + sub.setattr("__doc__", "Isosurface and isocontour extraction from sampled scalar fields: marching squares/cubes/tetrahedra, surface nets, dual contouring, and metaballs. Convention: a sample is \"inside\" when its value is below the iso level (matching signed distance fields, negative inside). Output triangles are wound counterclockwise seen from the outside (normals point toward values above the iso level); 2-D contours keep the inside region on their left, so they run counterclockwise around regions below the iso level.")?; + m_mesh__isosurface::register(py, &sub)?; + mods["mesh"].add("isosurface", &sub)?; + mods.insert("mesh::isosurface", sub); + } + { + let sub = PyModule::new(py, "numeria.mesh.parameterize")?; + sub.setattr("__doc__", "Mesh parameterization: closed-form spherical/planar/cylindrical projections, harmonic (cotangent-Laplace) disk parameterization with fixed boundaries, least-squares conformal maps (Lévy, Petitjean, Ray & Maillot 2002), and per-triangle conformal and area distortion measures.")?; + m_mesh__parameterize::register(py, &sub)?; + mods["mesh"].add("parameterize", &sub)?; + mods.insert("mesh::parameterize", sub); + } + { + let sub = PyModule::new(py, "numeria.mesh.subdivide")?; + sub.setattr("__doc__", "Subdivision surfaces (Loop, Catmull-Clark, sqrt(3), midpoint) and Laplacian-family smoothing.")?; + m_mesh__subdivide::register(py, &sub)?; + mods["mesh"].add("subdivide", &sub)?; + mods.insert("mesh::subdivide", sub); + } + { + let sub = PyModule::new(py, "numeria.mesh.surfaces")?; + sub.setattr("__doc__", "Parametric surfaces: Bézier/B-spline/NURBS patches, classic surface constructions, differential geometry via fundamental forms, and a catalogue of named surfaces.")?; + m_mesh__surfaces::register(py, &sub)?; + mods["mesh"].add("surfaces", &sub)?; + mods.insert("mesh::surfaces", sub); + } + { + let sub = PyModule::new(py, "numeria.monte_carlo.quasi")?; + sub.setattr("__doc__", "Quasi-random (low-discrepancy) sequences: Sobol and Halton. Sobol points use the Gray-code construction of Bratley & Fox with the Joe-Kuo (new-joe-kuo-6) primitive polynomials and initial direction numbers, embedded here for dimensions up to 21. Halton points use the radical inverse in the first `dim` primes.")?; + m_monte_carlo__quasi::register(py, &sub)?; + mods["monte_carlo"].add("quasi", &sub)?; + mods.insert("monte_carlo::quasi", sub); + } + { + let sub = PyModule::new(py, "numeria.numerical.bvp")?; + sub.setattr("__doc__", "Two-point boundary value problems. `shooting` reduces y'' = f(t, y, y') with y(t0) = y0, y(t1) = y1 to root finding on the initial slope (integrated with Dormand-Prince, slope found with Brent's method). `finite_difference_linear_bvp` discretizes the linear problem y'' + p·y' + q·y = r on a uniform grid and solves the tridiagonal system with the Thomas algorithm (Burden & Faires, *Numerical Analysis*, §11.3).")?; + m_numerical__bvp::register(py, &sub)?; + mods["numerical"].add("bvp", &sub)?; + mods.insert("numerical::bvp", sub); + } + { + let sub = PyModule::new(py, "numeria.numerical.integrate")?; + sub.setattr("__doc__", "Numerical integration (quadrature) rules.")?; + m_numerical__integrate::register(py, &sub)?; + mods["numerical"].add("integrate", &sub)?; + mods.insert("numerical::integrate", sub); + } + { + let sub = PyModule::new(py, "numeria.numerical.interpolate")?; + sub.setattr("__doc__", "Interpolation routines.")?; + m_numerical__interpolate::register(py, &sub)?; + mods["numerical"].add("interpolate", &sub)?; + mods.insert("numerical::interpolate", sub); + } + { + let sub = PyModule::new(py, "numeria.numerical.ode")?; + sub.setattr("__doc__", "Ordinary differential equation solvers.")?; + m_numerical__ode::register(py, &sub)?; + mods["numerical"].add("ode", &sub)?; + mods.insert("numerical::ode", sub); + } + { + let sub = PyModule::new(py, "numeria.numerical.roots")?; + sub.setattr("__doc__", "Scalar and polynomial root finding.")?; + m_numerical__roots::register(py, &sub)?; + mods["numerical"].add("roots", &sub)?; + mods.insert("numerical::roots", sub); + } + { + let sub = PyModule::new(py, "numeria.optimization.convex")?; + sub.setattr("__doc__", "Convex optimisation: gradient methods, quasi-Newton methods, proximal splitting, and constrained solvers. Convexity buys one thing, and it is decisive: every local minimum is global. That removes the question the methods in `optimization::metaheuristics` spend all their effort on -- where else to look -- and replaces it with a purely local question, how fast to get downhill. Everything here is an answer to that. The answers differ in what they know about curvature. Gradient descent knows nothing and pays for it: on a quadratic its error contracts by `(k-1)/(k+1)` per step, so a condition number of a thousand costs a thousand-fold more iterations than a condition number of one. Conjugate gradients build a set of mutually conjugate directions and finish an `n`-dimensional quadratic in at most `n` steps exactly. Newton's method uses the Hessian outright and lands on a quadratic's minimum in a single step. Quasi-Newton methods sit in between, accumulating an approximation to the Hessian from the gradients they have already paid for. Those are not asymptotic claims but exact ones, and the tests check them as such: Newton in one step, conjugate gradients in `n`, and every method against the closed-form minimiser `-Q^-1 c` of the quadratic it was given. The proximal half of the module handles objectives that are convex but not differentiable -- an L1 penalty, a constraint set -- by splitting them into a smooth part, handled by a gradient step, and a simple part, handled by its proximal operator. The reason that works is that the awkward part is usually simple in isolation: the proximal operator of an L1 penalty is soft thresholding, of a box is clamping, and of a simplex is a sorted shift. Each is a projection or near-projection with a closed form, so the non-smoothness costs almost nothing.")?; + m_optimization__convex::register(py, &sub)?; + mods["optimization"].add("convex", &sub)?; + mods.insert("optimization::convex", sub); + } + { + let sub = PyModule::new(py, "numeria.optimization.game_theory")?; + sub.setattr("__doc__", "Game theory: equilibria, dynamics, cooperative solution concepts, auctions, and two-player search. The organising fact of the non-cooperative half is that equilibrium is a *fixed-point* condition and not an optimisation: no player is optimising against a fixed environment, because the environment is the other players doing the same thing. That is why the zero-sum case is easy and the general case is not. In a zero-sum game the two players' problems are linear programs dual to each other, so von Neumann's minimax theorem is a corollary of LP duality and the equilibrium is computable in polynomial time. In a bimatrix game there is no such dual, the equilibrium set can be disconnected, and the best general algorithms are pivoting schemes with exponential worst cases. The cooperative half asks a different question -- not what players will do but how a surplus they have already agreed to create should be split -- and its solution concepts are axiomatic. The Shapley value is the unique allocation satisfying efficiency, symmetry, the null-player property and additivity; the core is the set of allocations no coalition can improve on; and the two can be disjoint, since a game can have an empty core while the Shapley value always exists.")?; + m_optimization__game_theory::register(py, &sub)?; + mods["optimization"].add("game_theory", &sub)?; + mods.insert("optimization::game_theory", sub); + } + { + let sub = PyModule::new(py, "numeria.optimization.integer")?; + sub.setattr("__doc__", "Integer programming, dynamic programming, and combinatorial search. Adding \"and the answer must be a whole number\" to a linear program changes its character completely. The feasible region stops being a convex polyhedron and becomes a scatter of lattice points inside one, so the guarantee that made linear programming easy -- that an optimum sits at a vertex, reachable by local moves -- is gone. What remains is the relaxation: drop the integrality, solve the linear program, and use its value as a bound on what any integer solution could achieve. Branch and bound is that observation applied recursively, and the bound is the only reason it terminates before enumerating everything. Most problems here have that flavour. A few do not, and those are the dynamic programming classics: when a problem decomposes into overlapping subproblems whose optimal solutions compose, the exponential search collapses to a table and the answer is exact in polynomial time. Knapsack, edit distance and the rest are here because the boundary between the two situations is worth being able to see -- the 0/1 knapsack is NP-hard and yet has a pseudo-polynomial table, which is not a contradiction but a statement about what \"polynomial\" is measured against. Where an exact method is impractical the module gives a greedy one with its proven ratio: first-fit-decreasing bin packing within `11/9` of optimal, greedy set cover within `H_n`, longest-processing-time scheduling within `4/3 - 1/(3m)`. Those ratios are worst-case guarantees rather than typical behaviour, and the tests check the guarantee holds against an exact answer on small instances rather than checking the greedy answer is merely plausible.")?; + m_optimization__integer::register(py, &sub)?; + mods["optimization"].add("integer", &sub)?; + mods.insert("optimization::integer", sub); + } + { + let sub = PyModule::new(py, "numeria.optimization.least_squares")?; + sub.setattr("__doc__", "Nonlinear least squares: Levenberg-Marquardt. Reference: Marquardt (1963); Nocedal & Wright, *Numerical Optimization*, §10.3. Minimizes ½‖r(p)‖² by solving (JᵀJ + λ·diag(JᵀJ))·δ = −Jᵀr and adapting λ on accept/reject.")?; + m_optimization__least_squares::register(py, &sub)?; + mods["optimization"].add("least_squares", &sub)?; + mods.insert("optimization::least_squares", sub); + } + { + let sub = PyModule::new(py, "numeria.optimization.lp")?; + sub.setattr("__doc__", "Linear programming: the simplex method, interior point methods, duality, and the classical models that reduce to a linear program. This module sits alongside the continuous optimisers in the parent module rather than replacing them. Those search a smooth objective by following gradients or shrinking a simplex, and stop at a local optimum. A linear program has no local optima to stop at: the objective is linear and the feasible region is a convex polyhedron, so any local optimum is global and at least one optimum sits at a vertex. That is the whole reason the subject exists as a separate discipline, and why an exact answer is available where a nonlinear problem admits only an approximation. Two solvers are provided because they fail in different ways. The simplex method walks vertex to vertex along the boundary, and terminates in an exactly optimal basis, but its worst case is exponential and it can cycle in the presence of degeneracy -- handled here by Bland's rule, which guarantees termination at the cost of speed. The interior point method approaches the optimum through the middle of the region, takes a number of iterations that barely grows with problem size, and never lands exactly on a vertex. Running both on the same problem and comparing is the cheapest real check available on either. Duality is the organising idea. Every linear program has a dual whose optimal value equals its own, and whose optimal solution is the vector of rates at which the primal objective responds to relaxing each constraint. Those rates -- shadow prices -- are usually worth more than the solution itself, since they say which constraint to attack. The convention used here is stated once and adhered to throughout: > `duals[i]` is the derivative of the reported objective with respect to > `b[i]`. That definition is what makes the sensitivity ranges mean something, and it is what the tests check: perturbing a right-hand side within its range changes the objective by exactly `duals[i]` times the perturbation.")?; + m_optimization__lp::register(py, &sub)?; + mods["optimization"].add("lp", &sub)?; + mods.insert("optimization::lp", sub); + } + { + let sub = PyModule::new(py, "numeria.optimization.metaheuristics")?; + sub.setattr("__doc__", "Derivative-free and population-based optimisation, and the benchmark landscapes used to tell one method from another. Every method here treats the objective as a black box: it may be discontinuous, noisy, or defined only by a simulation, and no gradient is available even in principle. That rules out every gradient-based method and leaves search. What distinguishes the methods here is what they do with the evaluations they have spent. Pattern search and Nelder-Mead keep a small geometric structure and move it downhill; they are cheap and get stuck in the first basin they find. Differential evolution and particle swarms keep a population, and their mutation steps are built from *differences between members*, so the search scale adapts to the spread of the population without anyone tuning it. CMA-ES goes furthest: it estimates the covariance of the successful steps and samples from that, which amounts to learning the local metric of the landscape, and is why it handles badly scaled and rotated problems that defeat the others. None of them is guaranteed to find a global optimum in finite time, and any claim otherwise is a claim about the objective rather than the method. What the tests here check is therefore not \"finds the optimum\" in general, but the properties that must hold regardless: bounds are respected, the best-so-far never worsens, a Pareto front contains nothing dominated, and on landscapes whose optima are known analytically the methods get there. The benchmark table exists so those claims can be made against something. Its stated optima are checked by dense sampling in the tests rather than taken on trust -- a benchmark whose recorded optimum is wrong silently invalidates every comparison made with it.")?; + m_optimization__metaheuristics::register(py, &sub)?; + mods["optimization"].add("metaheuristics", &sub)?; + mods.insert("optimization::metaheuristics", sub); + } + { + let sub = PyModule::new(py, "numeria.optimization.network")?; + sub.setattr("__doc__", "Network models and scheduling: project planning, flows on networks, and the sequencing rules that provably optimise a stated objective. Two threads run through this module. The first is that several graph problems are linear programs in disguise, and their constraint matrices are totally unimodular, so the linear relaxation is automatically integral. Shortest path and maximum flow both have this property, which is why they can be solved by combinatorial algorithms *and* by a general linear programming solver with the same answer. Having both is worth the duplication: the graph module's algorithms are far faster, and the linear programs are an independent check on them. The second is that scheduling is a subject of exact greedy rules rather than heuristics. Sorting by processing time minimises mean flow time; sorting by due date minimises maximum lateness; Moore and Hodgson's rule minimises the *number* of late jobs; Johnson's rule minimises makespan on two machines. Each is provably optimal for its own objective and provably not for the others -- shortest-processing-time can make a job catastrophically late while minimising the average -- so the objective must be chosen before the rule. The tests check each rule against exhaustive enumeration of every permutation, on the objective it claims and on nothing else.")?; + m_optimization__network::register(py, &sub)?; + mods["optimization"].add("network", &sub)?; + mods.insert("optimization::network", sub); + } + { + let sub = PyModule::new(py, "numeria.patterns.aperiodic")?; + sub.setattr("__doc__", "Aperiodic tilings: Penrose P2 (kite/dart) and P3 (rhombs) by Robinson-triangle deflation, de Bruijn multigrid projection, Ammann-Beenker, the hat and spectre monotiles (ported from the reference implementations accompanying Smith, Myers, Kaplan & Goodman-Strauss 2023), the pinwheel tiling, and 1-D quasiperiodic sequences.")?; + m_patterns__aperiodic::register(py, &sub)?; + mods["patterns"].add("aperiodic", &sub)?; + mods.insert("patterns::aperiodic", sub); + } + { + let sub = PyModule::new(py, "numeria.patterns.knots")?; + sub.setattr("__doc__", "Knots and space curves: parametric knot families, Frenet and rotation-minimizing frames, curvature/torsion estimates, and the classical knot invariants computable from a curve in space — writhe and linking number by the Gauss integral, crossing numbers of projections, and the Alexander polynomial from a knot diagram.")?; + m_patterns__knots::register(py, &sub)?; + mods["patterns"].add("knots", &sub)?; + mods.insert("patterns::knots", sub); + } + { + let sub = PyModule::new(py, "numeria.patterns.packing")?; + sub.setattr("__doc__", "Circle and sphere packings: Descartes/Apollonian circles, lattice packings, random sequential adsorption, Doyle spirals, Ford circles, Steiner chains, and the problem of Apollonius. Lattice generators include every circle/sphere whose *center* lies in the half-open region, so exact-multiple regions give the exact lattice density.")?; + m_patterns__packing::register(py, &sub)?; + mods["patterns"].add("packing", &sub)?; + mods.insert("patterns::packing", sub); + } + { + let sub = PyModule::new(py, "numeria.patterns.phyllotaxis")?; + sub.setattr("__doc__", "Phyllotactic patterns and spirals: Vogel sunflowers, Fibonacci point sets, the classical spiral family, and parastichy analysis.")?; + m_patterns__phyllotaxis::register(py, &sub)?; + mods["patterns"].add("phyllotaxis", &sub)?; + mods.insert("patterns::phyllotaxis", sub); + } + { + let sub = PyModule::new(py, "numeria.patterns.polygon_ops")?; + sub.setattr("__doc__", "2-D polygon algorithms: triangulation, simplification, offsetting, Minkowski sums, boolean operations, clipping, decomposition, hulls, skeletons, enclosing/inscribed shapes, and fill patterns.")?; + m_patterns__polygon_ops::register(py, &sub)?; + mods["patterns"].add("polygon_ops", &sub)?; + mods.insert("patterns::polygon_ops", sub); + } + { + let sub = PyModule::new(py, "numeria.patterns.polyhedra")?; + sub.setattr("__doc__", "Polyhedra: Platonic/Archimedean/Catalan/Johnson solids, Goldberg and geodesic polyhedra, and Conway polyhedron operators.")?; + m_patterns__polyhedra::register(py, &sub)?; + mods["patterns"].add("polyhedra", &sub)?; + mods.insert("patterns::polyhedra", sub); + } + { + let sub = PyModule::new(py, "numeria.patterns.sampling")?; + sub.setattr("__doc__", "Random and low-discrepancy sampling: Poisson disk (Bridson), blue-noise ranking, stratified jitter, uniform samplers over shapes, random polygons (Valtr), random rotations (Shoemake), and Lloyd relaxation.")?; + m_patterns__sampling::register(py, &sub)?; + mods["patterns"].add("sampling", &sub)?; + mods.insert("patterns::sampling", sub); + } + { + let sub = PyModule::new(py, "numeria.patterns.space_filling")?; + sub.setattr("__doc__", "Space-filling curves and locality-preserving orders: Hilbert (2-D and 3-D), Peano, Morton/Z-order, Gray codes, and L-system curves (Sierpiński arrowhead, Moore, Gosper).")?; + m_patterns__space_filling::register(py, &sub)?; + mods["patterns"].add("space_filling", &sub)?; + mods.insert("patterns::space_filling", sub); + } + { + let sub = PyModule::new(py, "numeria.patterns.symmetry")?; + sub.setattr("__doc__", "Plane symmetry groups (the 17 wallpaper groups and 7 frieze groups), lattices, 3-D point groups, symmetry detection, and Hankin-style Islamic star patterns. Wallpaper operations are expressed in unit-cell (lattice) coordinates: an element maps the unit cell to itself modulo unit translations, so the returned sets are the coset representatives of the point group (plus the centering translation for the centered groups cm and cmm).")?; + m_patterns__symmetry::register(py, &sub)?; + mods["patterns"].add("symmetry", &sub)?; + mods.insert("patterns::symmetry", sub); + } + { + let sub = PyModule::new(py, "numeria.patterns.tilings")?; + sub.setattr("__doc__", "Plane tilings: regular and Archimedean (uniform) tilings, their Laves duals, hex-grid coordinate algebra, and a few classic non-edge-to-edge patterns (brick, herringbone).")?; + m_patterns__tilings::register(py, &sub)?; + mods["patterns"].add("tilings", &sub)?; + mods.insert("patterns::tilings", sub); + } + { + let sub = PyModule::new(py, "numeria.quantum.algorithms")?; + sub.setattr("__doc__", "Quantum algorithms on the state-vector simulator. What the speedups have in common is not \"trying every answer at once\". A superposition over `2^n` inputs is easy; the difficulty is that measurement returns one of them at random, so the exponential is useless by itself. Every algorithm here earns its advantage by arranging *interference* -- amplitudes for wrong answers cancelling while the right one adds -- and the structure being exploited differs each time: a global property of a function for Deutsch-Jozsa, a hidden period for Shor, and nothing at all for Grover, which is why Grover's speedup is only quadratic and provably cannot be more. Oracles are given as ordinary Rust closures and applied directly to the amplitudes. That is exactly what a black box means: the algorithm is charged for each query and never sees inside.")?; + m_quantum__algorithms::register(py, &sub)?; + mods["quantum"].add("algorithms", &sub)?; + mods.insert("quantum::algorithms", sub); + } + { + let sub = PyModule::new(py, "numeria.quantum.circuit")?; + sub.setattr("__doc__", "A state-vector quantum circuit simulator, with density matrices and noise channels. The representation is the whole story. An `n`-qubit pure state is a vector of `2^n` complex amplitudes, so the memory doubles with each qubit: thirty qubits is sixteen gigabytes and there is no cleverness that avoids it for a general state. That exponential is not a limitation of this implementation but the reason quantum computers are interesting, and it is why everything here is capped at a couple of dozen qubits. Applying a one-qubit gate does *not* cost `2^n x 2^n` work. The gate acts on one tensor factor, so the amplitudes split into `2^(n-1)` independent pairs and each pair gets a two-by-two multiply: `O(2^n)` in total. Building the full unitary and multiplying would be `O(4^n)` and is offered only for small circuits, where seeing the matrix is the point. Qubit `q` is bit `q` of the amplitude index, so `|q_2 q_1 q_0>` has index `4 q_2 + 2 q_1 + q_0`. The opposite convention is equally common and the two disagree on every multi-qubit gate, so it is stated here rather than left to be inferred.")?; + m_quantum__circuit::register(py, &sub)?; + mods["quantum"].add("circuit", &sub)?; + mods.insert("quantum::circuit", sub); + } + { + let sub = PyModule::new(py, "numeria.quantum.schrodinger")?; + sub.setattr("__doc__", "Solvers for the Schrodinger equation, stationary and time dependent. The stationary problem is an eigenvalue problem and the time-dependent one is an initial value problem, and the two want different numerics. For the first, discretising the Hamiltonian gives a symmetric matrix whose eigenvalues converge to the true spectrum from below at second order in the grid; for the second, what matters is not local accuracy but *unitarity*, because an integrator that loses norm loses probability and one that gains it manufactures particles from nothing. Both methods offered here are unitary by construction rather than by accident: the split-operator method applies exponentials of Hermitian operators, and Crank-Nicolson applies a Cayley transform, which is unitary for any step size at all. Everything takes `hbar` and the mass explicitly, so `hbar = m = 1` is available for the cases with exact answers.")?; + m_quantum__schrodinger::register(py, &sub)?; + mods["quantum"].add("schrodinger", &sub)?; + mods.insert("quantum::schrodinger", sub); + } + { + let sub = PyModule::new(py, "numeria.quantum.solid_state")?; + sub.setattr("__doc__", "Electrons and phonons in crystals: bands, densities of states, transport, and the standard model systems. Bloch's theorem is the organising fact. A potential with a lattice translation symmetry has eigenstates labelled by a crystal momentum, so the infinite problem reduces to one over a single Brillouin zone -- and the spectrum breaks into bands separated by gaps. That the gaps exist at all is the reason there are insulators; that they are absent at the Fermi level is the reason there are metals; and everything about semiconductors is the behaviour of a gap small enough for temperature to matter. Functions take `hbar` and the masses explicitly where a natural-unit calculation is the point, and use SI constants where a number in electronvolts or siemens is wanted.")?; + m_quantum__solid_state::register(py, &sub)?; + mods["quantum"].add("solid_state", &sub)?; + mods.insert("quantum::solid_state", sub); + } + { + let sub = PyModule::new(py, "numeria.quantum.spin")?; + sub.setattr("__doc__", "Spin operators, quantum magnets, and magnetic resonance. Two quite different things live here. The first is many-body: a chain of coupled spins has a Hilbert space of dimension `2^n`, so exact diagonalisation stops at a dozen or so sites and everything past that is a matter of finding the small part of the space that matters. Lanczos does that for the ground state, and the reason it works is that the extreme eigenvalues of a large sparse matrix converge in a Krylov space of dimension far smaller than the matrix. The second is single-spin dynamics -- Larmor precession, Rabi flopping, echoes -- which is a two-level problem with closed-form answers and is interesting for the opposite reason: the classical Bloch equations describe it exactly, so it is where quantum mechanics is least mysterious. Spin-1/2 operators are `sigma / 2` throughout, and `hbar = 1` unless a function takes it explicitly.")?; + m_quantum__spin::register(py, &sub)?; + mods["quantum"].add("spin", &sub)?; + mods.insert("quantum::spin", sub); + } + { + let sub = PyModule::new(py, "numeria.quantum.wavefunction")?; + sub.setattr("__doc__", "One-dimensional wavefunctions, the standard eigenstates, and phase-space distributions. Everything here works in whatever unit system the caller supplies through `hbar` and the masses, so the natural choice for testing -- `hbar = m = 1` -- is available alongside SI. That matters more than it sounds: the quantities that can be checked exactly, like the harmonic oscillator's `(n + 1/2) hbar omega` spectrum or a Gaussian's saturation of the uncertainty bound, are clearest when the constants are one, and a module that hard-codes SI cannot express them. The one thing worth stating up front is the discretisation. A wavefunction is represented by its samples on a uniform grid, and every integral below is the corresponding Riemann sum. That is exact for none of them and spectrally accurate for a smooth function that has decayed to nothing at both ends -- which is the condition the callers here are responsible for arranging, and the one under which the tests hold to the tolerances they state.")?; + m_quantum__wavefunction::register(py, &sub)?; + mods["quantum"].add("wavefunction", &sub)?; + mods.insert("quantum::wavefunction", sub); + } + { + let sub = PyModule::new(py, "numeria.resonance.cavity")?; + sub.setattr("__doc__", "Resonant cavities and structures: RLC circuits, Helmholtz resonators, strings, air columns, membranes, plates, beams, rooms, optical etalons, and microwave cavities.")?; + m_resonance__cavity::register(py, &sub)?; + mods["resonance"].add("cavity", &sub)?; + mods.insert("resonance::cavity", sub); + } + { + let sub = PyModule::new(py, "numeria.resonance.coupled")?; + sub.setattr("__doc__", "Coupled linear oscillators: normal modes, modal superposition, receptance, classic two-body systems, Kuramoto synchronization, and tuned-mass-damper design.")?; + m_resonance__coupled::register(py, &sub)?; + mods["resonance"].add("coupled", &sub)?; + mods.insert("resonance::coupled", sub); + } + { + let sub = PyModule::new(py, "numeria.resonance.nonlinear")?; + sub.setattr("__doc__", "Nonlinear resonance: the Duffing and van der Pol oscillators, parametric (Mathieu) stability, Fano interference, synchronization pulling/locking, and generic harmonic-balance machinery. The Duffing convention throughout is x″ + δ·x′ + α·x + β·x³ = γ·cos(ωt).")?; + m_resonance__nonlinear::register(py, &sub)?; + mods["resonance"].add("nonlinear", &sub)?; + mods.insert("resonance::nonlinear", sub); + } + { + let sub = PyModule::new(py, "numeria.resonance.oscillator")?; + sub.setattr("__doc__", "The damped harmonic oscillator m·x″ + c·x′ + k·x = F(t): closed-form responses in every damping regime, frequency-domain descriptions, and resonance measurement (Lorentzian fits, Q extraction).")?; + m_resonance__oscillator::register(py, &sub)?; + mods["resonance"].add("oscillator", &sub)?; + mods.insert("resonance::oscillator", sub); + } + { + let sub = PyModule::new(py, "numeria.resonance.structural")?; + sub.setattr("__doc__", "Structural dynamics: finite-element bars and beams, modal analysis with general (consistent) mass matrices, Rayleigh damping, implicit time integration (Newmark-β, HHT-α), model reduction, response spectra, and experimental modal analysis tools.")?; + m_resonance__structural::register(py, &sub)?; + mods["resonance"].add("structural", &sub)?; + mods.insert("resonance::structural", sub); + } + { + let sub = PyModule::new(py, "numeria.sim.cloth_sim")?; + sub.setattr("__doc__", "Verlet cloth and rope with spring constraints. Particles are advanced by position Verlet, which stores the previous position rather than a velocity: it is stable under stiff constraints and conserves energy far better than explicit Euler at the same step size, because velocity is inferred from the positions rather than integrated separately. Structural, shear and bend springs are then satisfied by iterated position projection -- more iterations gives a stiffer cloth -- with pinning, sphere and floor collision, and wind and gravity forces.")?; + m_sim__cloth_sim::register(py, &sub)?; + mods["sim"].add("cloth_sim", &sub)?; + mods.insert("sim::cloth_sim", sub); + } + { + let sub = PyModule::new(py, "numeria.sim.em_sim")?; + sub.setattr("__doc__", "FDTD electromagnetic simulation in one and two dimensions. Explicit leapfrog on a Yee-style grid: the electric and magnetic fields are staggered by half a cell and half a time step, so each is updated from the curl of the other and the scheme is second-order accurate with no matrix to solve. Supports dielectric media, hard and soft sources, PEC (perfectly conducting) walls and Mur first-order absorbing boundaries. Stability requires the Courant condition, and the limit is set by the fastest medium in the grid -- that is, the smallest relative permittivity. For a Yee grid with Berenger split-field PML, photonic band gaps and waveguide cutoff, see `fem::fdtd`.")?; + m_sim__em_sim::register(py, &sub)?; + mods["sim"].add("em_sim", &sub)?; + mods.insert("sim::em_sim", sub); + } + { + let sub = PyModule::new(py, "numeria.sim.fluid_sim")?; + sub.setattr("__doc__", "Compact fluid solvers: column, shallow water, and 2-D Euler. A draining column for the simplest case, a 1-D shallow-water solver, and a 2-D incompressible Euler solver that advects velocity and then restores `∇·u = 0` by pressure projection -- subtracting the gradient of a pressure field found by solving a Poisson equation, which is what makes the result divergence-free. Written to be read and to run interactively. For well-balanced schemes, Riemann solvers and the rest of the research-grade machinery see `cfd`.")?; + m_sim__fluid_sim::register(py, &sub)?; + mods["sim"].add("fluid_sim", &sub)?; + mods.insert("sim::fluid_sim", sub); + } + { + let sub = PyModule::new(py, "numeria.sim.heat_sim")?; + sub.setattr("__doc__", "Heat conduction and convection-diffusion on a grid. Explicit finite differences in two and three dimensions, with Dirichlet and Neumann boundaries, sources, and an advection term for convection-diffusion. Explicit stepping is only conditionally stable: the step must satisfy `α Δt / Δx² ≤ 1/4` in 2-D and `1/6` in 3-D, so halving the grid spacing quarters the allowable time step. The stability limit is provided as a function rather than left to the caller to remember.")?; + m_sim__heat_sim::register(py, &sub)?; + mods["sim"].add("heat_sim", &sub)?; + mods.insert("sim::heat_sim", sub); + } + { + let sub = PyModule::new(py, "numeria.sim.rigid_body")?; + sub.setattr("__doc__", "Rigid body dynamics in three dimensions. State is position, linear velocity, orientation as a unit quaternion, and angular velocity. Rotation uses a quaternion rather than Euler angles because it composes without gimbal lock and stays well conditioned under renormalization. Angular motion follows Euler's equations, which carry the `ω × Iω` term -- the reason a freely spinning body with three distinct moments of inertia tumbles rather than spinning steadily about an intermediate axis. Includes inertia tensors for the standard bodies, force and torque accumulation, sphere-sphere collision detection, and impulse-based collision response with restitution.")?; + m_sim__rigid_body::register(py, &sub)?; + mods["sim"].add("rigid_body", &sub)?; + mods.insert("sim::rigid_body", sub); + } + { + let sub = PyModule::new(py, "numeria.sim.wave_sim")?; + sub.setattr("__doc__", "The wave equation in one and two dimensions. Explicit second-order finite differences on `∂²u/∂t² = c²∇²u`, with fixed, free, and Mur first-order absorbing boundaries. The absorbing condition passes a normally-incident wave out of the domain exactly and degrades with the angle of incidence. Stability requires the Courant number `r = cΔt/Δx` to satisfy `r ≤ 1` in 1-D and `r ≤ 1/√2` in 2-D. At exactly `r = 1` in one dimension the scheme is an exact shift and has no dispersion error at all.")?; + m_sim__wave_sim::register(py, &sub)?; + mods["sim"].add("wave_sim", &sub)?; + mods.insert("sim::wave_sim", sub); + } + { + let sub = PyModule::new(py, "numeria.spatial.bvh")?; + sub.setattr("__doc__", "Bounding volume hierarchy over axis-aligned boxes. Built top-down with binned surface-area-heuristic splits (12 bins; Wald 2007), falling back to a median split when SAH finds no gain. Leaves store index ranges into a permutation of the input.")?; + m_spatial__bvh::register(py, &sub)?; + mods["spatial"].add("bvh", &sub)?; + mods.insert("spatial::bvh", sub); + } + { + let sub = PyModule::new(py, "numeria.spatial.contain")?; + sub.setattr("__doc__", "Orientation predicates and containment tests. `orient2d_exact` follows Shewchuk's approach: a floating-point filter with a proven error bound, falling back to exact expansion arithmetic (error-free two_sum / two_product transforms) when the filter cannot decide (Shewchuk, \"Adaptive precision floating-point arithmetic and fast robust geometric predicates\", 1997).")?; + m_spatial__contain::register(py, &sub)?; + mods["spatial"].add("contain", &sub)?; + mods.insert("spatial::contain", sub); + } + { + let sub = PyModule::new(py, "numeria.spatial.distance")?; + sub.setattr("__doc__", "Closest-point queries and set distances. References: Ericson, *Real-Time Collision Detection*, ch. 5 (point and segment queries); Eiter & Mannila 1994 (discrete Fréchet distance).")?; + m_spatial__distance::register(py, &sub)?; + mods["spatial"].add("distance", &sub)?; + mods.insert("spatial::distance", sub); + } + { + let sub = PyModule::new(py, "numeria.spatial.frame")?; + sub.setattr("__doc__", "Rigid coordinate frames (origin + unit-quaternion rotation). `to_world(p_local) = origin + R·p_local`; composition and inverses follow the usual rigid-motion group structure SE(3).")?; + m_spatial__frame::register(py, &sub)?; + mods["spatial"].add("frame", &sub)?; + mods.insert("spatial::frame", sub); + } + { + let sub = PyModule::new(py, "numeria.spatial.intersect")?; + sub.setattr("__doc__", "Intersection tests between the spatial primitives. References: Ericson, *Real-Time Collision Detection* (RTCD); Möller & Trumbore 1997 (ray-triangle); Akenine-Möller 2001 (triangle-box SAT). Ray parameters are along the (normalized) ray direction; only t ≥ 0 counts as a hit.")?; + m_spatial__intersect::register(py, &sub)?; + mods["spatial"].add("intersect", &sub)?; + mods.insert("spatial::intersect", sub); + } + { + let sub = PyModule::new(py, "numeria.spatial.kdtree")?; + sub.setattr("__doc__", "k-d trees (3-D and 2-D) with median splits, plus a uniform spatial hash for broadphase neighbor queries. Reference: Bentley 1975; Friedman, Bentley & Finkel 1977 (nearest neighbor search with bounds pruning).")?; + m_spatial__kdtree::register(py, &sub)?; + mods["spatial"].add("kdtree", &sub)?; + mods.insert("spatial::kdtree", sub); + } + { + let sub = PyModule::new(py, "numeria.spatial.mat4")?; + sub.setattr("__doc__", "4×4 homogeneous transform matrix (row-major storage, column-vector convention: p' = M·p). References: Foley et al., *Computer Graphics: Principles and Practice*; the OpenGL clip-space conventions for `perspective` and `orthographic` (z mapped to [−1, 1]).")?; + m_spatial__mat4::register(py, &sub)?; + mods["spatial"].add("mat4", &sub)?; + mods.insert("spatial::mat4", sub); + } + { + let sub = PyModule::new(py, "numeria.spatial.octree")?; + sub.setattr("__doc__", "Barnes-Hut octree for N-body force approximation. Direct summation costs O(N²). The octree groups distant bodies and treats each group as a single mass at its centre of mass, which brings the cost to O(N log N). The approximation is controlled by `theta`: a node is used as a whole when its width divided by the distance to it is below that threshold. Smaller `theta` is more accurate and slower, and `theta = 0` degenerates to direct summation. The conventional default of 0.5 is `BH_THETA`.")?; + m_spatial__octree::register(py, &sub)?; + mods["spatial"].add("octree", &sub)?; + mods.insert("spatial::octree", sub); + } + { + let sub = PyModule::new(py, "numeria.spatial.primitives")?; + sub.setattr("__doc__", "Geometric primitive types shared by the intersection, distance, containment, and acceleration modules. Conventions: plane as n·p + d = 0 with unit normal; ray directions normalized by the constructor; polygons CCW-positive.")?; + m_spatial__primitives::register(py, &sub)?; + mods["spatial"].add("primitives", &sub)?; + mods.insert("spatial::primitives", sub); + } + { + let sub = PyModule::new(py, "numeria.spatial.projective")?; + sub.setattr("__doc__", "Homogeneous 2-D projective geometry: points, lines, cross ratios, and plane homographies (Hartley & Zisserman, *Multiple View Geometry*, ch. 2 and 4).")?; + m_spatial__projective::register(py, &sub)?; + mods["spatial"].add("projective", &sub)?; + mods.insert("spatial::projective", sub); + } + { + let sub = PyModule::new(py, "numeria.spatial.sdf")?; + sub.setattr("__doc__", "Signed distance fields: primitives, combinators, domain operators, and queries (sphere tracing, normals, AO, soft shadows). Primitive formulas follow Inigo Quilez's reference catalogue (iquilezles.org/articles/distfunctions). Negative inside, positive outside; all primitive SDFs are exact unless noted.")?; + m_spatial__sdf::register(py, &sub)?; + mods["spatial"].add("sdf", &sub)?; + mods.insert("spatial::sdf", sub); + } + { + let sub = PyModule::new(py, "numeria.spatial.transform2d")?; + sub.setattr("__doc__", "2-D affine transforms stored as 3×3 homogeneous matrices (last row 0 0 1), column-vector convention: p' = M·p.")?; + m_spatial__transform2d::register(py, &sub)?; + mods["spatial"].add("transform2d", &sub)?; + mods.insert("spatial::transform2d", sub); + } + { + let sub = PyModule::new(py, "numeria.special.bessel")?; + sub.setattr("__doc__", "Bessel functions of integer order. J and Y use the rational approximations of Numerical Recipes §6.5 (about 1e-8 absolute accuracy); higher orders use upward recurrence for Y and Miller's downward-recurrence algorithm for J when the argument is smaller than the order. I and K follow the polynomial approximations of Abramowitz & Stegun §9.8 (as in NR §6.6).")?; + m_special__bessel::register(py, &sub)?; + mods["special"].add("bessel", &sub)?; + mods.insert("special::bessel", sub); + } + { + let sub = PyModule::new(py, "numeria.special.beta")?; + sub.setattr("__doc__", "Beta function and regularized incomplete beta. B(a,b) = Γ(a)Γ(b)/Γ(a+b), evaluated in log space. The regularized incomplete beta I_x(a,b) uses the modified Lentz continued fraction of Numerical Recipes §6.4.")?; + m_special__beta::register(py, &sub)?; + mods["special"].add("beta", &sub)?; + mods.insert("special::beta", sub); + } + { + let sub = PyModule::new(py, "numeria.special.elliptic")?; + sub.setattr("__doc__", "Elliptic integrals and physical applications. Complete integrals use the arithmetic-geometric mean (Abramowitz & Stegun §17.6); incomplete integrals use the Carlson symmetric forms R_F and R_D (Carlson 1979; NR §6.11). The parameter is m = k².")?; + m_special__elliptic::register(py, &sub)?; + mods["special"].add("elliptic", &sub)?; + mods.insert("special::elliptic", sub); + } + { + let sub = PyModule::new(py, "numeria.special.erf")?; + sub.setattr("__doc__", "Error function family. `erf`/`erfc` implement W. J. Cody's rational Chebyshev approximations (\"Rational Chebyshev approximation for the error function\", Math. Comp. 23, 1969; the SPECFUN `CALERF` algorithm), accurate to full double precision. `erfinv` uses M. Giles' polynomial approximation (\"Approximating the erfinv function\", GPU Computing Gems, 2012) polished with Newton steps on `erf`.")?; + m_special__erf::register(py, &sub)?; + mods["special"].add("erf", &sub)?; + mods.insert("special::erf", sub); + } + { + let sub = PyModule::new(py, "numeria.special.expint")?; + sub.setattr("__doc__", "Exponential integrals Ei(x) and E1(x).")?; + m_special__expint::register(py, &sub)?; + mods["special"].add("expint", &sub)?; + mods.insert("special::expint", sub); + } + { + let sub = PyModule::new(py, "numeria.special.gamma")?; + sub.setattr("__doc__", "Gamma function family. `gamma` uses the Lanczos approximation (g = 7, n = 9); `lgamma` is the same approximation carried in log space so it does not overflow up to very large arguments. The regularized incomplete functions P(a,x) (`gamma_p`) and Q(a,x) (`gamma_q`) follow Numerical Recipes ch. 6.2 (series for x < a+1, Lentz continued fraction otherwise).")?; + m_special__gamma::register(py, &sub)?; + mods["special"].add("gamma", &sub)?; + mods.insert("special::gamma", sub); + } + { + let sub = PyModule::new(py, "numeria.special.legendre")?; + sub.setattr("__doc__", "Legendre polynomials, associated Legendre functions, real spherical harmonics, and Gauss-Legendre quadrature nodes. References: Abramowitz & Stegun ch. 8; Numerical Recipes §6.8 (`plgndr`) and §4.5 (`gauleg`).")?; + m_special__legendre::register(py, &sub)?; + mods["special"].add("legendre", &sub)?; + mods.insert("special::legendre", sub); + } + { + let sub = PyModule::new(py, "numeria.statistical_mechanics.ising")?; + sub.setattr("__doc__", "The Ising model and its relatives, by Monte Carlo. The two-dimensional Ising model is the one interacting system with a phase transition that is solved exactly, so it is where a Monte Carlo code can be checked against arithmetic rather than against another Monte Carlo code. Onsager's solution gives the critical temperature, the energy and the spontaneous magnetisation in closed form, and any sampler that disagrees with them is wrong. The algorithmic point of the module is the contrast between the two updates. Metropolis flips one spin at a time, so near the critical temperature -- where the correlation length diverges and whole regions must turn over together -- successive configurations stay correlated for a time growing as the system size to a power near two. Wolff builds a cluster whose size is itself set by the correlation length and flips it whole, which all but removes that critical slowing down. The two sample the same distribution; they differ only in how long it takes.")?; + m_statistical_mechanics__ising::register(py, &sub)?; + mods["statistical_mechanics"].add("ising", &sub)?; + mods.insert("statistical_mechanics::ising", sub); + } + { + let sub = PyModule::new(py, "numeria.statistical_mechanics.kinetics")?; + sub.setattr("__doc__", "Chemical kinetics: rate laws, deterministic and stochastic reaction networks, enzyme saturation, equilibrium composition, oscillating mechanisms, nucleation and transformation, and the acid-base and electrochemical relations that share their arithmetic. # What lives here and what lives in `chemistry` The elementary single-formula relations -- the Arrhenius rate, the equilibrium constant from a free energy, the Nernst potential, pH from a proton concentration -- are already in `chemistry`, and are not duplicated. This module is the part that needs a solver: networks integrated in time, fits inverted from data, compositions found by root-finding, and the stochastic algorithms. # Units Concentrations are molar, times are seconds, energies are joules per mole and temperatures are kelvin, so `R` rather than `k_B` appears throughout. The one exception is `kramers_rate_check`, which follows its own literature convention of barrier heights in units of `k_B T`; it is marked at the function.")?; + m_statistical_mechanics__kinetics::register(py, &sub)?; + mods["statistical_mechanics"].add("kinetics", &sub)?; + mods.insert("statistical_mechanics::kinetics", sub); + } + { + let sub = PyModule::new(py, "numeria.statistical_mechanics.lattice_models")?; + sub.setattr("__doc__", "Lattice models: percolation, walks, growth, and avalanches. These are the systems where critical behaviour appears without any Hamiltonian or temperature at all. Percolation has a sharp threshold and a divergent cluster size, self-avoiding walks have a non-trivial exponent that mean-field theory gets wrong, and a growing interface roughens with exponents shared by systems that have nothing physically in common. That last fact -- universality -- is what makes the subject more than a collection of models: the exponents depend on dimension and symmetry, and on essentially nothing else. Everything here is on a square lattice unless said otherwise, and the random routines take the crate's deterministic generator so a run can be repeated exactly.")?; + m_statistical_mechanics__lattice_models::register(py, &sub)?; + mods["statistical_mechanics"].add("lattice_models", &sub)?; + mods.insert("statistical_mechanics::lattice_models", sub); + } + { + let sub = PyModule::new(py, "numeria.statistical_mechanics.md")?; + sub.setattr("__doc__", "Molecular dynamics: pair potentials, a cell-list force evaluation, a symplectic integrator, thermostats and barostats, and the structural and transport measurements taken from a trajectory. # Units Everything here is in *reduced* Lennard-Jones units: `sigma`, `eps`, the particle mass and Boltzmann's constant are all one unless the caller says otherwise, so a temperature is an energy and a pressure is an energy per volume. This is not a convenience -- mixing SI constants into a molecular dynamics run is how the field's worst bugs happen, because the equations of motion are dimensionally consistent under any consistent choice and silently wrong under an inconsistent one. See `lj_reduced_units_note`. The roadmap gives `MdSystem` a `SpatialHash` field. The general-purpose hash in `spatial::kdtree` owns a copy of every position and knows nothing about periodic images, so this module carries its own cell list instead: it is rebuilt each step from the live positions and wraps at the box boundary, which is what the minimum-image convention needs.")?; + m_statistical_mechanics__md::register(py, &sub)?; + mods["statistical_mechanics"].add("md", &sub)?; + mods.insert("statistical_mechanics::md", sub); + } + { + let sub = PyModule::new(py, "numeria.statistics.descriptive")?; + sub.setattr("__doc__", "Descriptive statistics, error propagation, and weighted means.")?; + m_statistics__descriptive::register(py, &sub)?; + mods["statistics"].add("descriptive", &sub)?; + mods.insert("statistics::descriptive", sub); + } + { + let sub = PyModule::new(py, "numeria.statistics.distributions")?; + sub.setattr("__doc__", "Probability distributions: densities, mass functions, and CDFs.")?; + m_statistics__distributions::register(py, &sub)?; + mods["statistics"].add("distributions", &sub)?; + mods.insert("statistics::distributions", sub); + } + { + let sub = PyModule::new(py, "numeria.statistics.fourier")?; + sub.setattr("__doc__", "Discrete Fourier transform utilities. These are thin wrappers over `transforms::fft` (Step 0 of roadmap Part 3): every length now runs in O(n log n) via the mixed-radix / Bluestein FFT while keeping the original `(re, im)` tuple API.")?; + m_statistics__fourier::register(py, &sub)?; + mods["statistics"].add("fourier", &sub)?; + mods.insert("statistics::fourier", sub); + } + { + let sub = PyModule::new(py, "numeria.statistics.inference")?; + sub.setattr("__doc__", "Hypothesis tests and confidence intervals. p-values come from the crate's own distribution CDFs (Student t, chi-squared, F) and the asymptotic Kolmogorov distribution Q_KS(λ) = 2·Σ (−1)^{j−1} e^{−2j²λ²} (NR §14.3). All t-type tests report two-sided p-values.")?; + m_statistics__inference::register(py, &sub)?; + mods["statistics"].add("inference", &sub)?; + mods.insert("statistics::inference", sub); + } + { + let sub = PyModule::new(py, "numeria.statistics.resampling")?; + sub.setattr("__doc__", "Resampling methods: bootstrap, BCa bootstrap, permutation tests, and the jackknife. Reference: Efron & Tibshirani, *An Introduction to the Bootstrap* (1993), ch. 6 (percentile), ch. 14 (BCa), ch. 15 (permutation).")?; + m_statistics__resampling::register(py, &sub)?; + mods["statistics"].add("resampling", &sub)?; + mods.insert("statistics::resampling", sub); + } + { + let sub = PyModule::new(py, "numeria.stochastic.extreme")?; + sub.setattr("__doc__", "Extreme value theory and copulas: the distribution of maxima, the distribution of exceedances, and the dependence structure between them. Ordinary statistics describes the middle of a distribution, where there is data. Extreme value theory describes the edge, where by construction there is almost none, and it does so by an argument that parallels the central limit theorem. Just as a normalised *sum* of independent variables has only one possible limit whatever the summands, a normalised *maximum* has only three -- Gumbel, Frechet, Weibull -- and the generalised extreme value family holds all three, distinguished by the sign of a single shape parameter. That is what licenses extrapolating past the largest observation: the tail shape is not assumed, it is forced. Two routes lead to the same place. Taking the maximum of each block and fitting a GEV throws away every observation but one per block. Taking every exceedance over a high threshold instead keeps far more of the data, and the Pickands-Balkema-de Haan theorem says those exceedances follow a generalised Pareto distribution with the *same* shape parameter. The threshold approach is usually the better estimator; the block approach is easier to explain and needs no threshold chosen. The shape parameter is the whole story. Negative means a bounded tail with a finite upper endpoint; zero means an exponential tail, where every moment exists; positive means a power-law tail, where moments beyond `1/xi` do not. A hundred-year return level computed under the wrong sign is not slightly wrong. Copulas answer the other half of the question. Marginal tails say how extreme each variable gets; a copula says whether they get extreme together. The distinction matters because correlation does not capture it: a Gaussian copula has zero tail dependence at any correlation below one, so two variables can be strongly correlated in the body and yet asymptotically independent in the tail, which is precisely the failure mode a correlation-based risk model cannot see.")?; + m_stochastic__extreme::register(py, &sub)?; + mods["stochastic"].add("extreme", &sub)?; + mods.insert("stochastic::extreme", sub); + } + { + let sub = PyModule::new(py, "numeria.stochastic.hmm")?; + sub.setattr("__doc__", "Hidden state models: hidden Markov models, smoothing, and particle filters. The common thread is a state that evolves as a Markov chain and is never observed directly -- only through emissions that depend on it. Three questions follow, and each has its own algorithm. *How likely is this observation sequence?* is answered by summing over every possible state path, which the forward recursion does in linear time by never enumerating the paths. *Which single path best explains it?* is answered by Viterbi, the same recursion with the sum replaced by a maximum. *What parameters make it likeliest?* is answered by Baum-Welch, which is expectation-maximisation applied to the first two. The discrete and Gaussian models here differ only in what an emission is. The Kalman smoother and the particle filter answer the same questions for a continuous state: exactly, when the model is linear and Gaussian, and by sampling when it is not. Everything works in logs or with explicit scaling, because the probability of a sequence of a few hundred observations underflows a double long before the algorithm finishes.")?; + m_stochastic__hmm::register(py, &sub)?; + mods["stochastic"].add("hmm", &sub)?; + mods.insert("stochastic::hmm", sub); + } + { + let sub = PyModule::new(py, "numeria.stochastic.markov")?; + sub.setattr("__doc__", "Finite Markov chains and Markov chain Monte Carlo. A Markov chain is a square matrix whose rows sum to one, and almost everything about it follows from linear algebra applied to that matrix. The long-run behaviour is an eigenvector; how fast it is reached is the gap between the leading eigenvalue and the next; expected hitting times are the solution of a linear system; and the answer to \"what happens after `n` steps\" is a matrix power. Markov chain Monte Carlo runs the idea backwards. Given a distribution you can evaluate but not sample from, build a chain whose stationary distribution is that one, and run it. Metropolis-Hastings does this by proposing a move and accepting it with a probability that makes detailed balance hold; Hamiltonian Monte Carlo does it by simulating a physical trajectory that conserves energy, so the acceptance probability stays near one even for a long move. The samplers are only ever asymptotically correct, so the diagnostics -- effective sample size, the Gelman-Rubin statistic, the autocorrelation time -- are not optional extras but the only evidence that a run has converged.")?; + m_stochastic__markov::register(py, &sub)?; + mods["stochastic"].add("markov", &sub)?; + mods.insert("stochastic::markov", sub); + } + { + let sub = PyModule::new(py, "numeria.stochastic.point_process")?; + sub.setattr("__doc__", "Point processes: random collections of points in time or space. The Poisson process is the reference against which every other is described. It has no memory -- the chance of an event in the next instant does not depend on what happened before -- and everything else follows: counts in disjoint sets are independent and Poisson, waiting times are exponential, and given the count in an interval the points are uniformly scattered in it. The other processes here are departures from that in one of two directions. *Clustered* processes -- Hawkes, Cox, Matern, Thomas -- put more points near other points, either because events trigger events or because the rate is itself random. *Regular* processes have points that avoid each other. Ripley's `K` function and the pair correlation measure which of the three a pattern is, by comparing what is seen at each distance against what a Poisson process would give.")?; + m_stochastic__point_process::register(py, &sub)?; + mods["stochastic"].add("point_process", &sub)?; + mods.insert("stochastic::point_process", sub); + } + { + let sub = PyModule::new(py, "numeria.stochastic.queueing")?; + sub.setattr("__doc__", "Queueing theory: birth-death queues, Erlang loss and delay formulas, networks of queues, and continuous-time Markov chains. Almost every closed form here is a birth-death chain in disguise. A queue with Poisson arrivals and exponential service moves up one state at rate `lambda` and down one at a rate set by how many servers are busy, so the stationary distribution telescopes into a product of ratios and the means follow by summation. The Erlang formulas are the two boundary cases of that product: B when a full system turns customers away, C when it makes them wait. Two results tie the whole module together and are worth stating because the tests lean on them. Little's law, `L = lambda W`, holds for every model below -- it is a statement about areas under a sample path and assumes nothing about the arrival or service distributions. And the Pollaczek-Khinchine formula shows what the exponential assumption was buying: for a single server the mean queue depends on the service distribution only through its first two moments, so M/D/1 has exactly half the queue of M/M/1 at the same load. Where a model has no closed form the module simulates it instead. The event-driven simulator tracks the number in system by integrating over a merged event list rather than by invoking Little's law, so comparing its output against `lambda W` is a real check rather than a tautology.")?; + m_stochastic__queueing::register(py, &sub)?; + mods["stochastic"].add("queueing", &sub)?; + mods.insert("stochastic::queueing", sub); + } + { + let sub = PyModule::new(py, "numeria.stochastic.rmt")?; + sub.setattr("__doc__", "Random matrix theory: the classical ensembles, their limiting spectral laws, and the local statistics that distinguish correlated spectra from uncorrelated ones. The subject rests on a surprise: the eigenvalues of a large random matrix are not themselves random in any useful sense. Their *density* converges to a fixed shape that does not depend on the distribution of the entries -- Wigner's semicircle for a symmetric matrix, Marchenko-Pastur for a sample covariance -- and their *spacings* converge to a distribution that depends only on the symmetry class. Universality is what makes the subject applicable: a spectrum can be compared against these laws without knowing anything about the mechanism that produced it. The practical payoff is a null hypothesis. Eigenvalues of independent variables repel each other, in a way that independent *points* do not, so the spacing distribution separates a spectrum with genuine level correlations from a Poisson process of unrelated levels. In finance the same statement is a filter: any eigenvalue of a sample correlation matrix that falls inside the Marchenko-Pastur band is consistent with pure noise and carries no information about the correlations being estimated. Two conventions are fixed throughout. Ensembles are scaled so their limiting support stays put as `n` grows -- otherwise the semicircle's radius would drift and nothing would converge to compare against. And spacings are always measured on *unfolded* eigenvalues, rescaled to unit mean density, since the raw spacings of a semicircular spectrum are much tighter in the middle than at the edges and their distribution would say more about the density than about the correlations.")?; + m_stochastic__rmt::register(py, &sub)?; + mods["stochastic"].add("rmt", &sub)?; + mods.insert("stochastic::rmt", sub); + } + { + let sub = PyModule::new(py, "numeria.stochastic.sde")?; + sub.setattr("__doc__", "Stochastic differential equations: simulation, convergence, and the densities the paths are distributed by. An equation `dX = mu dt + sigma dW` is not an ordinary differential equation with noise added. Brownian motion is nowhere differentiable, and `dW` has magnitude of order `sqrt(dt)` rather than `dt`, so a term that would be second order in a deterministic expansion is first order here. That is the whole content of Ito's lemma, and it is why the numerical schemes are not the familiar ones: Euler-Maruyama looks like Euler's method but converges at half its order, and recovering first order needs the Milstein correction, which is precisely the term Ito's lemma says is missing. *Strong* convergence is about paths -- how close a simulated path is to the exact path driven by the same noise -- and *weak* convergence is about distributions, how close the expectation of a function is. They are genuinely different: Euler-Maruyama is strong order one half and weak order one. Which one matters depends on the question, and both are measured here rather than asserted.")?; + m_stochastic__sde::register(py, &sub)?; + mods["stochastic"].add("sde", &sub)?; + mods.insert("stochastic::sde", sub); + } + { + let sub = PyModule::new(py, "numeria.stochastic.timeseries")?; + sub.setattr("__doc__", "Time series analysis: correlation structure, stationarity, ARMA models, smoothing, volatility, and change detection. A time series differs from a sample only in that the order matters, and every tool here is a way of asking how much it matters. The autocorrelation function measures it directly; the partial autocorrelation strips out what is already explained by the lags in between; the spectral density says the same thing in the frequency domain. An ARMA model is a compact parameterisation of that structure, and its impulse-response weights are the bridge between the two views -- they generate the autocovariances, the forecast error variances, and the spectral density alike. Stationarity is the assumption the whole apparatus rests on, so it is tested rather than assumed. The augmented Dickey-Fuller test takes a unit root as the null and looks for evidence against it; the KPSS test takes stationarity as the null and looks for evidence against *that*. They are deliberately opposed: agreeing on a rejection is much stronger evidence than either alone, and disagreement is a signal that the series is neither cleanly one nor the other. The p-values for both come from tabulated quantiles of their non-standard null distributions, interpolated. Neither statistic is asymptotically normal or chi-squared -- a Dickey-Fuller `t`-ratio is not a `t` at all -- so a p-value computed from a standard distribution would be wrong rather than approximate. The tables are documented where they are used.")?; + m_stochastic__timeseries::register(py, &sub)?; + mods["stochastic"].add("timeseries", &sub)?; + mods.insert("stochastic::timeseries", sub); + } + { + let sub = PyModule::new(py, "numeria.transforms.dct")?; + sub.setattr("__doc__", "Discrete cosine, sine, and Hartley transforms. Conventions match scipy's unnormalized (`norm=None`) definitions: * DCT-I: y\\[k\\] = x\\[0\\] + (−1)^k x\\[N−1\\] + 2 Σ_{n=1}^{N−2} x\\[n\\] cos(πkn/(N−1)) * DCT-II: y\\[k\\] = 2 Σ x\\[n\\] cos(πk(2n+1)/(2N)) * DCT-III: y\\[k\\] = x\\[0\\] + 2 Σ_{n≥1} x\\[n\\] cos(πn(2k+1)/(2N)) * DCT-IV: y\\[k\\] = 2 Σ x\\[n\\] cos(π(2k+1)(2n+1)/(4N)) * DST-I: y\\[k\\] = 2 Σ x\\[n\\] sin(π(k+1)(n+1)/(N+1)) * DST-II: y\\[k\\] = 2 Σ x\\[n\\] sin(π(k+1)(2n+1)/(2N)) Everything runs in O(n log n) through the any-length FFT.")?; + m_transforms__dct::register(py, &sub)?; + mods["transforms"].add("dct", &sub)?; + mods.insert("transforms::dct", sub); + } + { + let sub = PyModule::new(py, "numeria.transforms.fft")?; + sub.setattr("__doc__", "Fast Fourier transforms. The power-of-two core is the iterative radix-2 Cooley-Tukey from Press et al., *Numerical Recipes*, §12.2. `fft_any` extends it to arbitrary lengths with a recursive mixed-radix 2/3/5 decomposition and a Bluestein chirp-z fallback for lengths with other prime factors.")?; + m_transforms__fft::register(py, &sub)?; + mods["transforms"].add("fft", &sub)?; + mods.insert("transforms::fft", sub); + } + { + let sub = PyModule::new(py, "numeria.transforms.hilbert")?; + sub.setattr("__doc__", "Hilbert transform, analytic signals, modulation, empirical mode decomposition, and causality (Kramers-Kronig) tools.")?; + m_transforms__hilbert::register(py, &sub)?; + mods["transforms"].add("hilbert", &sub)?; + mods.insert("transforms::hilbert", sub); + } + { + let sub = PyModule::new(py, "numeria.transforms.laplace")?; + sub.setattr("__doc__", "Laplace-domain tools: numerical inverse transforms (fixed-Talbot and Gaver-Stehfest), the z-transform, transfer-function responses, and a discrete fractional Fourier transform. Polynomial coefficient conventions: s-domain polynomials are highest-power-first (like `numerical::polynomial_roots`); digital filter coefficient arrays are in z⁻¹ powers (b\\[0\\] + b\\[1\\]z⁻¹ + …).")?; + m_transforms__laplace::register(py, &sub)?; + mods["transforms"].add("laplace", &sub)?; + mods.insert("transforms::laplace", sub); + } + { + let sub = PyModule::new(py, "numeria.transforms.radon")?; + sub.setattr("__doc__", "Radon transform and tomographic reconstruction, plus Hankel/Abel transforms and Hough voting. Images are row-major (index = y·w + x) with the projection geometry centered on the image; a projection at angle θ integrates along lines perpendicular to the direction (cos θ, sin θ).")?; + m_transforms__radon::register(py, &sub)?; + mods["transforms"].add("radon", &sub)?; + mods.insert("transforms::radon", sub); + } + { + let sub = PyModule::new(py, "numeria.transforms.spectral")?; + sub.setattr("__doc__", "Spectral estimation: periodogram, Welch averaging, multitaper (DPSS), parametric AR models (Burg, Yule-Walker), MUSIC, cross-spectra, coherence, cepstra, Lomb-Scargle, and spectrum descriptors. All PSDs are one-sided densities in units²/Hz: integrating them over frequency (trapezoid over the returned grid) recovers the signal's variance/power.")?; + m_transforms__spectral::register(py, &sub)?; + mods["transforms"].add("spectral", &sub)?; + mods.insert("transforms::spectral", sub); + } + { + let sub = PyModule::new(py, "numeria.transforms.stft")?; + sub.setattr("__doc__", "Short-time Fourier transform, spectrograms, Goertzel, chirp-z, and constant-Q analysis.")?; + m_transforms__stft::register(py, &sub)?; + mods["transforms"].add("stft", &sub)?; + mods.insert("transforms::stft", sub); + } + { + let sub = PyModule::new(py, "numeria.transforms.wavelet")?; + sub.setattr("__doc__", "Discrete and continuous wavelet transforms. Filter banks, boundary handling, and coefficient lengths follow the PyWavelets conventions (dwt output length ⌊(n + L − 1)/2⌋, idwt output length 2·len − L + 2), so round trips are exact for every wavelet and padding mode. The CWT follows Torrence & Compo (1998).")?; + m_transforms__wavelet::register(py, &sub)?; + mods["transforms"].add("wavelet", &sub)?; + mods.insert("transforms::wavelet", sub); + } + { + let sub = PyModule::new(py, "numeria.units.dimensional")?; + sub.setattr("__doc__", "Dimensional analysis: Buckingham's theorem, the named groups, natural units and the Planck scale. # Buckingham's theorem is a rank computation A physical relation among `n` quantities built from `r` independent dimensions can be rewritten as a relation among exactly `n - r` dimensionless groups. That is not a heuristic: the dimension vectors form the columns of a matrix, a dimensionless product of powers is a vector in its null space, and the dimension of a null space is the column count minus the rank. Every part of it is linear algebra over the rationals. Which is why `buckingham_pi` works in `Rational` rather than in floating point. An exponent vector is *exactly* in the null space or it is not, and a group whose dimensions cancel to `1e-16` instead of to zero is not a dimensionless group -- it is a rounding error that will be reported as physics. The returned exponents are exact rationals for the same reason: the Reynolds number's exponents happen to be integers, but the null space basis of a general problem is not integral, and rounding it would silently change the group. The theorem says how many groups there are, not which ones. Any basis of the null space works, and the conventional groups -- Reynolds, Froude, Mach -- are particular choices made for physical reasons that the algebra knows nothing about. `dimensionless_groups_named` lists those conventions; `buckingham_pi` finds a basis and makes no claim that it is the one anybody would name. # Natural units are a change of bookkeeping, not of physics Setting `hbar = c = 1` makes length, time and mass powers of a single unit, conventionally energy: `[L] = [T] = [E]^-1` and `[M] = [E]`. Nothing physical changes -- the dimensionless combinations are the same -- but a quantity's dimension collapses to one integer, its energy power, and `natural_units_convert` returns the magnitude in `eV` to that power. Electromagnetic and thermal dimensions need further conventions to absorb, so a dimension involving amperes, kelvin, moles or candela is refused rather than guessed at. # Checking a formula is not the same as evaluating it `dimensional_check_formula` walks a symbolic expression and asks whether it is dimensionally coherent: that every term of every sum agrees, and that nothing dimensioned is handed to a sine or an exponential. Neither question can be answered by running the formula, because both sides of `x + v` are perfectly good floats. It is the check a physicist does by eye before believing an algebra step, done mechanically, and it catches the dropped factor that numerical testing cannot.")?; + m_units__dimensional::register(py, &sub)?; + mods["units"].add("dimensional", &sub)?; + mods.insert("units::dimensional", sub); + } + { + let sub = PyModule::new(py, "numeria.units.quantity")?; + sub.setattr("__doc__", "Values that carry their dimensions. # Why a number alone is not a measurement The two most expensive unit mistakes on record -- the Mars Climate Orbiter's pound-seconds fed to a newton-second interface, and the Gimli Glider's kilograms of fuel loaded as pounds -- were both arithmetic that a computer performed correctly on numbers that meant something other than what the receiving code assumed. Neither was a rounding error and neither would have been caught by testing the arithmetic. A `Quantity` carries seven small integers alongside its value: the exponents of metre, kilogram, second, ampere, kelvin, mole and candela. Addition then checks that the two exponent vectors agree and refuses if they do not, multiplication adds them, and taking a square root fails unless every one of them is even. None of this is approximate -- the exponents are integers and the checks are exact. # The gram is the prefixable unit, not the kilogram The SI base unit of mass is the kilogram, which is the only base unit whose name already contains a prefix. The prefix system therefore attaches to the *gram*: `mg` is a milligram and not a milli-kilogram, and `kg` parses here as kilo applied to gram. The unit table stores the gram at `1e-3`, which makes `kg` come out at exactly one and the oddity disappear. # Parsing a unit is ambiguous and the rule has to be stated `m` is both the metre and the milli prefix, `T` is both the tesla and tera, `min` starts with the milli prefix followed by `in`. The rule used is: try the whole token as a unit name first, and only if that fails split off a prefix. So `m` is a metre, `mm` is a millimetre, `min` is a minute, and `T` is a tesla. It is a rule rather than a deduction, and any other rule would give different answers for the same strings.")?; + m_units__quantity::register(py, &sub)?; + mods["units"].add("quantity", &sub)?; + mods.insert("units::quantity", sub); + } + { + let sub = PyModule::new(py, "numeria.fractals.attractors.presets")?; + sub.setattr("__doc__", "Named systems: 3-D flows with customary parameters and time")?; + m_fractals__attractors__presets::register(py, &sub)?; + mods["fractals::attractors"].add("presets", &sub)?; + mods.insert("fractals::attractors::presets", sub); + } + { + let sub = PyModule::new(py, "numeria.fractals.automata.patterns")?; + sub.setattr("__doc__", "Classic Game of Life patterns in `.O` rows.")?; + m_fractals__automata__patterns::register(py, &sub)?; + mods["fractals::automata"].add("patterns", &sub)?; + mods.insert("fractals::automata::patterns", sub); + } + { + let sub = PyModule::new(py, "numeria.fractals.ifs.presets")?; + sub.setattr("__doc__", "Classic IFS attractors. 2-D maps are written x' = ax + by + e,")?; + m_fractals__ifs__presets::register(py, &sub)?; + mods["fractals::ifs"].add("presets", &sub)?; + mods.insert("fractals::ifs::presets", sub); + } + { + let sub = PyModule::new(py, "numeria.fractals.lsystem.presets")?; + sub.setattr("__doc__", "Classic L-systems, mostly from ABOP. Angles are the turtle turn")?; + m_fractals__lsystem__presets::register(py, &sub)?; + mods["fractals::lsystem"].add("presets", &sub)?; + mods.insert("fractals::lsystem::presets", sub); + } + { + let sub = PyModule::new(py, "numeria.manifold.clifford.cga3")?; + sub.setattr("__doc__", "Conformal geometric algebra Cl(4, 1): points, spheres, circles, lines")?; + m_manifold__clifford__cga3::register(py, &sub)?; + mods["manifold::clifford"].add("cga3", &sub)?; + mods.insert("manifold::clifford::cga3", sub); + } + { + let sub = PyModule::new(py, "numeria.manifold.clifford.cl3")?; + sub.setattr("__doc__", "Euclidean 3D geometric algebra Cl(3, 0).")?; + m_manifold__clifford__cl3::register(py, &sub)?; + mods["manifold::clifford"].add("cl3", &sub)?; + mods.insert("manifold::clifford::cl3", sub); + } + { + let sub = PyModule::new(py, "numeria.manifold.clifford.pga3")?; + sub.setattr("__doc__", "Plane-based projective geometric algebra Cl(3, 0, 1): planes are")?; + m_manifold__clifford__pga3::register(py, &sub)?; + mods["manifold::clifford"].add("pga3", &sub)?; + mods.insert("manifold::clifford::pga3", sub); + } + { + let sub = PyModule::new(py, "numeria.manifold.clifford.sta")?; + sub.setattr("__doc__", "Spacetime algebra Cl(1, 3): gamma_0 squares to +1 (bit 0), the spatial")?; + m_manifold__clifford__sta::register(py, &sub)?; + mods["manifold::clifford"].add("sta", &sub)?; + mods.insert("manifold::clifford::sta", sub); + } + { + let sub = PyModule::new(py, "numeria.numerical.ode.adaptive")?; + sub.setattr("__doc__", "Adaptive Runge-Kutta integration: Dormand-Prince 5(4). Reference: Dormand & Prince, \"A family of embedded Runge-Kutta formulae\" (1980); Hairer, Nørsett & Wanner, *Solving ODEs I*, §II.4. The embedded 4th-order solution provides the error estimate; steps use the FSAL (first-same-as-last) property.")?; + m_numerical__ode__adaptive::register(py, &sub)?; + mods["numerical::ode"].add("adaptive", &sub)?; + mods.insert("numerical::ode::adaptive", sub); + } + { + let sub = PyModule::new(py, "numeria.numerical.ode.explicit")?; + sub.setattr("__doc__", "Explicit fixed-step ODE integrators.")?; + m_numerical__ode__explicit::register(py, &sub)?; + mods["numerical::ode"].add("explicit", &sub)?; + mods.insert("numerical::ode::explicit", sub); + } + { + let sub = PyModule::new(py, "numeria.numerical.ode.implicit")?; + sub.setattr("__doc__", "Implicit (stiff-stable) ODE steps: backward Euler and BDF2. Both solve their implicit update equation with damped-free Newton iteration; the Jacobian of f is user-supplied or, when `None`, approximated by forward finite differences (an opaque `f64` closure cannot be differentiated with `core::dual`, so finite differences stand in for it here).")?; + m_numerical__ode__implicit::register(py, &sub)?; + mods["numerical::ode"].add("implicit", &sub)?; + mods.insert("numerical::ode::implicit", sub); + } + { + let sub = PyModule::new(py, "numeria.numerical.ode.symplectic")?; + sub.setattr("__doc__", "Symplectic integrators for second-order systems x'' = a(x). These preserve phase-space volume, so energy errors stay bounded instead of drifting. References: Verlet (1967); Yoshida, \"Construction of higher order symplectic integrators\", Phys. Lett. A 150 (1990).")?; + m_numerical__ode__symplectic::register(py, &sub)?; + mods["numerical::ode"].add("symplectic", &sub)?; + mods.insert("numerical::ode::symplectic", sub); + } + { let v = mods["audio::envelope"].getattr("Adsr")?; mods["audio"].add("Adsr", v)?; } + { let v = mods["audio::envelope"].getattr("AdsrExp")?; mods["audio"].add("AdsrExp", v)?; } + { let v = mods["audio::effects"].getattr("AllpassFilter")?; mods["audio"].add("AllpassFilter", v)?; } + { let v = mods["audio::envelope"].getattr("Ar")?; mods["audio"].add("Ar", v)?; } + { let v = mods["audio::physical"].getattr("BowedString")?; mods["audio"].add("BowedString", v)?; } + { let v = mods["audio::tuning"].getattr("ChordQuality")?; mods["audio"].add("ChordQuality", v)?; } + { let v = mods["audio::effects"].getattr("Chorus")?; mods["audio"].add("Chorus", v)?; } + { let v = mods["audio::effects"].getattr("CombFilter")?; mods["audio"].add("CombFilter", v)?; } + { let v = mods["audio::effects"].getattr("Compressor")?; mods["audio"].add("Compressor", v)?; } + { let v = mods["audio::effects"].getattr("DeEsser")?; mods["audio"].add("DeEsser", v)?; } + { let v = mods["audio::effects"].getattr("DelayLine")?; mods["audio"].add("DelayLine", v)?; } + { let v = mods["audio::effects"].getattr("Eq")?; mods["audio"].add("Eq", v)?; } + { let v = mods["audio::vocoder"].getattr("Excitation")?; mods["audio"].add("Excitation", v)?; } + { let v = mods["audio::effects"].getattr("Exciter")?; mods["audio"].add("Exciter", v)?; } + { let v = mods["audio::effects"].getattr("Expander")?; mods["audio"].add("Expander", v)?; } + { let v = mods["audio::envelope"].getattr("FadeShape")?; mods["audio"].add("FadeShape", v)?; } + { let v = mods["audio::effects"].getattr("Fdn")?; mods["audio"].add("Fdn", v)?; } + { let v = mods["audio::effects"].getattr("Flanger")?; mods["audio"].add("Flanger", v)?; } + { let v = mods["audio::synthesis"].getattr("FmOperator")?; mods["audio"].add("FmOperator", v)?; } + { let v = mods["audio::synthesis"].getattr("FmSynth")?; mods["audio"].add("FmSynth", v)?; } + { let v = mods["audio::effects"].getattr("Freeverb")?; mods["audio"].add("Freeverb", v)?; } + { let v = mods["audio::physical"].getattr("KellyLochbaum")?; mods["audio"].add("KellyLochbaum", v)?; } + { let v = mods["audio::envelope"].getattr("Lfo")?; mods["audio"].add("Lfo", v)?; } + { let v = mods["audio::effects"].getattr("Limiter")?; mods["audio"].add("Limiter", v)?; } + { let v = mods["audio::physical"].getattr("MassSpringString")?; mods["audio"].add("MassSpringString", v)?; } + { let v = mods["audio::physical"].getattr("Membrane2D")?; mods["audio"].add("Membrane2D", v)?; } + { let v = mods["audio::physical"].getattr("ModalSynth")?; mods["audio"].add("ModalSynth", v)?; } + { let v = mods["audio::tuning"].getattr("Mode")?; mods["audio"].add("Mode", v)?; } + { let v = mods["audio::oscillators"].getattr("NoiseColor")?; mods["audio"].add("NoiseColor", v)?; } + { let v = mods["audio::effects"].getattr("NoiseGate")?; mods["audio"].add("NoiseGate", v)?; } + { let v = mods["audio::oscillators"].getattr("NoiseGen")?; mods["audio"].add("NoiseGen", v)?; } + { let v = mods["audio::oscillators"].getattr("Oscillator")?; mods["audio"].add("Oscillator", v)?; } + { let v = mods["audio::effects"].getattr("PartitionedConvolver")?; mods["audio"].add("PartitionedConvolver", v)?; } + { let v = mods["audio::vocoder"].getattr("PhaseVocoder")?; mods["audio"].add("PhaseVocoder", v)?; } + { let v = mods["audio::effects"].getattr("Phaser")?; mods["audio"].add("Phaser", v)?; } + { let v = mods["audio::analysis"].getattr("PitchMethod")?; mods["audio"].add("PitchMethod", v)?; } + { let v = mods["audio::physical"].getattr("Plate2D")?; mods["audio"].add("Plate2D", v)?; } + { let v = mods["audio::effects"].getattr("SchroederReverb")?; mods["audio"].add("SchroederReverb", v)?; } + { let v = mods["audio::analysis"].getattr("SpectralFeatures")?; mods["audio"].add("SpectralFeatures", v)?; } + { let v = mods["audio::effects"].getattr("StereoWidener")?; mods["audio"].add("StereoWidener", v)?; } + { let v = mods["audio::effects"].getattr("Tremolo")?; mods["audio"].add("Tremolo", v)?; } + { let v = mods["audio::effects"].getattr("Vibrato")?; mods["audio"].add("Vibrato", v)?; } + { let v = mods["audio::synthesis"].getattr("Voice")?; mods["audio"].add("Voice", v)?; } + { let v = mods["audio::wav"].getattr("WavData")?; mods["audio"].add("WavData", v)?; } + { let v = mods["audio::oscillators"].getattr("Waveform")?; mods["audio"].add("Waveform", v)?; } + { let v = mods["audio::physical"].getattr("WaveguideString")?; mods["audio"].add("WaveguideString", v)?; } + { let v = mods["audio::physical"].getattr("WaveguideTube")?; mods["audio"].add("WaveguideTube", v)?; } + { let v = mods["audio::oscillators"].getattr("Wavetable")?; mods["audio"].add("Wavetable", v)?; } + { let v = mods["audio::synthesis"].getattr("additive")?; mods["audio"].add("additive", v)?; } + { let v = mods["audio::synthesis"].getattr("additive_evolving")?; mods["audio"].add("additive_evolving", v)?; } + { let v = mods["audio::oscillators"].getattr("additive_saw")?; mods["audio"].add("additive_saw", v)?; } + { let v = mods["audio::oscillators"].getattr("additive_square")?; mods["audio"].add("additive_square", v)?; } + { let v = mods["audio::oscillators"].getattr("additive_triangle")?; mods["audio"].add("additive_triangle", v)?; } + { let v = mods["audio::spatial"].getattr("air_absorption_filter")?; mods["audio"].add("air_absorption_filter", v)?; } + { let v = mods["audio::synthesis"].getattr("am")?; mods["audio"].add("am", v)?; } + { let v = mods["audio::spatial"].getattr("ambisonics_decode")?; mods["audio"].add("ambisonics_decode", v)?; } + { let v = mods["audio::spatial"].getattr("ambisonics_encode")?; mods["audio"].add("ambisonics_encode", v)?; } + { let v = mods["audio::spatial"].getattr("ambisonics_encode_1st")?; mods["audio"].add("ambisonics_encode_1st", v)?; } + { let v = mods["audio::spatial"].getattr("ambisonics_rotate")?; mods["audio"].add("ambisonics_rotate", v)?; } + { let v = mods["audio::envelope"].getattr("apply_envelope")?; mods["audio"].add("apply_envelope", v)?; } + { let v = mods["audio::analysis"].getattr("audio_fingerprint")?; mods["audio"].add("audio_fingerprint", v)?; } + { let v = mods["audio::analysis"].getattr("autocorrelation_fft")?; mods["audio"].add("autocorrelation_fft", v)?; } + { let v = mods["audio::vocoder"].getattr("autotune")?; mods["audio"].add("autotune", v)?; } + { let v = mods["audio::oscillators"].getattr("band_limited_impulse_train")?; mods["audio"].add("band_limited_impulse_train", v)?; } + { let v = mods["audio::physical"].getattr("banded_waveguide")?; mods["audio"].add("banded_waveguide", v)?; } + { let v = mods["audio::spatial"].getattr("beamforming_delay_sum")?; mods["audio"].add("beamforming_delay_sum", v)?; } + { let v = mods["audio::spatial"].getattr("beamforming_mvdr")?; mods["audio"].add("beamforming_mvdr", v)?; } + { let v = mods["audio::analysis"].getattr("beat_track")?; mods["audio"].add("beat_track", v)?; } + { let v = mods["audio::spatial"].getattr("binaural_simple")?; mods["audio"].add("binaural_simple", v)?; } + { let v = mods["audio::tuning"].getattr("bohlen_pierce")?; mods["audio"].add("bohlen_pierce", v)?; } + { let v = mods["audio::analysis"].getattr("c50")?; mods["audio"].add("c50", v)?; } + { let v = mods["audio::analysis"].getattr("c80")?; mods["audio"].add("c80", v)?; } + { let v = mods["audio::tuning"].getattr("cents_between")?; mods["audio"].add("cents_between", v)?; } + { let v = mods["audio::tuning"].getattr("cents_to_ratio")?; mods["audio"].add("cents_to_ratio", v)?; } + { let v = mods["audio::vocoder"].getattr("channel_vocoder")?; mods["audio"].add("channel_vocoder", v)?; } + { let v = mods["audio::synthesis"].getattr("chebyshev_waveshaper")?; mods["audio"].add("chebyshev_waveshaper", v)?; } + { let v = mods["audio::oscillators"].getattr("chirp_exponential")?; mods["audio"].add("chirp_exponential", v)?; } + { let v = mods["audio::oscillators"].getattr("chirp_hyperbolic")?; mods["audio"].add("chirp_hyperbolic", v)?; } + { let v = mods["audio::oscillators"].getattr("chirp_linear")?; mods["audio"].add("chirp_linear", v)?; } + { let v = mods["audio::analysis"].getattr("chord_estimate")?; mods["audio"].add("chord_estimate", v)?; } + { let v = mods["audio::tuning"].getattr("chord_tones")?; mods["audio"].add("chord_tones", v)?; } + { let v = mods["audio::analysis"].getattr("chroma")?; mods["audio"].add("chroma", v)?; } + { let v = mods["audio::tuning"].getattr("circle_of_fifths")?; mods["audio"].add("circle_of_fifths", v)?; } + { let v = mods["audio::physical"].getattr("commuted_synthesis")?; mods["audio"].add("commuted_synthesis", v)?; } + { let v = mods["audio::tuning"].getattr("consonance_plomp_levelt")?; mods["audio"].add("consonance_plomp_levelt", v)?; } + { let v = mods["audio::effects"].getattr("convolution_reverb")?; mods["audio"].add("convolution_reverb", v)?; } + { let v = mods["audio::vocoder"].getattr("cross_synthesis")?; mods["audio"].add("cross_synthesis", v)?; } + { let v = mods["audio::envelope"].getattr("crossfade")?; mods["audio"].add("crossfade", v)?; } + { let v = mods["audio::analysis"].getattr("d50")?; mods["audio"].add("d50", v)?; } + { let v = mods["audio::oscillators"].getattr("dc")?; mods["audio"].add("dc", v)?; } + { let v = mods["audio::effects"].getattr("dc_offset_remove")?; mods["audio"].add("dc_offset_remove", v)?; } + { let v = mods["audio::effects"].getattr("declick")?; mods["audio"].add("declick", v)?; } + { let v = mods["audio::analysis"].getattr("delta_features")?; mods["audio"].add("delta_features", v)?; } + { let v = mods["audio::tuning"].getattr("dissonance_curve")?; mods["audio"].add("dissonance_curve", v)?; } + { let v = mods["audio::spatial"].getattr("distance_gain")?; mods["audio"].add("distance_gain", v)?; } + { let v = mods["audio::effects"].getattr("distortion_foldback")?; mods["audio"].add("distortion_foldback", v)?; } + { let v = mods["audio::effects"].getattr("distortion_hard_clip")?; mods["audio"].add("distortion_hard_clip", v)?; } + { let v = mods["audio::effects"].getattr("distortion_soft_clip")?; mods["audio"].add("distortion_soft_clip", v)?; } + { let v = mods["audio::effects"].getattr("distortion_tube")?; mods["audio"].add("distortion_tube", v)?; } + { let v = mods["audio::effects"].getattr("dither_tpdf")?; mods["audio"].add("dither_tpdf", v)?; } + { let v = mods["audio::spatial"].getattr("doppler_resample")?; mods["audio"].add("doppler_resample", v)?; } + { let v = mods["audio::synthesis"].getattr("drum_clap")?; mods["audio"].add("drum_clap", v)?; } + { let v = mods["audio::synthesis"].getattr("drum_hihat")?; mods["audio"].add("drum_hihat", v)?; } + { let v = mods["audio::synthesis"].getattr("drum_kick")?; mods["audio"].add("drum_kick", v)?; } + { let v = mods["audio::synthesis"].getattr("drum_snare")?; mods["audio"].add("drum_snare", v)?; } + { let v = mods["audio::synthesis"].getattr("drum_tom")?; mods["audio"].add("drum_tom", v)?; } + { let v = mods["audio::analysis"].getattr("dynamic_time_warping")?; mods["audio"].add("dynamic_time_warping", v)?; } + { let v = mods["audio::spatial"].getattr("early_reflections")?; mods["audio"].add("early_reflections", v)?; } + { let v = mods["audio::analysis"].getattr("edt_from_ir")?; mods["audio"].add("edt_from_ir", v)?; } + { let v = mods["audio::analysis"].getattr("enob")?; mods["audio"].add("enob", v)?; } + { let v = mods["audio::envelope"].getattr("envelope_follower")?; mods["audio"].add("envelope_follower", v)?; } + { let v = mods["audio::tuning"].getattr("equal_temperament")?; mods["audio"].add("equal_temperament", v)?; } + { let v = mods["audio::analysis"].getattr("estimate_snr")?; mods["audio"].add("estimate_snr", v)?; } + { let v = mods["audio::envelope"].getattr("exponential_decay_envelope")?; mods["audio"].add("exponential_decay_envelope", v)?; } + { let v = mods["audio::envelope"].getattr("fade_in")?; mods["audio"].add("fade_in", v)?; } + { let v = mods["audio::envelope"].getattr("fade_out")?; mods["audio"].add("fade_out", v)?; } + { let v = mods["audio::analysis"].getattr("fluctuation_strength")?; mods["audio"].add("fluctuation_strength", v)?; } + { let v = mods["audio::synthesis"].getattr("fm_bessel_sidebands")?; mods["audio"].add("fm_bessel_sidebands", v)?; } + { let v = mods["audio::synthesis"].getattr("fm_simple")?; mods["audio"].add("fm_simple", v)?; } + { let v = mods["audio::synthesis"].getattr("formant_synth")?; mods["audio"].add("formant_synth", v)?; } + { let v = mods["audio::analysis"].getattr("formant_track")?; mods["audio"].add("formant_track", v)?; } + { let v = mods["audio::wav"].getattr("from_interleaved")?; mods["audio"].add("from_interleaved", v)?; } + { let v = mods["audio::effects"].getattr("gain_db")?; mods["audio"].add("gain_db", v)?; } + { let v = mods["audio::physical"].getattr("glottal_pulse_lf")?; mods["audio"].add("glottal_pulse_lf", v)?; } + { let v = mods["audio::synthesis"].getattr("granular")?; mods["audio"].add("granular", v)?; } + { let v = mods["audio::effects"].getattr("haas_delay")?; mods["audio"].add("haas_delay", v)?; } + { let v = mods["audio::physical"].getattr("hammer_string_interaction")?; mods["audio"].add("hammer_string_interaction", v)?; } + { let v = mods["audio::synthesis"].getattr("hard_sync_osc")?; mods["audio"].add("hard_sync_osc", v)?; } + { let v = mods["audio::tuning"].getattr("harmonic_series_scale")?; mods["audio"].add("harmonic_series_scale", v)?; } + { let v = mods["audio::analysis"].getattr("harmonic_to_noise_ratio")?; mods["audio"].add("harmonic_to_noise_ratio", v)?; } + { let v = mods["audio::vocoder"].getattr("harmonizer")?; mods["audio"].add("harmonizer", v)?; } + { let v = mods["audio::spatial"].getattr("ild_spherical_head")?; mods["audio"].add("ild_spherical_head", v)?; } + { let v = mods["audio::spatial"].getattr("image_source_ir")?; mods["audio"].add("image_source_ir", v)?; } + { let v = mods["audio::oscillators"].getattr("impulse")?; mods["audio"].add("impulse", v)?; } + { let v = mods["audio::analysis"].getattr("impulse_response_from_sweep")?; mods["audio"].add("impulse_response_from_sweep", v)?; } + { let v = mods["audio::physical"].getattr("inharmonic_partials")?; mods["audio"].add("inharmonic_partials", v)?; } + { let v = mods["audio::analysis"].getattr("inharmonicity_measure")?; mods["audio"].add("inharmonicity_measure", v)?; } + { let v = mods["audio::tuning"].getattr("interval_name")?; mods["audio"].add("interval_name", v)?; } + { let v = mods["audio::spatial"].getattr("itd_woodworth")?; mods["audio"].add("itd_woodworth", v)?; } + { let v = mods["audio::physical"].getattr("jet_nonlinearity")?; mods["audio"].add("jet_nonlinearity", v)?; } + { let v = mods["audio::tuning"].getattr("just_intonation_5limit")?; mods["audio"].add("just_intonation_5limit", v)?; } + { let v = mods["audio::synthesis"].getattr("karplus_strong")?; mods["audio"].add("karplus_strong", v)?; } + { let v = mods["audio::synthesis"].getattr("karplus_strong_extended")?; mods["audio"].add("karplus_strong_extended", v)?; } + { let v = mods["audio::tuning"].getattr("kirnberger_iii")?; mods["audio"].add("kirnberger_iii", v)?; } + { let v = mods["audio::physical"].getattr("lip_model")?; mods["audio"].add("lip_model", v)?; } + { let v = mods["audio::spatial"].getattr("localize_tdoa")?; mods["audio"].add("localize_tdoa", v)?; } + { let v = mods["audio::analysis"].getattr("loudness_sone")?; mods["audio"].add("loudness_sone", v)?; } + { let v = mods["audio::analysis"].getattr("lpc")?; mods["audio"].add("lpc", v)?; } + { let v = mods["audio::analysis"].getattr("lpc_spectrum")?; mods["audio"].add("lpc_spectrum", v)?; } + { let v = mods["audio::analysis"].getattr("lpc_to_formants")?; mods["audio"].add("lpc_to_formants", v)?; } + { let v = mods["audio::analysis"].getattr("lpc_to_lsp")?; mods["audio"].add("lpc_to_lsp", v)?; } + { let v = mods["audio::vocoder"].getattr("lpc_vocoder")?; mods["audio"].add("lpc_vocoder", v)?; } + { let v = mods["audio::analysis"].getattr("lsp_to_lpc")?; mods["audio"].add("lsp_to_lpc", v)?; } + { let v = mods["audio::tuning"].getattr("meantone_quarter_comma")?; mods["audio"].add("meantone_quarter_comma", v)?; } + { let v = mods["audio::effects"].getattr("measure_lufs")?; mods["audio"].add("measure_lufs", v)?; } + { let v = mods["audio::analysis"].getattr("mfcc")?; mods["audio"].add("mfcc", v)?; } + { let v = mods["audio::tuning"].getattr("midi_to_freq_tuned")?; mods["audio"].add("midi_to_freq_tuned", v)?; } + { let v = mods["audio::synthesis"].getattr("mix")?; mods["audio"].add("mix", v)?; } + { let v = mods["audio::oscillators"].getattr("multisine")?; mods["audio"].add("multisine", v)?; } + { let v = mods["audio::tuning"].getattr("nearest_note")?; mods["audio"].add("nearest_note", v)?; } + { let v = mods["audio::effects"].getattr("noise_shaping_dither")?; mods["audio"].add("noise_shaping_dither", v)?; } + { let v = mods["audio::effects"].getattr("normalize_lufs")?; mods["audio"].add("normalize_lufs", v)?; } + { let v = mods["audio::effects"].getattr("normalize_peak")?; mods["audio"].add("normalize_peak", v)?; } + { let v = mods["audio::effects"].getattr("normalize_rms")?; mods["audio"].add("normalize_rms", v)?; } + { let v = mods["audio::analysis"].getattr("onset_complex_domain")?; mods["audio"].add("onset_complex_domain", v)?; } + { let v = mods["audio::analysis"].getattr("onset_detect")?; mods["audio"].add("onset_detect", v)?; } + { let v = mods["audio::analysis"].getattr("onset_hfc")?; mods["audio"].add("onset_hfc", v)?; } + { let v = mods["audio::analysis"].getattr("onset_strength")?; mods["audio"].add("onset_strength", v)?; } + { let v = mods["audio::effects"].getattr("oversample_process")?; mods["audio"].add("oversample_process", v)?; } + { let v = mods["audio::spatial"].getattr("pan_constant_power")?; mods["audio"].add("pan_constant_power", v)?; } + { let v = mods["audio::spatial"].getattr("pan_linear")?; mods["audio"].add("pan_linear", v)?; } + { let v = mods["audio::spatial"].getattr("pan_minus_4_5_db")?; mods["audio"].add("pan_minus_4_5_db", v)?; } + { let v = mods["audio::spatial"].getattr("pan_vbap_2d")?; mods["audio"].add("pan_vbap_2d", v)?; } + { let v = mods["audio::spatial"].getattr("pan_vbap_3d")?; mods["audio"].add("pan_vbap_3d", v)?; } + { let v = mods["audio::envelope"].getattr("peak_envelope")?; mods["audio"].add("peak_envelope", v)?; } + { let v = mods["audio::analysis"].getattr("peak_pick")?; mods["audio"].add("peak_pick", v)?; } + { let v = mods["audio::synthesis"].getattr("phase_distortion")?; mods["audio"].add("phase_distortion", v)?; } + { let v = mods["audio::analysis"].getattr("pitch_autocorrelation")?; mods["audio"].add("pitch_autocorrelation", v)?; } + { let v = mods["audio::analysis"].getattr("pitch_cepstral")?; mods["audio"].add("pitch_cepstral", v)?; } + { let v = mods["audio::analysis"].getattr("pitch_hps")?; mods["audio"].add("pitch_hps", v)?; } + { let v = mods["audio::analysis"].getattr("pitch_mpm")?; mods["audio"].add("pitch_mpm", v)?; } + { let v = mods["audio::effects"].getattr("pitch_shift_simple")?; mods["audio"].add("pitch_shift_simple", v)?; } + { let v = mods["audio::analysis"].getattr("pitch_to_midi_track")?; mods["audio"].add("pitch_to_midi_track", v)?; } + { let v = mods["audio::analysis"].getattr("pitch_track")?; mods["audio"].add("pitch_track", v)?; } + { let v = mods["audio::analysis"].getattr("pitch_yin")?; mods["audio"].add("pitch_yin", v)?; } + { let v = mods["audio::synthesis"].getattr("pm_simple")?; mods["audio"].add("pm_simple", v)?; } + { let v = mods["audio::oscillators"].getattr("polyblep_saw")?; mods["audio"].add("polyblep_saw", v)?; } + { let v = mods["audio::oscillators"].getattr("polyblep_square")?; mods["audio"].add("polyblep_square", v)?; } + { let v = mods["audio::oscillators"].getattr("polyblep_triangle")?; mods["audio"].add("polyblep_triangle", v)?; } + { let v = mods["audio::envelope"].getattr("portamento")?; mods["audio"].add("portamento", v)?; } + { let v = mods["audio::vocoder"].getattr("psola_pitch_shift")?; mods["audio"].add("psola_pitch_shift", v)?; } + { let v = mods["audio::synthesis"].getattr("pulsar_synthesis")?; mods["audio"].add("pulsar_synthesis", v)?; } + { let v = mods["audio::oscillators"].getattr("pulse_train")?; mods["audio"].add("pulse_train", v)?; } + { let v = mods["audio::tuning"].getattr("pythagorean")?; mods["audio"].add("pythagorean", v)?; } + { let v = mods["audio::tuning"].getattr("pythagorean_comma")?; mods["audio"].add("pythagorean_comma", v)?; } + { let v = mods["audio::tuning"].getattr("ratio_to_cents")?; mods["audio"].add("ratio_to_cents", v)?; } + { let v = mods["audio::spatial"].getattr("ray_tracing_ir")?; mods["audio"].add("ray_tracing_ir", v)?; } + { let v = mods["audio::physical"].getattr("reed_nonlinearity")?; mods["audio"].add("reed_nonlinearity", v)?; } + { let v = mods["audio::synthesis"].getattr("ring_mod")?; mods["audio"].add("ring_mod", v)?; } + { let v = mods["audio::envelope"].getattr("rms_envelope")?; mods["audio"].add("rms_envelope", v)?; } + { let v = mods["audio::physical"].getattr("rosenberg_pulse")?; mods["audio"].add("rosenberg_pulse", v)?; } + { let v = mods["audio::analysis"].getattr("roughness")?; mods["audio"].add("roughness", v)?; } + { let v = mods["audio::analysis"].getattr("rt60_from_ir")?; mods["audio"].add("rt60_from_ir", v)?; } + { let v = mods["audio::synthesis"].getattr("sample_playback")?; mods["audio"].add("sample_playback", v)?; } + { let v = mods["audio::tuning"].getattr("scala_parse")?; mods["audio"].add("scala_parse", v)?; } + { let v = mods["audio::tuning"].getattr("scale_degrees")?; mods["audio"].add("scale_degrees", v)?; } + { let v = mods["audio::tuning"].getattr("schisma")?; mods["audio"].add("schisma", v)?; } + { let v = mods["audio::oscillators"].getattr("schroeder_phase_multisine")?; mods["audio"].add("schroeder_phase_multisine", v)?; } + { let v = mods["audio::analysis"].getattr("sharpness")?; mods["audio"].add("sharpness", v)?; } + { let v = mods["audio::analysis"].getattr("silence_detect")?; mods["audio"].add("silence_detect", v)?; } + { let v = mods["audio::analysis"].getattr("sinad")?; mods["audio"].add("sinad", v)?; } + { let v = mods["audio::oscillators"].getattr("sine_sweep_with_inverse")?; mods["audio"].add("sine_sweep_with_inverse", v)?; } + { let v = mods["audio::spatial"].getattr("sonar_equation")?; mods["audio"].add("sonar_equation", v)?; } + { let v = mods["audio::spatial"].getattr("sonar_range")?; mods["audio"].add("sonar_range", v)?; } + { let v = mods["audio::spatial"].getattr("speaker_baffle_step")?; mods["audio"].add("speaker_baffle_step", v)?; } + { let v = mods["audio::spatial"].getattr("speaker_crossover_lr4")?; mods["audio"].add("speaker_crossover_lr4", v)?; } + { let v = mods["audio::analysis"].getattr("spectral_centroid")?; mods["audio"].add("spectral_centroid", v)?; } + { let v = mods["audio::analysis"].getattr("spectral_crest")?; mods["audio"].add("spectral_crest", v)?; } + { let v = mods["audio::analysis"].getattr("spectral_decrease")?; mods["audio"].add("spectral_decrease", v)?; } + { let v = mods["audio::analysis"].getattr("spectral_entropy_mag")?; mods["audio"].add("spectral_entropy_mag", v)?; } + { let v = mods["audio::analysis"].getattr("spectral_features_track")?; mods["audio"].add("spectral_features_track", v)?; } + { let v = mods["audio::analysis"].getattr("spectral_flatness_mag")?; mods["audio"].add("spectral_flatness_mag", v)?; } + { let v = mods["audio::analysis"].getattr("spectral_flux")?; mods["audio"].add("spectral_flux", v)?; } + { let v = mods["audio::effects"].getattr("spectral_gate")?; mods["audio"].add("spectral_gate", v)?; } + { let v = mods["audio::analysis"].getattr("spectral_kurtosis")?; mods["audio"].add("spectral_kurtosis", v)?; } + { let v = mods["audio::vocoder"].getattr("spectral_morph")?; mods["audio"].add("spectral_morph", v)?; } + { let v = mods["audio::analysis"].getattr("spectral_rolloff")?; mods["audio"].add("spectral_rolloff", v)?; } + { let v = mods["audio::analysis"].getattr("spectral_skewness")?; mods["audio"].add("spectral_skewness", v)?; } + { let v = mods["audio::analysis"].getattr("spectral_slope")?; mods["audio"].add("spectral_slope", v)?; } + { let v = mods["audio::analysis"].getattr("spectral_spread")?; mods["audio"].add("spectral_spread", v)?; } + { let v = mods["audio::spatial"].getattr("spherical_head_hrtf")?; mods["audio"].add("spherical_head_hrtf", v)?; } + { let v = mods["audio::analysis"].getattr("sti_approx")?; mods["audio"].add("sti_approx", v)?; } + { let v = mods["audio::tuning"].getattr("stretch_tuning_railsback")?; mods["audio"].add("stretch_tuning_railsback", v)?; } + { let v = mods["audio::physical"].getattr("string_tension_from_freq")?; mods["audio"].add("string_tension_from_freq", v)?; } + { let v = mods["audio::synthesis"].getattr("subtractive")?; mods["audio"].add("subtractive", v)?; } + { let v = mods["audio::synthesis"].getattr("supersaw")?; mods["audio"].add("supersaw", v)?; } + { let v = mods["audio::effects"].getattr("synthesize_ir_exponential")?; mods["audio"].add("synthesize_ir_exponential", v)?; } + { let v = mods["audio::tuning"].getattr("syntonic_comma")?; mods["audio"].add("syntonic_comma", v)?; } + { let v = mods["audio::spatial"].getattr("tdoa_gcc_phat")?; mods["audio"].add("tdoa_gcc_phat", v)?; } + { let v = mods["audio::analysis"].getattr("tempo_estimate")?; mods["audio"].add("tempo_estimate", v)?; } + { let v = mods["audio::analysis"].getattr("thd_n")?; mods["audio"].add("thd_n", v)?; } + { let v = mods["audio::spatial"].getattr("thiele_small_response")?; mods["audio"].add("thiele_small_response", v)?; } + { let v = mods["audio::wav"].getattr("to_interleaved")?; mods["audio"].add("to_interleaved", v)?; } + { let v = mods["audio::wav"].getattr("to_mono")?; mods["audio"].add("to_mono", v)?; } + { let v = mods["audio::analysis"].getattr("transient_detect")?; mods["audio"].add("transient_detect", v)?; } + { let v = mods["audio::effects"].getattr("true_peak")?; mods["audio"].add("true_peak", v)?; } + { let v = mods["audio::physical"].getattr("vocal_tract")?; mods["audio"].add("vocal_tract", v)?; } + { let v = mods["audio::synthesis"].getattr("vowel_formants")?; mods["audio"].add("vowel_formants", v)?; } + { let v = mods["audio::wav"].getattr("wav_info")?; mods["audio"].add("wav_info", v)?; } + { let v = mods["audio::wav"].getattr("wav_read")?; mods["audio"].add("wav_read", v)?; } + { let v = mods["audio::wav"].getattr("wav_read_file")?; mods["audio"].add("wav_read_file", v)?; } + { let v = mods["audio::wav"].getattr("wav_write")?; mods["audio"].add("wav_write", v)?; } + { let v = mods["audio::wav"].getattr("wav_write_file")?; mods["audio"].add("wav_write_file", v)?; } + { let v = mods["audio::synthesis"].getattr("waveshaper")?; mods["audio"].add("waveshaper", v)?; } + { let v = mods["audio::tuning"].getattr("werckmeister_iii")?; mods["audio"].add("werckmeister_iii", v)?; } + { let v = mods["audio::vocoder"].getattr("wsola_time_stretch")?; mods["audio"].add("wsola_time_stretch", v)?; } + { let v = mods["audio::tuning"].getattr("young")?; mods["audio"].add("young", v)?; } + { let v = mods["audio::analysis"].getattr("zero_crossing_rate")?; mods["audio"].add("zero_crossing_rate", v)?; } + { let v = mods["cfd::grid"].getattr("CellField2")?; mods["cfd"].add("CellField2", v)?; } + { let v = mods["cfd::lbm"].getattr("Collision")?; mods["cfd"].add("Collision", v)?; } + { let v = mods["cfd::riemann"].getattr("Cons")?; mods["cfd"].add("Cons", v)?; } + { let v = mods["cfd::potential_flow"].getattr("Element")?; mods["cfd"].add("Element", v)?; } + { let v = mods["cfd::riemann"].getattr("Euler1D")?; mods["cfd"].add("Euler1D", v)?; } + { let v = mods["cfd::riemann"].getattr("Euler2D")?; mods["cfd"].add("Euler2D", v)?; } + { let v = mods["cfd::riemann"].getattr("EulerBc")?; mods["cfd"].add("EulerBc", v)?; } + { let v = mods["cfd::multiphase"].getattr("FlowPattern")?; mods["cfd"].add("FlowPattern", v)?; } + { let v = mods["cfd::grid"].getattr("FluidBc")?; mods["cfd"].add("FluidBc", v)?; } + { let v = mods["cfd::riemann"].getattr("FluxKind")?; mods["cfd"].add("FluxKind", v)?; } + { let v = mods["cfd::level_set"].getattr("FreeSurfaceFluid2")?; mods["cfd"].add("FreeSurfaceFluid2", v)?; } + { let v = mods["cfd::turbulence"].getattr("KEpsilon")?; mods["cfd"].add("KEpsilon", v)?; } + { let v = mods["cfd::turbulence"].getattr("KEpsilonVariant")?; mods["cfd"].add("KEpsilonVariant", v)?; } + { let v = mods["cfd::turbulence"].getattr("KOmegaSst")?; mods["cfd"].add("KOmegaSst", v)?; } + { let v = mods["cfd::sph"].getattr("Kernel")?; mods["cfd"].add("Kernel", v)?; } + { let v = mods["cfd::sph"].getattr("Kind")?; mods["cfd"].add("Kind", v)?; } + { let v = mods["cfd::lbm"].getattr("LbmD2Q9")?; mods["cfd"].add("LbmD2Q9", v)?; } + { let v = mods["cfd::lbm"].getattr("LbmD3Q19")?; mods["cfd"].add("LbmD3Q19", v)?; } + { let v = mods["cfd::lbm"].getattr("LbmD3Q27")?; mods["cfd"].add("LbmD3Q27", v)?; } + { let v = mods["cfd::level_set"].getattr("LevelSet2")?; mods["cfd"].add("LevelSet2", v)?; } + { let v = mods["cfd::level_set"].getattr("LevelSet3")?; mods["cfd"].add("LevelSet3", v)?; } + { let v = mods["cfd::advection"].getattr("Limiter")?; mods["cfd"].add("Limiter", v)?; } + { let v = mods["cfd::grid"].getattr("MacGrid2")?; mods["cfd"].add("MacGrid2", v)?; } + { let v = mods["cfd::grid"].getattr("MacGrid3")?; mods["cfd"].add("MacGrid3", v)?; } + { let v = mods["cfd::potential_flow"].getattr("PanelMethod")?; mods["cfd"].add("PanelMethod", v)?; } + { let v = mods["cfd::sph"].getattr("Plane")?; mods["cfd"].add("Plane", v)?; } + { let v = mods["cfd::potential_flow"].getattr("Plane2")?; mods["cfd"].add("Plane2", v)?; } + { let v = mods["cfd::potential_flow"].getattr("PotentialFlow2")?; mods["cfd"].add("PotentialFlow2", v)?; } + { let v = mods["cfd::stable_fluids"].getattr("PressureSolver")?; mods["cfd"].add("PressureSolver", v)?; } + { let v = mods["cfd::riemann"].getattr("Prim")?; mods["cfd"].add("Prim", v)?; } + { let v = mods["cfd::multiphase"].getattr("SaturatedFluid")?; mods["cfd"].add("SaturatedFluid", v)?; } + { let v = mods["cfd::advection"].getattr("Scheme")?; mods["cfd"].add("Scheme", v)?; } + { let v = mods["cfd::level_set"].getattr("Segment2")?; mods["cfd"].add("Segment2", v)?; } + { let v = mods["cfd::shallow_water"].getattr("ShallowWater2D")?; mods["cfd"].add("ShallowWater2D", v)?; } + { let v = mods["cfd::turbulence"].getattr("SpalartAllmaras")?; mods["cfd"].add("SpalartAllmaras", v)?; } + { let v = mods["cfd::sph"].getattr("SpatialHash")?; mods["cfd"].add("SpatialHash", v)?; } + { let v = mods["cfd::sph"].getattr("Sph")?; mods["cfd"].add("Sph", v)?; } + { let v = mods["cfd::sph"].getattr("SphParticle")?; mods["cfd"].add("SphParticle", v)?; } + { let v = mods["cfd::sph"].getattr("SphScheme")?; mods["cfd"].add("SphScheme", v)?; } + { let v = mods["cfd::stable_fluids"].getattr("StableFluid2")?; mods["cfd"].add("StableFluid2", v)?; } + { let v = mods["cfd::stable_fluids"].getattr("StableFluid3")?; mods["cfd"].add("StableFluid3", v)?; } + { let v = mods["cfd::porous"].getattr("VanGenuchten")?; mods["cfd"].add("VanGenuchten", v)?; } + { let v = mods["cfd::level_set"].getattr("Vof2")?; mods["cfd"].add("Vof2", v)?; } + { let v = mods["cfd::vortex"].getattr("VortexKernel")?; mods["cfd"].add("VortexKernel", v)?; } + { let v = mods["cfd::vortex"].getattr("VortexMethod2")?; mods["cfd"].add("VortexMethod2", v)?; } + { let v = mods["cfd::vortex"].getattr("VortexMethod3")?; mods["cfd"].add("VortexMethod3", v)?; } + { let v = mods["cfd::vortex"].getattr("VortexParticle")?; mods["cfd"].add("VortexParticle", v)?; } + { let v = mods["cfd::level_set"].getattr("WenoOrUpwind")?; mods["cfd"].add("WenoOrUpwind", v)?; } + { let v = mods["cfd::potential_flow"].getattr("WingGeometry")?; mods["cfd"].add("WingGeometry", v)?; } + { let v = mods["cfd::potential_flow"].getattr("added_mass_cylinder")?; mods["cfd"].add("added_mass_cylinder", v)?; } + { let v = mods["cfd::potential_flow"].getattr("added_mass_sphere")?; mods["cfd"].add("added_mass_sphere", v)?; } + { let v = mods["cfd::advection"].getattr("advect_bfecc_2d")?; mods["cfd"].add("advect_bfecc_2d", v)?; } + { let v = mods["cfd::advection"].getattr("advect_flux_limited_2d")?; mods["cfd"].add("advect_flux_limited_2d", v)?; } + { let v = mods["cfd::advection"].getattr("advect_lax_wendroff_1d")?; mods["cfd"].add("advect_lax_wendroff_1d", v)?; } + { let v = mods["cfd::advection"].getattr("advect_maccormack_2d")?; mods["cfd"].add("advect_maccormack_2d", v)?; } + { let v = mods["cfd::advection"].getattr("advect_muscl_1d")?; mods["cfd"].add("advect_muscl_1d", v)?; } + { let v = mods["cfd::advection"].getattr("advect_semi_lagrangian_2d")?; mods["cfd"].add("advect_semi_lagrangian_2d", v)?; } + { let v = mods["cfd::advection"].getattr("advect_upwind_1d")?; mods["cfd"].add("advect_upwind_1d", v)?; } + { let v = mods["cfd::advection"].getattr("advect_upwind_2d")?; mods["cfd"].add("advect_upwind_2d", v)?; } + { let v = mods["cfd::advection"].getattr("advect_velocity_semi_lagrangian")?; mods["cfd"].add("advect_velocity_semi_lagrangian", v)?; } + { let v = mods["cfd::advection"].getattr("advect_weno5_1d")?; mods["cfd"].add("advect_weno5_1d", v)?; } + { let v = mods["cfd::advection"].getattr("advection_diffusion_1d")?; mods["cfd"].add("advection_diffusion_1d", v)?; } + { let v = mods["cfd::porous"].getattr("advection_dispersion_1d")?; mods["cfd"].add("advection_dispersion_1d", v)?; } + { let v = mods["cfd::porous"].getattr("bioclogging_porosity_change")?; mods["cfd"].add("bioclogging_porosity_change", v)?; } + { let v = mods["cfd::vortex"].getattr("biot_savart_ring")?; mods["cfd"].add("biot_savart_ring", v)?; } + { let v = mods["cfd::vortex"].getattr("biot_savart_segment")?; mods["cfd"].add("biot_savart_segment", v)?; } + { let v = mods["cfd::boundary_layer"].getattr("blasius_cf")?; mods["cfd"].add("blasius_cf", v)?; } + { let v = mods["cfd::boundary_layer"].getattr("blasius_drag_plate")?; mods["cfd"].add("blasius_drag_plate", v)?; } + { let v = mods["cfd::boundary_layer"].getattr("blasius_profile")?; mods["cfd"].add("blasius_profile", v)?; } + { let v = mods["cfd::boundary_layer"].getattr("blasius_solve")?; mods["cfd"].add("blasius_solve", v)?; } + { let v = mods["cfd::boundary_layer"].getattr("blasius_thickness")?; mods["cfd"].add("blasius_thickness", v)?; } + { let v = mods["cfd::riemann"].getattr("blast_wave_woodward_colella")?; mods["cfd"].add("blast_wave_woodward_colella", v)?; } + { let v = mods["cfd::multiphase"].getattr("boiling_heat_flux_rohsenow")?; mods["cfd"].add("boiling_heat_flux_rohsenow", v)?; } + { let v = mods["cfd::multiphase"].getattr("breakup_rate_luo_svendsen")?; mods["cfd"].add("breakup_rate_luo_svendsen", v)?; } + { let v = mods["cfd::porous"].getattr("brinkman_velocity_profile")?; mods["cfd"].add("brinkman_velocity_profile", v)?; } + { let v = mods["cfd::porous"].getattr("brooks_corey")?; mods["cfd"].add("brooks_corey", v)?; } + { let v = mods["cfd::multiphase"].getattr("bubble_drag_coefficient")?; mods["cfd"].add("bubble_drag_coefficient", v)?; } + { let v = mods["cfd::multiphase"].getattr("bubble_rise_velocity")?; mods["cfd"].add("bubble_rise_velocity", v)?; } + { let v = mods["cfd::porous"].getattr("buckley_leverett")?; mods["cfd"].add("buckley_leverett", v)?; } + { let v = mods["cfd::advection"].getattr("burgers_exact_cole_hopf")?; mods["cfd"].add("burgers_exact_cole_hopf", v)?; } + { let v = mods["cfd::advection"].getattr("burgers_step")?; mods["cfd"].add("burgers_step", v)?; } + { let v = mods["cfd::vortex"].getattr("burgers_vortex")?; mods["cfd"].add("burgers_vortex", v)?; } + { let v = mods["cfd::porous"].getattr("capillary_pressure_leverett")?; mods["cfd"].add("capillary_pressure_leverett", v)?; } + { let v = mods["cfd::level_set"].getattr("capillary_wave_dispersion")?; mods["cfd"].add("capillary_wave_dispersion", v)?; } + { let v = mods["cfd::porous"].getattr("carman_kozeny_fibers")?; mods["cfd"].add("carman_kozeny_fibers", v)?; } + { let v = mods["cfd::multiphase"].getattr("cavitation_number")?; mods["cfd"].add("cavitation_number", v)?; } + { let v = mods["cfd::turbulence"].getattr("channel_flow_dns_reference")?; mods["cfd"].add("channel_flow_dns_reference", v)?; } + { let v = mods["cfd::multiphase"].getattr("chisholm")?; mods["cfd"].add("chisholm", v)?; } + { let v = mods["cfd::multiphase"].getattr("coalescence_rate_prince_blanch")?; mods["cfd"].add("coalescence_rate_prince_blanch", v)?; } + { let v = mods["cfd::multiphase"].getattr("condensation_nusselt_film")?; mods["cfd"].add("condensation_nusselt_film", v)?; } + { let v = mods["cfd::potential_flow"].getattr("conformal_map_flow")?; mods["cfd"].add("conformal_map_flow", v)?; } + { let v = mods["cfd::riemann"].getattr("cons_to_prim")?; mods["cfd"].add("cons_to_prim", v)?; } + { let v = mods["cfd::level_set"].getattr("contact_angle_young")?; mods["cfd"].add("contact_angle_young", v)?; } + { let v = mods["cfd::boundary_layer"].getattr("couette_flow")?; mods["cfd"].add("couette_flow", v)?; } + { let v = mods["cfd::multiphase"].getattr("critical_heat_flux_zuber")?; mods["cfd"].add("critical_heat_flux_zuber", v)?; } + { let v = mods["cfd::vortex"].getattr("crow_instability_growth")?; mods["cfd"].add("crow_instability_growth", v)?; } + { let v = mods["cfd::potential_flow"].getattr("cylinder_cp_exact")?; mods["cfd"].add("cylinder_cp_exact", v)?; } + { let v = mods["cfd::potential_flow"].getattr("cylinder_flow")?; mods["cfd"].add("cylinder_flow", v)?; } + { let v = mods["cfd::sph"].getattr("dam_break_2d")?; mods["cfd"].add("dam_break_2d", v)?; } + { let v = mods["cfd::sph"].getattr("dam_break_exact_front")?; mods["cfd"].add("dam_break_exact_front", v)?; } + { let v = mods["cfd::porous"].getattr("darcy_flow_rate")?; mods["cfd"].add("darcy_flow_rate", v)?; } + { let v = mods["cfd::porous"].getattr("darcy_velocity")?; mods["cfd"].add("darcy_velocity", v)?; } + { let v = mods["cfd::turbulence"].getattr("decaying_isotropic_turbulence")?; mods["cfd"].add("decaying_isotropic_turbulence", v)?; } + { let v = mods["cfd::turbulence"].getattr("delta_criterion")?; mods["cfd"].add("delta_criterion", v)?; } + { let v = mods["cfd::porous"].getattr("dispersion_coefficient")?; mods["cfd"].add("dispersion_coefficient", v)?; } + { let v = mods["cfd::shallow_water"].getattr("dispersion_deep")?; mods["cfd"].add("dispersion_deep", v)?; } + { let v = mods["cfd::shallow_water"].getattr("dispersion_full")?; mods["cfd"].add("dispersion_full", v)?; } + { let v = mods["cfd::shallow_water"].getattr("dispersion_shallow")?; mods["cfd"].add("dispersion_shallow", v)?; } + { let v = mods["cfd::turbulence"].getattr("dissipation_rate_from_spectrum")?; mods["cfd"].add("dissipation_rate_from_spectrum", v)?; } + { let v = mods["cfd::multiphase"].getattr("drift_flux_velocity")?; mods["cfd"].add("drift_flux_velocity", v)?; } + { let v = mods["cfd::sph"].getattr("droplet_oscillation")?; mods["cfd"].add("droplet_oscillation", v)?; } + { let v = mods["cfd::level_set"].getattr("droplet_shape_pendant")?; mods["cfd"].add("droplet_shape_pendant", v)?; } + { let v = mods["cfd::multiphase"].getattr("droplet_terminal_velocity")?; mods["cfd"].add("droplet_terminal_velocity", v)?; } + { let v = mods["cfd::porous"].getattr("dupuit_unconfined")?; mods["cfd"].add("dupuit_unconfined", v)?; } + { let v = mods["cfd::turbulence"].getattr("dynamic_smagorinsky_cs")?; mods["cfd"].add("dynamic_smagorinsky_cs", v)?; } + { let v = mods["cfd::porous"].getattr("effective_thermal_conductivity_porous")?; mods["cfd"].add("effective_thermal_conductivity_porous", v)?; } + { let v = mods["cfd::boundary_layer"].getattr("ekman_depth")?; mods["cfd"].add("ekman_depth", v)?; } + { let v = mods["cfd::boundary_layer"].getattr("ekman_spiral")?; mods["cfd"].add("ekman_spiral", v)?; } + { let v = mods["cfd::potential_flow"].getattr("elliptic_wing_cl")?; mods["cfd"].add("elliptic_wing_cl", v)?; } + { let v = mods["cfd::turbulence"].getattr("energy_spectrum_1d")?; mods["cfd"].add("energy_spectrum_1d", v)?; } + { let v = mods["cfd::turbulence"].getattr("energy_spectrum_2d")?; mods["cfd"].add("energy_spectrum_2d", v)?; } + { let v = mods["cfd::turbulence"].getattr("energy_spectrum_3d")?; mods["cfd"].add("energy_spectrum_3d", v)?; } + { let v = mods["cfd::multiphase"].getattr("eotvos")?; mods["cfd"].add("eotvos", v)?; } + { let v = mods["cfd::porous"].getattr("ergun_pressure_drop")?; mods["cfd"].add("ergun_pressure_drop", v)?; } + { let v = mods["cfd::multiphase"].getattr("evaporation_rate_hertz_knudsen")?; mods["cfd"].add("evaporation_rate_hertz_knudsen", v)?; } + { let v = mods["cfd::boundary_layer"].getattr("falkner_skan_separation_beta")?; mods["cfd"].add("falkner_skan_separation_beta", v)?; } + { let v = mods["cfd::boundary_layer"].getattr("falkner_skan_solve")?; mods["cfd"].add("falkner_skan_solve", v)?; } + { let v = mods["cfd::boundary_layer"].getattr("first_cell_height")?; mods["cfd"].add("first_cell_height", v)?; } + { let v = mods["cfd::boundary_layer"].getattr("flat_plate_heat_transfer_laminar")?; mods["cfd"].add("flat_plate_heat_transfer_laminar", v)?; } + { let v = mods["cfd::stable_fluids"].getattr("flow_past_cylinder")?; mods["cfd"].add("flow_past_cylinder", v)?; } + { let v = mods["cfd::multiphase"].getattr("flow_pattern_taitel_dukler")?; mods["cfd"].add("flow_pattern_taitel_dukler", v)?; } + { let v = mods["cfd::multiphase"].getattr("fluidization_minimum_velocity")?; mods["cfd"].add("fluidization_minimum_velocity", v)?; } + { let v = mods["cfd::riemann"].getattr("flux")?; mods["cfd"].add("flux", v)?; } + { let v = mods["cfd::riemann"].getattr("flux_ausm_plus")?; mods["cfd"].add("flux_ausm_plus", v)?; } + { let v = mods["cfd::riemann"].getattr("flux_hll")?; mods["cfd"].add("flux_hll", v)?; } + { let v = mods["cfd::riemann"].getattr("flux_hllc")?; mods["cfd"].add("flux_hllc", v)?; } + { let v = mods["cfd::riemann"].getattr("flux_roe")?; mods["cfd"].add("flux_roe", v)?; } + { let v = mods["cfd::riemann"].getattr("flux_rusanov")?; mods["cfd"].add("flux_rusanov", v)?; } + { let v = mods["cfd::porous"].getattr("forchheimer")?; mods["cfd"].add("forchheimer", v)?; } + { let v = mods["cfd::multiphase"].getattr("friedel_correlation")?; mods["cfd"].add("friedel_correlation", v)?; } + { let v = mods["cfd::shallow_water"].getattr("gerstner_wave")?; mods["cfd"].add("gerstner_wave", v)?; } + { let v = mods["cfd::potential_flow"].getattr("ground_effect_factor")?; mods["cfd"].add("ground_effect_factor", v)?; } + { let v = mods["cfd::porous"].getattr("groundwater_flow_2d")?; mods["cfd"].add("groundwater_flow_2d", v)?; } + { let v = mods["cfd::boundary_layer"].getattr("head_entrainment_method")?; mods["cfd"].add("head_entrainment_method", v)?; } + { let v = mods["cfd::vortex"].getattr("helicity_density")?; mods["cfd"].add("helicity_density", v)?; } + { let v = mods["cfd::vortex"].getattr("hill_spherical_vortex")?; mods["cfd"].add("hill_spherical_vortex", v)?; } + { let v = mods["cfd::multiphase"].getattr("hindered_settling_exponent")?; mods["cfd"].add("hindered_settling_exponent", v)?; } + { let v = mods["cfd::porous"].getattr("hydraulic_conductivity")?; mods["cfd"].add("hydraulic_conductivity", v)?; } + { let v = mods["cfd::sph"].getattr("hydrostatic_tank")?; mods["cfd"].add("hydrostatic_tank", v)?; } + { let v = mods["cfd::potential_flow"].getattr("induced_drag")?; mods["cfd"].add("induced_drag", v)?; } + { let v = mods["cfd::turbulence"].getattr("inertial_range_exponent")?; mods["cfd"].add("inertial_range_exponent", v)?; } + { let v = mods["cfd::turbulence"].getattr("integral_scale")?; mods["cfd"].add("integral_scale", v)?; } + { let v = mods["cfd::potential_flow"].getattr("inverse_joukowski")?; mods["cfd"].add("inverse_joukowski", v)?; } + { let v = mods["cfd::riemann"].getattr("isentropic_vortex_exact")?; mods["cfd"].add("isentropic_vortex_exact", v)?; } + { let v = mods["cfd::shallow_water"].getattr("jonswap_spectrum")?; mods["cfd"].add("jonswap_spectrum", v)?; } + { let v = mods["cfd::potential_flow"].getattr("joukowski_airfoil")?; mods["cfd"].add("joukowski_airfoil", v)?; } + { let v = mods["cfd::potential_flow"].getattr("joukowski_airfoil_flow")?; mods["cfd"].add("joukowski_airfoil_flow", v)?; } + { let v = mods["cfd::potential_flow"].getattr("joukowski_transform")?; mods["cfd"].add("joukowski_transform", v)?; } + { let v = mods["cfd::potential_flow"].getattr("karman_trefftz_airfoil")?; mods["cfd"].add("karman_trefftz_airfoil", v)?; } + { let v = mods["cfd::vortex"].getattr("kelvin_helmholtz_growth_exact")?; mods["cfd"].add("kelvin_helmholtz_growth_exact", v)?; } + { let v = mods["cfd::shallow_water"].getattr("kelvin_wake_angle")?; mods["cfd"].add("kelvin_wake_angle", v)?; } + { let v = mods["cfd::sph"].getattr("kernel_grad")?; mods["cfd"].add("kernel_grad", v)?; } + { let v = mods["cfd::sph"].getattr("kernel_laplacian")?; mods["cfd"].add("kernel_laplacian", v)?; } + { let v = mods["cfd::sph"].getattr("kernel_support")?; mods["cfd"].add("kernel_support", v)?; } + { let v = mods["cfd::sph"].getattr("kernel_w")?; mods["cfd"].add("kernel_w", v)?; } + { let v = mods["cfd::turbulence"].getattr("kolmogorov_scales")?; mods["cfd"].add("kolmogorov_scales", v)?; } + { let v = mods["cfd::turbulence"].getattr("kolmogorov_spectrum")?; mods["cfd"].add("kolmogorov_spectrum", v)?; } + { let v = mods["cfd::vortex"].getattr("lamb_oseen_velocity")?; mods["cfd"].add("lamb_oseen_velocity", v)?; } + { let v = mods["cfd::turbulence"].getattr("lambda2_criterion")?; mods["cfd"].add("lambda2_criterion", v)?; } + { let v = mods["cfd::boundary_layer"].getattr("law_of_the_wall")?; mods["cfd"].add("law_of_the_wall", v)?; } + { let v = mods["cfd::riemann"].getattr("lax_problem")?; mods["cfd"].add("lax_problem", v)?; } + { let v = mods["cfd::lbm"].getattr("lbm_cavity_step")?; mods["cfd"].add("lbm_cavity_step", v)?; } + { let v = mods["cfd::lbm"].getattr("lbm_cylinder")?; mods["cfd"].add("lbm_cylinder", v)?; } + { let v = mods["cfd::lbm"].getattr("lbm_lid_cavity")?; mods["cfd"].add("lbm_lid_cavity", v)?; } + { let v = mods["cfd::lbm"].getattr("lbm_poiseuille_2d")?; mods["cfd"].add("lbm_poiseuille_2d", v)?; } + { let v = mods["cfd::lbm"].getattr("lbm_thermal")?; mods["cfd"].add("lbm_thermal", v)?; } + { let v = mods["cfd::lbm"].getattr("lbm_to_physical")?; mods["cfd"].add("lbm_to_physical", v)?; } + { let v = mods["cfd::stable_fluids"].getattr("lid_driven_cavity")?; mods["cfd"].add("lid_driven_cavity", v)?; } + { let v = mods["cfd::potential_flow"].getattr("lifting_line")?; mods["cfd"].add("lifting_line", v)?; } + { let v = mods["cfd::turbulence"].getattr("log_law_fit")?; mods["cfd"].add("log_law_fit", v)?; } + { let v = mods["cfd::multiphase"].getattr("martinelli_parameter")?; mods["cfd"].add("martinelli_parameter", v)?; } + { let v = mods["cfd::potential_flow"].getattr("method_of_images_wall")?; mods["cfd"].add("method_of_images_wall", v)?; } + { let v = mods["cfd::boundary_layer"].getattr("michel_transition_criterion")?; mods["cfd"].add("michel_transition_criterion", v)?; } + { let v = mods["cfd::level_set"].getattr("minnaert_frequency")?; mods["cfd"].add("minnaert_frequency", v)?; } + { let v = mods["cfd::multiphase"].getattr("mixture_density")?; mods["cfd"].add("mixture_density", v)?; } + { let v = mods["cfd::multiphase"].getattr("mixture_viscosity_dukler")?; mods["cfd"].add("mixture_viscosity_dukler", v)?; } + { let v = mods["cfd::multiphase"].getattr("mixture_viscosity_mcadams")?; mods["cfd"].add("mixture_viscosity_mcadams", v)?; } + { let v = mods["cfd::multiphase"].getattr("morton_number")?; mods["cfd"].add("morton_number", v)?; } + { let v = mods["cfd::stable_fluids"].getattr("multigrid_vcycle")?; mods["cfd"].add("multigrid_vcycle", v)?; } + { let v = mods["cfd::potential_flow"].getattr("naca4")?; mods["cfd"].add("naca4", v)?; } + { let v = mods["cfd::potential_flow"].getattr("naca5")?; mods["cfd"].add("naca5", v)?; } + { let v = mods["cfd::riemann"].getattr("normal_shock_relations")?; mods["cfd"].add("normal_shock_relations", v)?; } + { let v = mods["cfd::riemann"].getattr("nozzle_area_ratio")?; mods["cfd"].add("nozzle_area_ratio", v)?; } + { let v = mods["cfd::riemann"].getattr("nozzle_mach_from_area")?; mods["cfd"].add("nozzle_mach_from_area", v)?; } + { let v = mods["cfd::riemann"].getattr("oblique_shock_angle")?; mods["cfd"].add("oblique_shock_angle", v)?; } + { let v = mods["cfd::porous"].getattr("ogata_banks")?; mods["cfd"].add("ogata_banks", v)?; } + { let v = mods["cfd::level_set"].getattr("ohnesorge")?; mods["cfd"].add("ohnesorge", v)?; } + { let v = mods["cfd::potential_flow"].getattr("oswald_efficiency_estimate")?; mods["cfd"].add("oswald_efficiency_estimate", v)?; } + { let v = mods["cfd::turbulence"].getattr("pao_spectrum")?; mods["cfd"].add("pao_spectrum", v)?; } + { let v = mods["cfd::multiphase"].getattr("particle_response_time")?; mods["cfd"].add("particle_response_time", v)?; } + { let v = mods["cfd::advection"].getattr("peclet_cell")?; mods["cfd"].add("peclet_cell", v)?; } + { let v = mods["cfd::porous"].getattr("peclet_porous")?; mods["cfd"].add("peclet_porous", v)?; } + { let v = mods["cfd::porous"].getattr("permeability_kozeny_carman")?; mods["cfd"].add("permeability_kozeny_carman", v)?; } + { let v = mods["cfd::shallow_water"].getattr("pierson_moskowitz")?; mods["cfd"].add("pierson_moskowitz", v)?; } + { let v = mods["cfd::boundary_layer"].getattr("pohlhausen_profile")?; mods["cfd"].add("pohlhausen_profile", v)?; } + { let v = mods["cfd::vortex"].getattr("point_vortex_hamiltonian")?; mods["cfd"].add("point_vortex_hamiltonian", v)?; } + { let v = mods["cfd::vortex"].getattr("point_vortex_step")?; mods["cfd"].add("point_vortex_step", v)?; } + { let v = mods["cfd::lbm"].getattr("poiseuille_exact")?; mods["cfd"].add("poiseuille_exact", v)?; } + { let v = mods["cfd::sph"].getattr("poiseuille_sph")?; mods["cfd"].add("poiseuille_sph", v)?; } + { let v = mods["cfd::multiphase"].getattr("population_balance_1d")?; mods["cfd"].add("population_balance_1d", v)?; } + { let v = mods["cfd::riemann"].getattr("prandtl_meyer")?; mods["cfd"].add("prandtl_meyer", v)?; } + { let v = mods["cfd::stable_fluids"].getattr("pressure_poisson_cg")?; mods["cfd"].add("pressure_poisson_cg", v)?; } + { let v = mods["cfd::riemann"].getattr("prim_to_cons")?; mods["cfd"].add("prim_to_cons", v)?; } + { let v = mods["cfd::turbulence"].getattr("q_criterion")?; mods["cfd"].add("q_criterion", v)?; } + { let v = mods["cfd::riemann"].getattr("quasi_1d_nozzle")?; mods["cfd"].add("quasi_1d_nozzle", v)?; } + { let v = mods["cfd::riemann"].getattr("rankine_hugoniot")?; mods["cfd"].add("rankine_hugoniot", v)?; } + { let v = mods["cfd::potential_flow"].getattr("rankine_oval")?; mods["cfd"].add("rankine_oval", v)?; } + { let v = mods["cfd::vortex"].getattr("rankine_vortex")?; mods["cfd"].add("rankine_vortex", v)?; } + { let v = mods["cfd::stable_fluids"].getattr("rayleigh_benard")?; mods["cfd"].add("rayleigh_benard", v)?; } + { let v = mods["cfd::level_set"].getattr("rayleigh_plesset")?; mods["cfd"].add("rayleigh_plesset", v)?; } + { let v = mods["cfd::turbulence"].getattr("re_lambda")?; mods["cfd"].add("re_lambda", v)?; } + { let v = mods["cfd::porous"].getattr("relative_permeability_corey")?; mods["cfd"].add("relative_permeability_corey", v)?; } + { let v = mods["cfd::turbulence"].getattr("reynolds_stress")?; mods["cfd"].add("reynolds_stress", v)?; } + { let v = mods["cfd::porous"].getattr("richards_equation_1d")?; mods["cfd"].add("richards_equation_1d", v)?; } + { let v = mods["cfd::turbulence"].getattr("richardson_cascade_time")?; mods["cfd"].add("richardson_cascade_time", v)?; } + { let v = mods["cfd::riemann"].getattr("riemann_exact")?; mods["cfd"].add("riemann_exact", v)?; } + { let v = mods["cfd::riemann"].getattr("riemann_exact_star")?; mods["cfd"].add("riemann_exact_star", v)?; } + { let v = mods["cfd::advection"].getattr("rk3_ssp")?; mods["cfd"].add("rk3_ssp", v)?; } + { let v = mods["cfd::multiphase"].getattr("rosin_rammler")?; mods["cfd"].add("rosin_rammler", v)?; } + { let v = mods["cfd::turbulence"].getattr("rotation_tensor")?; mods["cfd"].add("rotation_tensor", v)?; } + { let v = mods["cfd::multiphase"].getattr("sauter_mean_diameter")?; mods["cfd"].add("sauter_mean_diameter", v)?; } + { let v = mods["cfd::multiphase"].getattr("sedimentation_richardson_zaki")?; mods["cfd"].add("sedimentation_richardson_zaki", v)?; } + { let v = mods["cfd::riemann"].getattr("sedov_1d")?; mods["cfd"].add("sedov_1d", v)?; } + { let v = mods["cfd::multiphase"].getattr("settling_velocity")?; mods["cfd"].add("settling_velocity", v)?; } + { let v = mods["cfd::riemann"].getattr("shu_osher")?; mods["cfd"].add("shu_osher", v)?; } + { let v = mods["cfd::level_set"].getattr("single_vortex_deformation_test")?; mods["cfd"].add("single_vortex_deformation_test", v)?; } + { let v = mods["cfd::turbulence"].getattr("smagorinsky_nu_t")?; mods["cfd"].add("smagorinsky_nu_t", v)?; } + { let v = mods["cfd::riemann"].getattr("sod_exact")?; mods["cfd"].add("sod_exact", v)?; } + { let v = mods["cfd::riemann"].getattr("sod_shock_tube")?; mods["cfd"].add("sod_shock_tube", v)?; } + { let v = mods["cfd::riemann"].getattr("sound_speed")?; mods["cfd"].add("sound_speed", v)?; } + { let v = mods["cfd::boundary_layer"].getattr("spalding")?; mods["cfd"].add("spalding", v)?; } + { let v = mods["cfd::multiphase"].getattr("spray_penetration_hiroyasu")?; mods["cfd"].add("spray_penetration_hiroyasu", v)?; } + { let v = mods["cfd::shallow_water"].getattr("stokes_drift")?; mods["cfd"].add("stokes_drift", v)?; } + { let v = mods["cfd::boundary_layer"].getattr("stokes_first_problem")?; mods["cfd"].add("stokes_first_problem", v)?; } + { let v = mods["cfd::multiphase"].getattr("stokes_number")?; mods["cfd"].add("stokes_number", v)?; } + { let v = mods["cfd::boundary_layer"].getattr("stokes_second_problem")?; mods["cfd"].add("stokes_second_problem", v)?; } + { let v = mods["cfd::turbulence"].getattr("strain_tensor")?; mods["cfd"].add("strain_tensor", v)?; } + { let v = mods["cfd::boundary_layer"].getattr("stratford_separation_criterion")?; mods["cfd"].add("stratford_separation_criterion", v)?; } + { let v = mods["cfd::vortex"].getattr("strouhal_from_re")?; mods["cfd"].add("strouhal_from_re", v)?; } + { let v = mods["cfd::turbulence"].getattr("structure_function")?; mods["cfd"].add("structure_function", v)?; } + { let v = mods["cfd::shallow_water"].getattr("swe_1d_exact_dam_break")?; mods["cfd"].add("swe_1d_exact_dam_break", v)?; } + { let v = mods["cfd::shallow_water"].getattr("swe_1d_step_hll")?; mods["cfd"].add("swe_1d_step_hll", v)?; } + { let v = mods["cfd::turbulence"].getattr("synthetic_eddy_method")?; mods["cfd"].add("synthetic_eddy_method", v)?; } + { let v = mods["cfd::turbulence"].getattr("synthetic_turbulence_kraichnan")?; mods["cfd"].add("synthetic_turbulence_kraichnan", v)?; } + { let v = mods["cfd::level_set"].getattr("taylor_bubble_velocity")?; mods["cfd"].add("taylor_bubble_velocity", v)?; } + { let v = mods["cfd::stable_fluids"].getattr("taylor_green_exact")?; mods["cfd"].add("taylor_green_exact", v)?; } + { let v = mods["cfd::stable_fluids"].getattr("taylor_green_vortex")?; mods["cfd"].add("taylor_green_vortex", v)?; } + { let v = mods["cfd::turbulence"].getattr("taylor_microscale")?; mods["cfd"].add("taylor_microscale", v)?; } + { let v = mods["cfd::porous"].getattr("theis_drawdown")?; mods["cfd"].add("theis_drawdown", v)?; } + { let v = mods["cfd::boundary_layer"].getattr("thermal_bl_ratio")?; mods["cfd"].add("thermal_bl_ratio", v)?; } + { let v = mods["cfd::lbm"].getattr("thermal_step")?; mods["cfd"].add("thermal_step", v)?; } + { let v = mods["cfd::porous"].getattr("thiem_steady")?; mods["cfd"].add("thiem_steady", v)?; } + { let v = mods["cfd::potential_flow"].getattr("thin_airfoil_cl")?; mods["cfd"].add("thin_airfoil_cl", v)?; } + { let v = mods["cfd::potential_flow"].getattr("thin_airfoil_cl_flat")?; mods["cfd"].add("thin_airfoil_cl_flat", v)?; } + { let v = mods["cfd::boundary_layer"].getattr("thwaites_method")?; mods["cfd"].add("thwaites_method", v)?; } + { let v = mods["cfd::boundary_layer"].getattr("thwaites_separation_point")?; mods["cfd"].add("thwaites_separation_point", v)?; } + { let v = mods["cfd::vortex"].getattr("tip_vortex_decay")?; mods["cfd"].add("tip_vortex_decay", v)?; } + { let v = mods["cfd::advection"].getattr("total_variation")?; mods["cfd"].add("total_variation", v)?; } + { let v = mods["cfd::boundary_layer"].getattr("transition_re_x_estimate")?; mods["cfd"].add("transition_re_x_estimate", v)?; } + { let v = mods["cfd::shallow_water"].getattr("tsunami_runup_1d")?; mods["cfd"].add("tsunami_runup_1d", v)?; } + { let v = mods["cfd::turbulence"].getattr("turbulence_intensity")?; mods["cfd"].add("turbulence_intensity", v)?; } + { let v = mods["cfd::boundary_layer"].getattr("turbulent_bl_power_law")?; mods["cfd"].add("turbulent_bl_power_law", v)?; } + { let v = mods["cfd::boundary_layer"].getattr("turbulent_cf_prandtl")?; mods["cfd"].add("turbulent_cf_prandtl", v)?; } + { let v = mods["cfd::boundary_layer"].getattr("turbulent_cf_schlichting")?; mods["cfd"].add("turbulent_cf_schlichting", v)?; } + { let v = mods["cfd::turbulence"].getattr("turbulent_diffusivity")?; mods["cfd"].add("turbulent_diffusivity", v)?; } + { let v = mods["cfd::boundary_layer"].getattr("turbulent_thickness_1_7")?; mods["cfd"].add("turbulent_thickness_1_7", v)?; } + { let v = mods["cfd::multiphase"].getattr("two_phase_pressure_drop_lockhart_martinelli")?; mods["cfd"].add("two_phase_pressure_drop_lockhart_martinelli", v)?; } + { let v = mods["cfd::turbulence"].getattr("two_point_correlation")?; mods["cfd"].add("two_point_correlation", v)?; } + { let v = mods["cfd::boundary_layer"].getattr("u_tau")?; mods["cfd"].add("u_tau", v)?; } + { let v = mods["cfd::boundary_layer"].getattr("van_driest_damping")?; mods["cfd"].add("van_driest_damping", v)?; } + { let v = mods["cfd::multiphase"].getattr("void_fraction_drift_flux")?; mods["cfd"].add("void_fraction_drift_flux", v)?; } + { let v = mods["cfd::multiphase"].getattr("void_fraction_homogeneous")?; mods["cfd"].add("void_fraction_homogeneous", v)?; } + { let v = mods["cfd::multiphase"].getattr("void_fraction_lockhart_martinelli")?; mods["cfd"].add("void_fraction_lockhart_martinelli", v)?; } + { let v = mods["cfd::turbulence"].getattr("von_karman_spectrum")?; mods["cfd"].add("von_karman_spectrum", v)?; } + { let v = mods["cfd::turbulence"].getattr("vortex_identify_q")?; mods["cfd"].add("vortex_identify_q", v)?; } + { let v = mods["cfd::potential_flow"].getattr("vortex_lattice")?; mods["cfd"].add("vortex_lattice", v)?; } + { let v = mods["cfd::vortex"].getattr("vortex_line_trace")?; mods["cfd"].add("vortex_line_trace", v)?; } + { let v = mods["cfd::vortex"].getattr("vortex_pair_velocity")?; mods["cfd"].add("vortex_pair_velocity", v)?; } + { let v = mods["cfd::vortex"].getattr("vortex_ring_self_velocity")?; mods["cfd"].add("vortex_ring_self_velocity", v)?; } + { let v = mods["cfd::vortex"].getattr("vortex_shedding_frequency")?; mods["cfd"].add("vortex_shedding_frequency", v)?; } + { let v = mods["cfd::turbulence"].getattr("vreman_nu_t")?; mods["cfd"].add("vreman_nu_t", v)?; } + { let v = mods["cfd::turbulence"].getattr("wale_nu_t")?; mods["cfd"].add("wale_nu_t", v)?; } + { let v = mods["cfd::shallow_water"].getattr("wave_breaking_criterion")?; mods["cfd"].add("wave_breaking_criterion", v)?; } + { let v = mods["cfd::shallow_water"].getattr("wave_field_from_spectrum")?; mods["cfd"].add("wave_field_from_spectrum", v)?; } + { let v = mods["cfd::shallow_water"].getattr("wave_speed_shallow")?; mods["cfd"].add("wave_speed_shallow", v)?; } + { let v = mods["cfd::riemann"].getattr("wave_speeds_einfeldt")?; mods["cfd"].add("wave_speeds_einfeldt", v)?; } + { let v = mods["cfd::level_set"].getattr("weber_breakup_regime")?; mods["cfd"].add("weber_breakup_regime", v)?; } + { let v = mods["cfd::advection"].getattr("weno5_reconstruct")?; mods["cfd"].add("weno5_reconstruct", v)?; } + { let v = mods["cfd::boundary_layer"].getattr("y_plus")?; mods["cfd"].add("y_plus", v)?; } + { let v = mods["cfd::level_set"].getattr("young_laplace_pressure")?; mods["cfd"].add("young_laplace_pressure", v)?; } + { let v = mods["cfd::level_set"].getattr("zalesak_disk")?; mods["cfd"].add("zalesak_disk", v)?; } + { let v = mods["cfd::level_set"].getattr("zalesak_rotate")?; mods["cfd"].add("zalesak_rotate", v)?; } + { let v = mods["control_systems::kalman"].getattr("ExtendedKalmanFilter")?; mods["control_systems"].add("ExtendedKalmanFilter", v)?; } + { let v = mods["control_systems::kalman"].getattr("KalmanFilter")?; mods["control_systems"].add("KalmanFilter", v)?; } + { let v = mods["dsp::iir"].getattr("Biquad")?; mods["dsp"].add("Biquad", v)?; } + { let v = mods["dsp::fir"].getattr("FirState")?; mods["dsp"].add("FirState", v)?; } + { let v = mods["dsp::iir"].getattr("IirKind")?; mods["dsp"].add("IirKind", v)?; } + { let v = mods["dsp::iir"].getattr("Sos")?; mods["dsp"].add("Sos", v)?; } + { let v = mods["dsp::iir"].getattr("Svf")?; mods["dsp"].add("Svf", v)?; } + { let v = mods["dsp::windows"].getattr("WindowKind")?; mods["dsp"].add("WindowKind", v)?; } + { let v = mods["dsp::windows"].getattr("WindowMetrics")?; mods["dsp"].add("WindowMetrics", v)?; } + { let v = mods["dsp::iir"].getattr("a_weighting_filter")?; mods["dsp"].add("a_weighting_filter", v)?; } + { let v = mods["dsp::iir"].getattr("bessel")?; mods["dsp"].add("bessel", v)?; } + { let v = mods["dsp::iir"].getattr("bilinear_transform")?; mods["dsp"].add("bilinear_transform", v)?; } + { let v = mods["dsp::windows"].getattr("blackman_window")?; mods["dsp"].add("blackman_window", v)?; } + { let v = mods["dsp::iir"].getattr("butterworth")?; mods["dsp"].add("butterworth", v)?; } + { let v = mods["dsp::iir"].getattr("butterworth_order")?; mods["dsp"].add("butterworth_order", v)?; } + { let v = mods["dsp::iir"].getattr("c_weighting_filter")?; mods["dsp"].add("c_weighting_filter", v)?; } + { let v = mods["dsp::iir"].getattr("chebyshev1")?; mods["dsp"].add("chebyshev1", v)?; } + { let v = mods["dsp::iir"].getattr("chebyshev2")?; mods["dsp"].add("chebyshev2", v)?; } + { let v = mods["dsp::resample"].getattr("cic_decimate")?; mods["dsp"].add("cic_decimate", v)?; } + { let v = mods["dsp::iir"].getattr("dc_blocker")?; mods["dsp"].add("dc_blocker", v)?; } + { let v = mods["dsp::resample"].getattr("decimate")?; mods["dsp"].add("decimate", v)?; } + { let v = mods["dsp::iir"].getattr("elliptic")?; mods["dsp"].add("elliptic", v)?; } + { let v = mods["dsp::iir"].getattr("filtfilt")?; mods["dsp"].add("filtfilt", v)?; } + { let v = mods["dsp::fir"].getattr("filtfilt_fir")?; mods["dsp"].add("filtfilt_fir", v)?; } + { let v = mods["dsp::fir"].getattr("fir_apply")?; mods["dsp"].add("fir_apply", v)?; } + { let v = mods["dsp::fir"].getattr("fir_apply_fft")?; mods["dsp"].add("fir_apply_fft", v)?; } + { let v = mods["dsp::fir"].getattr("fir_bandpass")?; mods["dsp"].add("fir_bandpass", v)?; } + { let v = mods["dsp::fir"].getattr("fir_bandstop")?; mods["dsp"].add("fir_bandstop", v)?; } + { let v = mods["dsp::fir"].getattr("fir_differentiator")?; mods["dsp"].add("fir_differentiator", v)?; } + { let v = mods["dsp::fir"].getattr("fir_freq_response")?; mods["dsp"].add("fir_freq_response", v)?; } + { let v = mods["dsp::fir"].getattr("fir_gaussian")?; mods["dsp"].add("fir_gaussian", v)?; } + { let v = mods["dsp::fir"].getattr("fir_group_delay")?; mods["dsp"].add("fir_group_delay", v)?; } + { let v = mods["dsp::fir"].getattr("fir_highpass")?; mods["dsp"].add("fir_highpass", v)?; } + { let v = mods["dsp::fir"].getattr("fir_hilbert")?; mods["dsp"].add("fir_hilbert", v)?; } + { let v = mods["dsp::fir"].getattr("fir_kaiser_design")?; mods["dsp"].add("fir_kaiser_design", v)?; } + { let v = mods["dsp::fir"].getattr("fir_least_squares")?; mods["dsp"].add("fir_least_squares", v)?; } + { let v = mods["dsp::fir"].getattr("fir_lowpass")?; mods["dsp"].add("fir_lowpass", v)?; } + { let v = mods["dsp::fir"].getattr("fir_parks_mcclellan")?; mods["dsp"].add("fir_parks_mcclellan", v)?; } + { let v = mods["dsp::fir"].getattr("fir_raised_cosine")?; mods["dsp"].add("fir_raised_cosine", v)?; } + { let v = mods["dsp::fir"].getattr("fir_root_raised_cosine")?; mods["dsp"].add("fir_root_raised_cosine", v)?; } + { let v = mods["dsp::fir"].getattr("fir_savitzky_golay")?; mods["dsp"].add("fir_savitzky_golay", v)?; } + { let v = mods["dsp::iir"].getattr("first_order_highpass")?; mods["dsp"].add("first_order_highpass", v)?; } + { let v = mods["dsp::iir"].getattr("first_order_lowpass")?; mods["dsp"].add("first_order_lowpass", v)?; } + { let v = mods["dsp::iir"].getattr("group_delay")?; mods["dsp"].add("group_delay", v)?; } + { let v = mods["dsp::phase"].getattr("group_delay_from_phase")?; mods["dsp"].add("group_delay_from_phase", v)?; } + { let v = mods["dsp::resample"].getattr("half_band_filter")?; mods["dsp"].add("half_band_filter", v)?; } + { let v = mods["dsp::windows"].getattr("hamming_window")?; mods["dsp"].add("hamming_window", v)?; } + { let v = mods["dsp::windows"].getattr("hann_window")?; mods["dsp"].add("hann_window", v)?; } + { let v = mods["dsp::iir"].getattr("iir_apply")?; mods["dsp"].add("iir_apply", v)?; } + { let v = mods["dsp::iir"].getattr("impulse_response")?; mods["dsp"].add("impulse_response", v)?; } + { let v = mods["dsp::windows"].getattr("kaiser_beta_for_attenuation")?; mods["dsp"].add("kaiser_beta_for_attenuation", v)?; } + { let v = mods["dsp::iir"].getattr("one_pole_lowpass")?; mods["dsp"].add("one_pole_lowpass", v)?; } + { let v = mods["dsp::phase"].getattr("phase_difference")?; mods["dsp"].add("phase_difference", v)?; } + { let v = mods["dsp::phase"].getattr("phase_locked_loop")?; mods["dsp"].add("phase_locked_loop", v)?; } + { let v = mods["dsp::phase"].getattr("phase_vs_reference")?; mods["dsp"].add("phase_vs_reference", v)?; } + { let v = mods["dsp::iir"].getattr("rbj_q_from_bandwidth")?; mods["dsp"].add("rbj_q_from_bandwidth", v)?; } + { let v = mods["dsp::windows"].getattr("rectangular_window")?; mods["dsp"].add("rectangular_window", v)?; } + { let v = mods["dsp::resample"].getattr("resample_cubic")?; mods["dsp"].add("resample_cubic", v)?; } + { let v = mods["dsp::resample"].getattr("resample_linear")?; mods["dsp"].add("resample_linear", v)?; } + { let v = mods["dsp::resample"].getattr("resample_rational")?; mods["dsp"].add("resample_rational", v)?; } + { let v = mods["dsp::resample"].getattr("resample_sinc")?; mods["dsp"].add("resample_sinc", v)?; } + { let v = mods["dsp::resample"].getattr("resample_to_rate")?; mods["dsp"].add("resample_to_rate", v)?; } + { let v = mods["dsp::resample"].getattr("sinc_interpolate")?; mods["dsp"].add("sinc_interpolate", v)?; } + { let v = mods["dsp::iir"].getattr("state_variable_filter")?; mods["dsp"].add("state_variable_filter", v)?; } + { let v = mods["dsp::iir"].getattr("step_response")?; mods["dsp"].add("step_response", v)?; } + { let v = mods["dsp::iir"].getattr("tf_to_zpk")?; mods["dsp"].add("tf_to_zpk", v)?; } + { let v = mods["dsp::phase"].getattr("unwrap_phase")?; mods["dsp"].add("unwrap_phase", v)?; } + { let v = mods["dsp::phase"].getattr("unwrap_phase_2d")?; mods["dsp"].add("unwrap_phase_2d", v)?; } + { let v = mods["dsp::resample"].getattr("upsample")?; mods["dsp"].add("upsample", v)?; } + { let v = mods["dsp::windows"].getattr("window")?; mods["dsp"].add("window", v)?; } + { let v = mods["dsp::windows"].getattr("window_metrics")?; mods["dsp"].add("window_metrics", v)?; } + { let v = mods["dsp::phase"].getattr("wrap_phase")?; mods["dsp"].add("wrap_phase", v)?; } + { let v = mods["dsp::phase"].getattr("zero_crossing_times")?; mods["dsp"].add("zero_crossing_times", v)?; } + { let v = mods["dsp::iir"].getattr("zpk_to_sos")?; mods["dsp"].add("zpk_to_sos", v)?; } + { let v = mods["exact::bigfloat"].getattr("BigFloat")?; mods["exact"].add("BigFloat", v)?; } + { let v = mods["exact::symbolic"].getattr("Expr")?; mods["exact"].add("Expr", v)?; } + { let v = mods["geometry::geodesy"].getattr("Ellipsoid")?; mods["geometry"].add("Ellipsoid", v)?; } + { let v = mods["mesh"].getattr("Mesh")?; mods["geometry"].add("Mesh", v)?; } + { let v = mods["geometry::mesh"].getattr("RayHit")?; mods["geometry"].add("RayHit", v)?; } + { let v = mods["geometry::delaunay"].getattr("circumcircle")?; mods["geometry"].add("circumcircle", v)?; } + { let v = mods["geometry::hull"].getattr("convex_hull_2d")?; mods["geometry"].add("convex_hull_2d", v)?; } + { let v = mods["geometry::hull"].getattr("convex_hull_3d")?; mods["geometry"].add("convex_hull_3d", v)?; } + { let v = mods["geometry::delaunay"].getattr("delaunay_2d")?; mods["geometry"].add("delaunay_2d", v)?; } + { let v = mods["geometry::geodesy"].getattr("ecef_to_enu")?; mods["geometry"].add("ecef_to_enu", v)?; } + { let v = mods["geometry::geodesy"].getattr("ecef_to_geodetic")?; mods["geometry"].add("ecef_to_geodetic", v)?; } + { let v = mods["geometry::geodesy"].getattr("geodetic_to_ecef")?; mods["geometry"].add("geodetic_to_ecef", v)?; } + { let v = mods["geometry::hull"].getattr("point_in_polygon")?; mods["geometry"].add("point_in_polygon", v)?; } + { let v = mods["geometry::hull"].getattr("polygon_area_signed")?; mods["geometry"].add("polygon_area_signed", v)?; } + { let v = mods["geometry::geodesy"].getattr("vincenty_direct")?; mods["geometry"].add("vincenty_direct", v)?; } + { let v = mods["geometry::geodesy"].getattr("vincenty_inverse")?; mods["geometry"].add("vincenty_inverse", v)?; } + { let v = mods["geometry::delaunay"].getattr("voronoi_cells_2d")?; mods["geometry"].add("voronoi_cells_2d", v)?; } + { let v = mods["graph::core"].getattr("Graph")?; mods["graph"].add("Graph", v)?; } + { let v = mods["linalg::sparse"].getattr("CsrMatrix")?; mods["linalg"].add("CsrMatrix", v)?; } + { let v = mods["linalg::lu"].getattr("Lu")?; mods["linalg"].add("Lu", v)?; } + { let v = mods["linalg::matrix"].getattr("Matrix")?; mods["linalg"].add("Matrix", v)?; } + { let v = mods["linalg::qr"].getattr("Qr")?; mods["linalg"].add("Qr", v)?; } + { let v = mods["linalg::svd"].getattr("Svd")?; mods["linalg"].add("Svd", v)?; } + { let v = mods["linalg::eigen"].getattr("SymEigen")?; mods["linalg"].add("SymEigen", v)?; } + { let v = mods["linalg::cholesky"].getattr("cholesky_solve")?; mods["linalg"].add("cholesky_solve", v)?; } + { let v = mods["linalg::sparse"].getattr("conjugate_gradient")?; mods["linalg"].add("conjugate_gradient", v)?; } + { let v = mods["linalg::eigen"].getattr("eigen_symmetric")?; mods["linalg"].add("eigen_symmetric", v)?; } + { let v = mods["linalg::tridiagonal"].getattr("eigen_symmetric_tridiagonal")?; mods["linalg"].add("eigen_symmetric_tridiagonal", v)?; } + { let v = mods["linalg::eigen"].getattr("eigenvalues_general")?; mods["linalg"].add("eigenvalues_general", v)?; } + { let v = mods["linalg::svd"].getattr("kabsch")?; mods["linalg"].add("kabsch", v)?; } + { let v = mods["linalg::qr"].getattr("least_squares")?; mods["linalg"].add("least_squares", v)?; } + { let v = mods["linalg::lu"].getattr("lu_decompose")?; mods["linalg"].add("lu_decompose", v)?; } + { let v = mods["linalg::sparse"].getattr("pcg_jacobi")?; mods["linalg"].add("pcg_jacobi", v)?; } + { let v = mods["linalg::svd"].getattr("pseudoinverse")?; mods["linalg"].add("pseudoinverse", v)?; } + { let v = mods["linalg::qr"].getattr("qr_householder")?; mods["linalg"].add("qr_householder", v)?; } + { let v = mods["linalg::svd"].getattr("rank")?; mods["linalg"].add("rank", v)?; } + { let v = mods["linalg::lu"].getattr("solve")?; mods["linalg"].add("solve", v)?; } + { let v = mods["linalg::tridiagonal"].getattr("thomas_solve")?; mods["linalg"].add("thomas_solve", v)?; } + { let v = mods["manifold::spacetime"].getattr("Causal")?; mods["manifold"].add("Causal", v)?; } + { let v = mods["manifold::dec"].getattr("DecMesh")?; mods["manifold"].add("DecMesh", v)?; } + { let v = mods["manifold::spacetime"].getattr("FourVector")?; mods["manifold"].add("FourVector", v)?; } + { let v = mods["manifold::geodesic"].getattr("GeodesicState")?; mods["manifold"].add("GeodesicState", v)?; } + { let v = mods["manifold::lie"].getattr("Heisenberg3")?; mods["manifold"].add("Heisenberg3", v)?; } + { let v = mods["manifold::hyperbolic"].getattr("HypModel")?; mods["manifold"].add("HypModel", v)?; } + { let v = mods["manifold::hyperbolic"].getattr("HypPoint")?; mods["manifold"].add("HypPoint", v)?; } + { let v = mods["manifold::geodesic"].getattr("Integrator")?; mods["manifold"].add("Integrator", v)?; } + { let v = mods["manifold::spacetime"].getattr("KerrConstants")?; mods["manifold"].add("KerrConstants", v)?; } + { let v = mods["manifold::spacetime"].getattr("LorentzTransform")?; mods["manifold"].add("LorentzTransform", v)?; } + { let v = mods["manifold::metric"].getattr("Metric")?; mods["manifold"].add("Metric", v)?; } + { let v = mods["manifold::clifford"].getattr("Multivector")?; mods["manifold"].add("Multivector", v)?; } + { let v = mods["manifold::spacetime"].getattr("Plane")?; mods["manifold"].add("Plane", v)?; } + { let v = mods["manifold::polytope4"].getattr("Polytope4")?; mods["manifold"].add("Polytope4", v)?; } + { let v = mods["manifold::lie"].getattr("Se2")?; mods["manifold"].add("Se2", v)?; } + { let v = mods["manifold::lie"].getattr("Se3")?; mods["manifold"].add("Se3", v)?; } + { let v = mods["manifold::metric"].getattr("Sig")?; mods["manifold"].add("Sig", v)?; } + { let v = mods["manifold::lie"].getattr("Sim3")?; mods["manifold"].add("Sim3", v)?; } + { let v = mods["manifold::lie"].getattr("Sl2C")?; mods["manifold"].add("Sl2C", v)?; } + { let v = mods["manifold::lie"].getattr("Sl2Class")?; mods["manifold"].add("Sl2Class", v)?; } + { let v = mods["manifold::lie"].getattr("Sl2R")?; mods["manifold"].add("Sl2R", v)?; } + { let v = mods["manifold::lie"].getattr("So2")?; mods["manifold"].add("So2", v)?; } + { let v = mods["manifold::lie"].getattr("So3")?; mods["manifold"].add("So3", v)?; } + { let v = mods["manifold::lie"].getattr("So4")?; mods["manifold"].add("So4", v)?; } + { let v = mods["manifold::lie"].getattr("Su2")?; mods["manifold"].add("Su2", v)?; } + { let v = mods["manifold::vecn"].getattr("TensorN")?; mods["manifold"].add("TensorN", v)?; } + { let v = mods["manifold::lie"].getattr("Unitary")?; mods["manifold"].add("Unitary", v)?; } + { let v = mods["manifold::polytope4"].getattr("Vec4")?; mods["manifold"].add("Vec4", v)?; } + { let v = mods["manifold::vecn"].getattr("VecN")?; mods["manifold"].add("VecN", v)?; } + { let v = mods["manifold::clifford"].getattr("algebra_dimension")?; mods["manifold"].add("algebra_dimension", v)?; } + { let v = mods["manifold::hyperbolic"].getattr("apollonian_from_mobius")?; mods["manifold"].add("apollonian_from_mobius", v)?; } + { let v = mods["manifold::spherical"].getattr("azimuthal_equidistant")?; mods["manifold"].add("azimuthal_equidistant", v)?; } + { let v = mods["manifold::spacetime"].getattr("bekenstein_entropy")?; mods["manifold"].add("bekenstein_entropy", v)?; } + { let v = mods["manifold::dec"].getattr("betti_curve")?; mods["manifold"].add("betti_curve", v)?; } + { let v = mods["manifold::spacetime"].getattr("black_hole_shadow_radius")?; mods["manifold"].add("black_hole_shadow_radius", v)?; } + { let v = mods["manifold::clifford"].getattr("blade_name")?; mods["manifold"].add("blade_name", v)?; } + { let v = mods["manifold::embedding"].getattr("blobs")?; mods["manifold"].add("blobs", v)?; } + { let v = mods["manifold::lie"].getattr("casimir_so3")?; mods["manifold"].add("casimir_so3", v)?; } + { let v = mods["manifold::clifford"].getattr("cayley_table")?; mods["manifold"].add("cayley_table", v)?; } + { let v = mods["manifold::embedding"].getattr("classical_mds")?; mods["manifold"].add("classical_mds", v)?; } + { let v = mods["manifold::lie"].getattr("clebsch_gordan")?; mods["manifold"].add("clebsch_gordan", v)?; } + { let v = mods["manifold::polytope4"].getattr("clifford_torus")?; mods["manifold"].add("clifford_torus", v)?; } + { let v = mods["manifold::polytope4"].getattr("clifford_torus_mesh")?; mods["manifold"].add("clifford_torus_mesh", v)?; } + { let v = mods["manifold::embedding"].getattr("continuity")?; mods["manifold"].add("continuity", v)?; } + { let v = mods["manifold::spacetime"].getattr("cosmological_distances")?; mods["manifold"].add("cosmological_distances", v)?; } + { let v = mods["manifold::polytope4"].getattr("coxeter_plane_projection")?; mods["manifold"].add("coxeter_plane_projection", v)?; } + { let v = mods["manifold::polytope4"].getattr("cross_polytope_n")?; mods["manifold"].add("cross_polytope_n", v)?; } + { let v = mods["manifold::polytope4"].getattr("d4_lattice_points")?; mods["manifold"].add("d4_lattice_points", v)?; } + { let v = mods["manifold::vecn"].getattr("determinant_n")?; mods["manifold"].add("determinant_n", v)?; } + { let v = mods["manifold::embedding"].getattr("diffusion_maps")?; mods["manifold"].add("diffusion_maps", v)?; } + { let v = mods["manifold::hyperbolic"].getattr("disk_to_hyperboloid")?; mods["manifold"].add("disk_to_hyperboloid", v)?; } + { let v = mods["manifold::hyperbolic"].getattr("disk_to_klein")?; mods["manifold"].add("disk_to_klein", v)?; } + { let v = mods["manifold::hyperbolic"].getattr("disk_to_uhp")?; mods["manifold"].add("disk_to_uhp", v)?; } + { let v = mods["manifold::embedding"].getattr("dist_matrix")?; mods["manifold"].add("dist_matrix", v)?; } + { let v = mods["manifold::polytope4"].getattr("e8_lattice_nearest")?; mods["manifold"].add("e8_lattice_nearest", v)?; } + { let v = mods["manifold::polytope4"].getattr("e8_roots")?; mods["manifold"].add("e8_roots", v)?; } + { let v = mods["manifold::spacetime"].getattr("eddington_finkelstein")?; mods["manifold"].add("eddington_finkelstein", v)?; } + { let v = mods["manifold::hyperbolic"].getattr("equidistant_curve_disk")?; mods["manifold"].add("equidistant_curve_disk", v)?; } + { let v = mods["manifold::spherical"].getattr("equirectangular")?; mods["manifold"].add("equirectangular", v)?; } + { let v = mods["manifold::spacetime"].getattr("evaporation_time")?; mods["manifold"].add("evaporation_time", v)?; } + { let v = mods["manifold::spacetime"].getattr("extra_dimension_gravity_law")?; mods["manifold"].add("extra_dimension_gravity_law", v)?; } + { let v = mods["manifold::polytope4"].getattr("f4_roots")?; mods["manifold"].add("f4_roots", v)?; } + { let v = mods["manifold::hyperbolic"].getattr("fundamental_polygon_genus")?; mods["manifold"].add("fundamental_polygon_genus", v)?; } + { let v = mods["manifold::spherical"].getattr("gauss_legendre_sphere")?; mods["manifold"].add("gauss_legendre_sphere", v)?; } + { let v = mods["manifold::polytope4"].getattr("gaussian_concentration_radius")?; mods["manifold"].add("gaussian_concentration_radius", v)?; } + { let v = mods["manifold::embedding"].getattr("geodesic_distance_matrix")?; mods["manifold"].add("geodesic_distance_matrix", v)?; } + { let v = mods["manifold::embedding"].getattr("geodesic_kmeans")?; mods["manifold"].add("geodesic_kmeans", v)?; } + { let v = mods["manifold::geodesic"].getattr("geodesics_on_mesh_exact")?; mods["manifold"].add("geodesics_on_mesh_exact", v)?; } + { let v = mods["manifold::spherical"].getattr("gnomonic")?; mods["manifold"].add("gnomonic", v)?; } + { let v = mods["manifold::embedding"].getattr("grassmann_distance")?; mods["manifold"].add("grassmann_distance", v)?; } + { let v = mods["manifold::spacetime"].getattr("gravitational_lens_einstein_radius")?; mods["manifold"].add("gravitational_lens_einstein_radius", v)?; } + { let v = mods["manifold::geodesic"].getattr("great_circle_check")?; mods["manifold"].add("great_circle_check", v)?; } + { let v = mods["manifold::spacetime"].getattr("gw_chirp_mass")?; mods["manifold"].add("gw_chirp_mass", v)?; } + { let v = mods["manifold::spacetime"].getattr("gw_waveform_inspiral")?; mods["manifold"].add("gw_waveform_inspiral", v)?; } + { let v = mods["manifold::polytope4"].getattr("h4_roots")?; mods["manifold"].add("h4_roots", v)?; } + { let v = mods["manifold::lie"].getattr("hand_eye_calibration")?; mods["manifold"].add("hand_eye_calibration", v)?; } + { let v = mods["manifold::spherical"].getattr("haversine")?; mods["manifold"].add("haversine", v)?; } + { let v = mods["manifold::spacetime"].getattr("hawking_temperature")?; mods["manifold"].add("hawking_temperature", v)?; } + { let v = mods["manifold::spherical"].getattr("healpix_ang2pix")?; mods["manifold"].add("healpix_ang2pix", v)?; } + { let v = mods["manifold::spherical"].getattr("healpix_npix")?; mods["manifold"].add("healpix_npix", v)?; } + { let v = mods["manifold::spherical"].getattr("healpix_pix2ang")?; mods["manifold"].add("healpix_pix2ang", v)?; } + { let v = mods["manifold::geodesic"].getattr("heat_method_geodesic")?; mods["manifold"].add("heat_method_geodesic", v)?; } + { let v = mods["manifold::embedding"].getattr("helix_sample")?; mods["manifold"].add("helix_sample", v)?; } + { let v = mods["manifold::spherical"].getattr("hopf_fiber")?; mods["manifold"].add("hopf_fiber", v)?; } + { let v = mods["manifold::spherical"].getattr("hopf_fiber_stereographic")?; mods["manifold"].add("hopf_fiber_stereographic", v)?; } + { let v = mods["manifold::spherical"].getattr("hopf_fibration")?; mods["manifold"].add("hopf_fibration", v)?; } + { let v = mods["manifold::hyperbolic"].getattr("horocycle_disk")?; mods["manifold"].add("horocycle_disk", v)?; } + { let v = mods["manifold::hyperbolic"].getattr("hyp_angle_of_parallelism")?; mods["manifold"].add("hyp_angle_of_parallelism", v)?; } + { let v = mods["manifold::hyperbolic"].getattr("hyp_area_circle")?; mods["manifold"].add("hyp_area_circle", v)?; } + { let v = mods["manifold::hyperbolic"].getattr("hyp_area_triangle")?; mods["manifold"].add("hyp_area_triangle", v)?; } + { let v = mods["manifold::hyperbolic"].getattr("hyp_centroid_disk")?; mods["manifold"].add("hyp_centroid_disk", v)?; } + { let v = mods["manifold::hyperbolic"].getattr("hyp_circle_disk")?; mods["manifold"].add("hyp_circle_disk", v)?; } + { let v = mods["manifold::hyperbolic"].getattr("hyp_circumference")?; mods["manifold"].add("hyp_circumference", v)?; } + { let v = mods["manifold::hyperbolic"].getattr("hyp_convex_hull_disk")?; mods["manifold"].add("hyp_convex_hull_disk", v)?; } + { let v = mods["manifold::hyperbolic"].getattr("hyp_delaunay_disk")?; mods["manifold"].add("hyp_delaunay_disk", v)?; } + { let v = mods["manifold::hyperbolic"].getattr("hyp_distance_disk")?; mods["manifold"].add("hyp_distance_disk", v)?; } + { let v = mods["manifold::hyperbolic"].getattr("hyp_distance_hyperboloid")?; mods["manifold"].add("hyp_distance_hyperboloid", v)?; } + { let v = mods["manifold::hyperbolic"].getattr("hyp_distance_uhp")?; mods["manifold"].add("hyp_distance_uhp", v)?; } + { let v = mods["manifold::hyperbolic"].getattr("hyp_embed_graph_mds")?; mods["manifold"].add("hyp_embed_graph_mds", v)?; } + { let v = mods["manifold::hyperbolic"].getattr("hyp_embed_tree")?; mods["manifold"].add("hyp_embed_tree", v)?; } + { let v = mods["manifold::hyperbolic"].getattr("hyp_geodesic_circle_disk")?; mods["manifold"].add("hyp_geodesic_circle_disk", v)?; } + { let v = mods["manifold::hyperbolic"].getattr("hyp_geodesic_disk")?; mods["manifold"].add("hyp_geodesic_disk", v)?; } + { let v = mods["manifold::hyperbolic"].getattr("hyp_law_of_cosines")?; mods["manifold"].add("hyp_law_of_cosines", v)?; } + { let v = mods["manifold::hyperbolic"].getattr("hyp_law_of_sines")?; mods["manifold"].add("hyp_law_of_sines", v)?; } + { let v = mods["manifold::hyperbolic"].getattr("hyp_mean_curvature_flow")?; mods["manifold"].add("hyp_mean_curvature_flow", v)?; } + { let v = mods["manifold::hyperbolic"].getattr("hyp_tiling")?; mods["manifold"].add("hyp_tiling", v)?; } + { let v = mods["manifold::hyperbolic"].getattr("hyp_tiling_exists")?; mods["manifold"].add("hyp_tiling_exists", v)?; } + { let v = mods["manifold::hyperbolic"].getattr("hyp_triangle_from_angles")?; mods["manifold"].add("hyp_triangle_from_angles", v)?; } + { let v = mods["manifold::hyperbolic"].getattr("hyp_volume_ball")?; mods["manifold"].add("hyp_volume_ball", v)?; } + { let v = mods["manifold::hyperbolic"].getattr("hyp_voronoi_disk")?; mods["manifold"].add("hyp_voronoi_disk", v)?; } + { let v = mods["manifold::hyperbolic"].getattr("hyperbolic_rotation")?; mods["manifold"].add("hyperbolic_rotation", v)?; } + { let v = mods["manifold::hyperbolic"].getattr("hyperbolic_translation")?; mods["manifold"].add("hyperbolic_translation", v)?; } + { let v = mods["manifold::hyperbolic"].getattr("hyperboloid_to_disk")?; mods["manifold"].add("hyperboloid_to_disk", v)?; } + { let v = mods["manifold::polytope4"].getattr("hypercube_graph_n")?; mods["manifold"].add("hypercube_graph_n", v)?; } + { let v = mods["manifold::polytope4"].getattr("hypercube_n")?; mods["manifold"].add("hypercube_n", v)?; } + { let v = mods["manifold::polytope4"].getattr("hypercube_slicing_volume")?; mods["manifold"].add("hypercube_slicing_volume", v)?; } + { let v = mods["manifold::polytope4"].getattr("hypersphere_cap_fraction")?; mods["manifold"].add("hypersphere_cap_fraction", v)?; } + { let v = mods["manifold::polytope4"].getattr("hypersphere_s3_points")?; mods["manifold"].add("hypersphere_s3_points", v)?; } + { let v = mods["manifold::polytope4"].getattr("hypersphere_volume")?; mods["manifold"].add("hypersphere_volume", v)?; } + { let v = mods["manifold::embedding"].getattr("intrinsic_dimension_correlation")?; mods["manifold"].add("intrinsic_dimension_correlation", v)?; } + { let v = mods["manifold::embedding"].getattr("intrinsic_dimension_mle")?; mods["manifold"].add("intrinsic_dimension_mle", v)?; } + { let v = mods["manifold::embedding"].getattr("intrinsic_dimension_two_nn")?; mods["manifold"].add("intrinsic_dimension_two_nn", v)?; } + { let v = mods["manifold::spherical"].getattr("inverse_stereographic")?; mods["manifold"].add("inverse_stereographic", v)?; } + { let v = mods["manifold::clifford"].getattr("is_isomorphic_to_known")?; mods["manifold"].add("is_isomorphic_to_known", v)?; } + { let v = mods["manifold::embedding"].getattr("isomap")?; mods["manifold"].add("isomap", v)?; } + { let v = mods["manifold::hyperbolic"].getattr("isometry_disk_from_two_points")?; mods["manifold"].add("isometry_disk_from_two_points", v)?; } + { let v = mods["manifold::spherical"].getattr("kent_distribution_pdf")?; mods["manifold"].add("kent_distribution_pdf", v)?; } + { let v = mods["manifold::embedding"].getattr("kernel_pca")?; mods["manifold"].add("kernel_pca", v)?; } + { let v = mods["manifold::metric"].getattr("kerr_boyer_lindquist")?; mods["manifold"].add("kerr_boyer_lindquist", v)?; } + { let v = mods["manifold::spacetime"].getattr("kerr_geodesic_constants")?; mods["manifold"].add("kerr_geodesic_constants", v)?; } + { let v = mods["manifold::lie"].getattr("killing_form")?; mods["manifold"].add("killing_form", v)?; } + { let v = mods["manifold::polytope4"].getattr("kissing_number_known")?; mods["manifold"].add("kissing_number_known", v)?; } + { let v = mods["manifold::spacetime"].getattr("kk_compactification_mass_spectrum")?; mods["manifold"].add("kk_compactification_mass_spectrum", v)?; } + { let v = mods["manifold::spacetime"].getattr("kk_reduce_geodesic_to_charged")?; mods["manifold"].add("kk_reduce_geodesic_to_charged", v)?; } + { let v = mods["manifold::embedding"].getattr("klein_sample")?; mods["manifold"].add("klein_sample", v)?; } + { let v = mods["manifold::hyperbolic"].getattr("klein_to_disk")?; mods["manifold"].add("klein_to_disk", v)?; } + { let v = mods["manifold::embedding"].getattr("knn_graph")?; mods["manifold"].add("knn_graph", v)?; } + { let v = mods["manifold::spacetime"].getattr("kruskal_from_schwarzschild")?; mods["manifold"].add("kruskal_from_schwarzschild", v)?; } + { let v = mods["manifold::spherical"].getattr("lambert_azimuthal_equal_area")?; mods["manifold"].add("lambert_azimuthal_equal_area", v)?; } + { let v = mods["manifold::embedding"].getattr("laplacian_eigenmaps")?; mods["manifold"].add("laplacian_eigenmaps", v)?; } + { let v = mods["manifold::spherical"].getattr("lebedev_quadrature")?; mods["manifold"].add("lebedev_quadrature", v)?; } + { let v = mods["manifold::polytope4"].getattr("leech_lattice_min_vectors_count")?; mods["manifold"].add("leech_lattice_min_vectors_count", v)?; } + { let v = mods["manifold::spacetime"].getattr("lens_equation_solve")?; mods["manifold"].add("lens_equation_solve", v)?; } + { let v = mods["manifold::lie"].getattr("lie_bracket_matrix")?; mods["manifold"].add("lie_bracket_matrix", v)?; } + { let v = mods["manifold::spacetime"].getattr("light_cone_check")?; mods["manifold"].add("light_cone_check", v)?; } + { let v = mods["manifold::geodesic"].getattr("light_deflection")?; mods["manifold"].add("light_deflection", v)?; } + { let v = mods["manifold::hyperbolic"].getattr("limit_set_schottky")?; mods["manifold"].add("limit_set_schottky", v)?; } + { let v = mods["manifold::embedding"].getattr("lle")?; mods["manifold"].add("lle", v)?; } + { let v = mods["manifold::hyperbolic"].getattr("lorentz_boost_hyperboloid")?; mods["manifold"].add("lorentz_boost_hyperboloid", v)?; } + { let v = mods["manifold::embedding"].getattr("manifold_curvature_estimate")?; mods["manifold"].add("manifold_curvature_estimate", v)?; } + { let v = mods["manifold::embedding"].getattr("manifold_interpolation_rbf")?; mods["manifold"].add("manifold_interpolation_rbf", v)?; } + { let v = mods["manifold::lie"].getattr("matrix_exp")?; mods["manifold"].add("matrix_exp", v)?; } + { let v = mods["manifold::lie"].getattr("matrix_log")?; mods["manifold"].add("matrix_log", v)?; } + { let v = mods["manifold::lie"].getattr("matrix_sqrt")?; mods["manifold"].add("matrix_sqrt", v)?; } + { let v = mods["manifold::spherical"].getattr("mercator")?; mods["manifold"].add("mercator", v)?; } + { let v = mods["manifold::embedding"].getattr("metric_mds_smacof")?; mods["manifold"].add("metric_mds_smacof", v)?; } + { let v = mods["manifold::hyperbolic"].getattr("mobius_disk")?; mods["manifold"].add("mobius_disk", v)?; } + { let v = mods["manifold::embedding"].getattr("mobius_sample")?; mods["manifold"].add("mobius_sample", v)?; } + { let v = mods["manifold::hyperbolic"].getattr("mobius_uhp")?; mods["manifold"].add("mobius_uhp", v)?; } + { let v = mods["manifold::spherical"].getattr("mollweide")?; mods["manifold"].add("mollweide", v)?; } + { let v = mods["manifold::embedding"].getattr("neighborhood_preservation")?; mods["manifold"].add("neighborhood_preservation", v)?; } + { let v = mods["manifold::embedding"].getattr("nonmetric_mds")?; mods["manifold"].add("nonmetric_mds", v)?; } + { let v = mods["manifold::spacetime"].getattr("orbit_schwarzschild_full")?; mods["manifold"].add("orbit_schwarzschild_full", v)?; } + { let v = mods["manifold::spherical"].getattr("orthographic")?; mods["manifold"].add("orthographic", v)?; } + { let v = mods["manifold::hyperbolic"].getattr("parabolic")?; mods["manifold"].add("parabolic", v)?; } + { let v = mods["manifold::embedding"].getattr("pca")?; mods["manifold"].add("pca", v)?; } + { let v = mods["manifold::spacetime"].getattr("penrose_diagram_coords")?; mods["manifold"].add("penrose_diagram_coords", v)?; } + { let v = mods["manifold::geodesic"].getattr("perihelion_precession")?; mods["manifold"].add("perihelion_precession", v)?; } + { let v = mods["manifold::dec"].getattr("persistence_diagram_bottleneck")?; mods["manifold"].add("persistence_diagram_bottleneck", v)?; } + { let v = mods["manifold::dec"].getattr("persistent_homology_vietoris_rips")?; mods["manifold"].add("persistent_homology_vietoris_rips", v)?; } + { let v = mods["manifold::polytope4"].getattr("petrie_polygon_projection")?; mods["manifold"].add("petrie_polygon_projection", v)?; } + { let v = mods["manifold::geodesic"].getattr("photon_orbit_stability")?; mods["manifold"].add("photon_orbit_stability", v)?; } + { let v = mods["manifold::spacetime"].getattr("photon_ray_trace_schwarzschild")?; mods["manifold"].add("photon_ray_trace_schwarzschild", v)?; } + { let v = mods["manifold::hyperbolic"].getattr("poincare_embedding_train")?; mods["manifold"].add("poincare_embedding_train", v)?; } + { let v = mods["manifold::spacetime"].getattr("point_lens_magnification")?; mods["manifold"].add("point_lens_magnification", v)?; } + { let v = mods["manifold::embedding"].getattr("procrustes_align")?; mods["manifold"].add("procrustes_align", v)?; } + { let v = mods["manifold::polytope4"].getattr("project_n_to_2")?; mods["manifold"].add("project_n_to_2", v)?; } + { let v = mods["manifold::polytope4"].getattr("project_n_to_3")?; mods["manifold"].add("project_n_to_3", v)?; } + { let v = mods["manifold::polytope4"].getattr("random_walk_n_return_prob")?; mods["manifold"].add("random_walk_n_return_prob", v)?; } + { let v = mods["manifold::spacetime"].getattr("relativistic_rocket")?; mods["manifold"].add("relativistic_rocket", v)?; } + { let v = mods["manifold::embedding"].getattr("riemannian_gradient_descent_sphere")?; mods["manifold"].add("riemannian_gradient_descent_sphere", v)?; } + { let v = mods["manifold::spacetime"].getattr("rindler_coords")?; mods["manifold"].add("rindler_coords", v)?; } + { let v = mods["manifold::spacetime"].getattr("rindler_horizon")?; mods["manifold"].add("rindler_horizon", v)?; } + { let v = mods["manifold::spherical"].getattr("robinson")?; mods["manifold"].add("robinson", v)?; } + { let v = mods["manifold::polytope4"].getattr("rotate_4d")?; mods["manifold"].add("rotate_4d", v)?; } + { let v = mods["manifold::polytope4"].getattr("rotate_4d_double")?; mods["manifold"].add("rotate_4d_double", v)?; } + { let v = mods["manifold::spherical"].getattr("rotate_sphere_points")?; mods["manifold"].add("rotate_sphere_points", v)?; } + { let v = mods["manifold::lie"].getattr("rotate_spherical_harmonics")?; mods["manifold"].add("rotate_spherical_harmonics", v)?; } + { let v = mods["manifold::polytope4"].getattr("rotation_4d_planes")?; mods["manifold"].add("rotation_4d_planes", v)?; } + { let v = mods["manifold::lie"].getattr("rotation_averaging")?; mods["manifold"].add("rotation_averaging", v)?; } + { let v = mods["manifold::spherical"].getattr("s3_geodesic")?; mods["manifold"].add("s3_geodesic", v)?; } + { let v = mods["manifold::spherical"].getattr("s3_uniform_points")?; mods["manifold"].add("s3_uniform_points", v)?; } + { let v = mods["manifold::embedding"].getattr("s_curve")?; mods["manifold"].add("s_curve", v)?; } + { let v = mods["manifold::spacetime"].getattr("schwarzschild_geodesic_metric")?; mods["manifold"].add("schwarzschild_geodesic_metric", v)?; } + { let v = mods["manifold::geodesic"].getattr("schwarzschild_orbit")?; mods["manifold"].add("schwarzschild_orbit", v)?; } + { let v = mods["manifold::lie"].getattr("se3")?; mods["manifold"].add("se3", v)?; } + { let v = mods["manifold::geodesic"].getattr("shapiro_delay")?; mods["manifold"].add("shapiro_delay", v)?; } + { let v = mods["manifold::polytope4"].getattr("simplex_n")?; mods["manifold"].add("simplex_n", v)?; } + { let v = mods["manifold::spacetime"].getattr("simultaneity_plane")?; mods["manifold"].add("simultaneity_plane", v)?; } + { let v = mods["manifold::lie"].getattr("so3")?; mods["manifold"].add("so3", v)?; } + { let v = mods["manifold::lie"].getattr("so3_haar_measure_density")?; mods["manifold"].add("so3_haar_measure_density", v)?; } + { let v = mods["manifold::lie"].getattr("so3_uniform_grid")?; mods["manifold"].add("so3_uniform_grid", v)?; } + { let v = mods["manifold::embedding"].getattr("spectral_embedding")?; mods["manifold"].add("spectral_embedding", v)?; } + { let v = mods["manifold::spherical"].getattr("sphere_cap_area")?; mods["manifold"].add("sphere_cap_area", v)?; } + { let v = mods["manifold::spherical"].getattr("sphere_cap_volume")?; mods["manifold"].add("sphere_cap_volume", v)?; } + { let v = mods["manifold::spherical"].getattr("sphere_distance_n")?; mods["manifold"].add("sphere_distance_n", v)?; } + { let v = mods["manifold::spherical"].getattr("sphere_exp_n")?; mods["manifold"].add("sphere_exp_n", v)?; } + { let v = mods["manifold::spherical"].getattr("sphere_geodesic_n")?; mods["manifold"].add("sphere_geodesic_n", v)?; } + { let v = mods["manifold::spherical"].getattr("sphere_log_n")?; mods["manifold"].add("sphere_log_n", v)?; } + { let v = mods["manifold::spherical"].getattr("sphere_parallel_transport_n")?; mods["manifold"].add("sphere_parallel_transport_n", v)?; } + { let v = mods["manifold::embedding"].getattr("sphere_sample")?; mods["manifold"].add("sphere_sample", v)?; } + { let v = mods["manifold::spherical"].getattr("sphere_surface_n")?; mods["manifold"].add("sphere_surface_n", v)?; } + { let v = mods["manifold::spherical"].getattr("sphere_uniform_points_n")?; mods["manifold"].add("sphere_uniform_points_n", v)?; } + { let v = mods["manifold::spherical"].getattr("sphere_volume_n")?; mods["manifold"].add("sphere_volume_n", v)?; } + { let v = mods["manifold::spherical"].getattr("spherical_cap_packing")?; mods["manifold"].add("spherical_cap_packing", v)?; } + { let v = mods["manifold::spherical"].getattr("spherical_centroid")?; mods["manifold"].add("spherical_centroid", v)?; } + { let v = mods["manifold::spherical"].getattr("spherical_code_min_angle")?; mods["manifold"].add("spherical_code_min_angle", v)?; } + { let v = mods["manifold::spherical"].getattr("spherical_convex_hull")?; mods["manifold"].add("spherical_convex_hull", v)?; } + { let v = mods["manifold::spherical"].getattr("spherical_convolution")?; mods["manifold"].add("spherical_convolution", v)?; } + { let v = mods["manifold::spherical"].getattr("spherical_delaunay")?; mods["manifold"].add("spherical_delaunay", v)?; } + { let v = mods["manifold::spherical"].getattr("spherical_harmonic_inverse")?; mods["manifold"].add("spherical_harmonic_inverse", v)?; } + { let v = mods["manifold::spherical"].getattr("spherical_harmonic_transform")?; mods["manifold"].add("spherical_harmonic_transform", v)?; } + { let v = mods["manifold::spherical"].getattr("spherical_harmonics_complex")?; mods["manifold"].add("spherical_harmonics_complex", v)?; } + { let v = mods["manifold::spherical"].getattr("spherical_heat_flow")?; mods["manifold"].add("spherical_heat_flow", v)?; } + { let v = mods["manifold::spherical"].getattr("spherical_kmeans")?; mods["manifold"].add("spherical_kmeans", v)?; } + { let v = mods["manifold::spherical"].getattr("spherical_laplacian_spectral")?; mods["manifold"].add("spherical_laplacian_spectral", v)?; } + { let v = mods["manifold::spherical"].getattr("spherical_law_of_cosines")?; mods["manifold"].add("spherical_law_of_cosines", v)?; } + { let v = mods["manifold::spherical"].getattr("spherical_law_of_sines")?; mods["manifold"].add("spherical_law_of_sines", v)?; } + { let v = mods["manifold::spherical"].getattr("spherical_mean_weighted")?; mods["manifold"].add("spherical_mean_weighted", v)?; } + { let v = mods["manifold::spherical"].getattr("spherical_polygon_area")?; mods["manifold"].add("spherical_polygon_area", v)?; } + { let v = mods["manifold::spherical"].getattr("spherical_t_design")?; mods["manifold"].add("spherical_t_design", v)?; } + { let v = mods["manifold::spherical"].getattr("spherical_triangle_angles")?; mods["manifold"].add("spherical_triangle_angles", v)?; } + { let v = mods["manifold::spherical"].getattr("spherical_triangle_area")?; mods["manifold"].add("spherical_triangle_area", v)?; } + { let v = mods["manifold::spherical"].getattr("spherical_voronoi")?; mods["manifold"].add("spherical_voronoi", v)?; } + { let v = mods["manifold::spherical"].getattr("spherical_wavelets")?; mods["manifold"].add("spherical_wavelets", v)?; } + { let v = mods["manifold::spacetime"].getattr("sta_vs_matrix_lorentz_check")?; mods["manifold"].add("sta_vs_matrix_lorentz_check", v)?; } + { let v = mods["manifold::spherical"].getattr("stereographic")?; mods["manifold"].add("stereographic", v)?; } + { let v = mods["manifold::spherical"].getattr("stereographic_n")?; mods["manifold"].add("stereographic_n", v)?; } + { let v = mods["manifold::embedding"].getattr("stiefel_project")?; mods["manifold"].add("stiefel_project", v)?; } + { let v = mods["manifold::embedding"].getattr("stress")?; mods["manifold"].add("stress", v)?; } + { let v = mods["manifold::lie"].getattr("structure_constants")?; mods["manifold"].add("structure_constants", v)?; } + { let v = mods["manifold::embedding"].getattr("swiss_roll")?; mods["manifold"].add("swiss_roll", v)?; } + { let v = mods["manifold::embedding"].getattr("tangent_space_estimate")?; mods["manifold"].add("tangent_space_estimate", v)?; } + { let v = mods["manifold::spherical"].getattr("thomson_problem")?; mods["manifold"].add("thomson_problem", v)?; } + { let v = mods["manifold::embedding"].getattr("torus_sample")?; mods["manifold"].add("torus_sample", v)?; } + { let v = mods["manifold::embedding"].getattr("trustworthiness")?; mods["manifold"].add("trustworthiness", v)?; } + { let v = mods["manifold::embedding"].getattr("tsne")?; mods["manifold"].add("tsne", v)?; } + { let v = mods["manifold::spacetime"].getattr("twin_paradox_ages")?; mods["manifold"].add("twin_paradox_ages", v)?; } + { let v = mods["manifold::embedding"].getattr("two_moons")?; mods["manifold"].add("two_moons", v)?; } + { let v = mods["manifold::hyperbolic"].getattr("uhp_to_disk")?; mods["manifold"].add("uhp_to_disk", v)?; } + { let v = mods["manifold::embedding"].getattr("umap_lite")?; mods["manifold"].add("umap_lite", v)?; } + { let v = mods["manifold::lie"].getattr("umeyama_alignment")?; mods["manifold"].add("umeyama_alignment", v)?; } + { let v = mods["manifold::spacetime"].getattr("unruh_temperature")?; mods["manifold"].add("unruh_temperature", v)?; } + { let v = mods["manifold::spherical"].getattr("vmf_fit")?; mods["manifold"].add("vmf_fit", v)?; } + { let v = mods["manifold::spherical"].getattr("vmf_sample")?; mods["manifold"].add("vmf_sample", v)?; } + { let v = mods["manifold::polytope4"].getattr("volume_ball_vs_cube_ratio")?; mods["manifold"].add("volume_ball_vs_cube_ratio", v)?; } + { let v = mods["manifold::spherical"].getattr("von_mises_fisher_pdf")?; mods["manifold"].add("von_mises_fisher_pdf", v)?; } + { let v = mods["manifold::vecn"].getattr("wedge")?; mods["manifold"].add("wedge", v)?; } + { let v = mods["manifold::lie"].getattr("wigner_d")?; mods["manifold"].add("wigner_d", v)?; } + { let v = mods["manifold::lie"].getattr("wigner_d_small")?; mods["manifold"].add("wigner_d_small", v)?; } + { let v = mods["monte_carlo::quasi"].getattr("Halton")?; mods["monte_carlo"].add("Halton", v)?; } + { let v = mods["monte_carlo::quasi"].getattr("Sobol")?; mods["monte_carlo"].add("Sobol", v)?; } + { let v = mods["monte_carlo::quasi"].getattr("mc_integrate_sobol")?; mods["monte_carlo"].add("mc_integrate_sobol", v)?; } + { let v = mods["numerical::interpolate"].getattr("BSpline")?; mods["numerical"].add("BSpline", v)?; } + { let v = mods["numerical::interpolate"].getattr("CubicSpline")?; mods["numerical"].add("CubicSpline", v)?; } + { let v = mods["numerical::integrate"].getattr("QuadResult")?; mods["numerical"].add("QuadResult", v)?; } + { let v = mods["numerical::integrate"].getattr("adaptive_quad")?; mods["numerical"].add("adaptive_quad", v)?; } + { let v = mods["numerical::roots"].getattr("bisection")?; mods["numerical"].add("bisection", v)?; } + { let v = mods["numerical::roots"].getattr("brent_root")?; mods["numerical"].add("brent_root", v)?; } + { let v = mods["numerical::interpolate"].getattr("catmull_rom")?; mods["numerical"].add("catmull_rom", v)?; } + { let v = mods["numerical::interpolate"].getattr("catmull_rom_2d")?; mods["numerical"].add("catmull_rom_2d", v)?; } + { let v = mods["numerical::interpolate"].getattr("cubic_interp")?; mods["numerical"].add("cubic_interp", v)?; } + { let v = mods["numerical::interpolate"].getattr("de_casteljau")?; mods["numerical"].add("de_casteljau", v)?; } + { let v = mods["numerical::bvp"].getattr("finite_difference_linear_bvp")?; mods["numerical"].add("finite_difference_linear_bvp", v)?; } + { let v = mods["numerical::integrate"].getattr("gauss_kronrod_15")?; mods["numerical"].add("gauss_kronrod_15", v)?; } + { let v = mods["numerical::integrate"].getattr("gaussian_quadrature_5")?; mods["numerical"].add("gaussian_quadrature_5", v)?; } + { let v = mods["numerical::integrate"].getattr("integrate_infinite")?; mods["numerical"].add("integrate_infinite", v)?; } + { let v = mods["numerical::interpolate"].getattr("lerp")?; mods["numerical"].add("lerp", v)?; } + { let v = mods["numerical::interpolate"].getattr("linear_interp")?; mods["numerical"].add("linear_interp", v)?; } + { let v = mods["numerical::roots"].getattr("newton_raphson")?; mods["numerical"].add("newton_raphson", v)?; } + { let v = mods["numerical::roots"].getattr("polynomial_eval")?; mods["numerical"].add("polynomial_eval", v)?; } + { let v = mods["numerical::roots"].getattr("polynomial_eval_complex")?; mods["numerical"].add("polynomial_eval_complex", v)?; } + { let v = mods["numerical::roots"].getattr("polynomial_roots")?; mods["numerical"].add("polynomial_roots", v)?; } + { let v = mods["numerical::integrate"].getattr("richardson_extrapolate")?; mods["numerical"].add("richardson_extrapolate", v)?; } + { let v = mods["numerical::integrate"].getattr("romberg")?; mods["numerical"].add("romberg", v)?; } + { let v = mods["numerical::roots"].getattr("secant")?; mods["numerical"].add("secant", v)?; } + { let v = mods["numerical::bvp"].getattr("shooting")?; mods["numerical"].add("shooting", v)?; } + { let v = mods["numerical::integrate"].getattr("simpson")?; mods["numerical"].add("simpson", v)?; } + { let v = mods["numerical::integrate"].getattr("trapezoid")?; mods["numerical"].add("trapezoid", v)?; } + { let v = mods["numerical::ode::adaptive"].getattr("AdaptiveResult")?; mods["numerical::ode"].add("AdaptiveResult", v)?; } + { let v = mods["numerical::ode::adaptive"].getattr("dormand_prince")?; mods["numerical::ode"].add("dormand_prince", v)?; } + { let v = mods["numerical::ode::adaptive"].getattr("dormand_prince_dense")?; mods["numerical::ode"].add("dormand_prince_dense", v)?; } + { let v = mods["numerical::ode::explicit"].getattr("euler_step")?; mods["numerical::ode"].add("euler_step", v)?; } + { let v = mods["numerical::ode::symplectic"].getattr("leapfrog_kick_drift_kick")?; mods["numerical::ode"].add("leapfrog_kick_drift_kick", v)?; } + { let v = mods["numerical::ode::explicit"].getattr("rk4_solve")?; mods["numerical::ode"].add("rk4_solve", v)?; } + { let v = mods["numerical::ode::explicit"].getattr("rk4_step")?; mods["numerical::ode"].add("rk4_step", v)?; } + { let v = mods["numerical::ode::explicit"].getattr("rk4_step_vec")?; mods["numerical::ode"].add("rk4_step_vec", v)?; } + { let v = mods["numerical::ode::symplectic"].getattr("velocity_verlet")?; mods["numerical::ode"].add("velocity_verlet", v)?; } + { let v = mods["numerical::ode::symplectic"].getattr("yoshida4")?; mods["numerical::ode"].add("yoshida4", v)?; } + { let v = mods["optimization::least_squares"].getattr("LmResult")?; mods["optimization"].add("LmResult", v)?; } + { let v = mods["optimization::least_squares"].getattr("fit_exponential_decay")?; mods["optimization"].add("fit_exponential_decay", v)?; } + { let v = mods["optimization::least_squares"].getattr("fit_gaussian_peak")?; mods["optimization"].add("fit_gaussian_peak", v)?; } + { let v = mods["resonance::cavity"].getattr("BeamBc")?; mods["resonance"].add("BeamBc", v)?; } + { let v = mods["resonance::coupled"].getattr("CoupledOscillators")?; mods["resonance"].add("CoupledOscillators", v)?; } + { let v = mods["resonance::oscillator"].getattr("DampedOscillator")?; mods["resonance"].add("DampedOscillator", v)?; } + { let v = mods["resonance::oscillator"].getattr("Damping")?; mods["resonance"].add("Damping", v)?; } + { let v = mods["resonance::structural"].getattr("ModalModel")?; mods["resonance"].add("ModalModel", v)?; } + { let v = mods["resonance::cavity"].getattr("PlateBc")?; mods["resonance"].add("PlateBc", v)?; } + { let v = mods["resonance::cavity"].getattr("Rlc")?; mods["resonance"].add("Rlc", v)?; } + { let v = mods["resonance::nonlinear"].getattr("autoresonance_threshold")?; mods["resonance"].add("autoresonance_threshold", v)?; } + { let v = mods["resonance::cavity"].getattr("beam_mode_shape")?; mods["resonance"].add("beam_mode_shape", v)?; } + { let v = mods["resonance::cavity"].getattr("beam_modes")?; mods["resonance"].add("beam_modes", v)?; } + { let v = mods["resonance::cavity"].getattr("bell_modes_approx")?; mods["resonance"].add("bell_modes_approx", v)?; } + { let v = mods["resonance::oscillator"].getattr("bode_plot")?; mods["resonance"].add("bode_plot", v)?; } + { let v = mods["resonance::cavity"].getattr("cavity_photon_lifetime")?; mods["resonance"].add("cavity_photon_lifetime", v)?; } + { let v = mods["resonance::cavity"].getattr("cavity_q")?; mods["resonance"].add("cavity_q", v)?; } + { let v = mods["resonance::cavity"].getattr("chladni_pattern")?; mods["resonance"].add("chladni_pattern", v)?; } + { let v = mods["resonance::cavity"].getattr("chladni_pattern_mixed")?; mods["resonance"].add("chladni_pattern_mixed", v)?; } + { let v = mods["resonance::structural"].getattr("circle_fit")?; mods["resonance"].add("circle_fit", v)?; } + { let v = mods["resonance::cavity"].getattr("circular_membrane_modes")?; mods["resonance"].add("circular_membrane_modes", v)?; } + { let v = mods["resonance::cavity"].getattr("circular_membrane_shape")?; mods["resonance"].add("circular_membrane_shape", v)?; } + { let v = mods["resonance::cavity"].getattr("conical_tube_modes")?; mods["resonance"].add("conical_tube_modes", v)?; } + { let v = mods["resonance::cavity"].getattr("coupled_cavity_splitting")?; mods["resonance"].add("coupled_cavity_splitting", v)?; } + { let v = mods["resonance::cavity"].getattr("cylindrical_cavity_modes")?; mods["resonance"].add("cylindrical_cavity_modes", v)?; } + { let v = mods["resonance::nonlinear"].getattr("describing_function")?; mods["resonance"].add("describing_function", v)?; } + { let v = mods["resonance::nonlinear"].getattr("duffing_backbone")?; mods["resonance"].add("duffing_backbone", v)?; } + { let v = mods["resonance::nonlinear"].getattr("duffing_jump_frequencies")?; mods["resonance"].add("duffing_jump_frequencies", v)?; } + { let v = mods["resonance::nonlinear"].getattr("duffing_poincare")?; mods["resonance"].add("duffing_poincare", v)?; } + { let v = mods["resonance::nonlinear"].getattr("duffing_response_amplitude")?; mods["resonance"].add("duffing_response_amplitude", v)?; } + { let v = mods["resonance::nonlinear"].getattr("duffing_simulate")?; mods["resonance"].add("duffing_simulate", v)?; } + { let v = mods["resonance::structural"].getattr("experimental_modal_peak_picking")?; mods["resonance"].add("experimental_modal_peak_picking", v)?; } + { let v = mods["resonance::cavity"].getattr("fabry_perot_finesse")?; mods["resonance"].add("fabry_perot_finesse", v)?; } + { let v = mods["resonance::cavity"].getattr("fabry_perot_fsr")?; mods["resonance"].add("fabry_perot_fsr", v)?; } + { let v = mods["resonance::cavity"].getattr("fabry_perot_transmission")?; mods["resonance"].add("fabry_perot_transmission", v)?; } + { let v = mods["resonance::nonlinear"].getattr("fano_fit")?; mods["resonance"].add("fano_fit", v)?; } + { let v = mods["resonance::nonlinear"].getattr("fano_lineshape")?; mods["resonance"].add("fano_lineshape", v)?; } + { let v = mods["resonance::nonlinear"].getattr("frequency_pulling")?; mods["resonance"].add("frequency_pulling", v)?; } + { let v = mods["resonance::structural"].getattr("half_power_bandwidth")?; mods["resonance"].add("half_power_bandwidth", v)?; } + { let v = mods["resonance::nonlinear"].getattr("harmonic_balance")?; mods["resonance"].add("harmonic_balance", v)?; } + { let v = mods["resonance::cavity"].getattr("helmholtz_q")?; mods["resonance"].add("helmholtz_q", v)?; } + { let v = mods["resonance::cavity"].getattr("helmholtz_resonator")?; mods["resonance"].add("helmholtz_resonator", v)?; } + { let v = mods["resonance::coupled"].getattr("huygens_sync_simulate")?; mods["resonance"].add("huygens_sync_simulate", v)?; } + { let v = mods["resonance::nonlinear"].getattr("hysteresis_loop")?; mods["resonance"].add("hysteresis_loop", v)?; } + { let v = mods["resonance::cavity"].getattr("inharmonicity_coefficient")?; mods["resonance"].add("inharmonicity_coefficient", v)?; } + { let v = mods["resonance::nonlinear"].getattr("injection_locking_range")?; mods["resonance"].add("injection_locking_range", v)?; } + { let v = mods["resonance::nonlinear"].getattr("kapitza_pendulum_stable")?; mods["resonance"].add("kapitza_pendulum_stable", v)?; } + { let v = mods["resonance::coupled"].getattr("kuramoto")?; mods["resonance"].add("kuramoto", v)?; } + { let v = mods["resonance::coupled"].getattr("kuramoto_critical_coupling")?; mods["resonance"].add("kuramoto_critical_coupling", v)?; } + { let v = mods["resonance::oscillator"].getattr("lorentzian")?; mods["resonance"].add("lorentzian", v)?; } + { let v = mods["resonance::oscillator"].getattr("lorentzian_fit")?; mods["resonance"].add("lorentzian_fit", v)?; } + { let v = mods["resonance::nonlinear"].getattr("mathieu_stability")?; mods["resonance"].add("mathieu_stability", v)?; } + { let v = mods["resonance::nonlinear"].getattr("mathieu_stability_chart")?; mods["resonance"].add("mathieu_stability_chart", v)?; } + { let v = mods["resonance::cavity"].getattr("microwave_cavity_modes_rect")?; mods["resonance"].add("microwave_cavity_modes_rect", v)?; } + { let v = mods["resonance::oscillator"].getattr("nyquist_plot")?; mods["resonance"].add("nyquist_plot", v)?; } + { let v = mods["resonance::structural"].getattr("operational_deflection_shape")?; mods["resonance"].add("operational_deflection_shape", v)?; } + { let v = mods["resonance::nonlinear"].getattr("parametric_resonance_threshold")?; mods["resonance"].add("parametric_resonance_threshold", v)?; } + { let v = mods["resonance::oscillator"].getattr("q_from_ringdown")?; mods["resonance"].add("q_from_ringdown", v)?; } + { let v = mods["resonance::oscillator"].getattr("q_from_spectrum")?; mods["resonance"].add("q_from_spectrum", v)?; } + { let v = mods["resonance::oscillator"].getattr("quality_factor_combined")?; mods["resonance"].add("quality_factor_combined", v)?; } + { let v = mods["resonance::cavity"].getattr("quarter_wave_resonator")?; mods["resonance"].add("quarter_wave_resonator", v)?; } + { let v = mods["resonance::cavity"].getattr("rectangular_membrane_modes")?; mods["resonance"].add("rectangular_membrane_modes", v)?; } + { let v = mods["resonance::cavity"].getattr("rectangular_plate_modes")?; mods["resonance"].add("rectangular_plate_modes", v)?; } + { let v = mods["resonance::oscillator"].getattr("resonance_curve")?; mods["resonance"].add("resonance_curve", v)?; } + { let v = mods["resonance::cavity"].getattr("resonance_overlap")?; mods["resonance"].add("resonance_overlap", v)?; } + { let v = mods["resonance::cavity"].getattr("room_mode_density")?; mods["resonance"].add("room_mode_density", v)?; } + { let v = mods["resonance::cavity"].getattr("room_modes")?; mods["resonance"].add("room_modes", v)?; } + { let v = mods["resonance::cavity"].getattr("schroeder_frequency")?; mods["resonance"].add("schroeder_frequency", v)?; } + { let v = mods["resonance::structural"].getattr("shock_response_spectrum")?; mods["resonance"].add("shock_response_spectrum", v)?; } + { let v = mods["resonance::cavity"].getattr("stiff_string_modes")?; mods["resonance"].add("stiff_string_modes", v)?; } + { let v = mods["resonance::nonlinear"].getattr("stochastic_resonance_snr")?; mods["resonance"].add("stochastic_resonance_snr", v)?; } + { let v = mods["resonance::structural"].getattr("stochastic_subspace_identification")?; mods["resonance"].add("stochastic_subspace_identification", v)?; } + { let v = mods["resonance::cavity"].getattr("string_mode_shape")?; mods["resonance"].add("string_mode_shape", v)?; } + { let v = mods["resonance::cavity"].getattr("string_modes")?; mods["resonance"].add("string_modes", v)?; } + { let v = mods["resonance::nonlinear"].getattr("subharmonic_response")?; mods["resonance"].add("subharmonic_response", v)?; } + { let v = mods["resonance::oscillator"].getattr("transmissibility")?; mods["resonance"].add("transmissibility", v)?; } + { let v = mods["resonance::cavity"].getattr("tube_end_correction")?; mods["resonance"].add("tube_end_correction", v)?; } + { let v = mods["resonance::cavity"].getattr("tube_modes")?; mods["resonance"].add("tube_modes", v)?; } + { let v = mods["resonance::coupled"].getattr("tuned_mass_damper_design")?; mods["resonance"].add("tuned_mass_damper_design", v)?; } + { let v = mods["resonance::cavity"].getattr("tuning_fork_frequency")?; mods["resonance"].add("tuning_fork_frequency", v)?; } + { let v = mods["resonance::coupled"].getattr("two_pendulums_coupled")?; mods["resonance"].add("two_pendulums_coupled", v)?; } + { let v = mods["resonance::nonlinear"].getattr("van_der_pol_entrainment_range")?; mods["resonance"].add("van_der_pol_entrainment_range", v)?; } + { let v = mods["resonance::nonlinear"].getattr("van_der_pol_limit_cycle_amplitude")?; mods["resonance"].add("van_der_pol_limit_cycle_amplitude", v)?; } + { let v = mods["resonance::nonlinear"].getattr("van_der_pol_simulate")?; mods["resonance"].add("van_der_pol_simulate", v)?; } + { let v = mods["resonance::coupled"].getattr("wilberforce_pendulum")?; mods["resonance"].add("wilberforce_pendulum", v)?; } + { let v = mods["dsp::windows"].getattr("blackman_window")?; mods["signal_processing"].add("blackman_window", v)?; } + { let v = mods["transforms::fft"].getattr("fft")?; mods["signal_processing"].add("fft", v)?; } + { let v = mods["transforms::fft"].getattr("fft_convolve")?; mods["signal_processing"].add("fft_convolve", v)?; } + { let v = mods["dsp::iir"].getattr("first_order_highpass")?; mods["signal_processing"].add("first_order_highpass", v)?; } + { let v = mods["dsp::iir"].getattr("first_order_lowpass")?; mods["signal_processing"].add("first_order_lowpass", v)?; } + { let v = mods["dsp::windows"].getattr("hamming_window")?; mods["signal_processing"].add("hamming_window", v)?; } + { let v = mods["dsp::windows"].getattr("hann_window")?; mods["signal_processing"].add("hann_window", v)?; } + { let v = mods["transforms::fft"].getattr("ifft")?; mods["signal_processing"].add("ifft", v)?; } + { let v = mods["transforms::fft"].getattr("next_power_of_two")?; mods["signal_processing"].add("next_power_of_two", v)?; } + { let v = mods["dsp::windows"].getattr("rectangular_window")?; mods["signal_processing"].add("rectangular_window", v)?; } + { let v = mods["transforms::fft"].getattr("rfft")?; mods["signal_processing"].add("rfft", v)?; } + { let v = mods["spatial::primitives"].getattr("Aabb")?; mods["spatial"].add("Aabb", v)?; } + { let v = mods["spatial::transform2d"].getattr("Affine2")?; mods["spatial"].add("Affine2", v)?; } + { let v = mods["spatial::bvh"].getattr("Bvh")?; mods["spatial"].add("Bvh", v)?; } + { let v = mods["spatial::primitives"].getattr("Capsule")?; mods["spatial"].add("Capsule", v)?; } + { let v = mods["spatial::primitives"].getattr("Circle")?; mods["spatial"].add("Circle", v)?; } + { let v = mods["spatial::primitives"].getattr("Cylinder")?; mods["spatial"].add("Cylinder", v)?; } + { let v = mods["spatial::frame"].getattr("Frame")?; mods["spatial"].add("Frame", v)?; } + { let v = mods["spatial::projective"].getattr("Homography")?; mods["spatial"].add("Homography", v)?; } + { let v = mods["spatial::kdtree"].getattr("KdTree")?; mods["spatial"].add("KdTree", v)?; } + { let v = mods["spatial::kdtree"].getattr("KdTree2")?; mods["spatial"].add("KdTree2", v)?; } + { let v = mods["spatial::mat4"].getattr("Mat4")?; mods["spatial"].add("Mat4", v)?; } + { let v = mods["spatial::primitives"].getattr("Obb")?; mods["spatial"].add("Obb", v)?; } + { let v = mods["spatial::octree"].getattr("Octree")?; mods["spatial"].add("Octree", v)?; } + { let v = mods["spatial::primitives"].getattr("Plane")?; mods["spatial"].add("Plane", v)?; } + { let v = mods["spatial::primitives"].getattr("Polygon2")?; mods["spatial"].add("Polygon2", v)?; } + { let v = mods["spatial::primitives"].getattr("Polyline")?; mods["spatial"].add("Polyline", v)?; } + { let v = mods["spatial::primitives"].getattr("Ray")?; mods["spatial"].add("Ray", v)?; } + { let v = mods["spatial::intersect"].getattr("RayHit")?; mods["spatial"].add("RayHit", v)?; } + { let v = mods["spatial::primitives"].getattr("Rect")?; mods["spatial"].add("Rect", v)?; } + { let v = mods["spatial::primitives"].getattr("Segment")?; mods["spatial"].add("Segment", v)?; } + { let v = mods["spatial::primitives"].getattr("Segment2")?; mods["spatial"].add("Segment2", v)?; } + { let v = mods["spatial::kdtree"].getattr("SpatialHash")?; mods["spatial"].add("SpatialHash", v)?; } + { let v = mods["spatial::primitives"].getattr("Sphere")?; mods["spatial"].add("Sphere", v)?; } + { let v = mods["spatial::primitives"].getattr("Triangle")?; mods["spatial"].add("Triangle", v)?; } + { let v = mods["spatial::primitives"].getattr("Triangle2")?; mods["spatial"].add("Triangle2", v)?; } + { let v = mods["spatial::projective"].getattr("are_collinear")?; mods["spatial"].add("are_collinear", v)?; } + { let v = mods["spatial::projective"].getattr("cross_ratio")?; mods["spatial"].add("cross_ratio", v)?; } + { let v = mods["spatial::projective"].getattr("dehomogenize")?; mods["spatial"].add("dehomogenize", v)?; } + { let v = mods["spatial::projective"].getattr("line_through")?; mods["spatial"].add("line_through", v)?; } + { let v = mods["spatial::projective"].getattr("lines_intersect")?; mods["spatial"].add("lines_intersect", v)?; } + { let v = mods["spatial::projective"].getattr("point_h")?; mods["spatial"].add("point_h", v)?; } + { let v = mods["spatial::projective"].getattr("point_on_line")?; mods["spatial"].add("point_on_line", v)?; } + { let v = mods["spatial::projective"].getattr("rectify_quad_to_rect")?; mods["spatial"].add("rectify_quad_to_rect", v)?; } + { let v = mods["special::bessel"].getattr("bessel_i0")?; mods["special"].add("bessel_i0", v)?; } + { let v = mods["special::bessel"].getattr("bessel_i1")?; mods["special"].add("bessel_i1", v)?; } + { let v = mods["special::bessel"].getattr("bessel_j0")?; mods["special"].add("bessel_j0", v)?; } + { let v = mods["special::bessel"].getattr("bessel_j1")?; mods["special"].add("bessel_j1", v)?; } + { let v = mods["special::bessel"].getattr("bessel_j_zeros")?; mods["special"].add("bessel_j_zeros", v)?; } + { let v = mods["special::bessel"].getattr("bessel_jn")?; mods["special"].add("bessel_jn", v)?; } + { let v = mods["special::bessel"].getattr("bessel_k0")?; mods["special"].add("bessel_k0", v)?; } + { let v = mods["special::bessel"].getattr("bessel_k1")?; mods["special"].add("bessel_k1", v)?; } + { let v = mods["special::bessel"].getattr("bessel_y0")?; mods["special"].add("bessel_y0", v)?; } + { let v = mods["special::bessel"].getattr("bessel_y1")?; mods["special"].add("bessel_y1", v)?; } + { let v = mods["special::bessel"].getattr("bessel_yn")?; mods["special"].add("bessel_yn", v)?; } + { let v = mods["special::beta"].getattr("beta_inc")?; mods["special"].add("beta_inc", v)?; } + { let v = mods["special::expint"].getattr("e1")?; mods["special"].add("e1", v)?; } + { let v = mods["special::elliptic"].getattr("ellipse_perimeter_exact")?; mods["special"].add("ellipse_perimeter_exact", v)?; } + { let v = mods["special::elliptic"].getattr("elliptic_e")?; mods["special"].add("elliptic_e", v)?; } + { let v = mods["special::elliptic"].getattr("elliptic_e_inc")?; mods["special"].add("elliptic_e_inc", v)?; } + { let v = mods["special::elliptic"].getattr("elliptic_f")?; mods["special"].add("elliptic_f", v)?; } + { let v = mods["special::elliptic"].getattr("elliptic_k")?; mods["special"].add("elliptic_k", v)?; } + { let v = mods["special::erf"].getattr("erfc")?; mods["special"].add("erfc", v)?; } + { let v = mods["special::erf"].getattr("erfinv")?; mods["special"].add("erfinv", v)?; } + { let v = mods["special::expint"].getattr("exponential_integral")?; mods["special"].add("exponential_integral", v)?; } + { let v = mods["special::gamma"].getattr("gamma_p")?; mods["special"].add("gamma_p", v)?; } + { let v = mods["special::gamma"].getattr("gamma_q")?; mods["special"].add("gamma_q", v)?; } + { let v = mods["special::legendre"].getattr("gauss_legendre_nodes")?; mods["special"].add("gauss_legendre_nodes", v)?; } + { let v = mods["special::elliptic"].getattr("jacobi_elliptic")?; mods["special"].add("jacobi_elliptic", v)?; } + { let v = mods["special::legendre"].getattr("legendre_p")?; mods["special"].add("legendre_p", v)?; } + { let v = mods["special::legendre"].getattr("legendre_p_assoc")?; mods["special"].add("legendre_p_assoc", v)?; } + { let v = mods["special::gamma"].getattr("lgamma")?; mods["special"].add("lgamma", v)?; } + { let v = mods["special::elliptic"].getattr("pendulum_period_exact")?; mods["special"].add("pendulum_period_exact", v)?; } + { let v = mods["special::legendre"].getattr("spherical_harmonic_real")?; mods["special"].add("spherical_harmonic_real", v)?; } + { let v = mods["statistics::distributions"].getattr("Beta")?; mods["statistics"].add("Beta", v)?; } + { let v = mods["statistics::distributions"].getattr("Binomial")?; mods["statistics"].add("Binomial", v)?; } + { let v = mods["statistics::resampling"].getattr("BootstrapResult")?; mods["statistics"].add("BootstrapResult", v)?; } + { let v = mods["statistics::distributions"].getattr("ChiSquared")?; mods["statistics"].add("ChiSquared", v)?; } + { let v = mods["statistics::distributions"].getattr("Exponential")?; mods["statistics"].add("Exponential", v)?; } + { let v = mods["statistics::distributions"].getattr("FDist")?; mods["statistics"].add("FDist", v)?; } + { let v = mods["statistics::distributions"].getattr("Gamma")?; mods["statistics"].add("Gamma", v)?; } + { let v = mods["statistics::distributions"].getattr("LogNormal")?; mods["statistics"].add("LogNormal", v)?; } + { let v = mods["statistics::distributions"].getattr("Normal")?; mods["statistics"].add("Normal", v)?; } + { let v = mods["statistics::distributions"].getattr("Poisson")?; mods["statistics"].add("Poisson", v)?; } + { let v = mods["statistics::distributions"].getattr("StudentT")?; mods["statistics"].add("StudentT", v)?; } + { let v = mods["statistics::inference"].getattr("TestResult")?; mods["statistics"].add("TestResult", v)?; } + { let v = mods["statistics::distributions"].getattr("Weibull")?; mods["statistics"].add("Weibull", v)?; } + { let v = mods["statistics::inference"].getattr("anova_one_way")?; mods["statistics"].add("anova_one_way", v)?; } + { let v = mods["statistics::resampling"].getattr("bootstrap")?; mods["statistics"].add("bootstrap", v)?; } + { let v = mods["statistics::resampling"].getattr("bootstrap_bca")?; mods["statistics"].add("bootstrap_bca", v)?; } + { let v = mods["statistics::inference"].getattr("chi_squared_gof")?; mods["statistics"].add("chi_squared_gof", v)?; } + { let v = mods["statistics::inference"].getattr("chi_squared_independence")?; mods["statistics"].add("chi_squared_independence", v)?; } + { let v = mods["statistics::distributions"].getattr("chi_squared_pdf")?; mods["statistics"].add("chi_squared_pdf", v)?; } + { let v = mods["statistics::inference"].getattr("confidence_interval_mean")?; mods["statistics"].add("confidence_interval_mean", v)?; } + { let v = mods["statistics::descriptive"].getattr("correlation")?; mods["statistics"].add("correlation", v)?; } + { let v = mods["statistics::descriptive"].getattr("covariance")?; mods["statistics"].add("covariance", v)?; } + { let v = mods["statistics::fourier"].getattr("dft")?; mods["statistics"].add("dft", v)?; } + { let v = mods["statistics::fourier"].getattr("dominant_frequency")?; mods["statistics"].add("dominant_frequency", v)?; } + { let v = mods["statistics::descriptive"].getattr("error_propagation_product")?; mods["statistics"].add("error_propagation_product", v)?; } + { let v = mods["statistics::descriptive"].getattr("error_propagation_sum")?; mods["statistics"].add("error_propagation_sum", v)?; } + { let v = mods["statistics::distributions"].getattr("exponential_cdf")?; mods["statistics"].add("exponential_cdf", v)?; } + { let v = mods["statistics::distributions"].getattr("exponential_pdf")?; mods["statistics"].add("exponential_pdf", v)?; } + { let v = mods["statistics::distributions"].getattr("gaussian")?; mods["statistics"].add("gaussian", v)?; } + { let v = mods["statistics::distributions"].getattr("gaussian_cdf")?; mods["statistics"].add("gaussian_cdf", v)?; } + { let v = mods["statistics::distributions"].getattr("gaussian_cdf_approx")?; mods["statistics"].add("gaussian_cdf_approx", v)?; } + { let v = mods["statistics::fourier"].getattr("inverse_dft")?; mods["statistics"].add("inverse_dft", v)?; } + { let v = mods["statistics::resampling"].getattr("jackknife")?; mods["statistics"].add("jackknife", v)?; } + { let v = mods["statistics::inference"].getattr("ks_test_one_sample")?; mods["statistics"].add("ks_test_one_sample", v)?; } + { let v = mods["statistics::inference"].getattr("ks_test_two_sample")?; mods["statistics"].add("ks_test_two_sample", v)?; } + { let v = mods["statistics::descriptive"].getattr("mean")?; mods["statistics"].add("mean", v)?; } + { let v = mods["statistics::descriptive"].getattr("median")?; mods["statistics"].add("median", v)?; } + { let v = mods["statistics::inference"].getattr("pearson_test")?; mods["statistics"].add("pearson_test", v)?; } + { let v = mods["statistics::resampling"].getattr("permutation_test")?; mods["statistics"].add("permutation_test", v)?; } + { let v = mods["statistics::distributions"].getattr("poisson_pmf")?; mods["statistics"].add("poisson_pmf", v)?; } + { let v = mods["statistics::fourier"].getattr("power_spectrum")?; mods["statistics"].add("power_spectrum", v)?; } + { let v = mods["statistics::descriptive"].getattr("sample_std_deviation")?; mods["statistics"].add("sample_std_deviation", v)?; } + { let v = mods["statistics::descriptive"].getattr("sample_variance")?; mods["statistics"].add("sample_variance", v)?; } + { let v = mods["statistics::descriptive"].getattr("std_deviation")?; mods["statistics"].add("std_deviation", v)?; } + { let v = mods["statistics::inference"].getattr("t_test_one_sample")?; mods["statistics"].add("t_test_one_sample", v)?; } + { let v = mods["statistics::inference"].getattr("t_test_paired")?; mods["statistics"].add("t_test_paired", v)?; } + { let v = mods["statistics::inference"].getattr("t_test_two_sample")?; mods["statistics"].add("t_test_two_sample", v)?; } + { let v = mods["statistics::descriptive"].getattr("variance")?; mods["statistics"].add("variance", v)?; } + { let v = mods["statistics::descriptive"].getattr("weighted_mean")?; mods["statistics"].add("weighted_mean", v)?; } + { let v = mods["statistics::descriptive"].getattr("weighted_mean_error")?; mods["statistics"].add("weighted_mean_error", v)?; } + { let v = mods["transforms::dct"].getattr("Bc")?; mods["transforms"].add("Bc", v)?; } + { let v = mods["transforms::radon"].getattr("FbpFilter")?; mods["transforms"].add("FbpFilter", v)?; } + { let v = mods["transforms::fft"].getattr("FftPlan")?; mods["transforms"].add("FftPlan", v)?; } + { let v = mods["transforms::wavelet"].getattr("Mother")?; mods["transforms"].add("Mother", v)?; } + { let v = mods["transforms::wavelet"].getattr("PadMode")?; mods["transforms"].add("PadMode", v)?; } + { let v = mods["transforms::stft"].getattr("Stft")?; mods["transforms"].add("Stft", v)?; } + { let v = mods["transforms::wavelet"].getattr("Threshold")?; mods["transforms"].add("Threshold", v)?; } + { let v = mods["transforms::wavelet"].getattr("Wavelet")?; mods["transforms"].add("Wavelet", v)?; } + { let v = mods["transforms::radon"].getattr("abel_transform")?; mods["transforms"].add("abel_transform", v)?; } + { let v = mods["transforms::hilbert"].getattr("am_demodulate")?; mods["transforms"].add("am_demodulate", v)?; } + { let v = mods["transforms::hilbert"].getattr("analytic_signal")?; mods["transforms"].add("analytic_signal", v)?; } + { let v = mods["transforms::spectral"].getattr("ar_psd")?; mods["transforms"].add("ar_psd", v)?; } + { let v = mods["transforms::spectral"].getattr("burg_ar")?; mods["transforms"].add("burg_ar", v)?; } + { let v = mods["transforms::spectral"].getattr("cepstrum_power")?; mods["transforms"].add("cepstrum_power", v)?; } + { let v = mods["transforms::spectral"].getattr("cepstrum_real")?; mods["transforms"].add("cepstrum_real", v)?; } + { let v = mods["transforms::stft"].getattr("chirp_z")?; mods["transforms"].add("chirp_z", v)?; } + { let v = mods["transforms::spectral"].getattr("coherence")?; mods["transforms"].add("coherence", v)?; } + { let v = mods["transforms::stft"].getattr("constant_q_transform")?; mods["transforms"].add("constant_q_transform", v)?; } + { let v = mods["transforms::spectral"].getattr("cross_spectral_density")?; mods["transforms"].add("cross_spectral_density", v)?; } + { let v = mods["transforms::wavelet"].getattr("cwt")?; mods["transforms"].add("cwt", v)?; } + { let v = mods["transforms::dct"].getattr("dct_2d")?; mods["transforms"].add("dct_2d", v)?; } + { let v = mods["transforms::dct"].getattr("dct_compress")?; mods["transforms"].add("dct_compress", v)?; } + { let v = mods["transforms::dct"].getattr("dct_i")?; mods["transforms"].add("dct_i", v)?; } + { let v = mods["transforms::dct"].getattr("dct_ii")?; mods["transforms"].add("dct_ii", v)?; } + { let v = mods["transforms::dct"].getattr("dct_iii")?; mods["transforms"].add("dct_iii", v)?; } + { let v = mods["transforms::dct"].getattr("dct_iv")?; mods["transforms"].add("dct_iv", v)?; } + { let v = mods["transforms::dct"].getattr("dct_poisson_1d")?; mods["transforms"].add("dct_poisson_1d", v)?; } + { let v = mods["transforms::spectral"].getattr("detrend")?; mods["transforms"].add("detrend", v)?; } + { let v = mods["transforms::laplace"].getattr("digital_freq_response")?; mods["transforms"].add("digital_freq_response", v)?; } + { let v = mods["transforms::spectral"].getattr("dpss")?; mods["transforms"].add("dpss", v)?; } + { let v = mods["transforms::dct"].getattr("dst_i")?; mods["transforms"].add("dst_i", v)?; } + { let v = mods["transforms::dct"].getattr("dst_ii")?; mods["transforms"].add("dst_ii", v)?; } + { let v = mods["transforms::stft"].getattr("dtmf_decode")?; mods["transforms"].add("dtmf_decode", v)?; } + { let v = mods["transforms::wavelet"].getattr("dwt")?; mods["transforms"].add("dwt", v)?; } + { let v = mods["transforms::wavelet"].getattr("dwt_2d")?; mods["transforms"].add("dwt_2d", v)?; } + { let v = mods["transforms::hilbert"].getattr("empirical_mode_decomposition")?; mods["transforms"].add("empirical_mode_decomposition", v)?; } + { let v = mods["transforms::hilbert"].getattr("envelope")?; mods["transforms"].add("envelope", v)?; } + { let v = mods["transforms::fft"].getattr("fft_2d")?; mods["transforms"].add("fft_2d", v)?; } + { let v = mods["transforms::fft"].getattr("fft_3d")?; mods["transforms"].add("fft_3d", v)?; } + { let v = mods["transforms::fft"].getattr("fft_any")?; mods["transforms"].add("fft_any", v)?; } + { let v = mods["transforms::fft"].getattr("fft_convolve")?; mods["transforms"].add("fft_convolve", v)?; } + { let v = mods["transforms::fft"].getattr("fft_convolve_2d")?; mods["transforms"].add("fft_convolve_2d", v)?; } + { let v = mods["transforms::fft"].getattr("fft_correlate")?; mods["transforms"].add("fft_correlate", v)?; } + { let v = mods["transforms::fft"].getattr("fft_differentiate")?; mods["transforms"].add("fft_differentiate", v)?; } + { let v = mods["transforms::fft"].getattr("fft_freqs")?; mods["transforms"].add("fft_freqs", v)?; } + { let v = mods["transforms::fft"].getattr("fft_integrate")?; mods["transforms"].add("fft_integrate", v)?; } + { let v = mods["transforms::fft"].getattr("fft_interpolate")?; mods["transforms"].add("fft_interpolate", v)?; } + { let v = mods["transforms::fft"].getattr("fft_poisson_2d")?; mods["transforms"].add("fft_poisson_2d", v)?; } + { let v = mods["transforms::fft"].getattr("fft_shift")?; mods["transforms"].add("fft_shift", v)?; } + { let v = mods["transforms::hilbert"].getattr("fm_demodulate")?; mods["transforms"].add("fm_demodulate", v)?; } + { let v = mods["transforms::laplace"].getattr("fractional_fourier")?; mods["transforms"].add("fractional_fourier", v)?; } + { let v = mods["transforms::stft"].getattr("goertzel")?; mods["transforms"].add("goertzel", v)?; } + { let v = mods["transforms::stft"].getattr("goertzel_bank")?; mods["transforms"].add("goertzel_bank", v)?; } + { let v = mods["transforms::radon"].getattr("hankel_transform")?; mods["transforms"].add("hankel_transform", v)?; } + { let v = mods["transforms::dct"].getattr("hartley")?; mods["transforms"].add("hartley", v)?; } + { let v = mods["transforms::hilbert"].getattr("hilbert_fir")?; mods["transforms"].add("hilbert_fir", v)?; } + { let v = mods["transforms::hilbert"].getattr("hilbert_huang_spectrum")?; mods["transforms"].add("hilbert_huang_spectrum", v)?; } + { let v = mods["transforms::radon"].getattr("hough_circles")?; mods["transforms"].add("hough_circles", v)?; } + { let v = mods["transforms::radon"].getattr("hough_lines")?; mods["transforms"].add("hough_lines", v)?; } + { let v = mods["transforms::dct"].getattr("idct_2d")?; mods["transforms"].add("idct_2d", v)?; } + { let v = mods["transforms::dct"].getattr("idct_ii")?; mods["transforms"].add("idct_ii", v)?; } + { let v = mods["transforms::wavelet"].getattr("idwt")?; mods["transforms"].add("idwt", v)?; } + { let v = mods["transforms::wavelet"].getattr("idwt_2d")?; mods["transforms"].add("idwt_2d", v)?; } + { let v = mods["transforms::fft"].getattr("ifft")?; mods["transforms"].add("ifft", v)?; } + { let v = mods["transforms::fft"].getattr("ifft_2d")?; mods["transforms"].add("ifft_2d", v)?; } + { let v = mods["transforms::fft"].getattr("ifft_3d")?; mods["transforms"].add("ifft_3d", v)?; } + { let v = mods["transforms::fft"].getattr("ifft_any")?; mods["transforms"].add("ifft_any", v)?; } + { let v = mods["transforms::laplace"].getattr("impulse_response_from_tf")?; mods["transforms"].add("impulse_response_from_tf", v)?; } + { let v = mods["transforms::hilbert"].getattr("instantaneous_frequency")?; mods["transforms"].add("instantaneous_frequency", v)?; } + { let v = mods["transforms::hilbert"].getattr("instantaneous_phase")?; mods["transforms"].add("instantaneous_phase", v)?; } + { let v = mods["transforms::radon"].getattr("inverse_abel")?; mods["transforms"].add("inverse_abel", v)?; } + { let v = mods["transforms::laplace"].getattr("inverse_laplace_stehfest")?; mods["transforms"].add("inverse_laplace_stehfest", v)?; } + { let v = mods["transforms::laplace"].getattr("inverse_laplace_talbot")?; mods["transforms"].add("inverse_laplace_talbot", v)?; } + { let v = mods["transforms::radon"].getattr("inverse_radon_fbp")?; mods["transforms"].add("inverse_radon_fbp", v)?; } + { let v = mods["transforms::radon"].getattr("inverse_radon_sart")?; mods["transforms"].add("inverse_radon_sart", v)?; } + { let v = mods["transforms::fft"].getattr("irfft")?; mods["transforms"].add("irfft", v)?; } + { let v = mods["transforms::hilbert"].getattr("kramers_kronig")?; mods["transforms"].add("kramers_kronig", v)?; } + { let v = mods["transforms::laplace"].getattr("laplace_numeric")?; mods["transforms"].add("laplace_numeric", v)?; } + { let v = mods["transforms::wavelet"].getattr("lifting_dwt_53")?; mods["transforms"].add("lifting_dwt_53", v)?; } + { let v = mods["transforms::wavelet"].getattr("lifting_idwt_53")?; mods["transforms"].add("lifting_idwt_53", v)?; } + { let v = mods["transforms::spectral"].getattr("lomb_scargle")?; mods["transforms"].add("lomb_scargle", v)?; } + { let v = mods["transforms::stft"].getattr("mel_filterbank")?; mods["transforms"].add("mel_filterbank", v)?; } + { let v = mods["transforms::stft"].getattr("mel_spectrogram")?; mods["transforms"].add("mel_spectrogram", v)?; } + { let v = mods["transforms::hilbert"].getattr("minimum_phase_from_magnitude")?; mods["transforms"].add("minimum_phase_from_magnitude", v)?; } + { let v = mods["transforms::wavelet"].getattr("multiresolution_analysis")?; mods["transforms"].add("multiresolution_analysis", v)?; } + { let v = mods["transforms::spectral"].getattr("multitaper")?; mods["transforms"].add("multitaper", v)?; } + { let v = mods["transforms::spectral"].getattr("music")?; mods["transforms"].add("music", v)?; } + { let v = mods["transforms::fft"].getattr("next_power_of_two")?; mods["transforms"].add("next_power_of_two", v)?; } + { let v = mods["transforms::spectral"].getattr("periodogram")?; mods["transforms"].add("periodogram", v)?; } + { let v = mods["transforms::spectral"].getattr("power_law_fit")?; mods["transforms"].add("power_law_fit", v)?; } + { let v = mods["transforms::stft"].getattr("reassigned_spectrogram")?; mods["transforms"].add("reassigned_spectrogram", v)?; } + { let v = mods["transforms::fft"].getattr("rfft")?; mods["transforms"].add("rfft", v)?; } + { let v = mods["transforms::fft"].getattr("rfft_2d")?; mods["transforms"].add("rfft_2d", v)?; } + { let v = mods["transforms::laplace"].getattr("s_domain_freq_response")?; mods["transforms"].add("s_domain_freq_response", v)?; } + { let v = mods["transforms::wavelet"].getattr("scale_to_frequency")?; mods["transforms"].add("scale_to_frequency", v)?; } + { let v = mods["transforms::wavelet"].getattr("scalogram")?; mods["transforms"].add("scalogram", v)?; } + { let v = mods["transforms::radon"].getattr("shepp_logan_phantom")?; mods["transforms"].add("shepp_logan_phantom", v)?; } + { let v = mods["transforms::spectral"].getattr("spectral_entropy")?; mods["transforms"].add("spectral_entropy", v)?; } + { let v = mods["transforms::spectral"].getattr("spectral_flatness")?; mods["transforms"].add("spectral_flatness", v)?; } + { let v = mods["transforms::stft"].getattr("spectrogram")?; mods["transforms"].add("spectrogram", v)?; } + { let v = mods["transforms::hilbert"].getattr("ssb_modulate")?; mods["transforms"].add("ssb_modulate", v)?; } + { let v = mods["transforms::spectral"].getattr("transfer_function_estimate")?; mods["transforms"].add("transfer_function_estimate", v)?; } + { let v = mods["transforms::wavelet"].getattr("wavedec")?; mods["transforms"].add("wavedec", v)?; } + { let v = mods["transforms::wavelet"].getattr("wavelet_compress")?; mods["transforms"].add("wavelet_compress", v)?; } + { let v = mods["transforms::wavelet"].getattr("wavelet_denoise")?; mods["transforms"].add("wavelet_denoise", v)?; } + { let v = mods["transforms::wavelet"].getattr("wavelet_filters")?; mods["transforms"].add("wavelet_filters", v)?; } + { let v = mods["transforms::wavelet"].getattr("wavelet_packet_decompose")?; mods["transforms"].add("wavelet_packet_decompose", v)?; } + { let v = mods["transforms::wavelet"].getattr("waverec")?; mods["transforms"].add("waverec", v)?; } + { let v = mods["transforms::spectral"].getattr("welch")?; mods["transforms"].add("welch", v)?; } + { let v = mods["transforms::spectral"].getattr("yule_walker_ar")?; mods["transforms"].add("yule_walker_ar", v)?; } + { let v = mods["transforms::laplace"].getattr("z_transform_eval")?; mods["transforms"].add("z_transform_eval", v)?; } + { let v = mods["transforms::stft"].getattr("zoom_fft")?; mods["transforms"].add("zoom_fft", v)?; } + let names: Vec<&str> = vec![ + "acoustics", "astrophysics", "atmosphere", "audio", "biophysics", "cfd", "chemistry", "classical", "codes", "color_science", "continuum_mechanics", "control_systems", "core", "curves", "discrete", "dsp", "electromagnetism", "electronics", "exact", "fem", "fields", "finance", "fluid_instabilities", "fluids", "fractals", "general_relativity", "geometry", "geophysics", "graph", "gravitation", "information_theory", "learn", "linalg", "magnetohydrodynamics", "manifold", "materials", "math", "mesh", "monte_carlo", "neutronics", "nonlinear", "nuclear", "numerical", "optics", "optimization", "particle_physics", "patterns", "photonics", "plasma", "propulsion", "quantum", "quaternion", "radiation", "relativity", "resonance", "rf", "signal_processing", "sim", "solid_mechanics", "spatial", "special", "statistical_mechanics", "statistics", "stochastic", "thermodynamics", "transforms", "trigonometry", "units", "vector_calculus", "waves", "astrophysics.collisions", "astrophysics.coords", "astrophysics.gravitational_waves", "astrophysics.habitable_zone", "astrophysics.kepler", "astrophysics.lagrange", "astrophysics.lambert", "astrophysics.magnetosphere", "astrophysics.maneuvers", "astrophysics.nbody", "astrophysics.orbital_elements", "astrophysics.tidal", "astrophysics.time_systems", "audio.analysis", "audio.effects", "audio.envelope", "audio.oscillators", "audio.physical", "audio.spatial", "audio.synthesis", "audio.tuning", "audio.vocoder", "audio.wav", "biophysics.epidemiology", "biophysics.neuro", "biophysics.phylo", "biophysics.population", "biophysics.seq_align", "cfd.advection", "cfd.boundary_layer", "cfd.grid", "cfd.lbm", "cfd.level_set", "cfd.multiphase", "cfd.porous", "cfd.potential_flow", "cfd.riemann", "cfd.shallow_water", "cfd.sph", "cfd.stable_fluids", "cfd.turbulence", "cfd.vortex", "codes.block", "codes.checksum", "codes.compression", "codes.convolutional", "codes.crypto_math", "codes.reed_solomon", "control_systems.kalman", "core.compensated", "core.dual", "core.interval", "discrete.combinatorics", "discrete.disjoint_set", "discrete.number_theory", "discrete.partitions", "discrete.primes", "discrete.sequences", "dsp.fir", "dsp.iir", "dsp.phase", "dsp.resample", "dsp.windows", "exact.bigfloat", "exact.bigint", "exact.contfrac", "exact.polynomial", "exact.rational", "exact.symbolic", "fem.fdtd", "fem.fem1d", "fem.fem2d", "fem.spectral_pde", "finance.options", "finance.portfolio", "finance.rates", "finance.risk", "fractals.attractors", "fractals.automata", "fractals.escape_time", "fractals.ifs", "fractals.lsystem", "fractals.noise", "geometry.delaunay", "geometry.geodesy", "geometry.hull", "geometry.mesh", "graph.coloring", "graph.core", "graph.flow", "graph.layout", "graph.matching", "graph.paths", "graph.spectral", "learn.cluster", "learn.gp", "learn.nn", "learn.tree", "linalg.cholesky", "linalg.eigen", "linalg.lu", "linalg.matrix", "linalg.qr", "linalg.sparse", "linalg.svd", "linalg.tridiagonal", "manifold.clifford", "manifold.dec", "manifold.embedding", "manifold.geodesic", "manifold.hyperbolic", "manifold.lie", "manifold.metric", "manifold.polytope4", "manifold.spacetime", "manifold.spherical", "manifold.vecn", "materials.common", "materials.elements", "materials.fluids", "materials.gases", "math.constants", "mesh.analyze", "mesh.generate", "mesh.isosurface", "mesh.parameterize", "mesh.subdivide", "mesh.surfaces", "monte_carlo.quasi", "numerical.bvp", "numerical.integrate", "numerical.interpolate", "numerical.ode", "numerical.roots", "optimization.convex", "optimization.game_theory", "optimization.integer", "optimization.least_squares", "optimization.lp", "optimization.metaheuristics", "optimization.network", "patterns.aperiodic", "patterns.knots", "patterns.packing", "patterns.phyllotaxis", "patterns.polygon_ops", "patterns.polyhedra", "patterns.sampling", "patterns.space_filling", "patterns.symmetry", "patterns.tilings", "quantum.algorithms", "quantum.circuit", "quantum.schrodinger", "quantum.solid_state", "quantum.spin", "quantum.wavefunction", "resonance.cavity", "resonance.coupled", "resonance.nonlinear", "resonance.oscillator", "resonance.structural", "sim.cloth_sim", "sim.em_sim", "sim.fluid_sim", "sim.heat_sim", "sim.rigid_body", "sim.wave_sim", "spatial.bvh", "spatial.contain", "spatial.distance", "spatial.frame", "spatial.intersect", "spatial.kdtree", "spatial.mat4", "spatial.octree", "spatial.primitives", "spatial.projective", "spatial.sdf", "spatial.transform2d", "special.bessel", "special.beta", "special.elliptic", "special.erf", "special.expint", "special.gamma", "special.legendre", "statistical_mechanics.ising", "statistical_mechanics.kinetics", "statistical_mechanics.lattice_models", "statistical_mechanics.md", "statistics.descriptive", "statistics.distributions", "statistics.fourier", "statistics.inference", "statistics.resampling", "stochastic.extreme", "stochastic.hmm", "stochastic.markov", "stochastic.point_process", "stochastic.queueing", "stochastic.rmt", "stochastic.sde", "stochastic.timeseries", "transforms.dct", "transforms.fft", "transforms.hilbert", "transforms.laplace", "transforms.radon", "transforms.spectral", "transforms.stft", "transforms.wavelet", "units.dimensional", "units.quantity", "fractals.attractors.presets", "fractals.automata.patterns", "fractals.ifs.presets", "fractals.lsystem.presets", "manifold.clifford.cga3", "manifold.clifford.cl3", "manifold.clifford.pga3", "manifold.clifford.sta", "numerical.ode.adaptive", "numerical.ode.explicit", "numerical.ode.implicit", "numerical.ode.symplectic" + ]; + root.add("__submodules__", names)?; + Ok(()) +} diff --git a/bindings/python/src/generated/types/astrophysics.rs b/bindings/python/src/generated/types/astrophysics.rs new file mode 100644 index 0000000..ade3b29 --- /dev/null +++ b/bindings/python/src/generated/types/astrophysics.rs @@ -0,0 +1,720 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; + + +/// +/// Rust: `astrophysics::collisions::CollisionKind` +#[pyclass(name = "CollisionKind", module = "numeria.astrophysics.collisions", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyCollisionKind { + Merger, + PlanetaryDestruction, + StellarMerger, + BlackHoleAbsorption, + TidalDisruption, + GrazingCollision, + GiantImpact, +} +impl PyCollisionKind { + pub fn to_rust(&self) -> rust_physics_engine::astrophysics::collisions::CollisionKind { match self { + Self::Merger => rust_physics_engine::astrophysics::collisions::CollisionKind::Merger, + Self::PlanetaryDestruction => rust_physics_engine::astrophysics::collisions::CollisionKind::PlanetaryDestruction, + Self::StellarMerger => rust_physics_engine::astrophysics::collisions::CollisionKind::StellarMerger, + Self::BlackHoleAbsorption => rust_physics_engine::astrophysics::collisions::CollisionKind::BlackHoleAbsorption, + Self::TidalDisruption => rust_physics_engine::astrophysics::collisions::CollisionKind::TidalDisruption, + Self::GrazingCollision => rust_physics_engine::astrophysics::collisions::CollisionKind::GrazingCollision, + Self::GiantImpact => rust_physics_engine::astrophysics::collisions::CollisionKind::GiantImpact, + } } + pub fn from_rust(v: &rust_physics_engine::astrophysics::collisions::CollisionKind) -> Self { match v { + rust_physics_engine::astrophysics::collisions::CollisionKind::Merger => Self::Merger, + rust_physics_engine::astrophysics::collisions::CollisionKind::PlanetaryDestruction => Self::PlanetaryDestruction, + rust_physics_engine::astrophysics::collisions::CollisionKind::StellarMerger => Self::StellarMerger, + rust_physics_engine::astrophysics::collisions::CollisionKind::BlackHoleAbsorption => Self::BlackHoleAbsorption, + rust_physics_engine::astrophysics::collisions::CollisionKind::TidalDisruption => Self::TidalDisruption, + rust_physics_engine::astrophysics::collisions::CollisionKind::GrazingCollision => Self::GrazingCollision, + rust_physics_engine::astrophysics::collisions::CollisionKind::GiantImpact => Self::GiantImpact, + } } +} +#[pymethods] +impl PyCollisionKind { + fn __repr__(&self) -> &'static str { + match self { + Self::Merger => "CollisionKind.Merger", + Self::PlanetaryDestruction => "CollisionKind.PlanetaryDestruction", + Self::StellarMerger => "CollisionKind.StellarMerger", + Self::BlackHoleAbsorption => "CollisionKind.BlackHoleAbsorption", + Self::TidalDisruption => "CollisionKind.TidalDisruption", + Self::GrazingCollision => "CollisionKind.GrazingCollision", + Self::GiantImpact => "CollisionKind.GiantImpact", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// +/// Rust: `astrophysics::collisions::CollisionResult` +#[pyclass(name = "CollisionResult", module = "numeria.astrophysics.collisions", from_py_object)] +#[derive(Clone)] +pub struct PyCollisionResult { pub inner: rust_physics_engine::astrophysics::collisions::CollisionResult } +#[pymethods] +impl PyCollisionResult { + /// Builds a `CollisionResult` from its fields. + #[new] + #[pyo3(signature = (kind, merged_mass, merged_velocity, merged_radius, temperature_increase, debris))] + fn __new__(kind: crate::generated::types::PyCollisionKind, merged_mass: f64, merged_velocity: crate::generated::types::PyVec3Arg, merged_radius: f64, temperature_increase: f64, debris: crate::generated::types::PyDebrisParams) -> Self { + let kind = kind.to_rust(); + let merged_velocity = merged_velocity.0; + let debris = debris.inner; + Self { inner: rust_physics_engine::astrophysics::collisions::CollisionResult { kind: kind, merged_mass: merged_mass, merged_velocity: merged_velocity, merged_radius: merged_radius, temperature_increase: temperature_increase, debris: debris } } + } + + #[getter] + #[pyo3(name = "kind")] + fn py_get_kind(&self) -> PyResult { Ok(crate::generated::types::PyCollisionKind::from_rust(&self.inner.kind.clone())) } + + #[getter] + #[pyo3(name = "merged_mass")] + fn py_get_merged_mass(&self) -> PyResult { Ok(self.inner.merged_mass) } + + #[setter] + #[pyo3(name = "merged_mass")] + fn py_set_merged_mass(&mut self, v: f64) { self.inner.merged_mass = v; } + + #[getter] + #[pyo3(name = "merged_velocity")] + fn py_get_merged_velocity(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.merged_velocity.clone() }) } + + #[getter] + #[pyo3(name = "merged_radius")] + fn py_get_merged_radius(&self) -> PyResult { Ok(self.inner.merged_radius) } + + #[setter] + #[pyo3(name = "merged_radius")] + fn py_set_merged_radius(&mut self, v: f64) { self.inner.merged_radius = v; } + + #[getter] + #[pyo3(name = "temperature_increase")] + fn py_get_temperature_increase(&self) -> PyResult { Ok(self.inner.temperature_increase) } + + #[setter] + #[pyo3(name = "temperature_increase")] + fn py_set_temperature_increase(&mut self, v: f64) { self.inner.temperature_increase = v; } + + #[getter] + #[pyo3(name = "debris")] + fn py_get_debris(&self) -> PyResult { Ok(crate::generated::types::PyDebrisParams { inner: self.inner.debris.clone() }) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("CollisionResult", "CollisionResult", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// +/// Rust: `astrophysics::collisions::DebrisParams` +#[pyclass(name = "DebrisParams", module = "numeria.astrophysics.collisions", from_py_object)] +#[derive(Clone)] +pub struct PyDebrisParams { pub inner: rust_physics_engine::astrophysics::collisions::DebrisParams } +#[pymethods] +impl PyDebrisParams { + /// Builds a `DebrisParams` from its fields. + #[new] + #[pyo3(signature = (count, speed_factor, mass_fraction, base_temperature))] + fn __new__(count: usize, speed_factor: f64, mass_fraction: f64, base_temperature: f64) -> Self { + + Self { inner: rust_physics_engine::astrophysics::collisions::DebrisParams { count: count, speed_factor: speed_factor, mass_fraction: mass_fraction, base_temperature: base_temperature } } + } + + #[getter] + #[pyo3(name = "count")] + fn py_get_count(&self) -> PyResult { Ok(self.inner.count) } + + #[setter] + #[pyo3(name = "count")] + fn py_set_count(&mut self, v: usize) { self.inner.count = v; } + + #[getter] + #[pyo3(name = "speed_factor")] + fn py_get_speed_factor(&self) -> PyResult { Ok(self.inner.speed_factor) } + + #[setter] + #[pyo3(name = "speed_factor")] + fn py_set_speed_factor(&mut self, v: f64) { self.inner.speed_factor = v; } + + #[getter] + #[pyo3(name = "mass_fraction")] + fn py_get_mass_fraction(&self) -> PyResult { Ok(self.inner.mass_fraction) } + + #[setter] + #[pyo3(name = "mass_fraction")] + fn py_set_mass_fraction(&mut self, v: f64) { self.inner.mass_fraction = v; } + + #[getter] + #[pyo3(name = "base_temperature")] + fn py_get_base_temperature(&self) -> PyResult { Ok(self.inner.base_temperature) } + + #[setter] + #[pyo3(name = "base_temperature")] + fn py_set_base_temperature(&mut self, v: f64) { self.inner.base_temperature = v; } + + fn __repr__(&self) -> String { format!("DebrisParams(count={:?}, speed_factor={:?}, mass_fraction={:?}, base_temperature={:?})", self.inner.count, self.inner.speed_factor, self.inner.mass_fraction, self.inner.base_temperature) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// The planets this module's low-precision ephemeris covers. +/// +/// Rust: `astrophysics::coords::Planet` +#[pyclass(name = "Planet", module = "numeria.astrophysics.coords", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyPlanet { + Mercury, + Venus, + Earth, + Mars, + Jupiter, + Saturn, + Uranus, + Neptune, +} +impl PyPlanet { + pub fn to_rust(&self) -> rust_physics_engine::astrophysics::coords::Planet { match self { + Self::Mercury => rust_physics_engine::astrophysics::coords::Planet::Mercury, + Self::Venus => rust_physics_engine::astrophysics::coords::Planet::Venus, + Self::Earth => rust_physics_engine::astrophysics::coords::Planet::Earth, + Self::Mars => rust_physics_engine::astrophysics::coords::Planet::Mars, + Self::Jupiter => rust_physics_engine::astrophysics::coords::Planet::Jupiter, + Self::Saturn => rust_physics_engine::astrophysics::coords::Planet::Saturn, + Self::Uranus => rust_physics_engine::astrophysics::coords::Planet::Uranus, + Self::Neptune => rust_physics_engine::astrophysics::coords::Planet::Neptune, + } } + pub fn from_rust(v: &rust_physics_engine::astrophysics::coords::Planet) -> Self { match v { + rust_physics_engine::astrophysics::coords::Planet::Mercury => Self::Mercury, + rust_physics_engine::astrophysics::coords::Planet::Venus => Self::Venus, + rust_physics_engine::astrophysics::coords::Planet::Earth => Self::Earth, + rust_physics_engine::astrophysics::coords::Planet::Mars => Self::Mars, + rust_physics_engine::astrophysics::coords::Planet::Jupiter => Self::Jupiter, + rust_physics_engine::astrophysics::coords::Planet::Saturn => Self::Saturn, + rust_physics_engine::astrophysics::coords::Planet::Uranus => Self::Uranus, + rust_physics_engine::astrophysics::coords::Planet::Neptune => Self::Neptune, + } } +} +#[pymethods] +impl PyPlanet { + fn __repr__(&self) -> &'static str { + match self { + Self::Mercury => "Planet.Mercury", + Self::Venus => "Planet.Venus", + Self::Earth => "Planet.Earth", + Self::Mars => "Planet.Mars", + Self::Jupiter => "Planet.Jupiter", + Self::Saturn => "Planet.Saturn", + Self::Uranus => "Planet.Uranus", + Self::Neptune => "Planet.Neptune", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// The fields a two-line element set carries. +/// +/// Rust: `astrophysics::coords::TleElements` +#[pyclass(name = "TleElements", module = "numeria.astrophysics.coords", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyTleElements { pub inner: rust_physics_engine::astrophysics::coords::TleElements } +#[pymethods] +impl PyTleElements { + /// Builds a `TleElements` from its fields. + #[new] + #[pyo3(signature = (catalog_number, designator, epoch_jd, mean_motion_dot, bstar, inclination, raan, eccentricity, arg_perigee, mean_anomaly, mean_motion, revolution))] + fn __new__(catalog_number: u32, designator: String, epoch_jd: f64, mean_motion_dot: f64, bstar: f64, inclination: f64, raan: f64, eccentricity: f64, arg_perigee: f64, mean_anomaly: f64, mean_motion: f64, revolution: u32) -> Self { + + Self { inner: rust_physics_engine::astrophysics::coords::TleElements { catalog_number: catalog_number, designator: designator, epoch_jd: epoch_jd, mean_motion_dot: mean_motion_dot, bstar: bstar, inclination: inclination, raan: raan, eccentricity: eccentricity, arg_perigee: arg_perigee, mean_anomaly: mean_anomaly, mean_motion: mean_motion, revolution: revolution } } + } + + #[getter] + #[pyo3(name = "catalog_number")] + fn py_get_catalog_number(&self) -> PyResult { Ok(self.inner.catalog_number) } + + #[setter] + #[pyo3(name = "catalog_number")] + fn py_set_catalog_number(&mut self, v: u32) { self.inner.catalog_number = v; } + + #[getter] + #[pyo3(name = "designator")] + fn py_get_designator(&self) -> PyResult { Ok(self.inner.designator.to_string()) } + + #[getter] + #[pyo3(name = "epoch_jd")] + fn py_get_epoch_jd(&self) -> PyResult { Ok(self.inner.epoch_jd) } + + #[setter] + #[pyo3(name = "epoch_jd")] + fn py_set_epoch_jd(&mut self, v: f64) { self.inner.epoch_jd = v; } + + #[getter] + #[pyo3(name = "mean_motion_dot")] + fn py_get_mean_motion_dot(&self) -> PyResult { Ok(self.inner.mean_motion_dot) } + + #[setter] + #[pyo3(name = "mean_motion_dot")] + fn py_set_mean_motion_dot(&mut self, v: f64) { self.inner.mean_motion_dot = v; } + + #[getter] + #[pyo3(name = "bstar")] + fn py_get_bstar(&self) -> PyResult { Ok(self.inner.bstar) } + + #[setter] + #[pyo3(name = "bstar")] + fn py_set_bstar(&mut self, v: f64) { self.inner.bstar = v; } + + #[getter] + #[pyo3(name = "inclination")] + fn py_get_inclination(&self) -> PyResult { Ok(self.inner.inclination) } + + #[setter] + #[pyo3(name = "inclination")] + fn py_set_inclination(&mut self, v: f64) { self.inner.inclination = v; } + + #[getter] + #[pyo3(name = "raan")] + fn py_get_raan(&self) -> PyResult { Ok(self.inner.raan) } + + #[setter] + #[pyo3(name = "raan")] + fn py_set_raan(&mut self, v: f64) { self.inner.raan = v; } + + #[getter] + #[pyo3(name = "eccentricity")] + fn py_get_eccentricity(&self) -> PyResult { Ok(self.inner.eccentricity) } + + #[setter] + #[pyo3(name = "eccentricity")] + fn py_set_eccentricity(&mut self, v: f64) { self.inner.eccentricity = v; } + + #[getter] + #[pyo3(name = "arg_perigee")] + fn py_get_arg_perigee(&self) -> PyResult { Ok(self.inner.arg_perigee) } + + #[setter] + #[pyo3(name = "arg_perigee")] + fn py_set_arg_perigee(&mut self, v: f64) { self.inner.arg_perigee = v; } + + #[getter] + #[pyo3(name = "mean_anomaly")] + fn py_get_mean_anomaly(&self) -> PyResult { Ok(self.inner.mean_anomaly) } + + #[setter] + #[pyo3(name = "mean_anomaly")] + fn py_set_mean_anomaly(&mut self, v: f64) { self.inner.mean_anomaly = v; } + + #[getter] + #[pyo3(name = "mean_motion")] + fn py_get_mean_motion(&self) -> PyResult { Ok(self.inner.mean_motion) } + + #[setter] + #[pyo3(name = "mean_motion")] + fn py_set_mean_motion(&mut self, v: f64) { self.inner.mean_motion = v; } + + #[getter] + #[pyo3(name = "revolution")] + fn py_get_revolution(&self) -> PyResult { Ok(self.inner.revolution) } + + #[setter] + #[pyo3(name = "revolution")] + fn py_set_revolution(&mut self, v: u32) { self.inner.revolution = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("TleElements", "TleElements", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// +/// Rust: `astrophysics::magnetosphere::CelestialBodyType` +#[pyclass(name = "CelestialBodyType", module = "numeria.astrophysics.magnetosphere", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyCelestialBodyType { + Star, + GasGiant, + IceGiant, + Terrestrial, + NeutronStar, + BlackHole, +} +impl PyCelestialBodyType { + pub fn to_rust(&self) -> rust_physics_engine::astrophysics::magnetosphere::CelestialBodyType { match self { + Self::Star => rust_physics_engine::astrophysics::magnetosphere::CelestialBodyType::Star, + Self::GasGiant => rust_physics_engine::astrophysics::magnetosphere::CelestialBodyType::GasGiant, + Self::IceGiant => rust_physics_engine::astrophysics::magnetosphere::CelestialBodyType::IceGiant, + Self::Terrestrial => rust_physics_engine::astrophysics::magnetosphere::CelestialBodyType::Terrestrial, + Self::NeutronStar => rust_physics_engine::astrophysics::magnetosphere::CelestialBodyType::NeutronStar, + Self::BlackHole => rust_physics_engine::astrophysics::magnetosphere::CelestialBodyType::BlackHole, + } } + pub fn from_rust(v: &rust_physics_engine::astrophysics::magnetosphere::CelestialBodyType) -> Self { match v { + rust_physics_engine::astrophysics::magnetosphere::CelestialBodyType::Star => Self::Star, + rust_physics_engine::astrophysics::magnetosphere::CelestialBodyType::GasGiant => Self::GasGiant, + rust_physics_engine::astrophysics::magnetosphere::CelestialBodyType::IceGiant => Self::IceGiant, + rust_physics_engine::astrophysics::magnetosphere::CelestialBodyType::Terrestrial => Self::Terrestrial, + rust_physics_engine::astrophysics::magnetosphere::CelestialBodyType::NeutronStar => Self::NeutronStar, + rust_physics_engine::astrophysics::magnetosphere::CelestialBodyType::BlackHole => Self::BlackHole, + } } +} +#[pymethods] +impl PyCelestialBodyType { + fn __repr__(&self) -> &'static str { + match self { + Self::Star => "CelestialBodyType.Star", + Self::GasGiant => "CelestialBodyType.GasGiant", + Self::IceGiant => "CelestialBodyType.IceGiant", + Self::Terrestrial => "CelestialBodyType.Terrestrial", + Self::NeutronStar => "CelestialBodyType.NeutronStar", + Self::BlackHole => "CelestialBodyType.BlackHole", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// +/// Rust: `astrophysics::nbody::Body` +#[pyclass(name = "Body", module = "numeria.astrophysics.nbody", from_py_object)] +#[derive(Clone)] +pub struct PyBody { pub inner: rust_physics_engine::astrophysics::nbody::Body } +#[pymethods] +impl PyBody { + /// Creates a new body with the given properties and zero initial acceleration. + /// + /// Rust: `astrophysics::nbody::Body::new` + #[new] + #[pyo3(signature = (id, mass, radius, position, velocity))] + fn __new__(id: u32, mass: f64, radius: f64, position: crate::generated::types::PyVec3Arg, velocity: crate::generated::types::PyVec3Arg) -> PyResult { + let position = position.0; + let velocity = velocity.0; + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::nbody::Body::new(id, mass, radius, position, velocity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBody { inner: __v }) + } + + /// Computes kinetic energy of this body: KE = ½mv². + /// + /// Rust: `astrophysics::nbody::Body::kinetic_energy` + #[pyo3(name = "kinetic_energy")] + #[pyo3(signature = ())] + fn kinetic_energy(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.kinetic_energy()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "id")] + fn py_get_id(&self) -> PyResult { Ok(self.inner.id) } + + #[setter] + #[pyo3(name = "id")] + fn py_set_id(&mut self, v: u32) { self.inner.id = v; } + + #[getter] + #[pyo3(name = "mass")] + fn py_get_mass(&self) -> PyResult { Ok(self.inner.mass) } + + #[setter] + #[pyo3(name = "mass")] + fn py_set_mass(&mut self, v: f64) { self.inner.mass = v; } + + #[getter] + #[pyo3(name = "radius")] + fn py_get_radius(&self) -> PyResult { Ok(self.inner.radius) } + + #[setter] + #[pyo3(name = "radius")] + fn py_set_radius(&mut self, v: f64) { self.inner.radius = v; } + + #[getter] + #[pyo3(name = "position")] + fn py_get_position(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.position.clone() }) } + + #[getter] + #[pyo3(name = "velocity")] + fn py_get_velocity(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.velocity.clone() }) } + + #[getter] + #[pyo3(name = "acceleration")] + fn py_get_acceleration(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.acceleration.clone() }) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Body", "Body", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// +/// Rust: `astrophysics::nbody::NBodySystem` +#[pyclass(name = "NBodySystem", module = "numeria.astrophysics.nbody")] +pub struct PyNBodySystem { pub inner: rust_physics_engine::astrophysics::nbody::NBodySystem } +#[pymethods] +impl PyNBodySystem { + /// Initializes an N-body system with the given timestep and gravitational softening length, computing initial accelerations. + /// + /// Rust: `astrophysics::nbody::NBodySystem::new` + #[new] + #[pyo3(signature = (bodies, dt, softening))] + fn __new__(bodies: Vec, dt: f64, softening: f64) -> PyResult { + let bodies = bodies.into_iter().map(|__e| __e.inner).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::nbody::NBodySystem::new(bodies, dt, softening)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyNBodySystem { inner: __v }) + } + + /// Advances the simulation by one timestep using the velocity Verlet integrator. + /// + /// Rust: `astrophysics::nbody::NBodySystem::step` + #[pyo3(name = "step")] + #[pyo3(signature = ())] + fn step(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Computes the total mechanical energy (kinetic + potential) of the system. + /// + /// Rust: `astrophysics::nbody::NBodySystem::total_energy` + #[pyo3(name = "total_energy")] + #[pyo3(signature = ())] + fn total_energy(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.total_energy()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Computes the mass-weighted center of mass of all bodies. + /// + /// Rust: `astrophysics::nbody::NBodySystem::center_of_mass` + #[pyo3(name = "center_of_mass")] + #[pyo3(signature = ())] + fn center_of_mass(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.center_of_mass()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Computes the total linear momentum of the system: p = Σ(m_i × v_i). + /// + /// Rust: `astrophysics::nbody::NBodySystem::total_momentum` + #[pyo3(name = "total_momentum")] + #[pyo3(signature = ())] + fn total_momentum(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.total_momentum()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + #[getter] + #[pyo3(name = "bodies")] + fn py_get_bodies(&self) -> PyResult> { Ok(self.inner.bodies.clone().into_iter().map(|__x| crate::generated::types::PyBody { inner: __x }).collect::>()) } + + #[getter] + #[pyo3(name = "dt")] + fn py_get_dt(&self) -> PyResult { Ok(self.inner.dt) } + + #[setter] + #[pyo3(name = "dt")] + fn py_set_dt(&mut self, v: f64) { self.inner.dt = v; } + + #[getter] + #[pyo3(name = "softening")] + fn py_get_softening(&self) -> PyResult { Ok(self.inner.softening) } + + #[setter] + #[pyo3(name = "softening")] + fn py_set_softening(&mut self, v: f64) { self.inner.softening = v; } + + #[getter] + #[pyo3(name = "time")] + fn py_get_time(&self) -> PyResult { Ok(self.inner.time) } + + #[setter] + #[pyo3(name = "time")] + fn py_set_time(&mut self, v: f64) { self.inner.time = v; } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// +/// Rust: `astrophysics::orbital_elements::OrbitalElements` +#[pyclass(name = "OrbitalElements", module = "numeria.astrophysics.orbital_elements", from_py_object)] +#[derive(Clone)] +pub struct PyOrbitalElements { pub inner: rust_physics_engine::astrophysics::orbital_elements::OrbitalElements } +#[pymethods] +impl PyOrbitalElements { + /// Builds a `OrbitalElements` from its fields. + #[new] + #[pyo3(signature = (semi_major_axis, eccentricity, inclination, longitude_ascending_node, argument_periapsis, true_anomaly))] + fn __new__(semi_major_axis: f64, eccentricity: f64, inclination: f64, longitude_ascending_node: f64, argument_periapsis: f64, true_anomaly: f64) -> Self { + + Self { inner: rust_physics_engine::astrophysics::orbital_elements::OrbitalElements { semi_major_axis: semi_major_axis, eccentricity: eccentricity, inclination: inclination, longitude_ascending_node: longitude_ascending_node, argument_periapsis: argument_periapsis, true_anomaly: true_anomaly } } + } + + /// Converts Cartesian state vectors (position, velocity) to Keplerian orbital elements for gravitational parameter μ. + /// + /// Rust: `astrophysics::orbital_elements::OrbitalElements::from_state_vectors` + #[pyo3(name = "from_state_vectors")] + #[staticmethod] + #[pyo3(signature = (position, velocity, mu))] + fn from_state_vectors(position: crate::generated::types::PyVec3Arg, velocity: crate::generated::types::PyVec3Arg, mu: f64) -> PyResult { + let position = position.0; + let velocity = velocity.0; + let __r = crate::runtime::guard(|| rust_physics_engine::astrophysics::orbital_elements::OrbitalElements::from_state_vectors(position, velocity, mu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyOrbitalElements { inner: __v }) + } + + /// Returns true if the orbit is gravitationally bound: e < 1. + /// + /// Rust: `astrophysics::orbital_elements::OrbitalElements::is_bound` + #[pyo3(name = "is_bound")] + #[pyo3(signature = ())] + fn is_bound(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_bound()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Computes the orbital period: T = 2π√(a³/μ). + /// + /// Rust: `astrophysics::orbital_elements::OrbitalElements::period` + #[pyo3(name = "period")] + #[pyo3(signature = (mu))] + fn period(&self, mu: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.period(mu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Returns the periapsis distance: r_p = a(1 - e). + /// + /// Rust: `astrophysics::orbital_elements::OrbitalElements::periapsis` + #[pyo3(name = "periapsis")] + #[pyo3(signature = ())] + fn periapsis(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.periapsis()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Returns the apoapsis distance if bound (e < 1): r_a = a(1 + e). Returns None for unbound orbits. + /// + /// Rust: `astrophysics::orbital_elements::OrbitalElements::apoapsis` + #[pyo3(name = "apoapsis")] + #[pyo3(signature = ())] + fn apoapsis(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.apoapsis()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) + } + + #[getter] + #[pyo3(name = "semi_major_axis")] + fn py_get_semi_major_axis(&self) -> PyResult { Ok(self.inner.semi_major_axis) } + + #[setter] + #[pyo3(name = "semi_major_axis")] + fn py_set_semi_major_axis(&mut self, v: f64) { self.inner.semi_major_axis = v; } + + #[getter] + #[pyo3(name = "eccentricity")] + fn py_get_eccentricity(&self) -> PyResult { Ok(self.inner.eccentricity) } + + #[setter] + #[pyo3(name = "eccentricity")] + fn py_set_eccentricity(&mut self, v: f64) { self.inner.eccentricity = v; } + + #[getter] + #[pyo3(name = "inclination")] + fn py_get_inclination(&self) -> PyResult { Ok(self.inner.inclination) } + + #[setter] + #[pyo3(name = "inclination")] + fn py_set_inclination(&mut self, v: f64) { self.inner.inclination = v; } + + #[getter] + #[pyo3(name = "longitude_ascending_node")] + fn py_get_longitude_ascending_node(&self) -> PyResult { Ok(self.inner.longitude_ascending_node) } + + #[setter] + #[pyo3(name = "longitude_ascending_node")] + fn py_set_longitude_ascending_node(&mut self, v: f64) { self.inner.longitude_ascending_node = v; } + + #[getter] + #[pyo3(name = "argument_periapsis")] + fn py_get_argument_periapsis(&self) -> PyResult { Ok(self.inner.argument_periapsis) } + + #[setter] + #[pyo3(name = "argument_periapsis")] + fn py_set_argument_periapsis(&mut self, v: f64) { self.inner.argument_periapsis = v; } + + #[getter] + #[pyo3(name = "true_anomaly")] + fn py_get_true_anomaly(&self) -> PyResult { Ok(self.inner.true_anomaly) } + + #[setter] + #[pyo3(name = "true_anomaly")] + fn py_set_true_anomaly(&mut self, v: f64) { self.inner.true_anomaly = v; } + + fn __repr__(&self) -> String { format!("OrbitalElements(semi_major_axis={:?}, eccentricity={:?}, inclination={:?}, longitude_ascending_node={:?}, argument_periapsis={:?}, true_anomaly={:?})", self.inner.semi_major_axis, self.inner.eccentricity, self.inner.inclination, self.inner.longitude_ascending_node, self.inner.argument_periapsis, self.inner.true_anomaly) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A `OrbitalElements` argument, or anything that can stand in for one. +pub struct PyOrbitalElementsArg(pub rust_physics_engine::astrophysics::orbital_elements::OrbitalElements); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyOrbitalElementsArg { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result { + if let Ok(__w) = obj.extract::() { + return Ok(PyOrbitalElementsArg(__w.inner)); + } + let __v = crate::runtime::coerce::floats_exact(obj, 6, "OrbitalElements")?; + Ok(PyOrbitalElementsArg(rust_physics_engine::astrophysics::orbital_elements::OrbitalElements { semi_major_axis: __v[0], eccentricity: __v[1], inclination: __v[2], longitude_ascending_node: __v[3], argument_periapsis: __v[4], true_anomaly: __v[5] })) + } +} + diff --git a/bindings/python/src/generated/types/audio.rs b/bindings/python/src/generated/types/audio.rs new file mode 100644 index 0000000..3898a90 --- /dev/null +++ b/bindings/python/src/generated/types/audio.rs @@ -0,0 +1,2954 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; + + +/// Frame-wise pitch detection method selector. +/// +/// Rust: `audio::analysis::PitchMethod` +#[pyclass(name = "PitchMethod", module = "numeria.audio.analysis", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyPitchMethod { + Yin, + Autocorrelation, + Cepstral, + Hps, + Mpm, +} +impl PyPitchMethod { + pub fn to_rust(&self) -> rust_physics_engine::audio::analysis::PitchMethod { match self { + Self::Yin => rust_physics_engine::audio::analysis::PitchMethod::Yin, + Self::Autocorrelation => rust_physics_engine::audio::analysis::PitchMethod::Autocorrelation, + Self::Cepstral => rust_physics_engine::audio::analysis::PitchMethod::Cepstral, + Self::Hps => rust_physics_engine::audio::analysis::PitchMethod::Hps, + Self::Mpm => rust_physics_engine::audio::analysis::PitchMethod::Mpm, + } } + pub fn from_rust(v: &rust_physics_engine::audio::analysis::PitchMethod) -> Self { match v { + rust_physics_engine::audio::analysis::PitchMethod::Yin => Self::Yin, + rust_physics_engine::audio::analysis::PitchMethod::Autocorrelation => Self::Autocorrelation, + rust_physics_engine::audio::analysis::PitchMethod::Cepstral => Self::Cepstral, + rust_physics_engine::audio::analysis::PitchMethod::Hps => Self::Hps, + rust_physics_engine::audio::analysis::PitchMethod::Mpm => Self::Mpm, + } } +} +#[pymethods] +impl PyPitchMethod { + fn __repr__(&self) -> &'static str { + match self { + Self::Yin => "PitchMethod.Yin", + Self::Autocorrelation => "PitchMethod.Autocorrelation", + Self::Cepstral => "PitchMethod.Cepstral", + Self::Hps => "PitchMethod.Hps", + Self::Mpm => "PitchMethod.Mpm", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// One frame of spectral descriptors. +/// +/// Rust: `audio::analysis::SpectralFeatures` +#[pyclass(name = "SpectralFeatures", module = "numeria.audio.analysis", from_py_object)] +#[derive(Clone)] +pub struct PySpectralFeatures { pub inner: rust_physics_engine::audio::analysis::SpectralFeatures } +#[pymethods] +impl PySpectralFeatures { + /// Builds a `SpectralFeatures` from its fields. + #[new] + #[pyo3(signature = (centroid, spread, skewness, kurtosis, rolloff85, flux, flatness, crest, slope, decrease, entropy))] + fn __new__(centroid: f64, spread: f64, skewness: f64, kurtosis: f64, rolloff85: f64, flux: f64, flatness: f64, crest: f64, slope: f64, decrease: f64, entropy: f64) -> Self { + + Self { inner: rust_physics_engine::audio::analysis::SpectralFeatures { centroid: centroid, spread: spread, skewness: skewness, kurtosis: kurtosis, rolloff85: rolloff85, flux: flux, flatness: flatness, crest: crest, slope: slope, decrease: decrease, entropy: entropy } } + } + + #[getter] + #[pyo3(name = "centroid")] + fn py_get_centroid(&self) -> PyResult { Ok(self.inner.centroid) } + + #[setter] + #[pyo3(name = "centroid")] + fn py_set_centroid(&mut self, v: f64) { self.inner.centroid = v; } + + #[getter] + #[pyo3(name = "spread")] + fn py_get_spread(&self) -> PyResult { Ok(self.inner.spread) } + + #[setter] + #[pyo3(name = "spread")] + fn py_set_spread(&mut self, v: f64) { self.inner.spread = v; } + + #[getter] + #[pyo3(name = "skewness")] + fn py_get_skewness(&self) -> PyResult { Ok(self.inner.skewness) } + + #[setter] + #[pyo3(name = "skewness")] + fn py_set_skewness(&mut self, v: f64) { self.inner.skewness = v; } + + #[getter] + #[pyo3(name = "kurtosis")] + fn py_get_kurtosis(&self) -> PyResult { Ok(self.inner.kurtosis) } + + #[setter] + #[pyo3(name = "kurtosis")] + fn py_set_kurtosis(&mut self, v: f64) { self.inner.kurtosis = v; } + + #[getter] + #[pyo3(name = "rolloff85")] + fn py_get_rolloff85(&self) -> PyResult { Ok(self.inner.rolloff85) } + + #[setter] + #[pyo3(name = "rolloff85")] + fn py_set_rolloff85(&mut self, v: f64) { self.inner.rolloff85 = v; } + + #[getter] + #[pyo3(name = "flux")] + fn py_get_flux(&self) -> PyResult { Ok(self.inner.flux) } + + #[setter] + #[pyo3(name = "flux")] + fn py_set_flux(&mut self, v: f64) { self.inner.flux = v; } + + #[getter] + #[pyo3(name = "flatness")] + fn py_get_flatness(&self) -> PyResult { Ok(self.inner.flatness) } + + #[setter] + #[pyo3(name = "flatness")] + fn py_set_flatness(&mut self, v: f64) { self.inner.flatness = v; } + + #[getter] + #[pyo3(name = "crest")] + fn py_get_crest(&self) -> PyResult { Ok(self.inner.crest) } + + #[setter] + #[pyo3(name = "crest")] + fn py_set_crest(&mut self, v: f64) { self.inner.crest = v; } + + #[getter] + #[pyo3(name = "slope")] + fn py_get_slope(&self) -> PyResult { Ok(self.inner.slope) } + + #[setter] + #[pyo3(name = "slope")] + fn py_set_slope(&mut self, v: f64) { self.inner.slope = v; } + + #[getter] + #[pyo3(name = "decrease")] + fn py_get_decrease(&self) -> PyResult { Ok(self.inner.decrease) } + + #[setter] + #[pyo3(name = "decrease")] + fn py_set_decrease(&mut self, v: f64) { self.inner.decrease = v; } + + #[getter] + #[pyo3(name = "entropy")] + fn py_get_entropy(&self) -> PyResult { Ok(self.inner.entropy) } + + #[setter] + #[pyo3(name = "entropy")] + fn py_set_entropy(&mut self, v: f64) { self.inner.entropy = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("SpectralFeatures", "SpectralFeatures", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Schroeder all-pass diffuser. +/// +/// Rust: `audio::effects::AllpassFilter` +#[pyclass(name = "AllpassFilter", module = "numeria.audio.effects")] +pub struct PyAllpassFilter { pub inner: rust_physics_engine::audio::effects::AllpassFilter } +#[pymethods] +impl PyAllpassFilter { + /// New all-pass of the given loop length. + /// + /// Rust: `audio::effects::AllpassFilter::new` + #[new] + #[pyo3(signature = (delay_samples, gain))] + fn __new__(delay_samples: usize, gain: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::effects::AllpassFilter::new(delay_samples, gain)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAllpassFilter { inner: __v }) + } + + /// One sample through the all-pass. + /// + /// Rust: `audio::effects::AllpassFilter::process` + #[pyo3(name = "process")] + #[pyo3(signature = (x))] + fn process(&mut self, x: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.process(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "gain")] + fn py_get_gain(&self) -> PyResult { Ok(self.inner.gain) } + + #[setter] + #[pyo3(name = "gain")] + fn py_set_gain(&mut self, v: f64) { self.inner.gain = v; } + + fn __repr__(&self) -> String { format!("AllpassFilter(gain={:?})", self.inner.gain) } +} + +/// Chorus: LFO-modulated fractional delay mixed with the dry path. +/// +/// Rust: `audio::effects::Chorus` +#[pyclass(name = "Chorus", module = "numeria.audio.effects")] +pub struct PyChorus { pub inner: rust_physics_engine::audio::effects::Chorus } +#[pymethods] +impl PyChorus { + /// Typical chorus (20 ms base, 5 ms depth, 0.8 Hz). + /// + /// Rust: `audio::effects::Chorus::new` + #[new] + #[pyo3(signature = (fs))] + fn __new__(fs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::effects::Chorus::new(fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyChorus { inner: __v }) + } + + /// One sample. + /// + /// Rust: `audio::effects::Chorus::process` + #[pyo3(name = "process")] + #[pyo3(signature = (x))] + fn process(&mut self, x: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.process(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "base_ms")] + fn py_get_base_ms(&self) -> PyResult { Ok(self.inner.base_ms) } + + #[setter] + #[pyo3(name = "base_ms")] + fn py_set_base_ms(&mut self, v: f64) { self.inner.base_ms = v; } + + #[getter] + #[pyo3(name = "depth_ms")] + fn py_get_depth_ms(&self) -> PyResult { Ok(self.inner.depth_ms) } + + #[setter] + #[pyo3(name = "depth_ms")] + fn py_set_depth_ms(&mut self, v: f64) { self.inner.depth_ms = v; } + + #[getter] + #[pyo3(name = "mix")] + fn py_get_mix(&self) -> PyResult { Ok(self.inner.mix) } + + #[setter] + #[pyo3(name = "mix")] + fn py_set_mix(&mut self, v: f64) { self.inner.mix = v; } + + fn __repr__(&self) -> String { format!("Chorus(base_ms={:?}, depth_ms={:?}, mix={:?})", self.inner.base_ms, self.inner.depth_ms, self.inner.mix) } +} + +/// Feedback comb filter with a one-pole damping low-pass in the loop. +/// +/// Rust: `audio::effects::CombFilter` +#[pyclass(name = "CombFilter", module = "numeria.audio.effects")] +pub struct PyCombFilter { pub inner: rust_physics_engine::audio::effects::CombFilter } +#[pymethods] +impl PyCombFilter { + /// New comb of the given loop length. + /// + /// Rust: `audio::effects::CombFilter::new` + #[new] + #[pyo3(signature = (delay_samples, feedback, damping))] + fn __new__(delay_samples: usize, feedback: f64, damping: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::effects::CombFilter::new(delay_samples, feedback, damping)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCombFilter { inner: __v }) + } + + /// One sample through the comb. + /// + /// Rust: `audio::effects::CombFilter::process` + #[pyo3(name = "process")] + #[pyo3(signature = (x))] + fn process(&mut self, x: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.process(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "feedback")] + fn py_get_feedback(&self) -> PyResult { Ok(self.inner.feedback) } + + #[setter] + #[pyo3(name = "feedback")] + fn py_set_feedback(&mut self, v: f64) { self.inner.feedback = v; } + + #[getter] + #[pyo3(name = "damping")] + fn py_get_damping(&self) -> PyResult { Ok(self.inner.damping) } + + #[setter] + #[pyo3(name = "damping")] + fn py_set_damping(&mut self, v: f64) { self.inner.damping = v; } + + fn __repr__(&self) -> String { format!("CombFilter(feedback={:?}, damping={:?})", self.inner.feedback, self.inner.damping) } +} + +/// Feed-forward compressor with soft knee and log-domain smoothing. +/// +/// Rust: `audio::effects::Compressor` +#[pyclass(name = "Compressor", module = "numeria.audio.effects")] +pub struct PyCompressor { pub inner: rust_physics_engine::audio::effects::Compressor } +#[pymethods] +impl PyCompressor { + /// New compressor. + /// + /// Rust: `audio::effects::Compressor::new` + #[new] + #[pyo3(signature = (threshold_db, ratio, attack_ms, release_ms, fs))] + fn __new__(threshold_db: f64, ratio: f64, attack_ms: f64, release_ms: f64, fs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::effects::Compressor::new(threshold_db, ratio, attack_ms, release_ms, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCompressor { inner: __v }) + } + + /// One sample with the input as its own detector. + /// + /// Rust: `audio::effects::Compressor::process` + #[pyo3(name = "process")] + #[pyo3(signature = (x))] + fn process(&mut self, x: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.process(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// One sample with an external detector (key) signal. + /// + /// Rust: `audio::effects::Compressor::sidechain` + #[pyo3(name = "sidechain")] + #[pyo3(signature = (x, key))] + fn sidechain(&mut self, x: f64, key: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.sidechain(x, key)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Gain reduction applied to the most recent sample (≤ 0 dB). + /// + /// Rust: `audio::effects::Compressor::gain_reduction_db` + #[pyo3(name = "gain_reduction_db")] + #[pyo3(signature = ())] + fn gain_reduction_db(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.gain_reduction_db()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "threshold_db")] + fn py_get_threshold_db(&self) -> PyResult { Ok(self.inner.threshold_db) } + + #[setter] + #[pyo3(name = "threshold_db")] + fn py_set_threshold_db(&mut self, v: f64) { self.inner.threshold_db = v; } + + #[getter] + #[pyo3(name = "ratio")] + fn py_get_ratio(&self) -> PyResult { Ok(self.inner.ratio) } + + #[setter] + #[pyo3(name = "ratio")] + fn py_set_ratio(&mut self, v: f64) { self.inner.ratio = v; } + + #[getter] + #[pyo3(name = "attack_ms")] + fn py_get_attack_ms(&self) -> PyResult { Ok(self.inner.attack_ms) } + + #[setter] + #[pyo3(name = "attack_ms")] + fn py_set_attack_ms(&mut self, v: f64) { self.inner.attack_ms = v; } + + #[getter] + #[pyo3(name = "release_ms")] + fn py_get_release_ms(&self) -> PyResult { Ok(self.inner.release_ms) } + + #[setter] + #[pyo3(name = "release_ms")] + fn py_set_release_ms(&mut self, v: f64) { self.inner.release_ms = v; } + + #[getter] + #[pyo3(name = "knee_db")] + fn py_get_knee_db(&self) -> PyResult { Ok(self.inner.knee_db) } + + #[setter] + #[pyo3(name = "knee_db")] + fn py_set_knee_db(&mut self, v: f64) { self.inner.knee_db = v; } + + #[getter] + #[pyo3(name = "makeup_db")] + fn py_get_makeup_db(&self) -> PyResult { Ok(self.inner.makeup_db) } + + #[setter] + #[pyo3(name = "makeup_db")] + fn py_set_makeup_db(&mut self, v: f64) { self.inner.makeup_db = v; } + + fn __repr__(&self) -> String { format!("Compressor(threshold_db={:?}, ratio={:?}, attack_ms={:?}, release_ms={:?}, knee_db={:?}, makeup_db={:?})", self.inner.threshold_db, self.inner.ratio, self.inner.attack_ms, self.inner.release_ms, self.inner.knee_db, self.inner.makeup_db) } +} + +/// De-esser: sibilance-band compressor (band-passed key). +/// +/// Rust: `audio::effects::DeEsser` +#[pyclass(name = "DeEsser", module = "numeria.audio.effects")] +pub struct PyDeEsser { pub inner: rust_physics_engine::audio::effects::DeEsser } +#[pymethods] +impl PyDeEsser { + /// De-esser keyed around `freq` (typically 5–8 kHz). + /// + /// Rust: `audio::effects::DeEsser::new` + #[new] + #[pyo3(signature = (freq, threshold_db, fs))] + fn __new__(freq: f64, threshold_db: f64, fs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::effects::DeEsser::new(freq, threshold_db, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyDeEsser { inner: __v }) + } + + /// One sample. + /// + /// Rust: `audio::effects::DeEsser::process` + #[pyo3(name = "process")] + #[pyo3(signature = (x))] + fn process(&mut self, x: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.process(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// Circular delay line with fractional read. +/// +/// Rust: `audio::effects::DelayLine` +#[pyclass(name = "DelayLine", module = "numeria.audio.effects")] +pub struct PyDelayLine { pub inner: rust_physics_engine::audio::effects::DelayLine } +#[pymethods] +impl PyDelayLine { + /// New line holding up to `max_samples`. + /// + /// Rust: `audio::effects::DelayLine::new` + #[new] + #[pyo3(signature = (max_samples))] + fn __new__(max_samples: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::effects::DelayLine::new(max_samples)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyDelayLine { inner: __v }) + } + + /// Push one input sample. + /// + /// Rust: `audio::effects::DelayLine::write` + #[pyo3(name = "write")] + #[pyo3(signature = (x))] + fn write(&mut self, x: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.write(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Read `delay_samples` behind the write head. + /// + /// Rust: `audio::effects::DelayLine::read` + #[pyo3(name = "read")] + #[pyo3(signature = (delay_samples))] + fn read(&self, delay_samples: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.read(delay_samples)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Linear-interpolated fractional read. + /// + /// Rust: `audio::effects::DelayLine::read_interp` + #[pyo3(name = "read_interp")] + #[pyo3(signature = (delay_frac))] + fn read_interp(&self, delay_frac: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.read_interp(delay_frac)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Alias of `Self::read`. + /// + /// Rust: `audio::effects::DelayLine::tap` + #[pyo3(name = "tap")] + #[pyo3(signature = (d))] + fn tap(&self, d: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.tap(d)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// A bank of peaking/shelf biquads. +/// +/// Rust: `audio::effects::Eq` +#[pyclass(name = "Eq", module = "numeria.audio.effects")] +pub struct PyEq { pub inner: rust_physics_engine::audio::effects::Eq } +#[pymethods] +impl PyEq { + /// Standard 10-band graphic EQ (31.25 Hz–16 kHz octaves), flat. + /// + /// Rust: `audio::effects::Eq::graphic_10_band` + #[pyo3(name = "graphic_10_band")] + #[staticmethod] + #[pyo3(signature = (fs))] + fn graphic_10_band(fs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::effects::Eq::graphic_10_band(fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyEq { inner: __v }) + } + + /// Parametric EQ from (freq, Q, gain dB) bands. + /// + /// Rust: `audio::effects::Eq::parametric` + #[pyo3(name = "parametric")] + #[staticmethod] + #[pyo3(signature = (bands, fs))] + fn parametric(bands: Vec<(f64, f64, f64)>, fs: f64) -> PyResult { + let bands = bands.into_iter().map(|__e| (__e.0, __e.1, __e.2)).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::audio::effects::Eq::parametric(&bands, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyEq { inner: __v }) + } + + /// One sample through every band. + /// + /// Rust: `audio::effects::Eq::process` + #[pyo3(name = "process")] + #[pyo3(signature = (x))] + fn process(&mut self, x: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.process(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Set band i's gain (graphic-EQ style, Q 1.41). + /// + /// Rust: `audio::effects::Eq::set_gain` + #[pyo3(name = "set_gain")] + #[pyo3(signature = (i, db))] + fn set_gain(&mut self, i: usize, db: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.set_gain(i, db)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + #[getter] + #[pyo3(name = "bands")] + fn py_get_bands(&self) -> PyResult> { Ok(self.inner.bands.clone().into_iter().map(|__x| crate::generated::types::PyBiquad { inner: __x }).collect::>()) } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// Harmonic exciter: high-passed signal through a soft shaper, mixed in. +/// +/// Rust: `audio::effects::Exciter` +#[pyclass(name = "Exciter", module = "numeria.audio.effects")] +pub struct PyExciter { pub inner: rust_physics_engine::audio::effects::Exciter } +#[pymethods] +impl PyExciter { + /// Exciter brightening content above `freq`. + /// + /// Rust: `audio::effects::Exciter::new` + #[new] + #[pyo3(signature = (freq, drive, mix, fs))] + fn __new__(freq: f64, drive: f64, mix: f64, fs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::effects::Exciter::new(freq, drive, mix, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyExciter { inner: __v }) + } + + /// One sample. + /// + /// Rust: `audio::effects::Exciter::process` + #[pyo3(name = "process")] + #[pyo3(signature = (x))] + fn process(&mut self, x: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.process(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "drive")] + fn py_get_drive(&self) -> PyResult { Ok(self.inner.drive) } + + #[setter] + #[pyo3(name = "drive")] + fn py_set_drive(&mut self, v: f64) { self.inner.drive = v; } + + #[getter] + #[pyo3(name = "mix")] + fn py_get_mix(&self) -> PyResult { Ok(self.inner.mix) } + + #[setter] + #[pyo3(name = "mix")] + fn py_set_mix(&mut self, v: f64) { self.inner.mix = v; } + + fn __repr__(&self) -> String { format!("Exciter(drive={:?}, mix={:?})", self.inner.drive, self.inner.mix) } +} + +/// Downward expander (gentler than a gate). +/// +/// Rust: `audio::effects::Expander` +#[pyclass(name = "Expander", module = "numeria.audio.effects")] +pub struct PyExpander { pub inner: rust_physics_engine::audio::effects::Expander } +#[pymethods] +impl PyExpander { + /// Expander below the threshold with the given ratio. + /// + /// Rust: `audio::effects::Expander::new` + #[new] + #[pyo3(signature = (threshold_db, ratio, fs))] + fn __new__(threshold_db: f64, ratio: f64, fs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::effects::Expander::new(threshold_db, ratio, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyExpander { inner: __v }) + } + + /// One sample. + /// + /// Rust: `audio::effects::Expander::process` + #[pyo3(name = "process")] + #[pyo3(signature = (x))] + fn process(&mut self, x: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.process(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "threshold_db")] + fn py_get_threshold_db(&self) -> PyResult { Ok(self.inner.threshold_db) } + + #[setter] + #[pyo3(name = "threshold_db")] + fn py_set_threshold_db(&mut self, v: f64) { self.inner.threshold_db = v; } + + #[getter] + #[pyo3(name = "ratio")] + fn py_get_ratio(&self) -> PyResult { Ok(self.inner.ratio) } + + #[setter] + #[pyo3(name = "ratio")] + fn py_set_ratio(&mut self, v: f64) { self.inner.ratio = v; } + + fn __repr__(&self) -> String { format!("Expander(threshold_db={:?}, ratio={:?})", self.inner.threshold_db, self.inner.ratio) } +} + +/// Feedback delay network reverb with an orthogonal mixing matrix. +/// +/// Rust: `audio::effects::Fdn` +#[pyclass(name = "Fdn", module = "numeria.audio.effects")] +pub struct PyFdn { pub inner: rust_physics_engine::audio::effects::Fdn } +#[pymethods] +impl PyFdn { + /// New FDN with explicit delays (samples) and an identity-free + /// Householder mixing matrix. + /// + /// Rust: `audio::effects::Fdn::new` + #[new] + #[pyo3(signature = (n, delay_samples, fs))] + fn __new__(n: usize, delay_samples: Vec, fs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::effects::Fdn::new(n, &delay_samples, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyFdn { inner: __v }) + } + + /// Householder reflection H = I − (2/n)·11ᵀ (orthogonal, lossless). + /// + /// Rust: `audio::effects::Fdn::householder_matrix` + #[pyo3(name = "householder_matrix")] + #[staticmethod] + #[pyo3(signature = (n))] + fn householder_matrix(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::effects::Fdn::householder_matrix(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) + } + + /// Hadamard mixing matrix (n must be a power of two), scaled to be + /// orthogonal. + /// + /// Panics: + /// Panics unless n is a power of two. + /// + /// Rust: `audio::effects::Fdn::hadamard_matrix` + #[pyo3(name = "hadamard_matrix")] + #[staticmethod] + #[pyo3(signature = (n))] + fn hadamard_matrix(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::effects::Fdn::hadamard_matrix(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) + } + + /// Set a frequency-dependent decay: RT60 at low frequencies and a + /// (shorter) RT60 above ~2 kHz via one-pole shelving in each line. + /// + /// Rust: `audio::effects::Fdn::set_rt60` + #[pyo3(name = "set_rt60")] + #[pyo3(signature = (t60_low, t60_high, fs))] + fn set_rt60(&mut self, t60_low: f64, t60_high: f64, fs: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.set_rt60(t60_low, t60_high, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// One (wet) sample. + /// + /// Rust: `audio::effects::Fdn::process` + #[pyo3(name = "process")] + #[pyo3(signature = (x))] + fn process(&mut self, x: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.process(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// One sample to a decorrelated stereo pair (alternating-sign taps). + /// + /// Rust: `audio::effects::Fdn::process_stereo` + #[pyo3(name = "process_stereo")] + #[pyo3(signature = (x))] + fn process_stereo(&mut self, x: f64) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| self.inner.process_stereo(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) + } + + #[getter] + #[pyo3(name = "matrix")] + fn py_get_matrix(&self) -> PyResult { Ok(crate::generated::types::PyMatrix { inner: self.inner.matrix.clone() }) } + + #[getter] + #[pyo3(name = "gains")] + fn py_get_gains(&self) -> PyResult> { Ok(self.inner.gains.clone()) } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// Flanger: short modulated delay with feedback. +/// +/// Rust: `audio::effects::Flanger` +#[pyclass(name = "Flanger", module = "numeria.audio.effects")] +pub struct PyFlanger { pub inner: rust_physics_engine::audio::effects::Flanger } +#[pymethods] +impl PyFlanger { + /// Typical flanger (0–5 ms sweep at 0.25 Hz). + /// + /// Rust: `audio::effects::Flanger::new` + #[new] + #[pyo3(signature = (fs))] + fn __new__(fs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::effects::Flanger::new(fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyFlanger { inner: __v }) + } + + /// One sample. + /// + /// Rust: `audio::effects::Flanger::process` + #[pyo3(name = "process")] + #[pyo3(signature = (x))] + fn process(&mut self, x: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.process(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "depth_ms")] + fn py_get_depth_ms(&self) -> PyResult { Ok(self.inner.depth_ms) } + + #[setter] + #[pyo3(name = "depth_ms")] + fn py_set_depth_ms(&mut self, v: f64) { self.inner.depth_ms = v; } + + #[getter] + #[pyo3(name = "feedback")] + fn py_get_feedback(&self) -> PyResult { Ok(self.inner.feedback) } + + #[setter] + #[pyo3(name = "feedback")] + fn py_set_feedback(&mut self, v: f64) { self.inner.feedback = v; } + + #[getter] + #[pyo3(name = "mix")] + fn py_get_mix(&self) -> PyResult { Ok(self.inner.mix) } + + #[setter] + #[pyo3(name = "mix")] + fn py_set_mix(&mut self, v: f64) { self.inner.mix = v; } + + fn __repr__(&self) -> String { format!("Flanger(depth_ms={:?}, feedback={:?}, mix={:?})", self.inner.depth_ms, self.inner.feedback, self.inner.mix) } +} + +/// Jezar's Freeverb topology: 8 combs + 4 all-passes per channel with +/// a fixed stereo spread. +/// +/// Rust: `audio::effects::Freeverb` +#[pyclass(name = "Freeverb", module = "numeria.audio.effects")] +pub struct PyFreeverb { pub inner: rust_physics_engine::audio::effects::Freeverb } +#[pymethods] +impl PyFreeverb { + /// New Freeverb; the classic tunings are for 44.1 kHz and are + /// scaled to the requested rate. + /// + /// Rust: `audio::effects::Freeverb::new` + #[new] + #[pyo3(signature = (fs))] + fn __new__(fs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::effects::Freeverb::new(fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyFreeverb { inner: __v }) + } + + /// One input sample to a stereo pair (wet only). + /// + /// Rust: `audio::effects::Freeverb::process` + #[pyo3(name = "process")] + #[pyo3(signature = (x))] + fn process(&mut self, x: f64) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| self.inner.process(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) + } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// Brickwall limiter with lookahead. +/// +/// Rust: `audio::effects::Limiter` +#[pyclass(name = "Limiter", module = "numeria.audio.effects")] +pub struct PyEffectsLimiter { pub inner: rust_physics_engine::audio::effects::Limiter } +#[pymethods] +impl PyEffectsLimiter { + /// Limiter with the given ceiling (linear) and lookahead/release. + /// + /// Rust: `audio::effects::Limiter::new` + #[new] + #[pyo3(signature = (ceiling, lookahead_ms, release_ms, fs))] + fn __new__(ceiling: f64, lookahead_ms: f64, release_ms: f64, fs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::effects::Limiter::new(ceiling, lookahead_ms, release_ms, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyEffectsLimiter { inner: __v }) + } + + /// One sample; output never exceeds the ceiling. + /// + /// Rust: `audio::effects::Limiter::process` + #[pyo3(name = "process")] + #[pyo3(signature = (x))] + fn process(&mut self, x: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.process(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "ceiling")] + fn py_get_ceiling(&self) -> PyResult { Ok(self.inner.ceiling) } + + #[setter] + #[pyo3(name = "ceiling")] + fn py_set_ceiling(&mut self, v: f64) { self.inner.ceiling = v; } + + fn __repr__(&self) -> String { format!("Limiter(ceiling={:?})", self.inner.ceiling) } +} + +/// Downward noise gate. +/// +/// Rust: `audio::effects::NoiseGate` +#[pyclass(name = "NoiseGate", module = "numeria.audio.effects")] +pub struct PyNoiseGate { pub inner: rust_physics_engine::audio::effects::NoiseGate } +#[pymethods] +impl PyNoiseGate { + /// Gate at a linear threshold. + /// + /// Rust: `audio::effects::NoiseGate::new` + #[new] + #[pyo3(signature = (threshold, attack_ms, release_ms, fs))] + fn __new__(threshold: f64, attack_ms: f64, release_ms: f64, fs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::effects::NoiseGate::new(threshold, attack_ms, release_ms, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyNoiseGate { inner: __v }) + } + + /// One sample. + /// + /// Rust: `audio::effects::NoiseGate::process` + #[pyo3(name = "process")] + #[pyo3(signature = (x))] + fn process(&mut self, x: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.process(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "threshold")] + fn py_get_threshold(&self) -> PyResult { Ok(self.inner.threshold) } + + #[setter] + #[pyo3(name = "threshold")] + fn py_set_threshold(&mut self, v: f64) { self.inner.threshold = v; } + + #[getter] + #[pyo3(name = "attack_ms")] + fn py_get_attack_ms(&self) -> PyResult { Ok(self.inner.attack_ms) } + + #[setter] + #[pyo3(name = "attack_ms")] + fn py_set_attack_ms(&mut self, v: f64) { self.inner.attack_ms = v; } + + #[getter] + #[pyo3(name = "release_ms")] + fn py_get_release_ms(&self) -> PyResult { Ok(self.inner.release_ms) } + + #[setter] + #[pyo3(name = "release_ms")] + fn py_set_release_ms(&mut self, v: f64) { self.inner.release_ms = v; } + + fn __repr__(&self) -> String { format!("NoiseGate(threshold={:?}, attack_ms={:?}, release_ms={:?})", self.inner.threshold, self.inner.attack_ms, self.inner.release_ms) } +} + +/// Uniform partitioned (overlap-add, frequency-domain) convolver for +/// streaming long impulse responses. +/// +/// Rust: `audio::effects::PartitionedConvolver` +#[pyclass(name = "PartitionedConvolver", module = "numeria.audio.effects")] +pub struct PyPartitionedConvolver { pub inner: rust_physics_engine::audio::effects::PartitionedConvolver } +#[pymethods] +impl PyPartitionedConvolver { + /// Partition an impulse response into `block_size` chunks. + /// + /// Panics: + /// Panics if the IR is empty or block_size is zero. + /// + /// Rust: `audio::effects::PartitionedConvolver::new` + #[new] + #[pyo3(signature = (ir, block_size))] + fn __new__(ir: Vec, block_size: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::effects::PartitionedConvolver::new(&ir, block_size)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPartitionedConvolver { inner: __v }) + } + + /// Convolve one input block (length ≤ block size); returns exactly + /// one block of output. + /// + /// Rust: `audio::effects::PartitionedConvolver::process_block` + #[pyo3(name = "process_block")] + #[pyo3(signature = (x))] + fn process_block<'py>(&mut self, py: Python<'py>, x: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.process_block(&x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// Phaser: cascaded LFO-swept all-pass biquads. +/// +/// Rust: `audio::effects::Phaser` +#[pyclass(name = "Phaser", module = "numeria.audio.effects")] +pub struct PyPhaser { pub inner: rust_physics_engine::audio::effects::Phaser } +#[pymethods] +impl PyPhaser { + /// 4-stage phaser sweeping 300–1500 Hz at 0.5 Hz. + /// + /// Rust: `audio::effects::Phaser::new` + #[new] + #[pyo3(signature = (fs))] + fn __new__(fs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::effects::Phaser::new(fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPhaser { inner: __v }) + } + + /// One sample. + /// + /// Rust: `audio::effects::Phaser::process` + #[pyo3(name = "process")] + #[pyo3(signature = (x))] + fn process(&mut self, x: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.process(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "mix")] + fn py_get_mix(&self) -> PyResult { Ok(self.inner.mix) } + + #[setter] + #[pyo3(name = "mix")] + fn py_set_mix(&mut self, v: f64) { self.inner.mix = v; } + + fn __repr__(&self) -> String { format!("Phaser(mix={:?})", self.inner.mix) } +} + +/// Classic Schroeder reverberator: four parallel combs into two series +/// all-passes. +/// +/// Rust: `audio::effects::SchroederReverb` +#[pyclass(name = "SchroederReverb", module = "numeria.audio.effects")] +pub struct PySchroederReverb { pub inner: rust_physics_engine::audio::effects::SchroederReverb } +#[pymethods] +impl PySchroederReverb { + /// New reverb at the given sample rate (RT60 initially 1 s). + /// + /// Rust: `audio::effects::SchroederReverb::new` + #[new] + #[pyo3(signature = (fs))] + fn __new__(fs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::effects::SchroederReverb::new(fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySchroederReverb { inner: __v }) + } + + /// Set the decay time: comb feedback g = 10^(−3·delay/RT60). + /// + /// Rust: `audio::effects::SchroederReverb::set_rt60` + #[pyo3(name = "set_rt60")] + #[pyo3(signature = (t))] + fn set_rt60(&mut self, t: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.set_rt60(t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Set high-frequency damping (0..1) inside the comb loops. + /// + /// Rust: `audio::effects::SchroederReverb::set_damping` + #[pyo3(name = "set_damping")] + #[pyo3(signature = (d))] + fn set_damping(&mut self, d: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.set_damping(d)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// One (wet-only) sample. + /// + /// Rust: `audio::effects::SchroederReverb::process` + #[pyo3(name = "process")] + #[pyo3(signature = (x))] + fn process(&mut self, x: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.process(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// Mid/side stereo widener. +/// +/// Rust: `audio::effects::StereoWidener` +#[pyclass(name = "StereoWidener", module = "numeria.audio.effects")] +pub struct PyStereoWidener { pub inner: rust_physics_engine::audio::effects::StereoWidener } +#[pymethods] +impl PyStereoWidener { + /// Builds a `StereoWidener` from its fields. + #[new] + #[pyo3(signature = (width))] + fn __new__(width: f64) -> Self { + + Self { inner: rust_physics_engine::audio::effects::StereoWidener { width: width } } + } + + /// One stereo frame: width 1 = unchanged, > 1 wider, 0 = mono. + /// + /// Rust: `audio::effects::StereoWidener::process` + #[pyo3(name = "process")] + #[pyo3(signature = (l, r))] + fn process(&self, l: f64, r: f64) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| self.inner.process(l, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) + } + + #[getter] + #[pyo3(name = "width")] + fn py_get_width(&self) -> PyResult { Ok(self.inner.width) } + + #[setter] + #[pyo3(name = "width")] + fn py_set_width(&mut self, v: f64) { self.inner.width = v; } + + fn __repr__(&self) -> String { format!("StereoWidener(width={:?})", self.inner.width) } +} + +/// Tremolo (amplitude modulation by an LFO). +/// +/// Rust: `audio::effects::Tremolo` +#[pyclass(name = "Tremolo", module = "numeria.audio.effects")] +pub struct PyTremolo { pub inner: rust_physics_engine::audio::effects::Tremolo } +#[pymethods] +impl PyTremolo { + /// Tremolo at the given rate/depth. + /// + /// Rust: `audio::effects::Tremolo::new` + #[new] + #[pyo3(signature = (rate_hz, depth, fs))] + fn __new__(rate_hz: f64, depth: f64, fs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::effects::Tremolo::new(rate_hz, depth, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTremolo { inner: __v }) + } + + /// One sample. + /// + /// Rust: `audio::effects::Tremolo::process` + #[pyo3(name = "process")] + #[pyo3(signature = (x))] + fn process(&mut self, x: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.process(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "depth")] + fn py_get_depth(&self) -> PyResult { Ok(self.inner.depth) } + + #[setter] + #[pyo3(name = "depth")] + fn py_set_depth(&mut self, v: f64) { self.inner.depth = v; } + + fn __repr__(&self) -> String { format!("Tremolo(depth={:?})", self.inner.depth) } +} + +/// Vibrato (pitch modulation via modulated delay). +/// +/// Rust: `audio::effects::Vibrato` +#[pyclass(name = "Vibrato", module = "numeria.audio.effects")] +pub struct PyVibrato { pub inner: rust_physics_engine::audio::effects::Vibrato } +#[pymethods] +impl PyVibrato { + /// Vibrato at the given rate and delay depth. + /// + /// Rust: `audio::effects::Vibrato::new` + #[new] + #[pyo3(signature = (rate_hz, depth_ms, fs))] + fn __new__(rate_hz: f64, depth_ms: f64, fs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::effects::Vibrato::new(rate_hz, depth_ms, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVibrato { inner: __v }) + } + + /// One sample (wet only). + /// + /// Rust: `audio::effects::Vibrato::process` + #[pyo3(name = "process")] + #[pyo3(signature = (x))] + fn process(&mut self, x: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.process(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "depth_ms")] + fn py_get_depth_ms(&self) -> PyResult { Ok(self.inner.depth_ms) } + + #[setter] + #[pyo3(name = "depth_ms")] + fn py_set_depth_ms(&mut self, v: f64) { self.inner.depth_ms = v; } + + fn __repr__(&self) -> String { format!("Vibrato(depth_ms={:?})", self.inner.depth_ms) } +} + +/// Linear (optionally exponential-curved) ADSR envelope; times in +/// seconds, sustain as a level in \[0, 1\]. +/// +/// Rust: `audio::envelope::Adsr` +#[pyclass(name = "Adsr", module = "numeria.audio.envelope")] +pub struct PyAdsr { pub inner: rust_physics_engine::audio::envelope::Adsr } +#[pymethods] +impl PyAdsr { + /// New idle envelope. + /// + /// Rust: `audio::envelope::Adsr::new` + #[new] + #[pyo3(signature = (attack, decay, sustain, release, fs))] + fn __new__(attack: f64, decay: f64, sustain: f64, release: f64, fs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::envelope::Adsr::new(attack, decay, sustain, release, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAdsr { inner: __v }) + } + + /// Start the attack from the current level. + /// + /// Rust: `audio::envelope::Adsr::gate_on` + #[pyo3(name = "gate_on")] + #[pyo3(signature = ())] + fn gate_on(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.gate_on()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Enter the release phase. + /// + /// Rust: `audio::envelope::Adsr::gate_off` + #[pyo3(name = "gate_off")] + #[pyo3(signature = ())] + fn gate_off(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.gate_off()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Use exponential (one-pole style) segment shapes instead of + /// linear ramps. + /// + /// Rust: `audio::envelope::Adsr::set_curve` + #[pyo3(name = "set_curve")] + #[pyo3(signature = (exp))] + fn set_curve(&mut self, exp: bool) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.set_curve(exp)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Envelope value for the next sample. + /// + /// Rust: `audio::envelope::Adsr::next` + #[pyo3(name = "next")] + #[pyo3(signature = ())] + fn next(&mut self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.next()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// True while the envelope is producing signal. + /// + /// Rust: `audio::envelope::Adsr::is_active` + #[pyo3(name = "is_active")] + #[pyo3(signature = ())] + fn is_active(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_active()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "attack")] + fn py_get_attack(&self) -> PyResult { Ok(self.inner.attack) } + + #[setter] + #[pyo3(name = "attack")] + fn py_set_attack(&mut self, v: f64) { self.inner.attack = v; } + + #[getter] + #[pyo3(name = "decay")] + fn py_get_decay(&self) -> PyResult { Ok(self.inner.decay) } + + #[setter] + #[pyo3(name = "decay")] + fn py_set_decay(&mut self, v: f64) { self.inner.decay = v; } + + #[getter] + #[pyo3(name = "sustain")] + fn py_get_sustain(&self) -> PyResult { Ok(self.inner.sustain) } + + #[setter] + #[pyo3(name = "sustain")] + fn py_set_sustain(&mut self, v: f64) { self.inner.sustain = v; } + + #[getter] + #[pyo3(name = "release")] + fn py_get_release(&self) -> PyResult { Ok(self.inner.release) } + + #[setter] + #[pyo3(name = "release")] + fn py_set_release(&mut self, v: f64) { self.inner.release = v; } + + fn __repr__(&self) -> String { format!("Adsr(attack={:?}, decay={:?}, sustain={:?}, release={:?})", self.inner.attack, self.inner.decay, self.inner.sustain, self.inner.release) } +} + +/// Exponential ADSR driven by RC time constants (τ per segment). +/// +/// Rust: `audio::envelope::AdsrExp` +#[pyclass(name = "AdsrExp", module = "numeria.audio.envelope")] +pub struct PyAdsrExp { pub inner: rust_physics_engine::audio::envelope::AdsrExp } +#[pymethods] +impl PyAdsrExp { + /// New idle exponential envelope. + /// + /// Rust: `audio::envelope::AdsrExp::new` + #[new] + #[pyo3(signature = (attack_tau, decay_tau, sustain, release_tau, fs))] + fn __new__(attack_tau: f64, decay_tau: f64, sustain: f64, release_tau: f64, fs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::envelope::AdsrExp::new(attack_tau, decay_tau, sustain, release_tau, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAdsrExp { inner: __v }) + } + + /// Trigger the attack. + /// + /// Rust: `audio::envelope::AdsrExp::gate_on` + #[pyo3(name = "gate_on")] + #[pyo3(signature = ())] + fn gate_on(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.gate_on()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Begin the release. + /// + /// Rust: `audio::envelope::AdsrExp::gate_off` + #[pyo3(name = "gate_off")] + #[pyo3(signature = ())] + fn gate_off(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.gate_off()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Next envelope value. + /// + /// Rust: `audio::envelope::AdsrExp::next` + #[pyo3(name = "next")] + #[pyo3(signature = ())] + fn next(&mut self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.next()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// True while producing signal. + /// + /// Rust: `audio::envelope::AdsrExp::is_active` + #[pyo3(name = "is_active")] + #[pyo3(signature = ())] + fn is_active(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_active()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "attack_tau")] + fn py_get_attack_tau(&self) -> PyResult { Ok(self.inner.attack_tau) } + + #[setter] + #[pyo3(name = "attack_tau")] + fn py_set_attack_tau(&mut self, v: f64) { self.inner.attack_tau = v; } + + #[getter] + #[pyo3(name = "decay_tau")] + fn py_get_decay_tau(&self) -> PyResult { Ok(self.inner.decay_tau) } + + #[setter] + #[pyo3(name = "decay_tau")] + fn py_set_decay_tau(&mut self, v: f64) { self.inner.decay_tau = v; } + + #[getter] + #[pyo3(name = "sustain")] + fn py_get_sustain(&self) -> PyResult { Ok(self.inner.sustain) } + + #[setter] + #[pyo3(name = "sustain")] + fn py_set_sustain(&mut self, v: f64) { self.inner.sustain = v; } + + #[getter] + #[pyo3(name = "release_tau")] + fn py_get_release_tau(&self) -> PyResult { Ok(self.inner.release_tau) } + + #[setter] + #[pyo3(name = "release_tau")] + fn py_set_release_tau(&mut self, v: f64) { self.inner.release_tau = v; } + + fn __repr__(&self) -> String { format!("AdsrExp(attack_tau={:?}, decay_tau={:?}, sustain={:?}, release_tau={:?})", self.inner.attack_tau, self.inner.decay_tau, self.inner.sustain, self.inner.release_tau) } +} + +/// Simple linear attack-release envelope (a one-shot AD when the gate +/// is released immediately). +/// +/// Rust: `audio::envelope::Ar` +#[pyclass(name = "Ar", module = "numeria.audio.envelope")] +pub struct PyAr { pub inner: rust_physics_engine::audio::envelope::Ar } +#[pymethods] +impl PyAr { + /// New idle attack-release envelope. + /// + /// Rust: `audio::envelope::Ar::new` + #[new] + #[pyo3(signature = (attack, release, fs))] + fn __new__(attack: f64, release: f64, fs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::envelope::Ar::new(attack, release, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAr { inner: __v }) + } + + /// Trigger the attack. + /// + /// Rust: `audio::envelope::Ar::gate_on` + #[pyo3(name = "gate_on")] + #[pyo3(signature = ())] + fn gate_on(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.gate_on()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Begin the release. + /// + /// Rust: `audio::envelope::Ar::gate_off` + #[pyo3(name = "gate_off")] + #[pyo3(signature = ())] + fn gate_off(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.gate_off()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Next envelope value. + /// + /// Rust: `audio::envelope::Ar::next` + #[pyo3(name = "next")] + #[pyo3(signature = ())] + fn next(&mut self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.next()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// True while producing signal. + /// + /// Rust: `audio::envelope::Ar::is_active` + #[pyo3(name = "is_active")] + #[pyo3(signature = ())] + fn is_active(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_active()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "attack")] + fn py_get_attack(&self) -> PyResult { Ok(self.inner.attack) } + + #[setter] + #[pyo3(name = "attack")] + fn py_set_attack(&mut self, v: f64) { self.inner.attack = v; } + + #[getter] + #[pyo3(name = "release")] + fn py_get_release(&self) -> PyResult { Ok(self.inner.release) } + + #[setter] + #[pyo3(name = "release")] + fn py_set_release(&mut self, v: f64) { self.inner.release = v; } + + fn __repr__(&self) -> String { format!("Ar(attack={:?}, release={:?})", self.inner.attack, self.inner.release) } +} + +/// Fade curve shapes. +/// +/// Rust: `audio::envelope::FadeShape` +#[pyclass(name = "FadeShape", module = "numeria.audio.envelope", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyFadeShape { + Linear, + EqualPower, + Exponential, + SCurve, +} +impl PyFadeShape { + pub fn to_rust(&self) -> rust_physics_engine::audio::envelope::FadeShape { match self { + Self::Linear => rust_physics_engine::audio::envelope::FadeShape::Linear, + Self::EqualPower => rust_physics_engine::audio::envelope::FadeShape::EqualPower, + Self::Exponential => rust_physics_engine::audio::envelope::FadeShape::Exponential, + Self::SCurve => rust_physics_engine::audio::envelope::FadeShape::SCurve, + } } + pub fn from_rust(v: &rust_physics_engine::audio::envelope::FadeShape) -> Self { match v { + rust_physics_engine::audio::envelope::FadeShape::Linear => Self::Linear, + rust_physics_engine::audio::envelope::FadeShape::EqualPower => Self::EqualPower, + rust_physics_engine::audio::envelope::FadeShape::Exponential => Self::Exponential, + rust_physics_engine::audio::envelope::FadeShape::SCurve => Self::SCurve, + } } +} +#[pymethods] +impl PyFadeShape { + fn __repr__(&self) -> &'static str { + match self { + Self::Linear => "FadeShape.Linear", + Self::EqualPower => "FadeShape.EqualPower", + Self::Exponential => "FadeShape.Exponential", + Self::SCurve => "FadeShape.SCurve", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Low-frequency oscillator: scaled/offset wrapper over `Oscillator`. +/// +/// Rust: `audio::envelope::Lfo` +#[pyclass(name = "Lfo", module = "numeria.audio.envelope")] +pub struct PyLfo { pub inner: rust_physics_engine::audio::envelope::Lfo } +#[pymethods] +impl PyLfo { + /// Next LFO value offset + depth·osc. + /// + /// Rust: `audio::envelope::Lfo::next` + #[pyo3(name = "next")] + #[pyo3(signature = ())] + fn next(&mut self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.next()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Reset the LFO phase. + /// + /// Rust: `audio::envelope::Lfo::sync` + #[pyo3(name = "sync")] + #[pyo3(signature = ())] + fn sync(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.sync()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + #[getter] + #[pyo3(name = "depth")] + fn py_get_depth(&self) -> PyResult { Ok(self.inner.depth) } + + #[setter] + #[pyo3(name = "depth")] + fn py_set_depth(&mut self, v: f64) { self.inner.depth = v; } + + #[getter] + #[pyo3(name = "offset")] + fn py_get_offset(&self) -> PyResult { Ok(self.inner.offset) } + + #[setter] + #[pyo3(name = "offset")] + fn py_set_offset(&mut self, v: f64) { self.inner.offset = v; } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// Noise spectra for `NoiseGen`. +/// +/// Rust: `audio::oscillators::NoiseColor` +#[pyclass(name = "NoiseColor", module = "numeria.audio.oscillators", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyNoiseColor { + White, + Pink, + Brown, + Blue, + Violet, + Grey, +} +impl PyNoiseColor { + pub fn to_rust(&self) -> rust_physics_engine::audio::oscillators::NoiseColor { match self { + Self::White => rust_physics_engine::audio::oscillators::NoiseColor::White, + Self::Pink => rust_physics_engine::audio::oscillators::NoiseColor::Pink, + Self::Brown => rust_physics_engine::audio::oscillators::NoiseColor::Brown, + Self::Blue => rust_physics_engine::audio::oscillators::NoiseColor::Blue, + Self::Violet => rust_physics_engine::audio::oscillators::NoiseColor::Violet, + Self::Grey => rust_physics_engine::audio::oscillators::NoiseColor::Grey, + } } + pub fn from_rust(v: &rust_physics_engine::audio::oscillators::NoiseColor) -> Self { match v { + rust_physics_engine::audio::oscillators::NoiseColor::White => Self::White, + rust_physics_engine::audio::oscillators::NoiseColor::Pink => Self::Pink, + rust_physics_engine::audio::oscillators::NoiseColor::Brown => Self::Brown, + rust_physics_engine::audio::oscillators::NoiseColor::Blue => Self::Blue, + rust_physics_engine::audio::oscillators::NoiseColor::Violet => Self::Violet, + rust_physics_engine::audio::oscillators::NoiseColor::Grey => Self::Grey, + } } +} +#[pymethods] +impl PyNoiseColor { + fn __repr__(&self) -> &'static str { + match self { + Self::White => "NoiseColor.White", + Self::Pink => "NoiseColor.Pink", + Self::Brown => "NoiseColor.Brown", + Self::Blue => "NoiseColor.Blue", + Self::Violet => "NoiseColor.Violet", + Self::Grey => "NoiseColor.Grey", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Deterministic colored-noise generator. +/// +/// Rust: `audio::oscillators::NoiseGen` +#[pyclass(name = "NoiseGen", module = "numeria.audio.oscillators")] +pub struct PyNoiseGen { pub inner: rust_physics_engine::audio::oscillators::NoiseGen } +#[pymethods] +impl PyNoiseGen { + /// Seeded generator of the requested color. + /// + /// Rust: `audio::oscillators::NoiseGen::new` + #[new] + #[pyo3(signature = (seed, color))] + fn __new__(seed: u64, color: crate::generated::types::PyNoiseColor) -> PyResult { + let color = color.to_rust(); + let __r = crate::runtime::guard(|| rust_physics_engine::audio::oscillators::NoiseGen::new(seed, color)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyNoiseGen { inner: __v }) + } + + /// Next sample, roughly unit peak scale. + /// + /// Rust: `audio::oscillators::NoiseGen::next` + #[pyo3(name = "next")] + #[pyo3(signature = ())] + fn next(&mut self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.next()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// Phase-accumulating audio oscillator (PolyBLEP for saw/square, +/// polyBLAMP triangle, seeded noise, optional wavetables). +/// +/// Rust: `audio::oscillators::Oscillator` +#[pyclass(name = "Oscillator", module = "numeria.audio.oscillators")] +pub struct PyOscillator { pub inner: rust_physics_engine::audio::oscillators::Oscillator } +#[pymethods] +impl PyOscillator { + /// New oscillator at the given frequency and sample rate. + /// + /// Rust: `audio::oscillators::Oscillator::new` + #[new] + #[pyo3(signature = (kind, freq, fs))] + fn __new__(kind: crate::generated::types::PyWaveform, freq: f64, fs: f64) -> PyResult { + let kind = kind.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::audio::oscillators::Oscillator::new(kind, freq, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyOscillator { inner: __v }) + } + + /// Next sample. + /// + /// Rust: `audio::oscillators::Oscillator::next` + #[pyo3(name = "next")] + #[pyo3(signature = ())] + fn next(&mut self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.next()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Change frequency (phase-continuous). + /// + /// Rust: `audio::oscillators::Oscillator::set_freq` + #[pyo3(name = "set_freq")] + #[pyo3(signature = (freq))] + fn set_freq(&mut self, freq: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.set_freq(freq)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Jump to an absolute phase in \[0, 1). + /// + /// Rust: `audio::oscillators::Oscillator::set_phase` + #[pyo3(name = "set_phase")] + #[pyo3(signature = (phase))] + fn set_phase(&mut self, phase: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.set_phase(phase)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Render a block of n samples. + /// + /// Rust: `audio::oscillators::Oscillator::block` + #[pyo3(name = "block")] + #[pyo3(signature = (n))] + fn block<'py>(&mut self, py: Python<'py>, n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.block(n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// One sample with instantaneous frequency modulation (adds + /// `mod_hz` to the base frequency for this sample). + /// + /// Rust: `audio::oscillators::Oscillator::fm` + #[pyo3(name = "fm")] + #[pyo3(signature = (mod_hz))] + fn fm(&mut self, mod_hz: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.fm(mod_hz)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Hard sync: retrigger the phase when `reset` is true. + /// + /// Rust: `audio::oscillators::Oscillator::hard_sync` + #[pyo3(name = "hard_sync")] + #[pyo3(signature = (reset))] + fn hard_sync(&mut self, reset: bool) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.hard_sync(reset)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + #[getter] + #[pyo3(name = "phase")] + fn py_get_phase(&self) -> PyResult { Ok(self.inner.phase) } + + #[setter] + #[pyo3(name = "phase")] + fn py_set_phase(&mut self, v: f64) { self.inner.phase = v; } + + #[getter] + #[pyo3(name = "freq")] + fn py_get_freq(&self) -> PyResult { Ok(self.inner.freq) } + + #[setter] + #[pyo3(name = "freq")] + fn py_set_freq(&mut self, v: f64) { self.inner.freq = v; } + + #[getter] + #[pyo3(name = "fs")] + fn py_get_fs(&self) -> PyResult { Ok(self.inner.fs) } + + #[setter] + #[pyo3(name = "fs")] + fn py_set_fs(&mut self, v: f64) { self.inner.fs = v; } + + #[getter] + #[pyo3(name = "kind")] + fn py_get_kind(&self) -> PyResult { Ok(crate::generated::types::PyWaveform { inner: self.inner.kind.clone() }) } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// Waveform selector for `Oscillator`. +/// +/// Rust: `audio::oscillators::Waveform` +#[pyclass(name = "Waveform", module = "numeria.audio.oscillators", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyWaveform { pub inner: rust_physics_engine::audio::oscillators::Waveform } +#[pymethods] +impl PyWaveform { + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Waveform", "Waveform", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Mipmapped single-cycle wavetable: table m is band-limited so that it +/// can play at up to `base_freqs[m]` without aliasing. +/// +/// Rust: `audio::oscillators::Wavetable` +#[pyclass(name = "Wavetable", module = "numeria.audio.oscillators")] +pub struct PyWavetable { pub inner: rust_physics_engine::audio::oscillators::Wavetable } +#[pymethods] +impl PyWavetable { + /// Builds a `Wavetable` from its fields. + #[new] + #[pyo3(signature = (tables, base_freqs))] + fn __new__(tables: Vec>, base_freqs: Vec) -> Self { + + Self { inner: rust_physics_engine::audio::oscillators::Wavetable { tables: tables, base_freqs: base_freqs } } + } + + /// Build mips from one cycle of an arbitrary waveform f(phase), + /// phase ∈ \[0, 1): each mip keeps the harmonics that stay below + /// Nyquist at its maximum playback frequency (octave-spaced mips + /// starting at 20 Hz). + /// + /// Rust: `audio::oscillators::Wavetable::from_fn` + #[pyo3(name = "from_fn")] + #[staticmethod] + #[pyo3(signature = (f, size, n_mips, fs))] + fn from_fn(f: pyo3::Py, size: usize, n_mips: usize, fs: f64) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::audio::oscillators::Wavetable::from_fn(f, size, n_mips, fs)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyWavetable { inner: __v }) + } + + /// Build mips from harmonic amplitudes (index 0 = fundamental). + /// + /// Rust: `audio::oscillators::Wavetable::from_harmonics` + #[pyo3(name = "from_harmonics")] + #[staticmethod] + #[pyo3(signature = (amps, size, n_mips, fs))] + fn from_harmonics(amps: Vec, size: usize, n_mips: usize, fs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::oscillators::Wavetable::from_harmonics(&s, size, n_mips, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyWavetable { inner: __v }) + } + + /// Linear-interpolated lookup at phase ∈ \[0, 1), mip-selected for + /// playback frequency `freq`. + /// + /// Rust: `audio::oscillators::Wavetable::lookup` + #[pyo3(name = "lookup")] + #[pyo3(signature = (phase, freq))] + fn lookup(&self, phase: f64, freq: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.lookup(phase, freq)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Band-limited sawtooth wavetable. + /// + /// Rust: `audio::oscillators::Wavetable::saw` + #[pyo3(name = "saw")] + #[staticmethod] + #[pyo3(signature = (fs))] + fn saw(fs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::oscillators::Wavetable::saw(fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyWavetable { inner: __v }) + } + + /// Band-limited square wavetable. + /// + /// Rust: `audio::oscillators::Wavetable::square` + #[pyo3(name = "square")] + #[staticmethod] + #[pyo3(signature = (fs))] + fn square(fs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::oscillators::Wavetable::square(fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyWavetable { inner: __v }) + } + + /// Band-limited triangle wavetable. + /// + /// Rust: `audio::oscillators::Wavetable::triangle` + #[pyo3(name = "triangle")] + #[staticmethod] + #[pyo3(signature = (fs))] + fn triangle(fs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::oscillators::Wavetable::triangle(fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyWavetable { inner: __v }) + } + + #[getter] + #[pyo3(name = "tables")] + fn py_get_tables(&self) -> PyResult>> { Ok(self.inner.tables.clone()) } + + #[getter] + #[pyo3(name = "base_freqs")] + fn py_get_base_freqs(&self) -> PyResult> { Ok(self.inner.base_freqs.clone()) } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// Bowed string: two waveguide segments joined at the bow point with a +/// stick-slip friction curve producing Helmholtz motion. +/// +/// Rust: `audio::physical::BowedString` +#[pyclass(name = "BowedString", module = "numeria.audio.physical")] +pub struct PyBowedString { pub inner: rust_physics_engine::audio::physical::BowedString } +#[pymethods] +impl PyBowedString { + /// New bowed string at `freq`; the bow sits at ~1/8 of the length. + /// + /// Rust: `audio::physical::BowedString::new` + #[new] + #[pyo3(signature = (freq, fs))] + fn __new__(freq: f64, fs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::physical::BowedString::new(freq, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBowedString { inner: __v }) + } + + /// Advance one sample of bridge output. + /// + /// Rust: `audio::physical::BowedString::next` + #[pyo3(name = "next")] + #[pyo3(signature = ())] + fn next(&mut self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.next()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "bow_velocity")] + fn py_get_bow_velocity(&self) -> PyResult { Ok(self.inner.bow_velocity) } + + #[setter] + #[pyo3(name = "bow_velocity")] + fn py_set_bow_velocity(&mut self, v: f64) { self.inner.bow_velocity = v; } + + #[getter] + #[pyo3(name = "bow_force")] + fn py_get_bow_force(&self) -> PyResult { Ok(self.inner.bow_force) } + + #[setter] + #[pyo3(name = "bow_force")] + fn py_set_bow_force(&mut self, v: f64) { self.inner.bow_force = v; } + + fn __repr__(&self) -> String { format!("BowedString(bow_velocity={:?}, bow_force={:?})", self.inner.bow_velocity, self.inner.bow_force) } +} + +/// Kelly-Lochbaum piecewise-cylindrical vocal tract lattice. +/// +/// Rust: `audio::physical::KellyLochbaum` +#[pyclass(name = "KellyLochbaum", module = "numeria.audio.physical")] +pub struct PyKellyLochbaum { pub inner: rust_physics_engine::audio::physical::KellyLochbaum } +#[pymethods] +impl PyKellyLochbaum { + /// Update the reflection coefficients from a new area function. + /// + /// Rust: `audio::physical::KellyLochbaum::set_areas` + #[pyo3(name = "set_areas")] + #[pyo3(signature = (area_function))] + fn set_areas<'py>(&mut self, py: Python<'py>, area_function: Vec) -> PyResult<()> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.set_areas(&area_function))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// One sample: feed the glottal source in, return the lip output. + /// + /// Rust: `audio::physical::KellyLochbaum::next` + #[pyo3(name = "next")] + #[pyo3(signature = (glottal))] + fn next(&mut self, glottal: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.next(glottal)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "glottal_reflection")] + fn py_get_glottal_reflection(&self) -> PyResult { Ok(self.inner.glottal_reflection) } + + #[setter] + #[pyo3(name = "glottal_reflection")] + fn py_set_glottal_reflection(&mut self, v: f64) { self.inner.glottal_reflection = v; } + + #[getter] + #[pyo3(name = "lip_reflection")] + fn py_get_lip_reflection(&self) -> PyResult { Ok(self.inner.lip_reflection) } + + #[setter] + #[pyo3(name = "lip_reflection")] + fn py_set_lip_reflection(&mut self, v: f64) { self.inner.lip_reflection = v; } + + fn __repr__(&self) -> String { format!("KellyLochbaum(glottal_reflection={:?}, lip_reflection={:?})", self.inner.glottal_reflection, self.inner.lip_reflection) } +} + +/// Brute-force lumped mass-spring string with fixed ends, for validating +/// the waveguide against a direct Newtonian simulation. +/// +/// Rust: `audio::physical::MassSpringString` +#[pyclass(name = "MassSpringString", module = "numeria.audio.physical")] +pub struct PyMassSpringString { pub inner: rust_physics_engine::audio::physical::MassSpringString } +#[pymethods] +impl PyMassSpringString { + /// String of `n` moving masses tuned so the continuum limit has + /// fundamental `freq` (unit length, unit line density). + /// + /// Rust: `audio::physical::MassSpringString::new` + #[new] + #[pyo3(signature = (freq, n, fs))] + fn __new__(freq: f64, n: usize, fs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::physical::MassSpringString::new(freq, n, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMassSpringString { inner: __v }) + } + + /// Triangular pluck peaked at fractional position `pos`. + /// + /// Rust: `audio::physical::MassSpringString::pluck` + #[pyo3(name = "pluck")] + #[pyo3(signature = (pos, amp))] + fn pluck(&mut self, pos: f64, amp: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.pluck(pos, amp)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Advance one audio sample (semi-implicit Euler, substepped for + /// stability); returns the displacement at the pickup node. + /// + /// Rust: `audio::physical::MassSpringString::next` + #[pyo3(name = "next")] + #[pyo3(signature = ())] + fn next(&mut self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.next()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "masses")] + fn py_get_masses(&self) -> PyResult> { Ok(self.inner.masses.clone()) } + + #[getter] + #[pyo3(name = "positions")] + fn py_get_positions(&self) -> PyResult> { Ok(self.inner.positions.clone()) } + + #[getter] + #[pyo3(name = "velocities")] + fn py_get_velocities(&self) -> PyResult> { Ok(self.inner.velocities.clone()) } + + #[getter] + #[pyo3(name = "k")] + fn py_get_k(&self) -> PyResult { Ok(self.inner.k) } + + #[setter] + #[pyo3(name = "k")] + fn py_set_k(&mut self, v: f64) { self.inner.k = v; } + + #[getter] + #[pyo3(name = "damping")] + fn py_get_damping(&self) -> PyResult { Ok(self.inner.damping) } + + #[setter] + #[pyo3(name = "damping")] + fn py_set_damping(&mut self, v: f64) { self.inner.damping = v; } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// Circular drum head on a masked finite-difference grid, audio-rate. +/// +/// Rust: `audio::physical::Membrane2D` +#[pyclass(name = "Membrane2D", module = "numeria.audio.physical")] +pub struct PyMembrane2D { pub inner: rust_physics_engine::audio::physical::Membrane2D } +#[pymethods] +impl PyMembrane2D { + /// Circular drum of physical radius (m), membrane tension (N/m), and + /// surface density (kg/m²) on a res × res grid. + /// + /// Rust: `audio::physical::Membrane2D::drum` + #[pyo3(name = "drum")] + #[staticmethod] + #[pyo3(signature = (radius_m, tension, density, res, fs))] + fn drum(radius_m: f64, tension: f64, density: f64, res: usize, fs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::physical::Membrane2D::drum(radius_m, tension, density, res, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMembrane2D { inner: __v }) + } + + /// Strike at (x, y) in units of the radius (-1..1) with velocity + /// `vel`, as a smooth velocity bump. + /// + /// Rust: `audio::physical::Membrane2D::strike` + #[pyo3(name = "strike")] + #[pyo3(signature = (x, y, vel))] + fn strike(&mut self, x: f64, y: f64, vel: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.strike(x, y, vel)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Advance one audio sample; returns the displacement at the pickup. + /// + /// Rust: `audio::physical::Membrane2D::next` + #[pyo3(name = "next")] + #[pyo3(signature = ())] + fn next(&mut self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.next()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// Bank of two-pole resonators driven by an excitation buffer. +/// +/// Rust: `audio::physical::ModalSynth` +#[pyclass(name = "ModalSynth", module = "numeria.audio.physical")] +pub struct PyModalSynth { pub inner: rust_physics_engine::audio::physical::ModalSynth } +#[pymethods] +impl PyModalSynth { + /// Build from an explicit (freq, t60, gain) mode list; modes at or + /// above Nyquist are dropped. + /// + /// Rust: `audio::physical::ModalSynth::from_modes` + #[pyo3(name = "from_modes")] + #[staticmethod] + #[pyo3(signature = (modes, fs))] + fn from_modes(modes: Vec<(f64, f64, f64)>, fs: f64) -> PyResult { + let modes = modes.into_iter().map(|__e| (__e.0, __e.1, __e.2)).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::audio::physical::ModalSynth::from_modes(&modes, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyModalSynth { inner: __v }) + } + + /// Free-free bar of rectangular cross section (Euler-Bernoulli). + /// + /// Rust: `audio::physical::ModalSynth::bar` + #[pyo3(name = "bar")] + #[staticmethod] + #[pyo3(signature = (length, width, thickness, young, rho, fs))] + fn bar(length: f64, width: f64, thickness: f64, young: f64, rho: f64, fs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::physical::ModalSynth::bar(length, width, thickness, young, rho, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyModalSynth { inner: __v }) + } + + /// Circular membrane (drum head) modal model. + /// + /// Rust: `audio::physical::ModalSynth::membrane_circular` + #[pyo3(name = "membrane_circular")] + #[staticmethod] + #[pyo3(signature = (radius, tension, sigma, fs))] + fn membrane_circular(radius: f64, tension: f64, sigma: f64, fs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::physical::ModalSynth::membrane_circular(radius, tension, sigma, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyModalSynth { inner: __v }) + } + + /// Simply supported rectangular plate modal model. + /// + /// Rust: `audio::physical::ModalSynth::plate` + #[pyo3(name = "plate")] + #[staticmethod] + #[pyo3(signature = (a, b, thickness, young, rho, nu, fs))] + fn plate(a: f64, b: f64, thickness: f64, young: f64, rho: f64, nu: f64, fs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::physical::ModalSynth::plate(a, b, thickness, young, rho, nu, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyModalSynth { inner: __v }) + } + + /// Thin-ring bell flexural modes. + /// + /// Rust: `audio::physical::ModalSynth::bell` + #[pyo3(name = "bell")] + #[staticmethod] + #[pyo3(signature = (radius, thickness, young, rho, nu, fs))] + fn bell(radius: f64, thickness: f64, young: f64, rho: f64, nu: f64, fs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::physical::ModalSynth::bell(radius, thickness, young, rho, nu, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyModalSynth { inner: __v }) + } + + /// Wine-glass model from measured partial ratios of the (n,0) rim + /// modes: 1, 2.32, 4.25, 6.63, 9.38. + /// + /// Rust: `audio::physical::ModalSynth::glass` + #[pyo3(name = "glass")] + #[staticmethod] + #[pyo3(signature = (f0, fs))] + fn glass(f0: f64, fs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::physical::ModalSynth::glass(f0, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyModalSynth { inner: __v }) + } + + /// Queue an arbitrary excitation signal. + /// + /// Rust: `audio::physical::ModalSynth::excite` + #[pyo3(name = "excite")] + #[pyo3(signature = (impulse))] + fn excite<'py>(&mut self, py: Python<'py>, impulse: Vec) -> PyResult<()> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.excite(&impulse))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Strike with a raised-cosine impulse; `hardness` in (0..1], harder + /// strikes are shorter (brighter). + /// + /// Rust: `audio::physical::ModalSynth::strike` + #[pyo3(name = "strike")] + #[pyo3(signature = (hardness))] + fn strike(&mut self, hardness: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.strike(hardness)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Advance one output sample. + /// + /// Rust: `audio::physical::ModalSynth::next` + #[pyo3(name = "next")] + #[pyo3(signature = ())] + fn next(&mut self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.next()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "modes")] + fn py_get_modes(&self) -> PyResult> { Ok(self.inner.modes.clone().into_iter().map(|__x| (__x.0, __x.1, __x.2)).collect::>()) } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// Simply supported Kirchhoff plate (u_tt = -κ² ∇⁴u) on a +/// finite-difference grid, audio-rate. +/// +/// Rust: `audio::physical::Plate2D` +#[pyclass(name = "Plate2D", module = "numeria.audio.physical")] +pub struct PyPlate2D { pub inner: rust_physics_engine::audio::physical::Plate2D } +#[pymethods] +impl PyPlate2D { + /// Plate of size a × b (m), thickness h, Young's modulus, density, + /// and Poisson ratio on a res-wide grid. + /// + /// Rust: `audio::physical::Plate2D::new` + #[new] + #[pyo3(signature = (a, b, thickness, young, rho, nu, res, fs))] + fn __new__(a: f64, b: f64, thickness: f64, young: f64, rho: f64, nu: f64, res: usize, fs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::physical::Plate2D::new(a, b, thickness, young, rho, nu, res, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPlate2D { inner: __v }) + } + + /// Strike at fractional position (0..1, 0..1) with velocity `vel`. + /// + /// Rust: `audio::physical::Plate2D::strike` + #[pyo3(name = "strike")] + #[pyo3(signature = (x, y, vel))] + fn strike(&mut self, x: f64, y: f64, vel: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.strike(x, y, vel)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Advance one audio sample; returns the displacement at the pickup. + /// + /// Rust: `audio::physical::Plate2D::next` + #[pyo3(name = "next")] + #[pyo3(signature = ())] + fn next(&mut self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.next()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "damping")] + fn py_get_damping(&self) -> PyResult { Ok(self.inner.damping) } + + #[setter] + #[pyo3(name = "damping")] + fn py_set_damping(&mut self, v: f64) { self.inner.damping = v; } + + fn __repr__(&self) -> String { format!("Plate2D(damping={:?})", self.inner.damping) } +} + +/// Bidirectional digital waveguide string with bridge damping filter, +/// optional stiffness allpass, and an internal tuning allpass keeping the +/// pitch exact at the fundamental. +/// +/// Rust: `audio::physical::WaveguideString` +#[pyclass(name = "WaveguideString", module = "numeria.audio.physical")] +pub struct PyWaveguideString { pub inner: rust_physics_engine::audio::physical::WaveguideString } +#[pymethods] +impl PyWaveguideString { + /// New string tuned to `freq` at sample rate `fs`. + /// + /// Rust: `audio::physical::WaveguideString::new` + #[new] + #[pyo3(signature = (freq, fs))] + fn __new__(freq: f64, fs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::physical::WaveguideString::new(freq, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyWaveguideString { inner: __v }) + } + + /// Retune to a new fundamental, sizing the rails and the tuning + /// allpass so the loop delay is exactly `fs/freq` samples at `freq`. + /// + /// Rust: `audio::physical::WaveguideString::set_freq` + #[pyo3(name = "set_freq")] + #[pyo3(signature = (freq))] + fn set_freq(&mut self, freq: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.set_freq(freq)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Current fundamental (Hz). + /// + /// Rust: `audio::physical::WaveguideString::freq` + #[pyo3(name = "freq")] + #[pyo3(signature = ())] + fn freq(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.freq()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Pluck: load a smoothed triangular displacement peaked at `pos` + /// (0..1) with amplitude `amp`; `width` (0..1) smooths the corner. + /// + /// Rust: `audio::physical::WaveguideString::pluck` + #[pyo3(name = "pluck")] + #[pyo3(signature = (pos, amp, width))] + fn pluck(&mut self, pos: f64, amp: f64, width: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.pluck(pos, amp, width)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Strike: inject a narrow raised-cosine velocity pulse at `pos`. + /// + /// Rust: `audio::physical::WaveguideString::strike` + #[pyo3(name = "strike")] + #[pyo3(signature = (pos, vel))] + fn strike(&mut self, pos: f64, vel: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.strike(pos, vel)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Start (force > 0) or stop (force <= 0) bowing at `pos` with the + /// given bow force (0..1) and velocity. + /// + /// Rust: `audio::physical::WaveguideString::bow` + #[pyo3(name = "bow")] + #[pyo3(signature = (force, velocity, pos))] + fn bow(&mut self, force: f64, velocity: f64, pos: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.bow(force, velocity, pos)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Advance one sample; returns the wave arriving at the bridge. + /// + /// Rust: `audio::physical::WaveguideString::next` + #[pyo3(name = "next")] + #[pyo3(signature = ())] + fn next(&mut self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.next()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Displacement at fractional position `pos` (0..1). + /// + /// Rust: `audio::physical::WaveguideString::output_at` + #[pyo3(name = "output_at")] + #[pyo3(signature = (pos))] + fn output_at(&self, pos: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.output_at(pos)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "damping")] + fn py_get_damping(&self) -> PyResult { Ok(self.inner.damping) } + + #[setter] + #[pyo3(name = "damping")] + fn py_set_damping(&mut self, v: f64) { self.inner.damping = v; } + + #[getter] + #[pyo3(name = "stiffness_allpass")] + fn py_get_stiffness_allpass(&self) -> PyResult { Ok(crate::generated::types::PyBiquad { inner: self.inner.stiffness_allpass.clone() }) } + + #[getter] + #[pyo3(name = "bridge_filter")] + fn py_get_bridge_filter(&self) -> PyResult { Ok(crate::generated::types::PyBiquad { inner: self.inner.bridge_filter.clone() }) } + + #[getter] + #[pyo3(name = "pluck_pos")] + fn py_get_pluck_pos(&self) -> PyResult { Ok(self.inner.pluck_pos) } + + #[setter] + #[pyo3(name = "pluck_pos")] + fn py_set_pluck_pos(&mut self, v: f64) { self.inner.pluck_pos = v; } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// Single-reed (clarinet) or jet (flute) waveguide wind instrument. +/// +/// Rust: `audio::physical::WaveguideTube` +#[pyclass(name = "WaveguideTube", module = "numeria.audio.physical")] +pub struct PyWaveguideTube { pub inner: rust_physics_engine::audio::physical::WaveguideTube } +#[pymethods] +impl PyWaveguideTube { + /// Clarinet model: closed-open bore (round trip fs/(2 f)) with a reed + /// reflection nonlinearity. + /// + /// Rust: `audio::physical::WaveguideTube::clarinet` + #[pyo3(name = "clarinet")] + #[staticmethod] + #[pyo3(signature = (freq, fs))] + fn clarinet(freq: f64, fs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::physical::WaveguideTube::clarinet(freq, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyWaveguideTube { inner: __v }) + } + + /// Flute model: open-open bore (round trip fs/f) with a jet delay of + /// half the bore and a cubic jet nonlinearity. + /// + /// Rust: `audio::physical::WaveguideTube::flute` + #[pyo3(name = "flute")] + #[staticmethod] + #[pyo3(signature = (freq, fs))] + fn flute(freq: f64, fs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::physical::WaveguideTube::flute(freq, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyWaveguideTube { inner: __v }) + } + + /// Set the blowing pressure (0..~1). + /// + /// Rust: `audio::physical::WaveguideTube::set_breath` + #[pyo3(name = "set_breath")] + #[pyo3(signature = (p))] + fn set_breath(&mut self, p: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.set_breath(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Advance one sample of bore output. + /// + /// Rust: `audio::physical::WaveguideTube::next` + #[pyo3(name = "next")] + #[pyo3(signature = ())] + fn next(&mut self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.next()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// One FM operator: frequency ratio, modulation index (as output +/// amplitude when used as a modulator), envelope, and self-feedback. +/// +/// Rust: `audio::synthesis::FmOperator` +#[pyclass(name = "FmOperator", module = "numeria.audio.synthesis")] +pub struct PyFmOperator { pub inner: rust_physics_engine::audio::synthesis::FmOperator } +#[pymethods] +impl PyFmOperator { + #[getter] + #[pyo3(name = "ratio")] + fn py_get_ratio(&self) -> PyResult { Ok(self.inner.ratio) } + + #[setter] + #[pyo3(name = "ratio")] + fn py_set_ratio(&mut self, v: f64) { self.inner.ratio = v; } + + #[getter] + #[pyo3(name = "index")] + fn py_get_index(&self) -> PyResult { Ok(self.inner.index) } + + #[setter] + #[pyo3(name = "index")] + fn py_set_index(&mut self, v: f64) { self.inner.index = v; } + + #[getter] + #[pyo3(name = "feedback")] + fn py_get_feedback(&self) -> PyResult { Ok(self.inner.feedback) } + + #[setter] + #[pyo3(name = "feedback")] + fn py_set_feedback(&mut self, v: f64) { self.inner.feedback = v; } + + #[getter] + #[pyo3(name = "phase")] + fn py_get_phase(&self) -> PyResult { Ok(self.inner.phase) } + + #[setter] + #[pyo3(name = "phase")] + fn py_set_phase(&mut self, v: f64) { self.inner.phase = v; } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// DX7-style FM synth: `algorithm[i]` lists the operators that modulate +/// operator i (an empty list means it is a carrier unless someone else +/// consumes it; operators that appear in no modulation list sum into +/// the output). +/// +/// Rust: `audio::synthesis::FmSynth` +#[pyclass(name = "FmSynth", module = "numeria.audio.synthesis")] +pub struct PyFmSynth { pub inner: rust_physics_engine::audio::synthesis::FmSynth } +#[pymethods] +impl PyFmSynth { + /// A few classic 6-op routing tables (1-indexed DX7 numbering + /// reduced to the topology): 1 = single stack pairs, 32 = all + /// carriers. Unknown numbers fall back to algorithm 32. + /// + /// Rust: `audio::synthesis::FmSynth::dx7_algorithm` + #[pyo3(name = "dx7_algorithm")] + #[staticmethod] + #[pyo3(signature = (n))] + fn dx7_algorithm<'py>(py: Python<'py>, n: u8) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::audio::synthesis::FmSynth::dx7_algorithm(n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Gate every operator envelope on at the given frequency. + /// + /// Rust: `audio::synthesis::FmSynth::note_on` + #[pyo3(name = "note_on")] + #[pyo3(signature = (freq))] + fn note_on(&mut self, freq: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.note_on(freq)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Gate every envelope off. + /// + /// Rust: `audio::synthesis::FmSynth::note_off` + #[pyo3(name = "note_off")] + #[pyo3(signature = ())] + fn note_off(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.note_off()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// One output sample (modulators evaluated depth-first each tick). + /// + /// Rust: `audio::synthesis::FmSynth::next` + #[pyo3(name = "next")] + #[pyo3(signature = ())] + fn next(&mut self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.next()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Render a full note (attack at t = 0, release at 70% duration). + /// + /// Rust: `audio::synthesis::FmSynth::render` + #[pyo3(name = "render")] + #[pyo3(signature = (freq, duration))] + fn render<'py>(&mut self, py: Python<'py>, freq: f64, duration: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.render(freq, duration))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "algorithm")] + fn py_get_algorithm(&self) -> PyResult>> { Ok(self.inner.algorithm.clone()) } + + #[getter] + #[pyo3(name = "fs")] + fn py_get_fs(&self) -> PyResult { Ok(self.inner.fs) } + + #[setter] + #[pyo3(name = "fs")] + fn py_set_fs(&mut self, v: f64) { self.inner.fs = v; } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// Voice types for `vowel_formants`. +/// +/// Rust: `audio::synthesis::Voice` +#[pyclass(name = "Voice", module = "numeria.audio.synthesis", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyVoice { + Male, + Female, +} +impl PyVoice { + pub fn to_rust(&self) -> rust_physics_engine::audio::synthesis::Voice { match self { + Self::Male => rust_physics_engine::audio::synthesis::Voice::Male, + Self::Female => rust_physics_engine::audio::synthesis::Voice::Female, + } } + pub fn from_rust(v: &rust_physics_engine::audio::synthesis::Voice) -> Self { match v { + rust_physics_engine::audio::synthesis::Voice::Male => Self::Male, + rust_physics_engine::audio::synthesis::Voice::Female => Self::Female, + } } +} +#[pymethods] +impl PyVoice { + fn __repr__(&self) -> &'static str { + match self { + Self::Male => "Voice.Male", + Self::Female => "Voice.Female", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Chord qualities. +/// +/// Rust: `audio::tuning::ChordQuality` +#[pyclass(name = "ChordQuality", module = "numeria.audio.tuning", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyChordQuality { + Major, + Minor, + Diminished, + Augmented, + Major7, + Minor7, + Dominant7, + HalfDiminished7, + Diminished7, + Sus2, + Sus4, +} +impl PyChordQuality { + pub fn to_rust(&self) -> rust_physics_engine::audio::tuning::ChordQuality { match self { + Self::Major => rust_physics_engine::audio::tuning::ChordQuality::Major, + Self::Minor => rust_physics_engine::audio::tuning::ChordQuality::Minor, + Self::Diminished => rust_physics_engine::audio::tuning::ChordQuality::Diminished, + Self::Augmented => rust_physics_engine::audio::tuning::ChordQuality::Augmented, + Self::Major7 => rust_physics_engine::audio::tuning::ChordQuality::Major7, + Self::Minor7 => rust_physics_engine::audio::tuning::ChordQuality::Minor7, + Self::Dominant7 => rust_physics_engine::audio::tuning::ChordQuality::Dominant7, + Self::HalfDiminished7 => rust_physics_engine::audio::tuning::ChordQuality::HalfDiminished7, + Self::Diminished7 => rust_physics_engine::audio::tuning::ChordQuality::Diminished7, + Self::Sus2 => rust_physics_engine::audio::tuning::ChordQuality::Sus2, + Self::Sus4 => rust_physics_engine::audio::tuning::ChordQuality::Sus4, + } } + pub fn from_rust(v: &rust_physics_engine::audio::tuning::ChordQuality) -> Self { match v { + rust_physics_engine::audio::tuning::ChordQuality::Major => Self::Major, + rust_physics_engine::audio::tuning::ChordQuality::Minor => Self::Minor, + rust_physics_engine::audio::tuning::ChordQuality::Diminished => Self::Diminished, + rust_physics_engine::audio::tuning::ChordQuality::Augmented => Self::Augmented, + rust_physics_engine::audio::tuning::ChordQuality::Major7 => Self::Major7, + rust_physics_engine::audio::tuning::ChordQuality::Minor7 => Self::Minor7, + rust_physics_engine::audio::tuning::ChordQuality::Dominant7 => Self::Dominant7, + rust_physics_engine::audio::tuning::ChordQuality::HalfDiminished7 => Self::HalfDiminished7, + rust_physics_engine::audio::tuning::ChordQuality::Diminished7 => Self::Diminished7, + rust_physics_engine::audio::tuning::ChordQuality::Sus2 => Self::Sus2, + rust_physics_engine::audio::tuning::ChordQuality::Sus4 => Self::Sus4, + } } +} +#[pymethods] +impl PyChordQuality { + fn __repr__(&self) -> &'static str { + match self { + Self::Major => "ChordQuality.Major", + Self::Minor => "ChordQuality.Minor", + Self::Diminished => "ChordQuality.Diminished", + Self::Augmented => "ChordQuality.Augmented", + Self::Major7 => "ChordQuality.Major7", + Self::Minor7 => "ChordQuality.Minor7", + Self::Dominant7 => "ChordQuality.Dominant7", + Self::HalfDiminished7 => "ChordQuality.HalfDiminished7", + Self::Diminished7 => "ChordQuality.Diminished7", + Self::Sus2 => "ChordQuality.Sus2", + Self::Sus4 => "ChordQuality.Sus4", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Diatonic modes and common scales. +/// +/// Rust: `audio::tuning::Mode` +#[pyclass(name = "Mode", module = "numeria.audio.tuning", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyMode { + Ionian, + Dorian, + Phrygian, + Lydian, + Mixolydian, + Aeolian, + Locrian, + HarmonicMinor, + MelodicMinor, + MajorPentatonic, + MinorPentatonic, + Blues, + WholeTone, + Chromatic, +} +impl PyMode { + pub fn to_rust(&self) -> rust_physics_engine::audio::tuning::Mode { match self { + Self::Ionian => rust_physics_engine::audio::tuning::Mode::Ionian, + Self::Dorian => rust_physics_engine::audio::tuning::Mode::Dorian, + Self::Phrygian => rust_physics_engine::audio::tuning::Mode::Phrygian, + Self::Lydian => rust_physics_engine::audio::tuning::Mode::Lydian, + Self::Mixolydian => rust_physics_engine::audio::tuning::Mode::Mixolydian, + Self::Aeolian => rust_physics_engine::audio::tuning::Mode::Aeolian, + Self::Locrian => rust_physics_engine::audio::tuning::Mode::Locrian, + Self::HarmonicMinor => rust_physics_engine::audio::tuning::Mode::HarmonicMinor, + Self::MelodicMinor => rust_physics_engine::audio::tuning::Mode::MelodicMinor, + Self::MajorPentatonic => rust_physics_engine::audio::tuning::Mode::MajorPentatonic, + Self::MinorPentatonic => rust_physics_engine::audio::tuning::Mode::MinorPentatonic, + Self::Blues => rust_physics_engine::audio::tuning::Mode::Blues, + Self::WholeTone => rust_physics_engine::audio::tuning::Mode::WholeTone, + Self::Chromatic => rust_physics_engine::audio::tuning::Mode::Chromatic, + } } + pub fn from_rust(v: &rust_physics_engine::audio::tuning::Mode) -> Self { match v { + rust_physics_engine::audio::tuning::Mode::Ionian => Self::Ionian, + rust_physics_engine::audio::tuning::Mode::Dorian => Self::Dorian, + rust_physics_engine::audio::tuning::Mode::Phrygian => Self::Phrygian, + rust_physics_engine::audio::tuning::Mode::Lydian => Self::Lydian, + rust_physics_engine::audio::tuning::Mode::Mixolydian => Self::Mixolydian, + rust_physics_engine::audio::tuning::Mode::Aeolian => Self::Aeolian, + rust_physics_engine::audio::tuning::Mode::Locrian => Self::Locrian, + rust_physics_engine::audio::tuning::Mode::HarmonicMinor => Self::HarmonicMinor, + rust_physics_engine::audio::tuning::Mode::MelodicMinor => Self::MelodicMinor, + rust_physics_engine::audio::tuning::Mode::MajorPentatonic => Self::MajorPentatonic, + rust_physics_engine::audio::tuning::Mode::MinorPentatonic => Self::MinorPentatonic, + rust_physics_engine::audio::tuning::Mode::Blues => Self::Blues, + rust_physics_engine::audio::tuning::Mode::WholeTone => Self::WholeTone, + rust_physics_engine::audio::tuning::Mode::Chromatic => Self::Chromatic, + } } +} +#[pymethods] +impl PyMode { + fn __repr__(&self) -> &'static str { + match self { + Self::Ionian => "Mode.Ionian", + Self::Dorian => "Mode.Dorian", + Self::Phrygian => "Mode.Phrygian", + Self::Lydian => "Mode.Lydian", + Self::Mixolydian => "Mode.Mixolydian", + Self::Aeolian => "Mode.Aeolian", + Self::Locrian => "Mode.Locrian", + Self::HarmonicMinor => "Mode.HarmonicMinor", + Self::MelodicMinor => "Mode.MelodicMinor", + Self::MajorPentatonic => "Mode.MajorPentatonic", + Self::MinorPentatonic => "Mode.MinorPentatonic", + Self::Blues => "Mode.Blues", + Self::WholeTone => "Mode.WholeTone", + Self::Chromatic => "Mode.Chromatic", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Excitation source for the LPC vocoder. +/// +/// Rust: `audio::vocoder::Excitation` +#[pyclass(name = "Excitation", module = "numeria.audio.vocoder", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyExcitation { pub inner: rust_physics_engine::audio::vocoder::Excitation } +#[pymethods] +impl PyExcitation { + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Excitation", "Excitation", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Classic phase vocoder with optional identity phase locking +/// (Laroche-Dolson). +/// +/// Rust: `audio::vocoder::PhaseVocoder` +#[pyclass(name = "PhaseVocoder", module = "numeria.audio.vocoder")] +pub struct PyPhaseVocoder { pub inner: rust_physics_engine::audio::vocoder::PhaseVocoder } +#[pymethods] +impl PyPhaseVocoder { + /// New vocoder with the given FFT size and (synthesis) hop. + /// + /// Rust: `audio::vocoder::PhaseVocoder::new` + #[new] + #[pyo3(signature = (n_fft, hop, fs))] + fn __new__(n_fft: usize, hop: usize, fs: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::audio::vocoder::PhaseVocoder::new(n_fft, hop, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPhaseVocoder { inner: __v }) + } + + /// Enable/disable identity phase locking (phases of non-peak bins + /// follow their nearest spectral peak). + /// + /// Rust: `audio::vocoder::PhaseVocoder::phase_lock` + #[pyo3(name = "phase_lock")] + #[pyo3(signature = (on))] + fn phase_lock(&mut self, on: bool) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.phase_lock(on)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Time-stretch by `ratio` (>1 = longer) without changing pitch. + /// + /// Rust: `audio::vocoder::PhaseVocoder::time_stretch` + #[pyo3(name = "time_stretch")] + #[pyo3(signature = (x, ratio))] + fn time_stretch<'py>(&mut self, py: Python<'py>, x: Vec, ratio: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.time_stretch(&x, ratio))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Pitch-shift by `semitones` keeping the duration (stretch then + /// resample). + /// + /// Rust: `audio::vocoder::PhaseVocoder::pitch_shift` + #[pyo3(name = "pitch_shift")] + #[pyo3(signature = (x, semitones))] + fn pitch_shift<'py>(&mut self, py: Python<'py>, x: Vec, semitones: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.pitch_shift(&x, semitones))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Pitch shift with spectral-envelope (formant) preservation: the + /// shifted signal is re-filtered toward the original envelope. + /// + /// Rust: `audio::vocoder::PhaseVocoder::pitch_shift_formant_preserving` + #[pyo3(name = "pitch_shift_formant_preserving")] + #[pyo3(signature = (x, semitones))] + fn pitch_shift_formant_preserving<'py>(&mut self, py: Python<'py>, x: Vec, semitones: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.pitch_shift_formant_preserving(&x, semitones))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Loop the spectral frame at `at_sample` for `duration` samples. + /// + /// Rust: `audio::vocoder::PhaseVocoder::freeze` + #[pyo3(name = "freeze")] + #[pyo3(signature = (x, at_sample, duration))] + fn freeze<'py>(&mut self, py: Python<'py>, x: Vec, at_sample: usize, duration: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.freeze(&x, at_sample, duration))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Zero every phase: monotone "robot" voice at the frame rate. + /// + /// Rust: `audio::vocoder::PhaseVocoder::robotize` + #[pyo3(name = "robotize")] + #[pyo3(signature = (x))] + fn robotize<'py>(&mut self, py: Python<'py>, x: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.robotize(&x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Randomize every phase: breathy "whisper" voice. + /// + /// Rust: `audio::vocoder::PhaseVocoder::whisperize` + #[pyo3(name = "whisperize")] + #[pyo3(signature = (x))] + fn whisperize<'py>(&mut self, py: Python<'py>, x: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.whisperize(&x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// Decoded audio: sample rate, channel count, and per-channel samples +/// in −1..1. +/// +/// Rust: `audio::wav::WavData` +#[pyclass(name = "WavData", module = "numeria.audio.wav", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyWavData { pub inner: rust_physics_engine::audio::wav::WavData } +#[pymethods] +impl PyWavData { + /// Builds a `WavData` from its fields. + #[new] + #[pyo3(signature = (fs, channels, samples))] + fn __new__(fs: u32, channels: u16, samples: Vec>) -> Self { + + Self { inner: rust_physics_engine::audio::wav::WavData { fs: fs, channels: channels, samples: samples } } + } + + #[getter] + #[pyo3(name = "fs")] + fn py_get_fs(&self) -> PyResult { Ok(self.inner.fs) } + + #[setter] + #[pyo3(name = "fs")] + fn py_set_fs(&mut self, v: u32) { self.inner.fs = v; } + + #[getter] + #[pyo3(name = "channels")] + fn py_get_channels(&self) -> PyResult { Ok(self.inner.channels) } + + #[setter] + #[pyo3(name = "channels")] + fn py_set_channels(&mut self, v: u16) { self.inner.channels = v; } + + #[getter] + #[pyo3(name = "samples")] + fn py_get_samples(&self) -> PyResult>> { Ok(self.inner.samples.clone()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("WavData", "WavData", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} diff --git a/bindings/python/src/generated/types/biophysics.rs b/bindings/python/src/generated/types/biophysics.rs new file mode 100644 index 0000000..c207bca --- /dev/null +++ b/bindings/python/src/generated/types/biophysics.rs @@ -0,0 +1,764 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; + + +/// One sample of a compartment trajectory: `(time, S, E, I, R)`. +/// +/// Models without an exposed class report `E = 0`, so a caller can plot any +/// of them the same way. +/// +/// Rust: `biophysics::epidemiology::EpidemicSample` +#[pyclass(name = "EpidemicSample", module = "numeria.biophysics.epidemiology", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyEpidemicSample { pub inner: rust_physics_engine::biophysics::epidemiology::EpidemicSample } +#[pymethods] +impl PyEpidemicSample { + /// Builds a `EpidemicSample` from its fields. + #[new] + #[pyo3(signature = (t, s, e, i, r))] + fn __new__(t: f64, s: f64, e: f64, i: f64, r: f64) -> Self { + + Self { inner: rust_physics_engine::biophysics::epidemiology::EpidemicSample { t: t, s: s, e: e, i: i, r: r } } + } + + /// The total, which every model here conserves. + /// + /// Rust: `biophysics::epidemiology::EpidemicSample::total` + #[pyo3(name = "total")] + #[pyo3(signature = ())] + fn total(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.total()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "t")] + fn py_get_t(&self) -> PyResult { Ok(self.inner.t) } + + #[setter] + #[pyo3(name = "t")] + fn py_set_t(&mut self, v: f64) { self.inner.t = v; } + + #[getter] + #[pyo3(name = "s")] + fn py_get_s(&self) -> PyResult { Ok(self.inner.s) } + + #[setter] + #[pyo3(name = "s")] + fn py_set_s(&mut self, v: f64) { self.inner.s = v; } + + #[getter] + #[pyo3(name = "e")] + fn py_get_e(&self) -> PyResult { Ok(self.inner.e) } + + #[setter] + #[pyo3(name = "e")] + fn py_set_e(&mut self, v: f64) { self.inner.e = v; } + + #[getter] + #[pyo3(name = "i")] + fn py_get_i(&self) -> PyResult { Ok(self.inner.i) } + + #[setter] + #[pyo3(name = "i")] + fn py_set_i(&mut self, v: f64) { self.inner.i = v; } + + #[getter] + #[pyo3(name = "r")] + fn py_get_r(&self) -> PyResult { Ok(self.inner.r) } + + #[setter] + #[pyo3(name = "r")] + fn py_set_r(&mut self, v: f64) { self.inner.r = v; } + + fn __repr__(&self) -> String { format!("EpidemicSample(t={:?}, s={:?}, e={:?}, i={:?}, r={:?})", self.inner.t, self.inner.s, self.inner.e, self.inner.i, self.inner.r) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A `EpidemicSample` argument, or anything that can stand in for one. +pub struct PyEpidemicSampleArg(pub rust_physics_engine::biophysics::epidemiology::EpidemicSample); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyEpidemicSampleArg { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result { + if let Ok(__w) = obj.extract::() { + return Ok(PyEpidemicSampleArg(__w.inner)); + } + let __v = crate::runtime::coerce::floats_exact(obj, 5, "EpidemicSample")?; + Ok(PyEpidemicSampleArg(rust_physics_engine::biophysics::epidemiology::EpidemicSample { t: __v[0], s: __v[1], e: __v[2], i: __v[3], r: __v[4] })) + } +} + + +/// Morris-Lecar's parameters, in the squid axon's units. +/// +/// Rust: `biophysics::neuro::MorrisLecar` +#[pyclass(name = "MorrisLecar", module = "numeria.biophysics.neuro", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyMorrisLecar { pub inner: rust_physics_engine::biophysics::neuro::MorrisLecar } +#[pymethods] +impl PyMorrisLecar { + /// Builds a `MorrisLecar` from its fields. + #[new] + #[pyo3(signature = (c_m, g_l, g_ca, g_k, v_l, v_ca, v_k, v1, v2, v3, v4, phi))] + fn __new__(c_m: f64, g_l: f64, g_ca: f64, g_k: f64, v_l: f64, v_ca: f64, v_k: f64, v1: f64, v2: f64, v3: f64, v4: f64, phi: f64) -> Self { + + Self { inner: rust_physics_engine::biophysics::neuro::MorrisLecar { c_m: c_m, g_l: g_l, g_ca: g_ca, g_k: g_k, v_l: v_l, v_ca: v_ca, v_k: v_k, v1: v1, v2: v2, v3: v3, v4: v4, phi: phi } } + } + + /// The Hopf parameter set: a type II neuron, whose firing rate jumps + /// to a finite value at threshold as Hodgkin-Huxley's does. + /// + /// Rust: `biophysics::neuro::MorrisLecar::hopf` + #[pyo3(name = "hopf")] + #[staticmethod] + #[pyo3(signature = ())] + fn hopf() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::neuro::MorrisLecar::hopf()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMorrisLecar { inner: __v }) + } + + /// The saddle-node-on-a-circle parameter set: a type I neuron, which + /// can fire arbitrarily slowly just above threshold because the limit + /// cycle is born with infinite period. + /// + /// Rust: `biophysics::neuro::MorrisLecar::saddle_node` + #[pyo3(name = "saddle_node")] + #[staticmethod] + #[pyo3(signature = ())] + fn saddle_node() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::neuro::MorrisLecar::saddle_node()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMorrisLecar { inner: __v }) + } + + #[getter] + #[pyo3(name = "c_m")] + fn py_get_c_m(&self) -> PyResult { Ok(self.inner.c_m) } + + #[setter] + #[pyo3(name = "c_m")] + fn py_set_c_m(&mut self, v: f64) { self.inner.c_m = v; } + + #[getter] + #[pyo3(name = "g_l")] + fn py_get_g_l(&self) -> PyResult { Ok(self.inner.g_l) } + + #[setter] + #[pyo3(name = "g_l")] + fn py_set_g_l(&mut self, v: f64) { self.inner.g_l = v; } + + #[getter] + #[pyo3(name = "g_ca")] + fn py_get_g_ca(&self) -> PyResult { Ok(self.inner.g_ca) } + + #[setter] + #[pyo3(name = "g_ca")] + fn py_set_g_ca(&mut self, v: f64) { self.inner.g_ca = v; } + + #[getter] + #[pyo3(name = "g_k")] + fn py_get_g_k(&self) -> PyResult { Ok(self.inner.g_k) } + + #[setter] + #[pyo3(name = "g_k")] + fn py_set_g_k(&mut self, v: f64) { self.inner.g_k = v; } + + #[getter] + #[pyo3(name = "v_l")] + fn py_get_v_l(&self) -> PyResult { Ok(self.inner.v_l) } + + #[setter] + #[pyo3(name = "v_l")] + fn py_set_v_l(&mut self, v: f64) { self.inner.v_l = v; } + + #[getter] + #[pyo3(name = "v_ca")] + fn py_get_v_ca(&self) -> PyResult { Ok(self.inner.v_ca) } + + #[setter] + #[pyo3(name = "v_ca")] + fn py_set_v_ca(&mut self, v: f64) { self.inner.v_ca = v; } + + #[getter] + #[pyo3(name = "v_k")] + fn py_get_v_k(&self) -> PyResult { Ok(self.inner.v_k) } + + #[setter] + #[pyo3(name = "v_k")] + fn py_set_v_k(&mut self, v: f64) { self.inner.v_k = v; } + + #[getter] + #[pyo3(name = "v1")] + fn py_get_v1(&self) -> PyResult { Ok(self.inner.v1) } + + #[setter] + #[pyo3(name = "v1")] + fn py_set_v1(&mut self, v: f64) { self.inner.v1 = v; } + + #[getter] + #[pyo3(name = "v2")] + fn py_get_v2(&self) -> PyResult { Ok(self.inner.v2) } + + #[setter] + #[pyo3(name = "v2")] + fn py_set_v2(&mut self, v: f64) { self.inner.v2 = v; } + + #[getter] + #[pyo3(name = "v3")] + fn py_get_v3(&self) -> PyResult { Ok(self.inner.v3) } + + #[setter] + #[pyo3(name = "v3")] + fn py_set_v3(&mut self, v: f64) { self.inner.v3 = v; } + + #[getter] + #[pyo3(name = "v4")] + fn py_get_v4(&self) -> PyResult { Ok(self.inner.v4) } + + #[setter] + #[pyo3(name = "v4")] + fn py_set_v4(&mut self, v: f64) { self.inner.v4 = v; } + + #[getter] + #[pyo3(name = "phi")] + fn py_get_phi(&self) -> PyResult { Ok(self.inner.phi) } + + #[setter] + #[pyo3(name = "phi")] + fn py_set_phi(&mut self, v: f64) { self.inner.phi = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("MorrisLecar", "MorrisLecar", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Which distance method a bootstrap replicate should use. +/// +/// Rust: `biophysics::phylo::DistanceMethod` +#[pyclass(name = "DistanceMethod", module = "numeria.biophysics.phylo", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyDistanceMethod { + Upgma, + NeighborJoining, +} +impl PyDistanceMethod { + pub fn to_rust(&self) -> rust_physics_engine::biophysics::phylo::DistanceMethod { match self { + Self::Upgma => rust_physics_engine::biophysics::phylo::DistanceMethod::Upgma, + Self::NeighborJoining => rust_physics_engine::biophysics::phylo::DistanceMethod::NeighborJoining, + } } + pub fn from_rust(v: &rust_physics_engine::biophysics::phylo::DistanceMethod) -> Self { match v { + rust_physics_engine::biophysics::phylo::DistanceMethod::Upgma => Self::Upgma, + rust_physics_engine::biophysics::phylo::DistanceMethod::NeighborJoining => Self::NeighborJoining, + } } +} +#[pymethods] +impl PyDistanceMethod { + fn __repr__(&self) -> &'static str { + match self { + Self::Upgma => "DistanceMethod.Upgma", + Self::NeighborJoining => "DistanceMethod.NeighborJoining", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A rooted phylogenetic tree. +/// +/// Nodes `0..leaf_count` are leaves; the rest are internal. The root is the +/// unique node whose parent is `None`. +/// +/// Rust: `biophysics::phylo::PhyloTree` +#[pyclass(name = "PhyloTree", module = "numeria.biophysics.phylo", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyPhyloTree { pub inner: rust_physics_engine::biophysics::phylo::PhyloTree } +#[pymethods] +impl PyPhyloTree { + /// A tree from its arrays, checked for consistency. + /// + /// Errors: + /// Returns an error for mismatched lengths, a negative branch, no root + /// or more than one, a parent index out of range, or a cycle. + /// + /// Rust: `biophysics::phylo::PhyloTree::new` + #[new] + #[pyo3(signature = (parent, branch_length, labels))] + fn __new__(parent: Vec>, branch_length: Vec, labels: Vec) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::phylo::PhyloTree::new(parent, branch_length, labels)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyPhyloTree { inner: __v }) + } + + /// The number of nodes. + /// + /// Rust: `biophysics::phylo::PhyloTree::len` + #[pyo3(name = "len")] + #[pyo3(signature = ())] + fn len(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.len()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Whether the tree has no nodes. Never true for a constructed tree. + /// + /// Rust: `biophysics::phylo::PhyloTree::is_empty` + #[pyo3(name = "is_empty")] + #[pyo3(signature = ())] + fn is_empty(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_empty()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The root. + /// + /// Rust: `biophysics::phylo::PhyloTree::root` + #[pyo3(name = "root")] + #[pyo3(signature = ())] + fn root(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.root()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The children of a node, in index order. + /// + /// Rust: `biophysics::phylo::PhyloTree::children` + #[pyo3(name = "children")] + #[pyo3(signature = (node))] + fn children<'py>(&self, py: Python<'py>, node: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.children(node))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The leaves: nodes with no children, in index order. + /// + /// Rust: `biophysics::phylo::PhyloTree::leaves` + #[pyo3(name = "leaves")] + #[pyo3(signature = ())] + fn leaves<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.leaves())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Whether every internal node has exactly two children. + /// + /// A tree that is not binary has an unresolved node -- a polytomy -- + /// which usually means the data could not distinguish the orders, not + /// that three lineages truly diverged at once. + /// + /// Rust: `biophysics::phylo::PhyloTree::is_binary` + #[pyo3(name = "is_binary")] + #[pyo3(signature = ())] + fn is_binary(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_binary()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The path from a node to the root, inclusive of both. + /// + /// Rust: `biophysics::phylo::PhyloTree::path_to_root` + #[pyo3(name = "path_to_root")] + #[pyo3(signature = (node))] + fn path_to_root<'py>(&self, py: Python<'py>, node: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.path_to_root(node))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The distance from a node to the root, summing branch lengths. + /// + /// Rust: `biophysics::phylo::PhyloTree::depth` + #[pyo3(name = "depth")] + #[pyo3(signature = (node))] + fn depth(&self, node: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.depth(node)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The greatest root-to-leaf distance. + /// + /// Rust: `biophysics::phylo::PhyloTree::height` + #[pyo3(name = "height")] + #[pyo3(signature = ())] + fn height(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.height()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The sum of every branch length. + /// + /// Rust: `biophysics::phylo::PhyloTree::total_length` + #[pyo3(name = "total_length")] + #[pyo3(signature = ())] + fn total_length(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.total_length()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The most recent common ancestor of two nodes. + /// + /// Errors: + /// Returns an error for a node index out of range. + /// + /// Rust: `biophysics::phylo::PhyloTree::mrca` + #[pyo3(name = "mrca")] + #[pyo3(signature = (a, b))] + fn mrca(&self, a: usize, b: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.mrca(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) + } + + /// The patristic distance: the path length between two nodes through + /// their common ancestor. + /// + /// Errors: + /// Returns an error for a node index out of range. + /// + /// Rust: `biophysics::phylo::PhyloTree::distance` + #[pyo3(name = "distance")] + #[pyo3(signature = (a, b))] + fn distance(&self, a: usize, b: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.distance(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) + } + + /// Whether the tree is ultrametric: every leaf the same distance from + /// the root. + /// + /// True under a strict molecular clock and rarely otherwise. UPGMA + /// *imposes* it; neighbour joining does not. + /// + /// Rust: `biophysics::phylo::PhyloTree::is_ultrametric` + #[pyo3(name = "is_ultrametric")] + #[pyo3(signature = (tolerance))] + fn is_ultrametric(&self, tolerance: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_ultrametric(tolerance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The set of leaf labels below each internal node: the tree's splits. + /// + /// Two trees describe the same topology exactly when they induce the + /// same splits, which is what `PhyloTree::robinson_foulds` compares. + /// + /// Rust: `biophysics::phylo::PhyloTree::splits` + #[pyo3(name = "splits")] + #[pyo3(signature = ())] + fn splits<'py>(&self, py: Python<'py>) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.splits())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.into_iter().map(|__x| __x.to_string()).collect::>()).collect::>()) + } + + /// The tree's splits as *unrooted* bipartitions. + /// + /// Each internal node divides the leaves in two, and on an unrooted + /// tree neither side is "below" the other -- `{A,B}` and `{C,D}` on a + /// four-taxon tree name the same branch. Each bipartition is therefore + /// reported by its smaller side, with ties broken alphabetically, so + /// the two descriptions collapse to one. Bipartitions with fewer than + /// two leaves on a side are trivial and omitted. + /// + /// This is what to compare when the rooting is an artefact of the + /// method, as it is for `neighbor_joining`, and what bootstrap + /// support is conventionally reported on. + /// + /// Rust: `biophysics::phylo::PhyloTree::bipartitions` + #[pyo3(name = "bipartitions")] + #[pyo3(signature = ())] + fn bipartitions<'py>(&self, py: Python<'py>) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.bipartitions())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.into_iter().map(|__x| __x.to_string()).collect::>()).collect::>()) + } + + /// The Robinson-Foulds distance: the number of splits present in one + /// tree and not the other. + /// + /// Splits here are rooted clades, so two trees that differ only in + /// where the root sits score above zero. Compare + /// `PhyloTree::bipartitions` instead when the rooting carries no + /// meaning. + /// + /// A topological measure that ignores branch lengths entirely, which is + /// both its use and its weakness -- two trees can differ by one badly + /// placed leaf and score the maximum, so the raw number is hard to + /// interpret without normalising by the possible total. + /// + /// Errors: + /// Returns an error if the two trees do not have the same leaf labels. + /// + /// Rust: `biophysics::phylo::PhyloTree::robinson_foulds` + #[pyo3(name = "robinson_foulds")] + #[pyo3(signature = (other))] + fn robinson_foulds(&self, other: crate::generated::types::PyPhyloTree) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.robinson_foulds(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) + } + + /// The tree in Newick format, with branch lengths. + /// + /// Rust: `biophysics::phylo::PhyloTree::to_newick` + #[pyo3(name = "to_newick")] + #[pyo3(signature = ())] + fn to_newick(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.to_newick()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_string()) + } + + /// Parses a Newick string. + /// + /// Accepts the common subset: nested parentheses, optional labels, and + /// optional `:length` suffixes, terminated by a semicolon. + /// + /// Errors: + /// Returns an error for unbalanced parentheses, a missing semicolon, a + /// malformed branch length, or an empty tree. + /// + /// Rust: `biophysics::phylo::PhyloTree::from_newick` + #[pyo3(name = "from_newick")] + #[staticmethod] + #[pyo3(signature = (text))] + fn from_newick(text: String) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::phylo::PhyloTree::from_newick(&text)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyPhyloTree { inner: __v }) + } + + #[getter] + #[pyo3(name = "parent")] + fn py_get_parent(&self) -> PyResult>> { Ok(self.inner.parent.clone().into_iter().map(|__x| __x.map(|__x| __x)).collect::>()) } + + #[getter] + #[pyo3(name = "branch_length")] + fn py_get_branch_length(&self) -> PyResult> { Ok(self.inner.branch_length.clone()) } + + #[getter] + #[pyo3(name = "labels")] + fn py_get_labels(&self) -> PyResult> { Ok(self.inner.labels.clone().into_iter().map(|__x| __x.to_string()).collect::>()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("PhyloTree", "PhyloTree", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Which of the four outcomes a two-species Lotka-Volterra competition has. +/// +/// Rust: `biophysics::population::Competition` +#[pyclass(name = "Competition", module = "numeria.biophysics.population", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyCompetition { + Coexistence, + FirstExcludes, + SecondExcludes, + FounderControl, +} +impl PyCompetition { + pub fn to_rust(&self) -> rust_physics_engine::biophysics::population::Competition { match self { + Self::Coexistence => rust_physics_engine::biophysics::population::Competition::Coexistence, + Self::FirstExcludes => rust_physics_engine::biophysics::population::Competition::FirstExcludes, + Self::SecondExcludes => rust_physics_engine::biophysics::population::Competition::SecondExcludes, + Self::FounderControl => rust_physics_engine::biophysics::population::Competition::FounderControl, + } } + pub fn from_rust(v: &rust_physics_engine::biophysics::population::Competition) -> Self { match v { + rust_physics_engine::biophysics::population::Competition::Coexistence => Self::Coexistence, + rust_physics_engine::biophysics::population::Competition::FirstExcludes => Self::FirstExcludes, + rust_physics_engine::biophysics::population::Competition::SecondExcludes => Self::SecondExcludes, + rust_physics_engine::biophysics::population::Competition::FounderControl => Self::FounderControl, + } } +} +#[pymethods] +impl PyCompetition { + fn __repr__(&self) -> &'static str { + match self { + Self::Coexistence => "Competition.Coexistence", + Self::FirstExcludes => "Competition.FirstExcludes", + Self::SecondExcludes => "Competition.SecondExcludes", + Self::FounderControl => "Competition.FounderControl", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A substitution and gap scoring scheme. +/// +/// Rust: `biophysics::seq_align::Scoring` +#[pyclass(name = "Scoring", module = "numeria.biophysics.seq_align", from_py_object)] +#[derive(Clone)] +pub struct PyScoring { pub inner: rust_physics_engine::biophysics::seq_align::Scoring } +#[pymethods] +impl PyScoring { + /// Builds a `Scoring` from its fields. + #[new] + #[pyo3(signature = (match_score, mismatch, gap, matrix))] + fn __new__(match_score: i64, mismatch: i64, gap: i64, matrix: Option) -> Self { + let matrix = matrix.map(|__o| __o.inner); + Self { inner: rust_physics_engine::biophysics::seq_align::Scoring { match_score: match_score, mismatch: mismatch, gap: gap, matrix: matrix } } + } + + /// A simple scheme with no substitution matrix. + /// + /// Rust: `biophysics::seq_align::Scoring::simple` + #[pyo3(name = "simple")] + #[staticmethod] + #[pyo3(signature = (match_score, mismatch, gap))] + fn simple(match_score: i64, mismatch: i64, gap: i64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::biophysics::seq_align::Scoring::simple(match_score, mismatch, gap)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyScoring { inner: __v }) + } + + /// The score of substituting one residue for another. + /// + /// Rust: `biophysics::seq_align::Scoring::substitution` + #[pyo3(name = "substitution")] + #[pyo3(signature = (a, b))] + fn substitution(&self, a: u8, b: u8) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.substitution(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "match_score")] + fn py_get_match_score(&self) -> PyResult { Ok(self.inner.match_score) } + + #[setter] + #[pyo3(name = "match_score")] + fn py_set_match_score(&mut self, v: i64) { self.inner.match_score = v; } + + #[getter] + #[pyo3(name = "mismatch")] + fn py_get_mismatch(&self) -> PyResult { Ok(self.inner.mismatch) } + + #[setter] + #[pyo3(name = "mismatch")] + fn py_set_mismatch(&mut self, v: i64) { self.inner.mismatch = v; } + + #[getter] + #[pyo3(name = "gap")] + fn py_get_gap(&self) -> PyResult { Ok(self.inner.gap) } + + #[setter] + #[pyo3(name = "gap")] + fn py_set_gap(&mut self, v: i64) { self.inner.gap = v; } + + #[getter] + #[pyo3(name = "matrix")] + fn py_get_matrix(&self) -> PyResult> { Ok(self.inner.matrix.clone().map(|__x| crate::generated::types::PySubstitutionMatrix { inner: __x })) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Scoring", "Scoring", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A named substitution matrix over an alphabet. +/// +/// Rust: `biophysics::seq_align::SubstitutionMatrix` +#[pyclass(name = "SubstitutionMatrix", module = "numeria.biophysics.seq_align", from_py_object)] +#[derive(Clone)] +pub struct PySubstitutionMatrix { pub inner: rust_physics_engine::biophysics::seq_align::SubstitutionMatrix } +#[pymethods] +impl PySubstitutionMatrix { + /// Builds a `SubstitutionMatrix` from its fields. + #[new] + #[pyo3(signature = (alphabet, scores))] + fn __new__(alphabet: Vec, scores: Vec) -> Self { + + Self { inner: rust_physics_engine::biophysics::seq_align::SubstitutionMatrix { alphabet: alphabet, scores: scores } } + } + + /// The score for a pair of residues, or `None` if either is outside the + /// alphabet. + /// + /// Rust: `biophysics::seq_align::SubstitutionMatrix::lookup` + #[pyo3(name = "lookup")] + #[pyo3(signature = (a, b))] + fn lookup(&self, a: u8, b: u8) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.lookup(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) + } + + /// Whether the matrix is symmetric, as every substitution matrix + /// derived from a symmetric alignment count must be. + /// + /// Rust: `biophysics::seq_align::SubstitutionMatrix::is_symmetric` + #[pyo3(name = "is_symmetric")] + #[pyo3(signature = ())] + fn is_symmetric(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_symmetric()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "alphabet")] + fn py_get_alphabet(&self) -> PyResult> { Ok(self.inner.alphabet.clone()) } + + #[getter] + #[pyo3(name = "scores")] + fn py_get_scores(&self) -> PyResult> { Ok(self.inner.scores.clone()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("SubstitutionMatrix", "SubstitutionMatrix", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} diff --git a/bindings/python/src/generated/types/cfd.rs b/bindings/python/src/generated/types/cfd.rs new file mode 100644 index 0000000..03496c4 --- /dev/null +++ b/bindings/python/src/generated/types/cfd.rs @@ -0,0 +1,4649 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; + + +/// Slope limiters for MUSCL-type schemes. +/// +/// Rust: `cfd::advection::Limiter` +#[pyclass(name = "Limiter", module = "numeria.cfd.advection", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyAdvectionLimiter { + Minmod, + VanLeer, + Superbee, + Mc, + VanAlbada, + Koren, +} +impl PyAdvectionLimiter { + pub fn to_rust(&self) -> rust_physics_engine::cfd::advection::Limiter { match self { + Self::Minmod => rust_physics_engine::cfd::advection::Limiter::Minmod, + Self::VanLeer => rust_physics_engine::cfd::advection::Limiter::VanLeer, + Self::Superbee => rust_physics_engine::cfd::advection::Limiter::Superbee, + Self::Mc => rust_physics_engine::cfd::advection::Limiter::Mc, + Self::VanAlbada => rust_physics_engine::cfd::advection::Limiter::VanAlbada, + Self::Koren => rust_physics_engine::cfd::advection::Limiter::Koren, + } } + pub fn from_rust(v: &rust_physics_engine::cfd::advection::Limiter) -> Self { match v { + rust_physics_engine::cfd::advection::Limiter::Minmod => Self::Minmod, + rust_physics_engine::cfd::advection::Limiter::VanLeer => Self::VanLeer, + rust_physics_engine::cfd::advection::Limiter::Superbee => Self::Superbee, + rust_physics_engine::cfd::advection::Limiter::Mc => Self::Mc, + rust_physics_engine::cfd::advection::Limiter::VanAlbada => Self::VanAlbada, + rust_physics_engine::cfd::advection::Limiter::Koren => Self::Koren, + } } +} +#[pymethods] +impl PyAdvectionLimiter { + fn __repr__(&self) -> &'static str { + match self { + Self::Minmod => "Limiter.Minmod", + Self::VanLeer => "Limiter.VanLeer", + Self::Superbee => "Limiter.Superbee", + Self::Mc => "Limiter.Mc", + Self::VanAlbada => "Limiter.VanAlbada", + Self::Koren => "Limiter.Koren", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Spatial scheme selector for Burgers / advection-diffusion steps. +/// +/// Rust: `cfd::advection::Scheme` +#[pyclass(name = "Scheme", module = "numeria.cfd.advection", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyScheme { pub inner: rust_physics_engine::cfd::advection::Scheme } +#[pymethods] +impl PyScheme { + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Scheme", "Scheme", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Cell-centered scalar field on the same layout as `MacGrid2` +/// pressure cells; node (i, j) sits at world ((i+0.5) dx, (j+0.5) dx). +/// +/// Rust: `cfd::grid::CellField2` +#[pyclass(name = "CellField2", module = "numeria.cfd.grid", from_py_object)] +#[derive(Clone)] +pub struct PyCellField2 { pub inner: rust_physics_engine::cfd::grid::CellField2 } +#[pymethods] +impl PyCellField2 { + /// New zero field. + /// + /// Rust: `cfd::grid::CellField2::new` + #[new] + #[pyo3(signature = (nx, ny, dx))] + fn __new__(nx: usize, ny: usize, dx: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::grid::CellField2::new(nx, ny, dx)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCellField2 { inner: __v }) + } + + /// Build from a function of cell-center world coordinates. + /// + /// Rust: `cfd::grid::CellField2::from_fn` + #[pyo3(name = "from_fn")] + #[staticmethod] + #[pyo3(signature = (nx, ny, dx, f))] + fn from_fn(nx: usize, ny: usize, dx: f64, f: pyo3::Py) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64, __a1: f64| -> f64 { __cb.call::<_, f64>((__a0, __a1), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::grid::CellField2::from_fn(nx, ny, dx, f)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCellField2 { inner: __v }) + } + + /// Value at cell (i, j). + /// + /// Rust: `cfd::grid::CellField2::at` + #[pyo3(name = "at")] + #[pyo3(signature = (i, j))] + fn at(&self, i: usize, j: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.at(i, j)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Bilinear sample at a world position (clamped at the borders). + /// + /// Rust: `cfd::grid::CellField2::sample` + #[pyo3(name = "sample")] + #[pyo3(signature = (p))] + fn sample(&self, p: crate::generated::types::PyVec2Arg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.sample(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Clamped Catmull-Rom (monotone-limited) sample at a world position. + /// + /// Rust: `cfd::grid::CellField2::sample_cubic` + #[pyo3(name = "sample_cubic")] + #[pyo3(signature = (p))] + fn sample_cubic(&self, p: crate::generated::types::PyVec2Arg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.sample_cubic(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "nx")] + fn py_get_nx(&self) -> PyResult { Ok(self.inner.nx) } + + #[setter] + #[pyo3(name = "nx")] + fn py_set_nx(&mut self, v: usize) { self.inner.nx = v; } + + #[getter] + #[pyo3(name = "ny")] + fn py_get_ny(&self) -> PyResult { Ok(self.inner.ny) } + + #[setter] + #[pyo3(name = "ny")] + fn py_set_ny(&mut self, v: usize) { self.inner.ny = v; } + + #[getter] + #[pyo3(name = "dx")] + fn py_get_dx(&self) -> PyResult { Ok(self.inner.dx) } + + #[setter] + #[pyo3(name = "dx")] + fn py_set_dx(&mut self, v: f64) { self.inner.dx = v; } + + #[getter] + #[pyo3(name = "data")] + fn py_get_data(&self) -> PyResult> { Ok(self.inner.data.clone()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("CellField2", "CellField2", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Boundary condition for the velocity field. +/// +/// Rust: `cfd::grid::FluidBc` +#[pyclass(name = "FluidBc", module = "numeria.cfd.grid", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyFluidBc { pub inner: rust_physics_engine::cfd::grid::FluidBc } +#[pymethods] +impl PyFluidBc { + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("FluidBc", "FluidBc", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// 2D marker-and-cell grid: `u` on vertical faces ((nx+1) × ny), `v` on +/// horizontal faces (nx × (ny+1)), pressure and solid flags at cell +/// centers. Cell (i, j) spans [i·dx, (i+1)·dx) × [j·dx, (j+1)·dx). +/// +/// Rust: `cfd::grid::MacGrid2` +#[pyclass(name = "MacGrid2", module = "numeria.cfd.grid")] +pub struct PyMacGrid2 { pub inner: rust_physics_engine::cfd::grid::MacGrid2 } +#[pymethods] +impl PyMacGrid2 { + /// New grid with all fields zero and no solids. + /// + /// Rust: `cfd::grid::MacGrid2::new` + #[new] + #[pyo3(signature = (nx, ny, dx))] + fn __new__(nx: usize, ny: usize, dx: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::grid::MacGrid2::new(nx, ny, dx)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMacGrid2 { inner: __v }) + } + + /// Index into `u` (i in 0..=nx, j in 0..ny). + /// + /// Rust: `cfd::grid::MacGrid2::u_idx` + #[pyo3(name = "u_idx")] + #[pyo3(signature = (i, j))] + fn u_idx(&self, i: usize, j: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.u_idx(i, j)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Index into `v` (i in 0..nx, j in 0..=ny). + /// + /// Rust: `cfd::grid::MacGrid2::v_idx` + #[pyo3(name = "v_idx")] + #[pyo3(signature = (i, j))] + fn v_idx(&self, i: usize, j: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.v_idx(i, j)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Index into cell-centered arrays. + /// + /// Rust: `cfd::grid::MacGrid2::c_idx` + #[pyo3(name = "c_idx")] + #[pyo3(signature = (i, j))] + fn c_idx(&self, i: usize, j: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.c_idx(i, j)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// u face value at (i, j). + /// + /// Rust: `cfd::grid::MacGrid2::u_at` + #[pyo3(name = "u_at")] + #[pyo3(signature = (i, j))] + fn u_at(&self, i: usize, j: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.u_at(i, j)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// v face value at (i, j). + /// + /// Rust: `cfd::grid::MacGrid2::v_at` + #[pyo3(name = "v_at")] + #[pyo3(signature = (i, j))] + fn v_at(&self, i: usize, j: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.v_at(i, j)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Bilinearly interpolated velocity at a world position. + /// + /// Rust: `cfd::grid::MacGrid2::velocity_at` + #[pyo3(name = "velocity_at")] + #[pyo3(signature = (p))] + fn velocity_at(&self, p: crate::generated::types::PyVec2Arg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.velocity_at(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) + } + + /// Cell-centered divergence (1/s). + /// + /// Rust: `cfd::grid::MacGrid2::divergence` + #[pyo3(name = "divergence")] + #[pyo3(signature = ())] + fn divergence<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.divergence())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Cell-centered vorticity ω = ∂v/∂x − ∂u/∂y (central differences of + /// face-averaged components). + /// + /// Rust: `cfd::grid::MacGrid2::curl` + #[pyo3(name = "curl")] + #[pyo3(signature = ())] + fn curl<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.curl())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Largest face-velocity magnitude. + /// + /// Rust: `cfd::grid::MacGrid2::max_velocity` + #[pyo3(name = "max_velocity")] + #[pyo3(signature = ())] + fn max_velocity(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.max_velocity()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Time step honoring the CFL number. + /// + /// Rust: `cfd::grid::MacGrid2::cfl_dt` + #[pyo3(name = "cfl_dt")] + #[pyo3(signature = (cfl))] + fn cfl_dt(&self, cfl: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.cfl_dt(cfl)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Mark cells inside a world-space axis-aligned box as solid and + /// zero their faces. + /// + /// Rust: `cfd::grid::MacGrid2::set_solid_box` + #[pyo3(name = "set_solid_box")] + #[pyo3(signature = (x0, y0, x1, y1))] + fn set_solid_box(&mut self, x0: f64, y0: f64, x1: f64, y1: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.set_solid_box(x0, y0, x1, y1)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Mark cells inside a circle as solid and zero their faces. + /// + /// Rust: `cfd::grid::MacGrid2::set_solid_circle` + #[pyo3(name = "set_solid_circle")] + #[pyo3(signature = (cx, cy, r))] + fn set_solid_circle(&mut self, cx: f64, cy: f64, r: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.set_solid_circle(cx, cy, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Apply a domain boundary condition to the face velocities. + /// + /// Rust: `cfd::grid::MacGrid2::apply_bc` + #[pyo3(name = "apply_bc")] + #[pyo3(signature = (bc))] + fn apply_bc(&mut self, bc: crate::generated::types::PyFluidBc) -> PyResult<()> { + let bc = bc.inner; + let __r = crate::runtime::guard(|| self.inner.apply_bc(bc)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Total kinetic energy 0.5 Σ |v|² dx² (unit density). + /// + /// Rust: `cfd::grid::MacGrid2::kinetic_energy` + #[pyo3(name = "kinetic_energy")] + #[pyo3(signature = ())] + fn kinetic_energy(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.kinetic_energy()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Total enstrophy 0.5 Σ ω² dx². + /// + /// Rust: `cfd::grid::MacGrid2::enstrophy` + #[pyo3(name = "enstrophy")] + #[pyo3(signature = ())] + fn enstrophy(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.enstrophy()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "nx")] + fn py_get_nx(&self) -> PyResult { Ok(self.inner.nx) } + + #[setter] + #[pyo3(name = "nx")] + fn py_set_nx(&mut self, v: usize) { self.inner.nx = v; } + + #[getter] + #[pyo3(name = "ny")] + fn py_get_ny(&self) -> PyResult { Ok(self.inner.ny) } + + #[setter] + #[pyo3(name = "ny")] + fn py_set_ny(&mut self, v: usize) { self.inner.ny = v; } + + #[getter] + #[pyo3(name = "dx")] + fn py_get_dx(&self) -> PyResult { Ok(self.inner.dx) } + + #[setter] + #[pyo3(name = "dx")] + fn py_set_dx(&mut self, v: f64) { self.inner.dx = v; } + + #[getter] + #[pyo3(name = "u")] + fn py_get_u(&self) -> PyResult> { Ok(self.inner.u.clone()) } + + #[getter] + #[pyo3(name = "v")] + fn py_get_v(&self) -> PyResult> { Ok(self.inner.v.clone()) } + + #[getter] + #[pyo3(name = "p")] + fn py_get_p(&self) -> PyResult> { Ok(self.inner.p.clone()) } + + #[getter] + #[pyo3(name = "solid")] + fn py_get_solid(&self) -> PyResult> { Ok(self.inner.solid.clone()) } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// 3D MAC grid (faces staggered per axis). +/// +/// Rust: `cfd::grid::MacGrid3` +#[pyclass(name = "MacGrid3", module = "numeria.cfd.grid")] +pub struct PyMacGrid3 { pub inner: rust_physics_engine::cfd::grid::MacGrid3 } +#[pymethods] +impl PyMacGrid3 { + /// New grid with all fields zero. + /// + /// Rust: `cfd::grid::MacGrid3::new` + #[new] + #[pyo3(signature = (nx, ny, nz, dx))] + fn __new__(nx: usize, ny: usize, nz: usize, dx: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::grid::MacGrid3::new(nx, ny, nz, dx)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMacGrid3 { inner: __v }) + } + + /// u face value. + /// + /// Rust: `cfd::grid::MacGrid3::u_at` + #[pyo3(name = "u_at")] + #[pyo3(signature = (i, j, k))] + fn u_at(&self, i: usize, j: usize, k: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.u_at(i, j, k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// v face value. + /// + /// Rust: `cfd::grid::MacGrid3::v_at` + #[pyo3(name = "v_at")] + #[pyo3(signature = (i, j, k))] + fn v_at(&self, i: usize, j: usize, k: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.v_at(i, j, k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// w face value. + /// + /// Rust: `cfd::grid::MacGrid3::w_at` + #[pyo3(name = "w_at")] + #[pyo3(signature = (i, j, k))] + fn w_at(&self, i: usize, j: usize, k: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.w_at(i, j, k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Trilinearly interpolated velocity at a world position. + /// + /// Rust: `cfd::grid::MacGrid3::velocity_at` + #[pyo3(name = "velocity_at")] + #[pyo3(signature = (p))] + fn velocity_at(&self, p: crate::generated::types::PyVec3Arg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.velocity_at(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Cell-centered divergence. + /// + /// Rust: `cfd::grid::MacGrid3::divergence` + #[pyo3(name = "divergence")] + #[pyo3(signature = ())] + fn divergence<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.divergence())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Largest face-velocity magnitude bound. + /// + /// Rust: `cfd::grid::MacGrid3::max_velocity` + #[pyo3(name = "max_velocity")] + #[pyo3(signature = ())] + fn max_velocity(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.max_velocity()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Time step honoring the CFL number. + /// + /// Rust: `cfd::grid::MacGrid3::cfl_dt` + #[pyo3(name = "cfl_dt")] + #[pyo3(signature = (cfl))] + fn cfl_dt(&self, cfl: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.cfl_dt(cfl)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "nx")] + fn py_get_nx(&self) -> PyResult { Ok(self.inner.nx) } + + #[setter] + #[pyo3(name = "nx")] + fn py_set_nx(&mut self, v: usize) { self.inner.nx = v; } + + #[getter] + #[pyo3(name = "ny")] + fn py_get_ny(&self) -> PyResult { Ok(self.inner.ny) } + + #[setter] + #[pyo3(name = "ny")] + fn py_set_ny(&mut self, v: usize) { self.inner.ny = v; } + + #[getter] + #[pyo3(name = "nz")] + fn py_get_nz(&self) -> PyResult { Ok(self.inner.nz) } + + #[setter] + #[pyo3(name = "nz")] + fn py_set_nz(&mut self, v: usize) { self.inner.nz = v; } + + #[getter] + #[pyo3(name = "dx")] + fn py_get_dx(&self) -> PyResult { Ok(self.inner.dx) } + + #[setter] + #[pyo3(name = "dx")] + fn py_set_dx(&mut self, v: f64) { self.inner.dx = v; } + + #[getter] + #[pyo3(name = "u")] + fn py_get_u(&self) -> PyResult> { Ok(self.inner.u.clone()) } + + #[getter] + #[pyo3(name = "v")] + fn py_get_v(&self) -> PyResult> { Ok(self.inner.v.clone()) } + + #[getter] + #[pyo3(name = "w")] + fn py_get_w(&self) -> PyResult> { Ok(self.inner.w.clone()) } + + #[getter] + #[pyo3(name = "p")] + fn py_get_p(&self) -> PyResult> { Ok(self.inner.p.clone()) } + + #[getter] + #[pyo3(name = "solid")] + fn py_get_solid(&self) -> PyResult> { Ok(self.inner.solid.clone()) } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// Collision operator selector. +/// +/// Rust: `cfd::lbm::Collision` +#[pyclass(name = "Collision", module = "numeria.cfd.lbm", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyCollision { pub inner: rust_physics_engine::cfd::lbm::Collision } +#[pymethods] +impl PyCollision { + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Collision", "Collision", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// D2Q9 lattice Boltzmann solver. +/// +/// Rust: `cfd::lbm::LbmD2Q9` +#[pyclass(name = "LbmD2Q9", module = "numeria.cfd.lbm")] +pub struct PyLbmD2Q9 { pub inner: rust_physics_engine::cfd::lbm::LbmD2Q9 } +#[pymethods] +impl PyLbmD2Q9 { + /// Quiescent unit-density fluid. + /// + /// Rust: `cfd::lbm::LbmD2Q9::new` + #[new] + #[pyo3(signature = (nx, ny, tau))] + fn __new__(nx: usize, ny: usize, tau: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::lbm::LbmD2Q9::new(nx, ny, tau)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLbmD2Q9 { inner: __v }) + } + + /// Reset every node to equilibrium at (rho, u). + /// + /// Rust: `cfd::lbm::LbmD2Q9::init_equilibrium` + #[pyo3(name = "init_equilibrium")] + #[pyo3(signature = (rho, u))] + fn init_equilibrium(&mut self, rho: f64, u: crate::generated::types::PyVec2Arg) -> PyResult<()> { + let u = u.0; + let __r = crate::runtime::guard(|| self.inner.init_equilibrium(rho, u)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Kinematic viscosity ν = (τ − 1/2)/3 in lattice units. + /// + /// Rust: `cfd::lbm::LbmD2Q9::viscosity` + #[pyo3(name = "viscosity")] + #[pyo3(signature = ())] + fn viscosity(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.viscosity()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Densities at all nodes. + /// + /// Rust: `cfd::lbm::LbmD2Q9::density` + #[pyo3(name = "density")] + #[pyo3(signature = ())] + fn density<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.density())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Velocities at all nodes (forcing shift included). + /// + /// Rust: `cfd::lbm::LbmD2Q9::velocity` + #[pyo3(name = "velocity")] + #[pyo3(signature = ())] + fn velocity(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.velocity()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) + } + + /// Largest lattice Mach number |u|/c_s. + /// + /// Rust: `cfd::lbm::LbmD2Q9::mach_max` + #[pyo3(name = "mach_max")] + #[pyo3(signature = ())] + fn mach_max(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.mach_max()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Collision step (with Guo forcing). + /// + /// Rust: `cfd::lbm::LbmD2Q9::collide` + #[pyo3(name = "collide")] + #[pyo3(signature = ())] + fn collide(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.collide()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Streaming step (periodic wrap; solids handled by bounce-back). + /// + /// Rust: `cfd::lbm::LbmD2Q9::stream` + #[pyo3(name = "stream")] + #[pyo3(signature = ())] + fn stream(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.stream()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Half-way bounce-back on solid nodes. + /// + /// Rust: `cfd::lbm::LbmD2Q9::bounce_back` + #[pyo3(name = "bounce_back")] + #[pyo3(signature = ())] + fn bounce_back(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.bounce_back()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Zou-He velocity inlet on the left wall (x = 0), horizontal + /// velocity `u`. + /// + /// Rust: `cfd::lbm::LbmD2Q9::zou_he_velocity_inlet` + #[pyo3(name = "zou_he_velocity_inlet")] + #[pyo3(signature = (u))] + fn zou_he_velocity_inlet(&mut self, u: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.zou_he_velocity_inlet(u)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Zou-He pressure (density) outlet on the right wall. + /// + /// Rust: `cfd::lbm::LbmD2Q9::zou_he_pressure_outlet` + #[pyo3(name = "zou_he_pressure_outlet")] + #[pyo3(signature = (rho))] + fn zou_he_pressure_outlet(&mut self, rho: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.zou_he_pressure_outlet(rho)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Make both axes periodic (clears open boundaries). + /// + /// Rust: `cfd::lbm::LbmD2Q9::periodic` + #[pyo3(name = "periodic")] + #[pyo3(signature = ())] + fn periodic(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.periodic()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// One full update: collide, stream, bounce-back, boundaries. + /// + /// Rust: `cfd::lbm::LbmD2Q9::step` + #[pyo3(name = "step")] + #[pyo3(signature = ())] + fn step(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Run `n` steps. + /// + /// Rust: `cfd::lbm::LbmD2Q9::run` + #[pyo3(name = "run")] + #[pyo3(signature = (n))] + fn run(&mut self, n: usize) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.run(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Momentum-exchange drag on the solid set (lattice units). + /// + /// Rust: `cfd::lbm::LbmD2Q9::drag_on_solid` + #[pyo3(name = "drag_on_solid")] + #[pyo3(signature = ())] + fn drag_on_solid(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.drag_on_solid()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) + } + + /// Vorticity at interior nodes (central differences). + /// + /// Rust: `cfd::lbm::LbmD2Q9::vorticity` + #[pyo3(name = "vorticity")] + #[pyo3(signature = ())] + fn vorticity<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.vorticity())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "nx")] + fn py_get_nx(&self) -> PyResult { Ok(self.inner.nx) } + + #[setter] + #[pyo3(name = "nx")] + fn py_set_nx(&mut self, v: usize) { self.inner.nx = v; } + + #[getter] + #[pyo3(name = "ny")] + fn py_get_ny(&self) -> PyResult { Ok(self.inner.ny) } + + #[setter] + #[pyo3(name = "ny")] + fn py_set_ny(&mut self, v: usize) { self.inner.ny = v; } + + #[getter] + #[pyo3(name = "f")] + fn py_get_f(&self) -> PyResult>> { Ok(self.inner.f.clone().into_iter().map(|__x| __x.to_vec()).collect::>()) } + + #[getter] + #[pyo3(name = "tau")] + fn py_get_tau(&self) -> PyResult { Ok(self.inner.tau) } + + #[setter] + #[pyo3(name = "tau")] + fn py_set_tau(&mut self, v: f64) { self.inner.tau = v; } + + #[getter] + #[pyo3(name = "solid")] + fn py_get_solid(&self) -> PyResult> { Ok(self.inner.solid.clone()) } + + #[getter] + #[pyo3(name = "force")] + fn py_get_force(&self) -> PyResult { Ok(crate::generated::types::PyVec2 { inner: self.inner.force.clone() }) } + + #[getter] + #[pyo3(name = "collision")] + fn py_get_collision(&self) -> PyResult { Ok(crate::generated::types::PyCollision { inner: self.inner.collision.clone() }) } + + #[getter] + #[pyo3(name = "periodic_x")] + fn py_get_periodic_x(&self) -> PyResult { Ok(self.inner.periodic_x) } + + #[setter] + #[pyo3(name = "periodic_x")] + fn py_set_periodic_x(&mut self, v: bool) { self.inner.periodic_x = v; } + + #[getter] + #[pyo3(name = "periodic_y")] + fn py_get_periodic_y(&self) -> PyResult { Ok(self.inner.periodic_y) } + + #[setter] + #[pyo3(name = "periodic_y")] + fn py_set_periodic_y(&mut self, v: bool) { self.inner.periodic_y = v; } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// D3Q19 BGK lattice Boltzmann solver (periodic + bounce-back). +/// +/// Rust: `cfd::lbm::LbmD3Q19` +#[pyclass(name = "LbmD3Q19", module = "numeria.cfd.lbm")] +pub struct PyLbmD3Q19 { pub inner: rust_physics_engine::cfd::lbm::LbmD3Q19 } +#[pymethods] +impl PyLbmD3Q19 { + /// Quiescent unit-density fluid. + /// + /// Rust: `cfd::lbm::LbmD3Q19::new` + #[new] + #[pyo3(signature = (nx, ny, nz, tau))] + fn __new__(nx: usize, ny: usize, nz: usize, tau: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::lbm::LbmD3Q19::new(nx, ny, nz, tau)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLbmD3Q19 { inner: __v }) + } + + /// Kinematic viscosity. + /// + /// Rust: `cfd::lbm::LbmD3Q19::viscosity` + #[pyo3(name = "viscosity")] + #[pyo3(signature = ())] + fn viscosity(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.viscosity()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Node velocities. + /// + /// Rust: `cfd::lbm::LbmD3Q19::velocity` + #[pyo3(name = "velocity")] + #[pyo3(signature = ())] + fn velocity(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.velocity()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) + } + + /// One BGK step with Guo forcing. + /// + /// Rust: `cfd::lbm::LbmD3Q19::step` + #[pyo3(name = "step")] + #[pyo3(signature = ())] + fn step(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + #[getter] + #[pyo3(name = "nx")] + fn py_get_nx(&self) -> PyResult { Ok(self.inner.nx) } + + #[setter] + #[pyo3(name = "nx")] + fn py_set_nx(&mut self, v: usize) { self.inner.nx = v; } + + #[getter] + #[pyo3(name = "ny")] + fn py_get_ny(&self) -> PyResult { Ok(self.inner.ny) } + + #[setter] + #[pyo3(name = "ny")] + fn py_set_ny(&mut self, v: usize) { self.inner.ny = v; } + + #[getter] + #[pyo3(name = "nz")] + fn py_get_nz(&self) -> PyResult { Ok(self.inner.nz) } + + #[setter] + #[pyo3(name = "nz")] + fn py_set_nz(&mut self, v: usize) { self.inner.nz = v; } + + #[getter] + #[pyo3(name = "f")] + fn py_get_f(&self) -> PyResult>> { Ok(self.inner.f.clone().into_iter().map(|__x| __x.to_vec()).collect::>()) } + + #[getter] + #[pyo3(name = "tau")] + fn py_get_tau(&self) -> PyResult { Ok(self.inner.tau) } + + #[setter] + #[pyo3(name = "tau")] + fn py_set_tau(&mut self, v: f64) { self.inner.tau = v; } + + #[getter] + #[pyo3(name = "solid")] + fn py_get_solid(&self) -> PyResult> { Ok(self.inner.solid.clone()) } + + #[getter] + #[pyo3(name = "force")] + fn py_get_force(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.force.clone() }) } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// D3Q27 lattice constants (velocities and weights), for custom solvers. +/// +/// Rust: `cfd::lbm::LbmD3Q27` +#[pyclass(name = "LbmD3Q27", module = "numeria.cfd.lbm")] +pub struct PyLbmD3Q27 { pub inner: rust_physics_engine::cfd::lbm::LbmD3Q27 } +#[pymethods] +impl PyLbmD3Q27 { + /// The 27 lattice velocities. + /// + /// Rust: `cfd::lbm::LbmD3Q27::velocities` + #[pyo3(name = "velocities")] + #[staticmethod] + #[pyo3(signature = ())] + fn velocities<'py>(py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::cfd::lbm::LbmD3Q27::velocities())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, __x.2)).collect::>()) + } + + /// Weight of velocity (x, y, z). + /// + /// Rust: `cfd::lbm::LbmD3Q27::weight` + #[pyo3(name = "weight")] + #[staticmethod] + #[pyo3(signature = (e))] + fn weight(e: (i64, i64, i64)) -> PyResult { + let e = (e.0, e.1, e.2); + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::lbm::LbmD3Q27::weight(e)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// Level-set free-surface liquid on a stable-fluids solver (CSF surface +/// tension, gravity restricted to the liquid). +/// +/// Rust: `cfd::level_set::FreeSurfaceFluid2` +#[pyclass(name = "FreeSurfaceFluid2", module = "numeria.cfd.level_set")] +pub struct PyFreeSurfaceFluid2 { pub inner: rust_physics_engine::cfd::level_set::FreeSurfaceFluid2 } +#[pymethods] +impl PyFreeSurfaceFluid2 { + /// New free-surface solver on an n × n unit grid. + /// + /// Rust: `cfd::level_set::FreeSurfaceFluid2::new` + #[new] + #[pyo3(signature = (nx, ny, dx))] + fn __new__(nx: usize, ny: usize, dx: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::level_set::FreeSurfaceFluid2::new(nx, ny, dx)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyFreeSurfaceFluid2 { inner: __v }) + } + + /// One step: liquid-weighted gravity, CSF surface tension, project, + /// advect the interface (WENO), periodic reinitialization. + /// + /// Rust: `cfd::level_set::FreeSurfaceFluid2::step` + #[pyo3(name = "step")] + #[pyo3(signature = (dt))] + fn step(&mut self, dt: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step(dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Water column against the left wall. + /// + /// Rust: `cfd::level_set::FreeSurfaceFluid2::dam_break` + #[pyo3(name = "dam_break")] + #[staticmethod] + #[pyo3(signature = (nx, ny, dx))] + fn dam_break(nx: usize, ny: usize, dx: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::level_set::FreeSurfaceFluid2::dam_break(nx, ny, dx)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyFreeSurfaceFluid2 { inner: __v }) + } + + /// Falling droplet above a pool. + /// + /// Rust: `cfd::level_set::FreeSurfaceFluid2::droplet_fall` + #[pyo3(name = "droplet_fall")] + #[staticmethod] + #[pyo3(signature = (nx, ny, dx))] + fn droplet_fall(nx: usize, ny: usize, dx: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::level_set::FreeSurfaceFluid2::droplet_fall(nx, ny, dx)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyFreeSurfaceFluid2 { inner: __v }) + } + + /// Light bubble rising in liquid. + /// + /// Rust: `cfd::level_set::FreeSurfaceFluid2::rising_bubble` + #[pyo3(name = "rising_bubble")] + #[staticmethod] + #[pyo3(signature = (nx, ny, dx))] + fn rising_bubble(nx: usize, ny: usize, dx: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::level_set::FreeSurfaceFluid2::rising_bubble(nx, ny, dx)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyFreeSurfaceFluid2 { inner: __v }) + } + + /// Sloshing tank driven by a horizontal oscillation. + /// + /// Rust: `cfd::level_set::FreeSurfaceFluid2::sloshing_tank` + #[pyo3(name = "sloshing_tank")] + #[staticmethod] + #[pyo3(signature = (nx, ny, dx, amplitude, omega))] + fn sloshing_tank(nx: usize, ny: usize, dx: f64, amplitude: f64, omega: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::level_set::FreeSurfaceFluid2::sloshing_tank(nx, ny, dx, amplitude, omega)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyFreeSurfaceFluid2 { inner: __v }) + } + + #[getter] + #[pyo3(name = "surface_tension")] + fn py_get_surface_tension(&self) -> PyResult { Ok(self.inner.surface_tension) } + + #[setter] + #[pyo3(name = "surface_tension")] + fn py_set_surface_tension(&mut self, v: f64) { self.inner.surface_tension = v; } + + #[getter] + #[pyo3(name = "density_ratio")] + fn py_get_density_ratio(&self) -> PyResult { Ok(self.inner.density_ratio) } + + #[setter] + #[pyo3(name = "density_ratio")] + fn py_set_density_ratio(&mut self, v: f64) { self.inner.density_ratio = v; } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// 2D signed distance level set (φ < 0 inside). +/// +/// Rust: `cfd::level_set::LevelSet2` +#[pyclass(name = "LevelSet2", module = "numeria.cfd.level_set")] +pub struct PyLevelSet2 { pub inner: rust_physics_engine::cfd::level_set::LevelSet2 } +#[pymethods] +impl PyLevelSet2 { + /// Builds a `LevelSet2` from its fields. + #[new] + #[pyo3(signature = (phi, band))] + fn __new__(phi: crate::generated::types::PyCellField2, band: Option) -> Self { + let phi = phi.inner; + Self { inner: rust_physics_engine::cfd::level_set::LevelSet2 { phi: phi, band: band } } + } + + /// Build from a signed distance function of world coordinates. + /// + /// Rust: `cfd::level_set::LevelSet2::from_sdf` + #[pyo3(name = "from_sdf")] + #[staticmethod] + #[pyo3(signature = (f, nx, ny, dx))] + fn from_sdf(f: pyo3::Py, nx: usize, ny: usize, dx: f64) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64, __a1: f64| -> f64 { __cb.call::<_, f64>((__a0, __a1), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::level_set::LevelSet2::from_sdf(f, nx, ny, dx)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLevelSet2 { inner: __v }) + } + + /// Circle of radius r centered at (cx, cy). + /// + /// Rust: `cfd::level_set::LevelSet2::circle` + #[pyo3(name = "circle")] + #[staticmethod] + #[pyo3(signature = (nx, ny, dx, cx, cy, r))] + fn circle(nx: usize, ny: usize, dx: f64, cx: f64, cy: f64, r: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::level_set::LevelSet2::circle(nx, ny, dx, cx, cy, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLevelSet2 { inner: __v }) + } + + /// Axis-aligned box interior. + /// + /// Rust: `cfd::level_set::LevelSet2::box_` + #[pyo3(name = "box_")] + #[staticmethod] + #[pyo3(signature = (nx, ny, dx, x0, y0, x1, y1))] + fn box_(nx: usize, ny: usize, dx: f64, x0: f64, y0: f64, x1: f64, y1: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::level_set::LevelSet2::box_(nx, ny, dx, x0, y0, x1, y1)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLevelSet2 { inner: __v }) + } + + /// Advect through a MAC velocity field for one step. + /// + /// Rust: `cfd::level_set::LevelSet2::advect` + #[pyo3(name = "advect")] + #[pyo3(signature = (grid, dt, scheme))] + fn advect(&mut self, grid: pyo3::PyRef<'_, crate::generated::types::PyMacGrid2>, dt: f64, scheme: crate::generated::types::PyWenoOrUpwind) -> PyResult<()> { + let scheme = scheme.to_rust(); + let __r = crate::runtime::guard(|| self.inner.advect(&grid.inner, dt, scheme)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Sussman PDE reinitialization toward |∇φ| = 1. + /// + /// Rust: `cfd::level_set::LevelSet2::reinitialize` + #[pyo3(name = "reinitialize")] + #[pyo3(signature = (iters))] + fn reinitialize(&mut self, iters: usize) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.reinitialize(iters)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Fast marching redistancing (first-order Eikonal solve outward + /// from the interface). + /// + /// Rust: `cfd::level_set::LevelSet2::fast_marching` + #[pyo3(name = "fast_marching")] + #[pyo3(signature = ())] + fn fast_marching(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.fast_marching()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Mean curvature κ = ∇·(∇φ/|∇φ|). + /// + /// Rust: `cfd::level_set::LevelSet2::curvature` + #[pyo3(name = "curvature")] + #[pyo3(signature = ())] + fn curvature(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.curvature()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCellField2 { inner: __v }) + } + + /// Outward unit normal at a world position. + /// + /// Rust: `cfd::level_set::LevelSet2::normal` + #[pyo3(name = "normal")] + #[pyo3(signature = (p))] + fn normal(&self, p: crate::generated::types::PyVec2Arg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.normal(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) + } + + /// Enclosed (φ < 0) area via a smoothed Heaviside. + /// + /// Rust: `cfd::level_set::LevelSet2::area` + #[pyo3(name = "area")] + #[pyo3(signature = ())] + fn area(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.area()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Interface length via the smoothed delta. + /// + /// Rust: `cfd::level_set::LevelSet2::perimeter` + #[pyo3(name = "perimeter")] + #[pyo3(signature = ())] + fn perimeter(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.perimeter()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Marching-squares interface segments of φ = 0. + /// + /// Rust: `cfd::level_set::LevelSet2::interface_segments` + #[pyo3(name = "interface_segments")] + #[pyo3(signature = ())] + fn interface_segments(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.interface_segments()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyLevelSetSegment2 { inner: __x }).collect::>()) + } + + /// Smoothed Heaviside H_ε(φ) (0 inside, 1 outside). + /// + /// Rust: `cfd::level_set::LevelSet2::heaviside` + #[pyo3(name = "heaviside")] + #[pyo3(signature = (eps))] + fn heaviside(&self, eps: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.heaviside(eps)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCellField2 { inner: __v }) + } + + /// Smoothed delta δ_ε(φ). + /// + /// Rust: `cfd::level_set::LevelSet2::delta` + #[pyo3(name = "delta")] + #[pyo3(signature = (eps))] + fn delta(&self, eps: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.delta(eps)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCellField2 { inner: __v }) + } + + /// CSG union (min). + /// + /// Rust: `cfd::level_set::LevelSet2::union` + #[pyo3(name = "union")] + #[pyo3(signature = (other))] + fn union(&mut self, other: pyo3::PyRef<'_, crate::generated::types::PyLevelSet2>) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.union(&other.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// CSG intersection (max). + /// + /// Rust: `cfd::level_set::LevelSet2::intersect` + #[pyo3(name = "intersect")] + #[pyo3(signature = (other))] + fn intersect(&mut self, other: pyo3::PyRef<'_, crate::generated::types::PyLevelSet2>) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.intersect(&other.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// CSG subtraction (max with −other). + /// + /// Rust: `cfd::level_set::LevelSet2::subtract` + #[pyo3(name = "subtract")] + #[pyo3(signature = (other))] + fn subtract(&mut self, other: pyo3::PyRef<'_, crate::generated::types::PyLevelSet2>) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.subtract(&other.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Extend a scalar field off the interface along normals (upwind + /// sweeps of q_t + sign(φ) n·∇q = 0) within `band` distance. + /// + /// Rust: `cfd::level_set::LevelSet2::extend_velocity` + #[pyo3(name = "extend_velocity")] + #[pyo3(signature = (vel, band))] + fn extend_velocity(&self, vel: pyo3::PyRefMut<'_, crate::generated::types::PyCellField2>, band: f64) -> PyResult<()> { + let mut vel = vel; + let __r = crate::runtime::guard(|| self.inner.extend_velocity(&mut vel.inner, band)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Shift φ by a constant so the enclosed area matches `target_area`. + /// + /// Rust: `cfd::level_set::LevelSet2::volume_correction` + #[pyo3(name = "volume_correction")] + #[pyo3(signature = (target_area))] + fn volume_correction(&mut self, target_area: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.volume_correction(target_area)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + #[getter] + #[pyo3(name = "phi")] + fn py_get_phi(&self) -> PyResult { Ok(crate::generated::types::PyCellField2 { inner: self.inner.phi.clone() }) } + + #[getter] + #[pyo3(name = "band")] + fn py_get_band(&self) -> PyResult> { Ok(self.inner.band.clone().map(|__x| __x)) } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// 3D level set with mesh extraction. +/// +/// Rust: `cfd::level_set::LevelSet3` +#[pyclass(name = "LevelSet3", module = "numeria.cfd.level_set")] +pub struct PyLevelSet3 { pub inner: rust_physics_engine::cfd::level_set::LevelSet3 } +#[pymethods] +impl PyLevelSet3 { + /// Builds a `LevelSet3` from its fields. + #[new] + #[pyo3(signature = (phi, band))] + fn __new__(phi: crate::generated::types::PyFieldsScalarField3, band: Option) -> Self { + let phi = phi.inner; + Self { inner: rust_physics_engine::cfd::level_set::LevelSet3 { phi: phi, band: band } } + } + + /// Build from an SDF of world coordinates (node spacing dx). + /// + /// Rust: `cfd::level_set::LevelSet3::from_sdf` + #[pyo3(name = "from_sdf")] + #[staticmethod] + #[pyo3(signature = (f, n, dx))] + fn from_sdf(f: pyo3::Py, n: usize, dx: f64) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64, __a1: f64, __a2: f64| -> f64 { __cb.call::<_, f64>((__a0, __a1, __a2), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::level_set::LevelSet3::from_sdf(f, n, dx)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLevelSet3 { inner: __v }) + } + + /// Sphere SDF. + /// + /// Rust: `cfd::level_set::LevelSet3::sphere` + #[pyo3(name = "sphere")] + #[staticmethod] + #[pyo3(signature = (n, dx, c, r))] + fn sphere(n: usize, dx: f64, c: crate::generated::types::PyVec3Arg, r: f64) -> PyResult { + let c = c.0; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::level_set::LevelSet3::sphere(n, dx, c, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLevelSet3 { inner: __v }) + } + + /// Extract the φ = 0 isosurface as a triangle mesh (marching + /// tetrahedra: table-free, watertight per-cube). + /// + /// Rust: `cfd::level_set::LevelSet3::to_mesh` + #[pyo3(name = "to_mesh")] + #[pyo3(signature = ())] + fn to_mesh(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.to_mesh()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGeometryMeshMesh { inner: __v }) + } + + #[getter] + #[pyo3(name = "phi")] + fn py_get_phi(&self) -> PyResult { Ok(crate::generated::types::PyFieldsScalarField3 { inner: self.inner.phi.clone() }) } + + #[getter] + #[pyo3(name = "band")] + fn py_get_band(&self) -> PyResult> { Ok(self.inner.band.clone().map(|__x| __x)) } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// A line segment of the reconstructed interface. +/// +/// Rust: `cfd::level_set::Segment2` +#[pyclass(name = "Segment2", module = "numeria.cfd.level_set", from_py_object)] +#[derive(Clone)] +pub struct PyLevelSetSegment2 { pub inner: rust_physics_engine::cfd::level_set::Segment2 } +#[pymethods] +impl PyLevelSetSegment2 { + /// Builds a `Segment2` from its fields. + #[new] + #[pyo3(signature = (a, b))] + fn __new__(a: crate::generated::types::PyVec2Arg, b: crate::generated::types::PyVec2Arg) -> Self { + let a = a.0; + let b = b.0; + Self { inner: rust_physics_engine::cfd::level_set::Segment2 { a: a, b: b } } + } + + #[getter] + #[pyo3(name = "a")] + fn py_get_a(&self) -> PyResult { Ok(crate::generated::types::PyVec2 { inner: self.inner.a.clone() }) } + + #[getter] + #[pyo3(name = "b")] + fn py_get_b(&self) -> PyResult { Ok(crate::generated::types::PyVec2 { inner: self.inner.b.clone() }) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Segment2", "Segment2", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Volume-of-fluid interface tracking with PLIC reconstruction. +/// +/// Rust: `cfd::level_set::Vof2` +#[pyclass(name = "Vof2", module = "numeria.cfd.level_set")] +pub struct PyVof2 { pub inner: rust_physics_engine::cfd::level_set::Vof2 } +#[pymethods] +impl PyVof2 { + /// Builds a `Vof2` from its fields. + #[new] + #[pyo3(signature = (fraction))] + fn __new__(fraction: crate::generated::types::PyCellField2) -> Self { + let fraction = fraction.inner; + Self { inner: rust_physics_engine::cfd::level_set::Vof2 { fraction: fraction } } + } + + /// Initialize fractions from an SDF (φ < 0 = filled) by 4×4 + /// subsampling. + /// + /// Rust: `cfd::level_set::Vof2::init_from_sdf` + #[pyo3(name = "init_from_sdf")] + #[staticmethod] + #[pyo3(signature = (f, nx, ny, dx))] + fn init_from_sdf(f: pyo3::Py, nx: usize, ny: usize, dx: f64) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64, __a1: f64| -> f64 { __cb.call::<_, f64>((__a0, __a1), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::level_set::Vof2::init_from_sdf(f, nx, ny, dx)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVof2 { inner: __v }) + } + + /// Youngs finite-difference interface normals (pointing out of the + /// liquid). + /// + /// Rust: `cfd::level_set::Vof2::reconstruct_normals_youngs` + #[pyo3(name = "reconstruct_normals_youngs")] + #[pyo3(signature = ())] + fn reconstruct_normals_youngs(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.reconstruct_normals_youngs()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) + } + + /// ELVIRA-style normals: pick, per cell, the best of six candidate + /// column/row difference slopes by fraction reproduction error. + /// + /// Rust: `cfd::level_set::Vof2::reconstruct_normals_elvira` + #[pyo3(name = "reconstruct_normals_elvira")] + #[pyo3(signature = ())] + fn reconstruct_normals_elvira(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.reconstruct_normals_elvira()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) + } + + /// Directional-split geometric advection using PLIC subsampling of + /// donor regions. + /// + /// Rust: `cfd::level_set::Vof2::advect_plic` + #[pyo3(name = "advect_plic")] + #[pyo3(signature = (grid, dt))] + fn advect_plic(&mut self, grid: pyo3::PyRef<'_, crate::generated::types::PyMacGrid2>, dt: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.advect_plic(&grid.inner, dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// PLIC interface segments. + /// + /// Rust: `cfd::level_set::Vof2::interface_segments` + #[pyo3(name = "interface_segments")] + #[pyo3(signature = ())] + fn interface_segments(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.interface_segments()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyLevelSetSegment2 { inner: __x }).collect::>()) + } + + /// Total liquid volume (area in 2D). + /// + /// Rust: `cfd::level_set::Vof2::total_volume` + #[pyo3(name = "total_volume")] + #[pyo3(signature = ())] + fn total_volume(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.total_volume()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Height-function curvature per column (useful near horizontal + /// interfaces). + /// + /// Rust: `cfd::level_set::Vof2::curvature_height_function` + #[pyo3(name = "curvature_height_function")] + #[pyo3(signature = ())] + fn curvature_height_function<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.curvature_height_function())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "fraction")] + fn py_get_fraction(&self) -> PyResult { Ok(crate::generated::types::PyCellField2 { inner: self.inner.fraction.clone() }) } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// Advection scheme for the level set. +/// +/// Rust: `cfd::level_set::WenoOrUpwind` +#[pyclass(name = "WenoOrUpwind", module = "numeria.cfd.level_set", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyWenoOrUpwind { + Weno, + Upwind, +} +impl PyWenoOrUpwind { + pub fn to_rust(&self) -> rust_physics_engine::cfd::level_set::WenoOrUpwind { match self { + Self::Weno => rust_physics_engine::cfd::level_set::WenoOrUpwind::Weno, + Self::Upwind => rust_physics_engine::cfd::level_set::WenoOrUpwind::Upwind, + } } + pub fn from_rust(v: &rust_physics_engine::cfd::level_set::WenoOrUpwind) -> Self { match v { + rust_physics_engine::cfd::level_set::WenoOrUpwind::Weno => Self::Weno, + rust_physics_engine::cfd::level_set::WenoOrUpwind::Upwind => Self::Upwind, + } } +} +#[pymethods] +impl PyWenoOrUpwind { + fn __repr__(&self) -> &'static str { + match self { + Self::Weno => "WenoOrUpwind.Weno", + Self::Upwind => "WenoOrUpwind.Upwind", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Horizontal / near-horizontal two-phase flow patterns. +/// +/// Rust: `cfd::multiphase::FlowPattern` +#[pyclass(name = "FlowPattern", module = "numeria.cfd.multiphase", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyFlowPattern { + Stratified, + Intermittent, + Annular, + DispersedBubble, +} +impl PyFlowPattern { + pub fn to_rust(&self) -> rust_physics_engine::cfd::multiphase::FlowPattern { match self { + Self::Stratified => rust_physics_engine::cfd::multiphase::FlowPattern::Stratified, + Self::Intermittent => rust_physics_engine::cfd::multiphase::FlowPattern::Intermittent, + Self::Annular => rust_physics_engine::cfd::multiphase::FlowPattern::Annular, + Self::DispersedBubble => rust_physics_engine::cfd::multiphase::FlowPattern::DispersedBubble, + } } + pub fn from_rust(v: &rust_physics_engine::cfd::multiphase::FlowPattern) -> Self { match v { + rust_physics_engine::cfd::multiphase::FlowPattern::Stratified => Self::Stratified, + rust_physics_engine::cfd::multiphase::FlowPattern::Intermittent => Self::Intermittent, + rust_physics_engine::cfd::multiphase::FlowPattern::Annular => Self::Annular, + rust_physics_engine::cfd::multiphase::FlowPattern::DispersedBubble => Self::DispersedBubble, + } } +} +#[pymethods] +impl PyFlowPattern { + fn __repr__(&self) -> &'static str { + match self { + Self::Stratified => "FlowPattern.Stratified", + Self::Intermittent => "FlowPattern.Intermittent", + Self::Annular => "FlowPattern.Annular", + Self::DispersedBubble => "FlowPattern.DispersedBubble", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Saturated-fluid property bundle for boiling correlations. +/// +/// Rust: `cfd::multiphase::SaturatedFluid` +#[pyclass(name = "SaturatedFluid", module = "numeria.cfd.multiphase", from_py_object)] +#[derive(Clone)] +pub struct PySaturatedFluid { pub inner: rust_physics_engine::cfd::multiphase::SaturatedFluid } +#[pymethods] +impl PySaturatedFluid { + /// Builds a `SaturatedFluid` from its fields. + #[new] + #[pyo3(signature = (mu_l, h_fg, rho_l, rho_g, sigma, cp_l, pr_l))] + fn __new__(mu_l: f64, h_fg: f64, rho_l: f64, rho_g: f64, sigma: f64, cp_l: f64, pr_l: f64) -> Self { + + Self { inner: rust_physics_engine::cfd::multiphase::SaturatedFluid { mu_l: mu_l, h_fg: h_fg, rho_l: rho_l, rho_g: rho_g, sigma: sigma, cp_l: cp_l, pr_l: pr_l } } + } + + /// Water at 1 atm saturation. + /// + /// Rust: `cfd::multiphase::SaturatedFluid::water_1atm` + #[pyo3(name = "water_1atm")] + #[staticmethod] + #[pyo3(signature = ())] + fn water_1atm() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::multiphase::SaturatedFluid::water_1atm()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySaturatedFluid { inner: __v }) + } + + #[getter] + #[pyo3(name = "mu_l")] + fn py_get_mu_l(&self) -> PyResult { Ok(self.inner.mu_l) } + + #[setter] + #[pyo3(name = "mu_l")] + fn py_set_mu_l(&mut self, v: f64) { self.inner.mu_l = v; } + + #[getter] + #[pyo3(name = "h_fg")] + fn py_get_h_fg(&self) -> PyResult { Ok(self.inner.h_fg) } + + #[setter] + #[pyo3(name = "h_fg")] + fn py_set_h_fg(&mut self, v: f64) { self.inner.h_fg = v; } + + #[getter] + #[pyo3(name = "rho_l")] + fn py_get_rho_l(&self) -> PyResult { Ok(self.inner.rho_l) } + + #[setter] + #[pyo3(name = "rho_l")] + fn py_set_rho_l(&mut self, v: f64) { self.inner.rho_l = v; } + + #[getter] + #[pyo3(name = "rho_g")] + fn py_get_rho_g(&self) -> PyResult { Ok(self.inner.rho_g) } + + #[setter] + #[pyo3(name = "rho_g")] + fn py_set_rho_g(&mut self, v: f64) { self.inner.rho_g = v; } + + #[getter] + #[pyo3(name = "sigma")] + fn py_get_sigma(&self) -> PyResult { Ok(self.inner.sigma) } + + #[setter] + #[pyo3(name = "sigma")] + fn py_set_sigma(&mut self, v: f64) { self.inner.sigma = v; } + + #[getter] + #[pyo3(name = "cp_l")] + fn py_get_cp_l(&self) -> PyResult { Ok(self.inner.cp_l) } + + #[setter] + #[pyo3(name = "cp_l")] + fn py_set_cp_l(&mut self, v: f64) { self.inner.cp_l = v; } + + #[getter] + #[pyo3(name = "pr_l")] + fn py_get_pr_l(&self) -> PyResult { Ok(self.inner.pr_l) } + + #[setter] + #[pyo3(name = "pr_l")] + fn py_set_pr_l(&mut self, v: f64) { self.inner.pr_l = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("SaturatedFluid", "SaturatedFluid", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Van Genuchten soil retention parameters (`alpha` in 1/m, `k_s` in m/s). +/// +/// Rust: `cfd::porous::VanGenuchten` +#[pyclass(name = "VanGenuchten", module = "numeria.cfd.porous", from_py_object)] +#[derive(Clone)] +pub struct PyVanGenuchten { pub inner: rust_physics_engine::cfd::porous::VanGenuchten } +#[pymethods] +impl PyVanGenuchten { + /// Builds a `VanGenuchten` from its fields. + #[new] + #[pyo3(signature = (theta_r, theta_s, alpha, n, k_s))] + fn __new__(theta_r: f64, theta_s: f64, alpha: f64, n: f64, k_s: f64) -> Self { + + Self { inner: rust_physics_engine::cfd::porous::VanGenuchten { theta_r: theta_r, theta_s: theta_s, alpha: alpha, n: n, k_s: k_s } } + } + + /// Water content at pressure head `h` (m, negative when unsaturated). + /// + /// Rust: `cfd::porous::VanGenuchten::theta` + #[pyo3(name = "theta")] + #[pyo3(signature = (h))] + fn theta(&self, h: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.theta(h)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Effective saturation at pressure head `h`. + /// + /// Rust: `cfd::porous::VanGenuchten::effective_saturation` + #[pyo3(name = "effective_saturation")] + #[pyo3(signature = (h))] + fn effective_saturation(&self, h: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.effective_saturation(h)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Unsaturated hydraulic conductivity K(h) by Mualem-Van Genuchten. + /// + /// Rust: `cfd::porous::VanGenuchten::k` + #[pyo3(name = "k")] + #[pyo3(signature = (h))] + fn k(&self, h: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.k(h)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Specific moisture capacity C(h) = d theta / dh. + /// + /// Rust: `cfd::porous::VanGenuchten::capacity` + #[pyo3(name = "capacity")] + #[pyo3(signature = (h))] + fn capacity(&self, h: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.capacity(h)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Pressure head from water content (inverse retention curve). + /// + /// Rust: `cfd::porous::VanGenuchten::head_from_theta` + #[pyo3(name = "head_from_theta")] + #[pyo3(signature = (theta))] + fn head_from_theta(&self, theta: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.head_from_theta(theta)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Typical sand. + /// + /// Rust: `cfd::porous::VanGenuchten::sand` + #[pyo3(name = "sand")] + #[staticmethod] + #[pyo3(signature = ())] + fn sand() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::porous::VanGenuchten::sand()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVanGenuchten { inner: __v }) + } + + /// Typical loam. + /// + /// Rust: `cfd::porous::VanGenuchten::loam` + #[pyo3(name = "loam")] + #[staticmethod] + #[pyo3(signature = ())] + fn loam() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::porous::VanGenuchten::loam()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVanGenuchten { inner: __v }) + } + + /// Typical clay. + /// + /// Rust: `cfd::porous::VanGenuchten::clay` + #[pyo3(name = "clay")] + #[staticmethod] + #[pyo3(signature = ())] + fn clay() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::porous::VanGenuchten::clay()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVanGenuchten { inner: __v }) + } + + #[getter] + #[pyo3(name = "theta_r")] + fn py_get_theta_r(&self) -> PyResult { Ok(self.inner.theta_r) } + + #[setter] + #[pyo3(name = "theta_r")] + fn py_set_theta_r(&mut self, v: f64) { self.inner.theta_r = v; } + + #[getter] + #[pyo3(name = "theta_s")] + fn py_get_theta_s(&self) -> PyResult { Ok(self.inner.theta_s) } + + #[setter] + #[pyo3(name = "theta_s")] + fn py_set_theta_s(&mut self, v: f64) { self.inner.theta_s = v; } + + #[getter] + #[pyo3(name = "alpha")] + fn py_get_alpha(&self) -> PyResult { Ok(self.inner.alpha) } + + #[setter] + #[pyo3(name = "alpha")] + fn py_set_alpha(&mut self, v: f64) { self.inner.alpha = v; } + + #[getter] + #[pyo3(name = "n")] + fn py_get_n(&self) -> PyResult { Ok(self.inner.n) } + + #[setter] + #[pyo3(name = "n")] + fn py_set_n(&mut self, v: f64) { self.inner.n = v; } + + #[getter] + #[pyo3(name = "k_s")] + fn py_get_k_s(&self) -> PyResult { Ok(self.inner.k_s) } + + #[setter] + #[pyo3(name = "k_s")] + fn py_set_k_s(&mut self, v: f64) { self.inner.k_s = v; } + + fn __repr__(&self) -> String { format!("VanGenuchten(theta_r={:?}, theta_s={:?}, alpha={:?}, n={:?}, k_s={:?})", self.inner.theta_r, self.inner.theta_s, self.inner.alpha, self.inner.n, self.inner.k_s) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A `VanGenuchten` argument, or anything that can stand in for one. +pub struct PyVanGenuchtenArg(pub rust_physics_engine::cfd::porous::VanGenuchten); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyVanGenuchtenArg { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result { + if let Ok(__w) = obj.extract::() { + return Ok(PyVanGenuchtenArg(__w.inner)); + } + let __v = crate::runtime::coerce::floats_exact(obj, 5, "VanGenuchten")?; + Ok(PyVanGenuchtenArg(rust_physics_engine::cfd::porous::VanGenuchten { theta_r: __v[0], theta_s: __v[1], alpha: __v[2], n: __v[3], k_s: __v[4] })) + } +} + + +/// Elementary potential-flow element. +/// +/// Rust: `cfd::potential_flow::Element` +#[pyclass(name = "Element", module = "numeria.cfd.potential_flow", from_py_object)] +#[derive(Clone)] +pub struct PyPotentialFlowElement { pub inner: rust_physics_engine::cfd::potential_flow::Element } +#[pymethods] +impl PyPotentialFlowElement { + /// Complex potential W(z). + /// + /// Rust: `cfd::potential_flow::Element::complex_potential` + #[pyo3(name = "complex_potential")] + #[pyo3(signature = (z))] + fn complex_potential<'py>(&self, py: Python<'py>, z: crate::runtime::coerce::ComplexArg) -> PyResult> { + let z = z.0; + let __r = crate::runtime::guard(|| self.inner.complex_potential(z)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::complex_out(py, __v)) + } + + /// Complex velocity dW/dz = u − i v. + /// + /// Rust: `cfd::potential_flow::Element::complex_velocity` + #[pyo3(name = "complex_velocity")] + #[pyo3(signature = (z))] + fn complex_velocity<'py>(&self, py: Python<'py>, z: crate::runtime::coerce::ComplexArg) -> PyResult> { + let z = z.0; + let __r = crate::runtime::guard(|| self.inner.complex_velocity(z)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::complex_out(py, __v)) + } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Element", "Element", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Hess-Smith source/vortex panel method. +/// +/// Rust: `cfd::potential_flow::PanelMethod` +#[pyclass(name = "PanelMethod", module = "numeria.cfd.potential_flow")] +pub struct PyPanelMethod { pub inner: rust_physics_engine::cfd::potential_flow::PanelMethod } +#[pymethods] +impl PyPanelMethod { + /// Build panels from airfoil surface points (TE → upper → LE → + /// lower → TE ordering as produced by `naca4`). + /// + /// Rust: `cfd::potential_flow::PanelMethod::new` + #[new] + #[pyo3(signature = (airfoil))] + fn __new__(airfoil: Vec) -> PyResult { + let airfoil = airfoil.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::potential_flow::PanelMethod::new(&airfoil)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPanelMethod { inner: __v }) + } + + /// Assemble and solve the Hess-Smith system: no-penetration source + /// strengths for two trial vortex strengths, then the Kutta + /// condition (equal trailing-edge tangential speeds evaluated + /// through the surface-velocity operator) fixes γ by linearity. + /// + /// Rust: `cfd::potential_flow::PanelMethod::solve` + #[pyo3(name = "solve")] + #[pyo3(signature = ())] + fn solve(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.solve()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Velocity at an arbitrary field point. + /// + /// Rust: `cfd::potential_flow::PanelMethod::velocity_at` + #[pyo3(name = "velocity_at")] + #[pyo3(signature = (p))] + fn velocity_at(&self, p: crate::generated::types::PyVec2Arg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.velocity_at(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) + } + + /// Surface (x/c, cp) at panel midpoints. + /// + /// Rust: `cfd::potential_flow::PanelMethod::cp_distribution` + #[pyo3(name = "cp_distribution")] + #[pyo3(signature = ())] + fn cp_distribution<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.cp_distribution())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) + } + + /// Lift coefficient from the total circulation. + /// + /// Rust: `cfd::potential_flow::PanelMethod::cl` + #[pyo3(name = "cl")] + #[pyo3(signature = ())] + fn cl(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.cl()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Quarter-chord moment coefficient from the cp distribution. + /// + /// Rust: `cfd::potential_flow::PanelMethod::cm_quarter_chord` + #[pyo3(name = "cm_quarter_chord")] + #[pyo3(signature = ())] + fn cm_quarter_chord(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.cm_quarter_chord()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Streamlines around the airfoil. + /// + /// Rust: `cfd::potential_flow::PanelMethod::streamlines` + #[pyo3(name = "streamlines")] + #[pyo3(signature = (seeds, steps, dt))] + fn streamlines(&self, seeds: Vec, steps: usize, dt: f64) -> PyResult>> { + let seeds = seeds.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| self.inner.streamlines(&seeds, steps, dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()).collect::>()) + } + + /// Center of pressure x/c from the cp distribution. + /// + /// Rust: `cfd::potential_flow::PanelMethod::pressure_center` + #[pyo3(name = "pressure_center")] + #[pyo3(signature = ())] + fn pressure_center(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.pressure_center()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "alpha")] + fn py_get_alpha(&self) -> PyResult { Ok(self.inner.alpha) } + + #[setter] + #[pyo3(name = "alpha")] + fn py_set_alpha(&mut self, v: f64) { self.inner.alpha = v; } + + #[getter] + #[pyo3(name = "u_inf")] + fn py_get_u_inf(&self) -> PyResult { Ok(self.inner.u_inf) } + + #[setter] + #[pyo3(name = "u_inf")] + fn py_set_u_inf(&mut self, v: f64) { self.inner.u_inf = v; } + + #[getter] + #[pyo3(name = "sources")] + fn py_get_sources(&self) -> PyResult> { Ok(self.inner.sources.clone()) } + + #[getter] + #[pyo3(name = "gamma")] + fn py_get_gamma(&self) -> PyResult { Ok(self.inner.gamma) } + + #[setter] + #[pyo3(name = "gamma")] + fn py_set_gamma(&mut self, v: f64) { self.inner.gamma = v; } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// A wall line for the method of images. +/// +/// Rust: `cfd::potential_flow::Plane2` +#[pyclass(name = "Plane2", module = "numeria.cfd.potential_flow", from_py_object)] +#[derive(Clone)] +pub struct PyPlane2 { pub inner: rust_physics_engine::cfd::potential_flow::Plane2 } +#[pymethods] +impl PyPlane2 { + /// Builds a `Plane2` from its fields. + #[new] + #[pyo3(signature = (point, normal))] + fn __new__(point: crate::generated::types::PyVec2Arg, normal: crate::generated::types::PyVec2Arg) -> Self { + let point = point.0; + let normal = normal.0; + Self { inner: rust_physics_engine::cfd::potential_flow::Plane2 { point: point, normal: normal } } + } + + #[getter] + #[pyo3(name = "point")] + fn py_get_point(&self) -> PyResult { Ok(crate::generated::types::PyVec2 { inner: self.inner.point.clone() }) } + + #[getter] + #[pyo3(name = "normal")] + fn py_get_normal(&self) -> PyResult { Ok(crate::generated::types::PyVec2 { inner: self.inner.normal.clone() }) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Plane2", "Plane2", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Superposition of potential-flow elements. +/// +/// Rust: `cfd::potential_flow::PotentialFlow2` +#[pyclass(name = "PotentialFlow2", module = "numeria.cfd.potential_flow")] +pub struct PyPotentialFlow2 { pub inner: rust_physics_engine::cfd::potential_flow::PotentialFlow2 } +#[pymethods] +impl PyPotentialFlow2 { + /// Builds a `PotentialFlow2` from its fields. + #[new] + #[pyo3(signature = (elements))] + fn __new__(elements: Vec) -> Self { + let elements = elements.into_iter().map(|__e| __e.inner).collect::>(); + Self { inner: rust_physics_engine::cfd::potential_flow::PotentialFlow2 { elements: elements } } + } + + /// Velocity at a point. + /// + /// Rust: `cfd::potential_flow::PotentialFlow2::velocity` + #[pyo3(name = "velocity")] + #[pyo3(signature = (p))] + fn velocity(&self, p: crate::generated::types::PyVec2Arg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.velocity(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) + } + + /// Velocity potential φ. + /// + /// Rust: `cfd::potential_flow::PotentialFlow2::potential` + #[pyo3(name = "potential")] + #[pyo3(signature = (p))] + fn potential(&self, p: crate::generated::types::PyVec2Arg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.potential(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Stream function ψ. + /// + /// Rust: `cfd::potential_flow::PotentialFlow2::stream_function` + #[pyo3(name = "stream_function")] + #[pyo3(signature = (p))] + fn stream_function(&self, p: crate::generated::types::PyVec2Arg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.stream_function(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Complex potential W(z). + /// + /// Rust: `cfd::potential_flow::PotentialFlow2::complex_potential` + #[pyo3(name = "complex_potential")] + #[pyo3(signature = (z))] + fn complex_potential<'py>(&self, py: Python<'py>, z: crate::runtime::coerce::ComplexArg) -> PyResult> { + let z = z.0; + let __r = crate::runtime::guard(|| self.inner.complex_potential(z)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::complex_out(py, __v)) + } + + /// Complex velocity dW/dz. + /// + /// Rust: `cfd::potential_flow::PotentialFlow2::complex_velocity` + #[pyo3(name = "complex_velocity")] + #[pyo3(signature = (z))] + fn complex_velocity<'py>(&self, py: Python<'py>, z: crate::runtime::coerce::ComplexArg) -> PyResult> { + let z = z.0; + let __r = crate::runtime::guard(|| self.inner.complex_velocity(z)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::complex_out(py, __v)) + } + + /// Pressure coefficient 1 − |v|²/U∞². + /// + /// Rust: `cfd::potential_flow::PotentialFlow2::pressure_coefficient` + #[pyo3(name = "pressure_coefficient")] + #[pyo3(signature = (p, u_inf))] + fn pressure_coefficient(&self, p: crate::generated::types::PyVec2Arg, u_inf: f64) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.pressure_coefficient(p, u_inf)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Stagnation points found by Newton iteration on dW/dz = 0 from a + /// grid of starts. + /// + /// Rust: `cfd::potential_flow::PotentialFlow2::stagnation_points` + #[pyo3(name = "stagnation_points")] + #[pyo3(signature = ())] + fn stagnation_points(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.stagnation_points()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) + } + + /// Trace streamlines by RK2. + /// + /// Rust: `cfd::potential_flow::PotentialFlow2::streamlines` + #[pyo3(name = "streamlines")] + #[pyo3(signature = (seeds, steps, dt))] + fn streamlines(&self, seeds: Vec, steps: usize, dt: f64) -> PyResult>> { + let seeds = seeds.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| self.inner.streamlines(&seeds, steps, dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()).collect::>()) + } + + /// Kutta-Joukowski lift per unit span L' = ρ U Γ (sum of vortex + /// circulations). + /// + /// Rust: `cfd::potential_flow::PotentialFlow2::lift_kutta_joukowski` + #[pyo3(name = "lift_kutta_joukowski")] + #[pyo3(signature = (u_inf, rho))] + fn lift_kutta_joukowski(&self, u_inf: f64, rho: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.lift_kutta_joukowski(u_inf, rho)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "elements")] + fn py_get_elements(&self) -> PyResult> { Ok(self.inner.elements.clone().into_iter().map(|__x| crate::generated::types::PyPotentialFlowElement { inner: __x }).collect::>()) } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// Simple wing planform for the vortex lattice. +/// +/// Rust: `cfd::potential_flow::WingGeometry` +#[pyclass(name = "WingGeometry", module = "numeria.cfd.potential_flow", from_py_object)] +#[derive(Clone)] +pub struct PyWingGeometry { pub inner: rust_physics_engine::cfd::potential_flow::WingGeometry } +#[pymethods] +impl PyWingGeometry { + /// Builds a `WingGeometry` from its fields. + #[new] + #[pyo3(signature = (span, root_chord, tip_chord, sweep))] + fn __new__(span: f64, root_chord: f64, tip_chord: f64, sweep: f64) -> Self { + + Self { inner: rust_physics_engine::cfd::potential_flow::WingGeometry { span: span, root_chord: root_chord, tip_chord: tip_chord, sweep: sweep } } + } + + #[getter] + #[pyo3(name = "span")] + fn py_get_span(&self) -> PyResult { Ok(self.inner.span) } + + #[setter] + #[pyo3(name = "span")] + fn py_set_span(&mut self, v: f64) { self.inner.span = v; } + + #[getter] + #[pyo3(name = "root_chord")] + fn py_get_root_chord(&self) -> PyResult { Ok(self.inner.root_chord) } + + #[setter] + #[pyo3(name = "root_chord")] + fn py_set_root_chord(&mut self, v: f64) { self.inner.root_chord = v; } + + #[getter] + #[pyo3(name = "tip_chord")] + fn py_get_tip_chord(&self) -> PyResult { Ok(self.inner.tip_chord) } + + #[setter] + #[pyo3(name = "tip_chord")] + fn py_set_tip_chord(&mut self, v: f64) { self.inner.tip_chord = v; } + + #[getter] + #[pyo3(name = "sweep")] + fn py_get_sweep(&self) -> PyResult { Ok(self.inner.sweep) } + + #[setter] + #[pyo3(name = "sweep")] + fn py_set_sweep(&mut self, v: f64) { self.inner.sweep = v; } + + fn __repr__(&self) -> String { format!("WingGeometry(span={:?}, root_chord={:?}, tip_chord={:?}, sweep={:?})", self.inner.span, self.inner.root_chord, self.inner.tip_chord, self.inner.sweep) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A `WingGeometry` argument, or anything that can stand in for one. +pub struct PyWingGeometryArg(pub rust_physics_engine::cfd::potential_flow::WingGeometry); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyWingGeometryArg { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result { + if let Ok(__w) = obj.extract::() { + return Ok(PyWingGeometryArg(__w.inner)); + } + let __v = crate::runtime::coerce::floats_exact(obj, 4, "WingGeometry")?; + Ok(PyWingGeometryArg(rust_physics_engine::cfd::potential_flow::WingGeometry { span: __v[0], root_chord: __v[1], tip_chord: __v[2], sweep: __v[3] })) + } +} + + +/// Conserved state (density, momentum, total energy). +/// +/// Rust: `cfd::riemann::Cons` +#[pyclass(name = "Cons", module = "numeria.cfd.riemann", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyCons { pub inner: rust_physics_engine::cfd::riemann::Cons } +#[pymethods] +impl PyCons { + /// Builds a `Cons` from its fields. + #[new] + #[pyo3(signature = (rho, mom, e))] + fn __new__(rho: f64, mom: f64, e: f64) -> Self { + + Self { inner: rust_physics_engine::cfd::riemann::Cons { rho: rho, mom: mom, e: e } } + } + + #[getter] + #[pyo3(name = "rho")] + fn py_get_rho(&self) -> PyResult { Ok(self.inner.rho) } + + #[setter] + #[pyo3(name = "rho")] + fn py_set_rho(&mut self, v: f64) { self.inner.rho = v; } + + #[getter] + #[pyo3(name = "mom")] + fn py_get_mom(&self) -> PyResult { Ok(self.inner.mom) } + + #[setter] + #[pyo3(name = "mom")] + fn py_set_mom(&mut self, v: f64) { self.inner.mom = v; } + + #[getter] + #[pyo3(name = "e")] + fn py_get_e(&self) -> PyResult { Ok(self.inner.e) } + + #[setter] + #[pyo3(name = "e")] + fn py_set_e(&mut self, v: f64) { self.inner.e = v; } + + fn __repr__(&self) -> String { format!("Cons(rho={:?}, mom={:?}, e={:?})", self.inner.rho, self.inner.mom, self.inner.e) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A `Cons` argument, or anything that can stand in for one. +pub struct PyConsArg(pub rust_physics_engine::cfd::riemann::Cons); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyConsArg { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result { + if let Ok(__w) = obj.extract::() { + return Ok(PyConsArg(__w.inner)); + } + let __v = crate::runtime::coerce::floats_exact(obj, 3, "Cons")?; + Ok(PyConsArg(rust_physics_engine::cfd::riemann::Cons { rho: __v[0], mom: __v[1], e: __v[2] })) + } +} + + +/// 1D finite-volume Euler solver (order 1, or 2 with minmod MUSCL). +/// +/// Rust: `cfd::riemann::Euler1D` +#[pyclass(name = "Euler1D", module = "numeria.cfd.riemann")] +pub struct PyEuler1D { pub inner: rust_physics_engine::cfd::riemann::Euler1D } +#[pymethods] +impl PyEuler1D { + /// Uniform quiescent gas. + /// + /// Rust: `cfd::riemann::Euler1D::new` + #[new] + #[pyo3(signature = (n, dx, gamma))] + fn __new__(n: usize, dx: f64, gamma: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::riemann::Euler1D::new(n, dx, gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyEuler1D { inner: __v }) + } + + /// Set piecewise-constant left/right states split at fraction + /// `x_split` of the domain. + /// + /// Rust: `cfd::riemann::Euler1D::set_riemann_problem` + #[pyo3(name = "set_riemann_problem")] + #[pyo3(signature = (l, r, x_split))] + fn set_riemann_problem(&mut self, l: crate::generated::types::PyPrimArg, r: crate::generated::types::PyPrimArg, x_split: f64) -> PyResult<()> { + let l = l.0; + let r = r.0; + let __r = crate::runtime::guard(|| self.inner.set_riemann_problem(l, r, x_split)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// One step at the given CFL number; returns the dt used. + /// + /// Rust: `cfd::riemann::Euler1D::step` + #[pyo3(name = "step")] + #[pyo3(signature = (cfl))] + fn step(&mut self, cfl: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.step(cfl)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Step until time `t`. + /// + /// The CFL number follows the stability limit of the configured + /// reconstruction: the first-order update is TVD up to CFL 1, while + /// the MUSCL reconstruction advanced with a single forward-Euler + /// update is only TVD for CFL ≤ 1/2 (Harten's condition; see Toro, + /// *Riemann Solvers and Numerical Methods*, ch. 13). Running the + /// second-order scheme at CFL 0.9 does not converge under grid + /// refinement. + /// + /// Rust: `cfd::riemann::Euler1D::run_until` + #[pyo3(name = "run_until")] + #[pyo3(signature = (t))] + fn run_until(&mut self, t: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.run_until(t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Primitive states of all cells. + /// + /// Rust: `cfd::riemann::Euler1D::primitives` + #[pyo3(name = "primitives")] + #[pyo3(signature = ())] + fn primitives(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.primitives()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyPrim { inner: __x }).collect::>()) + } + + /// Total mass ∫ρ dx. + /// + /// Rust: `cfd::riemann::Euler1D::total_mass` + #[pyo3(name = "total_mass")] + #[pyo3(signature = ())] + fn total_mass(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.total_mass()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Total energy ∫E dx. + /// + /// Rust: `cfd::riemann::Euler1D::total_energy` + #[pyo3(name = "total_energy")] + #[pyo3(signature = ())] + fn total_energy(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.total_energy()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Position (domain fraction) of the steepest density gradient. + /// + /// Rust: `cfd::riemann::Euler1D::shock_position` + #[pyo3(name = "shock_position")] + #[pyo3(signature = ())] + fn shock_position(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.shock_position()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) + } + + #[getter] + #[pyo3(name = "cells")] + fn py_get_cells(&self) -> PyResult> { Ok(self.inner.cells.clone().into_iter().map(|__x| crate::generated::types::PyCons { inner: __x }).collect::>()) } + + #[getter] + #[pyo3(name = "dx")] + fn py_get_dx(&self) -> PyResult { Ok(self.inner.dx) } + + #[setter] + #[pyo3(name = "dx")] + fn py_set_dx(&mut self, v: f64) { self.inner.dx = v; } + + #[getter] + #[pyo3(name = "gamma")] + fn py_get_gamma(&self) -> PyResult { Ok(self.inner.gamma) } + + #[setter] + #[pyo3(name = "gamma")] + fn py_set_gamma(&mut self, v: f64) { self.inner.gamma = v; } + + #[getter] + #[pyo3(name = "flux")] + fn py_get_flux(&self) -> PyResult { Ok(crate::generated::types::PyFluxKind::from_rust(&self.inner.flux.clone())) } + + #[getter] + #[pyo3(name = "order")] + fn py_get_order(&self) -> PyResult { Ok(self.inner.order) } + + #[setter] + #[pyo3(name = "order")] + fn py_set_order(&mut self, v: usize) { self.inner.order = v; } + + #[getter] + #[pyo3(name = "bc")] + fn py_get_bc(&self) -> PyResult { Ok(crate::generated::types::PyEulerBc::from_rust(&self.inner.bc.clone())) } + + #[getter] + #[pyo3(name = "time")] + fn py_get_time(&self) -> PyResult { Ok(self.inner.time) } + + #[setter] + #[pyo3(name = "time")] + fn py_set_time(&mut self, v: f64) { self.inner.time = v; } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// 2D finite-volume Euler solver (dimensional splitting, MUSCL + HLLC). +/// +/// Rust: `cfd::riemann::Euler2D` +#[pyclass(name = "Euler2D", module = "numeria.cfd.riemann")] +pub struct PyEuler2D { pub inner: rust_physics_engine::cfd::riemann::Euler2D } +#[pymethods] +impl PyEuler2D { + /// Uniform gas at rest. + /// + /// Rust: `cfd::riemann::Euler2D::new` + #[new] + #[pyo3(signature = (nx, ny, dx, gamma))] + fn __new__(nx: usize, ny: usize, dx: f64, gamma: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::riemann::Euler2D::new(nx, ny, dx, gamma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyEuler2D { inner: __v }) + } + + /// Set the primitive state of one cell. + /// + /// Rust: `cfd::riemann::Euler2D::set_cell` + #[pyo3(name = "set_cell")] + #[pyo3(signature = (i, j, rho, u, v, p))] + fn set_cell(&mut self, i: usize, j: usize, rho: f64, u: f64, v: f64, p: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.set_cell(i, j, rho, u, v, p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// One dimensionally split step (x then y sweep); returns dt. + /// + /// Rust: `cfd::riemann::Euler2D::step` + #[pyo3(name = "step")] + #[pyo3(signature = (cfl))] + fn step(&mut self, cfl: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.step(cfl)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Kelvin-Helmholtz double shear layer with a velocity perturbation. + /// + /// Rust: `cfd::riemann::Euler2D::kelvin_helmholtz_init` + #[pyo3(name = "kelvin_helmholtz_init")] + #[pyo3(signature = ())] + fn kelvin_helmholtz_init(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.kelvin_helmholtz_init()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Rayleigh-Taylor: heavy over light in gravity `g`. + /// + /// Rust: `cfd::riemann::Euler2D::rayleigh_taylor_init` + #[pyo3(name = "rayleigh_taylor_init")] + #[pyo3(signature = (g))] + fn rayleigh_taylor_init(&mut self, g: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.rayleigh_taylor_init(g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Mach-10 double Mach reflection initial wedge state (simplified, + /// vertical shock at x = 1/6). + /// + /// Rust: `cfd::riemann::Euler2D::double_mach_reflection_init` + #[pyo3(name = "double_mach_reflection_init")] + #[pyo3(signature = ())] + fn double_mach_reflection_init(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.double_mach_reflection_init()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Shock hitting a low-density bubble. + /// + /// Rust: `cfd::riemann::Euler2D::shock_bubble_init` + #[pyo3(name = "shock_bubble_init")] + #[pyo3(signature = ())] + fn shock_bubble_init(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.shock_bubble_init()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Numerical schlieren |∇ρ| normalized to [0, 1]. + /// + /// Rust: `cfd::riemann::Euler2D::schlieren` + #[pyo3(name = "schlieren")] + #[pyo3(signature = ())] + fn schlieren<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.schlieren())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "nx")] + fn py_get_nx(&self) -> PyResult { Ok(self.inner.nx) } + + #[setter] + #[pyo3(name = "nx")] + fn py_set_nx(&mut self, v: usize) { self.inner.nx = v; } + + #[getter] + #[pyo3(name = "ny")] + fn py_get_ny(&self) -> PyResult { Ok(self.inner.ny) } + + #[setter] + #[pyo3(name = "ny")] + fn py_set_ny(&mut self, v: usize) { self.inner.ny = v; } + + #[getter] + #[pyo3(name = "dx")] + fn py_get_dx(&self) -> PyResult { Ok(self.inner.dx) } + + #[setter] + #[pyo3(name = "dx")] + fn py_set_dx(&mut self, v: f64) { self.inner.dx = v; } + + #[getter] + #[pyo3(name = "gamma")] + fn py_get_gamma(&self) -> PyResult { Ok(self.inner.gamma) } + + #[setter] + #[pyo3(name = "gamma")] + fn py_set_gamma(&mut self, v: f64) { self.inner.gamma = v; } + + #[getter] + #[pyo3(name = "rho")] + fn py_get_rho(&self) -> PyResult> { Ok(self.inner.rho.clone()) } + + #[getter] + #[pyo3(name = "momx")] + fn py_get_momx(&self) -> PyResult> { Ok(self.inner.momx.clone()) } + + #[getter] + #[pyo3(name = "momy")] + fn py_get_momy(&self) -> PyResult> { Ok(self.inner.momy.clone()) } + + #[getter] + #[pyo3(name = "e")] + fn py_get_e(&self) -> PyResult> { Ok(self.inner.e.clone()) } + + #[getter] + #[pyo3(name = "solid")] + fn py_get_solid(&self) -> PyResult> { Ok(self.inner.solid.clone()) } + + #[getter] + #[pyo3(name = "periodic")] + fn py_get_periodic(&self) -> PyResult { Ok(self.inner.periodic) } + + #[setter] + #[pyo3(name = "periodic")] + fn py_set_periodic(&mut self, v: bool) { self.inner.periodic = v; } + + #[getter] + #[pyo3(name = "gravity")] + fn py_get_gravity(&self) -> PyResult { Ok(self.inner.gravity) } + + #[setter] + #[pyo3(name = "gravity")] + fn py_set_gravity(&mut self, v: f64) { self.inner.gravity = v; } + + #[getter] + #[pyo3(name = "time")] + fn py_get_time(&self) -> PyResult { Ok(self.inner.time) } + + #[setter] + #[pyo3(name = "time")] + fn py_set_time(&mut self, v: f64) { self.inner.time = v; } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// Boundary condition for `Euler1D`. +/// +/// Rust: `cfd::riemann::EulerBc` +#[pyclass(name = "EulerBc", module = "numeria.cfd.riemann", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyEulerBc { + Transmissive, + Reflective, + Periodic, +} +impl PyEulerBc { + pub fn to_rust(&self) -> rust_physics_engine::cfd::riemann::EulerBc { match self { + Self::Transmissive => rust_physics_engine::cfd::riemann::EulerBc::Transmissive, + Self::Reflective => rust_physics_engine::cfd::riemann::EulerBc::Reflective, + Self::Periodic => rust_physics_engine::cfd::riemann::EulerBc::Periodic, + } } + pub fn from_rust(v: &rust_physics_engine::cfd::riemann::EulerBc) -> Self { match v { + rust_physics_engine::cfd::riemann::EulerBc::Transmissive => Self::Transmissive, + rust_physics_engine::cfd::riemann::EulerBc::Reflective => Self::Reflective, + rust_physics_engine::cfd::riemann::EulerBc::Periodic => Self::Periodic, + } } +} +#[pymethods] +impl PyEulerBc { + fn __repr__(&self) -> &'static str { + match self { + Self::Transmissive => "EulerBc.Transmissive", + Self::Reflective => "EulerBc.Reflective", + Self::Periodic => "EulerBc.Periodic", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Numerical flux selector. +/// +/// Rust: `cfd::riemann::FluxKind` +#[pyclass(name = "FluxKind", module = "numeria.cfd.riemann", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyFluxKind { + Exact, + Hll, + Hllc, + Roe, + Rusanov, + AusmPlus, +} +impl PyFluxKind { + pub fn to_rust(&self) -> rust_physics_engine::cfd::riemann::FluxKind { match self { + Self::Exact => rust_physics_engine::cfd::riemann::FluxKind::Exact, + Self::Hll => rust_physics_engine::cfd::riemann::FluxKind::Hll, + Self::Hllc => rust_physics_engine::cfd::riemann::FluxKind::Hllc, + Self::Roe => rust_physics_engine::cfd::riemann::FluxKind::Roe, + Self::Rusanov => rust_physics_engine::cfd::riemann::FluxKind::Rusanov, + Self::AusmPlus => rust_physics_engine::cfd::riemann::FluxKind::AusmPlus, + } } + pub fn from_rust(v: &rust_physics_engine::cfd::riemann::FluxKind) -> Self { match v { + rust_physics_engine::cfd::riemann::FluxKind::Exact => Self::Exact, + rust_physics_engine::cfd::riemann::FluxKind::Hll => Self::Hll, + rust_physics_engine::cfd::riemann::FluxKind::Hllc => Self::Hllc, + rust_physics_engine::cfd::riemann::FluxKind::Roe => Self::Roe, + rust_physics_engine::cfd::riemann::FluxKind::Rusanov => Self::Rusanov, + rust_physics_engine::cfd::riemann::FluxKind::AusmPlus => Self::AusmPlus, + } } +} +#[pymethods] +impl PyFluxKind { + fn __repr__(&self) -> &'static str { + match self { + Self::Exact => "FluxKind.Exact", + Self::Hll => "FluxKind.Hll", + Self::Hllc => "FluxKind.Hllc", + Self::Roe => "FluxKind.Roe", + Self::Rusanov => "FluxKind.Rusanov", + Self::AusmPlus => "FluxKind.AusmPlus", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Primitive state (density, velocity, pressure). +/// +/// Rust: `cfd::riemann::Prim` +#[pyclass(name = "Prim", module = "numeria.cfd.riemann", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyPrim { pub inner: rust_physics_engine::cfd::riemann::Prim } +#[pymethods] +impl PyPrim { + /// Builds a `Prim` from its fields. + #[new] + #[pyo3(signature = (rho, u, p))] + fn __new__(rho: f64, u: f64, p: f64) -> Self { + + Self { inner: rust_physics_engine::cfd::riemann::Prim { rho: rho, u: u, p: p } } + } + + #[getter] + #[pyo3(name = "rho")] + fn py_get_rho(&self) -> PyResult { Ok(self.inner.rho) } + + #[setter] + #[pyo3(name = "rho")] + fn py_set_rho(&mut self, v: f64) { self.inner.rho = v; } + + #[getter] + #[pyo3(name = "u")] + fn py_get_u(&self) -> PyResult { Ok(self.inner.u) } + + #[setter] + #[pyo3(name = "u")] + fn py_set_u(&mut self, v: f64) { self.inner.u = v; } + + #[getter] + #[pyo3(name = "p")] + fn py_get_p(&self) -> PyResult { Ok(self.inner.p) } + + #[setter] + #[pyo3(name = "p")] + fn py_set_p(&mut self, v: f64) { self.inner.p = v; } + + fn __repr__(&self) -> String { format!("Prim(rho={:?}, u={:?}, p={:?})", self.inner.rho, self.inner.u, self.inner.p) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A `Prim` argument, or anything that can stand in for one. +pub struct PyPrimArg(pub rust_physics_engine::cfd::riemann::Prim); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyPrimArg { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result { + if let Ok(__w) = obj.extract::() { + return Ok(PyPrimArg(__w.inner)); + } + let __v = crate::runtime::coerce::floats_exact(obj, 3, "Prim")?; + Ok(PyPrimArg(rust_physics_engine::cfd::riemann::Prim { rho: __v[0], u: __v[1], p: __v[2] })) + } +} + + +/// 2D shallow water solver on a square grid: HLL fluxes with Audusse +/// hydrostatic reconstruction (well-balanced over bathymetry), Manning +/// friction, Coriolis, and wet/dry tolerance. +/// +/// Rust: `cfd::shallow_water::ShallowWater2D` +#[pyclass(name = "ShallowWater2D", module = "numeria.cfd.shallow_water")] +pub struct PyShallowWater2D { pub inner: rust_physics_engine::cfd::shallow_water::ShallowWater2D } +#[pymethods] +impl PyShallowWater2D { + /// New dry basin with flat bathymetry. + /// + /// Rust: `cfd::shallow_water::ShallowWater2D::new` + #[new] + #[pyo3(signature = (nx, ny, dx, g))] + fn __new__(nx: usize, ny: usize, dx: f64, g: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::shallow_water::ShallowWater2D::new(nx, ny, dx, g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyShallowWater2D { inner: __v }) + } + + /// Set the bed elevation from a function of world (x, y). + /// + /// Rust: `cfd::shallow_water::ShallowWater2D::set_bathymetry` + #[pyo3(name = "set_bathymetry")] + #[pyo3(signature = (f))] + fn set_bathymetry(&mut self, f: pyo3::Py) -> PyResult<()> { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64, __a1: f64| -> f64 { __cb.call::<_, f64>((__a0, __a1), f64::NAN) } }; + let __r = crate::runtime::guard(|| self.inner.set_bathymetry(f)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Dam break: depth `h_l` left of world x = `x_split`, `h_r` right. + /// + /// Rust: `cfd::shallow_water::ShallowWater2D::set_dam_break` + #[pyo3(name = "set_dam_break")] + #[pyo3(signature = (x_split, h_l, h_r))] + fn set_dam_break(&mut self, x_split: f64, h_l: f64, h_r: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.set_dam_break(x_split, h_l, h_r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Gaussian free-surface bump of amplitude `amp` on still depth + /// `base_depth`. + /// + /// Rust: `cfd::shallow_water::ShallowWater2D::set_gaussian_bump` + #[pyo3(name = "set_gaussian_bump")] + #[pyo3(signature = (cx, cy, amp, sigma, base_depth))] + fn set_gaussian_bump(&mut self, cx: f64, cy: f64, amp: f64, sigma: f64, base_depth: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.set_gaussian_bump(cx, cy, amp, sigma, base_depth)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Add water volume at cell (i, j) at the given rate × 1 step. + /// + /// Rust: `cfd::shallow_water::ShallowWater2D::add_source` + #[pyo3(name = "add_source")] + #[pyo3(signature = (i, j, rate))] + fn add_source(&mut self, i: usize, j: usize, rate: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.add_source(i, j, rate)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// One step at the CFL number; returns the dt used. Reflective walls. + /// + /// Rust: `cfd::shallow_water::ShallowWater2D::step` + #[pyo3(name = "step")] + #[pyo3(signature = (cfl))] + fn step(&mut self, cfl: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.step(cfl)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Step until time `t`. + /// + /// Rust: `cfd::shallow_water::ShallowWater2D::run_until` + #[pyo3(name = "run_until")] + #[pyo3(signature = (t))] + fn run_until(&mut self, t: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.run_until(t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Number of wet cells. + /// + /// Rust: `cfd::shallow_water::ShallowWater2D::wet_cells` + #[pyo3(name = "wet_cells")] + #[pyo3(signature = ())] + fn wet_cells(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.wet_cells()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Total water volume. + /// + /// Rust: `cfd::shallow_water::ShallowWater2D::total_volume` + #[pyo3(name = "total_volume")] + #[pyo3(signature = ())] + fn total_volume(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.total_volume()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Total mechanical energy (kinetic + potential). + /// + /// Rust: `cfd::shallow_water::ShallowWater2D::total_energy` + #[pyo3(name = "total_energy")] + #[pyo3(signature = ())] + fn total_energy(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.total_energy()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Froude number per cell. + /// + /// Rust: `cfd::shallow_water::ShallowWater2D::froude_field` + #[pyo3(name = "froude_field")] + #[pyo3(signature = ())] + fn froude_field<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.froude_field())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Deepest water column. + /// + /// Rust: `cfd::shallow_water::ShallowWater2D::max_depth` + #[pyo3(name = "max_depth")] + #[pyo3(signature = ())] + fn max_depth(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.max_depth()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "nx")] + fn py_get_nx(&self) -> PyResult { Ok(self.inner.nx) } + + #[setter] + #[pyo3(name = "nx")] + fn py_set_nx(&mut self, v: usize) { self.inner.nx = v; } + + #[getter] + #[pyo3(name = "ny")] + fn py_get_ny(&self) -> PyResult { Ok(self.inner.ny) } + + #[setter] + #[pyo3(name = "ny")] + fn py_set_ny(&mut self, v: usize) { self.inner.ny = v; } + + #[getter] + #[pyo3(name = "dx")] + fn py_get_dx(&self) -> PyResult { Ok(self.inner.dx) } + + #[setter] + #[pyo3(name = "dx")] + fn py_set_dx(&mut self, v: f64) { self.inner.dx = v; } + + #[getter] + #[pyo3(name = "h")] + fn py_get_h(&self) -> PyResult> { Ok(self.inner.h.clone()) } + + #[getter] + #[pyo3(name = "hu")] + fn py_get_hu(&self) -> PyResult> { Ok(self.inner.hu.clone()) } + + #[getter] + #[pyo3(name = "hv")] + fn py_get_hv(&self) -> PyResult> { Ok(self.inner.hv.clone()) } + + #[getter] + #[pyo3(name = "bathymetry")] + fn py_get_bathymetry(&self) -> PyResult> { Ok(self.inner.bathymetry.clone()) } + + #[getter] + #[pyo3(name = "g")] + fn py_get_g(&self) -> PyResult { Ok(self.inner.g) } + + #[setter] + #[pyo3(name = "g")] + fn py_set_g(&mut self, v: f64) { self.inner.g = v; } + + #[getter] + #[pyo3(name = "manning_n")] + fn py_get_manning_n(&self) -> PyResult { Ok(self.inner.manning_n) } + + #[setter] + #[pyo3(name = "manning_n")] + fn py_set_manning_n(&mut self, v: f64) { self.inner.manning_n = v; } + + #[getter] + #[pyo3(name = "coriolis")] + fn py_get_coriolis(&self) -> PyResult { Ok(self.inner.coriolis) } + + #[setter] + #[pyo3(name = "coriolis")] + fn py_set_coriolis(&mut self, v: f64) { self.inner.coriolis = v; } + + #[getter] + #[pyo3(name = "dry_tol")] + fn py_get_dry_tol(&self) -> PyResult { Ok(self.inner.dry_tol) } + + #[setter] + #[pyo3(name = "dry_tol")] + fn py_set_dry_tol(&mut self, v: f64) { self.inner.dry_tol = v; } + + #[getter] + #[pyo3(name = "time")] + fn py_get_time(&self) -> PyResult { Ok(self.inner.time) } + + #[setter] + #[pyo3(name = "time")] + fn py_set_time(&mut self, v: f64) { self.inner.time = v; } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// SPH smoothing kernels. +/// +/// Rust: `cfd::sph::Kernel` +#[pyclass(name = "Kernel", module = "numeria.cfd.sph", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyKernel { + CubicSpline, + Quintic, + WendlandC2, + Poly6, + Spiky, + Viscosity, +} +impl PyKernel { + pub fn to_rust(&self) -> rust_physics_engine::cfd::sph::Kernel { match self { + Self::CubicSpline => rust_physics_engine::cfd::sph::Kernel::CubicSpline, + Self::Quintic => rust_physics_engine::cfd::sph::Kernel::Quintic, + Self::WendlandC2 => rust_physics_engine::cfd::sph::Kernel::WendlandC2, + Self::Poly6 => rust_physics_engine::cfd::sph::Kernel::Poly6, + Self::Spiky => rust_physics_engine::cfd::sph::Kernel::Spiky, + Self::Viscosity => rust_physics_engine::cfd::sph::Kernel::Viscosity, + } } + pub fn from_rust(v: &rust_physics_engine::cfd::sph::Kernel) -> Self { match v { + rust_physics_engine::cfd::sph::Kernel::CubicSpline => Self::CubicSpline, + rust_physics_engine::cfd::sph::Kernel::Quintic => Self::Quintic, + rust_physics_engine::cfd::sph::Kernel::WendlandC2 => Self::WendlandC2, + rust_physics_engine::cfd::sph::Kernel::Poly6 => Self::Poly6, + rust_physics_engine::cfd::sph::Kernel::Spiky => Self::Spiky, + rust_physics_engine::cfd::sph::Kernel::Viscosity => Self::Viscosity, + } } +} +#[pymethods] +impl PyKernel { + fn __repr__(&self) -> &'static str { + match self { + Self::CubicSpline => "Kernel.CubicSpline", + Self::Quintic => "Kernel.Quintic", + Self::WendlandC2 => "Kernel.WendlandC2", + Self::Poly6 => "Kernel.Poly6", + Self::Spiky => "Kernel.Spiky", + Self::Viscosity => "Kernel.Viscosity", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Particle type. +/// +/// Rust: `cfd::sph::Kind` +#[pyclass(name = "Kind", module = "numeria.cfd.sph", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyKind { + Fluid, + Boundary, +} +impl PyKind { + pub fn to_rust(&self) -> rust_physics_engine::cfd::sph::Kind { match self { + Self::Fluid => rust_physics_engine::cfd::sph::Kind::Fluid, + Self::Boundary => rust_physics_engine::cfd::sph::Kind::Boundary, + } } + pub fn from_rust(v: &rust_physics_engine::cfd::sph::Kind) -> Self { match v { + rust_physics_engine::cfd::sph::Kind::Fluid => Self::Fluid, + rust_physics_engine::cfd::sph::Kind::Boundary => Self::Boundary, + } } +} +#[pymethods] +impl PyKind { + fn __repr__(&self) -> &'static str { + match self { + Self::Fluid => "Kind.Fluid", + Self::Boundary => "Kind.Boundary", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// An infinite plane given by a point and unit normal. +/// +/// Rust: `cfd::sph::Plane` +#[pyclass(name = "Plane", module = "numeria.cfd.sph", from_py_object)] +#[derive(Clone)] +pub struct PySphPlane { pub inner: rust_physics_engine::cfd::sph::Plane } +#[pymethods] +impl PySphPlane { + /// Builds a `Plane` from its fields. + #[new] + #[pyo3(signature = (point, normal))] + fn __new__(point: crate::generated::types::PyVec3Arg, normal: crate::generated::types::PyVec3Arg) -> Self { + let point = point.0; + let normal = normal.0; + Self { inner: rust_physics_engine::cfd::sph::Plane { point: point, normal: normal } } + } + + /// Signed distance of `p` (positive on the normal side). + /// + /// Rust: `cfd::sph::Plane::signed_distance` + #[pyo3(name = "signed_distance")] + #[pyo3(signature = (p))] + fn signed_distance(&self, p: crate::generated::types::PyVec3Arg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.signed_distance(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "point")] + fn py_get_point(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.point.clone() }) } + + #[getter] + #[pyo3(name = "normal")] + fn py_get_normal(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.normal.clone() }) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Plane", "Plane", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Uniform-cell spatial hash for neighbor queries. +/// +/// Rust: `cfd::sph::SpatialHash` +#[pyclass(name = "SpatialHash", module = "numeria.cfd.sph")] +pub struct PySphSpatialHash { pub inner: rust_physics_engine::cfd::sph::SpatialHash } +#[pymethods] +impl PySphSpatialHash { + /// New hash with the given cell size (usually the support radius). + /// + /// Rust: `cfd::sph::SpatialHash::new` + #[new] + #[pyo3(signature = (cell))] + fn __new__(cell: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::sph::SpatialHash::new(cell)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySphSpatialHash { inner: __v }) + } + + /// Rebuild from particle positions. + /// + /// Rust: `cfd::sph::SpatialHash::rebuild` + #[pyo3(name = "rebuild")] + #[pyo3(signature = (positions))] + fn rebuild<'py>(&mut self, py: Python<'py>, positions: Vec) -> PyResult<()> { + let positions = positions.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || self.inner.rebuild(&positions))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Indices of particles in the 27-cell neighborhood of `p`. + /// + /// Rust: `cfd::sph::SpatialHash::neighbors` + #[pyo3(name = "neighbors")] + #[pyo3(signature = (p, out))] + fn neighbors<'py>(&self, p: crate::generated::types::PyVec3Arg, out: pyo3::Bound<'py, pyo3::PyAny>) -> PyResult<()> { + let p = p.0; + let mut out__v: Vec = out.extract()?; + let __r = crate::runtime::guard(|| self.inner.neighbors(p, &mut out__v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + crate::runtime::coerce::write_back(&out, &out__v)?; + Ok(()) + } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// SPH fluid solver. +/// +/// Rust: `cfd::sph::Sph` +#[pyclass(name = "Sph", module = "numeria.cfd.sph")] +pub struct PySph { pub inner: rust_physics_engine::cfd::sph::Sph } +#[pymethods] +impl PySph { + /// New 2D solver (particles live in the z = 0 plane). + /// + /// Rust: `cfd::sph::Sph::new_2d` + #[pyo3(name = "new_2d")] + #[staticmethod] + #[pyo3(signature = (h, rest_density))] + fn new_2d(h: f64, rest_density: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::sph::Sph::new_2d(h, rest_density)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySph { inner: __v }) + } + + /// New 3D solver. + /// + /// Rust: `cfd::sph::Sph::new_3d` + #[pyo3(name = "new_3d")] + #[staticmethod] + #[pyo3(signature = (h, rest_density))] + fn new_3d(h: f64, rest_density: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::sph::Sph::new_3d(h, rest_density)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySph { inner: __v }) + } + + /// Fill an axis-aligned block with fluid particles. + /// + /// Rust: `cfd::sph::Sph::add_block` + #[pyo3(name = "add_block")] + #[pyo3(signature = (min, max, spacing))] + fn add_block(&mut self, min: crate::generated::types::PyVec3Arg, max: crate::generated::types::PyVec3Arg, spacing: f64) -> PyResult<()> { + let min = min.0; + let max = max.0; + let __r = crate::runtime::guard(|| self.inner.add_block(min, max, spacing)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Line the outside of a box with static boundary particles + /// (`layers` shells at the given spacing). + /// + /// Rust: `cfd::sph::Sph::add_boundary_box` + #[pyo3(name = "add_boundary_box")] + #[pyo3(signature = (min, max, spacing, layers))] + fn add_boundary_box(&mut self, min: crate::generated::types::PyVec3Arg, max: crate::generated::types::PyVec3Arg, spacing: f64, layers: usize) -> PyResult<()> { + let min = min.0; + let max = max.0; + let __r = crate::runtime::guard(|| self.inner.add_boundary_box(min, max, spacing, layers)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Sample boundary particles over the triangles of a mesh. + /// + /// Rust: `cfd::sph::Sph::add_boundary_from_mesh` + #[pyo3(name = "add_boundary_from_mesh")] + #[pyo3(signature = (mesh, spacing))] + fn add_boundary_from_mesh(&mut self, mesh: crate::generated::types::PyGeometryMeshMesh, spacing: f64) -> PyResult<()> { + let mesh = mesh.inner; + let __r = crate::runtime::guard(|| self.inner.add_boundary_from_mesh(&mesh, spacing)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Summation density over all neighbors. + /// + /// Rust: `cfd::sph::Sph::compute_density` + #[pyo3(name = "compute_density")] + #[pyo3(signature = ())] + fn compute_density(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.compute_density()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Tait equation of state (clamped non-negative: no tensile + /// pressures). + /// + /// Rust: `cfd::sph::Sph::compute_pressure` + #[pyo3(name = "compute_pressure")] + #[pyo3(signature = ())] + fn compute_pressure(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.compute_pressure()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Pressure + artificial viscosity + gravity + cohesion forces. + /// + /// Rust: `cfd::sph::Sph::compute_forces` + #[pyo3(name = "compute_forces")] + #[pyo3(signature = ())] + fn compute_forces(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.compute_forces()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// One time step of the configured scheme (symplectic Euler). + /// + /// Rust: `cfd::sph::Sph::step` + #[pyo3(name = "step")] + #[pyo3(signature = (dt))] + fn step(&mut self, dt: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step(dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Stable step: min of the acoustic CFL 0.25 h/(c0 + |v|max) and the + /// viscous limit 0.125 h²/ν. + /// + /// Rust: `cfd::sph::Sph::stable_dt` + #[pyo3(name = "stable_dt")] + #[pyo3(signature = ())] + fn stable_dt(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.stable_dt()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// XSPH velocity smoothing. + /// + /// Rust: `cfd::sph::Sph::xsph_correction` + #[pyo3(name = "xsph_correction")] + #[pyo3(signature = (eps))] + fn xsph_correction(&mut self, eps: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.xsph_correction(eps)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Particle shifting toward uniform concentration. + /// + /// Rust: `cfd::sph::Sph::shifting` + #[pyo3(name = "shifting")] + #[pyo3(signature = ())] + fn shifting(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.shifting()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Kinetic energy of the fluid. + /// + /// Rust: `cfd::sph::Sph::kinetic_energy` + #[pyo3(name = "kinetic_energy")] + #[pyo3(signature = ())] + fn kinetic_energy(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.kinetic_energy()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Gravitational potential energy −m g·x. + /// + /// Rust: `cfd::sph::Sph::potential_energy` + #[pyo3(name = "potential_energy")] + #[pyo3(signature = ())] + fn potential_energy(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.potential_energy()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Total linear momentum of the fluid. + /// + /// Rust: `cfd::sph::Sph::total_momentum` + #[pyo3(name = "total_momentum")] + #[pyo3(signature = ())] + fn total_momentum(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.total_momentum()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Largest relative compression (ρ − ρ₀)/ρ₀ among fluid particles + /// (free-surface particles are density-deficient by construction, so + /// only over-density counts as error). + /// + /// Rust: `cfd::sph::Sph::max_density_error` + #[pyo3(name = "max_density_error")] + #[pyo3(signature = ())] + fn max_density_error(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.max_density_error()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Indices of fluid particles on the free surface (density + /// deficient). + /// + /// Rust: `cfd::sph::Sph::surface_particles` + #[pyo3(name = "surface_particles")] + #[pyo3(signature = ())] + fn surface_particles<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.surface_particles())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Splat the fluid color field Σ (m/ρ) W onto a grid for surface + /// extraction. + /// + /// Rust: `cfd::sph::Sph::to_density_field` + #[pyo3(name = "to_density_field")] + #[pyo3(signature = (min, max, res))] + fn to_density_field(&self, min: crate::generated::types::PyVec3Arg, max: crate::generated::types::PyVec3Arg, res: usize) -> PyResult { + let min = min.0; + let max = max.0; + let __r = crate::runtime::guard(|| self.inner.to_density_field(min, max, res)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyFieldsScalarField3 { inner: __v }) + } + + /// Mean fluid pressure within one support radius of a wall plane. + /// + /// Rust: `cfd::sph::Sph::pressure_on_wall` + #[pyo3(name = "pressure_on_wall")] + #[pyo3(signature = (wall))] + fn pressure_on_wall(&self, wall: crate::generated::types::PySphPlane) -> PyResult { + let wall = wall.inner; + let __r = crate::runtime::guard(|| self.inner.pressure_on_wall(&wall)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "particles")] + fn py_get_particles(&self) -> PyResult> { Ok(self.inner.particles.clone().into_iter().map(|__x| crate::generated::types::PySphParticle { inner: __x }).collect::>()) } + + #[getter] + #[pyo3(name = "h")] + fn py_get_h(&self) -> PyResult { Ok(self.inner.h) } + + #[setter] + #[pyo3(name = "h")] + fn py_set_h(&mut self, v: f64) { self.inner.h = v; } + + #[getter] + #[pyo3(name = "rest_density")] + fn py_get_rest_density(&self) -> PyResult { Ok(self.inner.rest_density) } + + #[setter] + #[pyo3(name = "rest_density")] + fn py_set_rest_density(&mut self, v: f64) { self.inner.rest_density = v; } + + #[getter] + #[pyo3(name = "gamma")] + fn py_get_gamma(&self) -> PyResult { Ok(self.inner.gamma) } + + #[setter] + #[pyo3(name = "gamma")] + fn py_set_gamma(&mut self, v: f64) { self.inner.gamma = v; } + + #[getter] + #[pyo3(name = "c0")] + fn py_get_c0(&self) -> PyResult { Ok(self.inner.c0) } + + #[setter] + #[pyo3(name = "c0")] + fn py_set_c0(&mut self, v: f64) { self.inner.c0 = v; } + + #[getter] + #[pyo3(name = "viscosity")] + fn py_get_viscosity(&self) -> PyResult { Ok(self.inner.viscosity) } + + #[setter] + #[pyo3(name = "viscosity")] + fn py_set_viscosity(&mut self, v: f64) { self.inner.viscosity = v; } + + #[getter] + #[pyo3(name = "surface_tension")] + fn py_get_surface_tension(&self) -> PyResult { Ok(self.inner.surface_tension) } + + #[setter] + #[pyo3(name = "surface_tension")] + fn py_set_surface_tension(&mut self, v: f64) { self.inner.surface_tension = v; } + + #[getter] + #[pyo3(name = "gravity")] + fn py_get_gravity(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.gravity.clone() }) } + + #[getter] + #[pyo3(name = "kernel")] + fn py_get_kernel(&self) -> PyResult { Ok(crate::generated::types::PyKernel::from_rust(&self.inner.kernel.clone())) } + + #[getter] + #[pyo3(name = "dim")] + fn py_get_dim(&self) -> PyResult { Ok(self.inner.dim) } + + #[setter] + #[pyo3(name = "dim")] + fn py_set_dim(&mut self, v: usize) { self.inner.dim = v; } + + #[getter] + #[pyo3(name = "scheme")] + fn py_get_scheme(&self) -> PyResult { Ok(crate::generated::types::PySphScheme { inner: self.inner.scheme.clone() }) } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// One SPH particle. +/// +/// Rust: `cfd::sph::SphParticle` +#[pyclass(name = "SphParticle", module = "numeria.cfd.sph", from_py_object)] +#[derive(Clone)] +pub struct PySphParticle { pub inner: rust_physics_engine::cfd::sph::SphParticle } +#[pymethods] +impl PySphParticle { + /// Builds a `SphParticle` from its fields. + #[new] + #[pyo3(signature = (pos, vel, mass, rho, p, kind))] + fn __new__(pos: crate::generated::types::PyVec3Arg, vel: crate::generated::types::PyVec3Arg, mass: f64, rho: f64, p: f64, kind: crate::generated::types::PyKind) -> Self { + let pos = pos.0; + let vel = vel.0; + let kind = kind.to_rust(); + Self { inner: rust_physics_engine::cfd::sph::SphParticle { pos: pos, vel: vel, mass: mass, rho: rho, p: p, kind: kind } } + } + + #[getter] + #[pyo3(name = "pos")] + fn py_get_pos(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.pos.clone() }) } + + #[getter] + #[pyo3(name = "vel")] + fn py_get_vel(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.vel.clone() }) } + + #[getter] + #[pyo3(name = "mass")] + fn py_get_mass(&self) -> PyResult { Ok(self.inner.mass) } + + #[setter] + #[pyo3(name = "mass")] + fn py_set_mass(&mut self, v: f64) { self.inner.mass = v; } + + #[getter] + #[pyo3(name = "rho")] + fn py_get_rho(&self) -> PyResult { Ok(self.inner.rho) } + + #[setter] + #[pyo3(name = "rho")] + fn py_set_rho(&mut self, v: f64) { self.inner.rho = v; } + + #[getter] + #[pyo3(name = "p")] + fn py_get_p(&self) -> PyResult { Ok(self.inner.p) } + + #[setter] + #[pyo3(name = "p")] + fn py_set_p(&mut self, v: f64) { self.inner.p = v; } + + #[getter] + #[pyo3(name = "kind")] + fn py_get_kind(&self) -> PyResult { Ok(crate::generated::types::PyKind::from_rust(&self.inner.kind.clone())) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("SphParticle", "SphParticle", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Pressure scheme. +/// +/// Rust: `cfd::sph::SphScheme` +#[pyclass(name = "SphScheme", module = "numeria.cfd.sph", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PySphScheme { pub inner: rust_physics_engine::cfd::sph::SphScheme } +#[pymethods] +impl PySphScheme { + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("SphScheme", "SphScheme", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Pressure Poisson solver choice. +/// +/// Rust: `cfd::stable_fluids::PressureSolver` +#[pyclass(name = "PressureSolver", module = "numeria.cfd.stable_fluids", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyPressureSolver { pub inner: rust_physics_engine::cfd::stable_fluids::PressureSolver } +#[pymethods] +impl PyPressureSolver { + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("PressureSolver", "PressureSolver", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// 2D stable-fluids solver. +/// +/// Rust: `cfd::stable_fluids::StableFluid2` +#[pyclass(name = "StableFluid2", module = "numeria.cfd.stable_fluids")] +pub struct PyStableFluid2 { pub inner: rust_physics_engine::cfd::stable_fluids::StableFluid2 } +#[pymethods] +impl PyStableFluid2 { + /// New quiescent solver. + /// + /// Rust: `cfd::stable_fluids::StableFluid2::new` + #[new] + #[pyo3(signature = (nx, ny, dx))] + fn __new__(nx: usize, ny: usize, dx: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::stable_fluids::StableFluid2::new(nx, ny, dx)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyStableFluid2 { inner: __v }) + } + + /// Choose the pressure solver. + /// + /// Rust: `cfd::stable_fluids::StableFluid2::set_pressure_solver` + #[pyo3(name = "set_pressure_solver")] + #[pyo3(signature = (s))] + fn set_pressure_solver(&mut self, s: crate::generated::types::PyPressureSolver) -> PyResult<()> { + let s = s.inner; + let __r = crate::runtime::guard(|| self.inner.set_pressure_solver(s)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// One full step: advect, buoyancy, diffuse, confine vorticity, + /// project. + /// + /// Rust: `cfd::stable_fluids::StableFluid2::step` + #[pyo3(name = "step")] + #[pyo3(signature = (dt))] + fn step(&mut self, dt: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step(dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Implicit viscosity solve (I − ν dt ∇²) u = u, per component; the + /// wall ghosts follow the domain BC (no-slip mirrors with sign flip, + /// free-slip mirrors) and the moving lid enters as a source term. + /// + /// Rust: `cfd::stable_fluids::StableFluid2::diffuse` + #[pyo3(name = "diffuse")] + #[pyo3(signature = (dt))] + fn diffuse(&mut self, dt: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.diffuse(dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Vorticity confinement force ε dx (N × ω). + /// + /// Rust: `cfd::stable_fluids::StableFluid2::apply_vorticity_confinement` + #[pyo3(name = "apply_vorticity_confinement")] + #[pyo3(signature = (dt))] + fn apply_vorticity_confinement(&mut self, dt: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.apply_vorticity_confinement(dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Pressure projection: solve the Neumann Poisson problem for the + /// divergence and subtract the pressure gradient. + /// + /// Rust: `cfd::stable_fluids::StableFluid2::project` + #[pyo3(name = "project")] + #[pyo3(signature = ())] + fn project(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.project()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Splat density into a Gaussian blob at world (x, y). + /// + /// Rust: `cfd::stable_fluids::StableFluid2::add_density` + #[pyo3(name = "add_density")] + #[pyo3(signature = (x, y, amount))] + fn add_density(&mut self, x: f64, y: f64, amount: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.add_density(x, y, amount)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Splat heat (temperature) at world (x, y). + /// + /// Rust: `cfd::stable_fluids::StableFluid2::add_heat` + #[pyo3(name = "add_heat")] + #[pyo3(signature = (x, y, amount))] + fn add_heat(&mut self, x: f64, y: f64, amount: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.add_heat(x, y, amount)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Add velocity in a Gaussian blob at world (x, y). + /// + /// Rust: `cfd::stable_fluids::StableFluid2::add_velocity` + #[pyo3(name = "add_velocity")] + #[pyo3(signature = (x, y, v))] + fn add_velocity(&mut self, x: f64, y: f64, v: crate::generated::types::PyVec2Arg) -> PyResult<()> { + let v = v.0; + let __r = crate::runtime::guard(|| self.inner.add_velocity(x, y, v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Largest cell divergence magnitude. + /// + /// Rust: `cfd::stable_fluids::StableFluid2::divergence_max` + #[pyo3(name = "divergence_max")] + #[pyo3(signature = ())] + fn divergence_max(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.divergence_max()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Integrate streamlines from seed points (RK2, fixed step). + /// + /// Rust: `cfd::stable_fluids::StableFluid2::streamlines` + #[pyo3(name = "streamlines")] + #[pyo3(signature = (seeds, steps, dt))] + fn streamlines(&self, seeds: Vec, steps: usize, dt: f64) -> PyResult>> { + let seeds = seeds.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| self.inner.streamlines(&seeds, steps, dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()).collect::>()) + } + + /// Advect passive tracer particles one step (RK2). + /// + /// Rust: `cfd::stable_fluids::StableFluid2::particles_advect` + #[pyo3(name = "particles_advect")] + #[pyo3(signature = (pts, dt))] + fn particles_advect<'py>(&self, pts: pyo3::Bound<'py, pyo3::PyAny>, dt: f64) -> PyResult<()> { + let mut pts__v: Vec = pts.extract::>()?.into_iter().map(|__e| __e.0).collect(); + let __r = crate::runtime::guard(|| self.inner.particles_advect(&mut pts__v, dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + crate::runtime::coerce::write_back_objects(&pts, pts__v.into_iter().map(|__e| crate::generated::types::PyVec2 { inner: __e }).collect::>())?; + Ok(()) + } + + /// Pressure force on the solid cells (unit density): F = Σ p n dx, + /// with n the outward normal of the fluid at each solid boundary + /// face. + /// + /// Rust: `cfd::stable_fluids::StableFluid2::drag_on_solid` + #[pyo3(name = "drag_on_solid")] + #[pyo3(signature = ())] + fn drag_on_solid(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.drag_on_solid()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) + } + + /// Lift (transverse pressure force) on the solid. + /// + /// Rust: `cfd::stable_fluids::StableFluid2::lift_on_solid` + #[pyo3(name = "lift_on_solid")] + #[pyo3(signature = ())] + fn lift_on_solid(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.lift_on_solid()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Cell-centered vorticity as a field. + /// + /// Rust: `cfd::stable_fluids::StableFluid2::vorticity_field` + #[pyo3(name = "vorticity_field")] + #[pyo3(signature = ())] + fn vorticity_field(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.vorticity_field()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCellField2 { inner: __v }) + } + + /// Stream function ψ with ∇²ψ = −ω and ψ = 0 on the boundary. + /// + /// Rust: `cfd::stable_fluids::StableFluid2::stream_function` + #[pyo3(name = "stream_function")] + #[pyo3(signature = ())] + fn stream_function(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.stream_function()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCellField2 { inner: __v }) + } + + #[getter] + #[pyo3(name = "density")] + fn py_get_density(&self) -> PyResult { Ok(crate::generated::types::PyCellField2 { inner: self.inner.density.clone() }) } + + #[getter] + #[pyo3(name = "temperature")] + fn py_get_temperature(&self) -> PyResult { Ok(crate::generated::types::PyCellField2 { inner: self.inner.temperature.clone() }) } + + #[getter] + #[pyo3(name = "viscosity")] + fn py_get_viscosity(&self) -> PyResult { Ok(self.inner.viscosity) } + + #[setter] + #[pyo3(name = "viscosity")] + fn py_set_viscosity(&mut self, v: f64) { self.inner.viscosity = v; } + + #[getter] + #[pyo3(name = "buoyancy")] + fn py_get_buoyancy(&self) -> PyResult { Ok(self.inner.buoyancy) } + + #[setter] + #[pyo3(name = "buoyancy")] + fn py_set_buoyancy(&mut self, v: f64) { self.inner.buoyancy = v; } + + #[getter] + #[pyo3(name = "vorticity_confinement")] + fn py_get_vorticity_confinement(&self) -> PyResult { Ok(self.inner.vorticity_confinement) } + + #[setter] + #[pyo3(name = "vorticity_confinement")] + fn py_set_vorticity_confinement(&mut self, v: f64) { self.inner.vorticity_confinement = v; } + + #[getter] + #[pyo3(name = "bc")] + fn py_get_bc(&self) -> PyResult { Ok(crate::generated::types::PyFluidBc { inner: self.inner.bc.clone() }) } + + #[getter] + #[pyo3(name = "lid_velocity")] + fn py_get_lid_velocity(&self) -> PyResult { Ok(self.inner.lid_velocity) } + + #[setter] + #[pyo3(name = "lid_velocity")] + fn py_set_lid_velocity(&mut self, v: f64) { self.inner.lid_velocity = v; } + + #[getter] + #[pyo3(name = "thermal_diffusivity")] + fn py_get_thermal_diffusivity(&self) -> PyResult { Ok(self.inner.thermal_diffusivity) } + + #[setter] + #[pyo3(name = "thermal_diffusivity")] + fn py_set_thermal_diffusivity(&mut self, v: f64) { self.inner.thermal_diffusivity = v; } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// Minimal 3D stable-fluids solver (semi-Lagrangian advection + CG +/// projection). +/// +/// Rust: `cfd::stable_fluids::StableFluid3` +#[pyclass(name = "StableFluid3", module = "numeria.cfd.stable_fluids")] +pub struct PyStableFluid3 { pub inner: rust_physics_engine::cfd::stable_fluids::StableFluid3 } +#[pymethods] +impl PyStableFluid3 { + /// New quiescent 3D solver. + /// + /// Rust: `cfd::stable_fluids::StableFluid3::new` + #[new] + #[pyo3(signature = (nx, ny, nz, dx))] + fn __new__(nx: usize, ny: usize, nz: usize, dx: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::stable_fluids::StableFluid3::new(nx, ny, nz, dx)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyStableFluid3 { inner: __v }) + } + + /// One step: semi-Lagrangian velocity advection, buoyancy, project. + /// + /// Rust: `cfd::stable_fluids::StableFluid3::step` + #[pyo3(name = "step")] + #[pyo3(signature = (dt))] + fn step(&mut self, dt: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step(dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// PCG pressure projection. + /// + /// Rust: `cfd::stable_fluids::StableFluid3::project` + #[pyo3(name = "project")] + #[pyo3(signature = ())] + fn project(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.project()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + #[getter] + #[pyo3(name = "density")] + fn py_get_density(&self) -> PyResult> { Ok(self.inner.density.clone()) } + + #[getter] + #[pyo3(name = "buoyancy")] + fn py_get_buoyancy(&self) -> PyResult { Ok(self.inner.buoyancy) } + + #[setter] + #[pyo3(name = "buoyancy")] + fn py_set_buoyancy(&mut self, v: f64) { self.inner.buoyancy = v; } + + #[getter] + #[pyo3(name = "temperature")] + fn py_get_temperature(&self) -> PyResult> { Ok(self.inner.temperature.clone()) } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// Homogeneous (0D) k-epsilon model state advanced by production balance. +/// +/// Rust: `cfd::turbulence::KEpsilon` +#[pyclass(name = "KEpsilon", module = "numeria.cfd.turbulence", from_py_object)] +#[derive(Clone)] +pub struct PyKEpsilon { pub inner: rust_physics_engine::cfd::turbulence::KEpsilon } +#[pymethods] +impl PyKEpsilon { + /// + /// Rust: `cfd::turbulence::KEpsilon::new` + #[new] + #[pyo3(signature = (k0, eps0, nu, variant))] + fn __new__(k0: f64, eps0: f64, nu: f64, variant: crate::generated::types::PyKEpsilonVariant) -> PyResult { + let variant = variant.to_rust(); + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::turbulence::KEpsilon::new(k0, eps0, nu, variant)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyKEpsilon { inner: __v }) + } + + /// Initialize from turbulence intensity `ti` (fraction), mean speed and a + /// length scale: k = 1.5 (ti U)^2, eps = C_mu^{3/4} k^{3/2} / L. + /// + /// Rust: `cfd::turbulence::KEpsilon::init_from_intensity` + #[pyo3(name = "init_from_intensity")] + #[staticmethod] + #[pyo3(signature = (ti, u_mean, length, nu))] + fn init_from_intensity(ti: f64, u_mean: f64, length: f64, nu: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::turbulence::KEpsilon::init_from_intensity(ti, u_mean, length, nu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyKEpsilon { inner: __v }) + } + + /// Eddy viscosity nu_t = C_mu k^2 / eps. + /// + /// Rust: `cfd::turbulence::KEpsilon::nu_t` + #[pyo3(name = "nu_t")] + #[pyo3(signature = ())] + fn nu_t(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.nu_t()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Production term P_k = nu_t |S|^2 with |S| = sqrt(2 S:S) given as + /// `s_mag`. + /// + /// Rust: `cfd::turbulence::KEpsilon::production` + #[pyo3(name = "production")] + #[pyo3(signature = (s_mag))] + fn production(&self, s_mag: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.production(s_mag)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Advance the homogeneous model one step of size `dt` under mean strain + /// magnitude `s_mag`: + /// dk/dt = P - eps, deps/dt = (C1 P - C2 eps) eps / k. + /// + /// Rust: `cfd::turbulence::KEpsilon::step` + #[pyo3(name = "step")] + #[pyo3(signature = (s_mag, dt))] + fn step(&mut self, s_mag: f64, dt: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step(s_mag, dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Standard wall-function values at wall distance `y` for friction + /// velocity `u_tau`: returns `(k, epsilon)` in the log layer. + /// + /// Rust: `cfd::turbulence::KEpsilon::wall_function` + #[pyo3(name = "wall_function")] + #[pyo3(signature = (u_tau, y))] + fn wall_function(&self, u_tau: f64, y: f64) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| self.inner.wall_function(u_tau, y)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) + } + + #[getter] + #[pyo3(name = "k")] + fn py_get_k(&self) -> PyResult { Ok(self.inner.k) } + + #[setter] + #[pyo3(name = "k")] + fn py_set_k(&mut self, v: f64) { self.inner.k = v; } + + #[getter] + #[pyo3(name = "epsilon")] + fn py_get_epsilon(&self) -> PyResult { Ok(self.inner.epsilon) } + + #[setter] + #[pyo3(name = "epsilon")] + fn py_set_epsilon(&mut self, v: f64) { self.inner.epsilon = v; } + + #[getter] + #[pyo3(name = "nu")] + fn py_get_nu(&self) -> PyResult { Ok(self.inner.nu) } + + #[setter] + #[pyo3(name = "nu")] + fn py_set_nu(&mut self, v: f64) { self.inner.nu = v; } + + #[getter] + #[pyo3(name = "variant")] + fn py_get_variant(&self) -> PyResult { Ok(crate::generated::types::PyKEpsilonVariant::from_rust(&self.inner.variant.clone())) } + + #[getter] + #[pyo3(name = "c_mu")] + fn py_get_c_mu(&self) -> PyResult { Ok(self.inner.c_mu) } + + #[setter] + #[pyo3(name = "c_mu")] + fn py_set_c_mu(&mut self, v: f64) { self.inner.c_mu = v; } + + #[getter] + #[pyo3(name = "c1")] + fn py_get_c1(&self) -> PyResult { Ok(self.inner.c1) } + + #[setter] + #[pyo3(name = "c1")] + fn py_set_c1(&mut self, v: f64) { self.inner.c1 = v; } + + #[getter] + #[pyo3(name = "c2")] + fn py_get_c2(&self) -> PyResult { Ok(self.inner.c2) } + + #[setter] + #[pyo3(name = "c2")] + fn py_set_c2(&mut self, v: f64) { self.inner.c2 = v; } + + #[getter] + #[pyo3(name = "sigma_k")] + fn py_get_sigma_k(&self) -> PyResult { Ok(self.inner.sigma_k) } + + #[setter] + #[pyo3(name = "sigma_k")] + fn py_set_sigma_k(&mut self, v: f64) { self.inner.sigma_k = v; } + + #[getter] + #[pyo3(name = "sigma_eps")] + fn py_get_sigma_eps(&self) -> PyResult { Ok(self.inner.sigma_eps) } + + #[setter] + #[pyo3(name = "sigma_eps")] + fn py_set_sigma_eps(&mut self, v: f64) { self.inner.sigma_eps = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("KEpsilon", "KEpsilon", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Which k-epsilon variant to use. +/// +/// Rust: `cfd::turbulence::KEpsilonVariant` +#[pyclass(name = "KEpsilonVariant", module = "numeria.cfd.turbulence", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyKEpsilonVariant { + Standard, + Rng, + Realizable, +} +impl PyKEpsilonVariant { + pub fn to_rust(&self) -> rust_physics_engine::cfd::turbulence::KEpsilonVariant { match self { + Self::Standard => rust_physics_engine::cfd::turbulence::KEpsilonVariant::Standard, + Self::Rng => rust_physics_engine::cfd::turbulence::KEpsilonVariant::Rng, + Self::Realizable => rust_physics_engine::cfd::turbulence::KEpsilonVariant::Realizable, + } } + pub fn from_rust(v: &rust_physics_engine::cfd::turbulence::KEpsilonVariant) -> Self { match v { + rust_physics_engine::cfd::turbulence::KEpsilonVariant::Standard => Self::Standard, + rust_physics_engine::cfd::turbulence::KEpsilonVariant::Rng => Self::Rng, + rust_physics_engine::cfd::turbulence::KEpsilonVariant::Realizable => Self::Realizable, + } } +} +#[pymethods] +impl PyKEpsilonVariant { + fn __repr__(&self) -> &'static str { + match self { + Self::Standard => "KEpsilonVariant.Standard", + Self::Rng => "KEpsilonVariant.Rng", + Self::Realizable => "KEpsilonVariant.Realizable", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Homogeneous k-omega SST model (Menter 1994), blended toward k-omega near +/// `blend = 1` and k-epsilon at `blend = 0`. +/// +/// Rust: `cfd::turbulence::KOmegaSst` +#[pyclass(name = "KOmegaSst", module = "numeria.cfd.turbulence", from_py_object)] +#[derive(Clone)] +pub struct PyKOmegaSst { pub inner: rust_physics_engine::cfd::turbulence::KOmegaSst } +#[pymethods] +impl PyKOmegaSst { + /// + /// Rust: `cfd::turbulence::KOmegaSst::new` + #[new] + #[pyo3(signature = (k0, omega0, nu))] + fn __new__(k0: f64, omega0: f64, nu: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::turbulence::KOmegaSst::new(k0, omega0, nu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyKOmegaSst { inner: __v }) + } + + /// SST eddy viscosity with the shear limiter: + /// nu_t = a1 k / max(a1 omega, |S| F2); pass F2 = 1 in free shear. + /// + /// Rust: `cfd::turbulence::KOmegaSst::nu_t` + #[pyo3(name = "nu_t")] + #[pyo3(signature = (s_mag, f2))] + fn nu_t(&self, s_mag: f64, f2: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.nu_t(s_mag, f2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Advance the homogeneous model with blending function `f1` in [0, 1] + /// (1 = inner k-omega constants, 0 = transformed k-epsilon constants). + /// + /// Rust: `cfd::turbulence::KOmegaSst::step` + #[pyo3(name = "step")] + #[pyo3(signature = (s_mag, f1, dt))] + fn step(&mut self, s_mag: f64, f1: f64, dt: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step(s_mag, f1, dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + #[getter] + #[pyo3(name = "k")] + fn py_get_k(&self) -> PyResult { Ok(self.inner.k) } + + #[setter] + #[pyo3(name = "k")] + fn py_set_k(&mut self, v: f64) { self.inner.k = v; } + + #[getter] + #[pyo3(name = "omega")] + fn py_get_omega(&self) -> PyResult { Ok(self.inner.omega) } + + #[setter] + #[pyo3(name = "omega")] + fn py_set_omega(&mut self, v: f64) { self.inner.omega = v; } + + #[getter] + #[pyo3(name = "nu")] + fn py_get_nu(&self) -> PyResult { Ok(self.inner.nu) } + + #[setter] + #[pyo3(name = "nu")] + fn py_set_nu(&mut self, v: f64) { self.inner.nu = v; } + + #[getter] + #[pyo3(name = "a1")] + fn py_get_a1(&self) -> PyResult { Ok(self.inner.a1) } + + #[setter] + #[pyo3(name = "a1")] + fn py_set_a1(&mut self, v: f64) { self.inner.a1 = v; } + + #[getter] + #[pyo3(name = "beta_star")] + fn py_get_beta_star(&self) -> PyResult { Ok(self.inner.beta_star) } + + #[setter] + #[pyo3(name = "beta_star")] + fn py_set_beta_star(&mut self, v: f64) { self.inner.beta_star = v; } + + fn __repr__(&self) -> String { format!("KOmegaSst(k={:?}, omega={:?}, nu={:?}, a1={:?}, beta_star={:?})", self.inner.k, self.inner.omega, self.inner.nu, self.inner.a1, self.inner.beta_star) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A `KOmegaSst` argument, or anything that can stand in for one. +pub struct PyKOmegaSstArg(pub rust_physics_engine::cfd::turbulence::KOmegaSst); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyKOmegaSstArg { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result { + if let Ok(__w) = obj.extract::() { + return Ok(PyKOmegaSstArg(__w.inner)); + } + let __v = crate::runtime::coerce::floats_exact(obj, 5, "KOmegaSst")?; + Ok(PyKOmegaSstArg(rust_physics_engine::cfd::turbulence::KOmegaSst { k: __v[0], omega: __v[1], nu: __v[2], a1: __v[3], beta_star: __v[4] })) + } +} + + +/// Homogeneous Spalart-Allmaras one-equation model (no wall term). +/// +/// Rust: `cfd::turbulence::SpalartAllmaras` +#[pyclass(name = "SpalartAllmaras", module = "numeria.cfd.turbulence", from_py_object)] +#[derive(Clone)] +pub struct PySpalartAllmaras { pub inner: rust_physics_engine::cfd::turbulence::SpalartAllmaras } +#[pymethods] +impl PySpalartAllmaras { + /// + /// Rust: `cfd::turbulence::SpalartAllmaras::new` + #[new] + #[pyo3(signature = (nu_tilde0, nu))] + fn __new__(nu_tilde0: f64, nu: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::turbulence::SpalartAllmaras::new(nu_tilde0, nu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySpalartAllmaras { inner: __v }) + } + + /// Eddy viscosity nu_t = nu_tilde fv1, fv1 = chi^3/(chi^3 + cv1^3). + /// + /// Rust: `cfd::turbulence::SpalartAllmaras::nu_t` + #[pyo3(name = "nu_t")] + #[pyo3(signature = ())] + fn nu_t(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.nu_t()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Advance with mean vorticity magnitude `omega_mag` and wall distance + /// `d` (destruction active for finite d). + /// + /// Rust: `cfd::turbulence::SpalartAllmaras::step` + #[pyo3(name = "step")] + #[pyo3(signature = (omega_mag, d, dt))] + fn step(&mut self, omega_mag: f64, d: f64, dt: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step(omega_mag, d, dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + #[getter] + #[pyo3(name = "nu_tilde")] + fn py_get_nu_tilde(&self) -> PyResult { Ok(self.inner.nu_tilde) } + + #[setter] + #[pyo3(name = "nu_tilde")] + fn py_set_nu_tilde(&mut self, v: f64) { self.inner.nu_tilde = v; } + + #[getter] + #[pyo3(name = "nu")] + fn py_get_nu(&self) -> PyResult { Ok(self.inner.nu) } + + #[setter] + #[pyo3(name = "nu")] + fn py_set_nu(&mut self, v: f64) { self.inner.nu = v; } + + fn __repr__(&self) -> String { format!("SpalartAllmaras(nu_tilde={:?}, nu={:?})", self.inner.nu_tilde, self.inner.nu) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A `SpalartAllmaras` argument, or anything that can stand in for one. +pub struct PySpalartAllmarasArg(pub rust_physics_engine::cfd::turbulence::SpalartAllmaras); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PySpalartAllmarasArg { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result { + if let Ok(__w) = obj.extract::() { + return Ok(PySpalartAllmarasArg(__w.inner)); + } + let __v = crate::runtime::coerce::floats_exact(obj, 2, "SpalartAllmaras")?; + Ok(PySpalartAllmarasArg(rust_physics_engine::cfd::turbulence::SpalartAllmaras { nu_tilde: __v[0], nu: __v[1] })) + } +} + + +/// Regularization kernel for the Biot-Savart sum. +/// +/// Rust: `cfd::vortex::VortexKernel` +#[pyclass(name = "VortexKernel", module = "numeria.cfd.vortex", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyVortexKernel { pub inner: rust_physics_engine::cfd::vortex::VortexKernel } +#[pymethods] +impl PyVortexKernel { + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("VortexKernel", "VortexKernel", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// 2D vortex blob method: particles carry scalar circulation. +/// +/// Rust: `cfd::vortex::VortexMethod2` +#[pyclass(name = "VortexMethod2", module = "numeria.cfd.vortex", from_py_object)] +#[derive(Clone)] +pub struct PyVortexMethod2 { pub inner: rust_physics_engine::cfd::vortex::VortexMethod2 } +#[pymethods] +impl PyVortexMethod2 { + /// + /// Rust: `cfd::vortex::VortexMethod2::new` + #[new] + #[pyo3(signature = (particles, delta))] + fn __new__(particles: Vec<(crate::generated::types::PyVec2Arg, f64)>, delta: f64) -> PyResult { + let particles = particles.into_iter().map(|__e| (__e.0.0, __e.1)).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::vortex::VortexMethod2::new(particles, delta)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVortexMethod2 { inner: __v }) + } + + /// Velocity at `p`: u = sum Gamma_i/(2 pi) (-dy, dx)/(r^2 + delta^2). + /// + /// Rust: `cfd::vortex::VortexMethod2::velocity_at` + #[pyo3(name = "velocity_at")] + #[pyo3(signature = (p))] + fn velocity_at(&self, p: crate::generated::types::PyVec2Arg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.velocity_at(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) + } + + /// One step: RK2 advection; viscosity by core spreading of `delta`. + /// + /// Rust: `cfd::vortex::VortexMethod2::step` + #[pyo3(name = "step")] + #[pyo3(signature = (dt, nu))] + fn step(&mut self, dt: f64, nu: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step(dt, nu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Flat vortex sheet of total circulation `gamma_total` along the x axis + /// from 0 to `length`, discretized into `n` blobs. + /// + /// Rust: `cfd::vortex::VortexMethod2::vortex_sheet` + #[pyo3(name = "vortex_sheet")] + #[staticmethod] + #[pyo3(signature = (n, gamma_total, length, delta))] + fn vortex_sheet(n: usize, gamma_total: f64, length: f64, delta: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::vortex::VortexMethod2::vortex_sheet(n, gamma_total, length, delta)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVortexMethod2 { inner: __v }) + } + + /// Sinusoidally perturbed periodic vortex sheet (one wavelength) for + /// Kelvin-Helmholtz roll-up studies. + /// + /// Rust: `cfd::vortex::VortexMethod2::kelvin_helmholtz_roll_up` + #[pyo3(name = "kelvin_helmholtz_roll_up")] + #[staticmethod] + #[pyo3(signature = (n, delta_u, wavelength, amp))] + fn kelvin_helmholtz_roll_up(n: usize, delta_u: f64, wavelength: f64, amp: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::vortex::VortexMethod2::kelvin_helmholtz_roll_up(n, delta_u, wavelength, amp)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVortexMethod2 { inner: __v }) + } + + /// Two counter-rotating point vortices separated by `d` (propagate + /// together at Gamma/(2 pi d)). + /// + /// Rust: `cfd::vortex::VortexMethod2::point_vortex_pair` + #[pyo3(name = "point_vortex_pair")] + #[staticmethod] + #[pyo3(signature = (gamma, d))] + fn point_vortex_pair(gamma: f64, d: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::vortex::VortexMethod2::point_vortex_pair(gamma, d)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVortexMethod2 { inner: __v }) + } + + /// Discretize a Lamb-Oseen vortex of circulation `gamma` and core radius + /// `r_c` into `n` rings of blobs. + /// + /// Rust: `cfd::vortex::VortexMethod2::lamb_oseen_init` + #[pyo3(name = "lamb_oseen_init")] + #[staticmethod] + #[pyo3(signature = (gamma, r_c, n))] + fn lamb_oseen_init(gamma: f64, r_c: f64, n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::vortex::VortexMethod2::lamb_oseen_init(gamma, r_c, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVortexMethod2 { inner: __v }) + } + + /// Merge blobs closer than `eps`: circulation adds, position is the + /// circulation-magnitude-weighted centroid. + /// + /// Rust: `cfd::vortex::VortexMethod2::merge_near` + #[pyo3(name = "merge_near")] + #[pyo3(signature = (eps))] + fn merge_near(&mut self, eps: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.merge_near(eps)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Sample the blob velocity field onto a MAC grid covering the particle + /// bounding box (padded by 10%). + /// + /// Rust: `cfd::vortex::VortexMethod2::to_grid` + #[pyo3(name = "to_grid")] + #[pyo3(signature = (nx, ny))] + fn to_grid(&self, nx: usize, ny: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.to_grid(nx, ny)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMacGrid2 { inner: __v }) + } + + #[getter] + #[pyo3(name = "particles")] + fn py_get_particles(&self) -> PyResult> { Ok(self.inner.particles.clone().into_iter().map(|__x| (crate::generated::types::PyVec2 { inner: __x.0 }, __x.1)).collect::>()) } + + #[getter] + #[pyo3(name = "delta")] + fn py_get_delta(&self) -> PyResult { Ok(self.inner.delta) } + + #[setter] + #[pyo3(name = "delta")] + fn py_set_delta(&mut self, v: f64) { self.inner.delta = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("VortexMethod2", "VortexMethod2", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// 3D vortex particle method with direct Biot-Savart summation. +/// +/// Rust: `cfd::vortex::VortexMethod3` +#[pyclass(name = "VortexMethod3", module = "numeria.cfd.vortex", from_py_object)] +#[derive(Clone)] +pub struct PyVortexMethod3 { pub inner: rust_physics_engine::cfd::vortex::VortexMethod3 } +#[pymethods] +impl PyVortexMethod3 { + /// + /// Rust: `cfd::vortex::VortexMethod3::new` + #[new] + #[pyo3(signature = (particles, kernel))] + fn __new__(particles: Vec, kernel: crate::generated::types::PyVortexKernel) -> PyResult { + let particles = particles.into_iter().map(|__e| __e.inner).collect::>(); + let kernel = kernel.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::vortex::VortexMethod3::new(particles, kernel)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVortexMethod3 { inner: __v }) + } + + /// Velocity induced at `p` by all particles: + /// u = -(1/4pi) sum (p - x_i) x alpha_i K(|p - x_i|). + /// + /// Rust: `cfd::vortex::VortexMethod3::velocity_at` + #[pyo3(name = "velocity_at")] + #[pyo3(signature = (p))] + fn velocity_at(&self, p: crate::generated::types::PyVec3Arg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.velocity_at(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// One step: RK2 advection of positions, vortex stretching of strengths + /// (classical scheme, alpha' = (alpha . grad) u), and viscous diffusion + /// by core spreading (the kernel radius grows as sigma^2 += 2 nu dt). + /// + /// Rust: `cfd::vortex::VortexMethod3::step` + #[pyo3(name = "step")] + #[pyo3(signature = (dt, nu))] + fn step(&mut self, dt: f64, nu: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step(dt, nu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// A discretized circular vortex ring of circulation `strength` in the + /// plane perpendicular to `normal`. + /// + /// Rust: `cfd::vortex::VortexMethod3::vortex_ring` + #[pyo3(name = "vortex_ring")] + #[staticmethod] + #[pyo3(signature = (center, radius, strength, core, n))] + fn vortex_ring(center: crate::generated::types::PyVec3Arg, radius: f64, strength: f64, core: f64, n: usize) -> PyResult { + let center = center.0; + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::vortex::VortexMethod3::vortex_ring(center, radius, strength, core, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVortexMethod3 { inner: __v }) + } + + /// Two coaxial rings of the same sign separated by `gap` along z: the + /// classical leapfrogging configuration. + /// + /// Rust: `cfd::vortex::VortexMethod3::two_rings_leapfrog` + #[pyo3(name = "two_rings_leapfrog")] + #[staticmethod] + #[pyo3(signature = (radius, strength, core, gap, n))] + fn two_rings_leapfrog(radius: f64, strength: f64, core: f64, gap: f64, n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::cfd::vortex::VortexMethod3::two_rings_leapfrog(radius, strength, core, gap, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVortexMethod3 { inner: __v }) + } + + /// Discrete helicity sum u(x_i) . alpha_i. + /// + /// Rust: `cfd::vortex::VortexMethod3::helicity` + #[pyo3(name = "helicity")] + #[pyo3(signature = ())] + fn helicity(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.helicity()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Particle-strength enstrophy proxy sum |alpha_i|^2. + /// + /// Rust: `cfd::vortex::VortexMethod3::enstrophy` + #[pyo3(name = "enstrophy")] + #[pyo3(signature = ())] + fn enstrophy(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.enstrophy()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Kinetic energy estimate E = (1/8pi) sum_{i != j} alpha_i . alpha_j / + /// r_ij (regularized with the particle core). + /// + /// Rust: `cfd::vortex::VortexMethod3::kinetic_energy` + #[pyo3(name = "kinetic_energy")] + #[pyo3(signature = ())] + fn kinetic_energy(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.kinetic_energy()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Hydrodynamic impulse I = (1/2) sum x_i x alpha_i. + /// + /// Rust: `cfd::vortex::VortexMethod3::impulse` + #[pyo3(name = "impulse")] + #[pyo3(signature = ())] + fn impulse(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.impulse()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Merge particles onto a lattice of the given spacing: strengths add, + /// positions are strength-weighted centroids. + /// + /// Rust: `cfd::vortex::VortexMethod3::remesh` + #[pyo3(name = "remesh")] + #[pyo3(signature = (spacing))] + fn remesh(&mut self, spacing: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.remesh(spacing)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + #[getter] + #[pyo3(name = "particles")] + fn py_get_particles(&self) -> PyResult> { Ok(self.inner.particles.clone().into_iter().map(|__x| crate::generated::types::PyVortexParticle { inner: __x }).collect::>()) } + + #[getter] + #[pyo3(name = "kernel")] + fn py_get_kernel(&self) -> PyResult { Ok(crate::generated::types::PyVortexKernel { inner: self.inner.kernel.clone() }) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("VortexMethod3", "VortexMethod3", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A vector-valued vortex particle: strength is circulation times length +/// (the integral of vorticity over the particle's volume). +/// +/// Rust: `cfd::vortex::VortexParticle` +#[pyclass(name = "VortexParticle", module = "numeria.cfd.vortex", from_py_object)] +#[derive(Clone)] +pub struct PyVortexParticle { pub inner: rust_physics_engine::cfd::vortex::VortexParticle } +#[pymethods] +impl PyVortexParticle { + /// Builds a `VortexParticle` from its fields. + #[new] + #[pyo3(signature = (pos, strength, core))] + fn __new__(pos: crate::generated::types::PyVec3Arg, strength: crate::generated::types::PyVec3Arg, core: f64) -> Self { + let pos = pos.0; + let strength = strength.0; + Self { inner: rust_physics_engine::cfd::vortex::VortexParticle { pos: pos, strength: strength, core: core } } + } + + #[getter] + #[pyo3(name = "pos")] + fn py_get_pos(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.pos.clone() }) } + + #[getter] + #[pyo3(name = "strength")] + fn py_get_strength(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.strength.clone() }) } + + #[getter] + #[pyo3(name = "core")] + fn py_get_core(&self) -> PyResult { Ok(self.inner.core) } + + #[setter] + #[pyo3(name = "core")] + fn py_set_core(&mut self, v: f64) { self.inner.core = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("VortexParticle", "VortexParticle", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} diff --git a/bindings/python/src/generated/types/codes.rs b/bindings/python/src/generated/types/codes.rs new file mode 100644 index 0000000..09de4fb --- /dev/null +++ b/bindings/python/src/generated/types/codes.rs @@ -0,0 +1,2253 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; + + +/// A matrix over `GF(2)`, one bit per entry, packed sixty-four to a word. +/// +/// Packing is not only for space: a row operation becomes a handful of word +/// XORs rather than a loop over bits, so elimination on a code-sized matrix +/// costs what a floating-point elimination on a matrix sixty-four times +/// smaller would. +/// +/// Rust: `codes::block::Gf2Matrix` +#[pyclass(name = "Gf2Matrix", module = "numeria.codes.block", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyGf2Matrix { pub inner: rust_physics_engine::codes::block::Gf2Matrix } +#[pymethods] +impl PyGf2Matrix { + /// Builds a `Gf2Matrix` from its fields. + #[new] + #[pyo3(signature = (rows, cols, data))] + fn __new__(rows: usize, cols: usize, data: Vec) -> Self { + + Self { inner: rust_physics_engine::codes::block::Gf2Matrix { rows: rows, cols: cols, data: data } } + } + + /// Words needed to hold one row. + /// + /// Rust: `codes::block::Gf2Matrix::words_per_row` + #[pyo3(name = "words_per_row")] + #[pyo3(signature = ())] + fn words_per_row(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.words_per_row()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// An all-zero matrix. + /// + /// Rust: `codes::block::Gf2Matrix::zeros` + #[pyo3(name = "zeros")] + #[staticmethod] + #[pyo3(signature = (rows, cols))] + fn zeros(rows: usize, cols: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::block::Gf2Matrix::zeros(rows, cols)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGf2Matrix { inner: __v }) + } + + /// The `n` by `n` identity. + /// + /// Rust: `codes::block::Gf2Matrix::identity` + #[pyo3(name = "identity")] + #[staticmethod] + #[pyo3(signature = (n))] + fn identity(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::block::Gf2Matrix::identity(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGf2Matrix { inner: __v }) + } + + /// A matrix from rows of booleans. + /// + /// Panics: + /// Panics if the rows are not all the same length. + /// + /// Rust: `codes::block::Gf2Matrix::from_rows` + #[pyo3(name = "from_rows")] + #[staticmethod] + #[pyo3(signature = (rows))] + fn from_rows(rows: Vec>) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::block::Gf2Matrix::from_rows(&rows)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGf2Matrix { inner: __v }) + } + + /// The entry at `(r, c)`. + /// + /// Panics: + /// Panics if the index is out of range. + /// + /// Rust: `codes::block::Gf2Matrix::get` + #[pyo3(name = "get")] + #[pyo3(signature = (r, c))] + fn get(&self, r: usize, c: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.get(r, c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Sets the entry at `(r, c)`. + /// + /// Panics: + /// Panics if the index is out of range. + /// + /// Rust: `codes::block::Gf2Matrix::set` + #[pyo3(name = "set")] + #[pyo3(signature = (r, c, value))] + fn set(&mut self, r: usize, c: usize, value: bool) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.set(r, c, value)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Row `r` as a vector of booleans. + /// + /// Panics: + /// Panics if `r` is out of range. + /// + /// Rust: `codes::block::Gf2Matrix::row` + #[pyo3(name = "row")] + #[pyo3(signature = (r))] + fn row<'py>(&self, py: Python<'py>, r: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.row(r))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Every row as a vector of booleans. + /// + /// Rust: `codes::block::Gf2Matrix::to_rows` + #[pyo3(name = "to_rows")] + #[pyo3(signature = ())] + fn to_rows<'py>(&self, py: Python<'py>) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.to_rows())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The reduced row echelon form, and the pivot column of each non-zero + /// row in order. + /// + /// Over `GF(2)` there is no scaling step: the only non-zero scalar is + /// one. Elimination is therefore exactly "find a row with a one in this + /// column, move it up, and XOR it into every other row that has one". + /// + /// Rust: `codes::block::Gf2Matrix::rref` + #[pyo3(name = "rref")] + #[pyo3(signature = ())] + fn rref(&self) -> PyResult<(crate::generated::types::PyGf2Matrix, Vec)> { + let __r = crate::runtime::guard(|| self.inner.rref()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((crate::generated::types::PyGf2Matrix { inner: __v.0 }, __v.1)) + } + + /// The rank: the number of independent rows. + /// + /// Rust: `codes::block::Gf2Matrix::rank` + #[pyo3(name = "rank")] + #[pyo3(signature = ())] + fn rank(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.rank()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The transpose. + /// + /// Rust: `codes::block::Gf2Matrix::transpose` + #[pyo3(name = "transpose")] + #[pyo3(signature = ())] + fn transpose(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.transpose()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGf2Matrix { inner: __v }) + } + + /// The matrix product over `GF(2)`. + /// + /// Panics: + /// Panics unless the shapes agree. + /// + /// Rust: `codes::block::Gf2Matrix::mul` + #[pyo3(name = "mul")] + #[pyo3(signature = (other))] + fn mul(&self, other: crate::generated::types::PyGf2Matrix) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.mul(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGf2Matrix { inner: __v }) + } + + /// The product with a column vector: `M x'`. + /// + /// Panics: + /// Panics unless `x` has one entry per column. + /// + /// Rust: `codes::block::Gf2Matrix::mul_vec` + #[pyo3(name = "mul_vec")] + #[pyo3(signature = (x))] + fn mul_vec<'py>(&self, py: Python<'py>, x: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.mul_vec(&x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The product with a row vector on the left: `x M`. + /// + /// Panics: + /// Panics unless `x` has one entry per row. + /// + /// Rust: `codes::block::Gf2Matrix::vec_mul` + #[pyo3(name = "vec_mul")] + #[pyo3(signature = (x))] + fn vec_mul<'py>(&self, py: Python<'py>, x: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.vec_mul(&x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// A solution `x` of `M x' = b'`, or `None` if there is none. + /// + /// Any solution: the system is under-determined whenever the kernel is + /// non-trivial, and the free variables are left at zero. + /// + /// Panics: + /// Panics unless `b` has one entry per row. + /// + /// Rust: `codes::block::Gf2Matrix::solve` + #[pyo3(name = "solve")] + #[pyo3(signature = (b))] + fn solve<'py>(&self, py: Python<'py>, b: Vec) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.solve(&b))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) + } + + /// A basis for the kernel `{ x : M x' = 0 }`. + /// + /// One basis vector per free column: set that free variable to one, the + /// others to zero, and read the pivot variables off the echelon form. + /// The count is `cols - rank`, which is the rank-nullity theorem and is + /// what the tests check it against. + /// + /// Rust: `codes::block::Gf2Matrix::kernel_basis` + #[pyo3(name = "kernel_basis")] + #[pyo3(signature = ())] + fn kernel_basis<'py>(&self, py: Python<'py>) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.kernel_basis())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "rows")] + fn py_get_rows(&self) -> PyResult { Ok(self.inner.rows) } + + #[setter] + #[pyo3(name = "rows")] + fn py_set_rows(&mut self, v: usize) { self.inner.rows = v; } + + #[getter] + #[pyo3(name = "cols")] + fn py_get_cols(&self) -> PyResult { Ok(self.inner.cols) } + + #[setter] + #[pyo3(name = "cols")] + fn py_set_cols(&mut self, v: usize) { self.inner.cols = v; } + + #[getter] + #[pyo3(name = "data")] + fn py_get_data(&self) -> PyResult> { Ok(self.inner.data.clone()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Gf2Matrix", "Gf2Matrix", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A binary linear code, held by both of its descriptions. +/// +/// `g` is `k` by `n` and its rows are a basis of the code; `h` is `n - k` by +/// `n` and its rows are a basis of the dual, so `G H'` is zero and a word is +/// a codeword exactly when its syndrome vanishes. +/// +/// Rust: `codes::block::LinearCode` +#[pyclass(name = "LinearCode", module = "numeria.codes.block", from_py_object)] +#[derive(Clone)] +pub struct PyLinearCode { pub inner: rust_physics_engine::codes::block::LinearCode } +#[pymethods] +impl PyLinearCode { + /// Builds a `LinearCode` from its fields. + #[new] + #[pyo3(signature = (g, h, n, k, d))] + fn __new__(g: crate::generated::types::PyGf2Matrix, h: crate::generated::types::PyGf2Matrix, n: usize, k: usize, d: usize) -> Self { + let g = g.inner; + let h = h.inner; + Self { inner: rust_physics_engine::codes::block::LinearCode { g: g, h: h, n: n, k: k, d: d } } + } + + /// The code generated by the rows of `g`, with the parity check matrix + /// and minimum distance derived. + /// + /// Dependent rows are dropped, so `k` is the rank rather than the row + /// count. The parity check matrix is a basis of the kernel of `g`, which + /// is the dual code by definition. + /// + /// Panics: + /// Panics if the generator has no columns, or if the dimension exceeds + /// twenty, since the distance is found by enumerating the code. + /// + /// Rust: `codes::block::LinearCode::from_generator` + #[pyo3(name = "from_generator")] + #[staticmethod] + #[pyo3(signature = (g))] + fn from_generator(g: crate::generated::types::PyGf2Matrix) -> PyResult { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::codes::block::LinearCode::from_generator(&g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLinearCode { inner: __v }) + } + + /// Every codeword, in order of the message it encodes. + /// + /// Panics: + /// Panics if the dimension exceeds twenty. + /// + /// Rust: `codes::block::LinearCode::codewords` + #[pyo3(name = "codewords")] + #[pyo3(signature = ())] + fn codewords<'py>(&self, py: Python<'py>) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.codewords())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The message times the generator. + /// + /// Panics: + /// Panics unless `msg` has one bit per dimension. + /// + /// Rust: `codes::block::LinearCode::encode` + #[pyo3(name = "encode")] + #[pyo3(signature = (msg))] + fn encode<'py>(&self, py: Python<'py>, msg: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.encode(&msg))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The syndrome `H x'`, which is zero exactly on codewords. + /// + /// Panics: + /// Panics unless `recv` has one bit per position. + /// + /// Rust: `codes::block::LinearCode::syndrome` + #[pyo3(name = "syndrome")] + #[pyo3(signature = (recv))] + fn syndrome<'py>(&self, py: Python<'py>, recv: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.syndrome(&recv))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Whether the word is in the code. + /// + /// Panics: + /// Panics unless `x` has one bit per position. + /// + /// Rust: `codes::block::LinearCode::contains` + #[pyo3(name = "contains")] + #[pyo3(signature = (x))] + fn contains<'py>(&self, py: Python<'py>, x: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.contains(&x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The minimum distance, by enumeration. + /// + /// Linearity turns a search over pairs into a search over words: the + /// distance between two codewords is the weight of their difference, + /// which is itself a codeword. Zero for the zero code, which has no + /// non-zero word to measure. + /// + /// Panics: + /// Panics if the dimension exceeds twenty. + /// + /// Rust: `codes::block::LinearCode::minimum_distance_small` + #[pyo3(name = "minimum_distance_small")] + #[pyo3(signature = ())] + fn minimum_distance_small(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.minimum_distance_small()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The weight enumerator: how many codewords have each weight, indexed + /// from zero to `n`. + /// + /// The coefficients of a linear code's weight enumerator determine its + /// undetected error probability on a symmetric channel exactly, and by + /// MacWilliams's identity they determine the dual code's enumerator too. + /// Counts are exact integers because a code of dimension sixty would + /// overflow anything narrower. + /// + /// Panics: + /// Panics if the dimension exceeds twenty. + /// + /// Rust: `codes::block::LinearCode::weight_enumerator` + #[pyo3(name = "weight_enumerator")] + #[pyo3(signature = ())] + fn weight_enumerator<'py>(&self, py: Python<'py>) -> PyResult>> { + let __r = crate::runtime::guard(|| self.inner.weight_enumerator()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| -> PyResult> { Ok(crate::runtime::coerce::bigint_out(py, &__x)?) }).collect::>>()?) + } + + /// The dual code, whose generator is this one's parity check matrix. + /// + /// Panics: + /// Panics if the dual's dimension exceeds twenty. + /// + /// Rust: `codes::block::LinearCode::dual` + #[pyo3(name = "dual")] + #[pyo3(signature = ())] + fn dual(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.dual()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLinearCode { inner: __v }) + } + + /// Whether the code equals its own dual, which needs `n = 2k` and every + /// pair of generator rows orthogonal. + /// + /// Rust: `codes::block::LinearCode::is_self_dual` + #[pyo3(name = "is_self_dual")] + #[pyo3(signature = ())] + fn is_self_dual(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_self_dual()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Syndrome decoding: subtract the lightest error pattern consistent with + /// what was received. + /// + /// Returns the corrected word and how many bits were changed. Correct + /// whenever the true error weighs at most `(d - 1) / 2`; beyond that the + /// lightest consistent pattern is some other coset member and the result + /// is a different codeword, which is not a failure of the method but the + /// definition of exceeding the correction radius. + /// + /// Panics: + /// Panics unless `recv` has one bit per position. + /// + /// Rust: `codes::block::LinearCode::decode_syndrome` + #[pyo3(name = "decode_syndrome")] + #[pyo3(signature = (recv))] + fn decode_syndrome<'py>(&self, py: Python<'py>, recv: Vec) -> PyResult<(Vec, usize)> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.decode_syndrome(&recv))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) + } + + /// Decoding through an explicit standard array. + /// + /// The same answer `decode_syndrome` gives, by a + /// different route: build the whole table first, then look up. Slower per + /// word and faster per thousand words, and useful as the reference the + /// incremental search is checked against. + /// + /// Panics: + /// Panics unless `recv` has one bit per position, or if the redundancy + /// exceeds twenty. + /// + /// Rust: `codes::block::LinearCode::standard_array_decode_small` + #[pyo3(name = "standard_array_decode_small")] + #[pyo3(signature = (recv))] + fn standard_array_decode_small<'py>(&self, py: Python<'py>, recv: Vec) -> PyResult<(Vec, usize)> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.standard_array_decode_small(&recv))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) + } + + /// The Hamming code of redundancy `r`: length `2^r - 1`, dimension + /// `2^r - 1 - r`, distance three. + /// + /// The parity check matrix has every non-zero `r`-bit column exactly + /// once, which is the whole construction. A single error in position `j` + /// then produces the syndrome that *is* column `j`, so the syndrome names + /// the error outright. It is perfect: the spheres of radius one around + /// the codewords tile the space with nothing left over, since + /// `2^k (1 + n) = 2^k 2^r = 2^n`. + /// + /// The distance is three by construction rather than by search: no one + /// or two distinct non-zero columns can sum to zero, and columns one, + /// two and three do. Rediscovering that by enumerating `2^26` words is + /// the only thing that would stop the family at `r = 4`. + /// + /// Panics: + /// Panics unless `r` is between two and eight. + /// + /// Rust: `codes::block::LinearCode::hamming` + #[pyo3(name = "hamming")] + #[staticmethod] + #[pyo3(signature = (r))] + fn hamming(r: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::block::LinearCode::hamming(r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLinearCode { inner: __v }) + } + + /// The extended Hamming code: a Hamming code with an overall parity bit, + /// giving length `2^r`, the same dimension, and distance four. + /// + /// The extra bit raises the distance from three to four, which does not + /// improve correction -- still one error -- but makes two errors always + /// detectable rather than sometimes mistaken for one. That is the + /// single-error-correcting, double-error-detecting code memory uses. + /// + /// Panics: + /// Panics unless `r` is between two and eight. + /// + /// Rust: `codes::block::LinearCode::extended_hamming` + #[pyo3(name = "extended_hamming")] + #[staticmethod] + #[pyo3(signature = (r))] + fn extended_hamming(r: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::block::LinearCode::extended_hamming(r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLinearCode { inner: __v }) + } + + /// The repetition code: one bit sent `n` times, distance `n`. + /// + /// Panics: + /// Panics if `n` is zero. + /// + /// Rust: `codes::block::LinearCode::repetition` + #[pyo3(name = "repetition")] + #[staticmethod] + #[pyo3(signature = (n))] + fn repetition(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::block::LinearCode::repetition(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLinearCode { inner: __v }) + } + + /// The single parity check code: `n - 1` message bits and their parity, + /// distance two. + /// + /// The dual of the repetition code of the same length, which is why the + /// two appear together. + /// + /// Panics: + /// Panics unless `n` is at least two. + /// + /// Rust: `codes::block::LinearCode::parity_check` + #[pyo3(name = "parity_check")] + #[staticmethod] + #[pyo3(signature = (n))] + fn parity_check(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::block::LinearCode::parity_check(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLinearCode { inner: __v }) + } + + /// The binary Golay code, `[23, 12, 7]`. + /// + /// Cyclic, generated by `1 + x + x^5 + x^6 + x^7 + x^9 + x^11`, one of + /// the two irreducible factors of `x^23 - 1` over `GF(2)` besides + /// `x - 1`. It is perfect: spheres of radius three around its 4096 + /// codewords tile `GF(2)^23` exactly, since + /// `4096 * (1 + 23 + 253 + 1771) = 2^23`. Only two non-trivial perfect + /// binary codes exist -- this and the Hamming family -- so the + /// arithmetic working out is not a coincidence that could have gone + /// another way. + /// + /// Rust: `codes::block::LinearCode::golay23` + #[pyo3(name = "golay23")] + #[staticmethod] + #[pyo3(signature = ())] + fn golay23() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::block::LinearCode::golay23()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLinearCode { inner: __v }) + } + + /// The extended binary Golay code, `[24, 12, 8]`. + /// + /// Self-dual, and the distance rises to eight, so every weight is a + /// multiple of four. It corrects three errors and detects four. + /// + /// Rust: `codes::block::LinearCode::golay24` + #[pyo3(name = "golay24")] + #[staticmethod] + #[pyo3(signature = ())] + fn golay24() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::block::LinearCode::golay24()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLinearCode { inner: __v }) + } + + /// The Reed-Muller code `RM(r, m)`: length `2^m`, distance `2^(m - r)`. + /// + /// The codewords are the truth tables of every Boolean polynomial in `m` + /// variables of degree at most `r`, so the generator rows are the + /// products of up to `r` coordinate functions evaluated at all `2^m` + /// points. `RM(0, m)` is the repetition code and `RM(m - 1, m)` is the + /// single parity check code, which is the cleanest statement of what the + /// family interpolates between. + /// + /// Panics: + /// Panics unless `r <= m` and the dimension stays at or below twenty. + /// + /// Rust: `codes::block::LinearCode::reed_muller` + #[pyo3(name = "reed_muller")] + #[staticmethod] + #[pyo3(signature = (r, m))] + fn reed_muller(r: usize, m: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::block::LinearCode::reed_muller(r, m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLinearCode { inner: __v }) + } + + #[getter] + #[pyo3(name = "g")] + fn py_get_g(&self) -> PyResult { Ok(crate::generated::types::PyGf2Matrix { inner: self.inner.g.clone() }) } + + #[getter] + #[pyo3(name = "h")] + fn py_get_h(&self) -> PyResult { Ok(crate::generated::types::PyGf2Matrix { inner: self.inner.h.clone() }) } + + #[getter] + #[pyo3(name = "n")] + fn py_get_n(&self) -> PyResult { Ok(self.inner.n) } + + #[setter] + #[pyo3(name = "n")] + fn py_set_n(&mut self, v: usize) { self.inner.n = v; } + + #[getter] + #[pyo3(name = "k")] + fn py_get_k(&self) -> PyResult { Ok(self.inner.k) } + + #[setter] + #[pyo3(name = "k")] + fn py_set_k(&mut self, v: usize) { self.inner.k = v; } + + #[getter] + #[pyo3(name = "d")] + fn py_get_d(&self) -> PyResult { Ok(self.inner.d) } + + #[setter] + #[pyo3(name = "d")] + fn py_set_d(&mut self, v: usize) { self.inner.d = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("LinearCode", "LinearCode", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Packs bits into bytes, most significant bit first. +/// +/// Rust: `codes::compression::BitWriter` +#[pyclass(name = "BitWriter", module = "numeria.codes.compression", from_py_object)] +#[derive(Clone)] +pub struct PyBitWriter { pub inner: rust_physics_engine::codes::compression::BitWriter } +#[pymethods] +impl PyBitWriter { + /// An empty writer. + /// + /// Rust: `codes::compression::BitWriter::new` + #[new] + #[pyo3(signature = ())] + fn __new__() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::compression::BitWriter::new()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBitWriter { inner: __v }) + } + + /// Appends one bit. + /// + /// Rust: `codes::compression::BitWriter::push` + #[pyo3(name = "push")] + #[pyo3(signature = (bit))] + fn push(&mut self, bit: bool) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.push(bit)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Appends the low `len` bits of `code`, most significant first. + /// + /// Rust: `codes::compression::BitWriter::push_bits` + #[pyo3(name = "push_bits")] + #[pyo3(signature = (code, len))] + fn push_bits(&mut self, code: u64, len: u8) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.push_bits(code, len)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// How many bits have been written. + /// + /// Rust: `codes::compression::BitWriter::bit_len` + #[pyo3(name = "bit_len")] + #[pyo3(signature = ())] + fn bit_len(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.bit_len()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The bytes, with the last one padded with zeros. + /// + /// Rust: `codes::compression::BitWriter::finish` + #[pyo3(name = "finish")] + #[pyo3(signature = ())] + fn finish<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.clone().finish())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("BitWriter", "BitWriter", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// One LZ77 token: a back reference and the literal that follows it. +/// +/// Rust: `codes::compression::Lz77Token` +#[pyclass(name = "Lz77Token", module = "numeria.codes.compression", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyLz77Token { pub inner: rust_physics_engine::codes::compression::Lz77Token } +#[pymethods] +impl PyLz77Token { + /// Builds a `Lz77Token` from its fields. + #[new] + #[pyo3(signature = (offset, length, next))] + fn __new__(offset: usize, length: usize, next: u8) -> Self { + + Self { inner: rust_physics_engine::codes::compression::Lz77Token { offset: offset, length: length, next: next } } + } + + #[getter] + #[pyo3(name = "offset")] + fn py_get_offset(&self) -> PyResult { Ok(self.inner.offset) } + + #[setter] + #[pyo3(name = "offset")] + fn py_set_offset(&mut self, v: usize) { self.inner.offset = v; } + + #[getter] + #[pyo3(name = "length")] + fn py_get_length(&self) -> PyResult { Ok(self.inner.length) } + + #[setter] + #[pyo3(name = "length")] + fn py_set_length(&mut self, v: usize) { self.inner.length = v; } + + #[getter] + #[pyo3(name = "next")] + fn py_get_next(&self) -> PyResult { Ok(self.inner.next) } + + #[setter] + #[pyo3(name = "next")] + fn py_set_next(&mut self, v: u8) { self.inner.next = v; } + + fn __repr__(&self) -> String { format!("Lz77Token(offset={:?}, length={:?}, next={:?})", self.inner.offset, self.inner.length, self.inner.next) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A rate `1/n` convolutional code, given by its constraint length and +/// generator polynomials. +/// +/// The generators are the taps of the shift register, conventionally written +/// in octal: the NASA standard's `171` and `133` are `0o171` and `0o133`, +/// seven bits each for a constraint length of seven. Bit `k - 1` of a +/// generator is the current input and bit zero the oldest bit in memory. +/// +/// Rust: `codes::convolutional::ConvolutionalCode` +#[pyclass(name = "ConvolutionalCode", module = "numeria.codes.convolutional", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyConvolutionalCode { pub inner: rust_physics_engine::codes::convolutional::ConvolutionalCode } +#[pymethods] +impl PyConvolutionalCode { + /// The code with the given constraint length and generators. + /// + /// Panics: + /// Panics unless the constraint length is between two and sixteen, there + /// is at least one generator, and every generator fits in `k` bits. + /// + /// Rust: `codes::convolutional::ConvolutionalCode::new` + #[new] + #[pyo3(signature = (k, polys))] + fn __new__(k: u32, polys: Vec) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::convolutional::ConvolutionalCode::new(k, &polys)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyConvolutionalCode { inner: __v }) + } + + /// The rate-`1/2`, constraint-length-seven code used on essentially every + /// NASA mission of the Voyager era and standardised by CCSDS. + /// + /// Generators `171` and `133` in octal, free distance ten. + /// + /// Rust: `codes::convolutional::ConvolutionalCode::nasa_standard` + #[pyo3(name = "nasa_standard")] + #[staticmethod] + #[pyo3(signature = ())] + fn nasa_standard() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::convolutional::ConvolutionalCode::nasa_standard()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyConvolutionalCode { inner: __v }) + } + + /// Bits of memory: one fewer than the constraint length. + /// + /// Rust: `codes::convolutional::ConvolutionalCode::memory` + #[pyo3(name = "memory")] + #[pyo3(signature = ())] + fn memory(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.memory()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The number of trellis states, `2^(k-1)`. + /// + /// Rust: `codes::convolutional::ConvolutionalCode::trellis_states` + #[pyo3(name = "trellis_states")] + #[pyo3(signature = ())] + fn trellis_states(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.trellis_states()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Output bits per input bit. + /// + /// Rust: `codes::convolutional::ConvolutionalCode::outputs` + #[pyo3(name = "outputs")] + #[pyo3(signature = ())] + fn outputs(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.outputs()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The outputs and next state for one input bit from one state. + /// + /// Rust: `codes::convolutional::ConvolutionalCode::step` + #[pyo3(name = "step")] + #[pyo3(signature = (state, input))] + fn step(&self, state: usize, input: bool) -> PyResult<(Vec, usize)> { + let __r = crate::runtime::guard(|| self.inner.step(state, input)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) + } + + /// Encodes a message, flushing the register with `k - 1` zeros so the + /// trellis ends where it started. + /// + /// Termination costs `k - 1` bits of rate and buys the decoder a known + /// endpoint, which is worth far more than it costs on any message longer + /// than the register. + /// + /// Rust: `codes::convolutional::ConvolutionalCode::encode` + #[pyo3(name = "encode")] + #[pyo3(signature = (bits))] + fn encode<'py>(&self, py: Python<'py>, bits: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.encode(&bits))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Maximum-likelihood decoding of a hard-decision stream by the Viterbi + /// algorithm. + /// + /// Panics: + /// Panics unless the stream's length is a multiple of the output count + /// and long enough to hold the flush. + /// + /// Rust: `codes::convolutional::ConvolutionalCode::viterbi_decode` + #[pyo3(name = "viterbi_decode")] + #[pyo3(signature = (recv_hard))] + fn viterbi_decode<'py>(&self, py: Python<'py>, recv_hard: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.viterbi_decode(&recv_hard))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Maximum-likelihood decoding from log-likelihood ratios, where a + /// positive value leans towards a zero bit. + /// + /// Soft decisions are worth about two decibels over hard ones on a + /// Gaussian channel, for no change to the algorithm beyond the branch + /// metric: a bit the demodulator was unsure of should not outvote one it + /// was certain of, and a hard decision throws away exactly that. + /// + /// Panics: + /// Panics unless the stream's length is a multiple of the output count + /// and long enough to hold the flush. + /// + /// Rust: `codes::convolutional::ConvolutionalCode::viterbi_soft` + #[pyo3(name = "viterbi_soft")] + #[pyo3(signature = (llr))] + fn viterbi_soft<'py>(&self, py: Python<'py>, llr: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.viterbi_soft(&llr))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The free distance: the smallest Hamming weight of any encoded path + /// that leaves the all-zero state and returns to it. + /// + /// The code is linear, so the distance between two encoded sequences is + /// the weight of the encoding of their difference; the worst case is + /// therefore the lightest non-zero excursion, and that is what an error + /// event costs. Found by shortest path over the trellis, with the first + /// step forced to a one so the excursion is genuinely non-zero. + /// + /// Rust: `codes::convolutional::ConvolutionalCode::free_distance_estimate` + #[pyo3(name = "free_distance_estimate")] + #[pyo3(signature = ())] + fn free_distance_estimate(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.free_distance_estimate()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Drops the encoded bits the pattern marks as absent, cycling the + /// pattern across the stream. + /// + /// Puncturing raises the rate without changing the encoder or the + /// decoder: the receiver puts a zero log-likelihood -- no information -- + /// where a punctured bit would have been, and Viterbi carries on. One + /// hardware design then serves every rate a link needs. + /// + /// Panics: + /// Panics on an empty pattern, or one that deletes everything. + /// + /// Rust: `codes::convolutional::ConvolutionalCode::puncture` + #[pyo3(name = "puncture")] + #[pyo3(signature = (encoded, pattern))] + fn puncture<'py>(&self, py: Python<'py>, encoded: Vec, pattern: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.puncture(&encoded, &pattern))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Restores a punctured stream to full length, with zero -- meaning no + /// evidence either way -- wherever a bit was dropped. + /// + /// Panics: + /// Panics on an empty pattern, or if the punctured stream does not match + /// the requested full length under that pattern. + /// + /// Rust: `codes::convolutional::ConvolutionalCode::depuncture_llr` + #[pyo3(name = "depuncture_llr")] + #[pyo3(signature = (punctured, pattern, full_len))] + fn depuncture_llr<'py>(&self, py: Python<'py>, punctured: Vec, pattern: Vec, full_len: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.depuncture_llr(&punctured, &pattern, full_len))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "k")] + fn py_get_k(&self) -> PyResult { Ok(self.inner.k) } + + #[setter] + #[pyo3(name = "k")] + fn py_set_k(&mut self, v: u32) { self.inner.k = v; } + + #[getter] + #[pyo3(name = "polys")] + fn py_get_polys(&self) -> PyResult> { Ok(self.inner.polys.clone()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("ConvolutionalCode", "ConvolutionalCode", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A rate-`1/2` recursive systematic convolutional encoder: the message +/// passes through unchanged, and one parity stream is generated with +/// feedback. +/// +/// Feedback is what makes a turbo code work. Without it, a low-weight input +/// gives a low-weight output whichever order the bits arrive in, so +/// interleaving buys nothing; with it, a weight-one input drives the register +/// forever and only very particular inputs produce light parity. The +/// interleaver can then almost always break whatever pattern was light for +/// the first encoder. +/// +/// Rust: `codes::convolutional::RscCode` +#[pyclass(name = "RscCode", module = "numeria.codes.convolutional", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyRscCode { pub inner: rust_physics_engine::codes::convolutional::RscCode } +#[pymethods] +impl PyRscCode { + /// The encoder with the given polynomials. + /// + /// Panics: + /// Panics unless the constraint length is between two and eight and both + /// polynomials fit in `k` bits with the leading feedback tap set. + /// + /// Rust: `codes::convolutional::RscCode::new` + #[new] + #[pyo3(signature = (k, feedback, feedforward))] + fn __new__(k: u32, feedback: u64, feedforward: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::convolutional::RscCode::new(k, feedback, feedforward)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyRscCode { inner: __v }) + } + + /// The `(1, 5/7)` encoder of constraint length three, the constituent + /// code of the original turbo construction. + /// + /// Rust: `codes::convolutional::RscCode::standard` + #[pyo3(name = "standard")] + #[staticmethod] + #[pyo3(signature = ())] + fn standard() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::convolutional::RscCode::standard()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyRscCode { inner: __v }) + } + + /// Bits of memory. + /// + /// Rust: `codes::convolutional::RscCode::memory` + #[pyo3(name = "memory")] + #[pyo3(signature = ())] + fn memory(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.memory()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The number of trellis states. + /// + /// Rust: `codes::convolutional::RscCode::trellis_states` + #[pyo3(name = "trellis_states")] + #[pyo3(signature = ())] + fn trellis_states(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.trellis_states()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// One step: the parity bit and the next state, for an input from a + /// state. + /// + /// Rust: `codes::convolutional::RscCode::step` + #[pyo3(name = "step")] + #[pyo3(signature = (state, input))] + fn step(&self, state: usize, input: bool) -> PyResult<(bool, usize)> { + let __r = crate::runtime::guard(|| self.inner.step(state, input)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) + } + + /// The input that drives the register towards zero from a given state, + /// which is how a recursive encoder is terminated. + /// + /// Rust: `codes::convolutional::RscCode::terminating_input` + #[pyo3(name = "terminating_input")] + #[pyo3(signature = (state))] + fn terminating_input(&self, state: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.terminating_input(state)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Encodes a message, returning the parity stream and the final state. + /// + /// Rust: `codes::convolutional::RscCode::encode` + #[pyo3(name = "encode")] + #[pyo3(signature = (bits))] + fn encode<'py>(&self, py: Python<'py>, bits: Vec) -> PyResult<(Vec, usize)> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.encode(&bits))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) + } + + /// Encodes with trellis termination, returning the systematic stream + /// including the tail, the parity stream, and nothing left in the + /// register. + /// + /// Rust: `codes::convolutional::RscCode::encode_terminated` + #[pyo3(name = "encode_terminated")] + #[pyo3(signature = (bits))] + fn encode_terminated<'py>(&self, py: Python<'py>, bits: Vec) -> PyResult<(Vec, Vec)> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.encode_terminated(&bits))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) + } + + /// One pass of the BCJR algorithm, in the max-log domain. + /// + /// Returns the *extrinsic* log-likelihood of each bit: what the trellis + /// and the parity stream say about it, with the bit's own systematic + /// evidence and whatever the other decoder already contributed both + /// subtracted out. Passing anything else between the two halves of a + /// turbo decoder feeds each its own opinion back as if it were news. + /// + /// The forward and backward recursions are the two halves of the same + /// sum: `alpha` accumulates every path into a state from the start, + /// `beta` every path out of it to the end, and their combination at a + /// transition is the likelihood of every path through it. + /// + /// Panics: + /// Panics unless all three inputs have the same length. + /// + /// Rust: `codes::convolutional::RscCode::bcjr_extrinsic` + #[pyo3(name = "bcjr_extrinsic")] + #[pyo3(signature = (ys, yp, la))] + fn bcjr_extrinsic<'py>(&self, py: Python<'py>, ys: Vec, yp: Vec, la: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.bcjr_extrinsic(&ys, &yp, &la))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Whether the encoder is terminated by the given tail, used to decide + /// whether the backward recursion may assume a known end state. + /// + /// Rust: `codes::convolutional::RscCode::ends_at_zero` + #[pyo3(name = "ends_at_zero")] + #[pyo3(signature = (bits))] + fn ends_at_zero<'py>(&self, py: Python<'py>, bits: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.ends_at_zero(&bits))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "k")] + fn py_get_k(&self) -> PyResult { Ok(self.inner.k) } + + #[setter] + #[pyo3(name = "k")] + fn py_set_k(&mut self, v: u32) { self.inner.k = v; } + + #[getter] + #[pyo3(name = "feedback")] + fn py_get_feedback(&self) -> PyResult { Ok(self.inner.feedback) } + + #[setter] + #[pyo3(name = "feedback")] + fn py_set_feedback(&mut self, v: u64) { self.inner.feedback = v; } + + #[getter] + #[pyo3(name = "feedforward")] + fn py_get_feedforward(&self) -> PyResult { Ok(self.inner.feedforward) } + + #[setter] + #[pyo3(name = "feedforward")] + fn py_set_feedforward(&mut self, v: u64) { self.inner.feedforward = v; } + + fn __repr__(&self) -> String { format!("RscCode(k={:?}, feedback={:?}, feedforward={:?})", self.inner.k, self.inner.feedback, self.inner.feedforward) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A turbo code: two recursive systematic encoders sharing a message, the +/// second seeing it through an interleaver. +/// +/// Rust: `codes::convolutional::TurboCode` +#[pyclass(name = "TurboCode", module = "numeria.codes.convolutional", from_py_object)] +#[derive(Clone)] +pub struct PyTurboCode { pub inner: rust_physics_engine::codes::convolutional::TurboCode } +#[pymethods] +impl PyTurboCode { + /// The code with the given constituent encoder and interleaver. + /// + /// Panics: + /// Panics unless the interleaver is a permutation. + /// + /// Rust: `codes::convolutional::TurboCode::new` + #[new] + #[pyo3(signature = (rsc, interleaver))] + fn __new__(rsc: crate::generated::types::PyRscCode, interleaver: Vec) -> PyResult { + let rsc = rsc.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::codes::convolutional::TurboCode::new(rsc, &interleaver)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTurboCode { inner: __v }) + } + + /// The message length the interleaver fixes. + /// + /// Rust: `codes::convolutional::TurboCode::len` + #[pyo3(name = "len")] + #[pyo3(signature = ())] + fn len(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.len()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Whether the code carries no message at all. + /// + /// Rust: `codes::convolutional::TurboCode::is_empty` + #[pyo3(name = "is_empty")] + #[pyo3(signature = ())] + fn is_empty(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_empty()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Encodes a message into a systematic stream and two parity streams. + /// + /// The first encoder is terminated, so the tail bits it needs join the + /// systematic stream; the second is left running, which is the usual + /// compromise -- terminating both would need an interleaver built to + /// allow it. + /// + /// Panics: + /// Panics unless the message matches the interleaver's length. + /// + /// Rust: `codes::convolutional::TurboCode::encode` + #[pyo3(name = "encode")] + #[pyo3(signature = (msg))] + fn encode<'py>(&self, py: Python<'py>, msg: Vec) -> PyResult<(Vec, Vec, Vec)> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.encode(&msg))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) + } + + /// Iterative decoding: the two halves exchange extrinsic information + /// until they agree or the iterations run out. + /// + /// Each round, the first decoder is told what the second concluded about + /// every bit from the interleaved parity, and the second is told what the + /// first concluded from its own. Neither is ever told a bit's own channel + /// value twice, which is what keeps the exchange from becoming a feedback + /// loop of the decoders' own certainty. + /// + /// Panics: + /// Panics unless the three streams have the lengths `encode` produced. + /// + /// Rust: `codes::convolutional::TurboCode::decode_bcjr` + #[pyo3(name = "decode_bcjr")] + #[pyo3(signature = (ys, yp1, yp2, iters))] + fn decode_bcjr<'py>(&self, py: Python<'py>, ys: Vec, yp1: Vec, yp2: Vec, iters: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.decode_bcjr(&ys, &yp1, &yp2, iters))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "rsc")] + fn py_get_rsc(&self) -> PyResult { Ok(crate::generated::types::PyRscCode { inner: self.inner.rsc.clone() }) } + + #[getter] + #[pyo3(name = "interleaver")] + fn py_get_interleaver(&self) -> PyResult> { Ok(self.inner.interleaver.clone()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("TurboCode", "TurboCode", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A short Weierstrass curve `y^2 = x^3 + a x + b` over the prime field +/// `F_p`. +/// +/// The points form a group under the chord-and-tangent construction: three +/// points on a line sum to the identity, so adding two points means drawing +/// the line through them, finding the third intersection, and reflecting it. +/// That the construction is associative is the one non-obvious fact, and it +/// is what makes the whole subject possible. +/// +/// Rust: `codes::crypto_math::EcCurve` +#[pyclass(name = "EcCurve", module = "numeria.codes.crypto_math", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyEcCurve { pub inner: rust_physics_engine::codes::crypto_math::EcCurve } +#[pymethods] +impl PyEcCurve { + /// The curve with the given coefficients over `F_p`. + /// + /// Panics: + /// Panics if the discriminant `4a^3 + 27b^2` vanishes, which means the + /// curve is singular and its points do not form a group. + /// + /// Rust: `codes::crypto_math::EcCurve::new` + #[new] + #[pyo3(signature = (a, b, p))] + fn __new__(a: crate::runtime::coerce::BigIntArg, b: crate::runtime::coerce::BigIntArg, p: crate::runtime::coerce::BigIntArg) -> PyResult { + let a = a.0; + let b = b.0; + let p = p.0; + let __r = crate::runtime::guard(|| rust_physics_engine::codes::crypto_math::EcCurve::new(a, b, p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyEcCurve { inner: __v }) + } + + /// Whether a point satisfies the curve equation. + /// + /// Rust: `codes::crypto_math::EcCurve::is_on_curve` + #[pyo3(name = "is_on_curve")] + #[pyo3(signature = (pt))] + fn is_on_curve(&self, pt: crate::generated::types::PyEcPoint) -> PyResult { + let pt = pt.inner; + let __r = crate::runtime::guard(|| self.inner.is_on_curve(&pt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The additive inverse: the reflection in the `x` axis. + /// + /// Rust: `codes::crypto_math::EcCurve::negate` + #[pyo3(name = "negate")] + #[pyo3(signature = (pt))] + fn negate(&self, pt: crate::generated::types::PyEcPoint) -> PyResult { + let pt = pt.inner; + let __r = crate::runtime::guard(|| self.inner.negate(&pt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyEcPoint { inner: __v }) + } + + /// The group law. + /// + /// Panics: + /// Panics if a required inverse does not exist, which cannot happen over + /// a prime field with a non-singular curve. + /// + /// Rust: `codes::crypto_math::EcCurve::add` + #[pyo3(name = "add")] + #[pyo3(signature = (p1, p2))] + fn add(&self, p1: crate::generated::types::PyEcPoint, p2: crate::generated::types::PyEcPoint) -> PyResult { + let p1 = p1.inner; + let p2 = p2.inner; + let __r = crate::runtime::guard(|| self.inner.add(&p1, &p2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyEcPoint { inner: __v }) + } + + /// Doubling, which the chord construction degenerates to when the two + /// points coincide and the line becomes the tangent. + /// + /// Panics: + /// Panics if a required inverse does not exist. + /// + /// Rust: `codes::crypto_math::EcCurve::double` + #[pyo3(name = "double")] + #[pyo3(signature = (pt))] + fn double(&self, pt: crate::generated::types::PyEcPoint) -> PyResult { + let pt = pt.inner; + let __r = crate::runtime::guard(|| self.inner.double(&pt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyEcPoint { inner: __v }) + } + + /// Repeated addition, by the double-and-add ladder. + /// + /// The exponentiation of the additive group, and the operation whose + /// difficulty to invert -- recovering `k` from `k P` -- everything + /// elliptic-curve rests on. + /// + /// Rust: `codes::crypto_math::EcCurve::scalar_mul` + #[pyo3(name = "scalar_mul")] + #[pyo3(signature = (k, pt))] + fn scalar_mul(&self, k: crate::runtime::coerce::BigIntArg, pt: crate::generated::types::PyEcPoint) -> PyResult { + let k = k.0; + let pt = pt.inner; + let __r = crate::runtime::guard(|| self.inner.scalar_mul(&k, &pt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyEcPoint { inner: __v }) + } + + /// Every affine point, for a curve small enough to enumerate. + /// + /// Panics: + /// Panics if the field has more than a million elements. + /// + /// Rust: `codes::crypto_math::EcCurve::all_points` + #[pyo3(name = "all_points")] + #[pyo3(signature = ())] + fn all_points(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.all_points()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyEcPoint { inner: __x }).collect::>()) + } + + /// The group order, including the point at infinity, by enumeration. + /// + /// Panics: + /// Panics if the field has more than a million elements. + /// + /// Rust: `codes::crypto_math::EcCurve::order_naive_small` + #[pyo3(name = "order_naive_small")] + #[pyo3(signature = ())] + fn order_naive_small(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.order_naive_small()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The order of a single point: the least positive `k` with `k P` at + /// infinity. + /// + /// Panics: + /// Panics if the field has more than a million elements, or the point is + /// not on the curve. + /// + /// Rust: `codes::crypto_math::EcCurve::point_order_small` + #[pyo3(name = "point_order_small")] + #[pyo3(signature = (pt))] + fn point_order_small(&self, pt: crate::generated::types::PyEcPoint) -> PyResult { + let pt = pt.inner; + let __r = crate::runtime::guard(|| self.inner.point_order_small(&pt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// A uniformly chosen affine point. + /// + /// Panics: + /// Panics if the field has more than a million elements, or the curve has + /// no affine points. + /// + /// Rust: `codes::crypto_math::EcCurve::random_point` + #[pyo3(name = "random_point")] + #[pyo3(signature = (rng))] + fn random_point(&self, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| self.inner.random_point(&mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyEcPoint { inner: __v }) + } + + /// The secp256k1 curve, `y^2 = x^3 + 7`, used by Bitcoin. + /// + /// Panics: + /// Panics only if the built-in constants fail to parse. + /// + /// Rust: `codes::crypto_math::EcCurve::secp256k1` + #[pyo3(name = "secp256k1")] + #[staticmethod] + #[pyo3(signature = ())] + fn secp256k1() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::crypto_math::EcCurve::secp256k1()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyEcCurve { inner: __v }) + } + + /// The generator of secp256k1, and its order. + /// + /// Panics: + /// Panics only if the built-in constants fail to parse. + /// + /// Rust: `codes::crypto_math::EcCurve::secp256k1_generator` + #[pyo3(name = "secp256k1_generator")] + #[staticmethod] + #[pyo3(signature = ())] + fn secp256k1_generator<'py>(py: Python<'py>) -> PyResult<(crate::generated::types::PyEcPoint, pyo3::Bound<'py, pyo3::PyAny>)> { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::crypto_math::EcCurve::secp256k1_generator()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((crate::generated::types::PyEcPoint { inner: __v.0 }, crate::runtime::coerce::bigint_out(py, &__v.1)?)) + } + + /// The NIST P-256 curve. + /// + /// Panics: + /// Panics only if the built-in constants fail to parse. + /// + /// Rust: `codes::crypto_math::EcCurve::p256` + #[pyo3(name = "p256")] + #[staticmethod] + #[pyo3(signature = ())] + fn p256() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::crypto_math::EcCurve::p256()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyEcCurve { inner: __v }) + } + + /// The generator of P-256, and its order. + /// + /// Panics: + /// Panics only if the built-in constants fail to parse. + /// + /// Rust: `codes::crypto_math::EcCurve::p256_generator` + #[pyo3(name = "p256_generator")] + #[staticmethod] + #[pyo3(signature = ())] + fn p256_generator<'py>(py: Python<'py>) -> PyResult<(crate::generated::types::PyEcPoint, pyo3::Bound<'py, pyo3::PyAny>)> { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::crypto_math::EcCurve::p256_generator()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((crate::generated::types::PyEcPoint { inner: __v.0 }, crate::runtime::coerce::bigint_out(py, &__v.1)?)) + } + + #[getter] + #[pyo3(name = "a")] + fn py_get_a<'py>(&self, py: Python<'py>) -> PyResult> { Ok(crate::runtime::coerce::bigint_out(py, &self.inner.a.clone())?) } + + #[getter] + #[pyo3(name = "b")] + fn py_get_b<'py>(&self, py: Python<'py>) -> PyResult> { Ok(crate::runtime::coerce::bigint_out(py, &self.inner.b.clone())?) } + + #[getter] + #[pyo3(name = "p")] + fn py_get_p<'py>(&self, py: Python<'py>) -> PyResult> { Ok(crate::runtime::coerce::bigint_out(py, &self.inner.p.clone())?) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("EcCurve", "EcCurve", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A point on a short Weierstrass curve, or the point at infinity. +/// +/// Rust: `codes::crypto_math::EcPoint` +#[pyclass(name = "EcPoint", module = "numeria.codes.crypto_math", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyEcPoint { pub inner: rust_physics_engine::codes::crypto_math::EcPoint } +#[pymethods] +impl PyEcPoint { + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("EcPoint", "EcPoint", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A binary BCH code: cyclic, with a designed distance, over `GF(2^m)`. +/// +/// The generator is the least common multiple of the minimal polynomials of +/// `alpha^1` through `alpha^(2t)`. Those `2t` consecutive roots force a +/// distance of at least `2t + 1` by the BCH bound, which is what "designed +/// distance" means -- the true distance can be larger, and often is. +/// +/// Rust: `codes::reed_solomon::BchCode` +#[pyclass(name = "BchCode", module = "numeria.codes.reed_solomon", from_py_object)] +#[derive(Clone)] +pub struct PyBchCode { pub inner: rust_physics_engine::codes::reed_solomon::BchCode } +#[pymethods] +impl PyBchCode { + /// The binary BCH code of length `2^m - 1` correcting `t` errors. + /// + /// Panics: + /// Panics unless `m` is between three and ten and the designed distance + /// leaves a positive dimension. + /// + /// Rust: `codes::reed_solomon::BchCode::new` + #[new] + #[pyo3(signature = (m, t))] + fn __new__(m: u32, t: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::reed_solomon::BchCode::new(m, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBchCode { inner: __v }) + } + + /// Encodes `k` message bits into `n`, systematically. + /// + /// Panics: + /// Panics unless the message has exactly `k` bits. + /// + /// Rust: `codes::reed_solomon::BchCode::encode` + #[pyo3(name = "encode")] + #[pyo3(signature = (msg))] + fn encode<'py>(&self, py: Python<'py>, msg: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.encode(&msg))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Decodes a received word by the same syndrome route Reed-Solomon uses, + /// carried out in `GF(2^m)`. + /// + /// Errors: + /// Returns `TooManyErrors` if the errors exceed the designed + /// capability. + /// + /// Panics: + /// Panics unless the word has exactly `n` bits. + /// + /// Rust: `codes::reed_solomon::BchCode::decode` + #[pyo3(name = "decode")] + #[pyo3(signature = (recv))] + fn decode<'py>(&self, py: Python<'py>, recv: Vec) -> PyResult<(Vec, usize)> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.decode(&recv))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::errors::map_display)?; + Ok((__v.0, __v.1)) + } + + #[getter] + #[pyo3(name = "m")] + fn py_get_m(&self) -> PyResult { Ok(self.inner.m) } + + #[setter] + #[pyo3(name = "m")] + fn py_set_m(&mut self, v: u32) { self.inner.m = v; } + + #[getter] + #[pyo3(name = "t")] + fn py_get_t(&self) -> PyResult { Ok(self.inner.t) } + + #[setter] + #[pyo3(name = "t")] + fn py_set_t(&mut self, v: usize) { self.inner.t = v; } + + #[getter] + #[pyo3(name = "n")] + fn py_get_n(&self) -> PyResult { Ok(self.inner.n) } + + #[setter] + #[pyo3(name = "n")] + fn py_set_n(&mut self, v: usize) { self.inner.n = v; } + + #[getter] + #[pyo3(name = "k")] + fn py_get_k(&self) -> PyResult { Ok(self.inner.k) } + + #[setter] + #[pyo3(name = "k")] + fn py_set_k(&mut self, v: usize) { self.inner.k = v; } + + #[getter] + #[pyo3(name = "generator")] + fn py_get_generator(&self) -> PyResult> { Ok(self.inner.generator.clone()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("BchCode", "BchCode", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// The field `GF(2^8)`, with logarithm and antilogarithm tables. +/// +/// Multiplication in a field of characteristic two is not the processor's +/// multiplication, so it is done through logarithms: every non-zero element +/// is a power of a primitive element, and a product of powers adds their +/// exponents. The `exp` table is doubled in length so the sum of two +/// exponents never needs reducing modulo 255 at the point of use. +/// +/// Rust: `codes::reed_solomon::Gf256` +#[pyclass(name = "Gf256", module = "numeria.codes.reed_solomon", from_py_object)] +#[derive(Clone)] +pub struct PyGf256 { pub inner: rust_physics_engine::codes::reed_solomon::Gf256 } +#[pymethods] +impl PyGf256 { + /// The field defined by a primitive polynomial, given with its leading + /// term: `0x11D` is `x^8 + x^4 + x^3 + x^2 + 1`, the polynomial used by + /// CCSDS telemetry, QR codes and most of the rest of the world. + /// + /// Panics: + /// Panics if the polynomial is not primitive, which shows up as the + /// powers of `alpha` repeating before they have covered all 255 non-zero + /// elements. + /// + /// Rust: `codes::reed_solomon::Gf256::new` + #[new] + #[pyo3(signature = (prim_poly))] + fn __new__(prim_poly: u32) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::reed_solomon::Gf256::new(prim_poly)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGf256 { inner: __v }) + } + + /// Addition, which in characteristic two is exclusive or and is its own + /// inverse. + /// + /// Rust: `codes::reed_solomon::Gf256::add` + #[pyo3(name = "add")] + #[staticmethod] + #[pyo3(signature = (a, b))] + fn add(a: u8, b: u8) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::reed_solomon::Gf256::add(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Multiplication, by adding logarithms. + /// + /// Rust: `codes::reed_solomon::Gf256::mul` + #[pyo3(name = "mul")] + #[pyo3(signature = (a, b))] + fn mul(&self, a: u8, b: u8) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.mul(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Division. + /// + /// Panics: + /// Panics on division by zero. + /// + /// Rust: `codes::reed_solomon::Gf256::div` + #[pyo3(name = "div")] + #[pyo3(signature = (a, b))] + fn div(&self, a: u8, b: u8) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.div(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The multiplicative inverse. + /// + /// Panics: + /// Panics on zero, which has none. + /// + /// Rust: `codes::reed_solomon::Gf256::inv` + #[pyo3(name = "inv")] + #[pyo3(signature = (a))] + fn inv(&self, a: u8) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.inv(a)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// A power, including negative exponents. + /// + /// Rust: `codes::reed_solomon::Gf256::pow` + #[pyo3(name = "pow")] + #[pyo3(signature = (a, e))] + fn pow(&self, a: u8, e: i32) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.pow(a, e)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// `alpha^e`, the `e`-th power of the primitive element. + /// + /// Rust: `codes::reed_solomon::Gf256::alpha` + #[pyo3(name = "alpha")] + #[pyo3(signature = (e))] + fn alpha(&self, e: i32) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.alpha(e)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Evaluates a polynomial, highest coefficient first, by Horner's rule. + /// + /// Rust: `codes::reed_solomon::Gf256::poly_eval` + #[pyo3(name = "poly_eval")] + #[pyo3(signature = (poly, x))] + fn poly_eval<'py>(&self, py: Python<'py>, poly: Vec, x: u8) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.poly_eval(&poly, x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The product of two polynomials, highest coefficient first. + /// + /// Rust: `codes::reed_solomon::Gf256::poly_mul` + #[pyo3(name = "poly_mul")] + #[pyo3(signature = (a, b))] + fn poly_mul<'py>(&self, py: Python<'py>, a: Vec, b: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.poly_mul(&a, &b))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The remainder of `a` on division by `b`, both highest coefficient + /// first. + /// + /// Panics: + /// Panics if the divisor is zero or has a zero leading coefficient. + /// + /// Rust: `codes::reed_solomon::Gf256::poly_rem` + #[pyo3(name = "poly_rem")] + #[pyo3(signature = (a, b))] + fn poly_rem<'py>(&self, py: Python<'py>, a: Vec, b: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.poly_rem(&a, &b))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "log")] + fn py_get_log(&self) -> PyResult> { Ok(self.inner.log.clone().to_vec()) } + + #[getter] + #[pyo3(name = "exp")] + fn py_get_exp(&self) -> PyResult> { Ok(self.inner.exp.clone().to_vec()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Gf256", "Gf256", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A general binary extension field `GF(2^m)`, elements held as bit patterns. +/// +/// Rust: `codes::reed_solomon::Gf2m` +#[pyclass(name = "Gf2m", module = "numeria.codes.reed_solomon", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyGf2m { pub inner: rust_physics_engine::codes::reed_solomon::Gf2m } +#[pymethods] +impl PyGf2m { + /// The field of degree `m` defined by `prim`. + /// + /// Panics: + /// Panics unless `m` is between one and sixteen and `prim` is primitive. + /// + /// Rust: `codes::reed_solomon::Gf2m::new` + #[new] + #[pyo3(signature = (m, prim))] + fn __new__(m: u32, prim: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::reed_solomon::Gf2m::new(m, prim)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGf2m { inner: __v }) + } + + /// `GF(2^m)` with a primitive polynomial chosen for the degree. + /// + /// Panics: + /// Panics unless `m` is between one and sixteen. + /// + /// Rust: `codes::reed_solomon::Gf2m::with_degree` + #[pyo3(name = "with_degree")] + #[staticmethod] + #[pyo3(signature = (m))] + fn with_degree(m: u32) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::reed_solomon::Gf2m::with_degree(m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGf2m { inner: __v }) + } + + /// The number of elements. + /// + /// Rust: `codes::reed_solomon::Gf2m::order` + #[pyo3(name = "order")] + #[pyo3(signature = ())] + fn order(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.order()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Addition, which is exclusive or. + /// + /// Rust: `codes::reed_solomon::Gf2m::add` + #[pyo3(name = "add")] + #[staticmethod] + #[pyo3(signature = (a, b))] + fn add(a: u64, b: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::reed_solomon::Gf2m::add(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Carry-less multiplication reduced by the primitive polynomial. + /// + /// Rust: `codes::reed_solomon::Gf2m::mul` + #[pyo3(name = "mul")] + #[pyo3(signature = (a, b))] + fn mul(&self, a: u64, b: u64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.mul(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// A power by repeated squaring. + /// + /// Rust: `codes::reed_solomon::Gf2m::pow` + #[pyo3(name = "pow")] + #[pyo3(signature = (a, e))] + fn pow(&self, a: u64, e: u64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.pow(a, e)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The multiplicative inverse, as `a^(2^m - 2)`. + /// + /// Panics: + /// Panics on zero. + /// + /// Rust: `codes::reed_solomon::Gf2m::inv` + #[pyo3(name = "inv")] + #[pyo3(signature = (a))] + fn inv(&self, a: u64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.inv(a)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The absolute trace: `a + a^2 + a^4 + ... + a^(2^(m-1))`. + /// + /// Always zero or one, because it lands in the prime subfield -- it is + /// fixed by squaring, and the only elements squaring fixes are the ones + /// satisfying `x^2 = x`. + /// + /// Rust: `codes::reed_solomon::Gf2m::trace` + #[pyo3(name = "trace")] + #[pyo3(signature = (a))] + fn trace(&self, a: u64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.trace(a)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Every element of the field, in increasing bit-pattern order. + /// + /// Rust: `codes::reed_solomon::Gf2m::all_elements` + #[pyo3(name = "all_elements")] + #[pyo3(signature = ())] + fn all_elements<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.all_elements())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The minimal polynomial of `alpha^e` over `GF(2)`, coefficients from + /// the constant term up. + /// + /// The conjugates of an element in characteristic two are its repeated + /// squares, and the minimal polynomial is the product of `x - c` over + /// that cyclotomic coset. Its coefficients land back in `GF(2)` because + /// squaring permutes the conjugates and so fixes the product. + /// + /// Rust: `codes::reed_solomon::Gf2m::minimal_polynomial` + #[pyo3(name = "minimal_polynomial")] + #[pyo3(signature = (e))] + fn minimal_polynomial<'py>(&self, py: Python<'py>, e: u64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.minimal_polynomial(e))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "m")] + fn py_get_m(&self) -> PyResult { Ok(self.inner.m) } + + #[setter] + #[pyo3(name = "m")] + fn py_set_m(&mut self, v: u32) { self.inner.m = v; } + + #[getter] + #[pyo3(name = "prim")] + fn py_get_prim(&self) -> PyResult { Ok(self.inner.prim) } + + #[setter] + #[pyo3(name = "prim")] + fn py_set_prim(&mut self, v: u64) { self.inner.prim = v; } + + fn __repr__(&self) -> String { format!("Gf2m(m={:?}, prim={:?})", self.inner.m, self.inner.prim) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A prime field `GF(p)`, for the places a power of two is the wrong shape. +/// +/// Rust: `codes::reed_solomon::GfP` +#[pyclass(name = "GfP", module = "numeria.codes.reed_solomon", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyGfP { pub inner: rust_physics_engine::codes::reed_solomon::GfP } +#[pymethods] +impl PyGfP { + /// The field of integers modulo `p`. + /// + /// Panics: + /// Panics unless `p` is prime. + /// + /// Rust: `codes::reed_solomon::GfP::new` + #[new] + #[pyo3(signature = (p))] + fn __new__(p: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::reed_solomon::GfP::new(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGfP { inner: __v }) + } + + /// Addition modulo `p`. + /// + /// Rust: `codes::reed_solomon::GfP::add` + #[pyo3(name = "add")] + #[pyo3(signature = (a, b))] + fn add(&self, a: u64, b: u64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.add(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Subtraction modulo `p`. + /// + /// Rust: `codes::reed_solomon::GfP::sub` + #[pyo3(name = "sub")] + #[pyo3(signature = (a, b))] + fn sub(&self, a: u64, b: u64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.sub(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Multiplication modulo `p`, widened so it cannot overflow. + /// + /// Rust: `codes::reed_solomon::GfP::mul` + #[pyo3(name = "mul")] + #[pyo3(signature = (a, b))] + fn mul(&self, a: u64, b: u64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.mul(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// A power by repeated squaring. + /// + /// Rust: `codes::reed_solomon::GfP::pow` + #[pyo3(name = "pow")] + #[pyo3(signature = (a, e))] + fn pow(&self, a: u64, e: u64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.pow(a, e)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The multiplicative inverse, by Fermat's little theorem. + /// + /// Panics: + /// Panics on zero. + /// + /// Rust: `codes::reed_solomon::GfP::inv` + #[pyo3(name = "inv")] + #[pyo3(signature = (a))] + fn inv(&self, a: u64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.inv(a)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "p")] + fn py_get_p(&self) -> PyResult { Ok(self.inner.p) } + + #[setter] + #[pyo3(name = "p")] + fn py_set_p(&mut self, v: u64) { self.inner.p = v; } + + fn __repr__(&self) -> String { format!("GfP(p={:?})", self.inner.p) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A Reed-Solomon code over `GF(256)`, systematic, with the parity symbols +/// appended. +/// +/// Rust: `codes::reed_solomon::ReedSolomon` +#[pyclass(name = "ReedSolomon", module = "numeria.codes.reed_solomon", from_py_object)] +#[derive(Clone)] +pub struct PyReedSolomon { pub inner: rust_physics_engine::codes::reed_solomon::ReedSolomon } +#[pymethods] +impl PyReedSolomon { + /// The code with the given length and dimension. + /// + /// The generator is `(x - alpha^1)(x - alpha^2) ... (x - alpha^(n-k))`, + /// so a codeword vanishes at those `n - k` powers. That is the whole + /// design: the parity symbols are chosen to make it so, and decoding + /// starts by checking whether it still does. + /// + /// Panics: + /// Panics unless `0 < k < n <= 255`. + /// + /// Rust: `codes::reed_solomon::ReedSolomon::new` + #[new] + #[pyo3(signature = (n, k))] + fn __new__(n: usize, k: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::codes::reed_solomon::ReedSolomon::new(n, k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyReedSolomon { inner: __v }) + } + + /// The number of symbol errors the code corrects, `(n - k) / 2`. + /// + /// Rust: `codes::reed_solomon::ReedSolomon::correction_capacity` + #[pyo3(name = "correction_capacity")] + #[pyo3(signature = ())] + fn correction_capacity(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.correction_capacity()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The minimum distance, `n - k + 1`. + /// + /// Equal to the Singleton bound, which is what makes Reed-Solomon codes + /// maximum distance separable. + /// + /// Rust: `codes::reed_solomon::ReedSolomon::distance` + #[pyo3(name = "distance")] + #[pyo3(signature = ())] + fn distance(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.distance()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Encodes a message into a systematic codeword: the message unchanged, + /// followed by `n - k` parity symbols. + /// + /// Panics: + /// Panics unless the message has exactly `k` symbols. + /// + /// Rust: `codes::reed_solomon::ReedSolomon::encode` + #[pyo3(name = "encode")] + #[pyo3(signature = (msg))] + fn encode<'py>(&self, py: Python<'py>, msg: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.encode(&msg))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The syndromes of a received word: its value at each generator root. + /// + /// All zero exactly when the word is a codeword. Crucially they depend + /// only on the error pattern, not on what was sent, since the transmitted + /// polynomial contributes zero at every one of these points. + /// + /// Rust: `codes::reed_solomon::ReedSolomon::syndromes` + #[pyo3(name = "syndromes")] + #[pyo3(signature = (recv))] + fn syndromes<'py>(&self, py: Python<'py>, recv: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.syndromes(&recv))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Decodes a received word, returning the message and the number of + /// symbols corrected. + /// + /// Berlekamp-Massey finds the shortest linear recurrence the syndromes + /// satisfy; its characteristic polynomial is the error locator, whose + /// roots are the reciprocals of the error positions. Chien search finds + /// them by evaluating at every field element, and Forney's formula + /// recovers each error's magnitude from the error evaluator polynomial. + /// + /// Errors: + /// Returns `TooManyErrors` when the word is further than + /// `(n - k) / 2` symbols from every codeword, which the decoder detects + /// as a locator whose roots do not account for its own degree. + /// + /// Panics: + /// Panics unless the word has exactly `n` symbols. + /// + /// Rust: `codes::reed_solomon::ReedSolomon::decode` + #[pyo3(name = "decode")] + #[pyo3(signature = (recv))] + fn decode<'py>(&self, py: Python<'py>, recv: Vec) -> PyResult<(Vec, usize)> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.decode(&recv))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::errors::map_display)?; + Ok((__v.0, __v.1)) + } + + /// Decodes to the full corrected codeword rather than just the message. + /// + /// Errors: + /// Returns `TooManyErrors` as `decode` does. + /// + /// Panics: + /// Panics unless the word has exactly `n` symbols. + /// + /// Rust: `codes::reed_solomon::ReedSolomon::correct` + #[pyo3(name = "correct")] + #[pyo3(signature = (recv))] + fn correct<'py>(&self, py: Python<'py>, recv: Vec) -> PyResult<(Vec, usize)> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.correct(&recv))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::errors::map_display)?; + Ok((__v.0, __v.1)) + } + + /// Decodes a word with known erasure positions. + /// + /// An erasure -- a symbol known to be unreliable but whose correct value + /// is unknown -- costs half what an error does, because its position is + /// already known and only its magnitude has to be found. The code can + /// handle any `e` errors and `f` erasures with `2e + f <= n - k`; this + /// routine takes the pure-erasure case, `f <= n - k`. + /// + /// Errors: + /// Returns `TooManyErrors` if there are more erasures than parity + /// symbols, or if the result is not a codeword. + /// + /// Panics: + /// Panics unless the word has `n` symbols and the positions are inside it. + /// + /// Rust: `codes::reed_solomon::ReedSolomon::decode_erasures` + #[pyo3(name = "decode_erasures")] + #[pyo3(signature = (recv, erasure_pos))] + fn decode_erasures<'py>(&self, py: Python<'py>, recv: Vec, erasure_pos: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.decode_erasures(&recv, &erasure_pos))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::errors::map_display)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "n")] + fn py_get_n(&self) -> PyResult { Ok(self.inner.n) } + + #[setter] + #[pyo3(name = "n")] + fn py_set_n(&mut self, v: usize) { self.inner.n = v; } + + #[getter] + #[pyo3(name = "k")] + fn py_get_k(&self) -> PyResult { Ok(self.inner.k) } + + #[setter] + #[pyo3(name = "k")] + fn py_set_k(&mut self, v: usize) { self.inner.k = v; } + + fn __repr__(&self) -> String { format!("ReedSolomon(n={:?}, k={:?})", self.inner.n, self.inner.k) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} diff --git a/bindings/python/src/generated/types/control_systems.rs b/bindings/python/src/generated/types/control_systems.rs new file mode 100644 index 0000000..c32a20d --- /dev/null +++ b/bindings/python/src/generated/types/control_systems.rs @@ -0,0 +1,242 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; + + +/// +/// Rust: `control_systems::PidController` +#[pyclass(name = "PidController", module = "numeria.control_systems")] +pub struct PyPidController { pub inner: rust_physics_engine::control_systems::PidController } +#[pymethods] +impl PyPidController { + /// Create a new PID controller with gains Kp, Ki, Kd and output clamping bounds. + /// + /// Rust: `control_systems::PidController::new` + #[new] + #[pyo3(signature = (kp, ki, kd, output_min, output_max))] + fn __new__(kp: f64, ki: f64, kd: f64, output_min: f64, output_max: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::control_systems::PidController::new(kp, ki, kd, output_min, output_max)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPidController { inner: __v }) + } + + /// Compute PID output with anti-windup: u = Kp·e + Ki·∫e·dt + Kd·de/dt + /// + /// Rust: `control_systems::PidController::update` + #[pyo3(name = "update")] + #[pyo3(signature = (setpoint, measured, dt))] + fn update(&mut self, setpoint: f64, measured: f64, dt: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.update(setpoint, measured, dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Reset the integral accumulator and previous error to zero. + /// + /// Rust: `control_systems::PidController::reset` + #[pyo3(name = "reset")] + #[pyo3(signature = ())] + fn reset(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.reset()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + #[getter] + #[pyo3(name = "kp")] + fn py_get_kp(&self) -> PyResult { Ok(self.inner.kp) } + + #[setter] + #[pyo3(name = "kp")] + fn py_set_kp(&mut self, v: f64) { self.inner.kp = v; } + + #[getter] + #[pyo3(name = "ki")] + fn py_get_ki(&self) -> PyResult { Ok(self.inner.ki) } + + #[setter] + #[pyo3(name = "ki")] + fn py_set_ki(&mut self, v: f64) { self.inner.ki = v; } + + #[getter] + #[pyo3(name = "kd")] + fn py_get_kd(&self) -> PyResult { Ok(self.inner.kd) } + + #[setter] + #[pyo3(name = "kd")] + fn py_set_kd(&mut self, v: f64) { self.inner.kd = v; } + + fn __repr__(&self) -> String { format!("PidController(kp={:?}, ki={:?}, kd={:?})", self.inner.kp, self.inner.ki, self.inner.kd) } +} + +/// Extended Kalman filter: nonlinear transition f and observation h +/// with user-supplied Jacobians, linearized at the current estimate. +/// +/// Rust: `control_systems::kalman::ExtendedKalmanFilter` +#[pyclass(name = "ExtendedKalmanFilter", module = "numeria.control_systems.kalman", unsendable)] +pub struct PyExtendedKalmanFilter { pub inner: rust_physics_engine::control_systems::kalman::ExtendedKalmanFilter } +#[pymethods] +impl PyExtendedKalmanFilter { + /// Time update: x ← f(x), P ← J_f·P·J_fᵀ + Q. + /// + /// Rust: `control_systems::kalman::ExtendedKalmanFilter::predict` + #[pyo3(name = "predict")] + #[pyo3(signature = ())] + fn predict(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.predict()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(()) + } + + /// Measurement update with observation z, linearizing h at x. + /// + /// Rust: `control_systems::kalman::ExtendedKalmanFilter::update` + #[pyo3(name = "update")] + #[pyo3(signature = (z))] + fn update(&mut self, z: Vec) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.update(&z)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(()) + } + + #[getter] + #[pyo3(name = "x")] + fn py_get_x(&self) -> PyResult> { Ok(self.inner.x.clone()) } + + #[getter] + #[pyo3(name = "p")] + fn py_get_p(&self) -> PyResult { Ok(crate::generated::types::PyMatrix { inner: self.inner.p.clone() }) } + + #[getter] + #[pyo3(name = "q")] + fn py_get_q(&self) -> PyResult { Ok(crate::generated::types::PyMatrix { inner: self.inner.q.clone() }) } + + #[getter] + #[pyo3(name = "r")] + fn py_get_r(&self) -> PyResult { Ok(crate::generated::types::PyMatrix { inner: self.inner.r.clone() }) } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// Linear Kalman filter with state x, covariance P, transition F, +/// observation H, process noise Q, and measurement noise R. +/// +/// Rust: `control_systems::kalman::KalmanFilter` +#[pyclass(name = "KalmanFilter", module = "numeria.control_systems.kalman", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyKalmanFilter { pub inner: rust_physics_engine::control_systems::kalman::KalmanFilter } +#[pymethods] +impl PyKalmanFilter { + /// Builds a `KalmanFilter` from its fields. + #[new] + #[pyo3(signature = (x, p, f, h, q, r))] + fn __new__(x: Vec, p: crate::generated::types::PyMatrixArg, f: crate::generated::types::PyMatrixArg, h: crate::generated::types::PyMatrixArg, q: crate::generated::types::PyMatrixArg, r: crate::generated::types::PyMatrixArg) -> Self { + let p = p.0; + let f = f.0; + let h = h.0; + let q = q.0; + let r = r.0; + Self { inner: rust_physics_engine::control_systems::kalman::KalmanFilter { x: x, p: p, f: f, h: h, q: q, r: r } } + } + + /// Time update: x ← F·x, P ← F·P·Fᵀ + Q. + /// + /// Rust: `control_systems::kalman::KalmanFilter::predict` + #[pyo3(name = "predict")] + #[pyo3(signature = ())] + fn predict(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.predict()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(()) + } + + /// Measurement update with observation z. + /// + /// Rust: `control_systems::kalman::KalmanFilter::update` + #[pyo3(name = "update")] + #[pyo3(signature = (z))] + fn update<'py>(&mut self, py: Python<'py>, z: Vec) -> PyResult<()> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.update(&z))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(()) + } + + /// 1-D constant-velocity tracker: state [position, velocity], white + /// noise acceleration model (Q from the discretized acceleration + /// spectral density `process_noise`). + /// + /// Panics: + /// Panics unless dt, process_noise, and measurement_noise are positive. + /// + /// Rust: `control_systems::kalman::KalmanFilter::constant_velocity_1d` + #[pyo3(name = "constant_velocity_1d")] + #[staticmethod] + #[pyo3(signature = (dt, process_noise, measurement_noise))] + fn constant_velocity_1d(dt: f64, process_noise: f64, measurement_noise: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::control_systems::kalman::KalmanFilter::constant_velocity_1d(dt, process_noise, measurement_noise)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyKalmanFilter { inner: __v }) + } + + /// 3-D constant-velocity tracker: state [x, y, z, vx, vy, vz], + /// position-only measurements. + /// + /// Panics: + /// Panics unless dt, q, and r are positive. + /// + /// Rust: `control_systems::kalman::KalmanFilter::constant_velocity_3d` + #[pyo3(name = "constant_velocity_3d")] + #[staticmethod] + #[pyo3(signature = (dt, q, r))] + fn constant_velocity_3d(dt: f64, q: f64, r: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::control_systems::kalman::KalmanFilter::constant_velocity_3d(dt, q, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyKalmanFilter { inner: __v }) + } + + #[getter] + #[pyo3(name = "x")] + fn py_get_x(&self) -> PyResult> { Ok(self.inner.x.clone()) } + + #[getter] + #[pyo3(name = "p")] + fn py_get_p(&self) -> PyResult { Ok(crate::generated::types::PyMatrix { inner: self.inner.p.clone() }) } + + #[getter] + #[pyo3(name = "f")] + fn py_get_f(&self) -> PyResult { Ok(crate::generated::types::PyMatrix { inner: self.inner.f.clone() }) } + + #[getter] + #[pyo3(name = "h")] + fn py_get_h(&self) -> PyResult { Ok(crate::generated::types::PyMatrix { inner: self.inner.h.clone() }) } + + #[getter] + #[pyo3(name = "q")] + fn py_get_q(&self) -> PyResult { Ok(crate::generated::types::PyMatrix { inner: self.inner.q.clone() }) } + + #[getter] + #[pyo3(name = "r")] + fn py_get_r(&self) -> PyResult { Ok(crate::generated::types::PyMatrix { inner: self.inner.r.clone() }) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("KalmanFilter", "KalmanFilter", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} diff --git a/bindings/python/src/generated/types/core.rs b/bindings/python/src/generated/types/core.rs new file mode 100644 index 0000000..497394b --- /dev/null +++ b/bindings/python/src/generated/types/core.rs @@ -0,0 +1,501 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; + + +/// Dual number: `re` carries the value, `eps` the derivative. +/// +/// Rust: `core::dual::Dual` +#[pyclass(name = "Dual", module = "numeria.core.dual", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyDual { pub inner: rust_physics_engine::core::dual::Dual } +#[pymethods] +impl PyDual { + /// Builds a `Dual` from its fields. + #[new] + #[pyo3(signature = (re, eps))] + fn __new__(re: f64, eps: f64) -> Self { + + Self { inner: rust_physics_engine::core::dual::Dual { re: re, eps: eps } } + } + + /// A variable seeded for differentiation: x + ε. + /// + /// Rust: `core::dual::Dual::variable` + #[pyo3(name = "variable")] + #[staticmethod] + #[pyo3(signature = (x))] + fn variable(x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::core::dual::Dual::variable(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyDual { inner: __v }) + } + + /// A constant: c + 0·ε. + /// + /// Rust: `core::dual::Dual::constant` + #[pyo3(name = "constant")] + #[staticmethod] + #[pyo3(signature = (c))] + fn constant(c: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::core::dual::Dual::constant(c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyDual { inner: __v }) + } + + /// sin(x): derivative cos(x). + /// + /// Rust: `core::dual::Dual::sin` + #[pyo3(name = "sin")] + #[pyo3(signature = ())] + fn sin(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.clone().sin()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyDual { inner: __v }) + } + + /// cos(x): derivative −sin(x). + /// + /// Rust: `core::dual::Dual::cos` + #[pyo3(name = "cos")] + #[pyo3(signature = ())] + fn cos(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.clone().cos()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyDual { inner: __v }) + } + + /// tan(x): derivative 1/cos²(x). + /// + /// Rust: `core::dual::Dual::tan` + #[pyo3(name = "tan")] + #[pyo3(signature = ())] + fn tan(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.clone().tan()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyDual { inner: __v }) + } + + /// eˣ: derivative eˣ. + /// + /// Rust: `core::dual::Dual::exp` + #[pyo3(name = "exp")] + #[pyo3(signature = ())] + fn exp(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.clone().exp()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyDual { inner: __v }) + } + + /// ln(x): derivative 1/x. + /// + /// Rust: `core::dual::Dual::ln` + #[pyo3(name = "ln")] + #[pyo3(signature = ())] + fn ln(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.clone().ln()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyDual { inner: __v }) + } + + /// √x: derivative 1/(2√x). + /// + /// Rust: `core::dual::Dual::sqrt` + #[pyo3(name = "sqrt")] + #[pyo3(signature = ())] + fn sqrt(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.clone().sqrt()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyDual { inner: __v }) + } + + /// x^p for constant real p: derivative p·x^(p−1). + /// + /// Rust: `core::dual::Dual::powf` + #[pyo3(name = "powf")] + #[pyo3(signature = (p))] + fn powf(&self, p: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.clone().powf(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyDual { inner: __v }) + } + + /// xⁿ for integer n: derivative n·xⁿ⁻¹ (exact for polynomials). + /// + /// Rust: `core::dual::Dual::powi` + #[pyo3(name = "powi")] + #[pyo3(signature = (n))] + fn powi(&self, n: i32) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.clone().powi(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyDual { inner: __v }) + } + + /// arctan(x): derivative 1/(1+x²). + /// + /// Rust: `core::dual::Dual::atan` + #[pyo3(name = "atan")] + #[pyo3(signature = ())] + fn atan(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.clone().atan()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyDual { inner: __v }) + } + + /// sinh(x): derivative cosh(x). + /// + /// Rust: `core::dual::Dual::sinh` + #[pyo3(name = "sinh")] + #[pyo3(signature = ())] + fn sinh(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.clone().sinh()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyDual { inner: __v }) + } + + /// cosh(x): derivative sinh(x). + /// + /// Rust: `core::dual::Dual::cosh` + #[pyo3(name = "cosh")] + #[pyo3(signature = ())] + fn cosh(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.clone().cosh()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyDual { inner: __v }) + } + + /// tanh(x): derivative 1/cosh²(x). + /// + /// Rust: `core::dual::Dual::tanh` + #[pyo3(name = "tanh")] + #[pyo3(signature = ())] + fn tanh(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.clone().tanh()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyDual { inner: __v }) + } + + /// |x|: derivative sign(x) (undefined at 0; returns 0 there). + /// + /// Rust: `core::dual::Dual::abs` + #[pyo3(name = "abs")] + #[pyo3(signature = ())] + fn abs(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.clone().abs()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyDual { inner: __v }) + } + + fn __add__(&self, rhs: crate::generated::types::PyDualArg) -> PyResult { + let rhs = rhs.0; + let __r = crate::runtime::guard(|| ::add(self.inner.clone(), rhs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyDual { inner: __v }) + } + + fn __sub__(&self, rhs: crate::generated::types::PyDualArg) -> PyResult { + let rhs = rhs.0; + let __r = crate::runtime::guard(|| ::sub(self.inner.clone(), rhs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyDual { inner: __v }) + } + + fn __mul__(&self, rhs: crate::generated::types::PyDualArg) -> PyResult { + let rhs = rhs.0; + let __r = crate::runtime::guard(|| ::mul(self.inner.clone(), rhs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyDual { inner: __v }) + } + + fn __truediv__(&self, rhs: crate::generated::types::PyDualArg) -> PyResult { + let rhs = rhs.0; + let __r = crate::runtime::guard(|| ::div(self.inner.clone(), rhs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyDual { inner: __v }) + } + + fn __neg__(&self) -> PyResult { + let __r = crate::runtime::guard(|| ::neg(self.inner.clone())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyDual { inner: __v }) + } + + #[getter] + #[pyo3(name = "re")] + fn py_get_re(&self) -> PyResult { Ok(self.inner.re) } + + #[setter] + #[pyo3(name = "re")] + fn py_set_re(&mut self, v: f64) { self.inner.re = v; } + + #[getter] + #[pyo3(name = "eps")] + fn py_get_eps(&self) -> PyResult { Ok(self.inner.eps) } + + #[setter] + #[pyo3(name = "eps")] + fn py_set_eps(&mut self, v: f64) { self.inner.eps = v; } + + fn __repr__(&self) -> String { format!("Dual(re={:?}, eps={:?})", self.inner.re, self.inner.eps) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A `Dual` argument, or anything that can stand in for one. +pub struct PyDualArg(pub rust_physics_engine::core::dual::Dual); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyDualArg { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result { + if let Ok(__w) = obj.extract::() { + return Ok(PyDualArg(__w.inner)); + } + let __v = crate::runtime::coerce::floats_exact(obj, 2, "Dual")?; + Ok(PyDualArg(rust_physics_engine::core::dual::Dual { re: __v[0], eps: __v[1] })) + } +} + + +/// Closed interval [lo, hi]. +/// +/// Rust: `core::interval::Interval` +#[pyclass(name = "Interval", module = "numeria.core.interval", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyInterval { pub inner: rust_physics_engine::core::interval::Interval } +#[pymethods] +impl PyInterval { + /// Panics: + /// Panics unless lo ≤ hi and both are finite. + /// + /// Rust: `core::interval::Interval::new` + #[new] + #[pyo3(signature = (lo, hi))] + fn __new__(lo: f64, hi: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::core::interval::Interval::new(lo, hi)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyInterval { inner: __v }) + } + + /// Degenerate interval [x, x]. + /// + /// Panics: + /// Panics unless x is finite. + /// + /// Rust: `core::interval::Interval::point` + #[pyo3(name = "point")] + #[staticmethod] + #[pyo3(signature = (x))] + fn point(x: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::core::interval::Interval::point(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyInterval { inner: __v }) + } + + /// Width hi − lo. + /// + /// Rust: `core::interval::Interval::width` + #[pyo3(name = "width")] + #[pyo3(signature = ())] + fn width(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.clone().width()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Midpoint (lo + hi)/2. + /// + /// Rust: `core::interval::Interval::midpoint` + #[pyo3(name = "midpoint")] + #[pyo3(signature = ())] + fn midpoint(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.clone().midpoint()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// True when x ∈ [lo, hi]. + /// + /// Rust: `core::interval::Interval::contains` + #[pyo3(name = "contains")] + #[pyo3(signature = (x))] + fn contains(&self, x: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.clone().contains(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Intersection, or `None` when the intervals are disjoint. + /// + /// Rust: `core::interval::Interval::intersect` + #[pyo3(name = "intersect")] + #[pyo3(signature = (other))] + fn intersect(&self, other: crate::generated::types::PyIntervalArg) -> PyResult> { + let other = other.0; + let __r = crate::runtime::guard(|| self.inner.clone().intersect(other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyInterval { inner: __x })) + } + + /// Smallest interval containing both operands. + /// + /// Rust: `core::interval::Interval::hull` + #[pyo3(name = "hull")] + #[pyo3(signature = (other))] + fn hull(&self, other: crate::generated::types::PyIntervalArg) -> PyResult { + let other = other.0; + let __r = crate::runtime::guard(|| self.inner.clone().hull(other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyInterval { inner: __v }) + } + + /// Interval square root; the operand must be non-negative. + /// + /// Panics: + /// Panics if lo < 0. + /// + /// Rust: `core::interval::Interval::sqrt` + #[pyo3(name = "sqrt")] + #[pyo3(signature = ())] + fn sqrt(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.clone().sqrt()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyInterval { inner: __v }) + } + + /// Interval exponential. + /// + /// Rust: `core::interval::Interval::exp` + #[pyo3(name = "exp")] + #[pyo3(signature = ())] + fn exp(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.clone().exp()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyInterval { inner: __v }) + } + + /// Interval sine: exact range over the interval, endpoints widened + /// outward. + /// + /// Rust: `core::interval::Interval::sin` + #[pyo3(name = "sin")] + #[pyo3(signature = ())] + fn sin(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.clone().sin()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyInterval { inner: __v }) + } + + /// Interval cosine. + /// + /// Rust: `core::interval::Interval::cos` + #[pyo3(name = "cos")] + #[pyo3(signature = ())] + fn cos(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.clone().cos()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyInterval { inner: __v }) + } + + /// Integer power with exact monotonicity handling (even powers of + /// sign-straddling intervals reach down to 0). + /// + /// Panics: + /// Panics for negative n when the interval contains 0. + /// + /// Rust: `core::interval::Interval::powi` + #[pyo3(name = "powi")] + #[pyo3(signature = (n))] + fn powi(&self, n: i32) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.clone().powi(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyInterval { inner: __v }) + } + + fn __add__(&self, rhs: crate::generated::types::PyIntervalArg) -> PyResult { + let rhs = rhs.0; + let __r = crate::runtime::guard(|| ::add(self.inner.clone(), rhs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyInterval { inner: __v }) + } + + fn __sub__(&self, rhs: crate::generated::types::PyIntervalArg) -> PyResult { + let rhs = rhs.0; + let __r = crate::runtime::guard(|| ::sub(self.inner.clone(), rhs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyInterval { inner: __v }) + } + + fn __mul__(&self, rhs: crate::generated::types::PyIntervalArg) -> PyResult { + let rhs = rhs.0; + let __r = crate::runtime::guard(|| ::mul(self.inner.clone(), rhs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyInterval { inner: __v }) + } + + fn __truediv__(&self, rhs: crate::generated::types::PyIntervalArg) -> PyResult { + let rhs = rhs.0; + let __r = crate::runtime::guard(|| ::div(self.inner.clone(), rhs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyInterval { inner: __v }) + } + + fn __neg__(&self) -> PyResult { + let __r = crate::runtime::guard(|| ::neg(self.inner.clone())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyInterval { inner: __v }) + } + + #[getter] + #[pyo3(name = "lo")] + fn py_get_lo(&self) -> PyResult { Ok(self.inner.lo) } + + #[setter] + #[pyo3(name = "lo")] + fn py_set_lo(&mut self, v: f64) { self.inner.lo = v; } + + #[getter] + #[pyo3(name = "hi")] + fn py_get_hi(&self) -> PyResult { Ok(self.inner.hi) } + + #[setter] + #[pyo3(name = "hi")] + fn py_set_hi(&mut self, v: f64) { self.inner.hi = v; } + + fn __repr__(&self) -> String { format!("Interval(lo={:?}, hi={:?})", self.inner.lo, self.inner.hi) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A `Interval` argument, or anything that can stand in for one. +pub struct PyIntervalArg(pub rust_physics_engine::core::interval::Interval); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyIntervalArg { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result { + if let Ok(__w) = obj.extract::() { + return Ok(PyIntervalArg(__w.inner)); + } + let __v = crate::runtime::coerce::floats_exact(obj, 2, "Interval")?; + Ok(PyIntervalArg(rust_physics_engine::core::interval::Interval { lo: __v[0], hi: __v[1] })) + } +} + diff --git a/bindings/python/src/generated/types/discrete.rs b/bindings/python/src/generated/types/discrete.rs new file mode 100644 index 0000000..03becf1 --- /dev/null +++ b/bindings/python/src/generated/types/discrete.rs @@ -0,0 +1,159 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; + + +/// Union-find over `0..n` with path compression and union by size. +/// +/// Shared infrastructure: graph minimum spanning trees, percolation cluster +/// labelling, and single-linkage clustering all reduce to the same +/// "merge these two, are these two together" question. +/// Disjoint-set forest over the elements `0..n`. +/// +/// Rust: `discrete::disjoint_set::DisjointSet` +#[pyclass(name = "DisjointSet", module = "numeria.discrete.disjoint_set", from_py_object)] +#[derive(Clone)] +pub struct PyDisjointSet { pub inner: rust_physics_engine::discrete::disjoint_set::DisjointSet } +#[pymethods] +impl PyDisjointSet { + /// `n` singleton sets. + /// + /// Rust: `discrete::disjoint_set::DisjointSet::new` + #[new] + #[pyo3(signature = (n))] + fn __new__(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::discrete::disjoint_set::DisjointSet::new(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyDisjointSet { inner: __v }) + } + + /// Number of elements the structure was built over. + /// + /// Rust: `discrete::disjoint_set::DisjointSet::len` + #[pyo3(name = "len")] + #[pyo3(signature = ())] + fn len(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.len()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// True when built over zero elements. + /// + /// Rust: `discrete::disjoint_set::DisjointSet::is_empty` + #[pyo3(name = "is_empty")] + #[pyo3(signature = ())] + fn is_empty(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_empty()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Number of disjoint sets. + /// + /// Starts at `n` and drops by one on every union that actually merges. + /// + /// Rust: `discrete::disjoint_set::DisjointSet::count` + #[pyo3(name = "count")] + #[pyo3(signature = ())] + fn count(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.count()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Representative of `x`'s set, compressing the path as it climbs. + /// + /// Iterative rather than recursive: a degenerate forest built by + /// `union_unbalanced`-style calls could otherwise overflow the stack, and + /// this is called in inner loops. + /// + /// Rust: `discrete::disjoint_set::DisjointSet::find` + #[pyo3(name = "find")] + #[pyo3(signature = (x))] + fn find(&mut self, x: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.find(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Merges the sets containing `a` and `b`. + /// + /// Returns `true` when they were previously separate, so a caller can + /// count merges (Kruskal accepts exactly the edges for which this is + /// true). + /// + /// Rust: `discrete::disjoint_set::DisjointSet::union` + #[pyo3(name = "union")] + #[pyo3(signature = (a, b))] + fn union(&mut self, a: usize, b: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.union(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// True when `a` and `b` lie in the same set. + /// + /// Rust: `discrete::disjoint_set::DisjointSet::connected` + #[pyo3(name = "connected")] + #[pyo3(signature = (a, b))] + fn connected(&mut self, a: usize, b: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.connected(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Size of the set containing `x`. + /// + /// Rust: `discrete::disjoint_set::DisjointSet::set_size` + #[pyo3(name = "set_size")] + #[pyo3(signature = (x))] + fn set_size(&mut self, x: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.set_size(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The sets, each as a sorted list of members, ordered by first member. + /// + /// Rust: `discrete::disjoint_set::DisjointSet::sets` + #[pyo3(name = "sets")] + #[pyo3(signature = ())] + fn sets<'py>(&mut self, py: Python<'py>) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.sets())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// A labelling in `0..count()` that is constant on each set. + /// + /// Labels are assigned in order of each set's smallest member, so the + /// result depends only on the partition and not on the union order. + /// + /// Rust: `discrete::disjoint_set::DisjointSet::labels` + #[pyo3(name = "labels")] + #[pyo3(signature = ())] + fn labels<'py>(&mut self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.labels())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("DisjointSet", "DisjointSet", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} diff --git a/bindings/python/src/generated/types/dsp.rs b/bindings/python/src/generated/types/dsp.rs new file mode 100644 index 0000000..b509b44 --- /dev/null +++ b/bindings/python/src/generated/types/dsp.rs @@ -0,0 +1,579 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; + + +/// Streaming FIR state: one-sample-at-a-time processing with an +/// internal circular delay line. +/// +/// Rust: `dsp::fir::FirState` +#[pyclass(name = "FirState", module = "numeria.dsp.fir")] +pub struct PyFirState { pub inner: rust_physics_engine::dsp::fir::FirState } +#[pymethods] +impl PyFirState { + /// Create a streaming filter from a kernel. + /// + /// Panics: + /// Panics if `h` is empty. + /// + /// Rust: `dsp::fir::FirState::new` + #[new] + #[pyo3(signature = (h))] + fn __new__(h: Vec) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::dsp::fir::FirState::new(h)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyFirState { inner: __v }) + } + + /// Push one sample and get the filtered output. + /// + /// Rust: `dsp::fir::FirState::process` + #[pyo3(name = "process")] + #[pyo3(signature = (x))] + fn process(&mut self, x: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.process(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Clear the delay line. + /// + /// Rust: `dsp::fir::FirState::reset` + #[pyo3(name = "reset")] + #[pyo3(signature = ())] + fn reset(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.reset()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// One second-order section in transposed direct form II, with the RBJ +/// cookbook designs as constructors. Coefficients are normalized +/// (a0 = 1); `a1`, `a2` are the denominator terms. +/// +/// Rust: `dsp::iir::Biquad` +#[pyclass(name = "Biquad", module = "numeria.dsp.iir", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyBiquad { pub inner: rust_physics_engine::dsp::iir::Biquad } +#[pymethods] +impl PyBiquad { + /// Build from raw normalized coefficients. + /// + /// Rust: `dsp::iir::Biquad::from_coeffs` + #[pyo3(name = "from_coeffs")] + #[staticmethod] + #[pyo3(signature = (b0, b1, b2, a1, a2))] + fn from_coeffs(b0: f64, b1: f64, b2: f64, a1: f64, a2: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::dsp::iir::Biquad::from_coeffs(b0, b1, b2, a1, a2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBiquad { inner: __v }) + } + + /// Identity (pass-through) section. + /// + /// Rust: `dsp::iir::Biquad::identity` + #[pyo3(name = "identity")] + #[staticmethod] + #[pyo3(signature = ())] + fn identity() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::dsp::iir::Biquad::identity()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBiquad { inner: __v }) + } + + /// RBJ low-pass with resonance q at cutoff fc. + /// + /// Rust: `dsp::iir::Biquad::lowpass` + #[pyo3(name = "lowpass")] + #[staticmethod] + #[pyo3(signature = (fc, fs, q))] + fn lowpass(fc: f64, fs: f64, q: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::dsp::iir::Biquad::lowpass(fc, fs, q)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBiquad { inner: __v }) + } + + /// RBJ high-pass. + /// + /// Rust: `dsp::iir::Biquad::highpass` + #[pyo3(name = "highpass")] + #[staticmethod] + #[pyo3(signature = (fc, fs, q))] + fn highpass(fc: f64, fs: f64, q: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::dsp::iir::Biquad::highpass(fc, fs, q)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBiquad { inner: __v }) + } + + /// RBJ band-pass (constant 0 dB peak gain). + /// + /// Rust: `dsp::iir::Biquad::bandpass` + #[pyo3(name = "bandpass")] + #[staticmethod] + #[pyo3(signature = (fc, fs, q))] + fn bandpass(fc: f64, fs: f64, q: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::dsp::iir::Biquad::bandpass(fc, fs, q)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBiquad { inner: __v }) + } + + /// RBJ notch. + /// + /// Rust: `dsp::iir::Biquad::notch` + #[pyo3(name = "notch")] + #[staticmethod] + #[pyo3(signature = (fc, fs, q))] + fn notch(fc: f64, fs: f64, q: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::dsp::iir::Biquad::notch(fc, fs, q)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBiquad { inner: __v }) + } + + /// RBJ all-pass (unit magnitude, phase rotation around fc). + /// + /// Rust: `dsp::iir::Biquad::allpass` + #[pyo3(name = "allpass")] + #[staticmethod] + #[pyo3(signature = (fc, fs, q))] + fn allpass(fc: f64, fs: f64, q: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::dsp::iir::Biquad::allpass(fc, fs, q)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBiquad { inner: __v }) + } + + /// RBJ peaking EQ: ±gain_db at fc. + /// + /// Rust: `dsp::iir::Biquad::peaking` + #[pyo3(name = "peaking")] + #[staticmethod] + #[pyo3(signature = (fc, fs, q, gain_db))] + fn peaking(fc: f64, fs: f64, q: f64, gain_db: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::dsp::iir::Biquad::peaking(fc, fs, q, gain_db)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBiquad { inner: __v }) + } + + /// RBJ low shelf with shelf slope S (S = 1 is steepest without ripple). + /// + /// Rust: `dsp::iir::Biquad::lowshelf` + #[pyo3(name = "lowshelf")] + #[staticmethod] + #[pyo3(signature = (fc, fs, slope, gain_db))] + fn lowshelf(fc: f64, fs: f64, slope: f64, gain_db: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::dsp::iir::Biquad::lowshelf(fc, fs, slope, gain_db)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBiquad { inner: __v }) + } + + /// RBJ high shelf. + /// + /// Rust: `dsp::iir::Biquad::highshelf` + #[pyo3(name = "highshelf")] + #[staticmethod] + #[pyo3(signature = (fc, fs, slope, gain_db))] + fn highshelf(fc: f64, fs: f64, slope: f64, gain_db: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::dsp::iir::Biquad::highshelf(fc, fs, slope, gain_db)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBiquad { inner: __v }) + } + + /// One sample through the transposed direct form II. + /// + /// Rust: `dsp::iir::Biquad::process` + #[pyo3(name = "process")] + #[pyo3(signature = (x))] + fn process(&mut self, x: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.process(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Filter a whole block. + /// + /// Rust: `dsp::iir::Biquad::process_block` + #[pyo3(name = "process_block")] + #[pyo3(signature = (x))] + fn process_block<'py>(&mut self, py: Python<'py>, x: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.process_block(&x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Clear the state. + /// + /// Rust: `dsp::iir::Biquad::reset` + #[pyo3(name = "reset")] + #[pyo3(signature = ())] + fn reset(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.reset()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Prime the state so a constant input `v` is already in steady + /// state (used by `filtfilt` for transient-free starts). + /// + /// Rust: `dsp::iir::Biquad::prime` + #[pyo3(name = "prime")] + #[pyo3(signature = (v))] + fn prime(&mut self, v: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.prime(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Complex response at frequency f (Hz). + /// + /// Rust: `dsp::iir::Biquad::freq_response` + #[pyo3(name = "freq_response")] + #[pyo3(signature = (f, fs))] + fn freq_response<'py>(&self, py: Python<'py>, f: f64, fs: f64) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.freq_response(f, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::complex_out(py, __v)) + } + + /// Coefficients as (\[b0, b1, b2\], \[1, a1, a2\]). + /// + /// Rust: `dsp::iir::Biquad::coeffs` + #[pyo3(name = "coeffs")] + #[pyo3(signature = ())] + fn coeffs(&self) -> PyResult<(Vec, Vec)> { + let __r = crate::runtime::guard(|| self.inner.coeffs()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0.to_vec(), __v.1.to_vec())) + } + + /// Both poles strictly inside the unit circle (Jury criterion). + /// + /// Rust: `dsp::iir::Biquad::is_stable` + #[pyo3(name = "is_stable")] + #[pyo3(signature = ())] + fn is_stable(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_stable()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "b0")] + fn py_get_b0(&self) -> PyResult { Ok(self.inner.b0) } + + #[setter] + #[pyo3(name = "b0")] + fn py_set_b0(&mut self, v: f64) { self.inner.b0 = v; } + + #[getter] + #[pyo3(name = "b1")] + fn py_get_b1(&self) -> PyResult { Ok(self.inner.b1) } + + #[setter] + #[pyo3(name = "b1")] + fn py_set_b1(&mut self, v: f64) { self.inner.b1 = v; } + + #[getter] + #[pyo3(name = "b2")] + fn py_get_b2(&self) -> PyResult { Ok(self.inner.b2) } + + #[setter] + #[pyo3(name = "b2")] + fn py_set_b2(&mut self, v: f64) { self.inner.b2 = v; } + + #[getter] + #[pyo3(name = "a1")] + fn py_get_a1(&self) -> PyResult { Ok(self.inner.a1) } + + #[setter] + #[pyo3(name = "a1")] + fn py_set_a1(&mut self, v: f64) { self.inner.a1 = v; } + + #[getter] + #[pyo3(name = "a2")] + fn py_get_a2(&self) -> PyResult { Ok(self.inner.a2) } + + #[setter] + #[pyo3(name = "a2")] + fn py_set_a2(&mut self, v: f64) { self.inner.a2 = v; } + + fn __repr__(&self) -> String { format!("Biquad(b0={:?}, b1={:?}, b2={:?}, a1={:?}, a2={:?})", self.inner.b0, self.inner.b1, self.inner.b2, self.inner.a1, self.inner.a2) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Filter band selectors for the classical designs; frequencies in Hz. +/// +/// Rust: `dsp::iir::IirKind` +#[pyclass(name = "IirKind", module = "numeria.dsp.iir", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyIirKind { pub inner: rust_physics_engine::dsp::iir::IirKind } +#[pymethods] +impl PyIirKind { + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("IirKind", "IirKind", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A cascade of biquads with an overall gain. +/// +/// Rust: `dsp::iir::Sos` +#[pyclass(name = "Sos", module = "numeria.dsp.iir", from_py_object)] +#[derive(Clone)] +pub struct PySos { pub inner: rust_physics_engine::dsp::iir::Sos } +#[pymethods] +impl PySos { + /// Builds a `Sos` from its fields. + #[new] + #[pyo3(signature = (sections, gain))] + fn __new__(sections: Vec, gain: f64) -> Self { + let sections = sections.into_iter().map(|__e| __e.inner).collect::>(); + Self { inner: rust_physics_engine::dsp::iir::Sos { sections: sections, gain: gain } } + } + + /// One sample through the whole cascade. + /// + /// Rust: `dsp::iir::Sos::process` + #[pyo3(name = "process")] + #[pyo3(signature = (x))] + fn process(&mut self, x: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.process(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Filter a whole block. + /// + /// Rust: `dsp::iir::Sos::process_block` + #[pyo3(name = "process_block")] + #[pyo3(signature = (x))] + fn process_block<'py>(&mut self, py: Python<'py>, x: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.process_block(&x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Clear all section states. + /// + /// Rust: `dsp::iir::Sos::reset` + #[pyo3(name = "reset")] + #[pyo3(signature = ())] + fn reset(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.reset()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Complex response at frequency f (Hz). + /// + /// Rust: `dsp::iir::Sos::freq_response` + #[pyo3(name = "freq_response")] + #[pyo3(signature = (f, fs))] + fn freq_response<'py>(&self, py: Python<'py>, f: f64, fs: f64) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.freq_response(f, fs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::complex_out(py, __v)) + } + + /// Expand the cascade into (b, a) polynomial coefficients in z⁻¹. + /// + /// Rust: `dsp::iir::Sos::to_tf` + #[pyo3(name = "to_tf")] + #[pyo3(signature = ())] + fn to_tf(&self) -> PyResult<(Vec, Vec)> { + let __r = crate::runtime::guard(|| self.inner.to_tf()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) + } + + /// Poles of every section. + /// + /// Rust: `dsp::iir::Sos::poles` + #[pyo3(name = "poles")] + #[pyo3(signature = ())] + fn poles<'py>(&self, py: Python<'py>) -> PyResult>> { + let __r = crate::runtime::guard(|| self.inner.poles()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) + } + + /// Zeros of every section. + /// + /// Rust: `dsp::iir::Sos::zeros` + #[pyo3(name = "zeros")] + #[pyo3(signature = ())] + fn zeros<'py>(&self, py: Python<'py>) -> PyResult>> { + let __r = crate::runtime::guard(|| self.inner.zeros()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) + } + + #[getter] + #[pyo3(name = "sections")] + fn py_get_sections(&self) -> PyResult> { Ok(self.inner.sections.clone().into_iter().map(|__x| crate::generated::types::PyBiquad { inner: __x }).collect::>()) } + + #[getter] + #[pyo3(name = "gain")] + fn py_get_gain(&self) -> PyResult { Ok(self.inner.gain) } + + #[setter] + #[pyo3(name = "gain")] + fn py_set_gain(&mut self, v: f64) { self.inner.gain = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Sos", "Sos", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Chamberlin state-variable filter producing simultaneous low-pass, +/// high-pass, band-pass, and notch outputs. +/// +/// Rust: `dsp::iir::Svf` +#[pyclass(name = "Svf", module = "numeria.dsp.iir")] +pub struct PySvf { pub inner: rust_physics_engine::dsp::iir::Svf } +#[pymethods] +impl PySvf { + /// One sample in, (low, high, band, notch) out. + /// + /// Rust: `dsp::iir::Svf::process` + #[pyo3(name = "process")] + #[pyo3(signature = (x))] + fn process(&mut self, x: f64) -> PyResult<(f64, f64, f64, f64)> { + let __r = crate::runtime::guard(|| self.inner.process(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2, __v.3)) + } + + /// Clear the integrator states. + /// + /// Rust: `dsp::iir::Svf::reset` + #[pyo3(name = "reset")] + #[pyo3(signature = ())] + fn reset(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.reset()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// Window families for `window`. Parameterized variants carry their +/// shape parameter: Kaiser β, Tukey taper fraction α ∈ [0, 1], Gaussian +/// σ (relative to the half-width), Dolph-Chebyshev sidelobe attenuation +/// in (positive) dB. +/// +/// Rust: `dsp::windows::WindowKind` +#[pyclass(name = "WindowKind", module = "numeria.dsp.windows", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyWindowKind { pub inner: rust_physics_engine::dsp::windows::WindowKind } +#[pymethods] +impl PyWindowKind { + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("WindowKind", "WindowKind", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Figures of merit for a window (Harris 1978). +/// +/// Rust: `dsp::windows::WindowMetrics` +#[pyclass(name = "WindowMetrics", module = "numeria.dsp.windows", from_py_object)] +#[derive(Clone)] +pub struct PyWindowMetrics { pub inner: rust_physics_engine::dsp::windows::WindowMetrics } +#[pymethods] +impl PyWindowMetrics { + /// Builds a `WindowMetrics` from its fields. + #[new] + #[pyo3(signature = (coherent_gain, enbw, scallop_loss_db, main_lobe_bins, max_sidelobe_db))] + fn __new__(coherent_gain: f64, enbw: f64, scallop_loss_db: f64, main_lobe_bins: f64, max_sidelobe_db: f64) -> Self { + + Self { inner: rust_physics_engine::dsp::windows::WindowMetrics { coherent_gain: coherent_gain, enbw: enbw, scallop_loss_db: scallop_loss_db, main_lobe_bins: main_lobe_bins, max_sidelobe_db: max_sidelobe_db } } + } + + #[getter] + #[pyo3(name = "coherent_gain")] + fn py_get_coherent_gain(&self) -> PyResult { Ok(self.inner.coherent_gain) } + + #[setter] + #[pyo3(name = "coherent_gain")] + fn py_set_coherent_gain(&mut self, v: f64) { self.inner.coherent_gain = v; } + + #[getter] + #[pyo3(name = "enbw")] + fn py_get_enbw(&self) -> PyResult { Ok(self.inner.enbw) } + + #[setter] + #[pyo3(name = "enbw")] + fn py_set_enbw(&mut self, v: f64) { self.inner.enbw = v; } + + #[getter] + #[pyo3(name = "scallop_loss_db")] + fn py_get_scallop_loss_db(&self) -> PyResult { Ok(self.inner.scallop_loss_db) } + + #[setter] + #[pyo3(name = "scallop_loss_db")] + fn py_set_scallop_loss_db(&mut self, v: f64) { self.inner.scallop_loss_db = v; } + + #[getter] + #[pyo3(name = "main_lobe_bins")] + fn py_get_main_lobe_bins(&self) -> PyResult { Ok(self.inner.main_lobe_bins) } + + #[setter] + #[pyo3(name = "main_lobe_bins")] + fn py_set_main_lobe_bins(&mut self, v: f64) { self.inner.main_lobe_bins = v; } + + #[getter] + #[pyo3(name = "max_sidelobe_db")] + fn py_get_max_sidelobe_db(&self) -> PyResult { Ok(self.inner.max_sidelobe_db) } + + #[setter] + #[pyo3(name = "max_sidelobe_db")] + fn py_set_max_sidelobe_db(&mut self, v: f64) { self.inner.max_sidelobe_db = v; } + + fn __repr__(&self) -> String { format!("WindowMetrics(coherent_gain={:?}, enbw={:?}, scallop_loss_db={:?}, main_lobe_bins={:?}, max_sidelobe_db={:?})", self.inner.coherent_gain, self.inner.enbw, self.inner.scallop_loss_db, self.inner.main_lobe_bins, self.inner.max_sidelobe_db) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A `WindowMetrics` argument, or anything that can stand in for one. +pub struct PyWindowMetricsArg(pub rust_physics_engine::dsp::windows::WindowMetrics); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyWindowMetricsArg { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result { + if let Ok(__w) = obj.extract::() { + return Ok(PyWindowMetricsArg(__w.inner)); + } + let __v = crate::runtime::coerce::floats_exact(obj, 5, "WindowMetrics")?; + Ok(PyWindowMetricsArg(rust_physics_engine::dsp::windows::WindowMetrics { coherent_gain: __v[0], enbw: __v[1], scallop_loss_db: __v[2], main_lobe_bins: __v[3], max_sidelobe_db: __v[4] })) + } +} + diff --git a/bindings/python/src/generated/types/exact.rs b/bindings/python/src/generated/types/exact.rs new file mode 100644 index 0000000..52bb846 --- /dev/null +++ b/bindings/python/src/generated/types/exact.rs @@ -0,0 +1,2159 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; + + +/// An arbitrary-precision binary float: the exact value +/// `mantissa * 2^exponent`, carried at `precision` bits. +/// +/// See the module documentation for the canonical form and the +/// rounding rules. Comparison, `PartialEq` and `Ord` are by *numeric +/// value*: two `BigFloat`s that represent the same number compare equal +/// even when their `precision` fields differ. +/// +/// Rust: `exact::bigfloat::BigFloat` +#[pyclass(name = "BigFloat", module = "numeria.exact.bigfloat", from_py_object)] +#[derive(Clone)] +pub struct PyBigFloat { pub inner: rust_physics_engine::exact::bigfloat::BigFloat } +#[pymethods] +impl PyBigFloat { + /// The normalized value `mantissa * 2^exponent`, rounded to + /// `precision` bits (nearest, ties to even). + /// + /// Panics: + /// Panics if `precision < 2`. + /// + /// Rust: `exact::bigfloat::BigFloat::new` + #[new] + #[pyo3(signature = (mantissa, exponent, precision))] + fn __new__(mantissa: crate::runtime::coerce::BigIntArg, exponent: i64, precision: usize) -> PyResult { + let mantissa = mantissa.0; + let __r = crate::runtime::guard(|| rust_physics_engine::exact::bigfloat::BigFloat::new(mantissa, exponent, precision)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBigFloat { inner: __v }) + } + + /// Zero at `precision` bits. + /// + /// Panics: + /// Panics if `precision < 2`. + /// + /// Rust: `exact::bigfloat::BigFloat::zero` + #[pyo3(name = "zero")] + #[staticmethod] + #[pyo3(signature = (precision))] + fn zero(precision: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::bigfloat::BigFloat::zero(precision)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBigFloat { inner: __v }) + } + + /// One at `precision` bits. + /// + /// Panics: + /// Panics if `precision < 2`. + /// + /// Rust: `exact::bigfloat::BigFloat::one` + #[pyo3(name = "one")] + #[staticmethod] + #[pyo3(signature = (precision))] + fn one(precision: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::bigfloat::BigFloat::one(precision)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBigFloat { inner: __v }) + } + + /// The integer `n` rounded to `precision` bits. + /// + /// Panics: + /// Panics if `precision < 2`. + /// + /// Rust: `exact::bigfloat::BigFloat::from_i64` + #[pyo3(name = "from_i64")] + #[staticmethod] + #[pyo3(signature = (n, precision))] + fn from_i64(n: i64, precision: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::bigfloat::BigFloat::from_i64(n, precision)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBigFloat { inner: __v }) + } + + /// The integer `n` rounded to `precision` bits. + /// + /// Panics: + /// Panics if `precision < 2`. + /// + /// Rust: `exact::bigfloat::BigFloat::from_bigint` + #[pyo3(name = "from_bigint")] + #[staticmethod] + #[pyo3(signature = (n, precision))] + fn from_bigint(n: crate::runtime::coerce::BigIntArg, precision: usize) -> PyResult { + let n = n.0; + let __r = crate::runtime::guard(|| rust_physics_engine::exact::bigfloat::BigFloat::from_bigint(&n, precision)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBigFloat { inner: __v }) + } + + /// The exact value of the finite `f64` `x`, rounded to `precision` + /// bits (exact whenever `precision >= 53`, since every `f64` is a + /// dyadic rational). + /// + /// Panics: + /// Panics if `x` is infinite or NaN, or if `precision < 2`. + /// + /// Rust: `exact::bigfloat::BigFloat::from_f64` + #[pyo3(name = "from_f64")] + #[staticmethod] + #[pyo3(signature = (x, precision))] + fn from_f64(x: f64, precision: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::bigfloat::BigFloat::from_f64(x, precision)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBigFloat { inner: __v }) + } + + /// Parse a decimal literal such as `"-12.5"`, `"3.14159e-7"` or + /// `"42"`, correctly rounded to `precision` bits. + /// + /// Errors: + /// Returns `GeomError::InvalidArgument` if the string is not a + /// decimal literal, or if its decimal exponent exceeds ±10000. + /// + /// Panics: + /// Panics if `precision < 2`. + /// + /// Rust: `exact::bigfloat::BigFloat::from_str` + #[pyo3(name = "from_str")] + #[staticmethod] + #[pyo3(signature = (s, precision))] + fn from_str(s: String, precision: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::bigfloat::BigFloat::from_str(&s, precision)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyBigFloat { inner: __v }) + } + + /// Is this value zero? + /// + /// Rust: `exact::bigfloat::BigFloat::is_zero` + #[pyo3(name = "is_zero")] + #[pyo3(signature = ())] + fn is_zero(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_zero()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Is this value strictly negative? + /// + /// Rust: `exact::bigfloat::BigFloat::is_negative` + #[pyo3(name = "is_negative")] + #[pyo3(signature = ())] + fn is_negative(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_negative()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Is this value strictly positive? + /// + /// Rust: `exact::bigfloat::BigFloat::is_positive` + #[pyo3(name = "is_positive")] + #[pyo3(signature = ())] + fn is_positive(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_positive()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The sign: `-1`, `0` or `+1`. + /// + /// Rust: `exact::bigfloat::BigFloat::signum` + #[pyo3(name = "signum")] + #[pyo3(signature = ())] + fn signum(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.signum()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The negation, exactly (negation never rounds). + /// + /// Rust: `exact::bigfloat::BigFloat::neg` + #[pyo3(name = "neg")] + #[pyo3(signature = ())] + fn neg(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.neg()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBigFloat { inner: __v }) + } + + /// The magnitude, exactly. + /// + /// Rust: `exact::bigfloat::BigFloat::abs` + #[pyo3(name = "abs")] + #[pyo3(signature = ())] + fn abs(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.abs()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBigFloat { inner: __v }) + } + + /// Multiply by `2^k`, exactly (only the exponent moves). + /// + /// Rust: `exact::bigfloat::BigFloat::mul_pow2` + #[pyo3(name = "mul_pow2")] + #[pyo3(signature = (k))] + fn mul_pow2(&self, k: i64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.mul_pow2(k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBigFloat { inner: __v }) + } + + /// The same value rounded to `precision` bits. + /// + /// Panics: + /// Panics if `precision < 2`. + /// + /// Rust: `exact::bigfloat::BigFloat::round_to` + #[pyo3(name = "round_to")] + #[pyo3(signature = (precision))] + fn round_to(&self, precision: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.round_to(precision)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBigFloat { inner: __v }) + } + + /// `self + other`, correctly rounded to `precision` bits. + /// + /// Panics: + /// Panics if `precision < 2`. + /// + /// Rust: `exact::bigfloat::BigFloat::add_prec` + #[pyo3(name = "add_prec")] + #[pyo3(signature = (other, precision))] + fn add_prec(&self, other: crate::generated::types::PyBigFloat, precision: usize) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.add_prec(&other, precision)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBigFloat { inner: __v }) + } + + /// `self - other`, correctly rounded to `precision` bits. + /// + /// Panics: + /// Panics if `precision < 2`. + /// + /// Rust: `exact::bigfloat::BigFloat::sub_prec` + #[pyo3(name = "sub_prec")] + #[pyo3(signature = (other, precision))] + fn sub_prec(&self, other: crate::generated::types::PyBigFloat, precision: usize) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.sub_prec(&other, precision)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBigFloat { inner: __v }) + } + + /// `self * other`, correctly rounded to `precision` bits. + /// + /// Panics: + /// Panics if `precision < 2`. + /// + /// Rust: `exact::bigfloat::BigFloat::mul_prec` + #[pyo3(name = "mul_prec")] + #[pyo3(signature = (other, precision))] + fn mul_prec(&self, other: crate::generated::types::PyBigFloat, precision: usize) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.mul_prec(&other, precision)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBigFloat { inner: __v }) + } + + /// `self / other`, correctly rounded to `precision` bits. + /// + /// Panics: + /// Panics if `other` is zero, or if `precision < 2`. + /// + /// Rust: `exact::bigfloat::BigFloat::div_prec` + #[pyo3(name = "div_prec")] + #[pyo3(signature = (other, precision))] + fn div_prec(&self, other: crate::generated::types::PyBigFloat, precision: usize) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.div_prec(&other, precision)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBigFloat { inner: __v }) + } + + /// The square root, correctly rounded to `precision` bits. + /// + /// Panics: + /// Panics if `self` is negative, or if `precision < 2`. + /// + /// Rust: `exact::bigfloat::BigFloat::sqrt_prec` + #[pyo3(name = "sqrt_prec")] + #[pyo3(signature = (precision))] + fn sqrt_prec(&self, precision: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.sqrt_prec(precision)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBigFloat { inner: __v }) + } + + /// `self + other`, correctly rounded to the larger of the two + /// operand precisions. + /// + /// Rust: `exact::bigfloat::BigFloat::add` + #[pyo3(name = "add")] + #[pyo3(signature = (other))] + fn add(&self, other: crate::generated::types::PyBigFloat) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.add(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBigFloat { inner: __v }) + } + + /// `self - other`, correctly rounded to the larger of the two + /// operand precisions. + /// + /// Rust: `exact::bigfloat::BigFloat::sub` + #[pyo3(name = "sub")] + #[pyo3(signature = (other))] + fn sub(&self, other: crate::generated::types::PyBigFloat) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.sub(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBigFloat { inner: __v }) + } + + /// `self * other`, correctly rounded to the larger of the two + /// operand precisions. + /// + /// Rust: `exact::bigfloat::BigFloat::mul` + #[pyo3(name = "mul")] + #[pyo3(signature = (other))] + fn mul(&self, other: crate::generated::types::PyBigFloat) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.mul(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBigFloat { inner: __v }) + } + + /// `self / other`, correctly rounded to the larger of the two + /// operand precisions. + /// + /// Panics: + /// Panics if `other` is zero. + /// + /// Rust: `exact::bigfloat::BigFloat::div` + #[pyo3(name = "div")] + #[pyo3(signature = (other))] + fn div(&self, other: crate::generated::types::PyBigFloat) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.div(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBigFloat { inner: __v }) + } + + /// The square root, correctly rounded to this value's precision. + /// + /// Panics: + /// Panics if `self` is negative. + /// + /// Rust: `exact::bigfloat::BigFloat::sqrt` + #[pyo3(name = "sqrt")] + #[pyo3(signature = ())] + fn sqrt(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.sqrt()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBigFloat { inner: __v }) + } + + /// The nearest `f64` (ties to even), saturating to ±∞ on overflow + /// and supporting the subnormal range. + /// + /// Exact whenever the value is representable, so + /// `BigFloat::from_f64(x, p).to_f64() == x` for every finite `x` and + /// every `p >= 53`. + /// + /// Rust: `exact::bigfloat::BigFloat::to_f64` + #[pyo3(name = "to_f64")] + #[pyo3(signature = ())] + fn to_f64(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.to_f64()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Fixed-point decimal string with exactly `digits` digits after the + /// decimal point, correctly rounded (ties to even). + /// + /// A negative value keeps its sign even when it rounds to zero, so + /// `-0.001` printed with two digits is `"-0.00"`. + /// + /// Panics: + /// Panics if the binary exponent exceeds ±2^32, which would make the + /// digit string astronomically long. + /// + /// Rust: `exact::bigfloat::BigFloat::to_string_decimal` + #[pyo3(name = "to_string_decimal")] + #[pyo3(signature = (digits))] + fn to_string_decimal(&self, digits: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.to_string_decimal(digits)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_string()) + } + + /// π to `precision` bits, by the Gauss-Legendre AGM iteration. + /// + /// The iteration doubles its correct digits each step, so it needs + /// only `O(log precision)` square roots. + /// + /// Panics: + /// Panics if `precision < 2`. + /// + /// Rust: `exact::bigfloat::BigFloat::pi` + #[pyo3(name = "pi")] + #[staticmethod] + #[pyo3(signature = (precision))] + fn pi(precision: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::bigfloat::BigFloat::pi(precision)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBigFloat { inner: __v }) + } + + /// Euler's number `e` to `precision` bits, by summing `1/k!`. + /// + /// Panics: + /// Panics if `precision < 2`. + /// + /// Rust: `exact::bigfloat::BigFloat::e` + #[pyo3(name = "e")] + #[staticmethod] + #[pyo3(signature = (precision))] + fn e(precision: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::bigfloat::BigFloat::e(precision)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBigFloat { inner: __v }) + } + + /// `ln 2` to `precision` bits, via `ln 2 = 2·atanh(1/3)`. + /// + /// Panics: + /// Panics if `precision < 2`. + /// + /// Rust: `exact::bigfloat::BigFloat::ln2` + #[pyo3(name = "ln2")] + #[staticmethod] + #[pyo3(signature = (precision))] + fn ln2(precision: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::bigfloat::BigFloat::ln2(precision)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBigFloat { inner: __v }) + } + + /// `e^self`, evaluated with 64 guard bits. + /// + /// Reduces `self = k·ln 2 + r` with `|r| <= ln2/2`, halves `r` ten + /// more times, sums the exponential series, then undoes both + /// reductions. + /// + /// Panics: + /// Panics if `|self|` is astronomically large (binary scale above + /// 2^20), where the range reduction would need unreasonable + /// precision. + /// + /// Rust: `exact::bigfloat::BigFloat::exp` + #[pyo3(name = "exp")] + #[pyo3(signature = ())] + fn exp(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.exp()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBigFloat { inner: __v }) + } + + /// The natural logarithm, evaluated with 64 guard bits. + /// + /// Splits `self = m·2^k` with `m` near 1, takes square roots until + /// `|m - 1| <= 2^-8`, then sums `2·atanh((m-1)/(m+1))`. + /// + /// Panics: + /// Panics if `self` is zero or negative. + /// + /// Rust: `exact::bigfloat::BigFloat::ln` + #[pyo3(name = "ln")] + #[pyo3(signature = ())] + fn ln(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.ln()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBigFloat { inner: __v }) + } + + /// The sine and cosine together, evaluated with 64 guard bits. + /// + /// Reduces the argument modulo `π/2` (carrying enough extra bits to + /// absorb the cancellation), sums both Maclaurin series on the + /// reduced argument, then applies the quadrant symmetry. + /// + /// Panics: + /// Panics if `|self|` is astronomically large (binary scale above + /// 2^20). + /// + /// Rust: `exact::bigfloat::BigFloat::sin_cos` + #[pyo3(name = "sin_cos")] + #[pyo3(signature = ())] + fn sin_cos(&self) -> PyResult<(crate::generated::types::PyBigFloat, crate::generated::types::PyBigFloat)> { + let __r = crate::runtime::guard(|| self.inner.sin_cos()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((crate::generated::types::PyBigFloat { inner: __v.0 }, crate::generated::types::PyBigFloat { inner: __v.1 })) + } + + /// The sine, evaluated with 64 guard bits. + /// + /// Panics: + /// Panics if `|self|` is astronomically large (binary scale above 2^20). + /// + /// Rust: `exact::bigfloat::BigFloat::sin` + #[pyo3(name = "sin")] + #[pyo3(signature = ())] + fn sin(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.sin()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBigFloat { inner: __v }) + } + + /// The cosine, evaluated with 64 guard bits. + /// + /// Panics: + /// Panics if `|self|` is astronomically large (binary scale above 2^20). + /// + /// Rust: `exact::bigfloat::BigFloat::cos` + #[pyo3(name = "cos")] + #[pyo3(signature = ())] + fn cos(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.cos()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBigFloat { inner: __v }) + } + + /// The arc tangent, evaluated with 64 guard bits. + /// + /// Uses `atan x = π/2 − atan(1/x)` for `|x| > 1`, then the halving + /// identity `atan x = 2·atan(x / (1 + sqrt(1 + x²)))` until the + /// argument is below `2^-8`, then the alternating series. + /// + /// Rust: `exact::bigfloat::BigFloat::atan` + #[pyo3(name = "atan")] + #[pyo3(signature = ())] + fn atan(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.atan()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBigFloat { inner: __v }) + } + + /// `self^n` for an integer exponent, by binary exponentiation with + /// guard bits. + /// + /// Panics: + /// Panics if `self` is zero and `n` is negative. + /// + /// Rust: `exact::bigfloat::BigFloat::powi` + #[pyo3(name = "powi")] + #[pyo3(signature = (n))] + fn powi(&self, n: i64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.powi(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBigFloat { inner: __v }) + } + + /// `self^exponent`. + /// + /// Integer exponents (including negative ones) go through + /// `BigFloat::powi` and so work for any sign of base; other + /// exponents are evaluated as `exp(exponent · ln self)`. + /// + /// Panics: + /// Panics if `exponent` is not an integer and `self` is not strictly + /// positive, or if `self` is zero and the exponent is a negative + /// integer. + /// + /// Rust: `exact::bigfloat::BigFloat::pow` + #[pyo3(name = "pow")] + #[pyo3(signature = (exponent))] + fn pow(&self, exponent: crate::generated::types::PyBigFloat) -> PyResult { + let exponent = exponent.inner; + let __r = crate::runtime::guard(|| self.inner.pow(&exponent)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBigFloat { inner: __v }) + } + + /// The arithmetic-geometric mean of `a` and `b`. + /// + /// Iterates `(a, b) -> ((a+b)/2, sqrt(a·b))`, which converges + /// quadratically to the common limit. + /// + /// Panics: + /// Panics if either argument is negative. + /// + /// Rust: `exact::bigfloat::BigFloat::agm` + #[pyo3(name = "agm")] + #[staticmethod] + #[pyo3(signature = (a, b))] + fn agm(a: crate::generated::types::PyBigFloat, b: crate::generated::types::PyBigFloat) -> PyResult { + let a = a.inner; + let b = b.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::exact::bigfloat::BigFloat::agm(&a, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBigFloat { inner: __v }) + } + + #[getter] + #[pyo3(name = "mantissa")] + fn py_get_mantissa<'py>(&self, py: Python<'py>) -> PyResult> { Ok(crate::runtime::coerce::bigint_out(py, &self.inner.mantissa.clone())?) } + + #[getter] + #[pyo3(name = "exponent")] + fn py_get_exponent(&self) -> PyResult { Ok(self.inner.exponent) } + + #[setter] + #[pyo3(name = "exponent")] + fn py_set_exponent(&mut self, v: i64) { self.inner.exponent = v; } + + #[getter] + #[pyo3(name = "precision")] + fn py_get_precision(&self) -> PyResult { Ok(self.inner.precision) } + + #[setter] + #[pyo3(name = "precision")] + fn py_set_precision(&mut self, v: usize) { self.inner.precision = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("BigFloat", "BigFloat", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A polynomial with `f64` coefficients, ordered from the constant term up. +/// +/// Rust: `exact::polynomial::Poly` +#[pyclass(name = "Poly", module = "numeria.exact.polynomial", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyPoly { pub inner: rust_physics_engine::exact::polynomial::Poly } +#[pymethods] +impl PyPoly { + /// The polynomial with the given coefficients, low degree first. + /// + /// Trailing zeros are dropped, so `new(vec![1.0, 0.0])` and + /// `new(vec![1.0])` are equal. + /// + /// Rust: `exact::polynomial::Poly::new` + #[new] + #[pyo3(signature = (c))] + fn __new__(c: Vec) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::polynomial::Poly::new(c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPoly { inner: __v }) + } + + /// The zero polynomial. + /// + /// Rust: `exact::polynomial::Poly::zero` + #[pyo3(name = "zero")] + #[staticmethod] + #[pyo3(signature = ())] + fn zero() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::polynomial::Poly::zero()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPoly { inner: __v }) + } + + /// The constant polynomial `a`. + /// + /// Rust: `exact::polynomial::Poly::constant` + #[pyo3(name = "constant")] + #[staticmethod] + #[pyo3(signature = (a))] + fn constant(a: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::polynomial::Poly::constant(a)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPoly { inner: __v }) + } + + /// The monomial `coeff * x^k`. + /// + /// Rust: `exact::polynomial::Poly::monomial` + #[pyo3(name = "monomial")] + #[staticmethod] + #[pyo3(signature = (k, coeff))] + fn monomial(k: usize, coeff: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::polynomial::Poly::monomial(k, coeff)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPoly { inner: __v }) + } + + /// Whether this is the zero polynomial. + /// + /// Rust: `exact::polynomial::Poly::is_zero` + #[pyo3(name = "is_zero")] + #[pyo3(signature = ())] + fn is_zero(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_zero()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The degree, with the convention that the zero polynomial has degree + /// `0` (pair with `Poly::is_zero` when that distinction matters). + /// + /// Rust: `exact::polynomial::Poly::degree` + #[pyo3(name = "degree")] + #[pyo3(signature = ())] + fn degree(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.degree()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The leading coefficient, or `0.0` for the zero polynomial. + /// + /// Rust: `exact::polynomial::Poly::leading` + #[pyo3(name = "leading")] + #[pyo3(signature = ())] + fn leading(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.leading()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Value at `x` by Horner's rule. + /// + /// Rust: `exact::polynomial::Poly::eval` + #[pyo3(name = "eval")] + #[pyo3(signature = (x))] + fn eval(&self, x: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.eval(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Value at a complex point by Horner's rule. + /// + /// Rust: `exact::polynomial::Poly::eval_complex` + #[pyo3(name = "eval_complex")] + #[pyo3(signature = (z))] + fn eval_complex<'py>(&self, py: Python<'py>, z: crate::runtime::coerce::ComplexArg) -> PyResult> { + let z = z.0; + let __r = crate::runtime::guard(|| self.inner.eval_complex(z)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::complex_out(py, __v)) + } + + /// Sum of two polynomials. + /// + /// Rust: `exact::polynomial::Poly::add` + #[pyo3(name = "add")] + #[pyo3(signature = (other))] + fn add(&self, other: crate::generated::types::PyPolyArg) -> PyResult { + let other = other.0; + let __r = crate::runtime::guard(|| self.inner.add(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPoly { inner: __v }) + } + + /// Difference `self - other`. + /// + /// Rust: `exact::polynomial::Poly::sub` + #[pyo3(name = "sub")] + #[pyo3(signature = (other))] + fn sub(&self, other: crate::generated::types::PyPolyArg) -> PyResult { + let other = other.0; + let __r = crate::runtime::guard(|| self.inner.sub(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPoly { inner: __v }) + } + + /// Additive inverse. + /// + /// Rust: `exact::polynomial::Poly::neg` + #[pyo3(name = "neg")] + #[pyo3(signature = ())] + fn neg(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.neg()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPoly { inner: __v }) + } + + /// Schoolbook product. See `polynomial_multiply_fft` for the + /// `O(n log n)` alternative. + /// + /// Rust: `exact::polynomial::Poly::mul` + #[pyo3(name = "mul")] + #[pyo3(signature = (other))] + fn mul(&self, other: crate::generated::types::PyPolyArg) -> PyResult { + let other = other.0; + let __r = crate::runtime::guard(|| self.inner.mul(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPoly { inner: __v }) + } + + /// Every coefficient multiplied by `k`. + /// + /// Rust: `exact::polynomial::Poly::mul_scalar` + #[pyo3(name = "mul_scalar")] + #[pyo3(signature = (k))] + fn mul_scalar(&self, k: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.mul_scalar(k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPoly { inner: __v }) + } + + /// Quotient and remainder of `self / divisor`, satisfying + /// `self == q * divisor + r` with `r` of lower degree than `divisor`. + /// + /// Returns `None` when `divisor` is the zero polynomial. + /// + /// Rust: `exact::polynomial::Poly::div_rem` + #[pyo3(name = "div_rem")] + #[pyo3(signature = (divisor))] + fn div_rem(&self, divisor: crate::generated::types::PyPolyArg) -> PyResult> { + let divisor = divisor.0; + let __r = crate::runtime::guard(|| self.inner.div_rem(&divisor)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| (crate::generated::types::PyPoly { inner: __x.0 }, crate::generated::types::PyPoly { inner: __x.1 }))) + } + + /// Derivative `p'(x)`. + /// + /// Rust: `exact::polynomial::Poly::derivative` + #[pyo3(name = "derivative")] + #[pyo3(signature = ())] + fn derivative(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.derivative()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPoly { inner: __v }) + } + + /// Antiderivative with constant term `c0`. + /// + /// Rust: `exact::polynomial::Poly::integral` + #[pyo3(name = "integral")] + #[pyo3(signature = (c0))] + fn integral(&self, c0: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.integral(c0)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPoly { inner: __v }) + } + + /// Composition `self(inner(x))`, by Horner's rule in the inner + /// polynomial. + /// + /// Rust: `exact::polynomial::Poly::compose` + #[pyo3(name = "compose")] + #[pyo3(signature = (inner))] + fn compose(&self, inner: crate::generated::types::PyPolyArg) -> PyResult { + let inner = inner.0; + let __r = crate::runtime::guard(|| self.inner.compose(&inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPoly { inner: __v }) + } + + /// The polynomial `p(k*x)`. + /// + /// Rust: `exact::polynomial::Poly::scale_arg` + #[pyo3(name = "scale_arg")] + #[pyo3(signature = (k))] + fn scale_arg(&self, k: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.scale_arg(k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPoly { inner: __v }) + } + + /// The polynomial `p(x + h)` (a Taylor shift, by repeated synthetic + /// division). + /// + /// Rust: `exact::polynomial::Poly::shift_arg` + #[pyo3(name = "shift_arg")] + #[pyo3(signature = (h))] + fn shift_arg(&self, h: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.shift_arg(h)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPoly { inner: __v }) + } + + /// Monic greatest common divisor, computed by the Euclidean algorithm + /// with a relative tolerance: a remainder whose coefficients are all + /// below `tol` times the scale of the inputs is treated as zero. + /// + /// Returns the zero polynomial when both inputs are zero. `tol` around + /// `1e-9` suits well-scaled polynomials of modest degree; exact input + /// deserves `PolyQ::gcd_exact` instead. + /// + /// Rust: `exact::polynomial::Poly::gcd` + #[pyo3(name = "gcd")] + #[pyo3(signature = (other, tol))] + fn gcd(&self, other: crate::generated::types::PyPolyArg, tol: f64) -> PyResult { + let other = other.0; + let __r = crate::runtime::guard(|| self.inner.gcd(&other, tol)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPoly { inner: __v }) + } + + /// All complex roots, via Durand-Kerner + /// (`numerical::polynomial_roots`). + /// + /// Errors: + /// Returns `SolveError::InvalidArgument` for a constant or zero + /// polynomial and `SolveError::NoConvergence` if the iteration stalls. + /// + /// Rust: `exact::polynomial::Poly::roots` + #[pyo3(name = "roots")] + #[pyo3(signature = ())] + fn roots<'py>(&self, py: Python<'py>) -> PyResult>> { + let __r = crate::runtime::guard(|| self.inner.roots()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) + } + + /// The monic polynomial with exactly the given real roots, + /// `prod (x - r_i)`. + /// + /// Rust: `exact::polynomial::Poly::from_roots` + #[pyo3(name = "from_roots")] + #[staticmethod] + #[pyo3(signature = (roots))] + fn from_roots(roots: Vec) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::polynomial::Poly::from_roots(&roots)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPoly { inner: __v }) + } + + /// The resultant `Res(self, other)`, as the determinant of the + /// Sylvester matrix. + /// + /// It vanishes exactly when the two polynomials share a root (or when + /// either is zero), and equals + /// `lc(p)^deg(q) * prod_{p(a)=0} q(a)` otherwise. + /// + /// Rust: `exact::polynomial::Poly::resultant` + #[pyo3(name = "resultant")] + #[pyo3(signature = (other))] + fn resultant(&self, other: crate::generated::types::PyPolyArg) -> PyResult { + let other = other.0; + let __r = crate::runtime::guard(|| self.inner.resultant(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The discriminant `(-1)^(n(n-1)/2) * Res(p, p') / lc(p)`. + /// + /// For `a x^2 + b x + c` this is `b^2 - 4ac`. It vanishes exactly when + /// the polynomial has a repeated root. Degenerate inputs return `0.0` + /// for the zero polynomial and non-zero constants (no roots to + /// collide) and `1.0` for a linear polynomial, the usual conventions. + /// + /// Rust: `exact::polynomial::Poly::discriminant` + #[pyo3(name = "discriminant")] + #[pyo3(signature = ())] + fn discriminant(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.discriminant()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The canonical Sturm chain `p, p', -rem(p, p'), ...`, each member + /// normalized to unit maximum coefficient so that long chains neither + /// overflow nor underflow. Normalizing by a positive scalar leaves + /// every sign, and therefore every root count, unchanged. + /// + /// The chain is empty for the zero polynomial. + /// + /// Rust: `exact::polynomial::Poly::sturm_sequence` + #[pyo3(name = "sturm_sequence")] + #[pyo3(signature = ())] + fn sturm_sequence(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.sturm_sequence()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyPoly { inner: __x }).collect::>()) + } + + /// Number of distinct real roots in the half-open interval `(a, b]`, + /// by Sturm's theorem. + /// + /// Multiple roots are counted once. Returns `0` when `a >= b` or for + /// the zero polynomial. + /// + /// Panics: + /// Panics if `a` or `b` is not finite. + /// + /// Rust: `exact::polynomial::Poly::count_real_roots` + #[pyo3(name = "count_real_roots")] + #[pyo3(signature = (a, b))] + fn count_real_roots(&self, a: f64, b: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.count_real_roots(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// A Cauchy bound: every real root lies in `[-r, r]`. + /// + /// Rust: `exact::polynomial::Poly::root_bound` + #[pyo3(name = "root_bound")] + #[pyo3(signature = ())] + fn root_bound(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.root_bound()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Disjoint intervals, one per distinct real root, found by bisecting + /// the Cauchy bound and counting with Sturm's theorem. + /// + /// Each returned `(a, b)` is a half-open interval `(a, b]` holding + /// exactly one distinct real root; feed one to `Poly::refine_root`. + /// The intervals come out in increasing order. + /// + /// Rust: `exact::polynomial::Poly::isolate_real_roots` + #[pyo3(name = "isolate_real_roots")] + #[pyo3(signature = ())] + fn isolate_real_roots<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.isolate_real_roots())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) + } + + /// Refine a root inside `interval` to an absolute width below `tol`. + /// + /// Bisection is used when the endpoints bracket a sign change (always + /// true for a root of odd multiplicity); otherwise the midpoint is + /// polished with Newton's method, which is what an even-multiplicity + /// root needs. Returns the best estimate found. + /// + /// Panics: + /// Panics if `tol` is not positive or the interval is not finite. + /// + /// Rust: `exact::polynomial::Poly::refine_root` + #[pyo3(name = "refine_root")] + #[pyo3(signature = (interval, tol))] + fn refine_root(&self, interval: (f64, f64), tol: f64) -> PyResult { + let interval = (interval.0, interval.1); + let __r = crate::runtime::guard(|| self.inner.refine_root(interval, tol)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The unique interpolating polynomial through `(xs[i], ys[i])`, in the + /// Lagrange form `sum_i y_i prod_{j != i} (x - x_j)/(x_i - x_j)`. + /// + /// Panics: + /// Panics if the two slices differ in length or if any two nodes + /// coincide. + /// + /// Rust: `exact::polynomial::Poly::interpolate_lagrange` + #[pyo3(name = "interpolate_lagrange")] + #[staticmethod] + #[pyo3(signature = (xs, ys))] + fn interpolate_lagrange(xs: Vec, ys: Vec) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::polynomial::Poly::interpolate_lagrange(&xs, &ys)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPoly { inner: __v }) + } + + /// The same interpolating polynomial built from Newton's divided + /// differences -- a different algorithm reaching the same answer. + /// + /// Panics: + /// Panics if the two slices differ in length or if any two nodes + /// coincide. + /// + /// Rust: `exact::polynomial::Poly::interpolate_newton` + #[pyo3(name = "interpolate_newton")] + #[staticmethod] + #[pyo3(signature = (xs, ys))] + fn interpolate_newton(xs: Vec, ys: Vec) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::polynomial::Poly::interpolate_newton(&xs, &ys)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPoly { inner: __v }) + } + + /// Evaluate `sum_k c_k T_k(x)` on `[-1, 1]` by Clenshaw recurrence. + /// + /// Rust: `exact::polynomial::Poly::chebyshev_eval` + #[pyo3(name = "chebyshev_eval")] + #[staticmethod] + #[pyo3(signature = (coeffs, x))] + fn chebyshev_eval<'py>(py: Python<'py>, coeffs: Vec, x: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::exact::polynomial::Poly::chebyshev_eval(&coeffs, x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Evaluate a `Poly::chebyshev_fit` result at `x` in the original + /// interval `[a, b]`. + /// + /// Panics: + /// Panics unless `a < b`. + /// + /// Rust: `exact::polynomial::Poly::chebyshev_eval_on` + #[pyo3(name = "chebyshev_eval_on")] + #[staticmethod] + #[pyo3(signature = (coeffs, a, b, x))] + fn chebyshev_eval_on<'py>(py: Python<'py>, coeffs: Vec, a: f64, b: f64, x: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::exact::polynomial::Poly::chebyshev_eval_on(&coeffs, a, b, x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The Chebyshev polynomials `T_0 .. T_n` in the monomial basis. + /// + /// Rust: `exact::polynomial::Poly::chebyshev_basis` + #[pyo3(name = "chebyshev_basis")] + #[staticmethod] + #[pyo3(signature = (n))] + fn chebyshev_basis(n: usize) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::polynomial::Poly::chebyshev_basis(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyPoly { inner: __x }).collect::>()) + } + + /// Coefficients of this polynomial in the Chebyshev basis on `[-1, 1]`, + /// so that `p(x) = sum_k out[k] T_k(x)` exactly (up to rounding). + /// + /// The zero polynomial maps to an empty vector. + /// + /// Rust: `exact::polynomial::Poly::to_chebyshev_basis` + #[pyo3(name = "to_chebyshev_basis")] + #[pyo3(signature = ())] + fn to_chebyshev_basis<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.to_chebyshev_basis())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The monomial-basis polynomial `sum_k coeffs[k] T_k(x)`. + /// + /// Rust: `exact::polynomial::Poly::from_chebyshev_basis` + #[pyo3(name = "from_chebyshev_basis")] + #[staticmethod] + #[pyo3(signature = (coeffs))] + fn from_chebyshev_basis(coeffs: Vec) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::polynomial::Poly::from_chebyshev_basis(&coeffs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPoly { inner: __v }) + } + + /// The Pade approximant `[m/n]` of the power series whose coefficients + /// are `self.c`: the rational function `P/Q` with `deg P <= m`, + /// `deg Q <= n`, `Q(0) = 1`, agreeing with the series through order + /// `x^(m + n)`. + /// + /// Returns `None` when the series has fewer than `m + n + 1` terms or + /// the defining linear system is singular (a degenerate Pade table + /// entry). + /// + /// Rust: `exact::polynomial::Poly::pade` + #[pyo3(name = "pade")] + #[pyo3(signature = (m, n))] + fn pade(&self, m: usize, n: usize) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.pade(m, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| (crate::generated::types::PyPoly { inner: __x.0 }, crate::generated::types::PyPoly { inner: __x.1 }))) + } + + /// Wilkinson's polynomial `prod_{k=1}^{n} (x - k)`, the classic + /// example of catastrophic root sensitivity in the monomial basis. + /// + /// Rust: `exact::polynomial::Poly::wilkinson` + #[pyo3(name = "wilkinson")] + #[staticmethod] + #[pyo3(signature = (n))] + fn wilkinson(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::polynomial::Poly::wilkinson(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPoly { inner: __v }) + } + + /// The `n`-th cyclotomic polynomial, exactly, from the defining + /// identity `x^n - 1 = prod_{d | n} Phi_d(x)`. + /// + /// Its degree is Euler's totient of `n` and its coefficients are + /// integers. + /// + /// Panics: + /// Panics if `n` is zero. + /// + /// Rust: `exact::polynomial::Poly::cyclotomic` + #[pyo3(name = "cyclotomic")] + #[staticmethod] + #[pyo3(signature = (n))] + fn cyclotomic(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::polynomial::Poly::cyclotomic(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyQ { inner: __v }) + } + + /// Whether the polynomial has no repeated factor, i.e. `gcd(p, p')` is + /// a non-zero constant within `tol`. + /// + /// Rust: `exact::polynomial::Poly::is_squarefree` + #[pyo3(name = "is_squarefree")] + #[pyo3(signature = (tol))] + fn is_squarefree(&self, tol: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_squarefree(tol)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The squarefree part `p / gcd(p, p')`: the same roots, each with + /// multiplicity one, normalized to monic. + /// + /// Rust: `exact::polynomial::Poly::squarefree_part` + #[pyo3(name = "squarefree_part")] + #[pyo3(signature = (tol))] + fn squarefree_part(&self, tol: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.squarefree_part(tol)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPoly { inner: __v }) + } + + #[getter] + #[pyo3(name = "c")] + fn py_get_c(&self) -> PyResult> { Ok(self.inner.c.clone()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Poly", "Poly", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A `Poly` argument, or anything that can stand in for one. +pub struct PyPolyArg(pub rust_physics_engine::exact::polynomial::Poly); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyPolyArg { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result { + if let Ok(__w) = obj.extract::() { + return Ok(PyPolyArg(__w.inner)); + } + let __v: Vec = obj.extract().map_err(|_| pyo3::exceptions::PyTypeError::new_err("Poly expects a sequence of floats"))?; + Ok(PyPolyArg(rust_physics_engine::exact::polynomial::Poly { c: __v })) + } +} + + +/// A polynomial with exact rational coefficients, ordered from the constant +/// term up. +/// +/// Rust: `exact::polynomial::PolyQ` +#[pyclass(name = "PolyQ", module = "numeria.exact.polynomial", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyPolyQ { pub inner: rust_physics_engine::exact::polynomial::PolyQ } +#[pymethods] +impl PyPolyQ { + /// The polynomial with the given exact coefficients, low degree first. + /// + /// Rust: `exact::polynomial::PolyQ::new` + #[new] + #[pyo3(signature = (c))] + fn __new__(c: Vec) -> PyResult { + let c = c.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::exact::polynomial::PolyQ::new(c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyQ { inner: __v }) + } + + /// The zero polynomial. + /// + /// Rust: `exact::polynomial::PolyQ::zero` + #[pyo3(name = "zero")] + #[staticmethod] + #[pyo3(signature = ())] + fn zero() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::polynomial::PolyQ::zero()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyQ { inner: __v }) + } + + /// The constant polynomial `a`. + /// + /// Rust: `exact::polynomial::PolyQ::constant` + #[pyo3(name = "constant")] + #[staticmethod] + #[pyo3(signature = (a))] + fn constant(a: crate::runtime::coerce::RationalArg) -> PyResult { + let a = a.0; + let __r = crate::runtime::guard(|| rust_physics_engine::exact::polynomial::PolyQ::constant(a)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyQ { inner: __v }) + } + + /// Integer coefficients, low degree first. + /// + /// Rust: `exact::polynomial::PolyQ::from_i64s` + #[pyo3(name = "from_i64s")] + #[staticmethod] + #[pyo3(signature = (c))] + fn from_i64s(c: Vec) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::polynomial::PolyQ::from_i64s(&c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyQ { inner: __v }) + } + + /// Whether this is the zero polynomial. + /// + /// Rust: `exact::polynomial::PolyQ::is_zero` + #[pyo3(name = "is_zero")] + #[pyo3(signature = ())] + fn is_zero(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_zero()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The degree, with the convention that the zero polynomial has degree + /// `0` (pair with `PolyQ::is_zero` when that distinction matters). + /// + /// Rust: `exact::polynomial::PolyQ::degree` + #[pyo3(name = "degree")] + #[pyo3(signature = ())] + fn degree(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.degree()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The leading coefficient, or zero for the zero polynomial. + /// + /// Rust: `exact::polynomial::PolyQ::leading` + #[pyo3(name = "leading")] + #[pyo3(signature = ())] + fn leading<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.leading()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::rational_out(py, &__v)?) + } + + /// The same polynomial with `f64` coefficients. + /// + /// Rust: `exact::polynomial::PolyQ::to_poly` + #[pyo3(name = "to_poly")] + #[pyo3(signature = ())] + fn to_poly(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.to_poly()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPoly { inner: __v }) + } + + /// Exact value at `x` by Horner's rule. + /// + /// Rust: `exact::polynomial::PolyQ::eval` + #[pyo3(name = "eval")] + #[pyo3(signature = (x))] + fn eval<'py>(&self, py: Python<'py>, x: crate::runtime::coerce::RationalArg) -> PyResult> { + let x = x.0; + let __r = crate::runtime::guard(|| self.inner.eval(&x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::rational_out(py, &__v)?) + } + + /// Exact sum. + /// + /// Rust: `exact::polynomial::PolyQ::add` + #[pyo3(name = "add")] + #[pyo3(signature = (other))] + fn add(&self, other: crate::generated::types::PyPolyQ) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.add(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyQ { inner: __v }) + } + + /// Exact difference `self - other`. + /// + /// Rust: `exact::polynomial::PolyQ::sub` + #[pyo3(name = "sub")] + #[pyo3(signature = (other))] + fn sub(&self, other: crate::generated::types::PyPolyQ) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.sub(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyQ { inner: __v }) + } + + /// Additive inverse. + /// + /// Rust: `exact::polynomial::PolyQ::neg` + #[pyo3(name = "neg")] + #[pyo3(signature = ())] + fn neg(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.neg()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyQ { inner: __v }) + } + + /// Exact schoolbook product. + /// + /// Rust: `exact::polynomial::PolyQ::mul` + #[pyo3(name = "mul")] + #[pyo3(signature = (other))] + fn mul(&self, other: crate::generated::types::PyPolyQ) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.mul(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyQ { inner: __v }) + } + + /// Every coefficient multiplied by `k`. + /// + /// Rust: `exact::polynomial::PolyQ::mul_scalar` + #[pyo3(name = "mul_scalar")] + #[pyo3(signature = (k))] + fn mul_scalar(&self, k: crate::runtime::coerce::RationalArg) -> PyResult { + let k = k.0; + let __r = crate::runtime::guard(|| self.inner.mul_scalar(&k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyQ { inner: __v }) + } + + /// Every coefficient divided by `k`, or `None` when `k` is zero. + /// + /// Rust: `exact::polynomial::PolyQ::div_scalar` + #[pyo3(name = "div_scalar")] + #[pyo3(signature = (k))] + fn div_scalar(&self, k: crate::runtime::coerce::RationalArg) -> PyResult> { + let k = k.0; + let __r = crate::runtime::guard(|| self.inner.div_scalar(&k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyPolyQ { inner: __x })) + } + + /// The monic associate `p / lc(p)`; the zero polynomial maps to itself. + /// + /// Rust: `exact::polynomial::PolyQ::monic` + #[pyo3(name = "monic")] + #[pyo3(signature = ())] + fn monic(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.monic()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyQ { inner: __v }) + } + + /// Exact quotient and remainder, satisfying `self == q * divisor + r` + /// with `r` of lower degree than `divisor`. + /// + /// Returns `None` when `divisor` is the zero polynomial. + /// + /// Rust: `exact::polynomial::PolyQ::div_rem` + #[pyo3(name = "div_rem")] + #[pyo3(signature = (divisor))] + fn div_rem(&self, divisor: crate::generated::types::PyPolyQ) -> PyResult> { + let divisor = divisor.inner; + let __r = crate::runtime::guard(|| self.inner.div_rem(&divisor)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| (crate::generated::types::PyPolyQ { inner: __x.0 }, crate::generated::types::PyPolyQ { inner: __x.1 }))) + } + + /// Exact derivative. + /// + /// Rust: `exact::polynomial::PolyQ::derivative` + #[pyo3(name = "derivative")] + #[pyo3(signature = ())] + fn derivative(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.derivative()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyQ { inner: __v }) + } + + /// Exact antiderivative with constant term `c0`. + /// + /// Rust: `exact::polynomial::PolyQ::integral` + #[pyo3(name = "integral")] + #[pyo3(signature = (c0))] + fn integral(&self, c0: crate::runtime::coerce::RationalArg) -> PyResult { + let c0 = c0.0; + let __r = crate::runtime::guard(|| self.inner.integral(&c0)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyQ { inner: __v }) + } + + /// Exact composition `self(inner(x))`. + /// + /// Rust: `exact::polynomial::PolyQ::compose` + #[pyo3(name = "compose")] + #[pyo3(signature = (inner))] + fn compose(&self, inner: crate::generated::types::PyPolyQ) -> PyResult { + let inner = inner.inner; + let __r = crate::runtime::guard(|| self.inner.compose(&inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyQ { inner: __v }) + } + + /// The polynomial `p(k*x)`. + /// + /// Rust: `exact::polynomial::PolyQ::scale_arg` + #[pyo3(name = "scale_arg")] + #[pyo3(signature = (k))] + fn scale_arg(&self, k: crate::runtime::coerce::RationalArg) -> PyResult { + let k = k.0; + let __r = crate::runtime::guard(|| self.inner.scale_arg(&k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyQ { inner: __v }) + } + + /// The polynomial `p(x + h)`, by repeated synthetic division. + /// + /// Rust: `exact::polynomial::PolyQ::shift_arg` + #[pyo3(name = "shift_arg")] + #[pyo3(signature = (h))] + fn shift_arg(&self, h: crate::runtime::coerce::RationalArg) -> PyResult { + let h = h.0; + let __r = crate::runtime::guard(|| self.inner.shift_arg(&h)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyQ { inner: __v }) + } + + /// The monic polynomial with exactly the given rational roots. + /// + /// Rust: `exact::polynomial::PolyQ::from_roots` + #[pyo3(name = "from_roots")] + #[staticmethod] + #[pyo3(signature = (roots))] + fn from_roots(roots: Vec) -> PyResult { + let roots = roots.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::exact::polynomial::PolyQ::from_roots(&roots)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyQ { inner: __v }) + } + + /// The content: the GCD of the numerators over the LCM of the + /// denominators, signed like the leading coefficient. + /// + /// Dividing by it yields an integer-coefficient polynomial with + /// positive leading coefficient and coefficient GCD one, so + /// `content * primitive_part == self` exactly. The zero polynomial has + /// content zero. + /// + /// Rust: `exact::polynomial::PolyQ::content` + #[pyo3(name = "content")] + #[pyo3(signature = ())] + fn content<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.content()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::rational_out(py, &__v)?) + } + + /// `self / content`: integer coefficients with GCD one and a positive + /// leading coefficient. The zero polynomial maps to itself. + /// + /// Rust: `exact::polynomial::PolyQ::primitive_part` + #[pyo3(name = "primitive_part")] + #[pyo3(signature = ())] + fn primitive_part(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.primitive_part()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyQ { inner: __v }) + } + + /// Pseudo-division: the pair `(q, r)` with + /// `lc(b)^(deg a - deg b + 1) * a == q * b + r` and `deg r < deg b`. + /// + /// Returns `None` when `b` is zero. When `deg a < deg b` the multiplier + /// is one and the answer is `(0, a)`. + /// + /// Rust: `exact::polynomial::PolyQ::pseudo_div` + #[pyo3(name = "pseudo_div")] + #[pyo3(signature = (b))] + fn pseudo_div(&self, b: crate::generated::types::PyPolyQ) -> PyResult> { + let b = b.inner; + let __r = crate::runtime::guard(|| self.inner.pseudo_div(&b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| (crate::generated::types::PyPolyQ { inner: __x.0 }, crate::generated::types::PyPolyQ { inner: __x.1 }))) + } + + /// Monic greatest common divisor, by the subresultant polynomial + /// remainder sequence. + /// + /// The subresultant scaling keeps the intermediate coefficients from + /// exploding the way a naive pseudo-remainder chain does, while every + /// step stays exact. `gcd(0, 0)` is the zero polynomial. + /// + /// Rust: `exact::polynomial::PolyQ::gcd_exact` + #[pyo3(name = "gcd_exact")] + #[pyo3(signature = (other))] + fn gcd_exact(&self, other: crate::generated::types::PyPolyQ) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.gcd_exact(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyQ { inner: __v }) + } + + /// Whether the polynomial has no repeated factor, i.e. `gcd(p, p')` is + /// a constant. The zero polynomial is not squarefree. + /// + /// Rust: `exact::polynomial::PolyQ::is_squarefree` + #[pyo3(name = "is_squarefree")] + #[pyo3(signature = ())] + fn is_squarefree(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_squarefree()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The monic squarefree part `p / gcd(p, p')`. + /// + /// Rust: `exact::polynomial::PolyQ::squarefree_part` + #[pyo3(name = "squarefree_part")] + #[pyo3(signature = ())] + fn squarefree_part(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.squarefree_part()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyQ { inner: __v }) + } + + /// The exact resultant, as the determinant of the Sylvester matrix. + /// + /// Rust: `exact::polynomial::PolyQ::resultant` + #[pyo3(name = "resultant")] + #[pyo3(signature = (other))] + fn resultant<'py>(&self, py: Python<'py>, other: crate::generated::types::PyPolyQ) -> PyResult> { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.resultant(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::rational_out(py, &__v)?) + } + + /// The exact discriminant `(-1)^(n(n-1)/2) Res(p, p') / lc(p)`. + /// + /// Returns zero for the zero polynomial and for non-zero constants, and + /// one for a linear polynomial. + /// + /// Rust: `exact::polynomial::PolyQ::discriminant` + #[pyo3(name = "discriminant")] + #[pyo3(signature = ())] + fn discriminant<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.discriminant()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::rational_out(py, &__v)?) + } + + /// Every rational root with its multiplicity, in increasing order, by + /// the rational root theorem. + /// + /// The polynomial is first made primitive (integer coefficients); + /// candidates are `+-p/q` for `p` dividing the constant term and `q` + /// the leading one, and each hit is divided out repeatedly to get its + /// multiplicity. A zero root is handled separately. + /// + /// Divisor search is by trial division, so a constant or leading term + /// whose magnitude exceeds `10^12` is out of reach and contributes no + /// candidates. + /// + /// Rust: `exact::polynomial::PolyQ::factor_rational_roots` + #[pyo3(name = "factor_rational_roots")] + #[pyo3(signature = ())] + fn factor_rational_roots<'py>(&self, py: Python<'py>) -> PyResult, usize)>> { + let __r = crate::runtime::guard(|| self.inner.factor_rational_roots()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| -> PyResult<(pyo3::Bound<'py, pyo3::PyAny>, usize)> { Ok((crate::runtime::coerce::rational_out(py, &__x.0)?, __x.1)) }).collect::>>()?) + } + + /// Eisenstein's irreducibility criterion at the prime `p`, applied to + /// the primitive part: `p` divides every coefficient but the leading + /// one, and `p^2` does not divide the constant term. + /// + /// `true` proves irreducibility over the rationals; `false` proves + /// nothing. `p` is assumed prime -- primality is not verified -- and a + /// polynomial of degree below one always returns `false`. + /// + /// Rust: `exact::polynomial::PolyQ::eisenstein_check` + #[pyo3(name = "eisenstein_check")] + #[pyo3(signature = (p))] + fn eisenstein_check(&self, p: crate::runtime::coerce::BigIntArg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.eisenstein_check(&p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "c")] + fn py_get_c<'py>(&self, py: Python<'py>) -> PyResult>> { Ok(self.inner.c.clone().into_iter().map(|__x| -> PyResult> { Ok(crate::runtime::coerce::rational_out(py, &__x)?) }).collect::>>()?) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("PolyQ", "PolyQ", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// An expression flattened to a stack program. +/// +/// Rust: `exact::symbolic::CompiledExpr` +#[pyclass(name = "CompiledExpr", module = "numeria.exact.symbolic", from_py_object)] +#[derive(Clone)] +pub struct PyCompiledExpr { pub inner: rust_physics_engine::exact::symbolic::CompiledExpr } +#[pymethods] +impl PyCompiledExpr { + /// The variable order the program expects. + /// + /// Rust: `exact::symbolic::CompiledExpr::vars` + #[pyo3(name = "vars")] + #[pyo3(signature = ())] + fn vars<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.vars())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_vec().into_iter().map(|__x| __x.to_string()).collect::>()) + } + + /// The number of instructions. + /// + /// Rust: `exact::symbolic::CompiledExpr::len` + #[pyo3(name = "len")] + #[pyo3(signature = ())] + fn len(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.len()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// + /// Rust: `exact::symbolic::CompiledExpr::is_empty` + #[pyo3(name = "is_empty")] + #[pyo3(signature = ())] + fn is_empty(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_empty()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Evaluate with variable values in `CompiledExpr::vars` order. + /// + /// Panics: + /// Panics if `vals` is shorter than the variable list. + /// + /// Rust: `exact::symbolic::CompiledExpr::eval` + #[pyo3(name = "eval")] + #[pyo3(signature = (vals))] + fn eval<'py>(&self, py: Python<'py>, vals: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.eval(&vals))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("CompiledExpr", "CompiledExpr", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A symbolic expression. +/// +/// Rust: `exact::symbolic::Expr` +#[pyclass(name = "Expr", module = "numeria.exact.symbolic", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyExpr { pub inner: rust_physics_engine::exact::symbolic::Expr } +#[pymethods] +impl PyExpr { + /// + /// Rust: `exact::symbolic::Expr::c` + #[pyo3(name = "c")] + #[staticmethod] + #[pyo3(signature = (v))] + fn c(v: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::symbolic::Expr::c(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyExpr { inner: __v }) + } + + /// + /// Rust: `exact::symbolic::Expr::var` + #[pyo3(name = "var")] + #[staticmethod] + #[pyo3(signature = (name))] + fn var(name: String) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::symbolic::Expr::var(&name)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyExpr { inner: __v }) + } + + /// + /// Rust: `exact::symbolic::Expr::zero` + #[pyo3(name = "zero")] + #[staticmethod] + #[pyo3(signature = ())] + fn zero() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::symbolic::Expr::zero()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyExpr { inner: __v }) + } + + /// + /// Rust: `exact::symbolic::Expr::one` + #[pyo3(name = "one")] + #[staticmethod] + #[pyo3(signature = ())] + fn one() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::symbolic::Expr::one()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyExpr { inner: __v }) + } + + /// + /// Rust: `exact::symbolic::Expr::add` + #[pyo3(name = "add")] + #[staticmethod] + #[pyo3(signature = (terms))] + fn add(terms: Vec) -> PyResult { + let terms = terms.into_iter().map(|__e| __e.inner).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::exact::symbolic::Expr::add(terms)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyExpr { inner: __v }) + } + + /// + /// Rust: `exact::symbolic::Expr::mul` + #[pyo3(name = "mul")] + #[staticmethod] + #[pyo3(signature = (factors))] + fn mul(factors: Vec) -> PyResult { + let factors = factors.into_iter().map(|__e| __e.inner).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::exact::symbolic::Expr::mul(factors)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyExpr { inner: __v }) + } + + /// + /// Rust: `exact::symbolic::Expr::pow` + #[pyo3(name = "pow")] + #[staticmethod] + #[pyo3(signature = (base, exp))] + fn pow(base: crate::generated::types::PyExpr, exp: crate::generated::types::PyExpr) -> PyResult { + let base = base.inner; + let exp = exp.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::exact::symbolic::Expr::pow(base, exp)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyExpr { inner: __v }) + } + + /// The numeric value of a constant leaf, if this is one. + /// + /// Rust: `exact::symbolic::Expr::as_number` + #[pyo3(name = "as_number")] + #[pyo3(signature = ())] + fn as_number(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.as_number()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) + } + + /// The number of nodes in the tree. + /// + /// Rust: `exact::symbolic::Expr::node_count` + #[pyo3(name = "node_count")] + #[pyo3(signature = ())] + fn node_count(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.node_count()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The height of the tree; a leaf has depth 1. + /// + /// Rust: `exact::symbolic::Expr::depth` + #[pyo3(name = "depth")] + #[pyo3(signature = ())] + fn depth(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.depth()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Every variable name appearing in the expression, sorted and unique. + /// + /// Collected into a `BTreeSet` rather than deduplicated by scanning a + /// growing vector, which would cost `O(v^2)` string comparisons in the + /// number of distinct variables. The set also supplies the sort. + /// + /// Rust: `exact::symbolic::Expr::variables` + #[pyo3(name = "variables")] + #[pyo3(signature = ())] + fn variables<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.variables())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.to_string()).collect::>()) + } + + /// Replace every occurrence of `var` with `replacement`. + /// + /// Rust: `exact::symbolic::Expr::substitute` + #[pyo3(name = "substitute")] + #[pyo3(signature = (var, replacement))] + fn substitute(&self, var: String, replacement: crate::generated::types::PyExpr) -> PyResult { + let replacement = replacement.inner; + let __r = crate::runtime::guard(|| self.inner.substitute(&var, &replacement)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyExpr { inner: __v }) + } + + /// Evaluate at the given variable bindings. + /// + /// Errors: + /// Returns `GeomError::InvalidArgument` if a variable in the + /// expression has no binding. + /// + /// Rust: `exact::symbolic::Expr::eval` + #[pyo3(name = "eval")] + #[pyo3(signature = (vars))] + fn eval<'py>(&self, py: Python<'py>, vars: Vec<(String, f64)>) -> PyResult { + let vars = vars.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let vars__b: Vec<(&str, f64)> = vars.iter().map(|__b| ((*__b).0.as_str(), (*__b).1)).collect(); + let __r = py.detach(move || crate::runtime::guard(move || self.inner.eval(&vars__b))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) + } + + /// Render as LaTeX. + /// + /// Rust: `exact::symbolic::Expr::to_latex` + #[pyo3(name = "to_latex")] + #[pyo3(signature = ())] + fn to_latex(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.to_latex()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_string()) + } + + /// Parse an infix expression such as `"3*x^2 + sin(y)/2"`. + /// + /// Supports `+ - * / ^`, parentheses, unary minus, the elementary + /// functions named by the variants of this enum, and `pi`. `^` is + /// right associative; `log` is accepted as a synonym for `ln`. + /// + /// Errors: + /// Returns `GeomError::InvalidArgument` for an unexpected character, + /// a malformed number, an unknown function, unbalanced parentheses, or + /// trailing input. + /// + /// Rust: `exact::symbolic::Expr::parse` + #[pyo3(name = "parse")] + #[staticmethod] + #[pyo3(signature = (s))] + fn parse(s: String) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::exact::symbolic::Expr::parse(&s)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyExpr { inner: __v }) + } + + /// The exact symbolic derivative with respect to `var`. + /// + /// The result is not simplified; call `Expr::simplify` on it. + /// + /// Rust: `exact::symbolic::Expr::diff` + #[pyo3(name = "diff")] + #[pyo3(signature = (var))] + fn diff(&self, var: String) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.diff(&var)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyExpr { inner: __v }) + } + + /// The gradient with respect to several variables. + /// + /// Rust: `exact::symbolic::Expr::gradient` + #[pyo3(name = "gradient")] + #[pyo3(signature = (vars))] + fn gradient(&self, vars: Vec) -> PyResult> { + let vars__b: Vec<&str> = vars.iter().map(|__b| (*__b).as_str()).collect(); + let __r = crate::runtime::guard(|| self.inner.gradient(&vars__b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyExpr { inner: __x }).collect::>()) + } + + /// Simplify: fold constants, flatten nested sums and products, collect + /// like terms and repeated factors, and apply the standard identities + /// for powers, exponentials and logarithms. + /// + /// This is deliberately a normaliser rather than a prover. It makes + /// cancellation visible -- the derivative of `sin(x)^2 + cos(x)^2` + /// collapses to zero because the two terms collect -- but it does not + /// search for trigonometric rewrites. + /// + /// Rust: `exact::symbolic::Expr::simplify` + #[pyo3(name = "simplify")] + #[pyo3(signature = ())] + fn simplify(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.simplify()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyExpr { inner: __v }) + } + + /// Distribute products over sums and expand small integer powers, then + /// simplify. + /// + /// Rust: `exact::symbolic::Expr::expand` + #[pyo3(name = "expand")] + #[pyo3(signature = ())] + fn expand(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.expand()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyExpr { inner: __v }) + } + + /// Extract the coefficients of a univariate polynomial in `var`, or + /// `None` if the expanded expression is not one. + /// + /// Rust: `exact::symbolic::Expr::as_polynomial` + #[pyo3(name = "as_polynomial")] + #[pyo3(signature = (var))] + fn as_polynomial(&self, var: String) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.as_polynomial(&var)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyPoly { inner: __x })) + } + + /// The Taylor polynomial of degree `order` about `at`, in `var`. + /// + /// Coefficients are the derivatives `f^(k)(at) / k!`, computed by + /// differentiating symbolically and evaluating, so they are exact up to + /// the evaluation itself. + /// + /// Errors: + /// Returns `None` if any derivative fails to evaluate at `at`, which + /// happens when the expression is undefined there or mentions another + /// variable. + /// + /// Rust: `exact::symbolic::Expr::taylor` + #[pyo3(name = "taylor")] + #[pyo3(signature = (var, at, order))] + fn taylor(&self, var: String, at: f64, order: usize) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.taylor(&var, at, order)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyPoly { inner: __x })) + } + + /// Flatten to a stack program for fast repeated evaluation. + /// + /// The compiled program reads variables positionally, in the order + /// given by `Expr::variables`. + /// + /// Rust: `exact::symbolic::Expr::compile` + #[pyo3(name = "compile")] + #[pyo3(signature = ())] + fn compile(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.compile()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCompiledExpr { inner: __v }) + } + + /// Antiderivative with respect to `var` by linearity, the power rule, + /// a small table of elementary forms, and the linear substitution + /// `u = a*var + b`. + /// + /// Returns `None` when none of those rules apply; it does not attempt + /// integration by parts or partial fractions. + /// + /// Rust: `exact::symbolic::Expr::integrate_simple` + #[pyo3(name = "integrate_simple")] + #[pyo3(signature = (var))] + fn integrate_simple(&self, var: String) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.integrate_simple(&var)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyExpr { inner: __x })) + } + + /// A one-sided or two-sided numeric limit, by sampling a geometric + /// sequence of offsets and requiring the values to settle. + /// + /// Returns `None` when the samples do not agree, which covers a genuine + /// divergence and a two-sided limit whose sides disagree. + /// + /// Rust: `exact::symbolic::Expr::limit_numeric` + #[pyo3(name = "limit_numeric")] + #[pyo3(signature = (var, at, side))] + fn limit_numeric(&self, var: String, at: f64, side: crate::generated::types::PySide) -> PyResult> { + let side = side.to_rust(); + let __r = crate::runtime::guard(|| self.inner.limit_numeric(&var, at, side)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) + } + + /// Test whether two expressions agree numerically at random points. + /// + /// This is a probabilistic check, not a proof: it samples the shared + /// variables and compares. Points where either side is undefined are + /// skipped rather than counted as disagreement. + /// + /// Rust: `exact::symbolic::Expr::equivalent_numeric` + #[pyo3(name = "equivalent_numeric")] + #[pyo3(signature = (other, trials, rng))] + fn equivalent_numeric(&self, other: crate::generated::types::PyExpr, trials: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let other = other.inner; + let mut rng = rng; + let __r = crate::runtime::guard(|| self.inner.equivalent_numeric(&other, trials, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Expr", "Expr", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Which side a one-sided limit approaches from. +/// +/// Rust: `exact::symbolic::Side` +#[pyclass(name = "Side", module = "numeria.exact.symbolic", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PySide { + Left, + Right, + Both, +} +impl PySide { + pub fn to_rust(&self) -> rust_physics_engine::exact::symbolic::Side { match self { + Self::Left => rust_physics_engine::exact::symbolic::Side::Left, + Self::Right => rust_physics_engine::exact::symbolic::Side::Right, + Self::Both => rust_physics_engine::exact::symbolic::Side::Both, + } } + pub fn from_rust(v: &rust_physics_engine::exact::symbolic::Side) -> Self { match v { + rust_physics_engine::exact::symbolic::Side::Left => Self::Left, + rust_physics_engine::exact::symbolic::Side::Right => Self::Right, + rust_physics_engine::exact::symbolic::Side::Both => Self::Both, + } } +} +#[pymethods] +impl PySide { + fn __repr__(&self) -> &'static str { + match self { + Self::Left => "Side.Left", + Self::Right => "Side.Right", + Self::Both => "Side.Both", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} diff --git a/bindings/python/src/generated/types/fem.rs b/bindings/python/src/generated/types/fem.rs new file mode 100644 index 0000000..0fb001a --- /dev/null +++ b/bindings/python/src/generated/types/fem.rs @@ -0,0 +1,485 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; + + +/// What to do at the ends of a one-dimensional grid. +/// +/// Rust: `fem::fdtd::Boundary1d` +#[pyclass(name = "Boundary1d", module = "numeria.fem.fdtd", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyBoundary1d { + Conductor, + Mur, +} +impl PyBoundary1d { + pub fn to_rust(&self) -> rust_physics_engine::fem::fdtd::Boundary1d { match self { + Self::Conductor => rust_physics_engine::fem::fdtd::Boundary1d::Conductor, + Self::Mur => rust_physics_engine::fem::fdtd::Boundary1d::Mur, + } } + pub fn from_rust(v: &rust_physics_engine::fem::fdtd::Boundary1d) -> Self { match v { + rust_physics_engine::fem::fdtd::Boundary1d::Conductor => Self::Conductor, + rust_physics_engine::fem::fdtd::Boundary1d::Mur => Self::Mur, + } } +} +#[pymethods] +impl PyBoundary1d { + fn __repr__(&self) -> &'static str { + match self { + Self::Conductor => "Boundary1d.Conductor", + Self::Mur => "Boundary1d.Mur", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// The result of a one-dimensional run: the electric field at every +/// step, and the magnetic field alongside it. +/// +/// Rust: `fem::fdtd::Fdtd1d` +#[pyclass(name = "Fdtd1d", module = "numeria.fem.fdtd", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyFdtd1d { pub inner: rust_physics_engine::fem::fdtd::Fdtd1d } +#[pymethods] +impl PyFdtd1d { + /// Builds a `Fdtd1d` from its fields. + #[new] + #[pyo3(signature = (e, h))] + fn __new__(e: Vec>, h: Vec>) -> Self { + + Self { inner: rust_physics_engine::fem::fdtd::Fdtd1d { e: e, h: h } } + } + + /// The exactly conserved energy of the leapfrog at snapshot `n`. + /// + /// Not the obvious `sum eps E^2 + sum H^2`, which oscillates by a + /// term of order `dt` forever without drifting. What leapfrog + /// conserves is the form with the magnetic term taken as the product + /// of the two half-steps straddling the electric one, + /// + /// + /// which is the discrete analogue of evaluating both fields at the + /// same instant. It is conserved to rounding in a closed lossless + /// domain, and it is the quantity whose boundedness is what + /// stability means. + /// + /// Snapshot `k` of `Fdtd1d::h` holds `H^{k-1/2}`, so the two + /// half-steps straddling `E^n` are `h[n]` and `h[n+1]`. Returns + /// `None` for the final snapshot, which has only the earlier of the + /// two available. + /// + /// Rust: `fem::fdtd::Fdtd1d::energy` + #[pyo3(name = "energy")] + #[pyo3(signature = (eps_r, n))] + fn energy<'py>(&self, py: Python<'py>, eps_r: Vec, n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.energy(&eps_r, n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) + } + + #[getter] + #[pyo3(name = "e")] + fn py_get_e(&self) -> PyResult>> { Ok(self.inner.e.clone()) } + + #[getter] + #[pyo3(name = "h")] + fn py_get_h(&self) -> PyResult>> { Ok(self.inner.h.clone()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Fdtd1d", "Fdtd1d", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// The state of a two-dimensional run. +/// +/// The final snapshot alone is close to useless for a driven problem -- +/// it is whatever phase the oscillation happened to land on -- so the +/// envelope is carried alongside it. That is a deliberate departure from +/// returning a bare field: what a steady-state calculation is *for* is +/// the amplitude, and reconstructing it from one snapshot is not +/// possible. +/// +/// Rust: `fem::fdtd::Fdtd2d` +#[pyclass(name = "Fdtd2d", module = "numeria.fem.fdtd", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyFdtd2d { pub inner: rust_physics_engine::fem::fdtd::Fdtd2d } +#[pymethods] +impl PyFdtd2d { + /// Builds a `Fdtd2d` from its fields. + #[new] + #[pyo3(signature = (nx, ny, ez, envelope))] + fn __new__(nx: usize, ny: usize, ez: Vec, envelope: Vec) -> Self { + + Self { inner: rust_physics_engine::fem::fdtd::Fdtd2d { nx: nx, ny: ny, ez: ez, envelope: envelope } } + } + + #[getter] + #[pyo3(name = "nx")] + fn py_get_nx(&self) -> PyResult { Ok(self.inner.nx) } + + #[setter] + #[pyo3(name = "nx")] + fn py_set_nx(&mut self, v: usize) { self.inner.nx = v; } + + #[getter] + #[pyo3(name = "ny")] + fn py_get_ny(&self) -> PyResult { Ok(self.inner.ny) } + + #[setter] + #[pyo3(name = "ny")] + fn py_set_ny(&mut self, v: usize) { self.inner.ny = v; } + + #[getter] + #[pyo3(name = "ez")] + fn py_get_ez(&self) -> PyResult> { Ok(self.inner.ez.clone()) } + + #[getter] + #[pyo3(name = "envelope")] + fn py_get_envelope(&self) -> PyResult> { Ok(self.inner.envelope.clone()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Fdtd2d", "Fdtd2d", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A boundary condition at one end of the interval. +/// +/// Flux conditions use the outward normal, so a given value means the +/// same physical thing at either end. +/// +/// Rust: `fem::fem1d::Bc` +#[pyclass(name = "Bc", module = "numeria.fem.fem1d", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyFem1dBc { pub inner: rust_physics_engine::fem::fem1d::Bc } +#[pymethods] +impl PyFem1dBc { + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Bc", "Bc", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A finite element solution, with the mesh it lives on. +/// +/// The solver functions return bare nodal values to match the shape of +/// the rest of the crate; wrapping them here is what makes it possible to +/// ask for the value *between* nodes, which is what an error norm needs. +/// +/// Rust: `fem::fem1d::Fem1dSolution` +#[pyclass(name = "Fem1dSolution", module = "numeria.fem.fem1d", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyFem1dSolution { pub inner: rust_physics_engine::fem::fem1d::Fem1dSolution } +#[pymethods] +impl PyFem1dSolution { + /// Wraps nodal values from one of the solvers. + /// + /// Errors: + /// + /// `SolveError::InvalidArgument` if the degree is not 1 or 2, the + /// interval is empty, or the value count is not `degree * k + 1` for + /// some positive `k`. + /// + /// Rust: `fem::fem1d::Fem1dSolution::new` + #[new] + #[pyo3(signature = (a, b, degree, values))] + fn __new__(a: f64, b: f64, degree: usize, values: Vec) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fem::fem1d::Fem1dSolution::new(a, b, degree, values)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(crate::generated::types::PyFem1dSolution { inner: __v }) + } + + /// The number of elements the mesh has. + /// + /// Rust: `fem::fem1d::Fem1dSolution::elements` + #[pyo3(name = "elements")] + #[pyo3(signature = ())] + fn elements(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.elements()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The mesh spacing, meaning the element width rather than the node + /// spacing -- for quadratic elements the nodes sit twice as close. + /// + /// Rust: `fem::fem1d::Fem1dSolution::h` + #[pyo3(name = "h")] + #[pyo3(signature = ())] + fn h(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.h()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The coordinates of the nodes. + /// + /// Rust: `fem::fem1d::Fem1dSolution::nodes` + #[pyo3(name = "nodes")] + #[pyo3(signature = ())] + fn nodes<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.nodes())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Evaluates the piecewise polynomial at `x`. + /// + /// Rust: `fem::fem1d::Fem1dSolution::eval` + #[pyo3(name = "eval")] + #[pyo3(signature = (x))] + fn eval(&self, x: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.eval(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Evaluates the derivative at `x`. + /// + /// The derivative jumps at element boundaries -- a finite element + /// solution is continuous but not smooth -- so the value returned + /// there is the one from the element `x` was located in. + /// + /// Rust: `fem::fem1d::Fem1dSolution::eval_derivative` + #[pyo3(name = "eval_derivative")] + #[pyo3(signature = (x))] + fn eval_derivative(&self, x: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.eval_derivative(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "a")] + fn py_get_a(&self) -> PyResult { Ok(self.inner.a) } + + #[setter] + #[pyo3(name = "a")] + fn py_set_a(&mut self, v: f64) { self.inner.a = v; } + + #[getter] + #[pyo3(name = "b")] + fn py_get_b(&self) -> PyResult { Ok(self.inner.b) } + + #[setter] + #[pyo3(name = "b")] + fn py_set_b(&mut self, v: f64) { self.inner.b = v; } + + #[getter] + #[pyo3(name = "degree")] + fn py_get_degree(&self) -> PyResult { Ok(self.inner.degree) } + + #[setter] + #[pyo3(name = "degree")] + fn py_set_degree(&mut self, v: usize) { self.inner.degree = v; } + + #[getter] + #[pyo3(name = "values")] + fn py_get_values(&self) -> PyResult> { Ok(self.inner.values.clone()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Fem1dSolution", "Fem1dSolution", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A conforming triangulation of a planar region. +/// +/// Rust: `fem::fem2d::FemMesh2` +#[pyclass(name = "FemMesh2", module = "numeria.fem.fem2d", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyFemMesh2 { pub inner: rust_physics_engine::fem::fem2d::FemMesh2 } +#[pymethods] +impl PyFemMesh2 { + /// Builds a mesh from nodes and triangles, orienting every triangle + /// counterclockwise and deriving the boundary from the edge counts. + /// + /// Orienting rather than rejecting is deliberate: a triangle listed + /// clockwise describes the same element, and the sign of its area is + /// a labelling convention rather than a property of the geometry. A + /// *zero* area is not, and is refused. + /// + /// Errors: + /// + /// `GeomError::Empty` with no triangles; + /// `GeomError::InvalidArgument` for an out-of-range index or a + /// repeated vertex within one triangle; `GeomError::Degenerate` for + /// a zero-area triangle; `GeomError::NotManifold` if any edge is + /// shared by more than two triangles. + /// + /// Rust: `fem::fem2d::FemMesh2::new` + #[new] + #[pyo3(signature = (nodes, tris))] + fn __new__(nodes: Vec, tris: Vec>) -> PyResult { + let nodes = nodes.into_iter().map(|__e| __e.0).collect::>(); + let tris = tris.into_iter().map(|__e| -> PyResult<[usize; 3]> { Ok(<[usize; 3]>::try_from(__e).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 3 values, got {}", __v.len())))?) }).collect::>>()?; + let __r = crate::runtime::guard(|| rust_physics_engine::fem::fem2d::FemMesh2::new(nodes, tris)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyFemMesh2 { inner: __v }) + } + + /// A right-triangle mesh of the rectangle `[0, w] x [0, h]`, each + /// cell split along one diagonal. + /// + /// The diagonals all run the same way, which makes the mesh Delaunay + /// -- every triangle is right-angled, so no angle opposite an edge + /// exceeds a right angle and the pair opposite any interior edge sums + /// to `pi` exactly. + /// + /// Errors: + /// + /// `GeomError::InvalidArgument` for a non-positive extent or a zero + /// subdivision count. + /// + /// Rust: `fem::fem2d::FemMesh2::rect` + #[pyo3(name = "rect")] + #[staticmethod] + #[pyo3(signature = (w, h, nx, ny))] + fn rect(w: f64, h: f64, nx: usize, ny: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fem::fem2d::FemMesh2::rect(w, h, nx, ny)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyFemMesh2 { inner: __v }) + } + + /// A fan-and-rings mesh of the disk of radius `r`, with `n` rings. + /// + /// The rings carry `6k` points at radius `k r / n`, which keeps the + /// arc spacing roughly equal to the radial spacing and so keeps the + /// triangles from degenerating towards the rim -- a fixed point count + /// per ring would make the outer triangles long and thin. + /// + /// Errors: + /// + /// `GeomError::InvalidArgument` for a non-positive radius or fewer + /// than one ring. + /// + /// Rust: `fem::fem2d::FemMesh2::disk` + #[pyo3(name = "disk")] + #[staticmethod] + #[pyo3(signature = (r, n))] + fn disk(r: f64, n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fem::fem2d::FemMesh2::disk(r, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyFemMesh2 { inner: __v }) + } + + /// A Delaunay triangulation of a point set. + /// + /// Errors: + /// + /// `GeomError::Empty` for fewer than three points, and whatever + /// `FemMesh2::new` reports for a degenerate result -- collinear + /// points produce no triangles at all. + /// + /// Rust: `fem::fem2d::FemMesh2::from_delaunay` + #[pyo3(name = "from_delaunay")] + #[staticmethod] + #[pyo3(signature = (points))] + fn from_delaunay(points: Vec) -> PyResult { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::fem::fem2d::FemMesh2::from_delaunay(&points)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyFemMesh2 { inner: __v }) + } + + /// Splits every triangle into four by joining its edge midpoints. + /// + /// All four children are similar to the parent, so the mesh quality + /// is preserved exactly rather than approximately: repeated + /// refinement of a good mesh stays good, and repeated refinement of a + /// sliver never recovers. + /// + /// Rust: `fem::fem2d::FemMesh2::refine_uniform` + #[pyo3(name = "refine_uniform")] + #[pyo3(signature = ())] + fn refine_uniform(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.refine_uniform()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyFemMesh2 { inner: __v }) + } + + /// The smallest interior angle anywhere in the mesh, in radians. + /// + /// The interpolation error constant grows as `1/sin` of this, which + /// is why it is the number to watch rather than the aspect ratio. + /// + /// Rust: `fem::fem2d::FemMesh2::quality_min_angle` + #[pyo3(name = "quality_min_angle")] + #[pyo3(signature = ())] + fn quality_min_angle(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.quality_min_angle()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The total area of the triangles. + /// + /// Rust: `fem::fem2d::FemMesh2::area` + #[pyo3(name = "area")] + #[pyo3(signature = ())] + fn area(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.area()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The number of distinct edges, which the Euler characteristic + /// relates to the node and triangle counts. + /// + /// Rust: `fem::fem2d::FemMesh2::edge_count` + #[pyo3(name = "edge_count")] + #[pyo3(signature = ())] + fn edge_count(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.edge_count()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "nodes")] + fn py_get_nodes(&self) -> PyResult> { Ok(self.inner.nodes.clone().into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) } + + #[getter] + #[pyo3(name = "tris")] + fn py_get_tris(&self) -> PyResult>> { Ok(self.inner.tris.clone().into_iter().map(|__x| __x.to_vec()).collect::>()) } + + #[getter] + #[pyo3(name = "boundary")] + fn py_get_boundary(&self) -> PyResult> { Ok(self.inner.boundary.clone()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("FemMesh2", "FemMesh2", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} diff --git a/bindings/python/src/generated/types/fields.rs b/bindings/python/src/generated/types/fields.rs new file mode 100644 index 0000000..6bb1336 --- /dev/null +++ b/bindings/python/src/generated/types/fields.rs @@ -0,0 +1,233 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; + + +/// Uniform-grid scalar fields. +/// +/// Minimal backfill of the Part 2 `ScalarField2`/`ScalarField3` types +/// that later roadmap phases build on: row-major storage with grid +/// spacing and bilinear/trilinear sampling. +/// 2D scalar field on an nx×ny uniform grid with spacing dx +/// (row-major: index = y·nx + x; physical position of node (i, j) is +/// (i·dx, j·dx)). +/// +/// Rust: `fields::ScalarField2` +#[pyclass(name = "ScalarField2", module = "numeria.fields", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyFieldsScalarField2 { pub inner: rust_physics_engine::fields::ScalarField2 } +#[pymethods] +impl PyFieldsScalarField2 { + /// Zero-filled field. + /// + /// Rust: `fields::ScalarField2::new` + #[new] + #[pyo3(signature = (nx, ny, dx))] + fn __new__(nx: usize, ny: usize, dx: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fields::ScalarField2::new(nx, ny, dx)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyFieldsScalarField2 { inner: __v }) + } + + /// Build from a function of the node position (x, y). + /// + /// Rust: `fields::ScalarField2::from_fn` + #[pyo3(name = "from_fn")] + #[staticmethod] + #[pyo3(signature = (nx, ny, dx, f))] + fn from_fn(nx: usize, ny: usize, dx: f64, f: pyo3::Py) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64, __a1: f64| -> f64 { __cb.call::<_, f64>((__a0, __a1), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::fields::ScalarField2::from_fn(nx, ny, dx, f)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyFieldsScalarField2 { inner: __v }) + } + + /// Node value. + /// + /// Rust: `fields::ScalarField2::get` + #[pyo3(name = "get")] + #[pyo3(signature = (i, j))] + fn get(&self, i: usize, j: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.get(i, j)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Set a node value. + /// + /// Rust: `fields::ScalarField2::set` + #[pyo3(name = "set")] + #[pyo3(signature = (i, j, v))] + fn set(&mut self, i: usize, j: usize, v: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.set(i, j, v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Bilinear sample at a physical position (clamped to the grid). + /// + /// Rust: `fields::ScalarField2::sample` + #[pyo3(name = "sample")] + #[pyo3(signature = (x, y))] + fn sample(&self, x: f64, y: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.sample(x, y)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Smallest and largest node values. + /// + /// Rust: `fields::ScalarField2::min_max` + #[pyo3(name = "min_max")] + #[pyo3(signature = ())] + fn min_max(&self) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| self.inner.min_max()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) + } + + #[getter] + #[pyo3(name = "nx")] + fn py_get_nx(&self) -> PyResult { Ok(self.inner.nx) } + + #[setter] + #[pyo3(name = "nx")] + fn py_set_nx(&mut self, v: usize) { self.inner.nx = v; } + + #[getter] + #[pyo3(name = "ny")] + fn py_get_ny(&self) -> PyResult { Ok(self.inner.ny) } + + #[setter] + #[pyo3(name = "ny")] + fn py_set_ny(&mut self, v: usize) { self.inner.ny = v; } + + #[getter] + #[pyo3(name = "dx")] + fn py_get_dx(&self) -> PyResult { Ok(self.inner.dx) } + + #[setter] + #[pyo3(name = "dx")] + fn py_set_dx(&mut self, v: f64) { self.inner.dx = v; } + + #[getter] + #[pyo3(name = "data")] + fn py_get_data(&self) -> PyResult> { Ok(self.inner.data.clone()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("ScalarField2", "ScalarField2", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// 3D scalar field on an nx×ny×nz uniform grid with spacing dx +/// (index = (k·ny + j)·nx + i). +/// +/// Rust: `fields::ScalarField3` +#[pyclass(name = "ScalarField3", module = "numeria.fields", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyFieldsScalarField3 { pub inner: rust_physics_engine::fields::ScalarField3 } +#[pymethods] +impl PyFieldsScalarField3 { + /// Zero-filled field. + /// + /// Rust: `fields::ScalarField3::new` + #[new] + #[pyo3(signature = (nx, ny, nz, dx))] + fn __new__(nx: usize, ny: usize, nz: usize, dx: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fields::ScalarField3::new(nx, ny, nz, dx)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyFieldsScalarField3 { inner: __v }) + } + + /// Node value. + /// + /// Rust: `fields::ScalarField3::get` + #[pyo3(name = "get")] + #[pyo3(signature = (i, j, k))] + fn get(&self, i: usize, j: usize, k: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.get(i, j, k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Set a node value. + /// + /// Rust: `fields::ScalarField3::set` + #[pyo3(name = "set")] + #[pyo3(signature = (i, j, k, v))] + fn set(&mut self, i: usize, j: usize, k: usize, v: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.set(i, j, k, v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Trilinear sample at a physical position (clamped to the grid). + /// + /// Rust: `fields::ScalarField3::sample` + #[pyo3(name = "sample")] + #[pyo3(signature = (x, y, z))] + fn sample(&self, x: f64, y: f64, z: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.sample(x, y, z)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "nx")] + fn py_get_nx(&self) -> PyResult { Ok(self.inner.nx) } + + #[setter] + #[pyo3(name = "nx")] + fn py_set_nx(&mut self, v: usize) { self.inner.nx = v; } + + #[getter] + #[pyo3(name = "ny")] + fn py_get_ny(&self) -> PyResult { Ok(self.inner.ny) } + + #[setter] + #[pyo3(name = "ny")] + fn py_set_ny(&mut self, v: usize) { self.inner.ny = v; } + + #[getter] + #[pyo3(name = "nz")] + fn py_get_nz(&self) -> PyResult { Ok(self.inner.nz) } + + #[setter] + #[pyo3(name = "nz")] + fn py_set_nz(&mut self, v: usize) { self.inner.nz = v; } + + #[getter] + #[pyo3(name = "dx")] + fn py_get_dx(&self) -> PyResult { Ok(self.inner.dx) } + + #[setter] + #[pyo3(name = "dx")] + fn py_set_dx(&mut self, v: f64) { self.inner.dx = v; } + + #[getter] + #[pyo3(name = "data")] + fn py_get_data(&self) -> PyResult> { Ok(self.inner.data.clone()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("ScalarField3", "ScalarField3", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} diff --git a/bindings/python/src/generated/types/finance.rs b/bindings/python/src/generated/types/finance.rs new file mode 100644 index 0000000..d976d93 --- /dev/null +++ b/bindings/python/src/generated/types/finance.rs @@ -0,0 +1,406 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; + + +/// Which barrier a knock-out or knock-in option watches. +/// +/// Rust: `finance::options::Barrier` +#[pyclass(name = "Barrier", module = "numeria.finance.options", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyBarrier { + UpAndOut, + DownAndOut, + UpAndIn, + DownAndIn, +} +impl PyBarrier { + pub fn to_rust(&self) -> rust_physics_engine::finance::options::Barrier { match self { + Self::UpAndOut => rust_physics_engine::finance::options::Barrier::UpAndOut, + Self::DownAndOut => rust_physics_engine::finance::options::Barrier::DownAndOut, + Self::UpAndIn => rust_physics_engine::finance::options::Barrier::UpAndIn, + Self::DownAndIn => rust_physics_engine::finance::options::Barrier::DownAndIn, + } } + pub fn from_rust(v: &rust_physics_engine::finance::options::Barrier) -> Self { match v { + rust_physics_engine::finance::options::Barrier::UpAndOut => Self::UpAndOut, + rust_physics_engine::finance::options::Barrier::DownAndOut => Self::DownAndOut, + rust_physics_engine::finance::options::Barrier::UpAndIn => Self::UpAndIn, + rust_physics_engine::finance::options::Barrier::DownAndIn => Self::DownAndIn, + } } +} +#[pymethods] +impl PyBarrier { + fn __repr__(&self) -> &'static str { + match self { + Self::UpAndOut => "Barrier.UpAndOut", + Self::DownAndOut => "Barrier.DownAndOut", + Self::UpAndIn => "Barrier.UpAndIn", + Self::DownAndIn => "Barrier.DownAndIn", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// The first-order sensitivities of an option price. +/// +/// Rust: `finance::options::Greeks` +#[pyclass(name = "Greeks", module = "numeria.finance.options", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyGreeks { pub inner: rust_physics_engine::finance::options::Greeks } +#[pymethods] +impl PyGreeks { + /// Builds a `Greeks` from its fields. + #[new] + #[pyo3(signature = (delta, gamma, vega, theta, rho))] + fn __new__(delta: f64, gamma: f64, vega: f64, theta: f64, rho: f64) -> Self { + + Self { inner: rust_physics_engine::finance::options::Greeks { delta: delta, gamma: gamma, vega: vega, theta: theta, rho: rho } } + } + + #[getter] + #[pyo3(name = "delta")] + fn py_get_delta(&self) -> PyResult { Ok(self.inner.delta) } + + #[setter] + #[pyo3(name = "delta")] + fn py_set_delta(&mut self, v: f64) { self.inner.delta = v; } + + #[getter] + #[pyo3(name = "gamma")] + fn py_get_gamma(&self) -> PyResult { Ok(self.inner.gamma) } + + #[setter] + #[pyo3(name = "gamma")] + fn py_set_gamma(&mut self, v: f64) { self.inner.gamma = v; } + + #[getter] + #[pyo3(name = "vega")] + fn py_get_vega(&self) -> PyResult { Ok(self.inner.vega) } + + #[setter] + #[pyo3(name = "vega")] + fn py_set_vega(&mut self, v: f64) { self.inner.vega = v; } + + #[getter] + #[pyo3(name = "theta")] + fn py_get_theta(&self) -> PyResult { Ok(self.inner.theta) } + + #[setter] + #[pyo3(name = "theta")] + fn py_set_theta(&mut self, v: f64) { self.inner.theta = v; } + + #[getter] + #[pyo3(name = "rho")] + fn py_get_rho(&self) -> PyResult { Ok(self.inner.rho) } + + #[setter] + #[pyo3(name = "rho")] + fn py_set_rho(&mut self, v: f64) { self.inner.rho = v; } + + fn __repr__(&self) -> String { format!("Greeks(delta={:?}, gamma={:?}, vega={:?}, theta={:?}, rho={:?})", self.inner.delta, self.inner.gamma, self.inner.vega, self.inner.theta, self.inner.rho) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A `Greeks` argument, or anything that can stand in for one. +pub struct PyGreeksArg(pub rust_physics_engine::finance::options::Greeks); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyGreeksArg { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result { + if let Ok(__w) = obj.extract::() { + return Ok(PyGreeksArg(__w.inner)); + } + let __v = crate::runtime::coerce::floats_exact(obj, 5, "Greeks")?; + Ok(PyGreeksArg(rust_physics_engine::finance::options::Greeks { delta: __v[0], gamma: __v[1], vega: __v[2], theta: __v[3], rho: __v[4] })) + } +} + + +/// The raw SVI parameterisation of a volatility smile. +/// +/// Rust: `finance::options::Svi` +#[pyclass(name = "Svi", module = "numeria.finance.options", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PySvi { pub inner: rust_physics_engine::finance::options::Svi } +#[pymethods] +impl PySvi { + /// Builds a `Svi` from its fields. + #[new] + #[pyo3(signature = (a, b, rho, m, sigma))] + fn __new__(a: f64, b: f64, rho: f64, m: f64, sigma: f64) -> Self { + + Self { inner: rust_physics_engine::finance::options::Svi { a: a, b: b, rho: rho, m: m, sigma: sigma } } + } + + #[getter] + #[pyo3(name = "a")] + fn py_get_a(&self) -> PyResult { Ok(self.inner.a) } + + #[setter] + #[pyo3(name = "a")] + fn py_set_a(&mut self, v: f64) { self.inner.a = v; } + + #[getter] + #[pyo3(name = "b")] + fn py_get_b(&self) -> PyResult { Ok(self.inner.b) } + + #[setter] + #[pyo3(name = "b")] + fn py_set_b(&mut self, v: f64) { self.inner.b = v; } + + #[getter] + #[pyo3(name = "rho")] + fn py_get_rho(&self) -> PyResult { Ok(self.inner.rho) } + + #[setter] + #[pyo3(name = "rho")] + fn py_set_rho(&mut self, v: f64) { self.inner.rho = v; } + + #[getter] + #[pyo3(name = "m")] + fn py_get_m(&self) -> PyResult { Ok(self.inner.m) } + + #[setter] + #[pyo3(name = "m")] + fn py_set_m(&mut self, v: f64) { self.inner.m = v; } + + #[getter] + #[pyo3(name = "sigma")] + fn py_get_sigma(&self) -> PyResult { Ok(self.inner.sigma) } + + #[setter] + #[pyo3(name = "sigma")] + fn py_set_sigma(&mut self, v: f64) { self.inner.sigma = v; } + + fn __repr__(&self) -> String { format!("Svi(a={:?}, b={:?}, rho={:?}, m={:?}, sigma={:?})", self.inner.a, self.inner.b, self.inner.rho, self.inner.m, self.inner.sigma) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A `Svi` argument, or anything that can stand in for one. +pub struct PySviArg(pub rust_physics_engine::finance::options::Svi); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PySviArg { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result { + if let Ok(__w) = obj.extract::() { + return Ok(PySviArg(__w.inner)); + } + let __v = crate::runtime::coerce::floats_exact(obj, 5, "Svi")?; + Ok(PySviArg(rust_physics_engine::finance::options::Svi { a: __v[0], b: __v[1], rho: __v[2], m: __v[3], sigma: __v[4] })) + } +} + + +/// How often a quoted rate compounds. +/// +/// Rust: `finance::rates::Compounding` +#[pyclass(name = "Compounding", module = "numeria.finance.rates", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyCompounding { + Annual, + SemiAnnual, + Quarterly, + Monthly, + Continuous, +} +impl PyCompounding { + pub fn to_rust(&self) -> rust_physics_engine::finance::rates::Compounding { match self { + Self::Annual => rust_physics_engine::finance::rates::Compounding::Annual, + Self::SemiAnnual => rust_physics_engine::finance::rates::Compounding::SemiAnnual, + Self::Quarterly => rust_physics_engine::finance::rates::Compounding::Quarterly, + Self::Monthly => rust_physics_engine::finance::rates::Compounding::Monthly, + Self::Continuous => rust_physics_engine::finance::rates::Compounding::Continuous, + } } + pub fn from_rust(v: &rust_physics_engine::finance::rates::Compounding) -> Self { match v { + rust_physics_engine::finance::rates::Compounding::Annual => Self::Annual, + rust_physics_engine::finance::rates::Compounding::SemiAnnual => Self::SemiAnnual, + rust_physics_engine::finance::rates::Compounding::Quarterly => Self::Quarterly, + rust_physics_engine::finance::rates::Compounding::Monthly => Self::Monthly, + rust_physics_engine::finance::rates::Compounding::Continuous => Self::Continuous, + } } +} +#[pymethods] +impl PyCompounding { + /// Periods per year, or `None` for continuous compounding. + /// + /// Rust: `finance::rates::Compounding::periods_per_year` + #[pyo3(name = "periods_per_year")] + #[pyo3(signature = ())] + fn periods_per_year(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.to_rust().periods_per_year()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) + } + + fn __repr__(&self) -> &'static str { + match self { + Self::Annual => "Compounding.Annual", + Self::SemiAnnual => "Compounding.SemiAnnual", + Self::Quarterly => "Compounding.Quarterly", + Self::Monthly => "Compounding.Monthly", + Self::Continuous => "Compounding.Continuous", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// One instrument on the curve to bootstrap: a bond quoted by price. +/// +/// Rust: `finance::rates::CurveBond` +#[pyclass(name = "CurveBond", module = "numeria.finance.rates", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyCurveBond { pub inner: rust_physics_engine::finance::rates::CurveBond } +#[pymethods] +impl PyCurveBond { + /// Builds a `CurveBond` from its fields. + #[new] + #[pyo3(signature = (maturity, coupon, price, frequency))] + fn __new__(maturity: f64, coupon: f64, price: f64, frequency: f64) -> Self { + + Self { inner: rust_physics_engine::finance::rates::CurveBond { maturity: maturity, coupon: coupon, price: price, frequency: frequency } } + } + + #[getter] + #[pyo3(name = "maturity")] + fn py_get_maturity(&self) -> PyResult { Ok(self.inner.maturity) } + + #[setter] + #[pyo3(name = "maturity")] + fn py_set_maturity(&mut self, v: f64) { self.inner.maturity = v; } + + #[getter] + #[pyo3(name = "coupon")] + fn py_get_coupon(&self) -> PyResult { Ok(self.inner.coupon) } + + #[setter] + #[pyo3(name = "coupon")] + fn py_set_coupon(&mut self, v: f64) { self.inner.coupon = v; } + + #[getter] + #[pyo3(name = "price")] + fn py_get_price(&self) -> PyResult { Ok(self.inner.price) } + + #[setter] + #[pyo3(name = "price")] + fn py_set_price(&mut self, v: f64) { self.inner.price = v; } + + #[getter] + #[pyo3(name = "frequency")] + fn py_get_frequency(&self) -> PyResult { Ok(self.inner.frequency) } + + #[setter] + #[pyo3(name = "frequency")] + fn py_set_frequency(&mut self, v: f64) { self.inner.frequency = v; } + + fn __repr__(&self) -> String { format!("CurveBond(maturity={:?}, coupon={:?}, price={:?}, frequency={:?})", self.inner.maturity, self.inner.coupon, self.inner.price, self.inner.frequency) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A `CurveBond` argument, or anything that can stand in for one. +pub struct PyCurveBondArg(pub rust_physics_engine::finance::rates::CurveBond); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyCurveBondArg { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result { + if let Ok(__w) = obj.extract::() { + return Ok(PyCurveBondArg(__w.inner)); + } + let __v = crate::runtime::coerce::floats_exact(obj, 4, "CurveBond")?; + Ok(PyCurveBondArg(rust_physics_engine::finance::rates::CurveBond { maturity: __v[0], coupon: __v[1], price: __v[2], frequency: __v[3] })) + } +} + + +/// What a backtest reports. +/// +/// Rust: `finance::risk::BacktestStats` +#[pyclass(name = "BacktestStats", module = "numeria.finance.risk", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyBacktestStats { pub inner: rust_physics_engine::finance::risk::BacktestStats } +#[pymethods] +impl PyBacktestStats { + /// Builds a `BacktestStats` from its fields. + #[new] + #[pyo3(signature = (total_return, trades, win_rate, max_drawdown, equity))] + fn __new__(total_return: f64, trades: usize, win_rate: f64, max_drawdown: f64, equity: Vec) -> Self { + + Self { inner: rust_physics_engine::finance::risk::BacktestStats { total_return: total_return, trades: trades, win_rate: win_rate, max_drawdown: max_drawdown, equity: equity } } + } + + #[getter] + #[pyo3(name = "total_return")] + fn py_get_total_return(&self) -> PyResult { Ok(self.inner.total_return) } + + #[setter] + #[pyo3(name = "total_return")] + fn py_set_total_return(&mut self, v: f64) { self.inner.total_return = v; } + + #[getter] + #[pyo3(name = "trades")] + fn py_get_trades(&self) -> PyResult { Ok(self.inner.trades) } + + #[setter] + #[pyo3(name = "trades")] + fn py_set_trades(&mut self, v: usize) { self.inner.trades = v; } + + #[getter] + #[pyo3(name = "win_rate")] + fn py_get_win_rate(&self) -> PyResult { Ok(self.inner.win_rate) } + + #[setter] + #[pyo3(name = "win_rate")] + fn py_set_win_rate(&mut self, v: f64) { self.inner.win_rate = v; } + + #[getter] + #[pyo3(name = "max_drawdown")] + fn py_get_max_drawdown(&self) -> PyResult { Ok(self.inner.max_drawdown) } + + #[setter] + #[pyo3(name = "max_drawdown")] + fn py_set_max_drawdown(&mut self, v: f64) { self.inner.max_drawdown = v; } + + #[getter] + #[pyo3(name = "equity")] + fn py_get_equity(&self) -> PyResult> { Ok(self.inner.equity.clone()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("BacktestStats", "BacktestStats", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} diff --git a/bindings/python/src/generated/types/fractals.rs b/bindings/python/src/generated/types/fractals.rs new file mode 100644 index 0000000..ee73e4d --- /dev/null +++ b/bindings/python/src/generated/types/fractals.rs @@ -0,0 +1,3354 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; + + +/// A discrete 2-D map x ← step(x). +/// +/// Rust: `fractals::attractors::Attractor2Map` +#[pyclass(name = "Attractor2Map", module = "numeria.fractals.attractors", unsendable)] +pub struct PyAttractor2Map { pub inner: rust_physics_engine::fractals::attractors::Attractor2Map } +#[pymethods] +impl PyAttractor2Map { + /// Iterates the map, discarding `burn_in` steps then keeping `n`. + /// + /// Rust: `fractals::attractors::Attractor2Map::trajectory` + #[pyo3(name = "trajectory")] + #[pyo3(signature = (x0, n, burn_in))] + fn trajectory(&self, x0: crate::generated::types::PyVec2Arg, n: usize, burn_in: usize) -> PyResult> { + let x0 = x0.0; + let __r = crate::runtime::guard(|| self.inner.trajectory(x0, n, burn_in)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) + } + + /// Histogram of `n` iterates over `bounds` (row-major). + /// + /// Panics: + /// Panics on an empty grid or degenerate bounds. + /// + /// Rust: `fractals::attractors::Attractor2Map::density_map` + #[pyo3(name = "density_map")] + #[pyo3(signature = (x0, n, res, bounds))] + fn density_map(&self, x0: crate::generated::types::PyVec2Arg, n: usize, res: (usize, usize), bounds: crate::generated::types::PyRect) -> PyResult> { + let x0 = x0.0; + let res = (res.0, res.1); + let bounds = bounds.inner; + let __r = crate::runtime::guard(|| self.inner.density_map(x0, n, res, &bounds)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Largest Lyapunov exponent per iteration, by renormalized + /// finite-difference perturbations. + /// + /// Panics: + /// Panics unless `n >= 100`. + /// + /// Rust: `fractals::attractors::Attractor2Map::lyapunov` + #[pyo3(name = "lyapunov")] + #[pyo3(signature = (x0, n))] + fn lyapunov(&self, x0: crate::generated::types::PyVec2Arg, n: usize) -> PyResult { + let x0 = x0.0; + let __r = crate::runtime::guard(|| self.inner.lyapunov(x0, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// A 3-D autonomous flow ẋ = f(x) with a preferred time step. +/// +/// Rust: `fractals::attractors::Attractor3` +#[pyclass(name = "Attractor3", module = "numeria.fractals.attractors", unsendable)] +pub struct PyAttractor3 { pub inner: rust_physics_engine::fractals::attractors::Attractor3 } +#[pymethods] +impl PyAttractor3 { + /// Integrates `n` steps from `x0`, returning the n+1 visited + /// states (including the start). + /// + /// Rust: `fractals::attractors::Attractor3::trajectory` + #[pyo3(name = "trajectory")] + #[pyo3(signature = (x0, n, method))] + fn trajectory(&self, x0: crate::generated::types::PyVec3Arg, n: usize, method: crate::generated::types::PyAttractorsIntegrator) -> PyResult> { + let x0 = x0.0; + let method = method.to_rust(); + let __r = crate::runtime::guard(|| self.inner.trajectory(x0, n, method)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) + } + + /// Full Lyapunov spectrum by the Benettin method: three + /// orthonormal perturbation vectors evolved through the + /// finite-difference flow map and re-orthonormalized (modified + /// Gram-Schmidt) each step; the exponents are the average log + /// stretching factors. Uses RK4 with step `dt`. + /// + /// Panics: + /// Panics unless `n >= 100` and `dt > 0`. + /// + /// Rust: `fractals::attractors::Attractor3::lyapunov_spectrum` + #[pyo3(name = "lyapunov_spectrum")] + #[pyo3(signature = (x0, n, dt))] + fn lyapunov_spectrum(&self, x0: crate::generated::types::PyVec3Arg, n: usize, dt: f64) -> PyResult> { + let x0 = x0.0; + let __r = crate::runtime::guard(|| self.inner.lyapunov_spectrum(x0, n, dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_vec()) + } + + /// Points where the trajectory crosses the plane in the +normal + /// direction, linearly interpolated between steps. + /// + /// Rust: `fractals::attractors::Attractor3::poincare_section` + #[pyo3(name = "poincare_section")] + #[pyo3(signature = (x0, n, plane))] + fn poincare_section(&self, x0: crate::generated::types::PyVec3Arg, n: usize, plane: crate::generated::types::PyPrimitivesPlane) -> PyResult> { + let x0 = x0.0; + let plane = plane.inner; + let __r = crate::runtime::guard(|| self.inner.poincare_section(x0, n, &plane)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) + } + + /// Histogram of the trajectory projected onto the xy plane over + /// `bounds` (row-major, x fastest). The first 100 steps are + /// discarded as transient. + /// + /// Panics: + /// Panics on an empty grid or degenerate bounds. + /// + /// Rust: `fractals::attractors::Attractor3::density_map` + #[pyo3(name = "density_map")] + #[pyo3(signature = (x0, n, res, bounds))] + fn density_map(&self, x0: crate::generated::types::PyVec3Arg, n: usize, res: (usize, usize), bounds: crate::generated::types::PyRect) -> PyResult> { + let x0 = x0.0; + let res = (res.0, res.1); + let bounds = bounds.inner; + let __r = crate::runtime::guard(|| self.inner.density_map(x0, n, res, &bounds)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "dt")] + fn py_get_dt(&self) -> PyResult { Ok(self.inner.dt) } + + #[setter] + #[pyo3(name = "dt")] + fn py_set_dt(&mut self, v: f64) { self.inner.dt = v; } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// Fixed-step integration schemes (`Rk45` takes two half steps and +/// keeps the fifth-order combination, giving adaptive-quality +/// accuracy at fixed cost). +/// +/// Rust: `fractals::attractors::Integrator` +#[pyclass(name = "Integrator", module = "numeria.fractals.attractors", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyAttractorsIntegrator { + Euler, + Rk4, + Rk45, +} +impl PyAttractorsIntegrator { + pub fn to_rust(&self) -> rust_physics_engine::fractals::attractors::Integrator { match self { + Self::Euler => rust_physics_engine::fractals::attractors::Integrator::Euler, + Self::Rk4 => rust_physics_engine::fractals::attractors::Integrator::Rk4, + Self::Rk45 => rust_physics_engine::fractals::attractors::Integrator::Rk45, + } } + pub fn from_rust(v: &rust_physics_engine::fractals::attractors::Integrator) -> Self { match v { + rust_physics_engine::fractals::attractors::Integrator::Euler => Self::Euler, + rust_physics_engine::fractals::attractors::Integrator::Rk4 => Self::Rk4, + rust_physics_engine::fractals::attractors::Integrator::Rk45 => Self::Rk45, + } } +} +#[pymethods] +impl PyAttractorsIntegrator { + fn __repr__(&self) -> &'static str { + match self { + Self::Euler => "Integrator.Euler", + Self::Rk4 => "Integrator.Rk4", + Self::Rk45 => "Integrator.Rk45", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Two-variable Oregonator model of the Belousov-Zhabotinsky +/// reaction: ∂u = ∇²u + (u(1−u) − f·v(u−q)/(u+q))/ε, ∂v = ∇²v·Dᵥ + u − v. +/// +/// Rust: `fractals::automata::BelousovZhabotinsky` +#[pyclass(name = "BelousovZhabotinsky", module = "numeria.fractals.automata", from_py_object)] +#[derive(Clone)] +pub struct PyBelousovZhabotinsky { pub inner: rust_physics_engine::fractals::automata::BelousovZhabotinsky } +#[pymethods] +impl PyBelousovZhabotinsky { + /// Resting medium with an excited spot in the center. + /// + /// Panics: + /// Panics unless the grid is at least 8×8. + /// + /// Rust: `fractals::automata::BelousovZhabotinsky::new` + #[new] + #[pyo3(signature = (w, h))] + fn __new__(w: usize, h: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::automata::BelousovZhabotinsky::new(w, h)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBelousovZhabotinsky { inner: __v }) + } + + /// One forward-Euler step. + /// + /// Rust: `fractals::automata::BelousovZhabotinsky::step` + #[pyo3(name = "step")] + #[pyo3(signature = ())] + fn step(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + #[getter] + #[pyo3(name = "w")] + fn py_get_w(&self) -> PyResult { Ok(self.inner.w) } + + #[setter] + #[pyo3(name = "w")] + fn py_set_w(&mut self, v: usize) { self.inner.w = v; } + + #[getter] + #[pyo3(name = "h")] + fn py_get_h(&self) -> PyResult { Ok(self.inner.h) } + + #[setter] + #[pyo3(name = "h")] + fn py_set_h(&mut self, v: usize) { self.inner.h = v; } + + #[getter] + #[pyo3(name = "u")] + fn py_get_u(&self) -> PyResult> { Ok(self.inner.u.clone()) } + + #[getter] + #[pyo3(name = "v")] + fn py_get_v(&self) -> PyResult> { Ok(self.inner.v.clone()) } + + #[getter] + #[pyo3(name = "eps")] + fn py_get_eps(&self) -> PyResult { Ok(self.inner.eps) } + + #[setter] + #[pyo3(name = "eps")] + fn py_set_eps(&mut self, v: f64) { self.inner.eps = v; } + + #[getter] + #[pyo3(name = "f")] + fn py_get_f(&self) -> PyResult { Ok(self.inner.f) } + + #[setter] + #[pyo3(name = "f")] + fn py_set_f(&mut self, v: f64) { self.inner.f = v; } + + #[getter] + #[pyo3(name = "q")] + fn py_get_q(&self) -> PyResult { Ok(self.inner.q) } + + #[setter] + #[pyo3(name = "q")] + fn py_set_q(&mut self, v: f64) { self.inner.q = v; } + + #[getter] + #[pyo3(name = "dv")] + fn py_get_dv(&self) -> PyResult { Ok(self.inner.dv) } + + #[setter] + #[pyo3(name = "dv")] + fn py_set_dv(&mut self, v: f64) { self.inner.dv = v; } + + #[getter] + #[pyo3(name = "dt")] + fn py_get_dt(&self) -> PyResult { Ok(self.inner.dt) } + + #[setter] + #[pyo3(name = "dt")] + fn py_set_dt(&mut self, v: f64) { self.inner.dt = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("BelousovZhabotinsky", "BelousovZhabotinsky", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Brian's Brain: three states (0 dead, 1 dying, 2 firing); a dead +/// cell fires with exactly two firing neighbors, firing cells decay +/// to dying, dying cells die. +/// +/// Rust: `fractals::automata::BriansBrain` +#[pyclass(name = "BriansBrain", module = "numeria.fractals.automata", from_py_object)] +#[derive(Clone)] +pub struct PyBriansBrain { pub inner: rust_physics_engine::fractals::automata::BriansBrain } +#[pymethods] +impl PyBriansBrain { + /// Blank grid. + /// + /// Panics: + /// Panics unless the grid is at least 3×3. + /// + /// Rust: `fractals::automata::BriansBrain::new` + #[new] + #[pyo3(signature = (w, h))] + fn __new__(w: usize, h: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::automata::BriansBrain::new(w, h)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBriansBrain { inner: __v }) + } + + /// Advances one generation (toroidal). + /// + /// Rust: `fractals::automata::BriansBrain::step` + #[pyo3(name = "step")] + #[pyo3(signature = ())] + fn step(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + #[getter] + #[pyo3(name = "w")] + fn py_get_w(&self) -> PyResult { Ok(self.inner.w) } + + #[setter] + #[pyo3(name = "w")] + fn py_set_w(&mut self, v: usize) { self.inner.w = v; } + + #[getter] + #[pyo3(name = "h")] + fn py_get_h(&self) -> PyResult { Ok(self.inner.h) } + + #[setter] + #[pyo3(name = "h")] + fn py_set_h(&mut self, v: usize) { self.inner.h = v; } + + #[getter] + #[pyo3(name = "cells")] + fn py_get_cells(&self) -> PyResult> { Ok(self.inner.cells.clone()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("BriansBrain", "BriansBrain", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Brusselator: ∂u = Dᵤ∇²u + A − (B+1)u + u²v, ∂v = Dᵥ∇²v + Bu − u²v. +/// +/// Rust: `fractals::automata::Brusselator` +#[pyclass(name = "Brusselator", module = "numeria.fractals.automata", from_py_object)] +#[derive(Clone)] +pub struct PyBrusselator { pub inner: rust_physics_engine::fractals::automata::Brusselator } +#[pymethods] +impl PyBrusselator { + /// Starts at the homogeneous fixed point (A, B/A) with small + /// random perturbations; B > 1 + A² puts it in the Turing/ + /// oscillatory regime. + /// + /// Panics: + /// Panics unless the grid is at least 3×3 and `a > 0`. + /// + /// Rust: `fractals::automata::Brusselator::new` + #[new] + #[pyo3(signature = (w, h, a, b, rng))] + fn __new__(w: usize, h: usize, a: f64, b: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::automata::Brusselator::new(w, h, a, b, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBrusselator { inner: __v }) + } + + /// One forward-Euler step. + /// + /// Rust: `fractals::automata::Brusselator::step` + #[pyo3(name = "step")] + #[pyo3(signature = ())] + fn step(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + #[getter] + #[pyo3(name = "w")] + fn py_get_w(&self) -> PyResult { Ok(self.inner.w) } + + #[setter] + #[pyo3(name = "w")] + fn py_set_w(&mut self, v: usize) { self.inner.w = v; } + + #[getter] + #[pyo3(name = "h")] + fn py_get_h(&self) -> PyResult { Ok(self.inner.h) } + + #[setter] + #[pyo3(name = "h")] + fn py_set_h(&mut self, v: usize) { self.inner.h = v; } + + #[getter] + #[pyo3(name = "u")] + fn py_get_u(&self) -> PyResult> { Ok(self.inner.u.clone()) } + + #[getter] + #[pyo3(name = "v")] + fn py_get_v(&self) -> PyResult> { Ok(self.inner.v.clone()) } + + #[getter] + #[pyo3(name = "a")] + fn py_get_a(&self) -> PyResult { Ok(self.inner.a) } + + #[setter] + #[pyo3(name = "a")] + fn py_set_a(&mut self, v: f64) { self.inner.a = v; } + + #[getter] + #[pyo3(name = "b")] + fn py_get_b(&self) -> PyResult { Ok(self.inner.b) } + + #[setter] + #[pyo3(name = "b")] + fn py_set_b(&mut self, v: f64) { self.inner.b = v; } + + #[getter] + #[pyo3(name = "du")] + fn py_get_du(&self) -> PyResult { Ok(self.inner.du) } + + #[setter] + #[pyo3(name = "du")] + fn py_set_du(&mut self, v: f64) { self.inner.du = v; } + + #[getter] + #[pyo3(name = "dv")] + fn py_get_dv(&self) -> PyResult { Ok(self.inner.dv) } + + #[setter] + #[pyo3(name = "dv")] + fn py_set_dv(&mut self, v: f64) { self.inner.dv = v; } + + #[getter] + #[pyo3(name = "dt")] + fn py_get_dt(&self) -> PyResult { Ok(self.inner.dt) } + + #[setter] + #[pyo3(name = "dt")] + fn py_set_dt(&mut self, v: f64) { self.inner.dt = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Brusselator", "Brusselator", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Elementary (radius-1, 2-state) 1-D cellular automaton with a +/// Wolfram rule number. +/// +/// Rust: `fractals::automata::Ca1D` +#[pyclass(name = "Ca1D", module = "numeria.fractals.automata", from_py_object)] +#[derive(Clone)] +pub struct PyCa1D { pub inner: rust_physics_engine::fractals::automata::Ca1D } +#[pymethods] +impl PyCa1D { + /// Automaton of `width` dead cells. + /// + /// Panics: + /// Panics unless `width >= 3`. + /// + /// Rust: `fractals::automata::Ca1D::new` + #[new] + #[pyo3(signature = (rule, width, wrap))] + fn __new__(rule: u8, width: usize, wrap: bool) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::automata::Ca1D::new(rule, width, wrap)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCa1D { inner: __v }) + } + + /// Sets the single center cell. + /// + /// Rust: `fractals::automata::Ca1D::seed_center` + #[pyo3(name = "seed_center")] + #[pyo3(signature = ())] + fn seed_center(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.seed_center()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Sets each cell alive with probability `p`. + /// + /// Rust: `fractals::automata::Ca1D::seed_random` + #[pyo3(name = "seed_random")] + #[pyo3(signature = (rng, p))] + fn seed_random(&mut self, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>, p: f64) -> PyResult<()> { + let mut rng = rng; + let __r = crate::runtime::guard(|| self.inner.seed_random(&mut rng.inner, p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Advances one generation. + /// + /// Rust: `fractals::automata::Ca1D::step` + #[pyo3(name = "step")] + #[pyo3(signature = ())] + fn step(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Runs `steps` generations, returning every row including the + /// initial state (steps+1 rows). + /// + /// Rust: `fractals::automata::Ca1D::run` + #[pyo3(name = "run")] + #[pyo3(signature = (steps))] + fn run<'py>(&mut self, py: Python<'py>, steps: usize) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.run(steps))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Shannon entropy (bits) of the 3-cell block distribution of + /// the current state. + /// + /// Rust: `fractals::automata::Ca1D::entropy` + #[pyo3(name = "entropy")] + #[pyo3(signature = ())] + fn entropy(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.entropy()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Heuristic for Wolfram class 4 (complex localized structures). + /// + /// Rust: `fractals::automata::Ca1D::is_class4_heuristic` + #[pyo3(name = "is_class4_heuristic")] + #[pyo3(signature = ())] + fn is_class4_heuristic(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_class4_heuristic()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "rule")] + fn py_get_rule(&self) -> PyResult { Ok(self.inner.rule) } + + #[setter] + #[pyo3(name = "rule")] + fn py_set_rule(&mut self, v: u8) { self.inner.rule = v; } + + #[getter] + #[pyo3(name = "cells")] + fn py_get_cells(&self) -> PyResult> { Ok(self.inner.cells.clone()) } + + #[getter] + #[pyo3(name = "wrap")] + fn py_get_wrap(&self) -> PyResult { Ok(self.inner.wrap) } + + #[setter] + #[pyo3(name = "wrap")] + fn py_set_wrap(&mut self, v: bool) { self.inner.wrap = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Ca1D", "Ca1D", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Cyclic cellular automaton: state k advances to k+1 (mod states) +/// when at least `threshold` neighbors within Chebyshev `range` +/// carry the successor state; produces spiral waves. +/// +/// Rust: `fractals::automata::CyclicCa` +#[pyclass(name = "CyclicCa", module = "numeria.fractals.automata", from_py_object)] +#[derive(Clone)] +pub struct PyCyclicCa { pub inner: rust_physics_engine::fractals::automata::CyclicCa } +#[pymethods] +impl PyCyclicCa { + /// Random initial configuration. + /// + /// Panics: + /// Panics unless the grid is at least 3×3, `states >= 2`, and + /// `range >= 1`. + /// + /// Rust: `fractals::automata::CyclicCa::new` + #[new] + #[pyo3(signature = (w, h, states, threshold, range, rng))] + fn __new__(w: usize, h: usize, states: u8, threshold: usize, range: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::automata::CyclicCa::new(w, h, states, threshold, range, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCyclicCa { inner: __v }) + } + + /// Advances one generation (toroidal). + /// + /// Rust: `fractals::automata::CyclicCa::step` + #[pyo3(name = "step")] + #[pyo3(signature = ())] + fn step(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Advances `n` generations. + /// + /// Rust: `fractals::automata::CyclicCa::run` + #[pyo3(name = "run")] + #[pyo3(signature = (n))] + fn run(&mut self, n: usize) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.run(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + #[getter] + #[pyo3(name = "w")] + fn py_get_w(&self) -> PyResult { Ok(self.inner.w) } + + #[setter] + #[pyo3(name = "w")] + fn py_set_w(&mut self, v: usize) { self.inner.w = v; } + + #[getter] + #[pyo3(name = "h")] + fn py_get_h(&self) -> PyResult { Ok(self.inner.h) } + + #[setter] + #[pyo3(name = "h")] + fn py_set_h(&mut self, v: usize) { self.inner.h = v; } + + #[getter] + #[pyo3(name = "states")] + fn py_get_states(&self) -> PyResult { Ok(self.inner.states) } + + #[setter] + #[pyo3(name = "states")] + fn py_set_states(&mut self, v: u8) { self.inner.states = v; } + + #[getter] + #[pyo3(name = "cells")] + fn py_get_cells(&self) -> PyResult> { Ok(self.inner.cells.clone()) } + + #[getter] + #[pyo3(name = "threshold")] + fn py_get_threshold(&self) -> PyResult { Ok(self.inner.threshold) } + + #[setter] + #[pyo3(name = "threshold")] + fn py_set_threshold(&mut self, v: usize) { self.inner.threshold = v; } + + #[getter] + #[pyo3(name = "range")] + fn py_get_range(&self) -> PyResult { Ok(self.inner.range) } + + #[setter] + #[pyo3(name = "range")] + fn py_set_range(&mut self, v: usize) { self.inner.range = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("CyclicCa", "CyclicCa", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// FitzHugh-Nagumo excitable medium: ∂v = D∇²v + v − v³/3 − w, +/// ∂w = ε(v + a − b·w), with no-flux boundaries (wrapped copies +/// annihilate spirals). +/// +/// Rust: `fractals::automata::FitzHughNagumo` +#[pyclass(name = "FitzHughNagumo", module = "numeria.fractals.automata", from_py_object)] +#[derive(Clone)] +pub struct PyFitzHughNagumo { pub inner: rust_physics_engine::fractals::automata::FitzHughNagumo } +#[pymethods] +impl PyFitzHughNagumo { + /// Uniform resting state, parameterized in the excitable regime + /// that supports rotating spirals on modest grids + /// (a = 0.5, b = 0.8, ε = 0.05, D = 0.3). + /// + /// Panics: + /// Panics unless the grid is at least 8×8. + /// + /// Rust: `fractals::automata::FitzHughNagumo::new` + #[new] + #[pyo3(signature = (w, h))] + fn __new__(w: usize, h: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::automata::FitzHughNagumo::new(w, h)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyFitzHughNagumo { inner: __v }) + } + + /// Seeds a phase-distributed spiral: v and w wind once around + /// the grid center, which relaxes into a rotating spiral wave. + /// + /// Rust: `fractals::automata::FitzHughNagumo::spiral_wave_seed` + #[pyo3(name = "spiral_wave_seed")] + #[pyo3(signature = ())] + fn spiral_wave_seed(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.spiral_wave_seed()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// One forward-Euler step (no-flux boundaries). + /// + /// Rust: `fractals::automata::FitzHughNagumo::step` + #[pyo3(name = "step")] + #[pyo3(signature = ())] + fn step(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Runs `n` steps. + /// + /// Rust: `fractals::automata::FitzHughNagumo::run` + #[pyo3(name = "run")] + #[pyo3(signature = (n))] + fn run(&mut self, n: usize) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.run(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + #[getter] + #[pyo3(name = "w")] + fn py_get_w(&self) -> PyResult { Ok(self.inner.w) } + + #[setter] + #[pyo3(name = "w")] + fn py_set_w(&mut self, v: usize) { self.inner.w = v; } + + #[getter] + #[pyo3(name = "h")] + fn py_get_h(&self) -> PyResult { Ok(self.inner.h) } + + #[setter] + #[pyo3(name = "h")] + fn py_set_h(&mut self, v: usize) { self.inner.h = v; } + + #[getter] + #[pyo3(name = "v")] + fn py_get_v(&self) -> PyResult> { Ok(self.inner.v.clone()) } + + #[getter] + #[pyo3(name = "w_")] + fn py_get_w_(&self) -> PyResult> { Ok(self.inner.w_.clone()) } + + #[getter] + #[pyo3(name = "a")] + fn py_get_a(&self) -> PyResult { Ok(self.inner.a) } + + #[setter] + #[pyo3(name = "a")] + fn py_set_a(&mut self, v: f64) { self.inner.a = v; } + + #[getter] + #[pyo3(name = "b")] + fn py_get_b(&self) -> PyResult { Ok(self.inner.b) } + + #[setter] + #[pyo3(name = "b")] + fn py_set_b(&mut self, v: f64) { self.inner.b = v; } + + #[getter] + #[pyo3(name = "eps")] + fn py_get_eps(&self) -> PyResult { Ok(self.inner.eps) } + + #[setter] + #[pyo3(name = "eps")] + fn py_set_eps(&mut self, v: f64) { self.inner.eps = v; } + + #[getter] + #[pyo3(name = "d")] + fn py_get_d(&self) -> PyResult { Ok(self.inner.d) } + + #[setter] + #[pyo3(name = "d")] + fn py_set_d(&mut self, v: f64) { self.inner.d = v; } + + #[getter] + #[pyo3(name = "dt")] + fn py_get_dt(&self) -> PyResult { Ok(self.inner.dt) } + + #[setter] + #[pyo3(name = "dt")] + fn py_set_dt(&mut self, v: f64) { self.inner.dt = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("FitzHughNagumo", "FitzHughNagumo", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Gray-Scott reaction-diffusion: ∂u = Dᵤ∇²u − uv² + F(1−u), +/// ∂v = Dᵥ∇²v + uv² − (F+k)v (Pearson 1993). +/// +/// Rust: `fractals::automata::GrayScott` +#[pyclass(name = "GrayScott", module = "numeria.fractals.automata", from_py_object)] +#[derive(Clone)] +pub struct PyGrayScott { pub inner: rust_physics_engine::fractals::automata::GrayScott } +#[pymethods] +impl PyGrayScott { + /// Uniform u = 1, v = 0 state with standard diffusion rates. + /// + /// Panics: + /// Panics unless the grid is at least 3×3. + /// + /// Rust: `fractals::automata::GrayScott::new` + #[new] + #[pyo3(signature = (w, h, feed, kill))] + fn __new__(w: usize, h: usize, feed: f64, kill: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::automata::GrayScott::new(w, h, feed, kill)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGrayScott { inner: __v }) + } + + /// Pearson's mitosis regime (F = 0.0367, k = 0.0649). + /// + /// Rust: `fractals::automata::GrayScott::mitosis` + #[pyo3(name = "mitosis")] + #[staticmethod] + #[pyo3(signature = (w, h))] + fn mitosis(w: usize, h: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::automata::GrayScott::mitosis(w, h)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGrayScott { inner: __v }) + } + + /// Coral growth regime (F = 0.0545, k = 0.062). + /// + /// Rust: `fractals::automata::GrayScott::coral` + #[pyo3(name = "coral")] + #[staticmethod] + #[pyo3(signature = (w, h))] + fn coral(w: usize, h: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::automata::GrayScott::coral(w, h)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGrayScott { inner: __v }) + } + + /// Spots (F = 0.03, k = 0.062). + /// + /// Rust: `fractals::automata::GrayScott::spots` + #[pyo3(name = "spots")] + #[staticmethod] + #[pyo3(signature = (w, h))] + fn spots(w: usize, h: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::automata::GrayScott::spots(w, h)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGrayScott { inner: __v }) + } + + /// Worms (F = 0.046, k = 0.063). + /// + /// Rust: `fractals::automata::GrayScott::worms` + #[pyo3(name = "worms")] + #[staticmethod] + #[pyo3(signature = (w, h))] + fn worms(w: usize, h: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::automata::GrayScott::worms(w, h)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGrayScott { inner: __v }) + } + + /// Maze-like labyrinths (F = 0.029, k = 0.057). + /// + /// Rust: `fractals::automata::GrayScott::maze` + #[pyo3(name = "maze")] + #[staticmethod] + #[pyo3(signature = (w, h))] + fn maze(w: usize, h: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::automata::GrayScott::maze(w, h)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGrayScott { inner: __v }) + } + + /// Holes (F = 0.039, k = 0.058). + /// + /// Rust: `fractals::automata::GrayScott::holes` + #[pyo3(name = "holes")] + #[staticmethod] + #[pyo3(signature = (w, h))] + fn holes(w: usize, h: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::automata::GrayScott::holes(w, h)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGrayScott { inner: __v }) + } + + /// Travelling waves (F = 0.014, k = 0.045). + /// + /// Rust: `fractals::automata::GrayScott::waves` + #[pyo3(name = "waves")] + #[staticmethod] + #[pyo3(signature = (w, h))] + fn waves(w: usize, h: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::automata::GrayScott::waves(w, h)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGrayScott { inner: __v }) + } + + /// Solitons (F = 0.03, k = 0.06). + /// + /// Rust: `fractals::automata::GrayScott::solitons` + #[pyo3(name = "solitons")] + #[staticmethod] + #[pyo3(signature = (w, h))] + fn solitons(w: usize, h: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::automata::GrayScott::solitons(w, h)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGrayScott { inner: __v }) + } + + /// Seeds a square of v = 1, u = 0.5 (the usual perturbation). + /// + /// Rust: `fractals::automata::GrayScott::seed_square` + #[pyo3(name = "seed_square")] + #[pyo3(signature = (x, y, size))] + fn seed_square(&mut self, x: usize, y: usize, size: usize) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.seed_square(x, y, size)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// One forward-Euler step. + /// + /// Rust: `fractals::automata::GrayScott::step` + #[pyo3(name = "step")] + #[pyo3(signature = ())] + fn step(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Runs `n` steps. + /// + /// Rust: `fractals::automata::GrayScott::run` + #[pyo3(name = "run")] + #[pyo3(signature = (n))] + fn run(&mut self, n: usize) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.run(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + #[getter] + #[pyo3(name = "w")] + fn py_get_w(&self) -> PyResult { Ok(self.inner.w) } + + #[setter] + #[pyo3(name = "w")] + fn py_set_w(&mut self, v: usize) { self.inner.w = v; } + + #[getter] + #[pyo3(name = "h")] + fn py_get_h(&self) -> PyResult { Ok(self.inner.h) } + + #[setter] + #[pyo3(name = "h")] + fn py_set_h(&mut self, v: usize) { self.inner.h = v; } + + #[getter] + #[pyo3(name = "u")] + fn py_get_u(&self) -> PyResult> { Ok(self.inner.u.clone()) } + + #[getter] + #[pyo3(name = "v")] + fn py_get_v(&self) -> PyResult> { Ok(self.inner.v.clone()) } + + #[getter] + #[pyo3(name = "du")] + fn py_get_du(&self) -> PyResult { Ok(self.inner.du) } + + #[setter] + #[pyo3(name = "du")] + fn py_set_du(&mut self, v: f64) { self.inner.du = v; } + + #[getter] + #[pyo3(name = "dv")] + fn py_get_dv(&self) -> PyResult { Ok(self.inner.dv) } + + #[setter] + #[pyo3(name = "dv")] + fn py_set_dv(&mut self, v: f64) { self.inner.dv = v; } + + #[getter] + #[pyo3(name = "feed")] + fn py_get_feed(&self) -> PyResult { Ok(self.inner.feed) } + + #[setter] + #[pyo3(name = "feed")] + fn py_set_feed(&mut self, v: f64) { self.inner.feed = v; } + + #[getter] + #[pyo3(name = "kill")] + fn py_get_kill(&self) -> PyResult { Ok(self.inner.kill) } + + #[setter] + #[pyo3(name = "kill")] + fn py_set_kill(&mut self, v: f64) { self.inner.kill = v; } + + #[getter] + #[pyo3(name = "dt")] + fn py_get_dt(&self) -> PyResult { Ok(self.inner.dt) } + + #[setter] + #[pyo3(name = "dt")] + fn py_set_dt(&mut self, v: f64) { self.inner.dt = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("GrayScott", "GrayScott", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Langton's ant generalized to multi-state turning rules ("RL" is +/// the classic ant; each letter gives the turn on a cell of that +/// color). +/// +/// Rust: `fractals::automata::LangtonsAnt` +#[pyclass(name = "LangtonsAnt", module = "numeria.fractals.automata", from_py_object)] +#[derive(Clone)] +pub struct PyLangtonsAnt { pub inner: rust_physics_engine::fractals::automata::LangtonsAnt } +#[pymethods] +impl PyLangtonsAnt { + /// Ant at the grid center on a blank toroidal grid. + /// + /// Panics: + /// Panics unless the grid is at least 3×3 and the rule is made + /// of L/R with at least 2 letters. + /// + /// Rust: `fractals::automata::LangtonsAnt::new` + #[new] + #[pyo3(signature = (w, h, rule))] + fn __new__(w: usize, h: usize, rule: String) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::automata::LangtonsAnt::new(w, h, &rule)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLangtonsAnt { inner: __v }) + } + + /// One ant step: turn by the current cell's rule letter, advance + /// the cell color, move forward. + /// + /// Rust: `fractals::automata::LangtonsAnt::step` + #[pyo3(name = "step")] + #[pyo3(signature = ())] + fn step(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Runs `n` steps. + /// + /// Rust: `fractals::automata::LangtonsAnt::run` + #[pyo3(name = "run")] + #[pyo3(signature = (n))] + fn run(&mut self, n: usize) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.run(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Heuristic highway detection for the classic RL ant: the + /// displacement over the last 104 steps repeats (the highway is + /// a period-104 translation). + /// + /// Rust: `fractals::automata::LangtonsAnt::highway_detected` + #[pyo3(name = "highway_detected")] + #[pyo3(signature = ())] + fn highway_detected(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.highway_detected()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "w")] + fn py_get_w(&self) -> PyResult { Ok(self.inner.w) } + + #[setter] + #[pyo3(name = "w")] + fn py_set_w(&mut self, v: usize) { self.inner.w = v; } + + #[getter] + #[pyo3(name = "h")] + fn py_get_h(&self) -> PyResult { Ok(self.inner.h) } + + #[setter] + #[pyo3(name = "h")] + fn py_set_h(&mut self, v: usize) { self.inner.h = v; } + + #[getter] + #[pyo3(name = "cells")] + fn py_get_cells(&self) -> PyResult> { Ok(self.inner.cells.clone()) } + + #[getter] + #[pyo3(name = "pos")] + fn py_get_pos(&self) -> PyResult<(usize, usize)> { Ok((self.inner.pos.clone().0, self.inner.pos.clone().1)) } + + #[getter] + #[pyo3(name = "dir")] + fn py_get_dir(&self) -> PyResult { Ok(self.inner.dir) } + + #[setter] + #[pyo3(name = "dir")] + fn py_set_dir(&mut self, v: u8) { self.inner.dir = v; } + + #[getter] + #[pyo3(name = "rule")] + fn py_get_rule(&self) -> PyResult { Ok(self.inner.rule.to_string()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("LangtonsAnt", "LangtonsAnt", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Lenia (Chan 2019): continuous cellular automaton with a smooth +/// ring kernel and a Gaussian growth mapping, integrated by direct +/// convolution. +/// +/// Rust: `fractals::automata::Lenia` +#[pyclass(name = "Lenia", module = "numeria.fractals.automata", from_py_object)] +#[derive(Clone)] +pub struct PyLenia { pub inner: rust_physics_engine::fractals::automata::Lenia } +#[pymethods] +impl PyLenia { + /// Standard Lenia with the smooth ring kernel + /// exp(4 − 1/(r(1−r))) and growth 2·exp(−(u−μ)²/2σ²) − 1. + /// + /// Panics: + /// Panics unless the grid is at least 2r+1 wide and σ > 0. + /// + /// Rust: `fractals::automata::Lenia::new` + #[new] + #[pyo3(signature = (w, h, radius, mu, sigma))] + fn __new__(w: usize, h: usize, radius: usize, mu: f64, sigma: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::automata::Lenia::new(w, h, radius, mu, sigma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLenia { inner: __v }) + } + + /// One Lenia timestep of size `dt`. + /// + /// Rust: `fractals::automata::Lenia::step` + #[pyo3(name = "step")] + #[pyo3(signature = (dt))] + fn step(&mut self, dt: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step(dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + #[getter] + #[pyo3(name = "w")] + fn py_get_w(&self) -> PyResult { Ok(self.inner.w) } + + #[setter] + #[pyo3(name = "w")] + fn py_set_w(&mut self, v: usize) { self.inner.w = v; } + + #[getter] + #[pyo3(name = "h")] + fn py_get_h(&self) -> PyResult { Ok(self.inner.h) } + + #[setter] + #[pyo3(name = "h")] + fn py_set_h(&mut self, v: usize) { self.inner.h = v; } + + #[getter] + #[pyo3(name = "field")] + fn py_get_field(&self) -> PyResult> { Ok(self.inner.field.clone()) } + + #[getter] + #[pyo3(name = "radius")] + fn py_get_radius(&self) -> PyResult { Ok(self.inner.radius) } + + #[setter] + #[pyo3(name = "radius")] + fn py_set_radius(&mut self, v: usize) { self.inner.radius = v; } + + #[getter] + #[pyo3(name = "kernel")] + fn py_get_kernel(&self) -> PyResult> { Ok(self.inner.kernel.clone()) } + + #[getter] + #[pyo3(name = "mu")] + fn py_get_mu(&self) -> PyResult { Ok(self.inner.mu) } + + #[setter] + #[pyo3(name = "mu")] + fn py_set_mu(&mut self, v: f64) { self.inner.mu = v; } + + #[getter] + #[pyo3(name = "sigma")] + fn py_get_sigma(&self) -> PyResult { Ok(self.inner.sigma) } + + #[setter] + #[pyo3(name = "sigma")] + fn py_set_sigma(&mut self, v: f64) { self.inner.sigma = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Lenia", "Lenia", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A life-like (outer-totalistic, Moore-neighborhood, 2-state) +/// automaton on a `w` × `h` grid. +/// +/// Rust: `fractals::automata::LifeLike` +#[pyclass(name = "LifeLike", module = "numeria.fractals.automata", from_py_object)] +#[derive(Clone)] +pub struct PyLifeLike { pub inner: rust_physics_engine::fractals::automata::LifeLike } +#[pymethods] +impl PyLifeLike { + /// Builds a `LifeLike` from its fields. + #[new] + #[pyo3(signature = (w, h, cells, birth, survive, wrap))] + fn __new__(w: usize, h: usize, cells: Vec, birth: Vec, survive: Vec, wrap: bool) -> PyResult { + let birth = <[bool; 9]>::try_from(birth).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 9 values, got {}", __v.len())))?; + let survive = <[bool; 9]>::try_from(survive).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 9 values, got {}", __v.len())))?; + Ok(Self { inner: rust_physics_engine::fractals::automata::LifeLike { w: w, h: h, cells: cells, birth: birth, survive: survive, wrap: wrap } }) + } + + /// Parses a "B3/S23"-style rule string (also HighLife "B36/S23", + /// Day & Night "B3678/S34678", Seeds "B2/S", Life without death + /// "B3/S012345678"). + /// + /// Errors: + /// `InvalidArgument` on a malformed rule string. + /// + /// Rust: `fractals::automata::LifeLike::from_rule_string` + #[pyo3(name = "from_rule_string")] + #[staticmethod] + #[pyo3(signature = (w, h, rule))] + fn from_rule_string(w: usize, h: usize, rule: String) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::automata::LifeLike::from_rule_string(w, h, &rule)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyLifeLike { inner: __v }) + } + + /// Conway's Game of Life (B3/S23). + /// + /// Panics: + /// Panics unless the grid is at least 3×3. + /// + /// Rust: `fractals::automata::LifeLike::conway` + #[pyo3(name = "conway")] + #[staticmethod] + #[pyo3(signature = (w, h))] + fn conway(w: usize, h: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::automata::LifeLike::conway(w, h)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLifeLike { inner: __v }) + } + + /// Advances one generation. + /// + /// Rust: `fractals::automata::LifeLike::step` + #[pyo3(name = "step")] + #[pyo3(signature = ())] + fn step(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Advances `n` generations. + /// + /// Rust: `fractals::automata::LifeLike::run` + #[pyo3(name = "run")] + #[pyo3(signature = (n))] + fn run(&mut self, n: usize) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.run(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Number of live cells. + /// + /// Rust: `fractals::automata::LifeLike::population` + #[pyo3(name = "population")] + #[pyo3(signature = ())] + fn population(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.population()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Stamps a `.O`-style pattern with its top-left corner at + /// (x, y): 'O' or '*' set cells, everything else clears them. + /// + /// Rust: `fractals::automata::LifeLike::place` + #[pyo3(name = "place")] + #[pyo3(signature = (x, y, pattern))] + fn place<'py>(&mut self, py: Python<'py>, x: usize, y: usize, pattern: Vec) -> PyResult<()> { + let pattern__b: Vec<&str> = pattern.iter().map(|__b| (*__b).as_str()).collect(); + let __r = py.detach(move || crate::runtime::guard(move || self.inner.place(x, y, &pattern__b))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Stamps a run-length-encoded pattern (`bo$2bo$3o!` etc.): + /// b = dead, o = alive, $ = next row, ! = end, digits repeat. + /// + /// Errors: + /// `InvalidArgument` on unexpected characters. + /// + /// Rust: `fractals::automata::LifeLike::place_rle` + #[pyo3(name = "place_rle")] + #[pyo3(signature = (x, y, rle))] + fn place_rle(&mut self, x: usize, y: usize, rle: String) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.place_rle(x, y, &rle)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(()) + } + + /// Bounding rectangle of the live cells (cell centers), or + /// `None` when empty. + /// + /// Rust: `fractals::automata::LifeLike::bounding_box` + #[pyo3(name = "bounding_box")] + #[pyo3(signature = ())] + fn bounding_box(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.bounding_box()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyRect { inner: __x })) + } + + /// Steps a copy until the exact grid state recurs (oscillator or + /// still-life period; spaceships on a wrapped grid recur when + /// they lap the torus). `None` if no recurrence within + /// `max_steps`. + /// + /// Rust: `fractals::automata::LifeLike::detect_period` + #[pyo3(name = "detect_period")] + #[pyo3(signature = (max_steps))] + fn detect_period(&self, max_steps: usize) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.detect_period(max_steps)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) + } + + /// True when one step leaves the grid unchanged. + /// + /// Rust: `fractals::automata::LifeLike::is_still_life` + #[pyo3(name = "is_still_life")] + #[pyo3(signature = ())] + fn is_still_life(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_still_life()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Renders the grid as newline-separated `.O` rows. + /// + /// Rust: `fractals::automata::LifeLike::to_string` + #[pyo3(name = "to_string")] + #[pyo3(signature = ())] + fn to_string(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.to_string()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_string()) + } + + #[getter] + #[pyo3(name = "w")] + fn py_get_w(&self) -> PyResult { Ok(self.inner.w) } + + #[setter] + #[pyo3(name = "w")] + fn py_set_w(&mut self, v: usize) { self.inner.w = v; } + + #[getter] + #[pyo3(name = "h")] + fn py_get_h(&self) -> PyResult { Ok(self.inner.h) } + + #[setter] + #[pyo3(name = "h")] + fn py_set_h(&mut self, v: usize) { self.inner.h = v; } + + #[getter] + #[pyo3(name = "cells")] + fn py_get_cells(&self) -> PyResult> { Ok(self.inner.cells.clone()) } + + #[getter] + #[pyo3(name = "birth")] + fn py_get_birth(&self) -> PyResult> { Ok(self.inner.birth.clone().to_vec()) } + + #[getter] + #[pyo3(name = "survive")] + fn py_get_survive(&self) -> PyResult> { Ok(self.inner.survive.clone().to_vec()) } + + #[getter] + #[pyo3(name = "wrap")] + fn py_get_wrap(&self) -> PyResult { Ok(self.inner.wrap) } + + #[setter] + #[pyo3(name = "wrap")] + fn py_set_wrap(&mut self, v: bool) { self.inner.wrap = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("LifeLike", "LifeLike", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Life-like automaton on a 3-D grid with the 26-cell Moore +/// neighborhood and a B/S rule (e.g. "B5/S45" for Clouds-like +/// rules). +/// +/// Rust: `fractals::automata::LifeLike3D` +#[pyclass(name = "LifeLike3D", module = "numeria.fractals.automata", from_py_object)] +#[derive(Clone)] +pub struct PyLifeLike3D { pub inner: rust_physics_engine::fractals::automata::LifeLike3D } +#[pymethods] +impl PyLifeLike3D { + /// Builds a `LifeLike3D` from its fields. + #[new] + #[pyo3(signature = (w, h, d, cells, birth, survive))] + fn __new__(w: usize, h: usize, d: usize, cells: Vec, birth: Vec, survive: Vec) -> PyResult { + let birth = <[bool; 27]>::try_from(birth).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 27 values, got {}", __v.len())))?; + let survive = <[bool; 27]>::try_from(survive).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 27 values, got {}", __v.len())))?; + Ok(Self { inner: rust_physics_engine::fractals::automata::LifeLike3D { w: w, h: h, d: d, cells: cells, birth: birth, survive: survive } }) + } + + /// Parses `"B/S"` where counts are comma-free + /// digit runs; multi-digit counts (10-26) are written with + /// parentheses, e.g. "B(10)(11)/S(12)". + /// + /// Errors: + /// `InvalidArgument` on malformed rules. + /// + /// Rust: `fractals::automata::LifeLike3D::from_rule_string` + #[pyo3(name = "from_rule_string")] + #[staticmethod] + #[pyo3(signature = (w, h, d, rule))] + fn from_rule_string(w: usize, h: usize, d: usize, rule: String) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::automata::LifeLike3D::from_rule_string(w, h, d, &rule)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyLifeLike3D { inner: __v }) + } + + /// Advances one generation (toroidal). + /// + /// Rust: `fractals::automata::LifeLike3D::step` + #[pyo3(name = "step")] + #[pyo3(signature = ())] + fn step(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Number of live cells. + /// + /// Rust: `fractals::automata::LifeLike3D::population` + #[pyo3(name = "population")] + #[pyo3(signature = ())] + fn population(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.population()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "w")] + fn py_get_w(&self) -> PyResult { Ok(self.inner.w) } + + #[setter] + #[pyo3(name = "w")] + fn py_set_w(&mut self, v: usize) { self.inner.w = v; } + + #[getter] + #[pyo3(name = "h")] + fn py_get_h(&self) -> PyResult { Ok(self.inner.h) } + + #[setter] + #[pyo3(name = "h")] + fn py_set_h(&mut self, v: usize) { self.inner.h = v; } + + #[getter] + #[pyo3(name = "d")] + fn py_get_d(&self) -> PyResult { Ok(self.inner.d) } + + #[setter] + #[pyo3(name = "d")] + fn py_set_d(&mut self, v: usize) { self.inner.d = v; } + + #[getter] + #[pyo3(name = "cells")] + fn py_get_cells(&self) -> PyResult> { Ok(self.inner.cells.clone()) } + + #[getter] + #[pyo3(name = "birth")] + fn py_get_birth(&self) -> PyResult> { Ok(self.inner.birth.clone().to_vec()) } + + #[getter] + #[pyo3(name = "survive")] + fn py_get_survive(&self) -> PyResult> { Ok(self.inner.survive.clone().to_vec()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("LifeLike3D", "LifeLike3D", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// SmoothLife: a continuous-state, continuous-neighborhood +/// generalization of Life, integrated by direct convolution (small +/// grids; no FFT dependency). +/// +/// Rust: `fractals::automata::SmoothLife` +#[pyclass(name = "SmoothLife", module = "numeria.fractals.automata", from_py_object)] +#[derive(Clone)] +pub struct PySmoothLife { pub inner: rust_physics_engine::fractals::automata::SmoothLife } +#[pymethods] +impl PySmoothLife { + /// Blank field. + /// + /// Panics: + /// Panics unless the grid is at least 8×8. + /// + /// Rust: `fractals::automata::SmoothLife::new` + #[new] + #[pyo3(signature = (w, h, params))] + fn __new__(w: usize, h: usize, params: crate::generated::types::PySmoothLifeParams) -> PyResult { + let params = params.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::automata::SmoothLife::new(w, h, params)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySmoothLife { inner: __v }) + } + + /// One smooth timestep of size `dt` (forward Euler on the + /// transition function). + /// + /// Rust: `fractals::automata::SmoothLife::step` + #[pyo3(name = "step")] + #[pyo3(signature = (dt))] + fn step(&mut self, dt: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step(dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + #[getter] + #[pyo3(name = "w")] + fn py_get_w(&self) -> PyResult { Ok(self.inner.w) } + + #[setter] + #[pyo3(name = "w")] + fn py_set_w(&mut self, v: usize) { self.inner.w = v; } + + #[getter] + #[pyo3(name = "h")] + fn py_get_h(&self) -> PyResult { Ok(self.inner.h) } + + #[setter] + #[pyo3(name = "h")] + fn py_set_h(&mut self, v: usize) { self.inner.h = v; } + + #[getter] + #[pyo3(name = "field")] + fn py_get_field(&self) -> PyResult> { Ok(self.inner.field.clone()) } + + #[getter] + #[pyo3(name = "params")] + fn py_get_params(&self) -> PyResult { Ok(crate::generated::types::PySmoothLifeParams { inner: self.inner.params.clone() }) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("SmoothLife", "SmoothLife", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// SmoothLife parameters (Rafler 2011): inner/outer disc radii and +/// the birth/death sigmoid intervals. +/// +/// Rust: `fractals::automata::SmoothLifeParams` +#[pyclass(name = "SmoothLifeParams", module = "numeria.fractals.automata", from_py_object)] +#[derive(Clone)] +pub struct PySmoothLifeParams { pub inner: rust_physics_engine::fractals::automata::SmoothLifeParams } +#[pymethods] +impl PySmoothLifeParams { + /// Builds a `SmoothLifeParams` from its fields. + #[new] + #[pyo3(signature = (inner_radius, outer_radius, b1, b2, d1, d2, alpha_n, alpha_m))] + fn __new__(inner_radius: f64, outer_radius: f64, b1: f64, b2: f64, d1: f64, d2: f64, alpha_n: f64, alpha_m: f64) -> Self { + + Self { inner: rust_physics_engine::fractals::automata::SmoothLifeParams { inner_radius: inner_radius, outer_radius: outer_radius, b1: b1, b2: b2, d1: d1, d2: d2, alpha_n: alpha_n, alpha_m: alpha_m } } + } + + #[getter] + #[pyo3(name = "inner_radius")] + fn py_get_inner_radius(&self) -> PyResult { Ok(self.inner.inner_radius) } + + #[setter] + #[pyo3(name = "inner_radius")] + fn py_set_inner_radius(&mut self, v: f64) { self.inner.inner_radius = v; } + + #[getter] + #[pyo3(name = "outer_radius")] + fn py_get_outer_radius(&self) -> PyResult { Ok(self.inner.outer_radius) } + + #[setter] + #[pyo3(name = "outer_radius")] + fn py_set_outer_radius(&mut self, v: f64) { self.inner.outer_radius = v; } + + #[getter] + #[pyo3(name = "b1")] + fn py_get_b1(&self) -> PyResult { Ok(self.inner.b1) } + + #[setter] + #[pyo3(name = "b1")] + fn py_set_b1(&mut self, v: f64) { self.inner.b1 = v; } + + #[getter] + #[pyo3(name = "b2")] + fn py_get_b2(&self) -> PyResult { Ok(self.inner.b2) } + + #[setter] + #[pyo3(name = "b2")] + fn py_set_b2(&mut self, v: f64) { self.inner.b2 = v; } + + #[getter] + #[pyo3(name = "d1")] + fn py_get_d1(&self) -> PyResult { Ok(self.inner.d1) } + + #[setter] + #[pyo3(name = "d1")] + fn py_set_d1(&mut self, v: f64) { self.inner.d1 = v; } + + #[getter] + #[pyo3(name = "d2")] + fn py_get_d2(&self) -> PyResult { Ok(self.inner.d2) } + + #[setter] + #[pyo3(name = "d2")] + fn py_set_d2(&mut self, v: f64) { self.inner.d2 = v; } + + #[getter] + #[pyo3(name = "alpha_n")] + fn py_get_alpha_n(&self) -> PyResult { Ok(self.inner.alpha_n) } + + #[setter] + #[pyo3(name = "alpha_n")] + fn py_set_alpha_n(&mut self, v: f64) { self.inner.alpha_n = v; } + + #[getter] + #[pyo3(name = "alpha_m")] + fn py_get_alpha_m(&self) -> PyResult { Ok(self.inner.alpha_m) } + + #[setter] + #[pyo3(name = "alpha_m")] + fn py_set_alpha_m(&mut self, v: f64) { self.inner.alpha_m = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("SmoothLifeParams", "SmoothLifeParams", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Gierer-Meinhardt activator-inhibitor system: +/// ∂a = Dₐ∇²a + a²/h − μa + ρ, ∂h = Dₕ∇²h + a² − νh. +/// +/// Rust: `fractals::automata::Turing` +#[pyclass(name = "Turing", module = "numeria.fractals.automata", from_py_object)] +#[derive(Clone)] +pub struct PyTuring { pub inner: rust_physics_engine::fractals::automata::Turing } +#[pymethods] +impl PyTuring { + /// Near-homogeneous start with small random perturbations. + /// + /// Panics: + /// Panics unless the grid is at least 3×3. + /// + /// Rust: `fractals::automata::Turing::new` + #[new] + #[pyo3(signature = (w, h, rng))] + fn __new__(w: usize, h: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::automata::Turing::new(w, h, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTuring { inner: __v }) + } + + /// One forward-Euler step. + /// + /// Rust: `fractals::automata::Turing::step` + #[pyo3(name = "step")] + #[pyo3(signature = ())] + fn step(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + #[getter] + #[pyo3(name = "w")] + fn py_get_w(&self) -> PyResult { Ok(self.inner.w) } + + #[setter] + #[pyo3(name = "w")] + fn py_set_w(&mut self, v: usize) { self.inner.w = v; } + + #[getter] + #[pyo3(name = "h")] + fn py_get_h(&self) -> PyResult { Ok(self.inner.h) } + + #[setter] + #[pyo3(name = "h")] + fn py_set_h(&mut self, v: usize) { self.inner.h = v; } + + #[getter] + #[pyo3(name = "activator")] + fn py_get_activator(&self) -> PyResult> { Ok(self.inner.activator.clone()) } + + #[getter] + #[pyo3(name = "inhibitor")] + fn py_get_inhibitor(&self) -> PyResult> { Ok(self.inner.inhibitor.clone()) } + + #[getter] + #[pyo3(name = "da")] + fn py_get_da(&self) -> PyResult { Ok(self.inner.da) } + + #[setter] + #[pyo3(name = "da")] + fn py_set_da(&mut self, v: f64) { self.inner.da = v; } + + #[getter] + #[pyo3(name = "dh")] + fn py_get_dh(&self) -> PyResult { Ok(self.inner.dh) } + + #[setter] + #[pyo3(name = "dh")] + fn py_set_dh(&mut self, v: f64) { self.inner.dh = v; } + + #[getter] + #[pyo3(name = "mu")] + fn py_get_mu(&self) -> PyResult { Ok(self.inner.mu) } + + #[setter] + #[pyo3(name = "mu")] + fn py_set_mu(&mut self, v: f64) { self.inner.mu = v; } + + #[getter] + #[pyo3(name = "nu")] + fn py_get_nu(&self) -> PyResult { Ok(self.inner.nu) } + + #[setter] + #[pyo3(name = "nu")] + fn py_set_nu(&mut self, v: f64) { self.inner.nu = v; } + + #[getter] + #[pyo3(name = "rho")] + fn py_get_rho(&self) -> PyResult { Ok(self.inner.rho) } + + #[setter] + #[pyo3(name = "rho")] + fn py_set_rho(&mut self, v: f64) { self.inner.rho = v; } + + #[getter] + #[pyo3(name = "dt")] + fn py_get_dt(&self) -> PyResult { Ok(self.inner.dt) } + + #[setter] + #[pyo3(name = "dt")] + fn py_set_dt(&mut self, v: f64) { self.inner.dt = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Turing", "Turing", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A turmite: a two-dimensional Turing machine on cell colors. The +/// transition table maps (machine state, cell color) to (color to +/// write, turn in quarter-turns clockwise, next state). +/// +/// Rust: `fractals::automata::Turmite` +#[pyclass(name = "Turmite", module = "numeria.fractals.automata", from_py_object)] +#[derive(Clone)] +pub struct PyTurmite { pub inner: rust_physics_engine::fractals::automata::Turmite } +#[pymethods] +impl PyTurmite { + /// Turmite at the center of a blank toroidal grid. + /// + /// Panics: + /// Panics on an empty transition table or a grid under 3×3. + /// + /// Rust: `fractals::automata::Turmite::new` + #[new] + #[pyo3(signature = (w, h, table))] + fn __new__(w: usize, h: usize, table: Vec>) -> PyResult { + let table = table.into_iter().map(|__e| __e.into_iter().map(|__e| (__e.0, __e.1, __e.2)).collect::>()).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::automata::Turmite::new(w, h, table)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTurmite { inner: __v }) + } + + /// One machine step. + /// + /// Rust: `fractals::automata::Turmite::step` + #[pyo3(name = "step")] + #[pyo3(signature = ())] + fn step(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Runs `n` steps. + /// + /// Rust: `fractals::automata::Turmite::run` + #[pyo3(name = "run")] + #[pyo3(signature = (n))] + fn run(&mut self, n: usize) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.run(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + #[getter] + #[pyo3(name = "w")] + fn py_get_w(&self) -> PyResult { Ok(self.inner.w) } + + #[setter] + #[pyo3(name = "w")] + fn py_set_w(&mut self, v: usize) { self.inner.w = v; } + + #[getter] + #[pyo3(name = "h")] + fn py_get_h(&self) -> PyResult { Ok(self.inner.h) } + + #[setter] + #[pyo3(name = "h")] + fn py_set_h(&mut self, v: usize) { self.inner.h = v; } + + #[getter] + #[pyo3(name = "cells")] + fn py_get_cells(&self) -> PyResult> { Ok(self.inner.cells.clone()) } + + #[getter] + #[pyo3(name = "pos")] + fn py_get_pos(&self) -> PyResult<(usize, usize)> { Ok((self.inner.pos.clone().0, self.inner.pos.clone().1)) } + + #[getter] + #[pyo3(name = "dir")] + fn py_get_dir(&self) -> PyResult { Ok(self.inner.dir) } + + #[setter] + #[pyo3(name = "dir")] + fn py_set_dir(&mut self, v: u8) { self.inner.dir = v; } + + #[getter] + #[pyo3(name = "state")] + fn py_get_state(&self) -> PyResult { Ok(self.inner.state) } + + #[setter] + #[pyo3(name = "state")] + fn py_set_state(&mut self, v: u8) { self.inner.state = v; } + + #[getter] + #[pyo3(name = "table")] + fn py_get_table(&self) -> PyResult>> { Ok(self.inner.table.clone().into_iter().map(|__x| __x.into_iter().map(|__x| (__x.0, __x.1, __x.2)).collect::>()).collect::>()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Turmite", "Turmite", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Wireworld: 0 empty, 1 electron head, 2 electron tail, +/// 3 conductor. Heads become tails, tails become conductor, and a +/// conductor becomes a head with one or two neighboring heads. +/// +/// Rust: `fractals::automata::Wireworld` +#[pyclass(name = "Wireworld", module = "numeria.fractals.automata", from_py_object)] +#[derive(Clone)] +pub struct PyWireworld { pub inner: rust_physics_engine::fractals::automata::Wireworld } +#[pymethods] +impl PyWireworld { + /// Builds a `Wireworld` from its fields. + #[new] + #[pyo3(signature = (w, h, cells))] + fn __new__(w: usize, h: usize, cells: Vec) -> Self { + + Self { inner: rust_physics_engine::fractals::automata::Wireworld { w: w, h: h, cells: cells } } + } + + /// Parses a circuit: ' ' or '.' empty, 'C' or '#' conductor, + /// 'H' electron head, 'T' electron tail. Rows are padded to the + /// longest line. + /// + /// Errors: + /// `InvalidArgument` on unknown characters or an empty diagram. + /// + /// Rust: `fractals::automata::Wireworld::from_string` + #[pyo3(name = "from_string")] + #[staticmethod] + #[pyo3(signature = (diagram))] + fn from_string(diagram: String) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::automata::Wireworld::from_string(&diagram)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyWireworld { inner: __v }) + } + + /// Advances one generation (non-wrapping). + /// + /// Rust: `fractals::automata::Wireworld::step` + #[pyo3(name = "step")] + #[pyo3(signature = ())] + fn step(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Advances `n` generations. + /// + /// Rust: `fractals::automata::Wireworld::run` + #[pyo3(name = "run")] + #[pyo3(signature = (n))] + fn run(&mut self, n: usize) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.run(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Number of electron heads on the board. + /// + /// Rust: `fractals::automata::Wireworld::count_electrons` + #[pyo3(name = "count_electrons")] + #[pyo3(signature = ())] + fn count_electrons(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.count_electrons()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "w")] + fn py_get_w(&self) -> PyResult { Ok(self.inner.w) } + + #[setter] + #[pyo3(name = "w")] + fn py_set_w(&mut self, v: usize) { self.inner.w = v; } + + #[getter] + #[pyo3(name = "h")] + fn py_get_h(&self) -> PyResult { Ok(self.inner.h) } + + #[setter] + #[pyo3(name = "h")] + fn py_set_h(&mut self, v: usize) { self.inner.h = v; } + + #[getter] + #[pyo3(name = "cells")] + fn py_get_cells(&self) -> PyResult> { Ok(self.inner.cells.clone()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Wireworld", "Wireworld", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Iteration parameters. +/// +/// Rust: `fractals::escape_time::EscapeParams` +#[pyclass(name = "EscapeParams", module = "numeria.fractals.escape_time", from_py_object)] +#[derive(Clone)] +pub struct PyEscapeParams { pub inner: rust_physics_engine::fractals::escape_time::EscapeParams } +#[pymethods] +impl PyEscapeParams { + /// Builds a `EscapeParams` from its fields. + #[new] + #[pyo3(signature = (max_iter, bailout, compute_distance, trap))] + fn __new__(max_iter: u32, bailout: f64, compute_distance: bool, trap: Option) -> Self { + let trap = trap.map(|__o| __o.inner); + Self { inner: rust_physics_engine::fractals::escape_time::EscapeParams { max_iter: max_iter, bailout: bailout, compute_distance: compute_distance, trap: trap } } + } + + #[getter] + #[pyo3(name = "max_iter")] + fn py_get_max_iter(&self) -> PyResult { Ok(self.inner.max_iter) } + + #[setter] + #[pyo3(name = "max_iter")] + fn py_set_max_iter(&mut self, v: u32) { self.inner.max_iter = v; } + + #[getter] + #[pyo3(name = "bailout")] + fn py_get_bailout(&self) -> PyResult { Ok(self.inner.bailout) } + + #[setter] + #[pyo3(name = "bailout")] + fn py_set_bailout(&mut self, v: f64) { self.inner.bailout = v; } + + #[getter] + #[pyo3(name = "compute_distance")] + fn py_get_compute_distance(&self) -> PyResult { Ok(self.inner.compute_distance) } + + #[setter] + #[pyo3(name = "compute_distance")] + fn py_set_compute_distance(&mut self, v: bool) { self.inner.compute_distance = v; } + + #[getter] + #[pyo3(name = "trap")] + fn py_get_trap(&self) -> PyResult> { Ok(self.inner.trap.clone().map(|__x| crate::generated::types::PyOrbitTrap { inner: __x })) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("EscapeParams", "EscapeParams", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Result of iterating one point. +/// +/// Rust: `fractals::escape_time::EscapeResult` +#[pyclass(name = "EscapeResult", module = "numeria.fractals.escape_time", from_py_object)] +#[derive(Clone)] +pub struct PyEscapeResult { pub inner: rust_physics_engine::fractals::escape_time::EscapeResult } +#[pymethods] +impl PyEscapeResult { + /// Builds a `EscapeResult` from its fields. + #[new] + #[pyo3(signature = (iterations, escaped, smooth, final_z, distance, orbit_trap))] + fn __new__(iterations: u32, escaped: bool, smooth: f64, final_z: crate::runtime::coerce::ComplexArg, distance: Option, orbit_trap: Option) -> Self { + let final_z = final_z.0; + Self { inner: rust_physics_engine::fractals::escape_time::EscapeResult { iterations: iterations, escaped: escaped, smooth: smooth, final_z: final_z, distance: distance, orbit_trap: orbit_trap } } + } + + #[getter] + #[pyo3(name = "iterations")] + fn py_get_iterations(&self) -> PyResult { Ok(self.inner.iterations) } + + #[setter] + #[pyo3(name = "iterations")] + fn py_set_iterations(&mut self, v: u32) { self.inner.iterations = v; } + + #[getter] + #[pyo3(name = "escaped")] + fn py_get_escaped(&self) -> PyResult { Ok(self.inner.escaped) } + + #[setter] + #[pyo3(name = "escaped")] + fn py_set_escaped(&mut self, v: bool) { self.inner.escaped = v; } + + #[getter] + #[pyo3(name = "smooth")] + fn py_get_smooth(&self) -> PyResult { Ok(self.inner.smooth) } + + #[setter] + #[pyo3(name = "smooth")] + fn py_set_smooth(&mut self, v: f64) { self.inner.smooth = v; } + + #[getter] + #[pyo3(name = "final_z")] + fn py_get_final_z<'py>(&self, py: Python<'py>) -> PyResult> { Ok(crate::runtime::coerce::complex_out(py, self.inner.final_z.clone())) } + + #[getter] + #[pyo3(name = "distance")] + fn py_get_distance(&self) -> PyResult> { Ok(self.inner.distance.clone().map(|__x| __x)) } + + #[getter] + #[pyo3(name = "orbit_trap")] + fn py_get_orbit_trap(&self) -> PyResult> { Ok(self.inner.orbit_trap.clone().map(|__x| __x)) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("EscapeResult", "EscapeResult", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Orbit trap shapes: the result records the minimum distance from +/// the orbit to the trap. +/// +/// Rust: `fractals::escape_time::OrbitTrap` +#[pyclass(name = "OrbitTrap", module = "numeria.fractals.escape_time", from_py_object)] +#[derive(Clone)] +pub struct PyOrbitTrap { pub inner: rust_physics_engine::fractals::escape_time::OrbitTrap } +#[pymethods] +impl PyOrbitTrap { + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("OrbitTrap", "OrbitTrap", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A 2-D iterated function system: contractive affine maps with +/// selection probabilities. +/// +/// Rust: `fractals::ifs::Ifs` +#[pyclass(name = "Ifs", module = "numeria.fractals.ifs", from_py_object)] +#[derive(Clone)] +pub struct PyIfs { pub inner: rust_physics_engine::fractals::ifs::Ifs } +#[pymethods] +impl PyIfs { + /// New system; probabilities are normalized to sum to 1. + /// + /// Panics: + /// Panics unless at least one map has positive probability. + /// + /// Rust: `fractals::ifs::Ifs::new` + #[new] + #[pyo3(signature = (maps))] + fn __new__(maps: Vec<(crate::generated::types::PyAffine2, f64)>) -> PyResult { + let maps = maps.into_iter().map(|__e| (__e.0.inner, __e.1)).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::ifs::Ifs::new(maps)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyIfs { inner: __v }) + } + + /// The chaos game: iterate randomly chosen maps from the origin, + /// discarding the first `burn_in` points, and return the next `n` + /// points (which lie on the attractor to within the contraction + /// tolerance). + /// + /// Rust: `fractals::ifs::Ifs::chaos_game` + #[pyo3(name = "chaos_game")] + #[pyo3(signature = (n, burn_in, rng))] + fn chaos_game(&self, n: usize, burn_in: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| self.inner.chaos_game(n, burn_in, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) + } + + /// Chaos game keeping the index of the map that produced each + /// point (for per-map coloring). + /// + /// Rust: `fractals::ifs::Ifs::chaos_game_colored` + #[pyo3(name = "chaos_game_colored")] + #[pyo3(signature = (n, burn_in, rng))] + fn chaos_game_colored(&self, n: usize, burn_in: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| self.inner.chaos_game_colored(n, burn_in, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (crate::generated::types::PyVec2 { inner: __x.0 }, __x.1)).collect::>()) + } + + /// Deterministic construction: applies every map to every + /// polygon, `depth` times, starting from `seed` — the m^depth + /// results converge to the attractor in Hausdorff distance. + /// + /// Panics: + /// Panics when m^depth would exceed 10^6 polygons. + /// + /// Rust: `fractals::ifs::Ifs::deterministic` + #[pyo3(name = "deterministic")] + #[pyo3(signature = (depth, seed))] + fn deterministic(&self, depth: usize, seed: crate::generated::types::PyPolygon2) -> PyResult> { + let seed = seed.inner; + let __r = crate::runtime::guard(|| self.inner.deterministic(depth, &seed)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyPolygon2 { inner: __x }).collect::>()) + } + + /// Deterministic point construction: all depth-fold compositions + /// applied to the fixed point of the first map. + /// + /// Panics: + /// Panics when m^depth would exceed 10^6 points. + /// + /// Rust: `fractals::ifs::Ifs::deterministic_points` + #[pyo3(name = "deterministic_points")] + #[pyo3(signature = (depth))] + fn deterministic_points(&self, depth: usize) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.deterministic_points(depth)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) + } + + /// Moran similarity dimension: the d solving Σ rᵢ^d = 1 where rᵢ + /// are the contraction ratios, valid when every map is a + /// similitude (uniform scale × rotation ± reflection) with + /// ratio < 1. Returns `None` otherwise. Solved by bisection. + /// + /// Rust: `fractals::ifs::Ifs::similarity_dimension` + #[pyo3(name = "similarity_dimension")] + #[pyo3(signature = ())] + fn similarity_dimension(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.similarity_dimension()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) + } + + /// Bounding rectangle of `n` chaos-game samples, padded by 1%. + /// + /// Rust: `fractals::ifs::Ifs::bounding_rect` + #[pyo3(name = "bounding_rect")] + #[pyo3(signature = (n, rng))] + fn bounding_rect(&self, n: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| self.inner.bounding_rect(n, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyRect { inner: __v }) + } + + /// Collage error: the symmetric Hausdorff distance between the + /// target point set and the union of its images under the maps. + /// The collage theorem bounds the distance from the target to the + /// attractor by error/(1 − s) for contractivity s. + /// + /// Panics: + /// Panics on an empty target. + /// + /// Rust: `fractals::ifs::Ifs::collage_error` + #[pyo3(name = "collage_error")] + #[pyo3(signature = (target))] + fn collage_error<'py>(&self, py: Python<'py>, target: Vec) -> PyResult { + let target = target.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || self.inner.collage_error(&target))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Renders `n` chaos-game samples into a `res.0` × `res.1` hit + /// count grid (row-major, y up) over the attractor's bounding + /// rectangle. + /// + /// Panics: + /// Panics on a zero-sized grid. + /// + /// Rust: `fractals::ifs::Ifs::render_density` + #[pyo3(name = "render_density")] + #[pyo3(signature = (n, res, rng))] + fn render_density(&self, n: usize, res: (usize, usize), rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let res = (res.0, res.1); + let mut rng = rng; + let __r = crate::runtime::guard(|| self.inner.render_density(n, res, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "maps")] + fn py_get_maps(&self) -> PyResult> { Ok(self.inner.maps.clone().into_iter().map(|__x| (crate::generated::types::PyAffine2 { inner: __x.0 }, __x.1)).collect::>()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Ifs", "Ifs", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A 3-D IFS with affine maps stored as `Mat4`. +/// +/// Rust: `fractals::ifs::Ifs3` +#[pyclass(name = "Ifs3", module = "numeria.fractals.ifs", from_py_object)] +#[derive(Clone)] +pub struct PyIfs3 { pub inner: rust_physics_engine::fractals::ifs::Ifs3 } +#[pymethods] +impl PyIfs3 { + /// New system; probabilities are normalized to sum to 1. + /// + /// Panics: + /// Panics unless at least one map has positive probability. + /// + /// Rust: `fractals::ifs::Ifs3::new` + #[new] + #[pyo3(signature = (maps))] + fn __new__(maps: Vec<(crate::generated::types::PyMat4Mat4, f64)>) -> PyResult { + let maps = maps.into_iter().map(|__e| (__e.0.inner, __e.1)).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::ifs::Ifs3::new(maps)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyIfs3 { inner: __v }) + } + + /// The chaos game in 3-D. + /// + /// Rust: `fractals::ifs::Ifs3::chaos_game` + #[pyo3(name = "chaos_game")] + #[pyo3(signature = (n, burn_in, rng))] + fn chaos_game(&self, n: usize, burn_in: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| self.inner.chaos_game(n, burn_in, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) + } + + /// All depth-fold compositions applied to the origin. + /// + /// Panics: + /// Panics when m^depth would exceed 10^6 points. + /// + /// Rust: `fractals::ifs::Ifs3::deterministic_points` + #[pyo3(name = "deterministic_points")] + #[pyo3(signature = (depth))] + fn deterministic_points(&self, depth: usize) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.deterministic_points(depth)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) + } + + #[getter] + #[pyo3(name = "maps")] + fn py_get_maps(&self) -> PyResult> { Ok(self.inner.maps.clone().into_iter().map(|__x| (crate::generated::types::PyMat4Mat4 { inner: __x.0 }, __x.1)).collect::>()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Ifs3", "Ifs3", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// The nonlinear variations of Draves & Reckase, "The Fractal Flame +/// Algorithm". Variations with free parameters use the fixed values +/// noted below; Julia uses the Ω = 0 branch so results are +/// deterministic. +/// +/// Rust: `fractals::ifs::Variation` +#[pyclass(name = "Variation", module = "numeria.fractals.ifs", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyVariation { + Linear, + Sinusoidal, + Spherical, + Swirl, + Horseshoe, + Polar, + Handkerchief, + Heart, + Disc, + Spiral, + Hyperbolic, + Diamond, + Ex, + Julia, + Bent, + Waves, + Fisheye, + Popcorn, + Exponential, + Power, + Cosine, + Rings, + Fan, +} +impl PyVariation { + pub fn to_rust(&self) -> rust_physics_engine::fractals::ifs::Variation { match self { + Self::Linear => rust_physics_engine::fractals::ifs::Variation::Linear, + Self::Sinusoidal => rust_physics_engine::fractals::ifs::Variation::Sinusoidal, + Self::Spherical => rust_physics_engine::fractals::ifs::Variation::Spherical, + Self::Swirl => rust_physics_engine::fractals::ifs::Variation::Swirl, + Self::Horseshoe => rust_physics_engine::fractals::ifs::Variation::Horseshoe, + Self::Polar => rust_physics_engine::fractals::ifs::Variation::Polar, + Self::Handkerchief => rust_physics_engine::fractals::ifs::Variation::Handkerchief, + Self::Heart => rust_physics_engine::fractals::ifs::Variation::Heart, + Self::Disc => rust_physics_engine::fractals::ifs::Variation::Disc, + Self::Spiral => rust_physics_engine::fractals::ifs::Variation::Spiral, + Self::Hyperbolic => rust_physics_engine::fractals::ifs::Variation::Hyperbolic, + Self::Diamond => rust_physics_engine::fractals::ifs::Variation::Diamond, + Self::Ex => rust_physics_engine::fractals::ifs::Variation::Ex, + Self::Julia => rust_physics_engine::fractals::ifs::Variation::Julia, + Self::Bent => rust_physics_engine::fractals::ifs::Variation::Bent, + Self::Waves => rust_physics_engine::fractals::ifs::Variation::Waves, + Self::Fisheye => rust_physics_engine::fractals::ifs::Variation::Fisheye, + Self::Popcorn => rust_physics_engine::fractals::ifs::Variation::Popcorn, + Self::Exponential => rust_physics_engine::fractals::ifs::Variation::Exponential, + Self::Power => rust_physics_engine::fractals::ifs::Variation::Power, + Self::Cosine => rust_physics_engine::fractals::ifs::Variation::Cosine, + Self::Rings => rust_physics_engine::fractals::ifs::Variation::Rings, + Self::Fan => rust_physics_engine::fractals::ifs::Variation::Fan, + } } + pub fn from_rust(v: &rust_physics_engine::fractals::ifs::Variation) -> Self { match v { + rust_physics_engine::fractals::ifs::Variation::Linear => Self::Linear, + rust_physics_engine::fractals::ifs::Variation::Sinusoidal => Self::Sinusoidal, + rust_physics_engine::fractals::ifs::Variation::Spherical => Self::Spherical, + rust_physics_engine::fractals::ifs::Variation::Swirl => Self::Swirl, + rust_physics_engine::fractals::ifs::Variation::Horseshoe => Self::Horseshoe, + rust_physics_engine::fractals::ifs::Variation::Polar => Self::Polar, + rust_physics_engine::fractals::ifs::Variation::Handkerchief => Self::Handkerchief, + rust_physics_engine::fractals::ifs::Variation::Heart => Self::Heart, + rust_physics_engine::fractals::ifs::Variation::Disc => Self::Disc, + rust_physics_engine::fractals::ifs::Variation::Spiral => Self::Spiral, + rust_physics_engine::fractals::ifs::Variation::Hyperbolic => Self::Hyperbolic, + rust_physics_engine::fractals::ifs::Variation::Diamond => Self::Diamond, + rust_physics_engine::fractals::ifs::Variation::Ex => Self::Ex, + rust_physics_engine::fractals::ifs::Variation::Julia => Self::Julia, + rust_physics_engine::fractals::ifs::Variation::Bent => Self::Bent, + rust_physics_engine::fractals::ifs::Variation::Waves => Self::Waves, + rust_physics_engine::fractals::ifs::Variation::Fisheye => Self::Fisheye, + rust_physics_engine::fractals::ifs::Variation::Popcorn => Self::Popcorn, + rust_physics_engine::fractals::ifs::Variation::Exponential => Self::Exponential, + rust_physics_engine::fractals::ifs::Variation::Power => Self::Power, + rust_physics_engine::fractals::ifs::Variation::Cosine => Self::Cosine, + rust_physics_engine::fractals::ifs::Variation::Rings => Self::Rings, + rust_physics_engine::fractals::ifs::Variation::Fan => Self::Fan, + } } +} +#[pymethods] +impl PyVariation { + fn __repr__(&self) -> &'static str { + match self { + Self::Linear => "Variation.Linear", + Self::Sinusoidal => "Variation.Sinusoidal", + Self::Spherical => "Variation.Spherical", + Self::Swirl => "Variation.Swirl", + Self::Horseshoe => "Variation.Horseshoe", + Self::Polar => "Variation.Polar", + Self::Handkerchief => "Variation.Handkerchief", + Self::Heart => "Variation.Heart", + Self::Disc => "Variation.Disc", + Self::Spiral => "Variation.Spiral", + Self::Hyperbolic => "Variation.Hyperbolic", + Self::Diamond => "Variation.Diamond", + Self::Ex => "Variation.Ex", + Self::Julia => "Variation.Julia", + Self::Bent => "Variation.Bent", + Self::Waves => "Variation.Waves", + Self::Fisheye => "Variation.Fisheye", + Self::Popcorn => "Variation.Popcorn", + Self::Exponential => "Variation.Exponential", + Self::Power => "Variation.Power", + Self::Cosine => "Variation.Cosine", + Self::Rings => "Variation.Rings", + Self::Fan => "Variation.Fan", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A Lindenmayer system: axiom, production rules, the turtle turn +/// angle its drawings use (radians), and characters ignored during +/// context matching. +/// +/// Rust: `fractals::lsystem::LSystem` +#[pyclass(name = "LSystem", module = "numeria.fractals.lsystem", from_py_object)] +#[derive(Clone)] +pub struct PyLSystem { pub inner: rust_physics_engine::fractals::lsystem::LSystem } +#[pymethods] +impl PyLSystem { + /// New system with the given axiom and turtle angle in degrees. + /// + /// Rust: `fractals::lsystem::LSystem::new` + #[new] + #[pyo3(signature = (axiom, angle_deg))] + fn __new__(axiom: String, angle_deg: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::lsystem::LSystem::new(&axiom, angle_deg)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLSystem { inner: __v }) + } + + /// Adds a simple rule (builder style). + /// + /// Rust: `fractals::lsystem::LSystem::rule` + #[pyo3(name = "rule")] + #[pyo3(signature = (from_, to))] + fn rule(&self, from_: char, to: String) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.clone().rule(from_, &to)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLSystem { inner: __v }) + } + + /// Adds a stochastic rule with relative probabilities. + /// + /// Panics: + /// Panics if no option has positive probability. + /// + /// Rust: `fractals::lsystem::LSystem::stochastic_rule` + #[pyo3(name = "stochastic_rule")] + #[pyo3(signature = (from_, options))] + fn stochastic_rule(&self, from_: char, options: Vec<(f64, String)>) -> PyResult { + let options = options.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let options__b: Vec<(f64, &str)> = options.iter().map(|__b| ((*__b).0, (*__b).1.as_str())).collect(); + let __r = crate::runtime::guard(|| self.inner.clone().stochastic_rule(from_, &options__b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLSystem { inner: __v }) + } + + #[getter] + #[pyo3(name = "axiom")] + fn py_get_axiom(&self) -> PyResult { Ok(self.inner.axiom.to_string()) } + + #[getter] + #[pyo3(name = "rules")] + fn py_get_rules(&self) -> PyResult> { Ok(self.inner.rules.clone().into_iter().map(|__x| crate::generated::types::PyRule { inner: __x }).collect::>()) } + + #[getter] + #[pyo3(name = "angle")] + fn py_get_angle(&self) -> PyResult { Ok(self.inner.angle) } + + #[setter] + #[pyo3(name = "angle")] + fn py_set_angle(&mut self, v: f64) { self.inner.angle = v; } + + #[getter] + #[pyo3(name = "ignore")] + fn py_get_ignore(&self) -> PyResult> { Ok(self.inner.ignore.clone()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("LSystem", "LSystem", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A production rule. +/// +/// Rust: `fractals::lsystem::Rule` +#[pyclass(name = "Rule", module = "numeria.fractals.lsystem", from_py_object)] +#[derive(Clone)] +pub struct PyRule { pub inner: rust_physics_engine::fractals::lsystem::Rule } +#[pymethods] +impl PyRule { + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Rule", "Rule", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// 2-D turtle interpreting the ABOP alphabet: `F`/`G` draw a step, +/// `f`/`g` move without drawing, `+`/`-` turn left/right by the +/// turn angle, `|` turns 180°, ``/`` push/pop state, `!` scales +/// the line width by `width_factor`. Other characters are ignored. +/// +/// Rust: `fractals::lsystem::Turtle2` +#[pyclass(name = "Turtle2", module = "numeria.fractals.lsystem", from_py_object)] +#[derive(Clone)] +pub struct PyTurtle2 { pub inner: rust_physics_engine::fractals::lsystem::Turtle2 } +#[pymethods] +impl PyTurtle2 { + /// Turtle at the origin heading +x. + /// + /// Panics: + /// Panics unless `step > 0`. + /// + /// Rust: `fractals::lsystem::Turtle2::new` + #[new] + #[pyo3(signature = (step, angle_deg))] + fn __new__(step: f64, angle_deg: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::lsystem::Turtle2::new(step, angle_deg)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTurtle2 { inner: __v }) + } + + /// Interprets the string, returning the drawn segments. + /// + /// Rust: `fractals::lsystem::Turtle2::interpret` + #[pyo3(name = "interpret")] + #[pyo3(signature = (s))] + fn interpret(&mut self, s: String) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.interpret(&s)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyPrimitivesSegment2 { inner: __x }).collect::>()) + } + + /// Interprets the string, returning segments with the line width + /// active while each was drawn. + /// + /// Rust: `fractals::lsystem::Turtle2::interpret_with_width` + #[pyo3(name = "interpret_with_width")] + #[pyo3(signature = (s))] + fn interpret_with_width(&mut self, s: String) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.interpret_with_width(&s)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (crate::generated::types::PyPrimitivesSegment2 { inner: __x.0 }, __x.1)).collect::>()) + } + + /// Bounding rectangle of every position visited so far. + /// + /// Rust: `fractals::lsystem::Turtle2::bounds` + #[pyo3(name = "bounds")] + #[pyo3(signature = ())] + fn bounds(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.bounds()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyRect { inner: __v }) + } + + #[getter] + #[pyo3(name = "pos")] + fn py_get_pos(&self) -> PyResult { Ok(crate::generated::types::PyVec2 { inner: self.inner.pos.clone() }) } + + #[getter] + #[pyo3(name = "heading")] + fn py_get_heading(&self) -> PyResult { Ok(self.inner.heading) } + + #[setter] + #[pyo3(name = "heading")] + fn py_set_heading(&mut self, v: f64) { self.inner.heading = v; } + + #[getter] + #[pyo3(name = "step")] + fn py_get_step(&self) -> PyResult { Ok(self.inner.step) } + + #[setter] + #[pyo3(name = "step")] + fn py_set_step(&mut self, v: f64) { self.inner.step = v; } + + #[getter] + #[pyo3(name = "angle")] + fn py_get_angle(&self) -> PyResult { Ok(self.inner.angle) } + + #[setter] + #[pyo3(name = "angle")] + fn py_set_angle(&mut self, v: f64) { self.inner.angle = v; } + + #[getter] + #[pyo3(name = "pen_down")] + fn py_get_pen_down(&self) -> PyResult { Ok(self.inner.pen_down) } + + #[setter] + #[pyo3(name = "pen_down")] + fn py_set_pen_down(&mut self, v: bool) { self.inner.pen_down = v; } + + #[getter] + #[pyo3(name = "line_width")] + fn py_get_line_width(&self) -> PyResult { Ok(self.inner.line_width) } + + #[setter] + #[pyo3(name = "line_width")] + fn py_set_line_width(&mut self, v: f64) { self.inner.line_width = v; } + + #[getter] + #[pyo3(name = "width_factor")] + fn py_get_width_factor(&self) -> PyResult { Ok(self.inner.width_factor) } + + #[setter] + #[pyo3(name = "width_factor")] + fn py_set_width_factor(&mut self, v: f64) { self.inner.width_factor = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Turtle2", "Turtle2", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// 3-D turtle: the frame's local x axis is the heading, y the left +/// vector, z the up vector. `+`/`-` yaw about up, `&`/`^` pitch +/// about left, `\`/`/` roll about the heading, `|` yaws 180°, +/// `F` draws, `f` moves, ``/`` push/pop, `!` tapers the radius. +/// +/// Rust: `fractals::lsystem::Turtle3` +#[pyclass(name = "Turtle3", module = "numeria.fractals.lsystem", from_py_object)] +#[derive(Clone)] +pub struct PyTurtle3 { pub inner: rust_physics_engine::fractals::lsystem::Turtle3 } +#[pymethods] +impl PyTurtle3 { + /// Turtle at the origin heading +x with up +z. + /// + /// Panics: + /// Panics unless `step > 0`. + /// + /// Rust: `fractals::lsystem::Turtle3::new` + #[new] + #[pyo3(signature = (step, angle_deg))] + fn __new__(step: f64, angle_deg: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::lsystem::Turtle3::new(step, angle_deg)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTurtle3 { inner: __v }) + } + + /// Interprets the string, returning drawn segments. + /// + /// Rust: `fractals::lsystem::Turtle3::interpret` + #[pyo3(name = "interpret")] + #[pyo3(signature = (s))] + fn interpret(&mut self, s: String) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.interpret(&s)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PySegment { inner: __x }).collect::>()) + } + + /// Interprets the string, returning segments with the branch + /// radius active while each was drawn. + /// + /// Rust: `fractals::lsystem::Turtle3::interpret_tree` + #[pyo3(name = "interpret_tree")] + #[pyo3(signature = (s))] + fn interpret_tree(&mut self, s: String) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.interpret_tree(&s)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (crate::generated::types::PySegment { inner: __x.0 }, __x.1)).collect::>()) + } + + /// Interprets the string as a branching structure and meshes each + /// drawn segment as an uncapped cylinder of its branch radius + /// (starting from `base_radius`, multiplied by `taper` at each + /// `!`). + /// + /// Panics: + /// Panics unless `base_radius > 0` and `segments >= 3`. + /// + /// Rust: `fractals::lsystem::Turtle3::to_mesh` + #[pyo3(name = "to_mesh")] + #[pyo3(signature = (s, base_radius, taper, segments))] + fn to_mesh(&mut self, s: String, base_radius: f64, taper: f64, segments: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.to_mesh(&s, base_radius, taper, segments)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMeshMesh { inner: __v }) + } + + #[getter] + #[pyo3(name = "frame")] + fn py_get_frame(&self) -> PyResult { Ok(crate::generated::types::PyFrame { inner: self.inner.frame.clone() }) } + + #[getter] + #[pyo3(name = "step")] + fn py_get_step(&self) -> PyResult { Ok(self.inner.step) } + + #[setter] + #[pyo3(name = "step")] + fn py_set_step(&mut self, v: f64) { self.inner.step = v; } + + #[getter] + #[pyo3(name = "angle")] + fn py_get_angle(&self) -> PyResult { Ok(self.inner.angle) } + + #[setter] + #[pyo3(name = "angle")] + fn py_set_angle(&mut self, v: f64) { self.inner.angle = v; } + + #[getter] + #[pyo3(name = "radius")] + fn py_get_radius(&self) -> PyResult { Ok(self.inner.radius) } + + #[setter] + #[pyo3(name = "radius")] + fn py_set_radius(&mut self, v: f64) { self.inner.radius = v; } + + #[getter] + #[pyo3(name = "taper")] + fn py_get_taper(&self) -> PyResult { Ok(self.inner.taper) } + + #[setter] + #[pyo3(name = "taper")] + fn py_set_taper(&mut self, v: f64) { self.inner.taper = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Turtle3", "Turtle3", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Hydraulic erosion droplet parameters (Beyer 2015-style droplet +/// simulation). +/// +/// Rust: `fractals::noise::ErosionParams` +#[pyclass(name = "ErosionParams", module = "numeria.fractals.noise", from_py_object)] +#[derive(Clone)] +pub struct PyErosionParams { pub inner: rust_physics_engine::fractals::noise::ErosionParams } +#[pymethods] +impl PyErosionParams { + /// Builds a `ErosionParams` from its fields. + #[new] + #[pyo3(signature = (inertia, capacity, min_capacity, erode_speed, deposit_speed, evaporate_speed, gravity, max_lifetime))] + fn __new__(inertia: f64, capacity: f64, min_capacity: f64, erode_speed: f64, deposit_speed: f64, evaporate_speed: f64, gravity: f64, max_lifetime: u32) -> Self { + + Self { inner: rust_physics_engine::fractals::noise::ErosionParams { inertia: inertia, capacity: capacity, min_capacity: min_capacity, erode_speed: erode_speed, deposit_speed: deposit_speed, evaporate_speed: evaporate_speed, gravity: gravity, max_lifetime: max_lifetime } } + } + + #[getter] + #[pyo3(name = "inertia")] + fn py_get_inertia(&self) -> PyResult { Ok(self.inner.inertia) } + + #[setter] + #[pyo3(name = "inertia")] + fn py_set_inertia(&mut self, v: f64) { self.inner.inertia = v; } + + #[getter] + #[pyo3(name = "capacity")] + fn py_get_capacity(&self) -> PyResult { Ok(self.inner.capacity) } + + #[setter] + #[pyo3(name = "capacity")] + fn py_set_capacity(&mut self, v: f64) { self.inner.capacity = v; } + + #[getter] + #[pyo3(name = "min_capacity")] + fn py_get_min_capacity(&self) -> PyResult { Ok(self.inner.min_capacity) } + + #[setter] + #[pyo3(name = "min_capacity")] + fn py_set_min_capacity(&mut self, v: f64) { self.inner.min_capacity = v; } + + #[getter] + #[pyo3(name = "erode_speed")] + fn py_get_erode_speed(&self) -> PyResult { Ok(self.inner.erode_speed) } + + #[setter] + #[pyo3(name = "erode_speed")] + fn py_set_erode_speed(&mut self, v: f64) { self.inner.erode_speed = v; } + + #[getter] + #[pyo3(name = "deposit_speed")] + fn py_get_deposit_speed(&self) -> PyResult { Ok(self.inner.deposit_speed) } + + #[setter] + #[pyo3(name = "deposit_speed")] + fn py_set_deposit_speed(&mut self, v: f64) { self.inner.deposit_speed = v; } + + #[getter] + #[pyo3(name = "evaporate_speed")] + fn py_get_evaporate_speed(&self) -> PyResult { Ok(self.inner.evaporate_speed) } + + #[setter] + #[pyo3(name = "evaporate_speed")] + fn py_set_evaporate_speed(&mut self, v: f64) { self.inner.evaporate_speed = v; } + + #[getter] + #[pyo3(name = "gravity")] + fn py_get_gravity(&self) -> PyResult { Ok(self.inner.gravity) } + + #[setter] + #[pyo3(name = "gravity")] + fn py_set_gravity(&mut self, v: f64) { self.inner.gravity = v; } + + #[getter] + #[pyo3(name = "max_lifetime")] + fn py_get_max_lifetime(&self) -> PyResult { Ok(self.inner.max_lifetime) } + + #[setter] + #[pyo3(name = "max_lifetime")] + fn py_set_max_lifetime(&mut self, v: u32) { self.inner.max_lifetime = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("ErosionParams", "ErosionParams", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Fractional Brownian motion parameters. +/// +/// Rust: `fractals::noise::FbmParams` +#[pyclass(name = "FbmParams", module = "numeria.fractals.noise", from_py_object)] +#[derive(Clone)] +pub struct PyFbmParams { pub inner: rust_physics_engine::fractals::noise::FbmParams } +#[pymethods] +impl PyFbmParams { + /// Builds a `FbmParams` from its fields. + #[new] + #[pyo3(signature = (octaves, lacunarity, gain, frequency, amplitude))] + fn __new__(octaves: u32, lacunarity: f64, gain: f64, frequency: f64, amplitude: f64) -> Self { + + Self { inner: rust_physics_engine::fractals::noise::FbmParams { octaves: octaves, lacunarity: lacunarity, gain: gain, frequency: frequency, amplitude: amplitude } } + } + + #[getter] + #[pyo3(name = "octaves")] + fn py_get_octaves(&self) -> PyResult { Ok(self.inner.octaves) } + + #[setter] + #[pyo3(name = "octaves")] + fn py_set_octaves(&mut self, v: u32) { self.inner.octaves = v; } + + #[getter] + #[pyo3(name = "lacunarity")] + fn py_get_lacunarity(&self) -> PyResult { Ok(self.inner.lacunarity) } + + #[setter] + #[pyo3(name = "lacunarity")] + fn py_set_lacunarity(&mut self, v: f64) { self.inner.lacunarity = v; } + + #[getter] + #[pyo3(name = "gain")] + fn py_get_gain(&self) -> PyResult { Ok(self.inner.gain) } + + #[setter] + #[pyo3(name = "gain")] + fn py_set_gain(&mut self, v: f64) { self.inner.gain = v; } + + #[getter] + #[pyo3(name = "frequency")] + fn py_get_frequency(&self) -> PyResult { Ok(self.inner.frequency) } + + #[setter] + #[pyo3(name = "frequency")] + fn py_set_frequency(&mut self, v: f64) { self.inner.frequency = v; } + + #[getter] + #[pyo3(name = "amplitude")] + fn py_get_amplitude(&self) -> PyResult { Ok(self.inner.amplitude) } + + #[setter] + #[pyo3(name = "amplitude")] + fn py_set_amplitude(&mut self, v: f64) { self.inner.amplitude = v; } + + fn __repr__(&self) -> String { format!("FbmParams(octaves={:?}, lacunarity={:?}, gain={:?}, frequency={:?}, amplitude={:?})", self.inner.octaves, self.inner.lacunarity, self.inner.gain, self.inner.frequency, self.inner.amplitude) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// One Gabor kernel: a Gaussian-windowed cosine wave. +/// +/// Rust: `fractals::noise::GaborKernel` +#[pyclass(name = "GaborKernel", module = "numeria.fractals.noise", from_py_object)] +#[derive(Clone)] +pub struct PyGaborKernel { pub inner: rust_physics_engine::fractals::noise::GaborKernel } +#[pymethods] +impl PyGaborKernel { + /// Builds a `GaborKernel` from its fields. + #[new] + #[pyo3(signature = (center, frequency, orientation, bandwidth, amplitude, phase))] + fn __new__(center: crate::generated::types::PyVec2Arg, frequency: f64, orientation: f64, bandwidth: f64, amplitude: f64, phase: f64) -> Self { + let center = center.0; + Self { inner: rust_physics_engine::fractals::noise::GaborKernel { center: center, frequency: frequency, orientation: orientation, bandwidth: bandwidth, amplitude: amplitude, phase: phase } } + } + + #[getter] + #[pyo3(name = "center")] + fn py_get_center(&self) -> PyResult { Ok(crate::generated::types::PyVec2 { inner: self.inner.center.clone() }) } + + #[getter] + #[pyo3(name = "frequency")] + fn py_get_frequency(&self) -> PyResult { Ok(self.inner.frequency) } + + #[setter] + #[pyo3(name = "frequency")] + fn py_set_frequency(&mut self, v: f64) { self.inner.frequency = v; } + + #[getter] + #[pyo3(name = "orientation")] + fn py_get_orientation(&self) -> PyResult { Ok(self.inner.orientation) } + + #[setter] + #[pyo3(name = "orientation")] + fn py_set_orientation(&mut self, v: f64) { self.inner.orientation = v; } + + #[getter] + #[pyo3(name = "bandwidth")] + fn py_get_bandwidth(&self) -> PyResult { Ok(self.inner.bandwidth) } + + #[setter] + #[pyo3(name = "bandwidth")] + fn py_set_bandwidth(&mut self, v: f64) { self.inner.bandwidth = v; } + + #[getter] + #[pyo3(name = "amplitude")] + fn py_get_amplitude(&self) -> PyResult { Ok(self.inner.amplitude) } + + #[setter] + #[pyo3(name = "amplitude")] + fn py_set_amplitude(&mut self, v: f64) { self.inner.amplitude = v; } + + #[getter] + #[pyo3(name = "phase")] + fn py_get_phase(&self) -> PyResult { Ok(self.inner.phase) } + + #[setter] + #[pyo3(name = "phase")] + fn py_set_phase(&mut self, v: f64) { self.inner.phase = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("GaborKernel", "GaborKernel", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Distance metrics for Worley noise. +/// +/// Rust: `fractals::noise::Metric` +#[pyclass(name = "Metric", module = "numeria.fractals.noise", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyNoiseMetric { pub inner: rust_physics_engine::fractals::noise::Metric } +#[pymethods] +impl PyNoiseMetric { + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Metric", "Metric", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// OpenSimplex2 noise (the "faster" variant): visually isotropic +/// gradient noise on simplex-style lattices, in [-1, 1]. The 3-D +/// evaluator uses the ImproveXY lattice orientation; 4-D noise is +/// not ported — use `Perlin::noise_4d` when a fourth dimension is +/// needed. +/// +/// Rust: `fractals::noise::OpenSimplex2` +#[pyclass(name = "OpenSimplex2", module = "numeria.fractals.noise", from_py_object)] +#[derive(Clone)] +pub struct PyOpenSimplex2 { pub inner: rust_physics_engine::fractals::noise::OpenSimplex2 } +#[pymethods] +impl PyOpenSimplex2 { + /// + /// Rust: `fractals::noise::OpenSimplex2::new` + #[new] + #[pyo3(signature = (seed))] + fn __new__(seed: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::noise::OpenSimplex2::new(seed)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyOpenSimplex2 { inner: __v }) + } + + /// 2-D noise, standard lattice orientation. + /// + /// Rust: `fractals::noise::OpenSimplex2::noise_2d` + #[pyo3(name = "noise_2d")] + #[pyo3(signature = (x, y))] + fn noise_2d(&self, x: f64, y: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.noise_2d(x, y)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// 3-D noise, ImproveXY orientation (Z up the lattice diagonal; + /// best for terrain and time-varied 2-D fields with z = time). + /// + /// Rust: `fractals::noise::OpenSimplex2::noise_3d` + #[pyo3(name = "noise_3d")] + #[pyo3(signature = (x, y, z))] + fn noise_3d(&self, x: f64, y: f64, z: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.noise_3d(x, y, z)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("OpenSimplex2", "OpenSimplex2", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Classic improved Perlin gradient noise (Perlin, "Improving +/// Noise", 2002) with a seeded permutation table. Values are in +/// [-1, 1] and zero at every integer lattice point. +/// +/// Rust: `fractals::noise::Perlin` +#[pyclass(name = "Perlin", module = "numeria.fractals.noise", from_py_object)] +#[derive(Clone)] +pub struct PyPerlin { pub inner: rust_physics_engine::fractals::noise::Perlin } +#[pymethods] +impl PyPerlin { + /// Permutation table shuffled by the seed (Fisher-Yates over the + /// crate Rng). + /// + /// Rust: `fractals::noise::Perlin::new` + #[new] + #[pyo3(signature = (seed))] + fn __new__(seed: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::noise::Perlin::new(seed)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPerlin { inner: __v }) + } + + /// 1-D gradient noise: gradients ±1, ±2 at integer knots. + /// + /// Rust: `fractals::noise::Perlin::noise_1d` + #[pyo3(name = "noise_1d")] + #[pyo3(signature = (x))] + fn noise_1d(&self, x: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.noise_1d(x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// 2-D Perlin noise in [-1, 1]. + /// + /// Rust: `fractals::noise::Perlin::noise_2d` + #[pyo3(name = "noise_2d")] + #[pyo3(signature = (x, y))] + fn noise_2d(&self, x: f64, y: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.noise_2d(x, y)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// 3-D Perlin noise in [-1, 1]. + /// + /// Rust: `fractals::noise::Perlin::noise_3d` + #[pyo3(name = "noise_3d")] + #[pyo3(signature = (x, y, z))] + fn noise_3d(&self, x: f64, y: f64, z: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.noise_3d(x, y, z)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// 4-D Perlin noise in [-1, 1]. + /// + /// Rust: `fractals::noise::Perlin::noise_4d` + #[pyo3(name = "noise_4d")] + #[pyo3(signature = (x, y, z, w))] + fn noise_4d(&self, x: f64, y: f64, z: f64, w: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.noise_4d(x, y, z, w)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Gradient of the 2-D noise by central differences. + /// + /// Rust: `fractals::noise::Perlin::gradient_2d` + #[pyo3(name = "gradient_2d")] + #[pyo3(signature = (x, y))] + fn gradient_2d(&self, x: f64, y: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.gradient_2d(x, y)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) + } + + /// Gradient of the 3-D noise by central differences. + /// + /// Rust: `fractals::noise::Perlin::gradient_3d` + #[pyo3(name = "gradient_3d")] + #[pyo3(signature = (x, y, z))] + fn gradient_3d(&self, x: f64, y: f64, z: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.gradient_3d(x, y, z)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Perlin", "Perlin", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Lattice value noise: random values at integer lattice points, +/// interpolated (quintic-smoothed bilinear, optional bicubic). +/// +/// Rust: `fractals::noise::ValueNoise` +#[pyclass(name = "ValueNoise", module = "numeria.fractals.noise", from_py_object)] +#[derive(Clone)] +pub struct PyValueNoise { pub inner: rust_physics_engine::fractals::noise::ValueNoise } +#[pymethods] +impl PyValueNoise { + /// + /// Rust: `fractals::noise::ValueNoise::new` + #[new] + #[pyo3(signature = (seed))] + fn __new__(seed: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::noise::ValueNoise::new(seed)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyValueNoise { inner: __v }) + } + + /// Smoothed bilinear value noise in [-1, 1]. + /// + /// Rust: `fractals::noise::ValueNoise::noise_2d` + #[pyo3(name = "noise_2d")] + #[pyo3(signature = (x, y))] + fn noise_2d(&self, x: f64, y: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.noise_2d(x, y)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Smoothed trilinear value noise in [-1, 1]. + /// + /// Rust: `fractals::noise::ValueNoise::noise_3d` + #[pyo3(name = "noise_3d")] + #[pyo3(signature = (x, y, z))] + fn noise_3d(&self, x: f64, y: f64, z: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.noise_3d(x, y, z)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Catmull-Rom bicubic value noise (C¹, wider support). + /// + /// Rust: `fractals::noise::ValueNoise::noise_2d_cubic` + #[pyo3(name = "noise_2d_cubic")] + #[pyo3(signature = (x, y))] + fn noise_2d_cubic(&self, x: f64, y: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.noise_2d_cubic(x, y)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("ValueNoise", "ValueNoise", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Worley (cellular) noise: one feature point per grid cell of size +/// `cell`, hashed from the seed; F1/F2 are the distances to the +/// nearest and second-nearest feature points under `metric`. +/// +/// Rust: `fractals::noise::Worley` +#[pyclass(name = "Worley", module = "numeria.fractals.noise", from_py_object)] +#[derive(Clone)] +pub struct PyWorley { pub inner: rust_physics_engine::fractals::noise::Worley } +#[pymethods] +impl PyWorley { + /// Panics: + /// Panics unless `cell > 0`. + /// + /// Rust: `fractals::noise::Worley::new` + #[new] + #[pyo3(signature = (seed, cell))] + fn __new__(seed: u64, cell: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::fractals::noise::Worley::new(seed, cell)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyWorley { inner: __v }) + } + + /// Distance to the nearest feature point. + /// + /// Rust: `fractals::noise::Worley::f1_2d` + #[pyo3(name = "f1_2d")] + #[pyo3(signature = (x, y))] + fn f1_2d(&self, x: f64, y: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.f1_2d(x, y)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Distance to the second-nearest feature point. + /// + /// Rust: `fractals::noise::Worley::f2_2d` + #[pyo3(name = "f2_2d")] + #[pyo3(signature = (x, y))] + fn f2_2d(&self, x: f64, y: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.f2_2d(x, y)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// F2 − F1 (ridged cell boundaries). + /// + /// Rust: `fractals::noise::Worley::f2_minus_f1_2d` + #[pyo3(name = "f2_minus_f1_2d")] + #[pyo3(signature = (x, y))] + fn f2_minus_f1_2d(&self, x: f64, y: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.f2_minus_f1_2d(x, y)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// + /// Rust: `fractals::noise::Worley::f1_3d` + #[pyo3(name = "f1_3d")] + #[pyo3(signature = (x, y, z))] + fn f1_3d(&self, x: f64, y: f64, z: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.f1_3d(x, y, z)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// + /// Rust: `fractals::noise::Worley::f2_3d` + #[pyo3(name = "f2_3d")] + #[pyo3(signature = (x, y, z))] + fn f2_3d(&self, x: f64, y: f64, z: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.f2_3d(x, y, z)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Stable id of the cell owning the nearest feature point. + /// + /// Rust: `fractals::noise::Worley::cell_id_2d` + #[pyo3(name = "cell_id_2d")] + #[pyo3(signature = (x, y))] + fn cell_id_2d(&self, x: f64, y: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.cell_id_2d(x, y)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "metric")] + fn py_get_metric(&self) -> PyResult { Ok(crate::generated::types::PyNoiseMetric { inner: self.inner.metric.clone() }) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Worley", "Worley", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} diff --git a/bindings/python/src/generated/types/geometry.rs b/bindings/python/src/generated/types/geometry.rs new file mode 100644 index 0000000..6566aa1 --- /dev/null +++ b/bindings/python/src/generated/types/geometry.rs @@ -0,0 +1,239 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; + + +/// Reference ellipsoid: semi-major axis a (m) and flattening f. +/// +/// Rust: `geometry::geodesy::Ellipsoid` +#[pyclass(name = "Ellipsoid", module = "numeria.geometry.geodesy", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyEllipsoid { pub inner: rust_physics_engine::geometry::geodesy::Ellipsoid } +#[pymethods] +impl PyEllipsoid { + /// Builds a `Ellipsoid` from its fields. + #[new] + #[pyo3(signature = (a, f))] + fn __new__(a: f64, f: f64) -> Self { + + Self { inner: rust_physics_engine::geometry::geodesy::Ellipsoid { a: a, f: f } } + } + + /// Semi-minor axis b = a(1 − f). + /// + /// Rust: `geometry::geodesy::Ellipsoid::b` + #[pyo3(name = "b")] + #[pyo3(signature = ())] + fn b(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.b()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// First eccentricity squared e² = f(2 − f). + /// + /// Rust: `geometry::geodesy::Ellipsoid::e_sq` + #[pyo3(name = "e_sq")] + #[pyo3(signature = ())] + fn e_sq(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.e_sq()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "a")] + fn py_get_a(&self) -> PyResult { Ok(self.inner.a) } + + #[setter] + #[pyo3(name = "a")] + fn py_set_a(&mut self, v: f64) { self.inner.a = v; } + + #[getter] + #[pyo3(name = "f")] + fn py_get_f(&self) -> PyResult { Ok(self.inner.f) } + + #[setter] + #[pyo3(name = "f")] + fn py_set_f(&mut self, v: f64) { self.inner.f = v; } + + #[classattr] + #[pyo3(name = "WGS84")] + fn const_wgs84() -> crate::generated::types::PyEllipsoid { crate::generated::types::PyEllipsoid { inner: rust_physics_engine::geometry::geodesy::Ellipsoid::WGS84 } } + + fn __repr__(&self) -> String { format!("Ellipsoid(a={:?}, f={:?})", self.inner.a, self.inner.f) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A `Ellipsoid` argument, or anything that can stand in for one. +pub struct PyEllipsoidArg(pub rust_physics_engine::geometry::geodesy::Ellipsoid); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyEllipsoidArg { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result { + if let Ok(__w) = obj.extract::() { + return Ok(PyEllipsoidArg(__w.inner)); + } + let __v = crate::runtime::coerce::floats_exact(obj, 2, "Ellipsoid")?; + Ok(PyEllipsoidArg(rust_physics_engine::geometry::geodesy::Ellipsoid { a: __v[0], f: __v[1] })) + } +} + + +/// Indexed triangle mesh. `materials[i]` is a per-triangle material index +/// (into caller-owned tables such as absorption coefficients). +/// +/// Rust: `geometry::mesh::Mesh` +#[pyclass(name = "Mesh", module = "numeria.geometry.mesh", from_py_object)] +#[derive(Clone)] +pub struct PyGeometryMeshMesh { pub inner: rust_physics_engine::geometry::mesh::Mesh } +#[pymethods] +impl PyGeometryMeshMesh { + /// Empty mesh. + /// + /// Rust: `geometry::mesh::Mesh::new` + #[new] + #[pyo3(signature = ())] + fn __new__() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::geometry::mesh::Mesh::new()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGeometryMeshMesh { inner: __v }) + } + + /// Axis-aligned box (e.g. a shoebox room) spanning from the origin to + /// `size`, with inward-facing triangles and material indices 0..5 in + /// the wall order -x, +x, -y, +y, -z, +z. + /// + /// Rust: `geometry::mesh::Mesh::box_room` + #[pyo3(name = "box_room")] + #[staticmethod] + #[pyo3(signature = (size))] + fn box_room(size: crate::generated::types::PyVec3Arg) -> PyResult { + let size = size.0; + let __r = crate::runtime::guard(|| rust_physics_engine::geometry::mesh::Mesh::box_room(size)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGeometryMeshMesh { inner: __v }) + } + + /// Geometric normal of triangle `i` (right-hand rule, unnormalized + /// winding as stored). + /// + /// Rust: `geometry::mesh::Mesh::triangle_normal` + #[pyo3(name = "triangle_normal")] + #[pyo3(signature = (i))] + fn triangle_normal(&self, i: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.triangle_normal(i)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Nearest ray intersection (Möller-Trumbore) with t > `t_min`. + /// + /// Rust: `geometry::mesh::Mesh::intersect_ray` + #[pyo3(name = "intersect_ray")] + #[pyo3(signature = (origin, dir, t_min))] + fn intersect_ray(&self, origin: crate::generated::types::PyVec3Arg, dir: crate::generated::types::PyVec3Arg, t_min: f64) -> PyResult> { + let origin = origin.0; + let dir = dir.0; + let __r = crate::runtime::guard(|| self.inner.intersect_ray(origin, dir, t_min)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyMeshRayHit { inner: __x })) + } + + /// True if the straight segment between two points is unobstructed. + /// + /// Rust: `geometry::mesh::Mesh::segment_clear` + #[pyo3(name = "segment_clear")] + #[pyo3(signature = (a, b))] + fn segment_clear(&self, a: crate::generated::types::PyVec3Arg, b: crate::generated::types::PyVec3Arg) -> PyResult { + let a = a.0; + let b = b.0; + let __r = crate::runtime::guard(|| self.inner.segment_clear(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "vertices")] + fn py_get_vertices(&self) -> PyResult> { Ok(self.inner.vertices.clone().into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) } + + #[getter] + #[pyo3(name = "triangles")] + fn py_get_triangles(&self) -> PyResult>> { Ok(self.inner.triangles.clone().into_iter().map(|__x| __x.to_vec()).collect::>()) } + + #[getter] + #[pyo3(name = "materials")] + fn py_get_materials(&self) -> PyResult> { Ok(self.inner.materials.clone()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Mesh", "Mesh", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A ray/mesh intersection. +/// +/// Rust: `geometry::mesh::RayHit` +#[pyclass(name = "RayHit", module = "numeria.geometry.mesh", from_py_object)] +#[derive(Clone)] +pub struct PyMeshRayHit { pub inner: rust_physics_engine::geometry::mesh::RayHit } +#[pymethods] +impl PyMeshRayHit { + /// Builds a `RayHit` from its fields. + #[new] + #[pyo3(signature = (t, point, normal, triangle))] + fn __new__(t: f64, point: crate::generated::types::PyVec3Arg, normal: crate::generated::types::PyVec3Arg, triangle: usize) -> Self { + let point = point.0; + let normal = normal.0; + Self { inner: rust_physics_engine::geometry::mesh::RayHit { t: t, point: point, normal: normal, triangle: triangle } } + } + + #[getter] + #[pyo3(name = "t")] + fn py_get_t(&self) -> PyResult { Ok(self.inner.t) } + + #[setter] + #[pyo3(name = "t")] + fn py_set_t(&mut self, v: f64) { self.inner.t = v; } + + #[getter] + #[pyo3(name = "point")] + fn py_get_point(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.point.clone() }) } + + #[getter] + #[pyo3(name = "normal")] + fn py_get_normal(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.normal.clone() }) } + + #[getter] + #[pyo3(name = "triangle")] + fn py_get_triangle(&self) -> PyResult { Ok(self.inner.triangle) } + + #[setter] + #[pyo3(name = "triangle")] + fn py_set_triangle(&mut self, v: usize) { self.inner.triangle = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("RayHit", "RayHit", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} diff --git a/bindings/python/src/generated/types/graph.rs b/bindings/python/src/generated/types/graph.rs new file mode 100644 index 0000000..49b96f5 --- /dev/null +++ b/bindings/python/src/generated/types/graph.rs @@ -0,0 +1,654 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; + + +/// The vertex order a greedy colouring walks. +/// +/// Greedy colouring gives every vertex the smallest colour none of its +/// already-coloured neighbours holds. The order is the whole algorithm: some +/// order always produces an optimal colouring, and finding it is the hard +/// part, so these are the standard heuristics for choosing one. +/// +/// Rust: `graph::coloring::Order` +#[pyclass(name = "Order", module = "numeria.graph.coloring", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyOrder { + Natural, + LargestFirst, + SmallestLast, + Dsatur, +} +impl PyOrder { + pub fn to_rust(&self) -> rust_physics_engine::graph::coloring::Order { match self { + Self::Natural => rust_physics_engine::graph::coloring::Order::Natural, + Self::LargestFirst => rust_physics_engine::graph::coloring::Order::LargestFirst, + Self::SmallestLast => rust_physics_engine::graph::coloring::Order::SmallestLast, + Self::Dsatur => rust_physics_engine::graph::coloring::Order::Dsatur, + } } + pub fn from_rust(v: &rust_physics_engine::graph::coloring::Order) -> Self { match v { + rust_physics_engine::graph::coloring::Order::Natural => Self::Natural, + rust_physics_engine::graph::coloring::Order::LargestFirst => Self::LargestFirst, + rust_physics_engine::graph::coloring::Order::SmallestLast => Self::SmallestLast, + rust_physics_engine::graph::coloring::Order::Dsatur => Self::Dsatur, + } } +} +#[pymethods] +impl PyOrder { + fn __repr__(&self) -> &'static str { + match self { + Self::Natural => "Order.Natural", + Self::LargestFirst => "Order.LargestFirst", + Self::SmallestLast => "Order.SmallestLast", + Self::Dsatur => "Order.Dsatur", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A weighted graph over the vertices `0..n`. +/// +/// Rust: `graph::core::Graph` +#[pyclass(name = "Graph", module = "numeria.graph.core", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyGraph { pub inner: rust_physics_engine::graph::core::Graph } +#[pymethods] +impl PyGraph { + /// An edgeless graph on `n` vertices. + /// + /// Rust: `graph::core::Graph::new` + #[new] + #[pyo3(signature = (n, directed))] + fn __new__(n: usize, directed: bool) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::graph::core::Graph::new(n, directed)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGraph { inner: __v }) + } + + /// Adds an arc `u -> v` of the given weight, and the reverse arc too when + /// the graph is undirected. + /// + /// Parallel edges and self-loops are permitted and are stored as given; an + /// undirected self-loop is stored once, so it contributes one to the + /// degree rather than the two of the usual convention. + /// + /// Panics: + /// Panics if either endpoint is outside `0..n`. + /// + /// Rust: `graph::core::Graph::add_edge` + #[pyo3(name = "add_edge")] + #[pyo3(signature = (u, v, w))] + fn add_edge(&mut self, u: usize, v: usize, w: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.add_edge(u, v, w)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Builds a graph from a list of `(u, v, weight)` triples. + /// + /// Rust: `graph::core::Graph::from_edges` + #[pyo3(name = "from_edges")] + #[staticmethod] + #[pyo3(signature = (n, edges, directed))] + fn from_edges(n: usize, edges: Vec<(usize, usize, f64)>, directed: bool) -> PyResult { + let edges = edges.into_iter().map(|__e| (__e.0, __e.1, __e.2)).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::graph::core::Graph::from_edges(n, &edges, directed)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGraph { inner: __v }) + } + + /// Builds a graph from a square weight matrix, treating a zero entry as + /// the absence of an edge. + /// + /// The graph is undirected when the matrix is symmetric, and in that case + /// each pair is added once. + /// + /// Panics: + /// Panics if the matrix is not square. + /// + /// Rust: `graph::core::Graph::from_adjacency_matrix` + #[pyo3(name = "from_adjacency_matrix")] + #[staticmethod] + #[pyo3(signature = (m))] + fn from_adjacency_matrix(m: crate::generated::types::PyMatrixArg) -> PyResult { + let m = m.0; + let __r = crate::runtime::guard(|| rust_physics_engine::graph::core::Graph::from_adjacency_matrix(&m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGraph { inner: __v }) + } + + /// The weight matrix. Parallel edges sum; absent edges are zero. + /// + /// Rust: `graph::core::Graph::to_adjacency_matrix` + #[pyo3(name = "to_adjacency_matrix")] + #[pyo3(signature = ())] + fn to_adjacency_matrix(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.to_adjacency_matrix()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) + } + + /// The number of arcs out of `v`, counting parallel edges. + /// + /// For an undirected graph this is the ordinary degree. + /// + /// Rust: `graph::core::Graph::degree` + #[pyo3(name = "degree")] + #[pyo3(signature = (v))] + fn degree(&self, v: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.degree(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Arcs out of `v`. Same as `Graph::degree`. + /// + /// Rust: `graph::core::Graph::out_degree` + #[pyo3(name = "out_degree")] + #[pyo3(signature = (v))] + fn out_degree(&self, v: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.out_degree(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Arcs into `v`, counted by scanning every adjacency list. + /// + /// Rust: `graph::core::Graph::in_degree` + #[pyo3(name = "in_degree")] + #[pyo3(signature = (v))] + fn in_degree(&self, v: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.in_degree(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The edges as `(u, v, weight)`. + /// + /// A directed graph reports every arc. An undirected graph reports each + /// edge once, with `u <= v`, so the count is the true edge count rather + /// than twice it. + /// + /// Rust: `graph::core::Graph::edges` + #[pyo3(name = "edges")] + #[pyo3(signature = ())] + fn edges<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.edges())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, __x.2)).collect::>()) + } + + /// The number of edges (arcs, if directed). + /// + /// Rust: `graph::core::Graph::edge_count` + #[pyo3(name = "edge_count")] + #[pyo3(signature = ())] + fn edge_count(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.edge_count()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The graph with every arc reversed. An undirected graph is unchanged. + /// + /// Rust: `graph::core::Graph::reverse` + #[pyo3(name = "reverse")] + #[pyo3(signature = ())] + fn reverse(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.reverse()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGraph { inner: __v }) + } + + /// The subgraph induced on `vs`, relabelled to `0..vs.len()` in the order + /// given. + /// + /// Panics: + /// Panics if `vs` contains a repeat or a vertex outside `0..n`. + /// + /// Rust: `graph::core::Graph::subgraph` + #[pyo3(name = "subgraph")] + #[pyo3(signature = (vs))] + fn subgraph(&self, vs: Vec) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.subgraph(&vs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGraph { inner: __v }) + } + + /// The complement: an unweighted graph with an edge exactly where this one + /// has none. Self-loops are never present in the result. + /// + /// Rust: `graph::core::Graph::complement` + #[pyo3(name = "complement")] + #[pyo3(signature = ())] + fn complement(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.complement()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGraph { inner: __v }) + } + + /// True when the underlying undirected graph is connected. + /// + /// The empty graph is connected by convention. + /// + /// Rust: `graph::core::Graph::is_connected` + #[pyo3(name = "is_connected")] + #[pyo3(signature = ())] + fn is_connected(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_connected()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The connected components of the underlying undirected graph, each + /// sorted, ordered by smallest member. + /// + /// Rust: `graph::core::Graph::connected_components` + #[pyo3(name = "connected_components")] + #[pyo3(signature = ())] + fn connected_components<'py>(&self, py: Python<'py>) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.connected_components())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The strongly connected components, by Tarjan's algorithm. + /// + /// Each component is sorted, and the components come out in reverse + /// topological order of the condensation -- a component appears before + /// every component that can reach it. For an undirected graph this is the + /// connected components. + /// + /// Rust: `graph::core::Graph::strongly_connected_components` + #[pyo3(name = "strongly_connected_components")] + #[pyo3(signature = ())] + fn strongly_connected_components<'py>(&self, py: Python<'py>) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.strongly_connected_components())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The condensation: one vertex per strongly connected component, with an + /// arc between distinct components that have an arc between them. + /// + /// Returns the graph and the component index of each original vertex. The + /// result is always a DAG. + /// + /// Rust: `graph::core::Graph::condensation` + #[pyo3(name = "condensation")] + #[pyo3(signature = ())] + fn condensation(&self) -> PyResult<(crate::generated::types::PyGraph, Vec)> { + let __r = crate::runtime::guard(|| self.inner.condensation()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((crate::generated::types::PyGraph { inner: __v.0 }, __v.1)) + } + + /// A two-colouring witnessing bipartiteness, or `None` if an odd cycle + /// exists. + /// + /// Direction is ignored. Isolated vertices and separate components are + /// each coloured starting from `false`. + /// + /// Rust: `graph::core::Graph::is_bipartite` + #[pyo3(name = "is_bipartite")] + #[pyo3(signature = ())] + fn is_bipartite(&self) -> PyResult>> { + let __r = crate::runtime::guard(|| self.inner.is_bipartite()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) + } + + /// True when the graph is a tree: connected, and with exactly `n - 1` + /// edges. The empty graph is not a tree; a single vertex is. + /// + /// Rust: `graph::core::Graph::is_tree` + #[pyo3(name = "is_tree")] + #[pyo3(signature = ())] + fn is_tree(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_tree()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// True when the graph is directed and acyclic. + /// + /// Rust: `graph::core::Graph::is_dag` + #[pyo3(name = "is_dag")] + #[pyo3(signature = ())] + fn is_dag(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_dag()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// A topological order, or `None` if the graph has a directed cycle or is + /// undirected with any edge. + /// + /// Kahn's algorithm, taking the smallest available vertex first so the + /// result is the lexicographically least topological order. + /// + /// Rust: `graph::core::Graph::topological_sort` + #[pyo3(name = "topological_sort")] + #[pyo3(signature = ())] + fn topological_sort(&self) -> PyResult>> { + let __r = crate::runtime::guard(|| self.inner.topological_sort()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) + } + + /// Hop distances from `s`, following arc direction. `None` for vertices + /// that `s` cannot reach. + /// + /// Rust: `graph::core::Graph::bfs` + #[pyo3(name = "bfs")] + #[pyo3(signature = (s))] + fn bfs<'py>(&self, py: Python<'py>, s: usize) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.bfs(s))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.map(|__x| __x)).collect::>()) + } + + /// The vertices reachable from `s`, in depth-first preorder, following arc + /// direction. Neighbours are visited in adjacency-list order. + /// + /// Rust: `graph::core::Graph::dfs` + #[pyo3(name = "dfs")] + #[pyo3(signature = (s))] + fn dfs<'py>(&self, py: Python<'py>, s: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.dfs(s))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The bridges: edges whose removal increases the number of connected + /// components. Reported as `(u, v)` with `u < v`, sorted. + /// + /// Direction is ignored. Parallel edges are handled: an edge duplicated in + /// the input is not a bridge, which is why this tracks the arc index used + /// to arrive rather than merely the parent vertex. + /// + /// Rust: `graph::core::Graph::bridges` + #[pyo3(name = "bridges")] + #[pyo3(signature = ())] + fn bridges<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.bridges())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) + } + + /// The articulation points: vertices whose removal increases the number of + /// connected components. Sorted. + /// + /// Direction is ignored. + /// + /// Rust: `graph::core::Graph::articulation_points` + #[pyo3(name = "articulation_points")] + #[pyo3(signature = ())] + fn articulation_points<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.articulation_points())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// An Eulerian circuit as a vertex sequence starting and ending at the + /// same vertex, or `None` when none exists. + /// + /// Hierholzer's algorithm. Exists exactly when every vertex with an edge + /// has even degree (undirected) or equal in- and out-degree (directed), + /// and all edges lie in one connected component. + /// + /// Rust: `graph::core::Graph::eulerian_circuit` + #[pyo3(name = "eulerian_circuit")] + #[pyo3(signature = ())] + fn eulerian_circuit(&self) -> PyResult>> { + let __r = crate::runtime::guard(|| self.inner.eulerian_circuit()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) + } + + /// An Eulerian path, or `None` when none exists. + /// + /// A circuit is also a path, so this succeeds whenever + /// `Graph::eulerian_circuit` does, and additionally when exactly two + /// vertices have odd degree (undirected), or one vertex has one more + /// outgoing arc than incoming and one has the reverse (directed). + /// + /// Rust: `graph::core::Graph::eulerian_path` + #[pyo3(name = "eulerian_path")] + #[pyo3(signature = ())] + fn eulerian_path(&self) -> PyResult>> { + let __r = crate::runtime::guard(|| self.inner.eulerian_path()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) + } + + /// A Hamiltonian path as a vertex sequence, or `None` when none exists. + /// + /// Bitmask dynamic programming over subsets, `O(2^n n^2)`. Only sensible + /// up to about twenty vertices, which is what the name says. + /// + /// Panics: + /// Panics if `n` exceeds 20. + /// + /// Rust: `graph::core::Graph::hamiltonian_path_small` + #[pyo3(name = "hamiltonian_path_small")] + #[pyo3(signature = ())] + fn hamiltonian_path_small(&self) -> PyResult>> { + let __r = crate::runtime::guard(|| self.inner.hamiltonian_path_small()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) + } + + /// The girth: the length of the shortest cycle, or `None` if acyclic. + /// + /// A BFS from each vertex, stopping at the first non-tree edge; that gives + /// the shortest cycle through that vertex to within one, and taking the + /// minimum over all starts gives the exact girth. Direction is ignored; + /// self-loops give girth 1 and parallel edges give 2. + /// + /// Rust: `graph::core::Graph::girth` + #[pyo3(name = "girth")] + #[pyo3(signature = ())] + fn girth(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.girth()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) + } + + /// The eccentricity of each vertex in hops, or `None` for a vertex that + /// cannot reach the whole graph. + /// + /// Rust: `graph::core::Graph::eccentricities` + #[pyo3(name = "eccentricities")] + #[pyo3(signature = ())] + fn eccentricities<'py>(&self, py: Python<'py>) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.eccentricities())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.map(|__x| __x)).collect::>()) + } + + /// The diameter in hops: the largest eccentricity, or `None` when some + /// vertex cannot reach another. + /// + /// Rust: `graph::core::Graph::diameter` + #[pyo3(name = "diameter")] + #[pyo3(signature = ())] + fn diameter(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.diameter()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) + } + + /// The radius in hops: the smallest eccentricity, or `None` when some + /// vertex cannot reach another. + /// + /// Rust: `graph::core::Graph::radius` + #[pyo3(name = "radius")] + #[pyo3(signature = ())] + fn radius(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.radius()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) + } + + /// The centre: the vertices whose eccentricity equals the radius. + /// + /// Rust: `graph::core::Graph::center` + #[pyo3(name = "center")] + #[pyo3(signature = ())] + fn center<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.center())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Edges present as a fraction of the maximum possible, ignoring parallel + /// edges and self-loops. Zero for fewer than two vertices. + /// + /// Rust: `graph::core::Graph::density` + #[pyo3(name = "density")] + #[pyo3(signature = ())] + fn density(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.density()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The local clustering coefficient of `v`: the fraction of pairs of its + /// neighbours that are themselves adjacent. + /// + /// Zero for a vertex of degree below two, which is the usual convention. + /// + /// Rust: `graph::core::Graph::clustering_coefficient` + #[pyo3(name = "clustering_coefficient")] + #[pyo3(signature = (v))] + fn clustering_coefficient(&self, v: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.clustering_coefficient(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The average of the local clustering coefficients. + /// + /// Rust: `graph::core::Graph::average_clustering` + #[pyo3(name = "average_clustering")] + #[pyo3(signature = ())] + fn average_clustering(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.average_clustering()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Transitivity: three times the number of triangles over the number of + /// connected triples. + /// + /// This is a global ratio and is not the average of the local + /// coefficients; the two differ whenever degree correlates with local + /// clustering. + /// + /// Rust: `graph::core::Graph::transitivity` + #[pyo3(name = "transitivity")] + #[pyo3(signature = ())] + fn transitivity(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.transitivity()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The number of vertices of each degree, indexed by degree. + /// + /// Rust: `graph::core::Graph::degree_distribution` + #[pyo3(name = "degree_distribution")] + #[pyo3(signature = ())] + fn degree_distribution<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.degree_distribution())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Degree assortativity: the Pearson correlation between the degrees at + /// the two ends of an edge. + /// + /// Positive when high-degree vertices attach to each other. Returns zero + /// when there are no edges or every edge has the same endpoint degrees, + /// where the correlation is undefined. + /// + /// Rust: `graph::core::Graph::assortativity` + #[pyo3(name = "assortativity")] + #[pyo3(signature = ())] + fn assortativity(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.assortativity()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The `k`-core: the largest induced subgraph in which every vertex has + /// degree at least `k`, returned as its vertex set, sorted. + /// + /// Rust: `graph::core::Graph::k_core` + #[pyo3(name = "k_core")] + #[pyo3(signature = (k))] + fn k_core<'py>(&self, py: Python<'py>, k: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.k_core(k))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The core number of each vertex: the largest `k` for which it survives + /// in the `k`-core. + /// + /// Peels the minimum-degree vertex repeatedly, which is the standard + /// linear-time algorithm; the degree at the moment of removal is the core + /// number. + /// + /// Rust: `graph::core::Graph::core_numbers` + #[pyo3(name = "core_numbers")] + #[pyo3(signature = ())] + fn core_numbers<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.core_numbers())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "n")] + fn py_get_n(&self) -> PyResult { Ok(self.inner.n) } + + #[setter] + #[pyo3(name = "n")] + fn py_set_n(&mut self, v: usize) { self.inner.n = v; } + + #[getter] + #[pyo3(name = "adj")] + fn py_get_adj(&self) -> PyResult>> { Ok(self.inner.adj.clone().into_iter().map(|__x| __x.into_iter().map(|__x| (__x.0, __x.1)).collect::>()).collect::>()) } + + #[getter] + #[pyo3(name = "directed")] + fn py_get_directed(&self) -> PyResult { Ok(self.inner.directed) } + + #[setter] + #[pyo3(name = "directed")] + fn py_set_directed(&mut self, v: bool) { self.inner.directed = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Graph", "Graph", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} diff --git a/bindings/python/src/generated/types/learn.rs b/bindings/python/src/generated/types/learn.rs new file mode 100644 index 0000000..e435a26 --- /dev/null +++ b/bindings/python/src/generated/types/learn.rs @@ -0,0 +1,957 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; + + +/// A fitted Gaussian mixture. +/// +/// Rust: `learn::cluster::Gmm` +#[pyclass(name = "Gmm", module = "numeria.learn.cluster", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyGmm { pub inner: rust_physics_engine::learn::cluster::Gmm } +#[pymethods] +impl PyGmm { + /// Builds a `Gmm` from its fields. + #[new] + #[pyo3(signature = (weights, means, covariances, log_likelihood_history))] + fn __new__(weights: Vec, means: Vec>, covariances: Vec, log_likelihood_history: Vec) -> Self { + let covariances = covariances.into_iter().map(|__e| __e.0).collect::>(); + Self { inner: rust_physics_engine::learn::cluster::Gmm { weights: weights, means: means, covariances: covariances, log_likelihood_history: log_likelihood_history } } + } + + /// The final log-likelihood. + /// + /// Rust: `learn::cluster::Gmm::log_likelihood` + #[pyo3(name = "log_likelihood")] + #[pyo3(signature = ())] + fn log_likelihood(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.log_likelihood()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "weights")] + fn py_get_weights(&self) -> PyResult> { Ok(self.inner.weights.clone()) } + + #[getter] + #[pyo3(name = "means")] + fn py_get_means(&self) -> PyResult>> { Ok(self.inner.means.clone()) } + + #[getter] + #[pyo3(name = "covariances")] + fn py_get_covariances(&self) -> PyResult> { Ok(self.inner.covariances.clone().into_iter().map(|__x| crate::generated::types::PyMatrix { inner: __x }).collect::>()) } + + #[getter] + #[pyo3(name = "log_likelihood_history")] + fn py_get_log_likelihood_history(&self) -> PyResult> { Ok(self.inner.log_likelihood_history.clone()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Gmm", "Gmm", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// The outcome of a k-means run. +/// +/// Rust: `learn::cluster::KMeans` +#[pyclass(name = "KMeans", module = "numeria.learn.cluster", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyKMeans { pub inner: rust_physics_engine::learn::cluster::KMeans } +#[pymethods] +impl PyKMeans { + /// Builds a `KMeans` from its fields. + #[new] + #[pyo3(signature = (centroids, labels, inertia_history, iterations))] + fn __new__(centroids: Vec>, labels: Vec, inertia_history: Vec, iterations: usize) -> Self { + + Self { inner: rust_physics_engine::learn::cluster::KMeans { centroids: centroids, labels: labels, inertia_history: inertia_history, iterations: iterations } } + } + + /// The final within-cluster sum of squared distances. + /// + /// Rust: `learn::cluster::KMeans::inertia` + #[pyo3(name = "inertia")] + #[pyo3(signature = ())] + fn inertia(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.inertia()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "centroids")] + fn py_get_centroids(&self) -> PyResult>> { Ok(self.inner.centroids.clone()) } + + #[getter] + #[pyo3(name = "labels")] + fn py_get_labels(&self) -> PyResult> { Ok(self.inner.labels.clone()) } + + #[getter] + #[pyo3(name = "inertia_history")] + fn py_get_inertia_history(&self) -> PyResult> { Ok(self.inner.inertia_history.clone()) } + + #[getter] + #[pyo3(name = "iterations")] + fn py_get_iterations(&self) -> PyResult { Ok(self.inner.iterations) } + + #[setter] + #[pyo3(name = "iterations")] + fn py_set_iterations(&mut self, v: usize) { self.inner.iterations = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("KMeans", "KMeans", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// How the distance between two merged clusters is defined. +/// +/// Rust: `learn::cluster::Linkage` +#[pyclass(name = "Linkage", module = "numeria.learn.cluster", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyLinkage { + Single, + Complete, + Average, + Centroid, +} +impl PyLinkage { + pub fn to_rust(&self) -> rust_physics_engine::learn::cluster::Linkage { match self { + Self::Single => rust_physics_engine::learn::cluster::Linkage::Single, + Self::Complete => rust_physics_engine::learn::cluster::Linkage::Complete, + Self::Average => rust_physics_engine::learn::cluster::Linkage::Average, + Self::Centroid => rust_physics_engine::learn::cluster::Linkage::Centroid, + } } + pub fn from_rust(v: &rust_physics_engine::learn::cluster::Linkage) -> Self { match v { + rust_physics_engine::learn::cluster::Linkage::Single => Self::Single, + rust_physics_engine::learn::cluster::Linkage::Complete => Self::Complete, + rust_physics_engine::learn::cluster::Linkage::Average => Self::Average, + rust_physics_engine::learn::cluster::Linkage::Centroid => Self::Centroid, + } } +} +#[pymethods] +impl PyLinkage { + fn __repr__(&self) -> &'static str { + match self { + Self::Single => "Linkage.Single", + Self::Complete => "Linkage.Complete", + Self::Average => "Linkage.Average", + Self::Centroid => "Linkage.Centroid", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A fitted Gaussian process. +/// +/// Rust: `learn::gp::Gp` +#[pyclass(name = "Gp", module = "numeria.learn.gp", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyGp { pub inner: rust_physics_engine::learn::gp::Gp } +#[pymethods] +impl PyGp { + /// Conditions the process on training data. + /// + /// Errors: + /// + /// `SolveError::InvalidArgument` for an invalid kernel, negative + /// noise, an empty or ragged dataset, or non-finite values; + /// `SolveError::DimensionMismatch` if the target count does not + /// match the input count; + /// `SolveError::NotPositiveDefinite` if the covariance matrix + /// cannot be factored even with jitter. + /// + /// Rust: `learn::gp::Gp::fit` + #[pyo3(name = "fit")] + #[staticmethod] + #[pyo3(signature = (kernel, noise, x, y))] + fn fit(kernel: crate::generated::types::PyKernelFn, noise: f64, x: Vec>, y: Vec) -> PyResult { + let kernel = kernel.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::learn::gp::Gp::fit(kernel, noise, &x, &y)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(crate::generated::types::PyGp { inner: __v }) + } + + /// How many points the process was conditioned on. + /// + /// Rust: `learn::gp::Gp::len` + #[pyo3(name = "len")] + #[pyo3(signature = ())] + fn len(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.len()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Whether the process has no training data. Always false, since + /// `Gp::fit` refuses an empty dataset; present because clippy asks + /// for it alongside `len`. + /// + /// Rust: `learn::gp::Gp::is_empty` + #[pyo3(name = "is_empty")] + #[pyo3(signature = ())] + fn is_empty(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_empty()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// A cheap *lower bound* on the condition number of the covariance + /// matrix, taken as the squared ratio of the largest to the smallest + /// diagonal entry of its Cholesky factor. + /// + /// A lower bound, not an estimate: the true condition number can be + /// an order of magnitude or two above this, since the factor's + /// diagonal says nothing about how the off-diagonal mass is + /// arranged. It is useful for noticing that a problem is badly + /// conditioned, not for predicting how badly. + /// + /// Worth having in public, because it is the number that decides how + /// much of an answer is real. A squared exponential kernel on points + /// spaced well inside its length scale produces a covariance matrix + /// that is singular to working precision -- the values it is + /// correlating are nearly the same random variable -- and the jitter + /// that makes the factorisation succeed is then what limits the + /// accuracy of everything downstream. The jitter perturbs the matrix + /// by a relative amount of its own size and the solve amplifies that + /// by the condition number, so a noiseless fit interpolates to about + /// the jitter times this -- which for a squared exponential on + /// closely spaced points can be parts in a million rather than the + /// parts in `1e16` the arithmetic would suggest. + /// + /// The remedy is not more precision. It is a shorter length scale, a + /// rougher kernel from the Matern family, or a nonzero noise, all of + /// which are statements about the model rather than about the + /// arithmetic. + /// + /// Rust: `learn::gp::Gp::condition_estimate` + #[pyo3(name = "condition_estimate")] + #[pyo3(signature = ())] + fn condition_estimate(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.condition_estimate()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The posterior mean and variance at each query point. + /// + /// The variance does not depend on the observed targets at all -- + /// see the module note. It is the prior variance minus what the data + /// locations explain, and adding observations can only reduce it. + /// + /// Errors: + /// + /// `SolveError::DimensionMismatch` if a query point has the wrong + /// dimension. + /// + /// Rust: `learn::gp::Gp::predict` + #[pyo3(name = "predict")] + #[pyo3(signature = (x_star))] + fn predict<'py>(&self, py: Python<'py>, x_star: Vec>) -> PyResult<(Vec, Vec)> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.predict(&x_star))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok((__v.0, __v.1)) + } + + /// `log p(y | X)`, the log marginal likelihood. + /// + /// Equal to `-y^T K^-1 y / 2 - log|K| / 2 - n log(2 pi) / 2`, with + /// the determinant read off the Cholesky diagonal rather than + /// computed separately -- `log|K|` is twice the sum of the logs of + /// the diagonal, which is both cheaper and better conditioned than + /// forming a determinant that underflows for any sizeable `n`. + /// + /// Rust: `learn::gp::Gp::log_marginal_likelihood` + #[pyo3(name = "log_marginal_likelihood")] + #[pyo3(signature = ())] + fn log_marginal_likelihood(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.log_marginal_likelihood()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Refits with the hyperparameters that maximise the log marginal + /// likelihood, searched by Nelder-Mead over their logarithms. + /// + /// Optimising the logarithms rather than the values keeps every + /// hyperparameter positive without a constraint, and makes the + /// search scale-free -- a length scale of `0.01` and one of `100` + /// are the same distance from `1` in log space, which is how they + /// should be treated when nothing is known about the scale. + /// + /// The likelihood surface is not concave and the search finds a + /// local optimum. `restarts` different starting points are tried, + /// spread geometrically around the current values, and the best is + /// kept. + /// + /// Errors: + /// + /// As `Gp::fit`, or `SolveError::NoConvergence` if no starting + /// point produced a usable fit. + /// + /// Rust: `learn::gp::Gp::optimize_hyperparams` + #[pyo3(name = "optimize_hyperparams")] + #[pyo3(signature = (restarts, rng))] + fn optimize_hyperparams(&self, restarts: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| self.inner.optimize_hyperparams(restarts, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(crate::generated::types::PyGp { inner: __v }) + } + + /// Draws `count` sample functions from the posterior at the given + /// points. + /// + /// Errors: + /// + /// As `Gp::predict`, plus + /// `SolveError::NotPositiveDefinite` if the joint posterior + /// covariance cannot be factored. + /// + /// Rust: `learn::gp::Gp::sample_posterior` + #[pyo3(name = "sample_posterior")] + #[pyo3(signature = (x_star, count, rng))] + fn sample_posterior(&self, x_star: Vec>, count: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult>> { + let mut rng = rng; + let __r = crate::runtime::guard(|| self.inner.sample_posterior(&x_star, count, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "kernel")] + fn py_get_kernel(&self) -> PyResult { Ok(crate::generated::types::PyKernelFn { inner: self.inner.kernel.clone() }) } + + #[getter] + #[pyo3(name = "noise")] + fn py_get_noise(&self) -> PyResult { Ok(self.inner.noise) } + + #[setter] + #[pyo3(name = "noise")] + fn py_set_noise(&mut self, v: f64) { self.inner.noise = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Gp", "Gp", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A covariance function. +/// +/// Rust: `learn::gp::KernelFn` +#[pyclass(name = "KernelFn", module = "numeria.learn.gp", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyKernelFn { pub inner: rust_physics_engine::learn::gp::KernelFn } +#[pymethods] +impl PyKernelFn { + /// Evaluates the covariance between two points. + /// + /// Rust: `learn::gp::KernelFn::eval` + #[pyo3(name = "eval")] + #[pyo3(signature = (a, b))] + fn eval<'py>(&self, py: Python<'py>, a: Vec, b: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.eval(&a, &b))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Whether every length scale and amplitude is positive and finite, + /// which is what makes the function a valid covariance. + /// + /// Rust: `learn::gp::KernelFn::is_valid` + #[pyo3(name = "is_valid")] + #[pyo3(signature = ())] + fn is_valid(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_valid()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The hyperparameters as a flat vector, in the order + /// `KernelFn::with_parameters` expects them back. + /// + /// Rust: `learn::gp::KernelFn::parameters` + #[pyo3(name = "parameters")] + #[pyo3(signature = ())] + fn parameters<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.parameters())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Rebuilds the kernel from a flat parameter vector. + /// + /// Errors: + /// + /// `SolveError::DimensionMismatch` if the count does not match. + /// + /// Rust: `learn::gp::KernelFn::with_parameters` + #[pyo3(name = "with_parameters")] + #[pyo3(signature = (values))] + fn with_parameters(&self, values: Vec) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.with_parameters(&values)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(crate::generated::types::PyKernelFn { inner: __v }) + } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("KernelFn", "KernelFn", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// The activation applied after a layer's affine map. +/// +/// Rust: `learn::nn::Act` +#[pyclass(name = "Act", module = "numeria.learn.nn", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyAct { + Relu, + Sigmoid, + Tanh, + Identity, + Softmax, +} +impl PyAct { + pub fn to_rust(&self) -> rust_physics_engine::learn::nn::Act { match self { + Self::Relu => rust_physics_engine::learn::nn::Act::Relu, + Self::Sigmoid => rust_physics_engine::learn::nn::Act::Sigmoid, + Self::Tanh => rust_physics_engine::learn::nn::Act::Tanh, + Self::Identity => rust_physics_engine::learn::nn::Act::Identity, + Self::Softmax => rust_physics_engine::learn::nn::Act::Softmax, + } } + pub fn from_rust(v: &rust_physics_engine::learn::nn::Act) -> Self { match v { + rust_physics_engine::learn::nn::Act::Relu => Self::Relu, + rust_physics_engine::learn::nn::Act::Sigmoid => Self::Sigmoid, + rust_physics_engine::learn::nn::Act::Tanh => Self::Tanh, + rust_physics_engine::learn::nn::Act::Identity => Self::Identity, + rust_physics_engine::learn::nn::Act::Softmax => Self::Softmax, + } } +} +#[pymethods] +impl PyAct { + fn __repr__(&self) -> &'static str { + match self { + Self::Relu => "Act.Relu", + Self::Sigmoid => "Act.Sigmoid", + Self::Tanh => "Act.Tanh", + Self::Identity => "Act.Identity", + Self::Softmax => "Act.Softmax", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// The gradient of the loss with respect to every parameter, shaped +/// like the network itself. +/// +/// Rust: `learn::nn::Gradients` +#[pyclass(name = "Gradients", module = "numeria.learn.nn", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyGradients { pub inner: rust_physics_engine::learn::nn::Gradients } +#[pymethods] +impl PyGradients { + /// Builds a `Gradients` from its fields. + #[new] + #[pyo3(signature = (layers))] + fn __new__(layers: Vec<(crate::generated::types::PyMatrixArg, Vec)>) -> Self { + let layers = layers.into_iter().map(|__e| (__e.0.0, __e.1)).collect::>(); + Self { inner: rust_physics_engine::learn::nn::Gradients { layers: layers } } + } + + /// The Euclidean norm over every parameter, used to compare a + /// gradient against a finite-difference estimate. + /// + /// Rust: `learn::nn::Gradients::norm` + #[pyo3(name = "norm")] + #[pyo3(signature = ())] + fn norm(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.norm()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "layers")] + fn py_get_layers(&self) -> PyResult)>> { Ok(self.inner.layers.clone().into_iter().map(|__x| (crate::generated::types::PyMatrix { inner: __x.0 }, __x.1)).collect::>()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Gradients", "Gradients", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// What the network is asked to minimise. +/// +/// Rust: `learn::nn::Loss` +#[pyclass(name = "Loss", module = "numeria.learn.nn", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyLoss { + Mse, + CrossEntropy, +} +impl PyLoss { + pub fn to_rust(&self) -> rust_physics_engine::learn::nn::Loss { match self { + Self::Mse => rust_physics_engine::learn::nn::Loss::Mse, + Self::CrossEntropy => rust_physics_engine::learn::nn::Loss::CrossEntropy, + } } + pub fn from_rust(v: &rust_physics_engine::learn::nn::Loss) -> Self { match v { + rust_physics_engine::learn::nn::Loss::Mse => Self::Mse, + rust_physics_engine::learn::nn::Loss::CrossEntropy => Self::CrossEntropy, + } } +} +#[pymethods] +impl PyLoss { + fn __repr__(&self) -> &'static str { + match self { + Self::Mse => "Loss.Mse", + Self::CrossEntropy => "Loss.CrossEntropy", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A fully connected feed-forward network. +/// +/// Rust: `learn::nn::Mlp` +#[pyclass(name = "Mlp", module = "numeria.learn.nn", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyMlp { pub inner: rust_physics_engine::learn::nn::Mlp } +#[pymethods] +impl PyMlp { + /// Builds a network with the given layer sizes, the first being the + /// input width and the last the output width. + /// + /// Weights are drawn from a normal distribution scaled by fan-in -- + /// He for rectifiers, Xavier otherwise -- and biases start at zero. + /// See the module note on why neither choice is cosmetic. + /// + /// Errors: + /// + /// `SolveError::InvalidArgument` for fewer than two sizes or any + /// zero-width layer. + /// + /// Rust: `learn::nn::Mlp::new` + #[new] + #[pyo3(signature = (sizes, activation, output_activation, rng))] + fn __new__(sizes: Vec, activation: crate::generated::types::PyAct, output_activation: crate::generated::types::PyAct, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let activation = activation.to_rust(); + let output_activation = output_activation.to_rust(); + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::learn::nn::Mlp::new(&sizes, activation, output_activation, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(crate::generated::types::PyMlp { inner: __v }) + } + + /// The input width the network expects. + /// + /// Rust: `learn::nn::Mlp::input_size` + #[pyo3(name = "input_size")] + #[pyo3(signature = ())] + fn input_size(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.input_size()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The output width. + /// + /// Rust: `learn::nn::Mlp::output_size` + #[pyo3(name = "output_size")] + #[pyo3(signature = ())] + fn output_size(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.output_size()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The total parameter count. + /// + /// Rust: `learn::nn::Mlp::parameter_count` + #[pyo3(name = "parameter_count")] + #[pyo3(signature = ())] + fn parameter_count(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.parameter_count()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The pre-activation of every layer -- the affine map's output, + /// before the activation is applied. + /// + /// Worth having in public because it is what says how close a + /// rectifier unit is to its kink. A unit whose pre-activation is + /// near zero is where a finite-difference gradient check is entitled + /// to disagree with the analytic gradient, and where a unit is about + /// to die or come back to life. + /// + /// Errors: + /// + /// `SolveError::DimensionMismatch` if the input width is wrong. + /// + /// Rust: `learn::nn::Mlp::preactivations` + #[pyo3(name = "preactivations")] + #[pyo3(signature = (x))] + fn preactivations<'py>(&self, py: Python<'py>, x: Vec) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.preactivations(&x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) + } + + /// Runs the network forward. + /// + /// Errors: + /// + /// `SolveError::DimensionMismatch` if the input width is wrong. + /// + /// Rust: `learn::nn::Mlp::forward` + #[pyo3(name = "forward")] + #[pyo3(signature = (x))] + fn forward<'py>(&self, py: Python<'py>, x: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.forward(&x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) + } + + /// The index of the largest output, for a classifier. + /// + /// Errors: + /// + /// As `Mlp::forward`. + /// + /// Rust: `learn::nn::Mlp::predict` + #[pyo3(name = "predict")] + #[pyo3(signature = (x))] + fn predict<'py>(&self, py: Python<'py>, x: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.predict(&x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) + } + + /// The loss on one example. + /// + /// Errors: + /// + /// `SolveError::DimensionMismatch` on a width mismatch; + /// `SolveError::InvalidArgument` if cross-entropy is asked for + /// without a softmax output. + /// + /// Rust: `learn::nn::Mlp::example_loss` + #[pyo3(name = "example_loss")] + #[pyo3(signature = (x, y, loss))] + fn example_loss<'py>(&self, py: Python<'py>, x: Vec, y: Vec, loss: crate::generated::types::PyLoss) -> PyResult { + let loss = loss.to_rust(); + let __r = py.detach(move || crate::runtime::guard(move || self.inner.example_loss(&x, &y, loss))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) + } + + /// The mean loss over a dataset. + /// + /// Errors: + /// + /// As `Mlp::example_loss`, plus + /// `SolveError::InvalidArgument` for an empty dataset. + /// + /// Rust: `learn::nn::Mlp::loss` + #[pyo3(name = "loss")] + #[pyo3(signature = (data, loss))] + fn loss<'py>(&self, py: Python<'py>, data: Vec<(Vec, Vec)>, loss: crate::generated::types::PyLoss) -> PyResult { + let data = data.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let loss = loss.to_rust(); + let __r = py.detach(move || crate::runtime::guard(move || self.inner.loss(&data, loss))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) + } + + /// The gradient of the loss on one example, by backpropagation. + /// + /// Errors: + /// + /// As `Mlp::example_loss`. + /// + /// Rust: `learn::nn::Mlp::backward` + #[pyo3(name = "backward")] + #[pyo3(signature = (x, y, loss))] + fn backward(&self, x: Vec, y: Vec, loss: crate::generated::types::PyLoss) -> PyResult { + let loss = loss.to_rust(); + let __r = crate::runtime::guard(|| self.inner.backward(&x, &y, loss)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(crate::generated::types::PyGradients { inner: __v }) + } + + /// Compares the analytic gradient against a central difference, + /// returning the relative difference of the two as vectors. + /// + /// This is the test that decides whether backpropagation was + /// implemented correctly. Training curves do not: descent reduces + /// the loss under a wrong gradient too, just more slowly and towards + /// somewhere else. + /// + /// A central difference is used rather than a forward one because + /// its truncation error is `O(h^2)` instead of `O(h)`, which with + /// `h = 1e-5` puts the truncation and the rounding at about the same + /// size and leaves eight digits of agreement to look for. A forward + /// difference would leave four, which is not enough to distinguish a + /// correct gradient from a nearly correct one. + /// + /// Errors: + /// + /// As `Mlp::backward`. + /// + /// Rust: `learn::nn::Mlp::numerical_grad_check` + #[pyo3(name = "numerical_grad_check")] + #[pyo3(signature = (x, y, loss))] + fn numerical_grad_check<'py>(&self, py: Python<'py>, x: Vec, y: Vec, loss: crate::generated::types::PyLoss) -> PyResult { + let loss = loss.to_rust(); + let __r = py.detach(move || crate::runtime::guard(move || self.inner.numerical_grad_check(&x, &y, loss))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) + } + + /// Trains by mini-batch stochastic gradient descent, returning the + /// mean loss after each epoch. + /// + /// Errors: + /// + /// `SolveError::InvalidArgument` for an empty dataset, a + /// non-positive batch size, or a non-finite learning rate. + /// + /// Rust: `learn::nn::Mlp::train_sgd` + #[pyo3(name = "train_sgd")] + #[pyo3(signature = (data, loss, epochs, lr, batch, rng))] + fn train_sgd(&mut self, data: Vec<(Vec, Vec)>, loss: crate::generated::types::PyLoss, epochs: usize, lr: f64, batch: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let data = data.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let loss = loss.to_rust(); + let mut rng = rng; + let __r = crate::runtime::guard(|| self.inner.train_sgd(&data, loss, epochs, lr, batch, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) + } + + /// Trains with Adam, returning the mean loss after each epoch. + /// + /// Adam keeps a running mean and a running mean square of each + /// parameter's gradient and steps by their ratio, which makes the + /// step size roughly scale-free: multiplying every gradient by a + /// constant leaves the update almost unchanged. The bias correction + /// matters most at the start, where both running averages begin at + /// zero and would otherwise make the first steps far too small. + /// + /// Errors: + /// + /// As `Mlp::train_sgd`. + /// + /// Rust: `learn::nn::Mlp::train_adam` + #[pyo3(name = "train_adam")] + #[pyo3(signature = (data, loss, epochs, lr, batch, rng))] + fn train_adam(&mut self, data: Vec<(Vec, Vec)>, loss: crate::generated::types::PyLoss, epochs: usize, lr: f64, batch: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let data = data.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let loss = loss.to_rust(); + let mut rng = rng; + let __r = crate::runtime::guard(|| self.inner.train_adam(&data, loss, epochs, lr, batch, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "layers")] + fn py_get_layers(&self) -> PyResult)>> { Ok(self.inner.layers.clone().into_iter().map(|__x| (crate::generated::types::PyMatrix { inner: __x.0 }, __x.1)).collect::>()) } + + #[getter] + #[pyo3(name = "activation")] + fn py_get_activation(&self) -> PyResult { Ok(crate::generated::types::PyAct::from_rust(&self.inner.activation.clone())) } + + #[getter] + #[pyo3(name = "output_activation")] + fn py_get_output_activation(&self) -> PyResult { Ok(crate::generated::types::PyAct::from_rust(&self.inner.output_activation.clone())) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Mlp", "Mlp", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// An ensemble of trees grown on bootstrap samples. +/// +/// Rust: `learn::tree::Forest` +#[pyclass(name = "Forest", module = "numeria.learn.tree", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyForest { pub inner: rust_physics_engine::learn::tree::Forest } +#[pymethods] +impl PyForest { + /// Builds a `Forest` from its fields. + #[new] + #[pyo3(signature = (trees, out_of_bag))] + fn __new__(trees: Vec, out_of_bag: Vec>) -> Self { + let trees = trees.into_iter().map(|__e| __e.inner).collect::>(); + Self { inner: rust_physics_engine::learn::tree::Forest { trees: trees, out_of_bag: out_of_bag } } + } + + #[getter] + #[pyo3(name = "trees")] + fn py_get_trees(&self) -> PyResult> { Ok(self.inner.trees.clone().into_iter().map(|__x| crate::generated::types::PyTree { inner: __x }).collect::>()) } + + #[getter] + #[pyo3(name = "out_of_bag")] + fn py_get_out_of_bag(&self) -> PyResult>> { Ok(self.inner.out_of_bag.clone()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Forest", "Forest", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A gradient boosted regressor: a constant plus a sequence of shallow +/// trees. +/// +/// Rust: `learn::tree::Gbm` +#[pyclass(name = "Gbm", module = "numeria.learn.tree", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyGbm { pub inner: rust_physics_engine::learn::tree::Gbm } +#[pymethods] +impl PyGbm { + /// Builds a `Gbm` from its fields. + #[new] + #[pyo3(signature = (base, trees, learning_rate, loss_history))] + fn __new__(base: f64, trees: Vec, learning_rate: f64, loss_history: Vec) -> Self { + let trees = trees.into_iter().map(|__e| __e.inner).collect::>(); + Self { inner: rust_physics_engine::learn::tree::Gbm { base: base, trees: trees, learning_rate: learning_rate, loss_history: loss_history } } + } + + #[getter] + #[pyo3(name = "base")] + fn py_get_base(&self) -> PyResult { Ok(self.inner.base) } + + #[setter] + #[pyo3(name = "base")] + fn py_set_base(&mut self, v: f64) { self.inner.base = v; } + + #[getter] + #[pyo3(name = "trees")] + fn py_get_trees(&self) -> PyResult> { Ok(self.inner.trees.clone().into_iter().map(|__x| crate::generated::types::PyTree { inner: __x }).collect::>()) } + + #[getter] + #[pyo3(name = "learning_rate")] + fn py_get_learning_rate(&self) -> PyResult { Ok(self.inner.learning_rate) } + + #[setter] + #[pyo3(name = "learning_rate")] + fn py_set_learning_rate(&mut self, v: f64) { self.inner.learning_rate = v; } + + #[getter] + #[pyo3(name = "loss_history")] + fn py_get_loss_history(&self) -> PyResult> { Ok(self.inner.loss_history.clone()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Gbm", "Gbm", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A fitted decision tree. Node zero is the root. +/// +/// Rust: `learn::tree::Tree` +#[pyclass(name = "Tree", module = "numeria.learn.tree", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyTree { pub inner: rust_physics_engine::learn::tree::Tree } +#[pymethods] +impl PyTree { + /// Builds a `Tree` from its fields. + #[new] + #[pyo3(signature = (nodes, n_features))] + fn __new__(nodes: Vec, n_features: usize) -> Self { + let nodes = nodes.into_iter().map(|__e| __e.inner).collect::>(); + Self { inner: rust_physics_engine::learn::tree::Tree { nodes: nodes, n_features: n_features } } + } + + #[getter] + #[pyo3(name = "nodes")] + fn py_get_nodes(&self) -> PyResult> { Ok(self.inner.nodes.clone().into_iter().map(|__x| crate::generated::types::PyTreeNode { inner: __x }).collect::>()) } + + #[getter] + #[pyo3(name = "n_features")] + fn py_get_n_features(&self) -> PyResult { Ok(self.inner.n_features) } + + #[setter] + #[pyo3(name = "n_features")] + fn py_set_n_features(&mut self, v: usize) { self.inner.n_features = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Tree", "Tree", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A node of a fitted tree. +/// +/// Rust: `learn::tree::TreeNode` +#[pyclass(name = "TreeNode", module = "numeria.learn.tree", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyTreeNode { pub inner: rust_physics_engine::learn::tree::TreeNode } +#[pymethods] +impl PyTreeNode { + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("TreeNode", "TreeNode", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} diff --git a/bindings/python/src/generated/types/linalg.rs b/bindings/python/src/generated/types/linalg.rs new file mode 100644 index 0000000..08e89c6 --- /dev/null +++ b/bindings/python/src/generated/types/linalg.rs @@ -0,0 +1,974 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; + + +/// +/// Rust: `linalg::Mat3` +#[pyclass(name = "Mat3", module = "numeria.linalg", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyMat3 { pub inner: rust_physics_engine::linalg::Mat3 } +#[pymethods] +impl PyMat3 { + /// Returns the 3x3 zero matrix. + /// + /// Rust: `linalg::Mat3::zero` + #[pyo3(name = "zero")] + #[staticmethod] + #[pyo3(signature = ())] + fn zero() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::linalg::Mat3::zero()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMat3 { inner: __v }) + } + + /// Returns the 3x3 identity matrix. + /// + /// Rust: `linalg::Mat3::identity` + #[pyo3(name = "identity")] + #[staticmethod] + #[pyo3(signature = ())] + fn identity() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::linalg::Mat3::identity()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMat3 { inner: __v }) + } + + /// Constructs a 3x3 matrix from three row arrays. + /// + /// Rust: `linalg::Mat3::from_rows` + #[pyo3(name = "from_rows")] + #[staticmethod] + #[pyo3(signature = (r0, r1, r2))] + fn from_rows(r0: Vec, r1: Vec, r2: Vec) -> PyResult { + let r0 = <[f64; 3]>::try_from(r0).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 3 values, got {}", __v.len())))?; + let r1 = <[f64; 3]>::try_from(r1).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 3 values, got {}", __v.len())))?; + let r2 = <[f64; 3]>::try_from(r2).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 3 values, got {}", __v.len())))?; + let __r = crate::runtime::guard(|| rust_physics_engine::linalg::Mat3::from_rows(r0, r1, r2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMat3 { inner: __v }) + } + + /// Computes the determinant using the Sarrus rule (cofactor expansion along the first row). + /// + /// Rust: `linalg::Mat3::determinant` + #[pyo3(name = "determinant")] + #[pyo3(signature = ())] + fn determinant(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.determinant()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Returns the transpose of this matrix: `A^T[i][j] = A[j][i]`. + /// + /// Rust: `linalg::Mat3::transpose` + #[pyo3(name = "transpose")] + #[pyo3(signature = ())] + fn transpose(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.transpose()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMat3 { inner: __v }) + } + + /// Computes the matrix inverse via the adjugate method: A⁻¹ = adj(A) / det(A). + /// + /// Rust: `linalg::Mat3::inverse` + #[pyo3(name = "inverse")] + #[pyo3(signature = ())] + fn inverse(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.inverse()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyMat3 { inner: __x })) + } + + /// Returns the trace (sum of diagonal elements) of the matrix. + /// + /// Rust: `linalg::Mat3::trace` + #[pyo3(name = "trace")] + #[pyo3(signature = ())] + fn trace(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.trace()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Multiplies this matrix by a column vector: result = A × v. + /// + /// Rust: `linalg::Mat3::mul_vec` + #[pyo3(name = "mul_vec")] + #[pyo3(signature = (v))] + fn mul_vec(&self, v: crate::generated::types::PyVec3Arg) -> PyResult { + let v = v.0; + let __r = crate::runtime::guard(|| self.inner.mul_vec(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Multiplies two 3x3 matrices: result = A × B. + /// + /// Rust: `linalg::Mat3::mul_mat` + #[pyo3(name = "mul_mat")] + #[pyo3(signature = (other))] + fn mul_mat(&self, other: crate::generated::types::PyMat3) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.mul_mat(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMat3 { inner: __v }) + } + + /// Returns a uniform scaling matrix: diag(s, s, s). + /// + /// Rust: `linalg::Mat3::scale` + #[pyo3(name = "scale")] + #[staticmethod] + #[pyo3(signature = (s))] + fn scale(s: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::linalg::Mat3::scale(s)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMat3 { inner: __v }) + } + + /// Principal axes of a symmetric 3×3 matrix (e.g. an inertia + /// tensor): eigenvalues in descending order paired with unit + /// eigenvectors as the columns of the returned matrix. + /// + /// Fails with `InvalidArgument` when the matrix is not symmetric. + /// + /// Rust: `linalg::Mat3::principal_axes` + #[pyo3(name = "principal_axes")] + #[pyo3(signature = ())] + fn principal_axes(&self) -> PyResult<(Vec, crate::generated::types::PyMat3)> { + let __r = crate::runtime::guard(|| self.inner.principal_axes()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok((__v.0.to_vec(), crate::generated::types::PyMat3 { inner: __v.1 })) + } + + /// Eigen-decomposition of a symmetric 3×3 matrix by a local cyclic + /// Jacobi iteration (no dense-matrix machinery): eigenvalues in + /// descending order paired with unit eigenvector columns. + /// + /// Panics: + /// Panics if the matrix is not symmetric within 1e-8·‖A‖. + /// + /// Rust: `linalg::Mat3::principal_axes_3x3` + #[pyo3(name = "principal_axes_3x3")] + #[pyo3(signature = ())] + fn principal_axes_3x3(&self) -> PyResult<(Vec, crate::generated::types::PyMat3)> { + let __r = crate::runtime::guard(|| self.inner.principal_axes_3x3()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0.to_vec(), crate::generated::types::PyMat3 { inner: __v.1 })) + } + + /// Multiplies every element of the matrix by a scalar. + /// + /// Rust: `linalg::Mat3::mul_scalar` + #[pyo3(name = "mul_scalar")] + #[pyo3(signature = (s))] + fn mul_scalar(&self, s: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.mul_scalar(s)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMat3 { inner: __v }) + } + + #[getter] + #[pyo3(name = "data")] + fn py_get_data(&self) -> PyResult>> { Ok(self.inner.data.clone().into_iter().map(|__x| __x.to_vec()).collect::>()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Mat3", "Mat3", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A dense 4x4 matrix with fixed-size storage. +/// +/// Rust: `linalg::Mat4` +#[pyclass(name = "Mat4", module = "numeria.linalg", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyLinalgMat4 { pub inner: rust_physics_engine::linalg::Mat4 } +#[pymethods] +impl PyLinalgMat4 { + /// The 4x4 zero matrix. + /// + /// Rust: `linalg::Mat4::zero` + #[pyo3(name = "zero")] + #[staticmethod] + #[pyo3(signature = ())] + fn zero() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::linalg::Mat4::zero()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLinalgMat4 { inner: __v }) + } + + /// The 4x4 identity matrix. + /// + /// Rust: `linalg::Mat4::identity` + #[pyo3(name = "identity")] + #[staticmethod] + #[pyo3(signature = ())] + fn identity() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::linalg::Mat4::identity()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLinalgMat4 { inner: __v }) + } + + /// Construct from four row arrays. + /// + /// Rust: `linalg::Mat4::from_rows` + #[pyo3(name = "from_rows")] + #[staticmethod] + #[pyo3(signature = (r0, r1, r2, r3))] + fn from_rows(r0: Vec, r1: Vec, r2: Vec, r3: Vec) -> PyResult { + let r0 = <[f64; 4]>::try_from(r0).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 4 values, got {}", __v.len())))?; + let r1 = <[f64; 4]>::try_from(r1).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 4 values, got {}", __v.len())))?; + let r2 = <[f64; 4]>::try_from(r2).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 4 values, got {}", __v.len())))?; + let r3 = <[f64; 4]>::try_from(r3).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 4 values, got {}", __v.len())))?; + let __r = crate::runtime::guard(|| rust_physics_engine::linalg::Mat4::from_rows(r0, r1, r2, r3)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLinalgMat4 { inner: __v }) + } + + /// Matrix product. + /// + /// Rust: `linalg::Mat4::mul_mat` + #[pyo3(name = "mul_mat")] + #[pyo3(signature = (other))] + fn mul_mat(&self, other: crate::generated::types::PyLinalgMat4) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.mul_mat(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLinalgMat4 { inner: __v }) + } + + /// Matrix-vector product. + /// + /// Rust: `linalg::Mat4::mul_vec4` + #[pyo3(name = "mul_vec4")] + #[pyo3(signature = (v))] + fn mul_vec4<'py>(&self, py: Python<'py>, v: Vec) -> PyResult> { + let v = <[f64; 4]>::try_from(v).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 4 values, got {}", __v.len())))?; + let __r = py.detach(move || crate::runtime::guard(move || self.inner.mul_vec4(v))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_vec()) + } + + /// Transpose. + /// + /// Rust: `linalg::Mat4::transpose` + #[pyo3(name = "transpose")] + #[pyo3(signature = ())] + fn transpose(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.transpose()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLinalgMat4 { inner: __v }) + } + + /// Trace. + /// + /// Rust: `linalg::Mat4::trace` + #[pyo3(name = "trace")] + #[pyo3(signature = ())] + fn trace(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.trace()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Determinant via LU on the general dense matrix. + /// + /// Rust: `linalg::Mat4::determinant` + #[pyo3(name = "determinant")] + #[pyo3(signature = ())] + fn determinant(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.determinant()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Inverse (None if singular). + /// + /// Rust: `linalg::Mat4::inverse` + #[pyo3(name = "inverse")] + #[pyo3(signature = ())] + fn inverse(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.inverse()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyLinalgMat4 { inner: __x })) + } + + /// Convert to a general dense matrix. + /// + /// Rust: `linalg::Mat4::to_matrix` + #[pyo3(name = "to_matrix")] + #[pyo3(signature = ())] + fn to_matrix(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.to_matrix()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) + } + + /// Convert from a 4x4 general dense matrix. + /// + /// Panics: + /// Panics unless `m` is 4x4. + /// + /// Rust: `linalg::Mat4::from_matrix` + #[pyo3(name = "from_matrix")] + #[staticmethod] + #[pyo3(signature = (m))] + fn from_matrix(m: crate::generated::types::PyMatrixArg) -> PyResult { + let m = m.0; + let __r = crate::runtime::guard(|| rust_physics_engine::linalg::Mat4::from_matrix(&m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLinalgMat4 { inner: __v }) + } + + #[getter] + #[pyo3(name = "data")] + fn py_get_data(&self) -> PyResult>> { Ok(self.inner.data.clone().into_iter().map(|__x| __x.to_vec()).collect::>()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Mat4", "Mat4", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Eigen-decomposition of a symmetric matrix: A·vᵢ = λᵢ·vᵢ. +/// +/// `values[i]` pairs with column i of `vectors`; entries are sorted in +/// descending eigenvalue order and the vectors are orthonormal. +/// +/// Rust: `linalg::eigen::SymEigen` +#[pyclass(name = "SymEigen", module = "numeria.linalg.eigen", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PySymEigen { pub inner: rust_physics_engine::linalg::eigen::SymEigen } +#[pymethods] +impl PySymEigen { + /// Builds a `SymEigen` from its fields. + #[new] + #[pyo3(signature = (values, vectors))] + fn __new__(values: Vec, vectors: crate::generated::types::PyMatrixArg) -> Self { + let vectors = vectors.0; + Self { inner: rust_physics_engine::linalg::eigen::SymEigen { values: values, vectors: vectors } } + } + + #[getter] + #[pyo3(name = "values")] + fn py_get_values(&self) -> PyResult> { Ok(self.inner.values.clone()) } + + #[getter] + #[pyo3(name = "vectors")] + fn py_get_vectors(&self) -> PyResult { Ok(crate::generated::types::PyMatrix { inner: self.inner.vectors.clone() }) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("SymEigen", "SymEigen", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Packed LU factorization P·A = L·U. +/// +/// `lu` stores U on and above the diagonal and the unit-lower-triangular +/// L (implicit ones on the diagonal) below it. `perm[i]` is the row of A +/// that ended up in position i; `sign` is the permutation's parity (±1). +/// +/// Rust: `linalg::lu::Lu` +#[pyclass(name = "Lu", module = "numeria.linalg.lu", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyLu { pub inner: rust_physics_engine::linalg::lu::Lu } +#[pymethods] +impl PyLu { + /// Builds a `Lu` from its fields. + #[new] + #[pyo3(signature = (lu, perm, sign))] + fn __new__(lu: crate::generated::types::PyMatrixArg, perm: Vec, sign: f64) -> Self { + let lu = lu.0; + Self { inner: rust_physics_engine::linalg::lu::Lu { lu: lu, perm: perm, sign: sign } } + } + + /// Solves A·x = b by forward and back substitution on the stored + /// factors. Fails with `DimensionMismatch` if `b.len() != n`. + /// + /// Rust: `linalg::lu::Lu::solve` + #[pyo3(name = "solve")] + #[pyo3(signature = (b))] + fn solve<'py>(&self, py: Python<'py>, b: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.solve(&b))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) + } + + /// Solves A·X = B column by column. + /// + /// Rust: `linalg::lu::Lu::solve_matrix` + #[pyo3(name = "solve_matrix")] + #[pyo3(signature = (b))] + fn solve_matrix(&self, b: crate::generated::types::PyMatrixArg) -> PyResult { + let b = b.0; + let __r = crate::runtime::guard(|| self.inner.solve_matrix(&b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) + } + + /// Determinant of A: sign · Π uᵢᵢ. + /// + /// Rust: `linalg::lu::Lu::determinant` + #[pyo3(name = "determinant")] + #[pyo3(signature = ())] + fn determinant(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.determinant()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Inverse of A, computed by solving A·X = I. + /// + /// Rust: `linalg::lu::Lu::inverse` + #[pyo3(name = "inverse")] + #[pyo3(signature = ())] + fn inverse(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.inverse()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) + } + + #[getter] + #[pyo3(name = "lu")] + fn py_get_lu(&self) -> PyResult { Ok(crate::generated::types::PyMatrix { inner: self.inner.lu.clone() }) } + + #[getter] + #[pyo3(name = "perm")] + fn py_get_perm(&self) -> PyResult> { Ok(self.inner.perm.clone()) } + + #[getter] + #[pyo3(name = "sign")] + fn py_get_sign(&self) -> PyResult { Ok(self.inner.sign) } + + #[setter] + #[pyo3(name = "sign")] + fn py_set_sign(&mut self, v: f64) { self.inner.sign = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Lu", "Lu", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Dense matrix with row-major storage: element (r, c) lives at +/// `data[r * cols + c]`. +/// +/// Rust: `linalg::matrix::Matrix` +#[pyclass(name = "Matrix", module = "numeria.linalg.matrix", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyMatrix { pub inner: rust_physics_engine::linalg::matrix::Matrix } +impl PyMatrix { + fn wrap_row(&self, i: isize) -> PyResult { + let n = self.inner.rows as isize; + let j = if i < 0 { i + n } else { i }; + if j < 0 || j >= n { + return Err(pyo3::exceptions::PyIndexError::new_err("row index out of range")); + } + Ok(j as usize) + } + fn wrap_col(&self, i: isize) -> PyResult { + let n = self.inner.cols as isize; + let j = if i < 0 { i + n } else { i }; + if j < 0 || j >= n { + return Err(pyo3::exceptions::PyIndexError::new_err("column index out of range")); + } + Ok(j as usize) + } +} +#[pymethods] +impl PyMatrix { + /// Builds a `Matrix` from its fields. + #[new] + #[pyo3(signature = (rows, cols, data))] + fn __new__(rows: usize, cols: usize, data: Vec) -> Self { + + Self { inner: rust_physics_engine::linalg::matrix::Matrix { rows: rows, cols: cols, data: data } } + } + + /// Matrix of the given shape filled with zeros. + /// + /// Panics: + /// Panics if `rows` or `cols` is zero. + /// + /// Rust: `linalg::matrix::Matrix::zeros` + #[pyo3(name = "zeros")] + #[staticmethod] + #[pyo3(signature = (rows, cols))] + fn zeros(rows: usize, cols: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::linalg::matrix::Matrix::zeros(rows, cols)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) + } + + /// n×n identity matrix. + /// + /// Panics: + /// Panics if `n` is zero. + /// + /// Rust: `linalg::matrix::Matrix::identity` + #[pyo3(name = "identity")] + #[staticmethod] + #[pyo3(signature = (n))] + fn identity(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::linalg::matrix::Matrix::identity(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) + } + + /// Builds a matrix from row slices. All rows must be non-empty and of + /// equal length; otherwise `SolveError::DimensionMismatch` is returned. + /// + /// Rust: `linalg::matrix::Matrix::from_rows` + #[pyo3(name = "from_rows")] + #[staticmethod] + #[pyo3(signature = (rows))] + fn from_rows(rows: Vec>) -> PyResult { + let rows__b: Vec<&[f64]> = rows.iter().map(|__b| (*__b).as_slice()).collect(); + let __r = crate::runtime::guard(|| rust_physics_engine::linalg::matrix::Matrix::from_rows(&rows__b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) + } + + /// Builds a matrix by evaluating `f(row, col)` at every position. + /// + /// Panics: + /// Panics if `rows` or `cols` is zero. + /// + /// Rust: `linalg::matrix::Matrix::from_fn` + #[pyo3(name = "from_fn")] + #[staticmethod] + #[pyo3(signature = (rows, cols, f))] + fn from_fn(rows: usize, cols: usize, f: pyo3::Py) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: usize, __a1: usize| -> f64 { __cb.call::<_, f64>((__a0, __a1), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::linalg::matrix::Matrix::from_fn(rows, cols, f)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) + } + + /// Bounds-checked element read. + /// + /// Panics: + /// Panics if `r >= rows` or `c >= cols`. + /// + /// Rust: `linalg::matrix::Matrix::get` + #[pyo3(name = "get")] + #[pyo3(signature = (r, c))] + fn get(&self, r: usize, c: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.get(r, c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Bounds-checked element write. + /// + /// Panics: + /// Panics if `r >= rows` or `c >= cols`. + /// + /// Rust: `linalg::matrix::Matrix::set` + #[pyo3(name = "set")] + #[pyo3(signature = (r, c, v))] + fn set(&mut self, r: usize, c: usize, v: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.set(r, c, v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Borrow row `r` as a slice. + /// + /// Panics: + /// Panics if `r >= rows`. + /// + /// Rust: `linalg::matrix::Matrix::row` + #[pyo3(name = "row")] + #[pyo3(signature = (r))] + fn row<'py>(&self, py: Python<'py>, r: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.row(r))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_vec()) + } + + /// Transpose: `B[c][r] = A[r][c]`. + /// + /// Rust: `linalg::matrix::Matrix::transpose` + #[pyo3(name = "transpose")] + #[pyo3(signature = ())] + fn transpose(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.transpose()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) + } + + /// Matrix product A·B; fails with `DimensionMismatch` unless + /// `self.cols == other.rows`. + /// + /// Rust: `linalg::matrix::Matrix::mul` + #[pyo3(name = "mul")] + #[pyo3(signature = (other))] + fn mul(&self, other: crate::generated::types::PyMatrixArg) -> PyResult { + let other = other.0; + let __r = crate::runtime::guard(|| self.inner.mul(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) + } + + /// Matrix-vector product A·v; fails with `DimensionMismatch` unless + /// `self.cols == v.len()`. + /// + /// Rust: `linalg::matrix::Matrix::mul_vec` + #[pyo3(name = "mul_vec")] + #[pyo3(signature = (v))] + fn mul_vec<'py>(&self, py: Python<'py>, v: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.mul_vec(&v))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(__v) + } + + /// Element-wise sum A + B; fails with `DimensionMismatch` on shape + /// disagreement. + /// + /// Rust: `linalg::matrix::Matrix::add` + #[pyo3(name = "add")] + #[pyo3(signature = (other))] + fn add(&self, other: crate::generated::types::PyMatrixArg) -> PyResult { + let other = other.0; + let __r = crate::runtime::guard(|| self.inner.add(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) + } + + /// Scalar multiple k·A. + /// + /// Rust: `linalg::matrix::Matrix::scale` + #[pyo3(name = "scale")] + #[pyo3(signature = (k))] + fn scale(&self, k: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.scale(k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) + } + + /// Frobenius norm: sqrt(Σ aᵢⱼ²). + /// + /// Rust: `linalg::matrix::Matrix::frobenius_norm` + #[pyo3(name = "frobenius_norm")] + #[pyo3(signature = ())] + fn frobenius_norm(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.frobenius_norm()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// True when the matrix is square. + /// + /// Rust: `linalg::matrix::Matrix::is_square` + #[pyo3(name = "is_square")] + #[pyo3(signature = ())] + fn is_square(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_square()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// True when the matrix is square and |A - Aᵀ| ≤ tol element-wise. + /// + /// Rust: `linalg::matrix::Matrix::is_symmetric` + #[pyo3(name = "is_symmetric")] + #[pyo3(signature = (tol))] + fn is_symmetric(&self, tol: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_symmetric(tol)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Converts a fixed-size `Mat3` into a 3×3 `Matrix`. + /// + /// Rust: `linalg::matrix::Matrix::from_mat3` + #[pyo3(name = "from_mat3")] + #[staticmethod] + #[pyo3(signature = (m))] + fn from_mat3(m: crate::generated::types::PyMat3) -> PyResult { + let m = m.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::linalg::matrix::Matrix::from_mat3(&m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) + } + + #[getter] + #[pyo3(name = "rows")] + fn py_get_rows(&self) -> PyResult { Ok(self.inner.rows) } + + #[setter] + #[pyo3(name = "rows")] + fn py_set_rows(&mut self, v: usize) { self.inner.rows = v; } + + #[getter] + #[pyo3(name = "cols")] + fn py_get_cols(&self) -> PyResult { Ok(self.inner.cols) } + + #[setter] + #[pyo3(name = "cols")] + fn py_set_cols(&mut self, v: usize) { self.inner.cols = v; } + + #[getter] + #[pyo3(name = "data")] + fn py_get_data(&self) -> PyResult> { Ok(self.inner.data.clone()) } + + fn __len__(&self) -> usize { self.inner.rows } + + /// `m[i, j]`, or `m[i]` for a whole row. + fn __getitem__(&self, py: Python<'_>, key: pyo3::Py) -> PyResult> { let k = key.bind(py); if let Ok((i, j)) = k.extract::<(isize, isize)>() { let (r, c) = (self.wrap_row(i)?, self.wrap_col(j)?); return Ok(self.inner.data[r * self.inner.cols + c].into_pyobject(py)?.unbind().into_any()); } let i = k.extract::().map_err(|_| pyo3::exceptions::PyTypeError::new_err( "index a Matrix with m[i, j] or m[i]"))?; let r = self.wrap_row(i)?; let row: Vec = self.inner.data[r * self.inner.cols..(r + 1) * self.inner.cols].to_vec(); Ok(row.into_pyobject(py)?.unbind().into_any()) } + + /// `m[i, j] = v`. + fn __setitem__(&mut self, key: (isize, isize), v: f64) -> PyResult<()> { let (r, c) = (self.wrap_row(key.0)?, self.wrap_col(key.1)?); let cols = self.inner.cols; self.inner.data[r * cols + c] = v; Ok(()) } + + /// The rows as a list of lists. + fn tolist(&self) -> Vec> { self.inner.data.chunks(self.inner.cols).map(<[f64]>::to_vec).collect() } + + /// `(rows, cols)`. + #[getter] + fn shape(&self) -> (usize, usize) { (self.inner.rows, self.inner.cols) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Matrix", "Matrix", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A `Matrix` argument, or anything that can stand in for one. +pub struct PyMatrixArg(pub rust_physics_engine::linalg::matrix::Matrix); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyMatrixArg { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result { + if let Ok(__w) = obj.extract::() { + return Ok(PyMatrixArg(__w.inner)); + } + let __rows = crate::runtime::coerce::rows(obj, "Matrix")?; + let __refs: Vec<&[f64]> = __rows.iter().map(Vec::as_slice).collect(); + rust_physics_engine::linalg::matrix::Matrix::from_rows(&__refs) + .map(PyMatrixArg) + .map_err(crate::runtime::map_solve) + } +} + + +/// QR factorization A = Q·R. +/// +/// Rust: `linalg::qr::Qr` +#[pyclass(name = "Qr", module = "numeria.linalg.qr", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyQr { pub inner: rust_physics_engine::linalg::qr::Qr } +#[pymethods] +impl PyQr { + /// Builds a `Qr` from its fields. + #[new] + #[pyo3(signature = (q, r))] + fn __new__(q: crate::generated::types::PyMatrixArg, r: crate::generated::types::PyMatrixArg) -> Self { + let q = q.0; + let r = r.0; + Self { inner: rust_physics_engine::linalg::qr::Qr { q: q, r: r } } + } + + #[getter] + #[pyo3(name = "q")] + fn py_get_q(&self) -> PyResult { Ok(crate::generated::types::PyMatrix { inner: self.inner.q.clone() }) } + + #[getter] + #[pyo3(name = "r")] + fn py_get_r(&self) -> PyResult { Ok(crate::generated::types::PyMatrix { inner: self.inner.r.clone() }) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Qr", "Qr", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Sparse matrix in CSR form: row r's entries live at indices +/// `row_ptr[r]..row_ptr[r+1]` of `col_idx`/`vals`. +/// +/// Rust: `linalg::sparse::CsrMatrix` +#[pyclass(name = "CsrMatrix", module = "numeria.linalg.sparse", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyCsrMatrix { pub inner: rust_physics_engine::linalg::sparse::CsrMatrix } +#[pymethods] +impl PyCsrMatrix { + /// Builds a `CsrMatrix` from its fields. + #[new] + #[pyo3(signature = (rows, cols, row_ptr, col_idx, vals))] + fn __new__(rows: usize, cols: usize, row_ptr: Vec, col_idx: Vec, vals: Vec) -> Self { + + Self { inner: rust_physics_engine::linalg::sparse::CsrMatrix { rows: rows, cols: cols, row_ptr: row_ptr, col_idx: col_idx, vals: vals } } + } + + /// Builds a CSR matrix from (row, col, value) triplets. Duplicate + /// positions are summed; explicit zeros are kept. + /// + /// Panics: + /// Panics if any triplet lies outside the given shape. + /// + /// Rust: `linalg::sparse::CsrMatrix::from_triplets` + #[pyo3(name = "from_triplets")] + #[staticmethod] + #[pyo3(signature = (rows, cols, entries))] + fn from_triplets(rows: usize, cols: usize, entries: Vec<(usize, usize, f64)>) -> PyResult { + let entries = entries.into_iter().map(|__e| (__e.0, __e.1, __e.2)).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::linalg::sparse::CsrMatrix::from_triplets(rows, cols, &entries)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCsrMatrix { inner: __v }) + } + + /// Converts a dense matrix, dropping entries with |v| ≤ tol. + /// + /// Rust: `linalg::sparse::CsrMatrix::from_dense` + #[pyo3(name = "from_dense")] + #[staticmethod] + #[pyo3(signature = (m, tol))] + fn from_dense(m: crate::generated::types::PyMatrixArg, tol: f64) -> PyResult { + let m = m.0; + let __r = crate::runtime::guard(|| rust_physics_engine::linalg::sparse::CsrMatrix::from_dense(&m, tol)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCsrMatrix { inner: __v }) + } + + /// Sparse matrix-vector product A·v. + /// + /// Panics: + /// Panics if `v.len() != self.cols`. + /// + /// Rust: `linalg::sparse::CsrMatrix::mul_vec` + #[pyo3(name = "mul_vec")] + #[pyo3(signature = (v))] + fn mul_vec<'py>(&self, py: Python<'py>, v: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.mul_vec(&v))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Negative 2-D Laplacian (SPD) on an nx×ny grid of interior points + /// with spacing h and Dirichlet (zero) boundary: the 5-point stencil + /// (4·u − neighbors)/h². Unknown (i, j) has index i·ny + j. + /// + /// Panics: + /// Panics unless nx, ny ≥ 1 and h > 0. + /// + /// Rust: `linalg::sparse::CsrMatrix::laplacian_2d` + #[pyo3(name = "laplacian_2d")] + #[staticmethod] + #[pyo3(signature = (nx, ny, h))] + fn laplacian_2d(nx: usize, ny: usize, h: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::linalg::sparse::CsrMatrix::laplacian_2d(nx, ny, h)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCsrMatrix { inner: __v }) + } + + #[getter] + #[pyo3(name = "rows")] + fn py_get_rows(&self) -> PyResult { Ok(self.inner.rows) } + + #[setter] + #[pyo3(name = "rows")] + fn py_set_rows(&mut self, v: usize) { self.inner.rows = v; } + + #[getter] + #[pyo3(name = "cols")] + fn py_get_cols(&self) -> PyResult { Ok(self.inner.cols) } + + #[setter] + #[pyo3(name = "cols")] + fn py_set_cols(&mut self, v: usize) { self.inner.cols = v; } + + #[getter] + #[pyo3(name = "row_ptr")] + fn py_get_row_ptr(&self) -> PyResult> { Ok(self.inner.row_ptr.clone()) } + + #[getter] + #[pyo3(name = "col_idx")] + fn py_get_col_idx(&self) -> PyResult> { Ok(self.inner.col_idx.clone()) } + + #[getter] + #[pyo3(name = "vals")] + fn py_get_vals(&self) -> PyResult> { Ok(self.inner.vals.clone()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("CsrMatrix", "CsrMatrix", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Thin SVD: A = U·Σ·Vᵀ. +/// +/// Rust: `linalg::svd::Svd` +#[pyclass(name = "Svd", module = "numeria.linalg.svd", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PySvd { pub inner: rust_physics_engine::linalg::svd::Svd } +#[pymethods] +impl PySvd { + /// Builds a `Svd` from its fields. + #[new] + #[pyo3(signature = (u, sigma, vt))] + fn __new__(u: crate::generated::types::PyMatrixArg, sigma: Vec, vt: crate::generated::types::PyMatrixArg) -> Self { + let u = u.0; + let vt = vt.0; + Self { inner: rust_physics_engine::linalg::svd::Svd { u: u, sigma: sigma, vt: vt } } + } + + #[getter] + #[pyo3(name = "u")] + fn py_get_u(&self) -> PyResult { Ok(crate::generated::types::PyMatrix { inner: self.inner.u.clone() }) } + + #[getter] + #[pyo3(name = "sigma")] + fn py_get_sigma(&self) -> PyResult> { Ok(self.inner.sigma.clone()) } + + #[getter] + #[pyo3(name = "vt")] + fn py_get_vt(&self) -> PyResult { Ok(crate::generated::types::PyMatrix { inner: self.inner.vt.clone() }) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Svd", "Svd", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} diff --git a/bindings/python/src/generated/types/manifold.rs b/bindings/python/src/generated/types/manifold.rs new file mode 100644 index 0000000..651978d --- /dev/null +++ b/bindings/python/src/generated/types/manifold.rs @@ -0,0 +1,5421 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; + + +/// A dense multivector in Cl(p, q, r): 2^(p+q+r) coefficients indexed by +/// basis-blade bitmask (bit i set means basis vector i is a factor; bits +/// 0..p square to +1, the next q to -1, the last r to 0). +/// +/// Rust: `manifold::clifford::Multivector` +#[pyclass(name = "Multivector", module = "numeria.manifold.clifford", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyMultivector { pub inner: rust_physics_engine::manifold::clifford::Multivector } +#[pymethods] +impl PyMultivector { + /// Builds a `Multivector` from its fields. + #[new] + #[pyo3(signature = (p, q, r, coeffs))] + fn __new__(p: usize, q: usize, r: usize, coeffs: Vec) -> Self { + + Self { inner: rust_physics_engine::manifold::clifford::Multivector { p: p, q: q, r: r, coeffs: coeffs } } + } + + /// + /// Rust: `manifold::clifford::Multivector::zero` + #[pyo3(name = "zero")] + #[staticmethod] + #[pyo3(signature = (p, q, r))] + fn zero(p: usize, q: usize, r: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::Multivector::zero(p, q, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) + } + + /// + /// Rust: `manifold::clifford::Multivector::scalar` + #[pyo3(name = "scalar")] + #[staticmethod] + #[pyo3(signature = (s, p, q, r))] + fn scalar(s: f64, p: usize, q: usize, r: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::Multivector::scalar(s, p, q, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) + } + + /// Grade-1 vector from components (one per basis vector). + /// + /// Rust: `manifold::clifford::Multivector::vector` + #[pyo3(name = "vector")] + #[staticmethod] + #[pyo3(signature = (v, p, q, r))] + fn vector(v: Vec, p: usize, q: usize, r: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::Multivector::vector(&v, p, q, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) + } + + /// Unit basis blade with the given bitmask. + /// + /// Rust: `manifold::clifford::Multivector::basis_blade` + #[pyo3(name = "basis_blade")] + #[staticmethod] + #[pyo3(signature = (mask, p, q, r))] + fn basis_blade(mask: usize, p: usize, q: usize, r: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::Multivector::basis_blade(mask, p, q, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) + } + + /// The unit pseudoscalar e_1...e_n. + /// + /// Rust: `manifold::clifford::Multivector::pseudoscalar` + #[pyo3(name = "pseudoscalar")] + #[staticmethod] + #[pyo3(signature = (p, q, r))] + fn pseudoscalar(p: usize, q: usize, r: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::Multivector::pseudoscalar(p, q, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) + } + + /// Full geometric product. + /// + /// Rust: `manifold::clifford::Multivector::geometric` + #[pyo3(name = "geometric")] + #[pyo3(signature = (o))] + fn geometric(&self, o: crate::generated::types::PyMultivector) -> PyResult { + let o = o.inner; + let __r = crate::runtime::guard(|| self.inner.geometric(&o)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) + } + + /// Outer (wedge) product: blade terms with no common factors. + /// + /// Rust: `manifold::clifford::Multivector::wedge` + #[pyo3(name = "wedge")] + #[pyo3(signature = (o))] + fn wedge(&self, o: crate::generated::types::PyMultivector) -> PyResult { + let o = o.inner; + let __r = crate::runtime::guard(|| self.inner.wedge(&o)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) + } + + /// Left contraction a ⌋ b: for basis blades, the grade + /// |grade(b)| - |grade(a)| part of the geometric product, nonzero only + /// when a's factors all lie inside b. + /// + /// Rust: `manifold::clifford::Multivector::inner` + #[pyo3(name = "inner")] + #[pyo3(signature = (o))] + fn inner(&self, o: crate::generated::types::PyMultivector) -> PyResult { + let o = o.inner; + let __r = crate::runtime::guard(|| self.inner.inner(&o)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) + } + + /// Scalar product `_0`. + /// + /// Rust: `manifold::clifford::Multivector::scalar_product` + #[pyo3(name = "scalar_product")] + #[pyo3(signature = (o))] + fn scalar_product(&self, o: crate::generated::types::PyMultivector) -> PyResult { + let o = o.inner; + let __r = crate::runtime::guard(|| self.inner.scalar_product(&o)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Commutator product (ab - ba)/2. + /// + /// Rust: `manifold::clifford::Multivector::commutator` + #[pyo3(name = "commutator")] + #[pyo3(signature = (o))] + fn commutator(&self, o: crate::generated::types::PyMultivector) -> PyResult { + let o = o.inner; + let __r = crate::runtime::guard(|| self.inner.commutator(&o)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) + } + + /// Regressive product a ∨ b = undual(dual(a) ∧ dual(b)). + /// + /// Rust: `manifold::clifford::Multivector::regressive` + #[pyo3(name = "regressive")] + #[pyo3(signature = (o))] + fn regressive(&self, o: crate::generated::types::PyMultivector) -> PyResult { + let o = o.inner; + let __r = crate::runtime::guard(|| self.inner.regressive(&o)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) + } + + /// Reverse: (-1)^{k(k-1)/2} per grade. + /// + /// Rust: `manifold::clifford::Multivector::reverse` + #[pyo3(name = "reverse")] + #[pyo3(signature = ())] + fn reverse(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.reverse()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) + } + + /// Grade involution: (-1)^k per grade. + /// + /// Rust: `manifold::clifford::Multivector::grade_involution` + #[pyo3(name = "grade_involution")] + #[pyo3(signature = ())] + fn grade_involution(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.grade_involution()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) + } + + /// Clifford conjugation: reverse of the grade involution. + /// + /// Rust: `manifold::clifford::Multivector::clifford_conjugate` + #[pyo3(name = "clifford_conjugate")] + #[pyo3(signature = ())] + fn clifford_conjugate(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.clifford_conjugate()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) + } + + /// Dual: right complement. For non-degenerate algebras this is x I^-1; + /// for degenerate ones (r > 0) the Poincare complement mask map with + /// the reordering sign (so that blade ∧ dual(blade) = pseudoscalar). + /// + /// Rust: `manifold::clifford::Multivector::dual` + #[pyo3(name = "dual")] + #[pyo3(signature = ())] + fn dual(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.dual()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) + } + + /// Inverse of `Multivector::dual`. + /// + /// Rust: `manifold::clifford::Multivector::undual` + #[pyo3(name = "undual")] + #[pyo3(signature = ())] + fn undual(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.undual()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) + } + + /// Grade-k part. + /// + /// Rust: `manifold::clifford::Multivector::grade` + #[pyo3(name = "grade")] + #[pyo3(signature = (k))] + fn grade(&self, k: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.grade(k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) + } + + /// The grades present (nonzero above tolerance). + /// + /// Rust: `manifold::clifford::Multivector::grades` + #[pyo3(name = "grades")] + #[pyo3(signature = ())] + fn grades<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.grades())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Heuristic blade check: single grade and X X~ is a scalar. + /// + /// Rust: `manifold::clifford::Multivector::is_blade` + #[pyo3(name = "is_blade")] + #[pyo3(signature = ())] + fn is_blade(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_blade()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Heuristic versor check: X X~ is a nonzero scalar and X has only even + /// or only odd grades. + /// + /// Rust: `manifold::clifford::Multivector::is_versor` + #[pyo3(name = "is_versor")] + #[pyo3(signature = ())] + fn is_versor(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_versor()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Squared magnitude _0 (may be negative in mixed signature). + /// + /// Rust: `manifold::clifford::Multivector::norm_squared` + #[pyo3(name = "norm_squared")] + #[pyo3(signature = ())] + fn norm_squared(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.norm_squared()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// + /// Rust: `manifold::clifford::Multivector::norm` + #[pyo3(name = "norm")] + #[pyo3(signature = ())] + fn norm(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.norm()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// + /// Rust: `manifold::clifford::Multivector::normalized` + #[pyo3(name = "normalized")] + #[pyo3(signature = ())] + fn normalized(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.normalized()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) + } + + /// Inverse for versor-like elements: X~/(X X~) when X X~ is scalar. + /// + /// Rust: `manifold::clifford::Multivector::inverse` + #[pyo3(name = "inverse")] + #[pyo3(signature = ())] + fn inverse(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.inverse()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyMultivector { inner: __x })) + } + + /// Exponential: closed form for blades with scalar square, series with + /// scaling-and-squaring otherwise. + /// + /// Rust: `manifold::clifford::Multivector::exp` + #[pyo3(name = "exp")] + #[pyo3(signature = ())] + fn exp(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.exp()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) + } + + /// Logarithm of a rotor `R = _0 + _2` (bivector generator). + /// + /// Rust: `manifold::clifford::Multivector::log` + #[pyo3(name = "log")] + #[pyo3(signature = ())] + fn log(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.log()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyMultivector { inner: __x })) + } + + /// Versor sandwich R x R~. + /// + /// Rust: `manifold::clifford::Multivector::sandwich` + #[pyo3(name = "sandwich")] + #[pyo3(signature = (x))] + fn sandwich(&self, x: crate::generated::types::PyMultivector) -> PyResult { + let x = x.inner; + let __r = crate::runtime::guard(|| self.inner.sandwich(&x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) + } + + /// Rotor rotating unit vector a to unit vector b: (1 + b a)/|1 + b a|. + /// + /// Rust: `manifold::clifford::Multivector::rotor_from_vectors` + #[pyo3(name = "rotor_from_vectors")] + #[staticmethod] + #[pyo3(signature = (a, b))] + fn rotor_from_vectors(a: crate::generated::types::PyMultivector, b: crate::generated::types::PyMultivector) -> PyResult { + let a = a.inner; + let b = b.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::Multivector::rotor_from_vectors(&a, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) + } + + /// Rotor for a rotation by `angle` in the plane of unit bivector `b`: + /// exp(-b angle/2). + /// + /// Rust: `manifold::clifford::Multivector::rotor_from_plane_angle` + #[pyo3(name = "rotor_from_plane_angle")] + #[staticmethod] + #[pyo3(signature = (b, angle))] + fn rotor_from_plane_angle(b: crate::generated::types::PyMultivector, angle: f64) -> PyResult { + let b = b.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::Multivector::rotor_from_plane_angle(&b, angle)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) + } + + /// Rotor interpolation R1 (R1^-1 R2)^t. + /// + /// Rust: `manifold::clifford::Multivector::rotor_interpolate` + #[pyo3(name = "rotor_interpolate")] + #[pyo3(signature = (o, t))] + fn rotor_interpolate(&self, o: crate::generated::types::PyMultivector, t: f64) -> PyResult { + let o = o.inner; + let __r = crate::runtime::guard(|| self.inner.rotor_interpolate(&o, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) + } + + /// Quaternion from the even subalgebra of Cl(3,0): + /// i = -e23, j = -e31 = e13, k = -e12. + /// + /// Rust: `manifold::clifford::Multivector::to_quaternion` + #[pyo3(name = "to_quaternion")] + #[pyo3(signature = ())] + fn to_quaternion(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.to_quaternion()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyQuaternion { inner: __x })) + } + + /// Rotor in Cl(3,0) from a quaternion (inverse of + /// `Multivector::to_quaternion`). + /// + /// Rust: `manifold::clifford::Multivector::from_quaternion` + #[pyo3(name = "from_quaternion")] + #[staticmethod] + #[pyo3(signature = (q))] + fn from_quaternion(q: crate::generated::types::PyQuaternionArg) -> PyResult { + let q = q.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::clifford::Multivector::from_quaternion(&q)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) + } + + /// Matrix of left multiplication by this multivector on the coefficient + /// space (a faithful 2^n-dimensional representation). + /// + /// Rust: `manifold::clifford::Multivector::to_matrix_rep` + #[pyo3(name = "to_matrix_rep")] + #[pyo3(signature = ())] + fn to_matrix_rep(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.to_matrix_rep()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) + } + + /// Meet (intersection) via the regressive product. + /// + /// Rust: `manifold::clifford::Multivector::meet` + #[pyo3(name = "meet")] + #[pyo3(signature = (o))] + fn meet(&self, o: crate::generated::types::PyMultivector) -> PyResult { + let o = o.inner; + let __r = crate::runtime::guard(|| self.inner.meet(&o)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) + } + + /// Join (union): the wedge when independent, otherwise the larger blade. + /// + /// Rust: `manifold::clifford::Multivector::join` + #[pyo3(name = "join")] + #[pyo3(signature = (o))] + fn join(&self, o: crate::generated::types::PyMultivector) -> PyResult { + let o = o.inner; + let __r = crate::runtime::guard(|| self.inner.join(&o)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) + } + + /// Factor a blade into orthogonal grade-1 vectors. + /// + /// Rust: `manifold::clifford::Multivector::blade_factor` + #[pyo3(name = "blade_factor")] + #[pyo3(signature = ())] + fn blade_factor(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.blade_factor()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyMultivector { inner: __x }).collect::>()) + } + + /// Projection of x onto blade B: (x ⌋ B) B^-1. + /// + /// Rust: `manifold::clifford::Multivector::project_onto_blade` + #[pyo3(name = "project_onto_blade")] + #[pyo3(signature = (b))] + fn project_onto_blade(&self, b: crate::generated::types::PyMultivector) -> PyResult { + let b = b.inner; + let __r = crate::runtime::guard(|| self.inner.project_onto_blade(&b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) + } + + /// Rejection from a blade. + /// + /// Rust: `manifold::clifford::Multivector::reject_from_blade` + #[pyo3(name = "reject_from_blade")] + #[pyo3(signature = (b))] + fn reject_from_blade(&self, b: crate::generated::types::PyMultivector) -> PyResult { + let b = b.inner; + let __r = crate::runtime::guard(|| self.inner.reject_from_blade(&b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) + } + + /// Reflection through the line of vector n: n X n^-1. + /// + /// Rust: `manifold::clifford::Multivector::reflect_in_vector` + #[pyo3(name = "reflect_in_vector")] + #[pyo3(signature = (n))] + fn reflect_in_vector(&self, n: crate::generated::types::PyMultivector) -> PyResult { + let n = n.inner; + let __r = crate::runtime::guard(|| self.inner.reflect_in_vector(&n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) + } + + /// Reflection in the hyperplane orthogonal to n: n X̂ n^-1. + /// + /// Rust: `manifold::clifford::Multivector::reflect_in_hyperplane` + #[pyo3(name = "reflect_in_hyperplane")] + #[pyo3(signature = (n))] + fn reflect_in_hyperplane(&self, n: crate::generated::types::PyMultivector) -> PyResult { + let n = n.inner; + let __r = crate::runtime::guard(|| self.inner.reflect_in_hyperplane(&n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) + } + + /// + /// Rust: `manifold::clifford::Multivector::add` + #[pyo3(name = "add")] + #[pyo3(signature = (o))] + fn add(&self, o: crate::generated::types::PyMultivector) -> PyResult { + let o = o.inner; + let __r = crate::runtime::guard(|| self.inner.add(&o)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) + } + + /// + /// Rust: `manifold::clifford::Multivector::sub` + #[pyo3(name = "sub")] + #[pyo3(signature = (o))] + fn sub(&self, o: crate::generated::types::PyMultivector) -> PyResult { + let o = o.inner; + let __r = crate::runtime::guard(|| self.inner.sub(&o)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) + } + + /// + /// Rust: `manifold::clifford::Multivector::scale` + #[pyo3(name = "scale")] + #[pyo3(signature = (k))] + fn scale(&self, k: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.scale(k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) + } + + /// Alias for `Multivector::scale`. + /// + /// Rust: `manifold::clifford::Multivector::mul_scalar` + #[pyo3(name = "mul_scalar")] + #[pyo3(signature = (k))] + fn mul_scalar(&self, k: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.mul_scalar(k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMultivector { inner: __v }) + } + + /// Human-readable blade expansion, e.g. "1.5 + 2e12 - 0.3e123". + /// + /// Rust: `manifold::clifford::Multivector::to_string_blades` + #[pyo3(name = "to_string_blades")] + #[pyo3(signature = ())] + fn to_string_blades(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.to_string_blades()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_string()) + } + + #[getter] + #[pyo3(name = "p")] + fn py_get_p(&self) -> PyResult { Ok(self.inner.p) } + + #[setter] + #[pyo3(name = "p")] + fn py_set_p(&mut self, v: usize) { self.inner.p = v; } + + #[getter] + #[pyo3(name = "q")] + fn py_get_q(&self) -> PyResult { Ok(self.inner.q) } + + #[setter] + #[pyo3(name = "q")] + fn py_set_q(&mut self, v: usize) { self.inner.q = v; } + + #[getter] + #[pyo3(name = "r")] + fn py_get_r(&self) -> PyResult { Ok(self.inner.r) } + + #[setter] + #[pyo3(name = "r")] + fn py_set_r(&mut self, v: usize) { self.inner.r = v; } + + #[getter] + #[pyo3(name = "coeffs")] + fn py_get_coeffs(&self) -> PyResult> { Ok(self.inner.coeffs.clone()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Multivector", "Multivector", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Kinds of CGA object. +/// +/// Rust: `manifold::clifford::cga3::CgaObject` +#[pyclass(name = "CgaObject", module = "numeria.manifold.clifford.cga3", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyCgaObject { + Point, + PointPair, + Line, + Circle, + Plane, + Sphere, + ImaginarySphere, + Ideal, + Unknown, +} +impl PyCgaObject { + pub fn to_rust(&self) -> rust_physics_engine::manifold::clifford::cga3::CgaObject { match self { + Self::Point => rust_physics_engine::manifold::clifford::cga3::CgaObject::Point, + Self::PointPair => rust_physics_engine::manifold::clifford::cga3::CgaObject::PointPair, + Self::Line => rust_physics_engine::manifold::clifford::cga3::CgaObject::Line, + Self::Circle => rust_physics_engine::manifold::clifford::cga3::CgaObject::Circle, + Self::Plane => rust_physics_engine::manifold::clifford::cga3::CgaObject::Plane, + Self::Sphere => rust_physics_engine::manifold::clifford::cga3::CgaObject::Sphere, + Self::ImaginarySphere => rust_physics_engine::manifold::clifford::cga3::CgaObject::ImaginarySphere, + Self::Ideal => rust_physics_engine::manifold::clifford::cga3::CgaObject::Ideal, + Self::Unknown => rust_physics_engine::manifold::clifford::cga3::CgaObject::Unknown, + } } + pub fn from_rust(v: &rust_physics_engine::manifold::clifford::cga3::CgaObject) -> Self { match v { + rust_physics_engine::manifold::clifford::cga3::CgaObject::Point => Self::Point, + rust_physics_engine::manifold::clifford::cga3::CgaObject::PointPair => Self::PointPair, + rust_physics_engine::manifold::clifford::cga3::CgaObject::Line => Self::Line, + rust_physics_engine::manifold::clifford::cga3::CgaObject::Circle => Self::Circle, + rust_physics_engine::manifold::clifford::cga3::CgaObject::Plane => Self::Plane, + rust_physics_engine::manifold::clifford::cga3::CgaObject::Sphere => Self::Sphere, + rust_physics_engine::manifold::clifford::cga3::CgaObject::ImaginarySphere => Self::ImaginarySphere, + rust_physics_engine::manifold::clifford::cga3::CgaObject::Ideal => Self::Ideal, + rust_physics_engine::manifold::clifford::cga3::CgaObject::Unknown => Self::Unknown, + } } +} +#[pymethods] +impl PyCgaObject { + fn __repr__(&self) -> &'static str { + match self { + Self::Point => "CgaObject.Point", + Self::PointPair => "CgaObject.PointPair", + Self::Line => "CgaObject.Line", + Self::Circle => "CgaObject.Circle", + Self::Plane => "CgaObject.Plane", + Self::Sphere => "CgaObject.Sphere", + Self::ImaginarySphere => "CgaObject.ImaginarySphere", + Self::Ideal => "CgaObject.Ideal", + Self::Unknown => "CgaObject.Unknown", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A triangle mesh with its DEC operators: primal edges, exterior +/// derivatives d0 and d1, and diagonal Hodge stars. +/// +/// Rust: `manifold::dec::DecMesh` +#[pyclass(name = "DecMesh", module = "numeria.manifold.dec")] +pub struct PyDecMesh { pub inner: rust_physics_engine::manifold::dec::DecMesh } +#[pymethods] +impl PyDecMesh { + /// Build the DEC operators (barycentric-lumped dual areas and cotangent + /// star1). + /// + /// Rust: `manifold::dec::DecMesh::new` + #[new] + #[pyo3(signature = (mesh))] + fn __new__(mesh: crate::generated::types::PyGeometryMeshMesh) -> PyResult { + let mesh = mesh.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::dec::DecMesh::new(&mesh)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyDecMesh { inner: __v }) + } + + /// Exterior derivative on 0-forms (vertices -> edges). + /// + /// Rust: `manifold::dec::DecMesh::d0` + #[pyo3(name = "d0")] + #[pyo3(signature = ())] + fn d0(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.d0()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCsrMatrix { inner: __v.clone() }) + } + + /// Exterior derivative on 1-forms (edges -> faces). + /// + /// Rust: `manifold::dec::DecMesh::d1` + #[pyo3(name = "d1")] + #[pyo3(signature = ())] + fn d1(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.d1()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCsrMatrix { inner: __v.clone() }) + } + + /// + /// Rust: `manifold::dec::DecMesh::hodge0` + #[pyo3(name = "hodge0")] + #[pyo3(signature = ())] + fn hodge0<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.hodge0())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_vec()) + } + + /// + /// Rust: `manifold::dec::DecMesh::hodge1` + #[pyo3(name = "hodge1")] + #[pyo3(signature = ())] + fn hodge1<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.hodge1())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_vec()) + } + + /// + /// Rust: `manifold::dec::DecMesh::hodge2` + #[pyo3(name = "hodge2")] + #[pyo3(signature = ())] + fn hodge2<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.hodge2())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_vec()) + } + + /// + /// Rust: `manifold::dec::DecMesh::dual_areas` + #[pyo3(name = "dual_areas")] + #[pyo3(signature = ())] + fn dual_areas<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.dual_areas())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_vec()) + } + + /// The weak Laplace-Beltrami operator L = d0^T star1 d0 (the cotangent + /// Laplacian, positive semidefinite). + /// + /// Rust: `manifold::dec::DecMesh::laplace_beltrami` + #[pyo3(name = "laplace_beltrami")] + #[pyo3(signature = ())] + fn laplace_beltrami(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.laplace_beltrami()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCsrMatrix { inner: __v }) + } + + /// The Hodge Laplacian on 1-forms: + /// Delta1 = d0 star0^-1 d0^T star1 + star1^-1 d1^T star2 d1. + /// + /// Rust: `manifold::dec::DecMesh::laplace_1form` + #[pyo3(name = "laplace_1form")] + #[pyo3(signature = ())] + fn laplace_1form(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.laplace_1form()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCsrMatrix { inner: __v }) + } + + /// Gradient of a vertex function as a 1-form (d0 f). + /// + /// Rust: `manifold::dec::DecMesh::gradient` + #[pyo3(name = "gradient")] + #[pyo3(signature = (f))] + fn gradient<'py>(&self, py: Python<'py>, f: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.gradient(&f))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Curl of a 1-form as a 2-form (d1 w). + /// + /// Rust: `manifold::dec::DecMesh::curl` + #[pyo3(name = "curl")] + #[pyo3(signature = (w))] + fn curl<'py>(&self, py: Python<'py>, w: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.curl(&w))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Codifferential divergence of a 1-form back onto vertices: + /// div w = -star0^-1 d0^T star1 w, signed so that div grad = Delta + /// (positive on convex functions, matching the continuous Laplacian). + /// + /// Rust: `manifold::dec::DecMesh::divergence` + #[pyo3(name = "divergence")] + #[pyo3(signature = (w))] + fn divergence<'py>(&self, py: Python<'py>, w: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.divergence(&w))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Hodge decomposition of a 1-form into (exact, coexact, harmonic), + /// orthogonal in the star1 inner product. + /// + /// Rust: `manifold::dec::DecMesh::hodge_decomposition` + #[pyo3(name = "hodge_decomposition")] + #[pyo3(signature = (w))] + fn hodge_decomposition<'py>(&self, py: Python<'py>, w: Vec) -> PyResult<(Vec, Vec, Vec)> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.hodge_decomposition(&w))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) + } + + /// A basis for the harmonic 1-forms (dimension = 2 genus on a closed + /// surface). Harmonic means closed (d1 w = 0) and coclosed + /// (d0^T star1 w = 0); the basis spans the nullspace of the Gram matrix + /// of those two constraint blocks, whose dimension is b1 by the Hodge + /// theorem. + /// + /// Rust: `manifold::dec::DecMesh::harmonic_forms` + #[pyo3(name = "harmonic_forms")] + #[pyo3(signature = ())] + fn harmonic_forms<'py>(&self, py: Python<'py>) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.harmonic_forms())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Betti numbers (b0, b1, b2) of the mesh. + /// + /// Rust: `manifold::dec::DecMesh::betti_numbers` + #[pyo3(name = "betti_numbers")] + #[pyo3(signature = ())] + fn betti_numbers<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.betti_numbers())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_vec()) + } + + /// Rank of the k-th simplicial cohomology (equals the Betti number). + /// + /// Rust: `manifold::dec::DecMesh::simplicial_cohomology_rank` + #[pyo3(name = "simplicial_cohomology_rank")] + #[pyo3(signature = (k))] + fn simplicial_cohomology_rank(&self, k: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.simplicial_cohomology_rank(k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Whitney interpolation of a 1-form to one vector per face (evaluated + /// at the barycenter). + /// + /// Rust: `manifold::dec::DecMesh::interpolate_1form_to_vectors` + #[pyo3(name = "interpolate_1form_to_vectors")] + #[pyo3(signature = (w))] + fn interpolate_1form_to_vectors(&self, w: Vec) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.interpolate_1form_to_vectors(&w)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) + } + + /// Integrate a per-face vector field onto edges (a discrete 1-form). + /// + /// Rust: `manifold::dec::DecMesh::vector_field_to_1form` + #[pyo3(name = "vector_field_to_1form")] + #[pyo3(signature = (v))] + fn vector_field_to_1form<'py>(&self, py: Python<'py>, v: Vec) -> PyResult> { + let v = v.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || self.inner.vector_field_to_1form(&v))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Implicit heat flow of a vertex function: `steps` backward-Euler + /// solves of (M + dt L) u = M u. + /// + /// Rust: `manifold::dec::DecMesh::heat_flow` + #[pyo3(name = "heat_flow")] + #[pyo3(signature = (f0, t, steps))] + fn heat_flow<'py>(&self, py: Python<'py>, f0: Vec, t: f64, steps: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.heat_flow(&f0, t, steps))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Poisson solve L u = M rho with Dirichlet values at `fixed` vertices. + /// + /// Rust: `manifold::dec::DecMesh::poisson_solve` + #[pyo3(name = "poisson_solve")] + #[pyo3(signature = (rho, fixed))] + fn poisson_solve<'py>(&self, py: Python<'py>, rho: Vec, fixed: Vec<(usize, f64)>) -> PyResult> { + let fixed = fixed.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || self.inner.poisson_solve(&rho, &fixed))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// First n Laplace-Beltrami eigenpairs (shape DNA): eigenvalues + /// ascending and vertex eigenfunctions. + /// + /// Rust: `manifold::dec::DecMesh::eigenmodes` + #[pyo3(name = "eigenmodes")] + #[pyo3(signature = (n))] + fn eigenmodes(&self, n: usize) -> PyResult<(Vec, Vec>)> { + let __r = crate::runtime::guard(|| self.inner.eigenmodes(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) + } + + /// Geodesic distance from a source vertex by the heat method. + /// + /// Rust: `manifold::dec::DecMesh::geodesic_heat_method` + #[pyo3(name = "geodesic_heat_method")] + #[pyo3(signature = (source, t))] + fn geodesic_heat_method<'py>(&self, py: Python<'py>, source: usize, t: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.geodesic_heat_method(source, t))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Approximate vector heat method: transports `v0` from the source over + /// the surface by projecting onto local tangent planes, weighted by heat + /// diffusion. + /// + /// Rust: `manifold::dec::DecMesh::vector_heat_method` + #[pyo3(name = "vector_heat_method")] + #[pyo3(signature = (source, v0, t))] + fn vector_heat_method(&self, source: usize, v0: crate::generated::types::PyVec3Arg, t: f64) -> PyResult> { + let v0 = v0.0; + let __r = crate::runtime::guard(|| self.inner.vector_heat_method(source, v0, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) + } + + /// Trivial connection (Crane et al., simplified): edge rotation angles + /// of least norm whose per-vertex holonomy cancels the angle defect up + /// to the prescribed singularities (vertex index, target index). + /// + /// Rust: `manifold::dec::DecMesh::trivial_connection` + #[pyo3(name = "trivial_connection")] + #[pyo3(signature = (singularities))] + fn trivial_connection<'py>(&self, py: Python<'py>, singularities: Vec<(usize, f64)>) -> PyResult> { + let singularities = singularities.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || self.inner.trivial_connection(&singularities))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Smoothest n-RoSy direction field: averaged in the n-fold-rotation + /// representation per face, returned as one unit vector per face. + /// + /// Rust: `manifold::dec::DecMesh::smoothest_direction_field` + #[pyo3(name = "smoothest_direction_field")] + #[pyo3(signature = (n_rosy))] + fn smoothest_direction_field(&self, n_rosy: usize) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.smoothest_direction_field(n_rosy)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) + } + + /// Stream function (2-form potential) of the coexact part of a 1-form: + /// per-face values beta with w_coexact = star1^-1 d1^T beta. + /// + /// Rust: `manifold::dec::DecMesh::stream_function` + #[pyo3(name = "stream_function")] + #[pyo3(signature = (v))] + fn stream_function<'py>(&self, py: Python<'py>, v: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.stream_function(&v))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Simplified circulation-preserving fluid step: viscous diffusion of + /// the 1-form followed by removal of the exact (gradient) part. + /// + /// Rust: `manifold::dec::DecMesh::fluid_step_dec` + #[pyo3(name = "fluid_step_dec")] + #[pyo3(signature = (w, dt, nu))] + fn fluid_step_dec<'py>(&self, w: pyo3::Bound<'py, pyo3::PyAny>, dt: f64, nu: f64) -> PyResult<()> { + let mut w__v: Vec = w.extract()?; + let __r = crate::runtime::guard(|| self.inner.fluid_step_dec(&mut w__v, dt, nu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + crate::runtime::coerce::write_back(&w, &w__v)?; + Ok(()) + } + + /// One explicit mean-curvature-flow step: x <- x - dt M^-1 L x. + /// + /// Rust: `manifold::dec::DecMesh::mean_curvature_flow_step` + #[pyo3(name = "mean_curvature_flow_step")] + #[pyo3(signature = (dt))] + fn mean_curvature_flow_step(&self, dt: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.mean_curvature_flow_step(dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGeometryMeshMesh { inner: __v }) + } + + /// Willmore energy: integral of squared mean curvature, from the + /// cotangent mean-curvature normal. + /// + /// Rust: `manifold::dec::DecMesh::willmore_energy` + #[pyo3(name = "willmore_energy")] + #[pyo3(signature = ())] + fn willmore_energy(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.willmore_energy()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Discrete Gauss-Bonnet residual: sum of angle defects minus 2 pi chi. + /// + /// Rust: `manifold::dec::DecMesh::discrete_gauss_bonnet_check` + #[pyo3(name = "discrete_gauss_bonnet_check")] + #[pyo3(signature = ())] + fn discrete_gauss_bonnet_check(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.discrete_gauss_bonnet_check()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "mesh")] + fn py_get_mesh(&self) -> PyResult { Ok(crate::generated::types::PyGeometryMeshMesh { inner: self.inner.mesh.clone() }) } + + #[getter] + #[pyo3(name = "edges")] + fn py_get_edges(&self) -> PyResult> { Ok(self.inner.edges.clone().into_iter().map(|__x| (__x.0, __x.1)).collect::>()) } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// A point on a geodesic: position, velocity, affine parameter. +/// +/// Rust: `manifold::geodesic::GeodesicState` +#[pyclass(name = "GeodesicState", module = "numeria.manifold.geodesic", from_py_object)] +#[derive(Clone)] +pub struct PyGeodesicState { pub inner: rust_physics_engine::manifold::geodesic::GeodesicState } +#[pymethods] +impl PyGeodesicState { + /// Builds a `GeodesicState` from its fields. + #[new] + #[pyo3(signature = (x, v, tau))] + fn __new__(x: crate::generated::types::PyVecNArg, v: crate::generated::types::PyVecNArg, tau: f64) -> Self { + let x = x.0; + let v = v.0; + Self { inner: rust_physics_engine::manifold::geodesic::GeodesicState { x: x, v: v, tau: tau } } + } + + #[getter] + #[pyo3(name = "x")] + fn py_get_x(&self) -> PyResult { Ok(crate::generated::types::PyVecN { inner: self.inner.x.clone() }) } + + #[getter] + #[pyo3(name = "v")] + fn py_get_v(&self) -> PyResult { Ok(crate::generated::types::PyVecN { inner: self.inner.v.clone() }) } + + #[getter] + #[pyo3(name = "tau")] + fn py_get_tau(&self) -> PyResult { Ok(self.inner.tau) } + + #[setter] + #[pyo3(name = "tau")] + fn py_set_tau(&mut self, v: f64) { self.inner.tau = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("GeodesicState", "GeodesicState", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Time integrator selection for geodesic integration. +/// +/// Rust: `manifold::geodesic::Integrator` +#[pyclass(name = "Integrator", module = "numeria.manifold.geodesic", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyGeodesicIntegrator { + Rk4, + DormandPrince, +} +impl PyGeodesicIntegrator { + pub fn to_rust(&self) -> rust_physics_engine::manifold::geodesic::Integrator { match self { + Self::Rk4 => rust_physics_engine::manifold::geodesic::Integrator::Rk4, + Self::DormandPrince => rust_physics_engine::manifold::geodesic::Integrator::DormandPrince, + } } + pub fn from_rust(v: &rust_physics_engine::manifold::geodesic::Integrator) -> Self { match v { + rust_physics_engine::manifold::geodesic::Integrator::Rk4 => Self::Rk4, + rust_physics_engine::manifold::geodesic::Integrator::DormandPrince => Self::DormandPrince, + } } +} +#[pymethods] +impl PyGeodesicIntegrator { + fn __repr__(&self) -> &'static str { + match self { + Self::Rk4 => "Integrator.Rk4", + Self::DormandPrince => "Integrator.DormandPrince", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// The classical models of hyperbolic space. +/// +/// Rust: `manifold::hyperbolic::HypModel` +#[pyclass(name = "HypModel", module = "numeria.manifold.hyperbolic", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyHypModel { + PoincareDisk, + PoincareBall, + UpperHalfPlane, + UpperHalfSpace, + Klein, + Hyperboloid, +} +impl PyHypModel { + pub fn to_rust(&self) -> rust_physics_engine::manifold::hyperbolic::HypModel { match self { + Self::PoincareDisk => rust_physics_engine::manifold::hyperbolic::HypModel::PoincareDisk, + Self::PoincareBall => rust_physics_engine::manifold::hyperbolic::HypModel::PoincareBall, + Self::UpperHalfPlane => rust_physics_engine::manifold::hyperbolic::HypModel::UpperHalfPlane, + Self::UpperHalfSpace => rust_physics_engine::manifold::hyperbolic::HypModel::UpperHalfSpace, + Self::Klein => rust_physics_engine::manifold::hyperbolic::HypModel::Klein, + Self::Hyperboloid => rust_physics_engine::manifold::hyperbolic::HypModel::Hyperboloid, + } } + pub fn from_rust(v: &rust_physics_engine::manifold::hyperbolic::HypModel) -> Self { match v { + rust_physics_engine::manifold::hyperbolic::HypModel::PoincareDisk => Self::PoincareDisk, + rust_physics_engine::manifold::hyperbolic::HypModel::PoincareBall => Self::PoincareBall, + rust_physics_engine::manifold::hyperbolic::HypModel::UpperHalfPlane => Self::UpperHalfPlane, + rust_physics_engine::manifold::hyperbolic::HypModel::UpperHalfSpace => Self::UpperHalfSpace, + rust_physics_engine::manifold::hyperbolic::HypModel::Klein => Self::Klein, + rust_physics_engine::manifold::hyperbolic::HypModel::Hyperboloid => Self::Hyperboloid, + } } +} +#[pymethods] +impl PyHypModel { + fn __repr__(&self) -> &'static str { + match self { + Self::PoincareDisk => "HypModel.PoincareDisk", + Self::PoincareBall => "HypModel.PoincareBall", + Self::UpperHalfPlane => "HypModel.UpperHalfPlane", + Self::UpperHalfSpace => "HypModel.UpperHalfSpace", + Self::Klein => "HypModel.Klein", + Self::Hyperboloid => "HypModel.Hyperboloid", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A point of hyperbolic space tagged with the model its coordinates use. +/// +/// Rust: `manifold::hyperbolic::HypPoint` +#[pyclass(name = "HypPoint", module = "numeria.manifold.hyperbolic", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyHypPoint { pub inner: rust_physics_engine::manifold::hyperbolic::HypPoint } +#[pymethods] +impl PyHypPoint { + /// Builds a `HypPoint` from its fields. + #[new] + #[pyo3(signature = (coords, model))] + fn __new__(coords: crate::generated::types::PyVecNArg, model: crate::generated::types::PyHypModel) -> Self { + let coords = coords.0; + let model = model.to_rust(); + Self { inner: rust_physics_engine::manifold::hyperbolic::HypPoint { coords: coords, model: model } } + } + + /// The origin of hyperbolic space in the given model and dimension. + /// + /// Rust: `manifold::hyperbolic::HypPoint::origin` + #[pyo3(name = "origin")] + #[staticmethod] + #[pyo3(signature = (model, dim))] + fn origin(model: crate::generated::types::PyHypModel, dim: usize) -> PyResult { + let model = model.to_rust(); + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::hyperbolic::HypPoint::origin(model, dim)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyHypPoint { inner: __v }) + } + + /// A 2D point at hyperbolic polar coordinates (r, theta) from the disk + /// origin. + /// + /// Rust: `manifold::hyperbolic::HypPoint::from_polar` + #[pyo3(name = "from_polar")] + #[staticmethod] + #[pyo3(signature = (r, theta))] + fn from_polar(r: f64, theta: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::hyperbolic::HypPoint::from_polar(r, theta)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyHypPoint { inner: __v }) + } + + /// Convert to another model. + /// + /// Rust: `manifold::hyperbolic::HypPoint::to` + #[pyo3(name = "to")] + #[pyo3(signature = (model))] + fn to(&self, model: crate::generated::types::PyHypModel) -> PyResult { + let model = model.to_rust(); + let __r = crate::runtime::guard(|| self.inner.to(model)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyHypPoint { inner: __v }) + } + + /// Hyperbolic distance to another point (models may differ). + /// + /// Rust: `manifold::hyperbolic::HypPoint::distance` + #[pyo3(name = "distance")] + #[pyo3(signature = (other))] + fn distance(&self, other: crate::generated::types::PyHypPoint) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.distance(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Sample the geodesic to another point at n+1 evenly spaced hyperbolic + /// parameters. + /// + /// Rust: `manifold::hyperbolic::HypPoint::geodesic_to` + #[pyo3(name = "geodesic_to")] + #[pyo3(signature = (other, n))] + fn geodesic_to(&self, other: crate::generated::types::PyHypPoint, n: usize) -> PyResult> { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.geodesic_to(&other, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyHypPoint { inner: __x }).collect::>()) + } + + /// Hyperbolic midpoint. + /// + /// Rust: `manifold::hyperbolic::HypPoint::midpoint` + #[pyo3(name = "midpoint")] + #[pyo3(signature = (other))] + fn midpoint(&self, other: crate::generated::types::PyHypPoint) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.midpoint(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyHypPoint { inner: __v }) + } + + /// Reflect across the geodesic through two points (2D disk model). + /// + /// Rust: `manifold::hyperbolic::HypPoint::reflect_across` + #[pyo3(name = "reflect_across")] + #[pyo3(signature = (geodesic))] + fn reflect_across(&self, geodesic: (crate::generated::types::PyHypPoint, crate::generated::types::PyHypPoint)) -> PyResult { + let geodesic = (geodesic.0.inner, geodesic.1.inner); + let __r = crate::runtime::guard(|| self.inner.reflect_across((&geodesic.0, &geodesic.1))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyHypPoint { inner: __v }) + } + + /// Interior angle at this vertex formed by geodesics to a and b (2D). + /// + /// Rust: `manifold::hyperbolic::HypPoint::angle_at` + #[pyo3(name = "angle_at")] + #[pyo3(signature = (a, b))] + fn angle_at(&self, a: crate::generated::types::PyHypPoint, b: crate::generated::types::PyHypPoint) -> PyResult { + let a = a.inner; + let b = b.inner; + let __r = crate::runtime::guard(|| self.inner.angle_at(&a, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Euclidean display coordinates (disk/ball models pass through; the + /// hyperboloid projects to the disk). + /// + /// Rust: `manifold::hyperbolic::HypPoint::to_euclidean_display` + #[pyo3(name = "to_euclidean_display")] + #[pyo3(signature = ())] + fn to_euclidean_display(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.to_euclidean_display()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVecN { inner: __v }) + } + + #[getter] + #[pyo3(name = "coords")] + fn py_get_coords(&self) -> PyResult { Ok(crate::generated::types::PyVecN { inner: self.inner.coords.clone() }) } + + #[getter] + #[pyo3(name = "model")] + fn py_get_model(&self) -> PyResult { Ok(crate::generated::types::PyHypModel::from_rust(&self.inner.model.clone())) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("HypPoint", "HypPoint", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// The 3D Heisenberg group with coordinates (x, y, z) and product +/// (x, y, z)(x', y', z') = (x + x', y + y', z + z' + x y'). +/// +/// Rust: `manifold::lie::Heisenberg3` +#[pyclass(name = "Heisenberg3", module = "numeria.manifold.lie", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyHeisenberg3 { pub inner: rust_physics_engine::manifold::lie::Heisenberg3 } +#[pymethods] +impl PyHeisenberg3 { + /// Builds a `Heisenberg3` from its fields. + #[new] + #[pyo3(signature = (x, y, z))] + fn __new__(x: f64, y: f64, z: f64) -> Self { + + Self { inner: rust_physics_engine::manifold::lie::Heisenberg3 { x: x, y: y, z: z } } + } + + /// + /// Rust: `manifold::lie::Heisenberg3::identity` + #[pyo3(name = "identity")] + #[staticmethod] + #[pyo3(signature = ())] + fn identity() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::Heisenberg3::identity()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyHeisenberg3 { inner: __v }) + } + + /// + /// Rust: `manifold::lie::Heisenberg3::compose` + #[pyo3(name = "compose")] + #[pyo3(signature = (o))] + fn compose(&self, o: crate::generated::types::PyHeisenberg3Arg) -> PyResult { + let o = o.0; + let __r = crate::runtime::guard(|| self.inner.compose(&o)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyHeisenberg3 { inner: __v }) + } + + /// + /// Rust: `manifold::lie::Heisenberg3::inverse` + #[pyo3(name = "inverse")] + #[pyo3(signature = ())] + fn inverse(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.inverse()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyHeisenberg3 { inner: __v }) + } + + /// Group commutator a b a^-1 b^-1 (lands in the center). + /// + /// Rust: `manifold::lie::Heisenberg3::commutator` + #[pyo3(name = "commutator")] + #[pyo3(signature = (o))] + fn commutator(&self, o: crate::generated::types::PyHeisenberg3Arg) -> PyResult { + let o = o.0; + let __r = crate::runtime::guard(|| self.inner.commutator(&o)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyHeisenberg3 { inner: __v }) + } + + #[getter] + #[pyo3(name = "x")] + fn py_get_x(&self) -> PyResult { Ok(self.inner.x) } + + #[setter] + #[pyo3(name = "x")] + fn py_set_x(&mut self, v: f64) { self.inner.x = v; } + + #[getter] + #[pyo3(name = "y")] + fn py_get_y(&self) -> PyResult { Ok(self.inner.y) } + + #[setter] + #[pyo3(name = "y")] + fn py_set_y(&mut self, v: f64) { self.inner.y = v; } + + #[getter] + #[pyo3(name = "z")] + fn py_get_z(&self) -> PyResult { Ok(self.inner.z) } + + #[setter] + #[pyo3(name = "z")] + fn py_set_z(&mut self, v: f64) { self.inner.z = v; } + + fn __repr__(&self) -> String { format!("Heisenberg3(x={:?}, y={:?}, z={:?})", self.inner.x, self.inner.y, self.inner.z) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A `Heisenberg3` argument, or anything that can stand in for one. +pub struct PyHeisenberg3Arg(pub rust_physics_engine::manifold::lie::Heisenberg3); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyHeisenberg3Arg { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result { + if let Ok(__w) = obj.extract::() { + return Ok(PyHeisenberg3Arg(__w.inner)); + } + let __v = crate::runtime::coerce::floats_exact(obj, 3, "Heisenberg3")?; + Ok(PyHeisenberg3Arg(rust_physics_engine::manifold::lie::Heisenberg3 { x: __v[0], y: __v[1], z: __v[2] })) + } +} + + +/// Planar rigid transform. +/// +/// Rust: `manifold::lie::Se2` +#[pyclass(name = "Se2", module = "numeria.manifold.lie", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PySe2 { pub inner: rust_physics_engine::manifold::lie::Se2 } +#[pymethods] +impl PySe2 { + /// Builds a `Se2` from its fields. + #[new] + #[pyo3(signature = (theta, t))] + fn __new__(theta: f64, t: crate::generated::types::PyVec2Arg) -> Self { + let t = t.0; + Self { inner: rust_physics_engine::manifold::lie::Se2 { theta: theta, t: t } } + } + + /// + /// Rust: `manifold::lie::Se2::identity` + #[pyo3(name = "identity")] + #[staticmethod] + #[pyo3(signature = ())] + fn identity() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::Se2::identity()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySe2 { inner: __v }) + } + + /// Exponential of (vx, vy, omega). + /// + /// Rust: `manifold::lie::Se2::exp` + #[pyo3(name = "exp")] + #[staticmethod] + #[pyo3(signature = (v))] + fn exp(v: Vec) -> PyResult { + let v = <[f64; 3]>::try_from(v).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 3 values, got {}", __v.len())))?; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::Se2::exp(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySe2 { inner: __v }) + } + + /// Logarithm: (vx, vy, omega). + /// + /// Rust: `manifold::lie::Se2::log` + #[pyo3(name = "log")] + #[pyo3(signature = ())] + fn log<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.log())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_vec()) + } + + /// + /// Rust: `manifold::lie::Se2::compose` + #[pyo3(name = "compose")] + #[pyo3(signature = (other))] + fn compose(&self, other: crate::generated::types::PySe2) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.compose(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySe2 { inner: __v }) + } + + /// + /// Rust: `manifold::lie::Se2::inverse` + #[pyo3(name = "inverse")] + #[pyo3(signature = ())] + fn inverse(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.inverse()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySe2 { inner: __v }) + } + + /// 3x3 adjoint on (v, omega). + /// + /// Rust: `manifold::lie::Se2::adjoint` + #[pyo3(name = "adjoint")] + #[pyo3(signature = ())] + fn adjoint<'py>(&self, py: Python<'py>) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.adjoint())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.to_vec()).collect::>()) + } + + /// + /// Rust: `manifold::lie::Se2::apply` + #[pyo3(name = "apply")] + #[pyo3(signature = (p))] + fn apply(&self, p: crate::generated::types::PyVec2Arg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.apply(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) + } + + /// + /// Rust: `manifold::lie::Se2::interpolate` + #[pyo3(name = "interpolate")] + #[pyo3(signature = (other, t))] + fn interpolate(&self, other: crate::generated::types::PySe2, t: f64) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.interpolate(&other, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySe2 { inner: __v }) + } + + /// Row-major 2x3 affine matrix [R | t]. + /// + /// Rust: `manifold::lie::Se2::to_affine2` + #[pyo3(name = "to_affine2")] + #[pyo3(signature = ())] + fn to_affine2<'py>(&self, py: Python<'py>) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.to_affine2())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.to_vec()).collect::>()) + } + + #[getter] + #[pyo3(name = "theta")] + fn py_get_theta(&self) -> PyResult { Ok(self.inner.theta) } + + #[setter] + #[pyo3(name = "theta")] + fn py_set_theta(&mut self, v: f64) { self.inner.theta = v; } + + #[getter] + #[pyo3(name = "t")] + fn py_get_t(&self) -> PyResult { Ok(crate::generated::types::PyVec2 { inner: self.inner.t.clone() }) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Se2", "Se2", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Rigid transform: rotation then translation. +/// +/// Rust: `manifold::lie::Se3` +#[pyclass(name = "Se3", module = "numeria.manifold.lie", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PySe3 { pub inner: rust_physics_engine::manifold::lie::Se3 } +#[pymethods] +impl PySe3 { + /// Builds a `Se3` from its fields. + #[new] + #[pyo3(signature = (r, t))] + fn __new__(r: crate::generated::types::PySo3, t: crate::generated::types::PyVec3Arg) -> Self { + let r = r.inner; + let t = t.0; + Self { inner: rust_physics_engine::manifold::lie::Se3 { r: r, t: t } } + } + + /// + /// Rust: `manifold::lie::Se3::identity` + #[pyo3(name = "identity")] + #[staticmethod] + #[pyo3(signature = ())] + fn identity() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::Se3::identity()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySe3 { inner: __v }) + } + + /// Exponential map: t = J_l(phi) rho. + /// + /// Rust: `manifold::lie::Se3::exp` + #[pyo3(name = "exp")] + #[staticmethod] + #[pyo3(signature = (xi))] + fn exp(xi: crate::generated::types::Pyse3) -> PyResult { + let xi = xi.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::Se3::exp(xi)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySe3 { inner: __v }) + } + + /// Logarithm map. + /// + /// Rust: `manifold::lie::Se3::log` + #[pyo3(name = "log")] + #[pyo3(signature = ())] + fn log(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.log()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::Pyse3 { inner: __v }) + } + + /// + /// Rust: `manifold::lie::Se3::compose` + #[pyo3(name = "compose")] + #[pyo3(signature = (other))] + fn compose(&self, other: crate::generated::types::PySe3) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.compose(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySe3 { inner: __v }) + } + + /// + /// Rust: `manifold::lie::Se3::inverse` + #[pyo3(name = "inverse")] + #[pyo3(signature = ())] + fn inverse(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.inverse()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySe3 { inner: __v }) + } + + /// 6x6 adjoint [[R, hat(t) R], [0, R]], ordering (rho, phi). + /// + /// Rust: `manifold::lie::Se3::adjoint` + #[pyo3(name = "adjoint")] + #[pyo3(signature = ())] + fn adjoint<'py>(&self, py: Python<'py>) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.adjoint())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.to_vec()).collect::>()) + } + + /// + /// Rust: `manifold::lie::Se3::apply_point` + #[pyo3(name = "apply_point")] + #[pyo3(signature = (p))] + fn apply_point(&self, p: crate::generated::types::PyVec3Arg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.apply_point(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// + /// Rust: `manifold::lie::Se3::apply_vector` + #[pyo3(name = "apply_vector")] + #[pyo3(signature = (v))] + fn apply_vector(&self, v: crate::generated::types::PyVec3Arg) -> PyResult { + let v = v.0; + let __r = crate::runtime::guard(|| self.inner.apply_vector(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// + /// Rust: `manifold::lie::Se3::to_mat4` + #[pyo3(name = "to_mat4")] + #[pyo3(signature = ())] + fn to_mat4(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.to_mat4()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLinalgMat4 { inner: __v }) + } + + /// + /// Rust: `manifold::lie::Se3::from_mat4` + #[pyo3(name = "from_mat4")] + #[staticmethod] + #[pyo3(signature = (m))] + fn from_mat4(m: crate::generated::types::PyLinalgMat4) -> PyResult { + let m = m.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::Se3::from_mat4(&m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySe3 { inner: __v }) + } + + /// Build from a rotation frame and origin. + /// + /// Rust: `manifold::lie::Se3::from_frame` + #[pyo3(name = "from_frame")] + #[staticmethod] + #[pyo3(signature = (r, origin))] + fn from_frame(r: crate::generated::types::PyMat3, origin: crate::generated::types::PyVec3Arg) -> PyResult { + let r = r.inner; + let origin = origin.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::Se3::from_frame(&r, origin)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySe3 { inner: __v }) + } + + /// Extract (rotation, origin). + /// + /// Rust: `manifold::lie::Se3::to_frame` + #[pyo3(name = "to_frame")] + #[pyo3(signature = ())] + fn to_frame(&self) -> PyResult<(crate::generated::types::PyMat3, crate::generated::types::PyVec3)> { + let __r = crate::runtime::guard(|| self.inner.to_frame()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((crate::generated::types::PyMat3 { inner: __v.0 }, crate::generated::types::PyVec3 { inner: __v.1 })) + } + + /// Screw-motion interpolation: exp(t log(self^-1 other)) composed on + /// the left with self. + /// + /// Rust: `manifold::lie::Se3::interpolate` + #[pyo3(name = "interpolate")] + #[pyo3(signature = (other, t))] + fn interpolate(&self, other: crate::generated::types::PySe3, t: f64) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.interpolate(&other, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySe3 { inner: __v }) + } + + /// Screw axis of the motion: (direction, point on axis, angle, + /// translation along the axis). + /// + /// Rust: `manifold::lie::Se3::screw_axis` + #[pyo3(name = "screw_axis")] + #[pyo3(signature = ())] + fn screw_axis(&self) -> PyResult<(crate::generated::types::PyVec3, crate::generated::types::PyVec3, f64, f64)> { + let __r = crate::runtime::guard(|| self.inner.screw_axis()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((crate::generated::types::PyVec3 { inner: __v.0 }, crate::generated::types::PyVec3 { inner: __v.1 }, __v.2, __v.3)) + } + + /// Velocity of a point under a twist: v = rho + phi x p. + /// + /// Rust: `manifold::lie::Se3::twist_to_velocity` + #[pyo3(name = "twist_to_velocity")] + #[staticmethod] + #[pyo3(signature = (xi, p))] + fn twist_to_velocity(xi: crate::generated::types::Pyse3, p: crate::generated::types::PyVec3Arg) -> PyResult { + let xi = xi.inner; + let p = p.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::Se3::twist_to_velocity(&xi, p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Left Jacobian of SE(3) (block form with the Barfoot Q matrix), + /// ordering (rho, phi). + /// + /// Rust: `manifold::lie::Se3::jacobian_left` + #[pyo3(name = "jacobian_left")] + #[staticmethod] + #[pyo3(signature = (xi))] + fn jacobian_left<'py>(py: Python<'py>, xi: crate::generated::types::Pyse3) -> PyResult>> { + let xi = xi.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::manifold::lie::Se3::jacobian_left(&xi))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.to_vec()).collect::>()) + } + + /// Right Jacobian: J_r(xi) = J_l(-xi). + /// + /// Rust: `manifold::lie::Se3::jacobian_right` + #[pyo3(name = "jacobian_right")] + #[staticmethod] + #[pyo3(signature = (xi))] + fn jacobian_right<'py>(py: Python<'py>, xi: crate::generated::types::Pyse3) -> PyResult>> { + let xi = xi.inner; + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::manifold::lie::Se3::jacobian_right(&xi))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.to_vec()).collect::>()) + } + + /// Weighted distance: sqrt(|t_rel|^2 + weight * angle^2). + /// + /// Rust: `manifold::lie::Se3::distance` + #[pyo3(name = "distance")] + #[pyo3(signature = (other, weight))] + fn distance(&self, other: crate::generated::types::PySe3, weight: f64) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.distance(&other, weight)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// + /// Rust: `manifold::lie::Se3::random` + #[pyo3(name = "random")] + #[staticmethod] + #[pyo3(signature = (rng))] + fn random(rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::Se3::random(&mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySe3 { inner: __v }) + } + + /// Iterative mean of poses in the group. + /// + /// Rust: `manifold::lie::Se3::mean` + #[pyo3(name = "mean")] + #[staticmethod] + #[pyo3(signature = (poses, iters))] + fn mean(poses: Vec, iters: usize) -> PyResult { + let poses = poses.into_iter().map(|__e| __e.inner).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::Se3::mean(&poses, iters)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySe3 { inner: __v }) + } + + /// Relative transform a^-1 b. + /// + /// Rust: `manifold::lie::Se3::relative` + #[pyo3(name = "relative")] + #[staticmethod] + #[pyo3(signature = (a, b))] + fn relative(a: crate::generated::types::PySe3, b: crate::generated::types::PySe3) -> PyResult { + let a = a.inner; + let b = b.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::Se3::relative(&a, &b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySe3 { inner: __v }) + } + + #[getter] + #[pyo3(name = "r")] + fn py_get_r(&self) -> PyResult { Ok(crate::generated::types::PySo3 { inner: self.inner.r.clone() }) } + + #[getter] + #[pyo3(name = "t")] + fn py_get_t(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.t.clone() }) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Se3", "Se3", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Similarity transform: scale, rotation, translation. +/// +/// Rust: `manifold::lie::Sim3` +#[pyclass(name = "Sim3", module = "numeria.manifold.lie", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PySim3 { pub inner: rust_physics_engine::manifold::lie::Sim3 } +#[pymethods] +impl PySim3 { + /// Builds a `Sim3` from its fields. + #[new] + #[pyo3(signature = (s, r, t))] + fn __new__(s: f64, r: crate::generated::types::PySo3, t: crate::generated::types::PyVec3Arg) -> Self { + let r = r.inner; + let t = t.0; + Self { inner: rust_physics_engine::manifold::lie::Sim3 { s: s, r: r, t: t } } + } + + /// + /// Rust: `manifold::lie::Sim3::identity` + #[pyo3(name = "identity")] + #[staticmethod] + #[pyo3(signature = ())] + fn identity() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::Sim3::identity()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySim3 { inner: __v }) + } + + /// + /// Rust: `manifold::lie::Sim3::apply` + #[pyo3(name = "apply")] + #[pyo3(signature = (p))] + fn apply(&self, p: crate::generated::types::PyVec3Arg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.apply(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// + /// Rust: `manifold::lie::Sim3::compose` + #[pyo3(name = "compose")] + #[pyo3(signature = (other))] + fn compose(&self, other: crate::generated::types::PySim3) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.compose(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySim3 { inner: __v }) + } + + /// + /// Rust: `manifold::lie::Sim3::inverse` + #[pyo3(name = "inverse")] + #[pyo3(signature = ())] + fn inverse(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.inverse()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySim3 { inner: __v }) + } + + #[getter] + #[pyo3(name = "s")] + fn py_get_s(&self) -> PyResult { Ok(self.inner.s) } + + #[setter] + #[pyo3(name = "s")] + fn py_set_s(&mut self, v: f64) { self.inner.s = v; } + + #[getter] + #[pyo3(name = "r")] + fn py_get_r(&self) -> PyResult { Ok(crate::generated::types::PySo3 { inner: self.inner.r.clone() }) } + + #[getter] + #[pyo3(name = "t")] + fn py_get_t(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.t.clone() }) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Sim3", "Sim3", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// SL(2, C) matrix. +/// +/// Rust: `manifold::lie::Sl2C` +#[pyclass(name = "Sl2C", module = "numeria.manifold.lie", from_py_object)] +#[derive(Clone)] +pub struct PySl2C { pub inner: rust_physics_engine::manifold::lie::Sl2C } +#[pymethods] +impl PySl2C { + /// + /// Rust: `manifold::lie::Sl2C::identity` + #[pyo3(name = "identity")] + #[staticmethod] + #[pyo3(signature = ())] + fn identity() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::Sl2C::identity()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySl2C { inner: __v }) + } + + /// Mobius action on the Riemann sphere. + /// + /// Rust: `manifold::lie::Sl2C::mobius` + #[pyo3(name = "mobius")] + #[pyo3(signature = (z))] + fn mobius<'py>(&self, py: Python<'py>, z: crate::runtime::coerce::ComplexArg) -> PyResult> { + let z = z.0; + let __r = crate::runtime::guard(|| self.inner.mobius(z)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::complex_out(py, __v)) + } + + /// + /// Rust: `manifold::lie::Sl2C::compose` + #[pyo3(name = "compose")] + #[pyo3(signature = (o))] + fn compose(&self, o: crate::generated::types::PySl2C) -> PyResult { + let o = o.inner; + let __r = crate::runtime::guard(|| self.inner.compose(&o)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySl2C { inner: __v }) + } + + /// + /// Rust: `manifold::lie::Sl2C::inverse` + #[pyo3(name = "inverse")] + #[pyo3(signature = ())] + fn inverse(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.inverse()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySl2C { inner: __v }) + } + + /// Double cover onto SO(3,1): X = t I + x s1 + y s2 + z s3 maps as + /// X -> A X A^dagger; returns the 4x4 Lorentz matrix acting on + /// (t, x, y, z). + /// + /// Rust: `manifold::lie::Sl2C::to_lorentz` + #[pyo3(name = "to_lorentz")] + #[pyo3(signature = ())] + fn to_lorentz(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.to_lorentz()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLinalgMat4 { inner: __v }) + } + + /// Pure boost with rapidity `phi` along the unit direction `n`: + /// A = cosh(phi/2) I + sinh(phi/2) (n . sigma). + /// + /// Rust: `manifold::lie::Sl2C::from_lorentz_boost` + #[pyo3(name = "from_lorentz_boost")] + #[staticmethod] + #[pyo3(signature = (n, phi))] + fn from_lorentz_boost(n: crate::generated::types::PyVec3Arg, phi: f64) -> PyResult { + let n = n.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::Sl2C::from_lorentz_boost(n, phi)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySl2C { inner: __v }) + } + + /// Classification by the (complex) trace. + /// + /// Rust: `manifold::lie::Sl2C::classify` + #[pyo3(name = "classify")] + #[pyo3(signature = ())] + fn classify(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.classify()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySl2Class::from_rust(&__v)) + } + + #[getter] + #[pyo3(name = "m")] + fn py_get_m<'py>(&self, py: Python<'py>) -> PyResult>>> { Ok(self.inner.m.clone().into_iter().map(|__x| __x.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()).collect::>()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Sl2C", "Sl2C", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Classification of Mobius/SL(2) elements. +/// +/// Rust: `manifold::lie::Sl2Class` +#[pyclass(name = "Sl2Class", module = "numeria.manifold.lie", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PySl2Class { + Elliptic, + Parabolic, + Hyperbolic, + Loxodromic, +} +impl PySl2Class { + pub fn to_rust(&self) -> rust_physics_engine::manifold::lie::Sl2Class { match self { + Self::Elliptic => rust_physics_engine::manifold::lie::Sl2Class::Elliptic, + Self::Parabolic => rust_physics_engine::manifold::lie::Sl2Class::Parabolic, + Self::Hyperbolic => rust_physics_engine::manifold::lie::Sl2Class::Hyperbolic, + Self::Loxodromic => rust_physics_engine::manifold::lie::Sl2Class::Loxodromic, + } } + pub fn from_rust(v: &rust_physics_engine::manifold::lie::Sl2Class) -> Self { match v { + rust_physics_engine::manifold::lie::Sl2Class::Elliptic => Self::Elliptic, + rust_physics_engine::manifold::lie::Sl2Class::Parabolic => Self::Parabolic, + rust_physics_engine::manifold::lie::Sl2Class::Hyperbolic => Self::Hyperbolic, + rust_physics_engine::manifold::lie::Sl2Class::Loxodromic => Self::Loxodromic, + } } +} +#[pymethods] +impl PySl2Class { + fn __repr__(&self) -> &'static str { + match self { + Self::Elliptic => "Sl2Class.Elliptic", + Self::Parabolic => "Sl2Class.Parabolic", + Self::Hyperbolic => "Sl2Class.Hyperbolic", + Self::Loxodromic => "Sl2Class.Loxodromic", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// SL(2, R) matrix. +/// +/// Rust: `manifold::lie::Sl2R` +#[pyclass(name = "Sl2R", module = "numeria.manifold.lie", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PySl2R { pub inner: rust_physics_engine::manifold::lie::Sl2R } +#[pymethods] +impl PySl2R { + /// + /// Rust: `manifold::lie::Sl2R::identity` + #[pyo3(name = "identity")] + #[staticmethod] + #[pyo3(signature = ())] + fn identity() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::Sl2R::identity()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySl2R { inner: __v }) + } + + /// Logarithm via the general matrix log. + /// + /// Rust: `manifold::lie::Sl2R::log` + #[pyo3(name = "log")] + #[pyo3(signature = ())] + fn log<'py>(&self, py: Python<'py>) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.log())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.to_vec()).collect::>()) + } + + /// + /// Rust: `manifold::lie::Sl2R::compose` + #[pyo3(name = "compose")] + #[pyo3(signature = (o))] + fn compose(&self, o: crate::generated::types::PySl2R) -> PyResult { + let o = o.inner; + let __r = crate::runtime::guard(|| self.inner.compose(&o)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySl2R { inner: __v }) + } + + /// + /// Rust: `manifold::lie::Sl2R::inverse` + #[pyo3(name = "inverse")] + #[pyo3(signature = ())] + fn inverse(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.inverse()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySl2R { inner: __v }) + } + + /// Mobius action on the upper half-plane: z -> (az + b)/(cz + d). + /// + /// Rust: `manifold::lie::Sl2R::act_on_upper_half_plane` + #[pyo3(name = "act_on_upper_half_plane")] + #[pyo3(signature = (z))] + fn act_on_upper_half_plane<'py>(&self, py: Python<'py>, z: crate::runtime::coerce::ComplexArg) -> PyResult> { + let z = z.0; + let __r = crate::runtime::guard(|| self.inner.act_on_upper_half_plane(z)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::complex_out(py, __v)) + } + + /// Classification by |trace|. + /// + /// Rust: `manifold::lie::Sl2R::classify` + #[pyo3(name = "classify")] + #[pyo3(signature = ())] + fn classify(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.classify()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySl2Class::from_rust(&__v)) + } + + /// Fixed points of the Mobius action (roots of c z^2 + (d - a) z - b). + /// + /// Rust: `manifold::lie::Sl2R::fixed_points` + #[pyo3(name = "fixed_points")] + #[pyo3(signature = ())] + fn fixed_points<'py>(&self, py: Python<'py>) -> PyResult>> { + let __r = crate::runtime::guard(|| self.inner.fixed_points()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) + } + + /// Translation length of a hyperbolic element: 2 acosh(|tr|/2). + /// + /// Rust: `manifold::lie::Sl2R::translation_length` + #[pyo3(name = "translation_length")] + #[pyo3(signature = ())] + fn translation_length(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.translation_length()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "m")] + fn py_get_m(&self) -> PyResult>> { Ok(self.inner.m.clone().into_iter().map(|__x| __x.to_vec()).collect::>()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Sl2R", "Sl2R", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Planar rotation by an angle. +/// +/// Rust: `manifold::lie::So2` +#[pyclass(name = "So2", module = "numeria.manifold.lie", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PySo2 { pub inner: rust_physics_engine::manifold::lie::So2 } +#[pymethods] +impl PySo2 { + /// + /// Rust: `manifold::lie::So2::compose` + #[pyo3(name = "compose")] + #[pyo3(signature = (other))] + fn compose(&self, other: crate::generated::types::PySo2) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.compose(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySo2 { inner: __v }) + } + + /// + /// Rust: `manifold::lie::So2::inverse` + #[pyo3(name = "inverse")] + #[pyo3(signature = ())] + fn inverse(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.inverse()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySo2 { inner: __v }) + } + + /// + /// Rust: `manifold::lie::So2::apply` + #[pyo3(name = "apply")] + #[pyo3(signature = (v))] + fn apply(&self, v: crate::generated::types::PyVec2Arg) -> PyResult { + let v = v.0; + let __r = crate::runtime::guard(|| self.inner.apply(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) + } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("So2", "So2", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A 3D rotation stored as a matrix. +/// +/// Rust: `manifold::lie::So3` +#[pyclass(name = "So3", module = "numeria.manifold.lie", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PySo3 { pub inner: rust_physics_engine::manifold::lie::So3 } +#[pymethods] +impl PySo3 { + /// + /// Rust: `manifold::lie::So3::identity` + #[pyo3(name = "identity")] + #[staticmethod] + #[pyo3(signature = ())] + fn identity() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::So3::identity()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySo3 { inner: __v }) + } + + /// Hat operator: w -> skew-symmetric matrix. + /// + /// Rust: `manifold::lie::So3::hat` + #[pyo3(name = "hat")] + #[staticmethod] + #[pyo3(signature = (w))] + fn hat(w: crate::generated::types::PyVec3Arg) -> PyResult { + let w = w.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::So3::hat(w)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMat3 { inner: __v }) + } + + /// Vee operator: skew-symmetric matrix -> vector. + /// + /// Rust: `manifold::lie::So3::vee` + #[pyo3(name = "vee")] + #[staticmethod] + #[pyo3(signature = (m))] + fn vee(m: crate::generated::types::PyMat3) -> PyResult { + let m = m.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::So3::vee(&m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Rodrigues exponential. + /// + /// Rust: `manifold::lie::So3::exp` + #[pyo3(name = "exp")] + #[staticmethod] + #[pyo3(signature = (w))] + fn exp(w: crate::generated::types::PyVec3Arg) -> PyResult { + let w = w.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::So3::exp(w)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySo3 { inner: __v }) + } + + /// Logarithm: rotation vector with |w| in [0, pi]. + /// + /// Rust: `manifold::lie::So3::log` + #[pyo3(name = "log")] + #[pyo3(signature = ())] + fn log(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.log()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// + /// Rust: `manifold::lie::So3::from_axis_angle` + #[pyo3(name = "from_axis_angle")] + #[staticmethod] + #[pyo3(signature = (axis, angle))] + fn from_axis_angle(axis: crate::generated::types::PyVec3Arg, angle: f64) -> PyResult { + let axis = axis.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::So3::from_axis_angle(axis, angle)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySo3 { inner: __v }) + } + + /// + /// Rust: `manifold::lie::So3::from_quat` + #[pyo3(name = "from_quat")] + #[staticmethod] + #[pyo3(signature = (q))] + fn from_quat(q: crate::generated::types::PyQuaternionArg) -> PyResult { + let q = q.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::So3::from_quat(&q)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySo3 { inner: __v }) + } + + /// + /// Rust: `manifold::lie::So3::to_quat` + #[pyo3(name = "to_quat")] + #[pyo3(signature = ())] + fn to_quat(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.to_quat()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuaternion { inner: __v }) + } + + /// + /// Rust: `manifold::lie::So3::compose` + #[pyo3(name = "compose")] + #[pyo3(signature = (other))] + fn compose(&self, other: crate::generated::types::PySo3) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.compose(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySo3 { inner: __v }) + } + + /// + /// Rust: `manifold::lie::So3::inverse` + #[pyo3(name = "inverse")] + #[pyo3(signature = ())] + fn inverse(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.inverse()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySo3 { inner: __v }) + } + + /// Adjoint of SO(3) is the rotation matrix itself. + /// + /// Rust: `manifold::lie::So3::adjoint` + #[pyo3(name = "adjoint")] + #[pyo3(signature = ())] + fn adjoint(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.adjoint()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMat3 { inner: __v }) + } + + /// + /// Rust: `manifold::lie::So3::apply` + #[pyo3(name = "apply")] + #[pyo3(signature = (v))] + fn apply(&self, v: crate::generated::types::PyVec3Arg) -> PyResult { + let v = v.0; + let __r = crate::runtime::guard(|| self.inner.apply(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Rotation angle in [0, pi]. + /// + /// Rust: `manifold::lie::So3::angle` + #[pyo3(name = "angle")] + #[pyo3(signature = ())] + fn angle(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.angle()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Geodesic distance (relative rotation angle). + /// + /// Rust: `manifold::lie::So3::distance` + #[pyo3(name = "distance")] + #[pyo3(signature = (other))] + fn distance(&self, other: crate::generated::types::PySo3) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.distance(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Geodesic interpolation R exp(t log(R^-1 S)). + /// + /// Rust: `manifold::lie::So3::interpolate` + #[pyo3(name = "interpolate")] + #[pyo3(signature = (other, t))] + fn interpolate(&self, other: crate::generated::types::PySo3, t: f64) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.interpolate(&other, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySo3 { inner: __v }) + } + + /// Uniform random rotation (via random unit quaternion). + /// + /// Rust: `manifold::lie::So3::random` + #[pyo3(name = "random")] + #[staticmethod] + #[pyo3(signature = (rng))] + fn random(rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::So3::random(&mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySo3 { inner: __v }) + } + + /// Nearest rotation to an arbitrary matrix (polar projection via SVD). + /// + /// Rust: `manifold::lie::So3::project` + #[pyo3(name = "project")] + #[staticmethod] + #[pyo3(signature = (m))] + fn project(m: crate::generated::types::PyMat3) -> PyResult { + let m = m.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::So3::project(&m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySo3 { inner: __v }) + } + + /// Left Jacobian of SO(3). + /// + /// Rust: `manifold::lie::So3::left_jacobian` + #[pyo3(name = "left_jacobian")] + #[staticmethod] + #[pyo3(signature = (w))] + fn left_jacobian(w: crate::generated::types::PyVec3Arg) -> PyResult { + let w = w.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::So3::left_jacobian(w)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMat3 { inner: __v }) + } + + /// Right Jacobian: J_r(w) = J_l(-w). + /// + /// Rust: `manifold::lie::So3::right_jacobian` + #[pyo3(name = "right_jacobian")] + #[staticmethod] + #[pyo3(signature = (w))] + fn right_jacobian(w: crate::generated::types::PyVec3Arg) -> PyResult { + let w = w.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::So3::right_jacobian(w)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMat3 { inner: __v }) + } + + /// Inverse left Jacobian. + /// + /// Rust: `manifold::lie::So3::left_jacobian_inv` + #[pyo3(name = "left_jacobian_inv")] + #[staticmethod] + #[pyo3(signature = (w))] + fn left_jacobian_inv(w: crate::generated::types::PyVec3Arg) -> PyResult { + let w = w.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::So3::left_jacobian_inv(w)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMat3 { inner: __v }) + } + + /// Inverse right Jacobian. + /// + /// Rust: `manifold::lie::So3::right_jacobian_inv` + #[pyo3(name = "right_jacobian_inv")] + #[staticmethod] + #[pyo3(signature = (w))] + fn right_jacobian_inv(w: crate::generated::types::PyVec3Arg) -> PyResult { + let w = w.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::So3::right_jacobian_inv(w)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMat3 { inner: __v }) + } + + /// Baker-Campbell-Hausdorff series for so(3) to the given order + /// (1, 2, or 3): log(exp a exp b). + /// + /// Rust: `manifold::lie::So3::bch` + #[pyo3(name = "bch")] + #[staticmethod] + #[pyo3(signature = (a, b, order))] + fn bch(a: crate::generated::types::PyVec3Arg, b: crate::generated::types::PyVec3Arg, order: usize) -> PyResult { + let a = a.0; + let b = b.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::So3::bch(a, b, order)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Geodesic (Karcher) mean of rotations. + /// + /// Rust: `manifold::lie::So3::geodesic_mean` + #[pyo3(name = "geodesic_mean")] + #[staticmethod] + #[pyo3(signature = (rots, iters))] + fn geodesic_mean(rots: Vec, iters: usize) -> PyResult { + let rots = rots.into_iter().map(|__e| __e.inner).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::So3::geodesic_mean(&rots, iters)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySo3 { inner: __v }) + } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("So3", "So3", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A 4D rotation matrix. +/// +/// Rust: `manifold::lie::So4` +#[pyclass(name = "So4", module = "numeria.manifold.lie", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PySo4 { pub inner: rust_physics_engine::manifold::lie::So4 } +#[pymethods] +impl PySo4 { + /// + /// Rust: `manifold::lie::So4::identity` + #[pyo3(name = "identity")] + #[staticmethod] + #[pyo3(signature = ())] + fn identity() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::So4::identity()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySo4 { inner: __v }) + } + + /// Exponential of a bivector (b01, b02, b03, b12, b13, b23): builds the + /// antisymmetric generator and exponentiates. + /// + /// Rust: `manifold::lie::So4::exp` + #[pyo3(name = "exp")] + #[staticmethod] + #[pyo3(signature = (bivector))] + fn exp(bivector: Vec) -> PyResult { + let bivector = <[f64; 6]>::try_from(bivector).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 6 values, got {}", __v.len())))?; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::So4::exp(bivector)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySo4 { inner: __v }) + } + + /// Logarithm: bivector components in the same ordering as `So4::exp`. + /// + /// Rust: `manifold::lie::So4::log` + #[pyo3(name = "log")] + #[pyo3(signature = ())] + fn log<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.log())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_vec()) + } + + /// Rotation p -> l p r_conj with quaternions acting on (w, x, y, z). + /// + /// Rust: `manifold::lie::So4::from_double_quaternion` + #[pyo3(name = "from_double_quaternion")] + #[staticmethod] + #[pyo3(signature = (l, r))] + fn from_double_quaternion(l: crate::generated::types::PyQuaternionArg, r: crate::generated::types::PyQuaternionArg) -> PyResult { + let l = l.0; + let r = r.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::So4::from_double_quaternion(l, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySo4 { inner: __v }) + } + + /// Factor into the double quaternion pair (l, r), unique up to a common + /// sign. + /// + /// Rust: `manifold::lie::So4::to_double_quaternion` + #[pyo3(name = "to_double_quaternion")] + #[pyo3(signature = ())] + fn to_double_quaternion(&self) -> PyResult<(crate::generated::types::PyQuaternion, crate::generated::types::PyQuaternion)> { + let __r = crate::runtime::guard(|| self.inner.to_double_quaternion()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((crate::generated::types::PyQuaternion { inner: __v.0 }, crate::generated::types::PyQuaternion { inner: __v.1 })) + } + + /// Left-isoclinic rotation p -> q p. + /// + /// Rust: `manifold::lie::So4::isoclinic_left` + #[pyo3(name = "isoclinic_left")] + #[staticmethod] + #[pyo3(signature = (q))] + fn isoclinic_left(q: crate::generated::types::PyQuaternionArg) -> PyResult { + let q = q.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::So4::isoclinic_left(q)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySo4 { inner: __v }) + } + + /// Right-isoclinic rotation p -> p q_conj. + /// + /// Rust: `manifold::lie::So4::isoclinic_right` + #[pyo3(name = "isoclinic_right")] + #[staticmethod] + #[pyo3(signature = (q))] + fn isoclinic_right(q: crate::generated::types::PyQuaternionArg) -> PyResult { + let q = q.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::So4::isoclinic_right(q)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySo4 { inner: __v }) + } + + /// + /// Rust: `manifold::lie::So4::compose` + #[pyo3(name = "compose")] + #[pyo3(signature = (other))] + fn compose(&self, other: crate::generated::types::PySo4) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.compose(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySo4 { inner: __v }) + } + + /// + /// Rust: `manifold::lie::So4::inverse` + #[pyo3(name = "inverse")] + #[pyo3(signature = ())] + fn inverse(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.inverse()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySo4 { inner: __v }) + } + + /// + /// Rust: `manifold::lie::So4::apply` + #[pyo3(name = "apply")] + #[pyo3(signature = (p))] + fn apply<'py>(&self, py: Python<'py>, p: Vec) -> PyResult> { + let p = <[f64; 4]>::try_from(p).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 4 values, got {}", __v.len())))?; + let __r = py.detach(move || crate::runtime::guard(move || self.inner.apply(p))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_vec()) + } + + /// + /// Rust: `manifold::lie::So4::random` + #[pyo3(name = "random")] + #[staticmethod] + #[pyo3(signature = (rng))] + fn random(rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::So4::random(&mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySo4 { inner: __v }) + } + + /// Rotation by `angle` in a single coordinate plane. + /// + /// Rust: `manifold::lie::So4::simple_rotation` + #[pyo3(name = "simple_rotation")] + #[staticmethod] + #[pyo3(signature = (plane, angle))] + fn simple_rotation(plane: (usize, usize), angle: f64) -> PyResult { + let plane = (plane.0, plane.1); + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::So4::simple_rotation(plane, angle)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySo4 { inner: __v }) + } + + /// Double rotation: `angle1` in the (0,1) plane, `angle2` in (2,3). + /// + /// Rust: `manifold::lie::So4::double_rotation` + #[pyo3(name = "double_rotation")] + #[staticmethod] + #[pyo3(signature = (angle1, angle2))] + fn double_rotation(angle1: f64, angle2: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::So4::double_rotation(angle1, angle2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySo4 { inner: __v }) + } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("So4", "So4", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// SU(2) element stored as a unit quaternion. +/// +/// Rust: `manifold::lie::Su2` +#[pyclass(name = "Su2", module = "numeria.manifold.lie", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PySu2 { pub inner: rust_physics_engine::manifold::lie::Su2 } +#[pymethods] +impl PySu2 { + /// Exponential: rotation by angle |a| about a/|a| (double cover of + /// SO(3): the quaternion carries the half angle). + /// + /// Rust: `manifold::lie::Su2::exp` + #[pyo3(name = "exp")] + #[staticmethod] + #[pyo3(signature = (a))] + fn exp(a: crate::generated::types::PyVec3Arg) -> PyResult { + let a = a.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::Su2::exp(a)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySu2 { inner: __v }) + } + + /// Logarithm: axis times angle, angle in [0, 2 pi). + /// + /// Rust: `manifold::lie::Su2::log` + #[pyo3(name = "log")] + #[pyo3(signature = ())] + fn log(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.log()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Double cover onto SO(3): q and -q map to the same rotation. + /// + /// Rust: `manifold::lie::Su2::to_so3` + #[pyo3(name = "to_so3")] + #[pyo3(signature = ())] + fn to_so3(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.to_so3()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySo3 { inner: __v }) + } + + /// Coefficients (c0, c1, c2, c3) with U = c0 I + c1 s1 + c2 s2 + c3 s3 + /// in the Pauli basis: U = w I - i (x s1 + y s2 + z s3). + /// + /// Rust: `manifold::lie::Su2::pauli_decompose` + #[pyo3(name = "pauli_decompose")] + #[pyo3(signature = ())] + fn pauli_decompose<'py>(&self, py: Python<'py>) -> PyResult>> { + let __r = crate::runtime::guard(|| self.inner.pauli_decompose()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) + } + + /// The 2x2 complex matrix U = w I - i (x s1 + y s2 + z s3). + /// + /// Rust: `manifold::lie::Su2::to_matrix_2x2` + #[pyo3(name = "to_matrix_2x2")] + #[pyo3(signature = ())] + fn to_matrix_2x2<'py>(&self, py: Python<'py>) -> PyResult>>> { + let __r = crate::runtime::guard(|| self.inner.to_matrix_2x2()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()).collect::>()) + } + + /// + /// Rust: `manifold::lie::Su2::compose` + #[pyo3(name = "compose")] + #[pyo3(signature = (other))] + fn compose(&self, other: crate::generated::types::PySu2) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.compose(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySu2 { inner: __v }) + } + + /// + /// Rust: `manifold::lie::Su2::inverse` + #[pyo3(name = "inverse")] + #[pyo3(signature = ())] + fn inverse(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.inverse()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySu2 { inner: __v }) + } + + /// Matrix trace (real: 2w). + /// + /// Rust: `manifold::lie::Su2::trace` + #[pyo3(name = "trace")] + #[pyo3(signature = ())] + fn trace(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.trace()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Character of the spin-j representation: + /// chi_j(theta) = sin((2j+1) theta/2)/sin(theta/2) where theta is the + /// SO(3) rotation angle. + /// + /// Rust: `manifold::lie::Su2::character` + #[pyo3(name = "character")] + #[pyo3(signature = (j))] + fn character(&self, j: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.character(j)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Su2", "Su2", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A unitary matrix U(n) with complex entries. +/// +/// Rust: `manifold::lie::Unitary` +#[pyclass(name = "Unitary", module = "numeria.manifold.lie", from_py_object)] +#[derive(Clone)] +pub struct PyUnitary { pub inner: rust_physics_engine::manifold::lie::Unitary } +#[pymethods] +impl PyUnitary { + /// Builds a `Unitary` from its fields. + #[new] + #[pyo3(signature = (m))] + fn __new__(m: Vec>) -> Self { + let m = m.into_iter().map(|__e| __e.into_iter().map(|__e| __e.0).collect::>()).collect::>(); + Self { inner: rust_physics_engine::manifold::lie::Unitary { m: m } } + } + + /// U = exp(-i H t) for Hermitian H, by Taylor with scaling-squaring. + /// + /// Rust: `manifold::lie::Unitary::from_hermitian_exp` + #[pyo3(name = "from_hermitian_exp")] + #[staticmethod] + #[pyo3(signature = (h, t))] + fn from_hermitian_exp(h: Vec>, t: f64) -> PyResult { + let h = h.into_iter().map(|__e| __e.into_iter().map(|__e| __e.0).collect::>()).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::Unitary::from_hermitian_exp(&h, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyUnitary { inner: __v }) + } + + /// Check U U^dagger = I within `tol`. + /// + /// Rust: `manifold::lie::Unitary::is_unitary` + #[pyo3(name = "is_unitary")] + #[pyo3(signature = (tol))] + fn is_unitary(&self, tol: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_unitary(tol)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// + /// Rust: `manifold::lie::Unitary::compose` + #[pyo3(name = "compose")] + #[pyo3(signature = (o))] + fn compose(&self, o: crate::generated::types::PyUnitary) -> PyResult { + let o = o.inner; + let __r = crate::runtime::guard(|| self.inner.compose(&o)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyUnitary { inner: __v }) + } + + /// Conjugate transpose. + /// + /// Rust: `manifold::lie::Unitary::dagger` + #[pyo3(name = "dagger")] + #[pyo3(signature = ())] + fn dagger(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.dagger()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyUnitary { inner: __v }) + } + + /// Haar-random unitary via Gram-Schmidt of a complex Gaussian matrix + /// with phase normalization. + /// + /// Rust: `manifold::lie::Unitary::random_haar` + #[pyo3(name = "random_haar")] + #[staticmethod] + #[pyo3(signature = (n, rng))] + fn random_haar(n: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::lie::Unitary::random_haar(n, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyUnitary { inner: __v }) + } + + #[getter] + #[pyo3(name = "m")] + fn py_get_m<'py>(&self, py: Python<'py>) -> PyResult>>> { Ok(self.inner.m.clone().into_iter().map(|__x| __x.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()).collect::>()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Unitary", "Unitary", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// se(3) algebra element: linear part rho, angular part phi. +/// +/// Rust: `manifold::lie::se3` +#[pyclass(name = "se3", module = "numeria.manifold.lie", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct Pyse3 { pub inner: rust_physics_engine::manifold::lie::se3 } +#[pymethods] +impl Pyse3 { + /// Builds a `se3` from its fields. + #[new] + #[pyo3(signature = (rho, phi))] + fn __new__(rho: crate::generated::types::PyVec3Arg, phi: crate::generated::types::PyVec3Arg) -> Self { + let rho = rho.0; + let phi = phi.0; + Self { inner: rust_physics_engine::manifold::lie::se3 { rho: rho, phi: phi } } + } + + #[getter] + #[pyo3(name = "rho")] + fn py_get_rho(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.rho.clone() }) } + + #[getter] + #[pyo3(name = "phi")] + fn py_get_phi(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.phi.clone() }) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("se3", "se3", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// so(3) algebra element (axis-angle vector). +/// +/// Rust: `manifold::lie::so3` +#[pyclass(name = "so3", module = "numeria.manifold.lie", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct Pyso3 { pub inner: rust_physics_engine::manifold::lie::so3 } +#[pymethods] +impl Pyso3 { + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("so3", "so3", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A (pseudo-)Riemannian metric given by a coordinate chart function. +/// +/// Rust: `manifold::metric::Metric` +#[pyclass(name = "Metric", module = "numeria.manifold.metric", unsendable)] +pub struct PyMetricMetric { pub inner: rust_physics_engine::manifold::metric::Metric } +#[pymethods] +impl PyMetricMetric { + /// Flat Euclidean space in Cartesian coordinates. + /// + /// Rust: `manifold::metric::Metric::euclidean` + #[pyo3(name = "euclidean")] + #[staticmethod] + #[pyo3(signature = (n))] + fn euclidean(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::metric::Metric::euclidean(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMetricMetric { inner: __v }) + } + + /// Flat Minkowski space with the time coordinate first. + /// + /// Rust: `manifold::metric::Metric::minkowski` + #[pyo3(name = "minkowski")] + #[staticmethod] + #[pyo3(signature = (n, signature))] + fn minkowski(n: usize, signature: crate::generated::types::PySig) -> PyResult { + let signature = signature.to_rust(); + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::metric::Metric::minkowski(n, signature)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMetricMetric { inner: __v }) + } + + /// Round n-sphere of radius `r` in hyperspherical angles + /// (theta_1, ..., theta_n): g_ii = r^2 prod_{k PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::metric::Metric::sphere(n, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMetricMetric { inner: __v }) + } + + /// Poincare ball model of hyperbolic n-space (curvature -1): + /// g = 4 delta / (1 - |x|^2)^2. + /// + /// Rust: `manifold::metric::Metric::hyperbolic_ball` + #[pyo3(name = "hyperbolic_ball")] + #[staticmethod] + #[pyo3(signature = (n))] + fn hyperbolic_ball(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::metric::Metric::hyperbolic_ball(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMetricMetric { inner: __v }) + } + + /// Poincare half-space model of hyperbolic n-space: g = delta / x_n^2 + /// (last coordinate positive). + /// + /// Rust: `manifold::metric::Metric::poincare_half_space` + #[pyo3(name = "poincare_half_space")] + #[staticmethod] + #[pyo3(signature = (n))] + fn poincare_half_space(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::metric::Metric::poincare_half_space(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMetricMetric { inner: __v }) + } + + /// Flat n-torus with circumference radii `radii` (angle coordinates). + /// + /// Rust: `manifold::metric::Metric::torus_flat` + #[pyo3(name = "torus_flat")] + #[staticmethod] + #[pyo3(signature = (n, radii))] + fn torus_flat(n: usize, radii: Vec) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::metric::Metric::torus_flat(n, &radii)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMetricMetric { inner: __v }) + } + + /// Schwarzschild exterior in coordinates (t, r, theta, phi), G = c = 1. + /// + /// Rust: `manifold::metric::Metric::schwarzschild` + #[pyo3(name = "schwarzschild")] + #[staticmethod] + #[pyo3(signature = (m))] + fn schwarzschild(m: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::metric::Metric::schwarzschild(m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMetricMetric { inner: __v }) + } + + /// Kerr metric in Boyer-Lindquist coordinates (t, r, theta, phi). + /// + /// Rust: `manifold::metric::Metric::kerr` + #[pyo3(name = "kerr")] + #[staticmethod] + #[pyo3(signature = (m, a))] + fn kerr(m: f64, a: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::metric::Metric::kerr(m, a)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMetricMetric { inner: __v }) + } + + /// De Sitter space in static coordinates with Hubble length `l`. + /// + /// Rust: `manifold::metric::Metric::de_sitter` + #[pyo3(name = "de_sitter")] + #[staticmethod] + #[pyo3(signature = (l))] + fn de_sitter(l: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::metric::Metric::de_sitter(l)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMetricMetric { inner: __v }) + } + + /// Anti-de Sitter space in static coordinates with AdS radius `l`. + /// + /// Rust: `manifold::metric::Metric::anti_de_sitter` + #[pyo3(name = "anti_de_sitter")] + #[staticmethod] + #[pyo3(signature = (l))] + fn anti_de_sitter(l: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::metric::Metric::anti_de_sitter(l)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMetricMetric { inner: __v }) + } + + /// Induced metric from an embedding of an n-manifold into R^m: + /// g_ij = d(embed)/dx^i . d(embed)/dx^j (finite differences). + /// + /// Rust: `manifold::metric::Metric::induced_from_embedding` + #[pyo3(name = "induced_from_embedding")] + #[staticmethod] + #[pyo3(signature = (dim, embed))] + fn induced_from_embedding(dim: usize, embed: pyo3::Py) -> PyResult { + let __cb_embed = std::rc::Rc::new(crate::runtime::Callback::new(embed)); + let embed = { let __cb = __cb_embed.clone(); move |__a0: &rust_physics_engine::manifold::vecn::VecN| -> rust_physics_engine::manifold::vecn::VecN { { let __r = __cb.call::<_, crate::generated::types::PyVecNArg>((crate::generated::types::PyVecN { inner: __a0.clone() },), crate::generated::types::PyVecNArg(rust_physics_engine::manifold::vecn::VecN { data: Vec::new() })); __r.0 } } }; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::metric::Metric::induced_from_embedding(dim, embed)); + crate::runtime::callback::check(&[&__cb_embed], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMetricMetric { inner: __v }) + } + + /// + /// Rust: `manifold::metric::Metric::at` + #[pyo3(name = "at")] + #[pyo3(signature = (p))] + fn at(&self, p: crate::generated::types::PyVecNArg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.at(&p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) + } + + /// + /// Rust: `manifold::metric::Metric::inverse_at` + #[pyo3(name = "inverse_at")] + #[pyo3(signature = (p))] + fn inverse_at(&self, p: crate::generated::types::PyVecNArg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.inverse_at(&p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) + } + + /// + /// Rust: `manifold::metric::Metric::det_at` + #[pyo3(name = "det_at")] + #[pyo3(signature = (p))] + fn det_at(&self, p: crate::generated::types::PyVecNArg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.det_at(&p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Metric signature at `p`: (number of positive, number of negative) + /// eigenvalues. + /// + /// Rust: `manifold::metric::Metric::signature` + #[pyo3(name = "signature")] + #[pyo3(signature = (p))] + fn signature(&self, p: crate::generated::types::PyVecNArg) -> PyResult<(usize, usize)> { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.signature(&p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) + } + + /// d g_ij / d x^k by central differences. + /// + /// Rust: `manifold::metric::Metric::dg` + #[pyo3(name = "dg")] + #[pyo3(signature = (p, k))] + fn dg(&self, p: crate::generated::types::PyVecNArg, k: usize) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.dg(&p, k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) + } + + /// Christoffel symbols of the second kind Gamma^i_{jk}, shape [n, n, n]. + /// + /// Rust: `manifold::metric::Metric::christoffel` + #[pyo3(name = "christoffel")] + #[pyo3(signature = (p))] + fn christoffel(&self, p: crate::generated::types::PyVecNArg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.christoffel(&p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTensorN { inner: __v }) + } + + /// Christoffel symbols of the first kind Gamma_{ijk} = + /// (1/2)(d_j g_ik + d_k g_ij - d_i g_jk). + /// + /// Rust: `manifold::metric::Metric::christoffel_first_kind` + #[pyo3(name = "christoffel_first_kind")] + #[pyo3(signature = (p))] + fn christoffel_first_kind(&self, p: crate::generated::types::PyVecNArg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.christoffel_first_kind(&p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTensorN { inner: __v }) + } + + /// Riemann tensor R^i_{jkl} = d_k Gamma^i_{lj} - d_l Gamma^i_{kj} + /// + Gamma^i_{km} Gamma^m_{lj} - Gamma^i_{lm} Gamma^m_{kj}. + /// + /// Rust: `manifold::metric::Metric::riemann` + #[pyo3(name = "riemann")] + #[pyo3(signature = (p))] + fn riemann(&self, p: crate::generated::types::PyVecNArg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.riemann(&p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTensorN { inner: __v }) + } + + /// Fully lowered Riemann tensor R_{ijkl}. + /// + /// Rust: `manifold::metric::Metric::riemann_lowered` + #[pyo3(name = "riemann_lowered")] + #[pyo3(signature = (p))] + fn riemann_lowered(&self, p: crate::generated::types::PyVecNArg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.riemann_lowered(&p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTensorN { inner: __v }) + } + + /// Ricci tensor R_{jl} = R^i_{jil}. + /// + /// Rust: `manifold::metric::Metric::ricci` + #[pyo3(name = "ricci")] + #[pyo3(signature = (p))] + fn ricci(&self, p: crate::generated::types::PyVecNArg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.ricci(&p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) + } + + /// Ricci scalar R = g^{jl} R_{jl}. + /// + /// Rust: `manifold::metric::Metric::ricci_scalar` + #[pyo3(name = "ricci_scalar")] + #[pyo3(signature = (p))] + fn ricci_scalar(&self, p: crate::generated::types::PyVecNArg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.ricci_scalar(&p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Einstein tensor G_ij = R_ij - (R/2) g_ij. + /// + /// Rust: `manifold::metric::Metric::einstein_tensor` + #[pyo3(name = "einstein_tensor")] + #[pyo3(signature = (p))] + fn einstein_tensor(&self, p: crate::generated::types::PyVecNArg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.einstein_tensor(&p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) + } + + /// Weyl conformal tensor C_{ijkl} (dimension at least 3). + /// + /// Rust: `manifold::metric::Metric::weyl` + #[pyo3(name = "weyl")] + #[pyo3(signature = (p))] + fn weyl(&self, p: crate::generated::types::PyVecNArg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.weyl(&p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTensorN { inner: __v }) + } + + /// Kretschmann scalar K = R_{ijkl} R^{ijkl}. + /// + /// Rust: `manifold::metric::Metric::kretschmann` + #[pyo3(name = "kretschmann")] + #[pyo3(signature = (p))] + fn kretschmann(&self, p: crate::generated::types::PyVecNArg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.kretschmann(&p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Sectional curvature of the plane spanned by `u`, `v` at `p`. + /// + /// Rust: `manifold::metric::Metric::sectional_curvature` + #[pyo3(name = "sectional_curvature")] + #[pyo3(signature = (p, u, v))] + fn sectional_curvature(&self, p: crate::generated::types::PyVecNArg, u: crate::generated::types::PyVecNArg, v: crate::generated::types::PyVecNArg) -> PyResult { + let p = p.0; + let u = u.0; + let v = v.0; + let __r = crate::runtime::guard(|| self.inner.sectional_curvature(&p, &u, &v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Gaussian curvature (dimension 2 only): K = R_{0101} / det g. + /// + /// Rust: `manifold::metric::Metric::gaussian_curvature` + #[pyo3(name = "gaussian_curvature")] + #[pyo3(signature = (p))] + fn gaussian_curvature(&self, p: crate::generated::types::PyVecNArg) -> PyResult> { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.gaussian_curvature(&p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) + } + + /// True when the Riemann tensor vanishes within `tol` (per component, + /// relative to the metric scale). + /// + /// Rust: `manifold::metric::Metric::is_flat` + #[pyo3(name = "is_flat")] + #[pyo3(signature = (p, tol))] + fn is_flat(&self, p: crate::generated::types::PyVecNArg, tol: f64) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.is_flat(&p, tol)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// True when Ricci = (R/n) g within `tol`. + /// + /// Rust: `manifold::metric::Metric::is_einstein` + #[pyo3(name = "is_einstein")] + #[pyo3(signature = (p, tol))] + fn is_einstein(&self, p: crate::generated::types::PyVecNArg, tol: f64) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.is_einstein(&p, tol)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Covariant derivative of a vector field along `direction`: + /// (nabla_d v)^i = d^j d_j v^i + Gamma^i_{jk} d^j v^k. + /// + /// Rust: `manifold::metric::Metric::covariant_derivative_vector` + #[pyo3(name = "covariant_derivative_vector")] + #[pyo3(signature = (v, p, direction))] + fn covariant_derivative_vector(&self, v: pyo3::Py, p: crate::generated::types::PyVecNArg, direction: crate::generated::types::PyVecNArg) -> PyResult { + let __cb_v = std::rc::Rc::new(crate::runtime::Callback::new(v)); + let v = { let __cb = __cb_v.clone(); move |__a0: &rust_physics_engine::manifold::vecn::VecN| -> rust_physics_engine::manifold::vecn::VecN { { let __r = __cb.call::<_, crate::generated::types::PyVecNArg>((crate::generated::types::PyVecN { inner: __a0.clone() },), crate::generated::types::PyVecNArg(rust_physics_engine::manifold::vecn::VecN { data: Vec::new() })); __r.0 } } }; + let p = p.0; + let direction = direction.0; + let __r = crate::runtime::guard(|| self.inner.covariant_derivative_vector(&v, &p, &direction)); + crate::runtime::callback::check(&[&__cb_v], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVecN { inner: __v }) + } + + /// Covariant divergence of a vector field: + /// nabla_i v^i = (1/sqrt|g|) d_i (sqrt|g| v^i). + /// + /// Rust: `manifold::metric::Metric::divergence` + #[pyo3(name = "divergence")] + #[pyo3(signature = (v, p))] + fn divergence(&self, v: pyo3::Py, p: crate::generated::types::PyVecNArg) -> PyResult { + let __cb_v = std::rc::Rc::new(crate::runtime::Callback::new(v)); + let v = { let __cb = __cb_v.clone(); move |__a0: &rust_physics_engine::manifold::vecn::VecN| -> rust_physics_engine::manifold::vecn::VecN { { let __r = __cb.call::<_, crate::generated::types::PyVecNArg>((crate::generated::types::PyVecN { inner: __a0.clone() },), crate::generated::types::PyVecNArg(rust_physics_engine::manifold::vecn::VecN { data: Vec::new() })); __r.0 } } }; + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.divergence(&v, &p)); + crate::runtime::callback::check(&[&__cb_v], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Laplace-Beltrami operator on a scalar field: + /// (1/sqrt|g|) d_i (sqrt|g| g^{ij} d_j f). + /// + /// Rust: `manifold::metric::Metric::laplace_beltrami` + #[pyo3(name = "laplace_beltrami")] + #[pyo3(signature = (f, p))] + fn laplace_beltrami(&self, f: pyo3::Py, p: crate::generated::types::PyVecNArg) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: &rust_physics_engine::manifold::vecn::VecN| -> f64 { __cb.call::<_, f64>((crate::generated::types::PyVecN { inner: __a0.clone() },), f64::NAN) } }; + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.laplace_beltrami(&f, &p)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Riemannian gradient (index raised): (grad f)^i = g^{ij} d_j f. + /// + /// Rust: `manifold::metric::Metric::gradient` + #[pyo3(name = "gradient")] + #[pyo3(signature = (f, p))] + fn gradient(&self, f: pyo3::Py, p: crate::generated::types::PyVecNArg) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: &rust_physics_engine::manifold::vecn::VecN| -> f64 { __cb.call::<_, f64>((crate::generated::types::PyVecN { inner: __a0.clone() },), f64::NAN) } }; + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.gradient(&f, &p)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVecN { inner: __v }) + } + + /// Volume element sqrt |det g|. + /// + /// Rust: `manifold::metric::Metric::volume_element` + #[pyo3(name = "volume_element")] + #[pyo3(signature = (p))] + fn volume_element(&self, p: crate::generated::types::PyVecNArg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.volume_element(&p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Midpoint-rule integral of `f` against the volume element over a + /// coordinate box. + /// + /// Rust: `manifold::metric::Metric::volume_integrate` + #[pyo3(name = "volume_integrate")] + #[pyo3(signature = (f, bounds, n_per_dim))] + fn volume_integrate(&self, f: pyo3::Py, bounds: Vec<(f64, f64)>, n_per_dim: usize) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: &rust_physics_engine::manifold::vecn::VecN| -> f64 { __cb.call::<_, f64>((crate::generated::types::PyVecN { inner: __a0.clone() },), f64::NAN) } }; + let bounds = bounds.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let __r = crate::runtime::guard(|| self.inner.volume_integrate(&f, &bounds, n_per_dim)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Length of a curve c(t) from t0 to t1 using n midpoint samples of + /// sqrt |g(c', c')|. + /// + /// Rust: `manifold::metric::Metric::length_of_curve` + #[pyo3(name = "length_of_curve")] + #[pyo3(signature = (c, t0, t1, n))] + fn length_of_curve(&self, c: pyo3::Py, t0: f64, t1: f64, n: usize) -> PyResult { + let __cb_c = std::rc::Rc::new(crate::runtime::Callback::new(c)); + let c = { let __cb = __cb_c.clone(); move |__a0: f64| -> rust_physics_engine::manifold::vecn::VecN { { let __r = __cb.call::<_, crate::generated::types::PyVecNArg>((__a0,), crate::generated::types::PyVecNArg(rust_physics_engine::manifold::vecn::VecN { data: Vec::new() })); __r.0 } } }; + let __r = crate::runtime::guard(|| self.inner.length_of_curve(&c, t0, t1, n)); + crate::runtime::callback::check(&[&__cb_c], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Metric inner product g(u, v) at p. + /// + /// Rust: `manifold::metric::Metric::inner` + #[pyo3(name = "inner")] + #[pyo3(signature = (p, u, v))] + fn inner(&self, p: crate::generated::types::PyVecNArg, u: crate::generated::types::PyVecNArg, v: crate::generated::types::PyVecNArg) -> PyResult { + let p = p.0; + let u = u.0; + let v = v.0; + let __r = crate::runtime::guard(|| self.inner.inner(&p, &u, &v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Metric norm sqrt |g(v, v)|. + /// + /// Rust: `manifold::metric::Metric::norm` + #[pyo3(name = "norm")] + #[pyo3(signature = (p, v))] + fn norm(&self, p: crate::generated::types::PyVecNArg, v: crate::generated::types::PyVecNArg) -> PyResult { + let p = p.0; + let v = v.0; + let __r = crate::runtime::guard(|| self.inner.norm(&p, &v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Angle between u and v in the metric. + /// + /// Rust: `manifold::metric::Metric::angle` + #[pyo3(name = "angle")] + #[pyo3(signature = (p, u, v))] + fn angle(&self, p: crate::generated::types::PyVecNArg, u: crate::generated::types::PyVecNArg, v: crate::generated::types::PyVecNArg) -> PyResult { + let p = p.0; + let u = u.0; + let v = v.0; + let __r = crate::runtime::guard(|| self.inner.angle(&p, &u, &v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Orthonormal frame (vielbein) at `p`: rows are frame vectors obtained + /// by Gram-Schmidt of the coordinate basis in the metric inner product, + /// normalized by sqrt |g(e, e)| (works for Lorentzian signatures). + /// + /// Rust: `manifold::metric::Metric::orthonormal_frame` + #[pyo3(name = "orthonormal_frame")] + #[pyo3(signature = (p))] + fn orthonormal_frame(&self, p: crate::generated::types::PyVecNArg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.orthonormal_frame(&p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) + } + + /// Lie derivative of the metric along `xi`: + /// (L_xi g)_{ij} = xi^k d_k g_ij + g_kj d_i xi^k + g_ik d_j xi^k. + /// + /// Rust: `manifold::metric::Metric::lie_derivative_metric` + #[pyo3(name = "lie_derivative_metric")] + #[pyo3(signature = (xi, p))] + fn lie_derivative_metric(&self, xi: pyo3::Py, p: crate::generated::types::PyVecNArg) -> PyResult { + let __cb_xi = std::rc::Rc::new(crate::runtime::Callback::new(xi)); + let xi = { let __cb = __cb_xi.clone(); move |__a0: &rust_physics_engine::manifold::vecn::VecN| -> rust_physics_engine::manifold::vecn::VecN { { let __r = __cb.call::<_, crate::generated::types::PyVecNArg>((crate::generated::types::PyVecN { inner: __a0.clone() },), crate::generated::types::PyVecNArg(rust_physics_engine::manifold::vecn::VecN { data: Vec::new() })); __r.0 } } }; + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.lie_derivative_metric(&xi, &p)); + crate::runtime::callback::check(&[&__cb_xi], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) + } + + /// True when `xi` is a Killing field at `p` within `tol`. + /// + /// Rust: `manifold::metric::Metric::killing_check` + #[pyo3(name = "killing_check")] + #[pyo3(signature = (xi, p, tol))] + fn killing_check(&self, xi: pyo3::Py, p: crate::generated::types::PyVecNArg, tol: f64) -> PyResult { + let __cb_xi = std::rc::Rc::new(crate::runtime::Callback::new(xi)); + let xi = { let __cb = __cb_xi.clone(); move |__a0: &rust_physics_engine::manifold::vecn::VecN| -> rust_physics_engine::manifold::vecn::VecN { { let __r = __cb.call::<_, crate::generated::types::PyVecNArg>((crate::generated::types::PyVecN { inner: __a0.clone() },), crate::generated::types::PyVecNArg(rust_physics_engine::manifold::vecn::VecN { data: Vec::new() })); __r.0 } } }; + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.killing_check(&xi, &p, tol)); + crate::runtime::callback::check(&[&__cb_xi], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// If g = lambda * g_other at `p` (componentwise, consistent), return + /// lambda. + /// + /// Rust: `manifold::metric::Metric::conformal_factor_to` + #[pyo3(name = "conformal_factor_to")] + #[pyo3(signature = (other, p))] + fn conformal_factor_to(&self, other: pyo3::PyRef<'_, crate::generated::types::PyMetricMetric>, p: crate::generated::types::PyVecNArg) -> PyResult> { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.conformal_factor_to(&other.inner, &p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) + } + + /// Max residual of the first Bianchi identity + /// `R_{i[jkl]}` : `R_ijkl + R_iklj + R_iljk = 0`, normalized by the largest + /// Riemann component. + /// + /// Rust: `manifold::metric::Metric::bianchi_identity_residual` + #[pyo3(name = "bianchi_identity_residual")] + #[pyo3(signature = (p))] + fn bianchi_identity_residual(&self, p: crate::generated::types::PyVecNArg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.bianchi_identity_residual(&p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "dim")] + fn py_get_dim(&self) -> PyResult { Ok(self.inner.dim) } + + #[setter] + #[pyo3(name = "dim")] + fn py_set_dim(&mut self, v: usize) { self.inner.dim = v; } + + #[getter] + #[pyo3(name = "h")] + fn py_get_h(&self) -> PyResult { Ok(self.inner.h) } + + #[setter] + #[pyo3(name = "h")] + fn py_set_h(&mut self, v: f64) { self.inner.h = v; } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// Signature convention for Minkowski-type metrics. +/// +/// Rust: `manifold::metric::Sig` +#[pyclass(name = "Sig", module = "numeria.manifold.metric", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PySig { + MostlyPlus, + MostlyMinus, +} +impl PySig { + pub fn to_rust(&self) -> rust_physics_engine::manifold::metric::Sig { match self { + Self::MostlyPlus => rust_physics_engine::manifold::metric::Sig::MostlyPlus, + Self::MostlyMinus => rust_physics_engine::manifold::metric::Sig::MostlyMinus, + } } + pub fn from_rust(v: &rust_physics_engine::manifold::metric::Sig) -> Self { match v { + rust_physics_engine::manifold::metric::Sig::MostlyPlus => Self::MostlyPlus, + rust_physics_engine::manifold::metric::Sig::MostlyMinus => Self::MostlyMinus, + } } +} +#[pymethods] +impl PySig { + fn __repr__(&self) -> &'static str { + match self { + Self::MostlyPlus => "Sig.MostlyPlus", + Self::MostlyMinus => "Sig.MostlyMinus", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A 4-polytope: vertices with edge, face (2D), and cell (3D facet) +/// combinatorics. Faces and cells list vertex indices. +/// +/// Rust: `manifold::polytope4::Polytope4` +#[pyclass(name = "Polytope4", module = "numeria.manifold.polytope4", from_py_object)] +#[derive(Clone)] +pub struct PyPolytope4 { pub inner: rust_physics_engine::manifold::polytope4::Polytope4 } +#[pymethods] +impl PyPolytope4 { + /// Builds a `Polytope4` from its fields. + #[new] + #[pyo3(signature = (vertices, edges, faces, cells))] + fn __new__(vertices: Vec, edges: Vec<(usize, usize)>, faces: Vec>, cells: Vec>) -> Self { + let vertices = vertices.into_iter().map(|__e| __e.0).collect::>(); + let edges = edges.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + Self { inner: rust_physics_engine::manifold::polytope4::Polytope4 { vertices: vertices, edges: edges, faces: faces, cells: cells } } + } + + /// The 8-cell (tesseract). + /// + /// Rust: `manifold::polytope4::Polytope4::tesseract` + #[pyo3(name = "tesseract")] + #[staticmethod] + #[pyo3(signature = ())] + fn tesseract() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::polytope4::Polytope4::tesseract()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolytope4 { inner: __v }) + } + + /// The 16-cell (4D cross-polytope). + /// + /// Rust: `manifold::polytope4::Polytope4::cell16` + #[pyo3(name = "cell16")] + #[staticmethod] + #[pyo3(signature = ())] + fn cell16() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::polytope4::Polytope4::cell16()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolytope4 { inner: __v }) + } + + /// The self-dual 24-cell. + /// + /// Rust: `manifold::polytope4::Polytope4::cell24` + #[pyo3(name = "cell24")] + #[staticmethod] + #[pyo3(signature = ())] + fn cell24() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::polytope4::Polytope4::cell24()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolytope4 { inner: __v }) + } + + /// The regular 5-cell (4-simplex). + /// + /// Rust: `manifold::polytope4::Polytope4::simplex5` + #[pyo3(name = "simplex5")] + #[staticmethod] + #[pyo3(signature = ())] + fn simplex5() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::polytope4::Polytope4::simplex5()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolytope4 { inner: __v }) + } + + /// The 600-cell (vertices are the 120 unit icosians; cells are the + /// tetrahedral 4-cliques of the edge graph). + /// + /// Rust: `manifold::polytope4::Polytope4::cell600` + #[pyo3(name = "cell600")] + #[staticmethod] + #[pyo3(signature = ())] + fn cell600() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::polytope4::Polytope4::cell600()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolytope4 { inner: __v }) + } + + /// The 120-cell: vertices are the 600 cell centers of the 600-cell, + /// with dodecahedral cells found from the 600-cell vertex directions. + /// + /// Rust: `manifold::polytope4::Polytope4::cell120` + #[pyo3(name = "cell120")] + #[staticmethod] + #[pyo3(signature = ())] + fn cell120() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::polytope4::Polytope4::cell120()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolytope4 { inner: __v }) + } + + /// p,q-duoprism: the product of a p-gon and a q-gon. + /// + /// Rust: `manifold::polytope4::Polytope4::duoprism` + #[pyo3(name = "duoprism")] + #[staticmethod] + #[pyo3(signature = (p, q))] + fn duoprism(p: usize, q: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::polytope4::Polytope4::duoprism(p, q)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolytope4 { inner: __v }) + } + + /// Discretized duocylinder (n x n grid on the Clifford-torus ridge); + /// vertices and edges only. + /// + /// Rust: `manifold::polytope4::Polytope4::duocylinder` + #[pyo3(name = "duocylinder")] + #[staticmethod] + #[pyo3(signature = (n))] + fn duocylinder(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::polytope4::Polytope4::duocylinder(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolytope4 { inner: __v }) + } + + /// Grand antiprism: the 600-cell with two orthogonal rings of ten + /// vertices removed. Vertices and edges only. + /// + /// Rust: `manifold::polytope4::Polytope4::grand_antiprism` + #[pyo3(name = "grand_antiprism")] + #[staticmethod] + #[pyo3(signature = ())] + fn grand_antiprism() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::polytope4::Polytope4::grand_antiprism()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolytope4 { inner: __v }) + } + + /// Discretized cubinder (cylinder x square); vertices/edges only. + /// + /// Rust: `manifold::polytope4::Polytope4::cubinder` + #[pyo3(name = "cubinder")] + #[staticmethod] + #[pyo3(signature = (n))] + fn cubinder(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::polytope4::Polytope4::cubinder(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolytope4 { inner: __v }) + } + + /// Discretized spherinder (sphere x segment); vertices/edges only. + /// + /// Rust: `manifold::polytope4::Polytope4::spherinder` + #[pyo3(name = "spherinder")] + #[staticmethod] + #[pyo3(signature = (n))] + fn spherinder(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::polytope4::Polytope4::spherinder(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolytope4 { inner: __v }) + } + + /// Rectified polytope: vertices at edge midpoints (vertices and edges). + /// + /// Rust: `manifold::polytope4::Polytope4::rectified` + #[pyo3(name = "rectified")] + #[pyo3(signature = ())] + fn rectified(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.rectified()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolytope4 { inner: __v }) + } + + /// Truncated polytope: two vertices per edge at the one-third points + /// (vertices and edges). + /// + /// Rust: `manifold::polytope4::Polytope4::truncated` + #[pyo3(name = "truncated")] + #[pyo3(signature = ())] + fn truncated(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.truncated()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolytope4 { inner: __v }) + } + + /// Dual polytope: vertices at cell centers, full combinatorics from the + /// original vertex directions. + /// + /// Rust: `manifold::polytope4::Polytope4::dual` + #[pyo3(name = "dual")] + #[pyo3(signature = ())] + fn dual(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.dual()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolytope4 { inner: __v }) + } + + /// Euler characteristic V - E + F - C (zero for all convex 4-polytopes). + /// + /// Rust: `manifold::polytope4::Polytope4::euler_characteristic` + #[pyo3(name = "euler_characteristic")] + #[pyo3(signature = ())] + fn euler_characteristic(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.euler_characteristic()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// (V, E, F, C). + /// + /// Rust: `manifold::polytope4::Polytope4::f_vector` + #[pyo3(name = "f_vector")] + #[pyo3(signature = ())] + fn f_vector<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.f_vector())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_vec()) + } + + /// Common edge length. + /// + /// Rust: `manifold::polytope4::Polytope4::edge_length` + #[pyo3(name = "edge_length")] + #[pyo3(signature = ())] + fn edge_length(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.edge_length()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Rotate all vertices by a 4D rotation. + /// + /// Rust: `manifold::polytope4::Polytope4::rotate` + #[pyo3(name = "rotate")] + #[pyo3(signature = (r))] + fn rotate(&self, r: crate::generated::types::PySo4) -> PyResult { + let r = r.inner; + let __r = crate::runtime::guard(|| self.inner.rotate(&r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolytope4 { inner: __v }) + } + + /// Perspective projection from w = `distance` into 3D, triangulating the + /// faces into a mesh. + /// + /// Rust: `manifold::polytope4::Polytope4::project_perspective` + #[pyo3(name = "project_perspective")] + #[pyo3(signature = (distance))] + fn project_perspective(&self, distance: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.project_perspective(distance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGeometryMeshMesh { inner: __v }) + } + + /// Orthographic projection dropping one axis. + /// + /// Rust: `manifold::polytope4::Polytope4::project_orthographic` + #[pyo3(name = "project_orthographic")] + #[pyo3(signature = (drop_axis))] + fn project_orthographic(&self, drop_axis: usize) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.project_orthographic(drop_axis)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) + } + + /// Stereographic projection from S3 (for unit-radius polytopes such as + /// the 120-cell and 600-cell). + /// + /// Rust: `manifold::polytope4::Polytope4::project_stereographic` + #[pyo3(name = "project_stereographic")] + #[pyo3(signature = ())] + fn project_stereographic(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.project_stereographic()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) + } + + /// 3D cross-section by the hyperplane w = const (convex hull of the + /// edge-hyperplane intersection points). + /// + /// Rust: `manifold::polytope4::Polytope4::cross_section` + #[pyo3(name = "cross_section")] + #[pyo3(signature = (w))] + fn cross_section(&self, w: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.cross_section(w)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGeometryMeshMesh { inner: __v }) + } + + /// Cross-section by the hyperplane normal . x = offset. + /// + /// Rust: `manifold::polytope4::Polytope4::cross_section_oriented` + #[pyo3(name = "cross_section_oriented")] + #[pyo3(signature = (normal, offset))] + fn cross_section_oriented(&self, normal: crate::generated::types::PyVec4Arg, offset: f64) -> PyResult { + let normal = normal.0; + let __r = crate::runtime::guard(|| self.inner.cross_section_oriented(normal, offset)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGeometryMeshMesh { inner: __v }) + } + + /// Unfold the cells into disjoint 3D meshes (a simple net: each cell is + /// projected into its own hyperplane coordinates and translated apart; + /// for the tesseract this is the classical 8-cube cross layout). + /// + /// Rust: `manifold::polytope4::Polytope4::unfold_net` + #[pyo3(name = "unfold_net")] + #[pyo3(signature = ())] + fn unfold_net(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.unfold_net()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyGeometryMeshMesh { inner: __x }).collect::>()) + } + + /// Schlegel diagram: perspective projection from just outside the + /// center of the chosen cell. + /// + /// Rust: `manifold::polytope4::Polytope4::schlegel_diagram` + #[pyo3(name = "schlegel_diagram")] + #[pyo3(signature = (cell))] + fn schlegel_diagram(&self, cell: usize) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.schlegel_diagram(cell)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) + } + + /// Vertex figure: the polyhedron whose vertices are the neighbors of + /// `v`, in the neighbors' midpoint positions (returned as a hull mesh). + /// + /// Rust: `manifold::polytope4::Polytope4::vertex_figure` + #[pyo3(name = "vertex_figure")] + #[pyo3(signature = (v))] + fn vertex_figure(&self, v: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.vertex_figure(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGeometryMeshMesh { inner: __v }) + } + + /// Order of the full symmetry group (regular polytopes only). + /// + /// Rust: `manifold::polytope4::Polytope4::symmetry_order` + #[pyo3(name = "symmetry_order")] + #[pyo3(signature = ())] + fn symmetry_order(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.symmetry_order()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// A handful of rotations generating (a subgroup of) the symmetry + /// group: rotations by the face angle in coordinate planes that map the + /// vertex set to itself. + /// + /// Rust: `manifold::polytope4::Polytope4::coxeter_group_generators` + #[pyo3(name = "coxeter_group_generators")] + #[pyo3(signature = ())] + fn coxeter_group_generators(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.coxeter_group_generators()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PySo4 { inner: __x }).collect::>()) + } + + /// Construct from a Schlafli/Wythoff symbol. + /// + /// Rust: `manifold::polytope4::Polytope4::from_wythoff` + #[pyo3(name = "from_wythoff")] + #[staticmethod] + #[pyo3(signature = (symbol))] + fn from_wythoff(symbol: String) -> PyResult> { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::polytope4::Polytope4::from_wythoff(&symbol)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyPolytope4 { inner: __x })) + } + + /// Dihedral angle between adjacent cells (across a shared face). + /// + /// Rust: `manifold::polytope4::Polytope4::dihedral_angle` + #[pyo3(name = "dihedral_angle")] + #[pyo3(signature = ())] + fn dihedral_angle(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.dihedral_angle()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Circumradius (max vertex distance from the origin). + /// + /// Rust: `manifold::polytope4::Polytope4::circumradius` + #[pyo3(name = "circumradius")] + #[pyo3(signature = ())] + fn circumradius(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.circumradius()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Inradius (distance from the origin to a cell hyperplane). + /// + /// Rust: `manifold::polytope4::Polytope4::inradius` + #[pyo3(name = "inradius")] + #[pyo3(signature = ())] + fn inradius(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.inradius()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// 4D hypervolume: sum of cone volumes over cells, + /// (1/4) * cell volume * inradius contribution. + /// + /// Rust: `manifold::polytope4::Polytope4::hypervolume` + #[pyo3(name = "hypervolume")] + #[pyo3(signature = ())] + fn hypervolume(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.hypervolume()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Total surface volume: sum of the 3D volumes of all cells. + /// + /// Rust: `manifold::polytope4::Polytope4::surface_volume` + #[pyo3(name = "surface_volume")] + #[pyo3(signature = ())] + fn surface_volume(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.surface_volume()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "vertices")] + fn py_get_vertices(&self) -> PyResult> { Ok(self.inner.vertices.clone().into_iter().map(|__x| crate::generated::types::PyVec4 { inner: __x }).collect::>()) } + + #[getter] + #[pyo3(name = "edges")] + fn py_get_edges(&self) -> PyResult> { Ok(self.inner.edges.clone().into_iter().map(|__x| (__x.0, __x.1)).collect::>()) } + + #[getter] + #[pyo3(name = "faces")] + fn py_get_faces(&self) -> PyResult>> { Ok(self.inner.faces.clone()) } + + #[getter] + #[pyo3(name = "cells")] + fn py_get_cells(&self) -> PyResult>> { Ok(self.inner.cells.clone()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Polytope4", "Polytope4", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A 4D vector. +/// +/// Rust: `manifold::polytope4::Vec4` +#[pyclass(name = "Vec4", module = "numeria.manifold.polytope4", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyVec4 { pub inner: rust_physics_engine::manifold::polytope4::Vec4 } +#[pymethods] +impl PyVec4 { + /// + /// Rust: `manifold::polytope4::Vec4::new` + #[new] + #[pyo3(signature = (x, y, z, w))] + fn __new__(x: f64, y: f64, z: f64, w: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::polytope4::Vec4::new(x, y, z, w)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec4 { inner: __v }) + } + + /// + /// Rust: `manifold::polytope4::Vec4::dot` + #[pyo3(name = "dot")] + #[pyo3(signature = (o))] + fn dot(&self, o: crate::generated::types::PyVec4Arg) -> PyResult { + let o = o.0; + let __r = crate::runtime::guard(|| self.inner.dot(&o)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// + /// Rust: `manifold::polytope4::Vec4::norm` + #[pyo3(name = "norm")] + #[pyo3(signature = ())] + fn norm(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.norm()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// + /// Rust: `manifold::polytope4::Vec4::normalized` + #[pyo3(name = "normalized")] + #[pyo3(signature = ())] + fn normalized(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.normalized()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec4 { inner: __v }) + } + + /// + /// Rust: `manifold::polytope4::Vec4::add` + #[pyo3(name = "add")] + #[pyo3(signature = (o))] + fn add(&self, o: crate::generated::types::PyVec4Arg) -> PyResult { + let o = o.0; + let __r = crate::runtime::guard(|| self.inner.add(&o)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec4 { inner: __v }) + } + + /// + /// Rust: `manifold::polytope4::Vec4::sub` + #[pyo3(name = "sub")] + #[pyo3(signature = (o))] + fn sub(&self, o: crate::generated::types::PyVec4Arg) -> PyResult { + let o = o.0; + let __r = crate::runtime::guard(|| self.inner.sub(&o)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec4 { inner: __v }) + } + + /// + /// Rust: `manifold::polytope4::Vec4::scale` + #[pyo3(name = "scale")] + #[pyo3(signature = (k))] + fn scale(&self, k: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.scale(k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec4 { inner: __v }) + } + + #[getter] + #[pyo3(name = "x")] + fn py_get_x(&self) -> PyResult { Ok(self.inner.x) } + + #[setter] + #[pyo3(name = "x")] + fn py_set_x(&mut self, v: f64) { self.inner.x = v; } + + #[getter] + #[pyo3(name = "y")] + fn py_get_y(&self) -> PyResult { Ok(self.inner.y) } + + #[setter] + #[pyo3(name = "y")] + fn py_set_y(&mut self, v: f64) { self.inner.y = v; } + + #[getter] + #[pyo3(name = "z")] + fn py_get_z(&self) -> PyResult { Ok(self.inner.z) } + + #[setter] + #[pyo3(name = "z")] + fn py_set_z(&mut self, v: f64) { self.inner.z = v; } + + #[getter] + #[pyo3(name = "w")] + fn py_get_w(&self) -> PyResult { Ok(self.inner.w) } + + #[setter] + #[pyo3(name = "w")] + fn py_set_w(&mut self, v: f64) { self.inner.w = v; } + + fn __repr__(&self) -> String { format!("Vec4(x={:?}, y={:?}, z={:?}, w={:?})", self.inner.x, self.inner.y, self.inner.z, self.inner.w) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A `Vec4` argument, or anything that can stand in for one. +pub struct PyVec4Arg(pub rust_physics_engine::manifold::polytope4::Vec4); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyVec4Arg { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result { + if let Ok(__w) = obj.extract::() { + return Ok(PyVec4Arg(__w.inner)); + } + let __v = crate::runtime::coerce::floats_exact(obj, 4, "Vec4")?; + Ok(PyVec4Arg(rust_physics_engine::manifold::polytope4::Vec4 { x: __v[0], y: __v[1], z: __v[2], w: __v[3] })) + } +} + + +/// Causal relation of event `b` relative to event `a`. +/// +/// Rust: `manifold::spacetime::Causal` +#[pyclass(name = "Causal", module = "numeria.manifold.spacetime", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyCausal { + Past, + Future, + Spacelike, + Null, +} +impl PyCausal { + pub fn to_rust(&self) -> rust_physics_engine::manifold::spacetime::Causal { match self { + Self::Past => rust_physics_engine::manifold::spacetime::Causal::Past, + Self::Future => rust_physics_engine::manifold::spacetime::Causal::Future, + Self::Spacelike => rust_physics_engine::manifold::spacetime::Causal::Spacelike, + Self::Null => rust_physics_engine::manifold::spacetime::Causal::Null, + } } + pub fn from_rust(v: &rust_physics_engine::manifold::spacetime::Causal) -> Self { match v { + rust_physics_engine::manifold::spacetime::Causal::Past => Self::Past, + rust_physics_engine::manifold::spacetime::Causal::Future => Self::Future, + rust_physics_engine::manifold::spacetime::Causal::Spacelike => Self::Spacelike, + rust_physics_engine::manifold::spacetime::Causal::Null => Self::Null, + } } +} +#[pymethods] +impl PyCausal { + fn __repr__(&self) -> &'static str { + match self { + Self::Past => "Causal.Past", + Self::Future => "Causal.Future", + Self::Spacelike => "Causal.Spacelike", + Self::Null => "Causal.Null", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A spacetime four-vector (t, x) in units with c = 1. +/// +/// Rust: `manifold::spacetime::FourVector` +#[pyclass(name = "FourVector", module = "numeria.manifold.spacetime", from_py_object)] +#[derive(Clone)] +pub struct PyFourVector { pub inner: rust_physics_engine::manifold::spacetime::FourVector } +#[pymethods] +impl PyFourVector { + /// + /// Rust: `manifold::spacetime::FourVector::new` + #[new] + #[pyo3(signature = (t, x))] + fn __new__(t: f64, x: crate::generated::types::PyVec3Arg) -> PyResult { + let x = x.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spacetime::FourVector::new(t, x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyFourVector { inner: __v }) + } + + /// Minkowski inner product in the mostly-minus convention + /// (+, -, -, -): a.b = a_t b_t - a_x . b_x. See + /// `FourVector::minkowski_dot_sig` for the other signature. + /// + /// Rust: `manifold::spacetime::FourVector::minkowski_dot` + #[pyo3(name = "minkowski_dot")] + #[pyo3(signature = (o))] + fn minkowski_dot(&self, o: crate::generated::types::PyFourVector) -> PyResult { + let o = o.inner; + let __r = crate::runtime::guard(|| self.inner.minkowski_dot(&o)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Minkowski inner product with an explicit signature convention. + /// + /// Rust: `manifold::spacetime::FourVector::minkowski_dot_sig` + #[pyo3(name = "minkowski_dot_sig")] + #[pyo3(signature = (o, sig))] + fn minkowski_dot_sig(&self, o: crate::generated::types::PyFourVector, sig: crate::generated::types::PySig) -> PyResult { + let o = o.inner; + let sig = sig.to_rust(); + let __r = crate::runtime::guard(|| self.inner.minkowski_dot_sig(&o, sig)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Invariant norm squared t^2 - |x|^2 (positive for timelike vectors). + /// + /// Rust: `manifold::spacetime::FourVector::norm_squared` + #[pyo3(name = "norm_squared")] + #[pyo3(signature = ())] + fn norm_squared(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.norm_squared()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// + /// Rust: `manifold::spacetime::FourVector::is_timelike` + #[pyo3(name = "is_timelike")] + #[pyo3(signature = ())] + fn is_timelike(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_timelike()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// + /// Rust: `manifold::spacetime::FourVector::is_spacelike` + #[pyo3(name = "is_spacelike")] + #[pyo3(signature = ())] + fn is_spacelike(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_spacelike()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// + /// Rust: `manifold::spacetime::FourVector::is_null` + #[pyo3(name = "is_null")] + #[pyo3(signature = (tol))] + fn is_null(&self, tol: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_null(tol)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Active boost: the transform that maps a particle at rest to + /// three-velocity `v` (|v| < 1). + /// + /// Rust: `manifold::spacetime::FourVector::boost` + #[pyo3(name = "boost")] + #[pyo3(signature = (v))] + fn boost(&self, v: crate::generated::types::PyVec3Arg) -> PyResult { + let v = v.0; + let __r = crate::runtime::guard(|| self.inner.boost(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyFourVector { inner: __v }) + } + + /// Spatial rotation of the vector part. + /// + /// Rust: `manifold::spacetime::FourVector::rotate` + #[pyo3(name = "rotate")] + #[pyo3(signature = (r))] + fn rotate(&self, r: crate::generated::types::PySo3) -> PyResult { + let r = r.inner; + let __r = crate::runtime::guard(|| self.inner.rotate(&r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyFourVector { inner: __v }) + } + + /// Four-velocity gamma (1, v) of a particle moving at `v` (|v| < 1). + /// + /// Rust: `manifold::spacetime::FourVector::from_velocity` + #[pyo3(name = "from_velocity")] + #[staticmethod] + #[pyo3(signature = (v))] + fn from_velocity(v: crate::generated::types::PyVec3Arg) -> PyResult { + let v = v.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spacetime::FourVector::from_velocity(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyFourVector { inner: __v }) + } + + /// Four-momentum m gamma (1, v) of a mass m moving at `v`. + /// + /// Rust: `manifold::spacetime::FourVector::from_momentum` + #[pyo3(name = "from_momentum")] + #[staticmethod] + #[pyo3(signature = (m, v))] + fn from_momentum(m: f64, v: crate::generated::types::PyVec3Arg) -> PyResult { + let v = v.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spacetime::FourVector::from_momentum(m, v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyFourVector { inner: __v }) + } + + /// Energy (time component, c = 1). + /// + /// Rust: `manifold::spacetime::FourVector::energy` + #[pyo3(name = "energy")] + #[pyo3(signature = ())] + fn energy(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.energy()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Spatial momentum (vector part). + /// + /// Rust: `manifold::spacetime::FourVector::spatial_momentum` + #[pyo3(name = "spatial_momentum")] + #[pyo3(signature = ())] + fn spatial_momentum(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.spatial_momentum()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Rapidity of the three-velocity x/t: atanh(|x|/t). + /// + /// Rust: `manifold::spacetime::FourVector::rapidity` + #[pyo3(name = "rapidity")] + #[pyo3(signature = ())] + fn rapidity(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.rapidity()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// + /// Rust: `manifold::spacetime::FourVector::scale` + #[pyo3(name = "scale")] + #[pyo3(signature = (k))] + fn scale(&self, k: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.scale(k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyFourVector { inner: __v }) + } + + /// Proper time along the straight worldline from this event to `o` + /// (zero if the separation is not timelike). + /// + /// Rust: `manifold::spacetime::FourVector::proper_time_to` + #[pyo3(name = "proper_time_to")] + #[pyo3(signature = (o))] + fn proper_time_to(&self, o: crate::generated::types::PyFourVector) -> PyResult { + let o = o.inner; + let __r = crate::runtime::guard(|| self.inner.proper_time_to(&o)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + fn __add__(&self, o: crate::generated::types::PyFourVector) -> PyResult { + let o = o.inner; + let __r = crate::runtime::guard(|| ::add(self.inner.clone(), o)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyFourVector { inner: __v }) + } + + fn __sub__(&self, o: crate::generated::types::PyFourVector) -> PyResult { + let o = o.inner; + let __r = crate::runtime::guard(|| ::sub(self.inner.clone(), o)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyFourVector { inner: __v }) + } + + #[getter] + #[pyo3(name = "t")] + fn py_get_t(&self) -> PyResult { Ok(self.inner.t) } + + #[setter] + #[pyo3(name = "t")] + fn py_set_t(&mut self, v: f64) { self.inner.t = v; } + + #[getter] + #[pyo3(name = "x")] + fn py_get_x(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.x.clone() }) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("FourVector", "FourVector", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Carter constants of motion for a timelike Kerr geodesic: energy `e`, +/// axial angular momentum `l`, and Carter constant `q` per unit mass. +/// +/// Rust: `manifold::spacetime::KerrConstants` +#[pyclass(name = "KerrConstants", module = "numeria.manifold.spacetime", from_py_object)] +#[derive(Clone)] +pub struct PyKerrConstants { pub inner: rust_physics_engine::manifold::spacetime::KerrConstants } +#[pymethods] +impl PyKerrConstants { + /// Builds a `KerrConstants` from its fields. + #[new] + #[pyo3(signature = (m, a, e, l, q))] + fn __new__(m: f64, a: f64, e: f64, l: f64, q: f64) -> Self { + + Self { inner: rust_physics_engine::manifold::spacetime::KerrConstants { m: m, a: a, e: e, l: l, q: q } } + } + + /// Radial potential R(r): Sigma^2 (dr/dtau)^2 = R(r). + /// + /// Rust: `manifold::spacetime::KerrConstants::radial_potential` + #[pyo3(name = "radial_potential")] + #[pyo3(signature = (r))] + fn radial_potential(&self, r: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.radial_potential(r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Polar potential Theta(theta): Sigma^2 (dtheta/dtau)^2 = Theta. + /// + /// Rust: `manifold::spacetime::KerrConstants::theta_potential` + #[pyo3(name = "theta_potential")] + #[pyo3(signature = (theta))] + fn theta_potential(&self, theta: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.theta_potential(theta)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "m")] + fn py_get_m(&self) -> PyResult { Ok(self.inner.m) } + + #[setter] + #[pyo3(name = "m")] + fn py_set_m(&mut self, v: f64) { self.inner.m = v; } + + #[getter] + #[pyo3(name = "a")] + fn py_get_a(&self) -> PyResult { Ok(self.inner.a) } + + #[setter] + #[pyo3(name = "a")] + fn py_set_a(&mut self, v: f64) { self.inner.a = v; } + + #[getter] + #[pyo3(name = "e")] + fn py_get_e(&self) -> PyResult { Ok(self.inner.e) } + + #[setter] + #[pyo3(name = "e")] + fn py_set_e(&mut self, v: f64) { self.inner.e = v; } + + #[getter] + #[pyo3(name = "l")] + fn py_get_l(&self) -> PyResult { Ok(self.inner.l) } + + #[setter] + #[pyo3(name = "l")] + fn py_set_l(&mut self, v: f64) { self.inner.l = v; } + + #[getter] + #[pyo3(name = "q")] + fn py_get_q(&self) -> PyResult { Ok(self.inner.q) } + + #[setter] + #[pyo3(name = "q")] + fn py_set_q(&mut self, v: f64) { self.inner.q = v; } + + fn __repr__(&self) -> String { format!("KerrConstants(m={:?}, a={:?}, e={:?}, l={:?}, q={:?})", self.inner.m, self.inner.a, self.inner.e, self.inner.l, self.inner.q) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A `KerrConstants` argument, or anything that can stand in for one. +pub struct PyKerrConstantsArg(pub rust_physics_engine::manifold::spacetime::KerrConstants); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyKerrConstantsArg { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result { + if let Ok(__w) = obj.extract::() { + return Ok(PyKerrConstantsArg(__w.inner)); + } + let __v = crate::runtime::coerce::floats_exact(obj, 5, "KerrConstants")?; + Ok(PyKerrConstantsArg(rust_physics_engine::manifold::spacetime::KerrConstants { m: __v[0], a: __v[1], e: __v[2], l: __v[3], q: __v[4] })) + } +} + + +/// A Lorentz transformation as a 4x4 matrix acting on (t, x, y, z). +/// +/// Rust: `manifold::spacetime::LorentzTransform` +#[pyclass(name = "LorentzTransform", module = "numeria.manifold.spacetime", from_py_object)] +#[derive(Clone)] +pub struct PyLorentzTransform { pub inner: rust_physics_engine::manifold::spacetime::LorentzTransform } +#[pymethods] +impl PyLorentzTransform { + /// + /// Rust: `manifold::spacetime::LorentzTransform::identity` + #[pyo3(name = "identity")] + #[staticmethod] + #[pyo3(signature = ())] + fn identity() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spacetime::LorentzTransform::identity()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLorentzTransform { inner: __v }) + } + + /// Active boost taking a particle at rest to three-velocity `v`. + /// + /// Rust: `manifold::spacetime::LorentzTransform::boost` + #[pyo3(name = "boost")] + #[staticmethod] + #[pyo3(signature = (v))] + fn boost(v: crate::generated::types::PyVec3Arg) -> PyResult { + let v = v.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spacetime::LorentzTransform::boost(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLorentzTransform { inner: __v }) + } + + /// Boost along +x with speed `beta`. + /// + /// Rust: `manifold::spacetime::LorentzTransform::boost_x` + #[pyo3(name = "boost_x")] + #[staticmethod] + #[pyo3(signature = (beta))] + fn boost_x(beta: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spacetime::LorentzTransform::boost_x(beta)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLorentzTransform { inner: __v }) + } + + /// Spatial rotation embedded as a Lorentz transform. + /// + /// Rust: `manifold::spacetime::LorentzTransform::rotation` + #[pyo3(name = "rotation")] + #[staticmethod] + #[pyo3(signature = (r))] + fn rotation(r: crate::generated::types::PySo3) -> PyResult { + let r = r.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spacetime::LorentzTransform::rotation(&r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLorentzTransform { inner: __v }) + } + + /// + /// Rust: `manifold::spacetime::LorentzTransform::compose` + #[pyo3(name = "compose")] + #[pyo3(signature = (o))] + fn compose(&self, o: crate::generated::types::PyLorentzTransform) -> PyResult { + let o = o.inner; + let __r = crate::runtime::guard(|| self.inner.compose(&o)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLorentzTransform { inner: __v }) + } + + /// Inverse via eta Lambda^T eta (exact for Lorentz matrices). + /// + /// Rust: `manifold::spacetime::LorentzTransform::inverse` + #[pyo3(name = "inverse")] + #[pyo3(signature = ())] + fn inverse(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.inverse()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLorentzTransform { inner: __v }) + } + + /// + /// Rust: `manifold::spacetime::LorentzTransform::apply` + #[pyo3(name = "apply")] + #[pyo3(signature = (v))] + fn apply(&self, v: crate::generated::types::PyFourVector) -> PyResult { + let v = v.inner; + let __r = crate::runtime::guard(|| self.inner.apply(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyFourVector { inner: __v }) + } + + /// Check Lambda^T eta Lambda = eta to tolerance `tol`. + /// + /// Rust: `manifold::spacetime::LorentzTransform::is_lorentz` + #[pyo3(name = "is_lorentz")] + #[pyo3(signature = (tol))] + fn is_lorentz(&self, tol: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_lorentz(tol)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Thomas-Wigner rotation of the composition B(v1) B(v2): the residual + /// spatial rotation R with B(v1) B(v2) = B(v1 + v2) R. + /// + /// Rust: `manifold::spacetime::LorentzTransform::thomas_wigner_rotation` + #[pyo3(name = "thomas_wigner_rotation")] + #[staticmethod] + #[pyo3(signature = (v1, v2))] + fn thomas_wigner_rotation(v1: crate::generated::types::PyVec3Arg, v2: crate::generated::types::PyVec3Arg) -> PyResult { + let v1 = v1.0; + let v2 = v2.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spacetime::LorentzTransform::thomas_wigner_rotation(v1, v2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySo3 { inner: __v }) + } + + /// Relativistic velocity addition: the velocity of a particle moving at + /// `v` in a frame that itself moves at `u` (i.e. B(u) applied to the + /// four-velocity of `v`). + /// + /// Rust: `manifold::spacetime::LorentzTransform::velocity_addition` + #[pyo3(name = "velocity_addition")] + #[staticmethod] + #[pyo3(signature = (u, v))] + fn velocity_addition(u: crate::generated::types::PyVec3Arg, v: crate::generated::types::PyVec3Arg) -> PyResult { + let u = u.0; + let v = v.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spacetime::LorentzTransform::velocity_addition(u, v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// The Lorentz transform covered by an SL(2, C) element. + /// + /// Rust: `manifold::spacetime::LorentzTransform::from_sl2c` + #[pyo3(name = "from_sl2c")] + #[staticmethod] + #[pyo3(signature = (m))] + fn from_sl2c(m: crate::generated::types::PySl2C) -> PyResult { + let m = m.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::spacetime::LorentzTransform::from_sl2c(&m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLorentzTransform { inner: __v }) + } + + /// An SL(2, C) element covering this transform (defined up to sign), + /// via the polar split Lambda = B(u) R. + /// + /// Rust: `manifold::spacetime::LorentzTransform::to_sl2c` + #[pyo3(name = "to_sl2c")] + #[pyo3(signature = ())] + fn to_sl2c(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.to_sl2c()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySl2C { inner: __v }) + } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("LorentzTransform", "LorentzTransform", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A hyperplane of events: all x with normal . x = offset (Minkowski dot). +/// +/// Rust: `manifold::spacetime::Plane` +#[pyclass(name = "Plane", module = "numeria.manifold.spacetime", from_py_object)] +#[derive(Clone)] +pub struct PySpacetimePlane { pub inner: rust_physics_engine::manifold::spacetime::Plane } +#[pymethods] +impl PySpacetimePlane { + /// Builds a `Plane` from its fields. + #[new] + #[pyo3(signature = (normal, offset))] + fn __new__(normal: crate::generated::types::PyFourVector, offset: f64) -> Self { + let normal = normal.inner; + Self { inner: rust_physics_engine::manifold::spacetime::Plane { normal: normal, offset: offset } } + } + + #[getter] + #[pyo3(name = "normal")] + fn py_get_normal(&self) -> PyResult { Ok(crate::generated::types::PyFourVector { inner: self.inner.normal.clone() }) } + + #[getter] + #[pyo3(name = "offset")] + fn py_get_offset(&self) -> PyResult { Ok(self.inner.offset) } + + #[setter] + #[pyo3(name = "offset")] + fn py_set_offset(&mut self, v: f64) { self.inner.offset = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Plane", "Plane", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A dense tensor of arbitrary rank, stored row-major (last index fastest). +/// +/// Rust: `manifold::vecn::TensorN` +#[pyclass(name = "TensorN", module = "numeria.manifold.vecn", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyTensorN { pub inner: rust_physics_engine::manifold::vecn::TensorN } +#[pymethods] +impl PyTensorN { + /// Builds a `TensorN` from its fields. + #[new] + #[pyo3(signature = (shape, data))] + fn __new__(shape: Vec, data: Vec) -> Self { + + Self { inner: rust_physics_engine::manifold::vecn::TensorN { shape: shape, data: data } } + } + + /// + /// Rust: `manifold::vecn::TensorN::zeros` + #[pyo3(name = "zeros")] + #[staticmethod] + #[pyo3(signature = (shape))] + fn zeros(shape: Vec) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::vecn::TensorN::zeros(&shape)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTensorN { inner: __v }) + } + + /// + /// Rust: `manifold::vecn::TensorN::ones` + #[pyo3(name = "ones")] + #[staticmethod] + #[pyo3(signature = (shape))] + fn ones(shape: Vec) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::vecn::TensorN::ones(&shape)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTensorN { inner: __v }) + } + + /// Rank-2 identity (Kronecker delta) of dimension n. + /// + /// Rust: `manifold::vecn::TensorN::identity_2` + #[pyo3(name = "identity_2")] + #[staticmethod] + #[pyo3(signature = (n))] + fn identity_2(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::vecn::TensorN::identity_2(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTensorN { inner: __v }) + } + + /// + /// Rust: `manifold::vecn::TensorN::from_fn` + #[pyo3(name = "from_fn")] + #[staticmethod] + #[pyo3(signature = (shape, f))] + fn from_fn(shape: Vec, f: pyo3::Py) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: &[usize]| -> f64 { __cb.call::<_, f64>((__a0.to_vec(),), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::vecn::TensorN::from_fn(&shape, f)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTensorN { inner: __v }) + } + + /// + /// Rust: `manifold::vecn::TensorN::from_matrix` + #[pyo3(name = "from_matrix")] + #[staticmethod] + #[pyo3(signature = (m))] + fn from_matrix(m: crate::generated::types::PyMatrixArg) -> PyResult { + let m = m.0; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::vecn::TensorN::from_matrix(&m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTensorN { inner: __v }) + } + + /// Rank-2 tensors convert back to a matrix. + /// + /// Rust: `manifold::vecn::TensorN::to_matrix` + #[pyo3(name = "to_matrix")] + #[pyo3(signature = ())] + fn to_matrix(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.to_matrix()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyMatrix { inner: __x })) + } + + /// + /// Rust: `manifold::vecn::TensorN::get` + #[pyo3(name = "get")] + #[pyo3(signature = (idx))] + fn get<'py>(&self, py: Python<'py>, idx: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.get(&idx))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// + /// Rust: `manifold::vecn::TensorN::set` + #[pyo3(name = "set")] + #[pyo3(signature = (idx, v))] + fn set<'py>(&mut self, py: Python<'py>, idx: Vec, v: f64) -> PyResult<()> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.set(&idx, v))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// + /// Rust: `manifold::vecn::TensorN::rank` + #[pyo3(name = "rank")] + #[pyo3(signature = ())] + fn rank(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.rank()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// + /// Rust: `manifold::vecn::TensorN::size` + #[pyo3(name = "size")] + #[pyo3(signature = ())] + fn size(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.size()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Trace over indices `i` and `j` (which must have equal extent), + /// producing a tensor of rank two less. A rank-2 trace yields a rank-0 + /// tensor (shape `[]` with a single entry). + /// + /// Rust: `manifold::vecn::TensorN::contract` + #[pyo3(name = "contract")] + #[pyo3(signature = (i, j))] + fn contract(&self, i: usize, j: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.contract(i, j)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTensorN { inner: __v }) + } + + /// Tensor (outer) product: rank adds. + /// + /// Rust: `manifold::vecn::TensorN::tensor_product` + #[pyo3(name = "tensor_product")] + #[pyo3(signature = (other))] + fn tensor_product(&self, other: crate::generated::types::PyTensorN) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.tensor_product(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTensorN { inner: __v }) + } + + /// Generalized matrix multiplication: contract index `i_self` of self + /// with index `j_other` of other. + /// + /// Rust: `manifold::vecn::TensorN::contract_with` + #[pyo3(name = "contract_with")] + #[pyo3(signature = (other, i_self, j_other))] + fn contract_with(&self, other: crate::generated::types::PyTensorN, i_self: usize, j_other: usize) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.contract_with(&other, i_self, j_other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTensorN { inner: __v }) + } + + /// Permute indices: `perm[k]` names which original axis becomes axis k. + /// + /// Rust: `manifold::vecn::TensorN::transpose` + #[pyo3(name = "transpose")] + #[pyo3(signature = (perm))] + fn transpose(&self, perm: Vec) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.transpose(&perm)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTensorN { inner: __v }) + } + + /// Symmetrize over indices i and j. + /// + /// Rust: `manifold::vecn::TensorN::symmetrize` + #[pyo3(name = "symmetrize")] + #[pyo3(signature = (i, j))] + fn symmetrize(&self, i: usize, j: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.symmetrize(i, j)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTensorN { inner: __v }) + } + + /// Antisymmetrize over indices i and j. + /// + /// Rust: `manifold::vecn::TensorN::antisymmetrize` + #[pyo3(name = "antisymmetrize")] + #[pyo3(signature = (i, j))] + fn antisymmetrize(&self, i: usize, j: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.antisymmetrize(i, j)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTensorN { inner: __v }) + } + + /// True when exchange of indices i and j leaves the tensor unchanged + /// within `tol`. + /// + /// Rust: `manifold::vecn::TensorN::is_symmetric` + #[pyo3(name = "is_symmetric")] + #[pyo3(signature = (i, j, tol))] + fn is_symmetric(&self, i: usize, j: usize, tol: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_symmetric(i, j, tol)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Raise index `i` with the inverse metric. + /// + /// Rust: `manifold::vecn::TensorN::raise_index` + #[pyo3(name = "raise_index")] + #[pyo3(signature = (i, metric_inv))] + fn raise_index(&self, i: usize, metric_inv: crate::generated::types::PyMatrixArg) -> PyResult { + let metric_inv = metric_inv.0; + let __r = crate::runtime::guard(|| self.inner.raise_index(i, &metric_inv)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTensorN { inner: __v }) + } + + /// Lower index `i` with the metric. + /// + /// Rust: `manifold::vecn::TensorN::lower_index` + #[pyo3(name = "lower_index")] + #[pyo3(signature = (i, metric))] + fn lower_index(&self, i: usize, metric: crate::generated::types::PyMatrixArg) -> PyResult { + let metric = metric.0; + let __r = crate::runtime::guard(|| self.inner.lower_index(i, &metric)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTensorN { inner: __v }) + } + + /// Fix `axis` to `idx`, dropping that axis. + /// + /// Rust: `manifold::vecn::TensorN::slice` + #[pyo3(name = "slice")] + #[pyo3(signature = (axis, idx))] + fn slice(&self, axis: usize, idx: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.slice(axis, idx)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTensorN { inner: __v }) + } + + /// + /// Rust: `manifold::vecn::TensorN::norm_frobenius` + #[pyo3(name = "norm_frobenius")] + #[pyo3(signature = ())] + fn norm_frobenius(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.norm_frobenius()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// + /// Rust: `manifold::vecn::TensorN::map` + #[pyo3(name = "map")] + #[pyo3(signature = (f))] + fn map(&self, f: pyo3::Py) -> PyResult { + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| self.inner.map(f)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTensorN { inner: __v }) + } + + /// + /// Rust: `manifold::vecn::TensorN::add` + #[pyo3(name = "add")] + #[pyo3(signature = (other))] + fn add(&self, other: crate::generated::types::PyTensorN) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.add(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTensorN { inner: __v }) + } + + /// + /// Rust: `manifold::vecn::TensorN::sub` + #[pyo3(name = "sub")] + #[pyo3(signature = (other))] + fn sub(&self, other: crate::generated::types::PyTensorN) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.sub(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTensorN { inner: __v }) + } + + /// + /// Rust: `manifold::vecn::TensorN::scale` + #[pyo3(name = "scale")] + #[pyo3(signature = (k))] + fn scale(&self, k: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.scale(k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTensorN { inner: __v }) + } + + /// Einstein summation over named indices, e.g. `"ij,jk->ik"`, + /// `"ijk,k->ij"`, `"ii->"`. Each operand's index string length must + /// match its rank; repeated labels are summed. + /// + /// Rust: `manifold::vecn::TensorN::einsum` + #[pyo3(name = "einsum")] + #[staticmethod] + #[pyo3(signature = (spec, tensors))] + fn einsum(spec: String, tensors: Vec) -> PyResult { + let tensors = tensors.into_iter().map(|__e| __e.inner).collect::>(); + let tensors__b: Vec<&rust_physics_engine::manifold::vecn::TensorN> = tensors.iter().map(|__b| &(*__b)).collect(); + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::vecn::TensorN::einsum(&spec, &tensors__b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(crate::generated::types::PyTensorN { inner: __v }) + } + + /// The rank-n Levi-Civita symbol in n dimensions. + /// + /// Rust: `manifold::vecn::TensorN::levi_civita` + #[pyo3(name = "levi_civita")] + #[staticmethod] + #[pyo3(signature = (n))] + fn levi_civita(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::vecn::TensorN::levi_civita(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTensorN { inner: __v }) + } + + /// Kronecker delta as a rank-2 tensor. + /// + /// Rust: `manifold::vecn::TensorN::kronecker` + #[pyo3(name = "kronecker")] + #[staticmethod] + #[pyo3(signature = (n))] + fn kronecker(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::vecn::TensorN::kronecker(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTensorN { inner: __v }) + } + + /// Hodge dual of a fully antisymmetric rank-k tensor with respect to + /// `metric`: (*T)_{j...} = (1/k!) sqrt|g| T^{i...} eps_{i... j...}, with + /// indices raised by the inverse metric. + /// + /// Rust: `manifold::vecn::TensorN::hodge_dual_vector` + #[pyo3(name = "hodge_dual_vector")] + #[pyo3(signature = (metric))] + fn hodge_dual_vector(&self, metric: crate::generated::types::PyMatrixArg) -> PyResult { + let metric = metric.0; + let __r = crate::runtime::guard(|| self.inner.hodge_dual_vector(&metric)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTensorN { inner: __v }) + } + + #[getter] + #[pyo3(name = "shape")] + fn py_get_shape(&self) -> PyResult> { Ok(self.inner.shape.clone()) } + + #[getter] + #[pyo3(name = "data")] + fn py_get_data(&self) -> PyResult> { Ok(self.inner.data.clone()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("TensorN", "TensorN", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A dense n-dimensional vector. +/// +/// Rust: `manifold::vecn::VecN` +#[pyclass(name = "VecN", module = "numeria.manifold.vecn", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyVecN { pub inner: rust_physics_engine::manifold::vecn::VecN } +#[pymethods] +impl PyVecN { + /// Builds a `VecN` from its fields. + #[new] + #[pyo3(signature = (data))] + fn __new__(data: Vec) -> Self { + + Self { inner: rust_physics_engine::manifold::vecn::VecN { data: data } } + } + + /// + /// Rust: `manifold::vecn::VecN::zeros` + #[pyo3(name = "zeros")] + #[staticmethod] + #[pyo3(signature = (n))] + fn zeros(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::vecn::VecN::zeros(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVecN { inner: __v }) + } + + /// + /// Rust: `manifold::vecn::VecN::ones` + #[pyo3(name = "ones")] + #[staticmethod] + #[pyo3(signature = (n))] + fn ones(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::vecn::VecN::ones(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVecN { inner: __v }) + } + + /// The i-th standard basis vector in n dimensions. + /// + /// Rust: `manifold::vecn::VecN::unit` + #[pyo3(name = "unit")] + #[staticmethod] + #[pyo3(signature = (n, i))] + fn unit(n: usize, i: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::vecn::VecN::unit(n, i)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVecN { inner: __v }) + } + + /// + /// Rust: `manifold::vecn::VecN::from` + #[pyo3(name = "from_")] + #[staticmethod] + #[pyo3(signature = (slice))] + fn from(slice: Vec) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::vecn::VecN::from(&slice)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVecN { inner: __v }) + } + + /// + /// Rust: `manifold::vecn::VecN::dim` + #[pyo3(name = "dim")] + #[pyo3(signature = ())] + fn dim(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.dim()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// + /// Rust: `manifold::vecn::VecN::dot` + #[pyo3(name = "dot")] + #[pyo3(signature = (other))] + fn dot(&self, other: crate::generated::types::PyVecNArg) -> PyResult { + let other = other.0; + let __r = crate::runtime::guard(|| self.inner.dot(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// + /// Rust: `manifold::vecn::VecN::norm` + #[pyo3(name = "norm")] + #[pyo3(signature = ())] + fn norm(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.norm()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// + /// Rust: `manifold::vecn::VecN::normalized` + #[pyo3(name = "normalized")] + #[pyo3(signature = ())] + fn normalized(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.normalized()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVecN { inner: __v }) + } + + /// + /// Rust: `manifold::vecn::VecN::add` + #[pyo3(name = "add")] + #[pyo3(signature = (other))] + fn add(&self, other: crate::generated::types::PyVecNArg) -> PyResult { + let other = other.0; + let __r = crate::runtime::guard(|| self.inner.add(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVecN { inner: __v }) + } + + /// + /// Rust: `manifold::vecn::VecN::sub` + #[pyo3(name = "sub")] + #[pyo3(signature = (other))] + fn sub(&self, other: crate::generated::types::PyVecNArg) -> PyResult { + let other = other.0; + let __r = crate::runtime::guard(|| self.inner.sub(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVecN { inner: __v }) + } + + /// + /// Rust: `manifold::vecn::VecN::scale` + #[pyo3(name = "scale")] + #[pyo3(signature = (k))] + fn scale(&self, k: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.scale(k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVecN { inner: __v }) + } + + /// Outer product a b^T as a matrix. + /// + /// Rust: `manifold::vecn::VecN::outer` + #[pyo3(name = "outer")] + #[pyo3(signature = (other))] + fn outer(&self, other: crate::generated::types::PyVecNArg) -> PyResult { + let other = other.0; + let __r = crate::runtime::guard(|| self.inner.outer(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) + } + + /// Projection of self onto `other`. + /// + /// Rust: `manifold::vecn::VecN::project_onto` + #[pyo3(name = "project_onto")] + #[pyo3(signature = (other))] + fn project_onto(&self, other: crate::generated::types::PyVecNArg) -> PyResult { + let other = other.0; + let __r = crate::runtime::guard(|| self.inner.project_onto(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVecN { inner: __v }) + } + + /// + /// Rust: `manifold::vecn::VecN::angle_between` + #[pyo3(name = "angle_between")] + #[pyo3(signature = (other))] + fn angle_between(&self, other: crate::generated::types::PyVecNArg) -> PyResult { + let other = other.0; + let __r = crate::runtime::guard(|| self.inner.angle_between(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// + /// Rust: `manifold::vecn::VecN::lerp` + #[pyo3(name = "lerp")] + #[pyo3(signature = (other, t))] + fn lerp(&self, other: crate::generated::types::PyVecNArg, t: f64) -> PyResult { + let other = other.0; + let __r = crate::runtime::guard(|| self.inner.lerp(&other, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVecN { inner: __v }) + } + + /// Cross product, defined only in three dimensions. + /// + /// Rust: `manifold::vecn::VecN::cross_3d` + #[pyo3(name = "cross_3d")] + #[pyo3(signature = (other))] + fn cross_3d(&self, other: crate::generated::types::PyVecNArg) -> PyResult> { + let other = other.0; + let __r = crate::runtime::guard(|| self.inner.cross_3d(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyVec3 { inner: __x })) + } + + /// + /// Rust: `manifold::vecn::VecN::to_vec3` + #[pyo3(name = "to_vec3")] + #[pyo3(signature = ())] + fn to_vec3(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.to_vec3()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyVec3 { inner: __x })) + } + + /// + /// Rust: `manifold::vecn::VecN::to_vec2` + #[pyo3(name = "to_vec2")] + #[pyo3(signature = ())] + fn to_vec2(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.to_vec2()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyVec2 { inner: __x })) + } + + /// Gram-Schmidt orthonormalization; near-dependent vectors are dropped. + /// + /// Rust: `manifold::vecn::VecN::gram_schmidt` + #[pyo3(name = "gram_schmidt")] + #[staticmethod] + #[pyo3(signature = (vectors))] + fn gram_schmidt(vectors: Vec) -> PyResult> { + let vectors = vectors.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::vecn::VecN::gram_schmidt(&vectors)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVecN { inner: __x }).collect::>()) + } + + /// Uniform random direction on the unit (n-1)-sphere. + /// + /// Rust: `manifold::vecn::VecN::random_unit` + #[pyo3(name = "random_unit")] + #[staticmethod] + #[pyo3(signature = (n, rng))] + fn random_unit(n: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::vecn::VecN::random_unit(n, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVecN { inner: __v }) + } + + /// Standard normal components. + /// + /// Rust: `manifold::vecn::VecN::random_gaussian` + #[pyo3(name = "random_gaussian")] + #[staticmethod] + #[pyo3(signature = (n, rng))] + fn random_gaussian(n: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::manifold::vecn::VecN::random_gaussian(n, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVecN { inner: __v }) + } + + fn __add__(&self, rhs: crate::generated::types::PyVecNArg) -> PyResult { + let rhs = rhs.0; + let __r = crate::runtime::guard(|| ::add(self.inner.clone(), rhs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVecN { inner: __v }) + } + + fn __sub__(&self, rhs: crate::generated::types::PyVecNArg) -> PyResult { + let rhs = rhs.0; + let __r = crate::runtime::guard(|| ::sub(self.inner.clone(), rhs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVecN { inner: __v }) + } + + fn __mul__(&self, k: f64) -> PyResult { + let __r = crate::runtime::guard(|| >::mul(self.inner.clone(), k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVecN { inner: __v }) + } + + fn __neg__(&self) -> PyResult { + let __r = crate::runtime::guard(|| ::neg(self.inner.clone())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVecN { inner: __v }) + } + + #[getter] + #[pyo3(name = "data")] + fn py_get_data(&self) -> PyResult> { Ok(self.inner.data.clone()) } + + fn __len__(&self) -> usize { self.inner.data.len() } + + fn __getitem__(&self, i: isize) -> PyResult { let n = self.inner.data.len() as isize; let j = if i < 0 { i + n } else { i }; if j < 0 || j >= n { return Err(pyo3::exceptions::PyIndexError::new_err("VecN index out of range")); } Ok(self.inner.data[j as usize]) } + + fn __iter__(slf: pyo3::PyRef<'_, Self>) -> PyResult> { let v = slf.inner.data.clone(); Ok(v.into_pyobject(slf.py())?.try_iter()?.unbind().into_any()) } + + /// The components as a plain list. + fn tolist(&self) -> Vec { self.inner.data.clone() } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("VecN", "VecN", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A `VecN` argument, or anything that can stand in for one. +pub struct PyVecNArg(pub rust_physics_engine::manifold::vecn::VecN); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyVecNArg { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result { + if let Ok(__w) = obj.extract::() { + return Ok(PyVecNArg(__w.inner)); + } + let __v: Vec = obj.extract().map_err(|_| pyo3::exceptions::PyTypeError::new_err("VecN expects a sequence of floats"))?; + Ok(PyVecNArg(rust_physics_engine::manifold::vecn::VecN { data: __v })) + } +} + diff --git a/bindings/python/src/generated/types/materials.rs b/bindings/python/src/generated/types/materials.rs new file mode 100644 index 0000000..fe23dab --- /dev/null +++ b/bindings/python/src/generated/types/materials.rs @@ -0,0 +1,574 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; + + +/// Engineering solids: metals, alloys, polymers and ceramics. +/// +/// Density, Young's modulus, yield and tensile strength, Poisson's ratio, +/// thermal conductivity and expansion, and specific heat. Room-temperature +/// values; a specific alloy, temper or grade will differ, sometimes +/// substantially. +/// An engineering material with mechanical and thermal properties. +/// +/// All values use SI units: +/// - `density`: kg/m^3 +/// - `youngs_modulus`, `yield_strength`, `tensile_strength`: Pa +/// - `thermal_conductivity`: W/(m*K) +/// - `specific_heat`: J/(kg*K) +/// - `thermal_expansion`: 1/K +/// - `melting_point`: K +/// +/// Rust: `materials::common::Material` +#[pyclass(name = "Material", module = "numeria.materials.common", from_py_object)] +#[derive(Clone)] +pub struct PyMaterial { pub inner: rust_physics_engine::materials::common::Material } +#[pymethods] +impl PyMaterial { + #[getter] + #[pyo3(name = "name")] + fn py_get_name(&self) -> PyResult { Ok(self.inner.name.to_string()) } + + #[getter] + #[pyo3(name = "density")] + fn py_get_density(&self) -> PyResult { Ok(self.inner.density) } + + #[setter] + #[pyo3(name = "density")] + fn py_set_density(&mut self, v: f64) { self.inner.density = v; } + + #[getter] + #[pyo3(name = "youngs_modulus")] + fn py_get_youngs_modulus(&self) -> PyResult { Ok(self.inner.youngs_modulus) } + + #[setter] + #[pyo3(name = "youngs_modulus")] + fn py_set_youngs_modulus(&mut self, v: f64) { self.inner.youngs_modulus = v; } + + #[getter] + #[pyo3(name = "poisson_ratio")] + fn py_get_poisson_ratio(&self) -> PyResult { Ok(self.inner.poisson_ratio) } + + #[setter] + #[pyo3(name = "poisson_ratio")] + fn py_set_poisson_ratio(&mut self, v: f64) { self.inner.poisson_ratio = v; } + + #[getter] + #[pyo3(name = "yield_strength")] + fn py_get_yield_strength(&self) -> PyResult { Ok(self.inner.yield_strength) } + + #[setter] + #[pyo3(name = "yield_strength")] + fn py_set_yield_strength(&mut self, v: f64) { self.inner.yield_strength = v; } + + #[getter] + #[pyo3(name = "tensile_strength")] + fn py_get_tensile_strength(&self) -> PyResult { Ok(self.inner.tensile_strength) } + + #[setter] + #[pyo3(name = "tensile_strength")] + fn py_set_tensile_strength(&mut self, v: f64) { self.inner.tensile_strength = v; } + + #[getter] + #[pyo3(name = "thermal_conductivity")] + fn py_get_thermal_conductivity(&self) -> PyResult { Ok(self.inner.thermal_conductivity) } + + #[setter] + #[pyo3(name = "thermal_conductivity")] + fn py_set_thermal_conductivity(&mut self, v: f64) { self.inner.thermal_conductivity = v; } + + #[getter] + #[pyo3(name = "specific_heat")] + fn py_get_specific_heat(&self) -> PyResult { Ok(self.inner.specific_heat) } + + #[setter] + #[pyo3(name = "specific_heat")] + fn py_set_specific_heat(&mut self, v: f64) { self.inner.specific_heat = v; } + + #[getter] + #[pyo3(name = "thermal_expansion")] + fn py_get_thermal_expansion(&self) -> PyResult { Ok(self.inner.thermal_expansion) } + + #[setter] + #[pyo3(name = "thermal_expansion")] + fn py_set_thermal_expansion(&mut self, v: f64) { self.inner.thermal_expansion = v; } + + #[getter] + #[pyo3(name = "melting_point")] + fn py_get_melting_point(&self) -> PyResult { Ok(self.inner.melting_point) } + + #[setter] + #[pyo3(name = "melting_point")] + fn py_set_melting_point(&mut self, v: f64) { self.inner.melting_point = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Material", "Material", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A chemical element with its physical and chemical properties. +/// +/// All values use SI units unless otherwise noted: +/// - `density`: kg/m^3 +/// - `melting_point`, `boiling_point`: Kelvin +/// - `specific_heat`: J/(kg*K) +/// - `thermal_conductivity`: W/(m*K) +/// - `ionization_energy`, `electron_affinity`: eV +/// - `atomic_radius`: pm (picometers) +/// +/// Rust: `materials::elements::Element` +#[pyclass(name = "Element", module = "numeria.materials.elements", from_py_object)] +#[derive(Clone)] +pub struct PyElementsElement { pub inner: rust_physics_engine::materials::elements::Element } +#[pymethods] +impl PyElementsElement { + #[getter] + #[pyo3(name = "atomic_number")] + fn py_get_atomic_number(&self) -> PyResult { Ok(self.inner.atomic_number) } + + #[setter] + #[pyo3(name = "atomic_number")] + fn py_set_atomic_number(&mut self, v: u32) { self.inner.atomic_number = v; } + + #[getter] + #[pyo3(name = "symbol")] + fn py_get_symbol(&self) -> PyResult { Ok(self.inner.symbol.to_string()) } + + #[getter] + #[pyo3(name = "name")] + fn py_get_name(&self) -> PyResult { Ok(self.inner.name.to_string()) } + + #[getter] + #[pyo3(name = "atomic_mass")] + fn py_get_atomic_mass(&self) -> PyResult { Ok(self.inner.atomic_mass) } + + #[setter] + #[pyo3(name = "atomic_mass")] + fn py_set_atomic_mass(&mut self, v: f64) { self.inner.atomic_mass = v; } + + #[getter] + #[pyo3(name = "density")] + fn py_get_density(&self) -> PyResult { Ok(self.inner.density) } + + #[setter] + #[pyo3(name = "density")] + fn py_set_density(&mut self, v: f64) { self.inner.density = v; } + + #[getter] + #[pyo3(name = "melting_point")] + fn py_get_melting_point(&self) -> PyResult { Ok(self.inner.melting_point) } + + #[setter] + #[pyo3(name = "melting_point")] + fn py_set_melting_point(&mut self, v: f64) { self.inner.melting_point = v; } + + #[getter] + #[pyo3(name = "boiling_point")] + fn py_get_boiling_point(&self) -> PyResult { Ok(self.inner.boiling_point) } + + #[setter] + #[pyo3(name = "boiling_point")] + fn py_set_boiling_point(&mut self, v: f64) { self.inner.boiling_point = v; } + + #[getter] + #[pyo3(name = "specific_heat")] + fn py_get_specific_heat(&self) -> PyResult { Ok(self.inner.specific_heat) } + + #[setter] + #[pyo3(name = "specific_heat")] + fn py_set_specific_heat(&mut self, v: f64) { self.inner.specific_heat = v; } + + #[getter] + #[pyo3(name = "thermal_conductivity")] + fn py_get_thermal_conductivity(&self) -> PyResult { Ok(self.inner.thermal_conductivity) } + + #[setter] + #[pyo3(name = "thermal_conductivity")] + fn py_set_thermal_conductivity(&mut self, v: f64) { self.inner.thermal_conductivity = v; } + + #[getter] + #[pyo3(name = "electronegativity")] + fn py_get_electronegativity(&self) -> PyResult { Ok(self.inner.electronegativity) } + + #[setter] + #[pyo3(name = "electronegativity")] + fn py_set_electronegativity(&mut self, v: f64) { self.inner.electronegativity = v; } + + #[getter] + #[pyo3(name = "ionization_energy")] + fn py_get_ionization_energy(&self) -> PyResult { Ok(self.inner.ionization_energy) } + + #[setter] + #[pyo3(name = "ionization_energy")] + fn py_set_ionization_energy(&mut self, v: f64) { self.inner.ionization_energy = v; } + + #[getter] + #[pyo3(name = "electron_affinity")] + fn py_get_electron_affinity(&self) -> PyResult { Ok(self.inner.electron_affinity) } + + #[setter] + #[pyo3(name = "electron_affinity")] + fn py_set_electron_affinity(&mut self, v: f64) { self.inner.electron_affinity = v; } + + #[getter] + #[pyo3(name = "atomic_radius")] + fn py_get_atomic_radius(&self) -> PyResult { Ok(self.inner.atomic_radius) } + + #[setter] + #[pyo3(name = "atomic_radius")] + fn py_set_atomic_radius(&mut self, v: f64) { self.inner.atomic_radius = v; } + + #[getter] + #[pyo3(name = "category")] + fn py_get_category(&self) -> PyResult { Ok(crate::generated::types::PyElementCategory::from_rust(&self.inner.category.clone())) } + + #[getter] + #[pyo3(name = "standard_state")] + fn py_get_standard_state(&self) -> PyResult { Ok(crate::generated::types::PyStandardState::from_rust(&self.inner.standard_state.clone())) } + + #[getter] + #[pyo3(name = "electron_configuration")] + fn py_get_electron_configuration(&self) -> PyResult { Ok(self.inner.electron_configuration.to_string()) } + + #[getter] + #[pyo3(name = "oxidation_states")] + fn py_get_oxidation_states(&self) -> PyResult { Ok(self.inner.oxidation_states.to_string()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Element", "Element", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// The 118 chemical elements. +/// +/// Atomic number, symbol, name, atomic mass, density, melting and boiling +/// points, and thermal and electrical conductivity, with lookup by atomic +/// number, symbol or name. +/// +/// Densities are for the standard state at room temperature, so gases are +/// quoted at STP. Where an element has no stable isotope the atomic mass +/// is that of the longest-lived one, and properties that have never been +/// measured are absent rather than guessed. +/// Classification of an element within the periodic table. +/// +/// Rust: `materials::elements::ElementCategory` +#[pyclass(name = "ElementCategory", module = "numeria.materials.elements", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyElementCategory { + Nonmetal, + NobleGas, + AlkaliMetal, + AlkalineEarthMetal, + Metalloid, + Halogen, + TransitionMetal, + PostTransitionMetal, + Lanthanide, + Actinide, +} +impl PyElementCategory { + pub fn to_rust(&self) -> rust_physics_engine::materials::elements::ElementCategory { match self { + Self::Nonmetal => rust_physics_engine::materials::elements::ElementCategory::Nonmetal, + Self::NobleGas => rust_physics_engine::materials::elements::ElementCategory::NobleGas, + Self::AlkaliMetal => rust_physics_engine::materials::elements::ElementCategory::AlkaliMetal, + Self::AlkalineEarthMetal => rust_physics_engine::materials::elements::ElementCategory::AlkalineEarthMetal, + Self::Metalloid => rust_physics_engine::materials::elements::ElementCategory::Metalloid, + Self::Halogen => rust_physics_engine::materials::elements::ElementCategory::Halogen, + Self::TransitionMetal => rust_physics_engine::materials::elements::ElementCategory::TransitionMetal, + Self::PostTransitionMetal => rust_physics_engine::materials::elements::ElementCategory::PostTransitionMetal, + Self::Lanthanide => rust_physics_engine::materials::elements::ElementCategory::Lanthanide, + Self::Actinide => rust_physics_engine::materials::elements::ElementCategory::Actinide, + } } + pub fn from_rust(v: &rust_physics_engine::materials::elements::ElementCategory) -> Self { match v { + rust_physics_engine::materials::elements::ElementCategory::Nonmetal => Self::Nonmetal, + rust_physics_engine::materials::elements::ElementCategory::NobleGas => Self::NobleGas, + rust_physics_engine::materials::elements::ElementCategory::AlkaliMetal => Self::AlkaliMetal, + rust_physics_engine::materials::elements::ElementCategory::AlkalineEarthMetal => Self::AlkalineEarthMetal, + rust_physics_engine::materials::elements::ElementCategory::Metalloid => Self::Metalloid, + rust_physics_engine::materials::elements::ElementCategory::Halogen => Self::Halogen, + rust_physics_engine::materials::elements::ElementCategory::TransitionMetal => Self::TransitionMetal, + rust_physics_engine::materials::elements::ElementCategory::PostTransitionMetal => Self::PostTransitionMetal, + rust_physics_engine::materials::elements::ElementCategory::Lanthanide => Self::Lanthanide, + rust_physics_engine::materials::elements::ElementCategory::Actinide => Self::Actinide, + } } +} +#[pymethods] +impl PyElementCategory { + fn __repr__(&self) -> &'static str { + match self { + Self::Nonmetal => "ElementCategory.Nonmetal", + Self::NobleGas => "ElementCategory.NobleGas", + Self::AlkaliMetal => "ElementCategory.AlkaliMetal", + Self::AlkalineEarthMetal => "ElementCategory.AlkalineEarthMetal", + Self::Metalloid => "ElementCategory.Metalloid", + Self::Halogen => "ElementCategory.Halogen", + Self::TransitionMetal => "ElementCategory.TransitionMetal", + Self::PostTransitionMetal => "ElementCategory.PostTransitionMetal", + Self::Lanthanide => "ElementCategory.Lanthanide", + Self::Actinide => "ElementCategory.Actinide", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// The standard state of an element at room temperature and pressure (STP). +/// +/// Rust: `materials::elements::StandardState` +#[pyclass(name = "StandardState", module = "numeria.materials.elements", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyStandardState { + Solid, + Liquid, + Gas, + Unknown, +} +impl PyStandardState { + pub fn to_rust(&self) -> rust_physics_engine::materials::elements::StandardState { match self { + Self::Solid => rust_physics_engine::materials::elements::StandardState::Solid, + Self::Liquid => rust_physics_engine::materials::elements::StandardState::Liquid, + Self::Gas => rust_physics_engine::materials::elements::StandardState::Gas, + Self::Unknown => rust_physics_engine::materials::elements::StandardState::Unknown, + } } + pub fn from_rust(v: &rust_physics_engine::materials::elements::StandardState) -> Self { match v { + rust_physics_engine::materials::elements::StandardState::Solid => Self::Solid, + rust_physics_engine::materials::elements::StandardState::Liquid => Self::Liquid, + rust_physics_engine::materials::elements::StandardState::Gas => Self::Gas, + rust_physics_engine::materials::elements::StandardState::Unknown => Self::Unknown, + } } +} +#[pymethods] +impl PyStandardState { + fn __repr__(&self) -> &'static str { + match self { + Self::Solid => "StandardState.Solid", + Self::Liquid => "StandardState.Liquid", + Self::Gas => "StandardState.Gas", + Self::Unknown => "StandardState.Unknown", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Common liquids. +/// +/// Density, dynamic and kinematic viscosity, surface tension, speed of +/// sound, specific heat, and boiling and freezing points, at room +/// temperature and one atmosphere. +/// +/// Viscosity is the strongly temperature-dependent one: it can change by a +/// factor of several over a few tens of degrees, so a single figure is +/// only a starting point. +/// A fluid (liquid) with its mechanical and thermal properties at 20 degrees C +/// unless otherwise noted in the entry. +/// +/// All values use SI units: +/// - `density`: kg/m^3 +/// - `dynamic_viscosity`: Pa*s +/// - `kinematic_viscosity`: m^2/s +/// - `surface_tension`: N/m +/// - `specific_heat`: J/(kg*K) +/// - `thermal_conductivity`: W/(m*K) +/// - `boiling_point`, `freezing_point`: K +/// +/// Rust: `materials::fluids::Fluid` +#[pyclass(name = "Fluid", module = "numeria.materials.fluids", from_py_object)] +#[derive(Clone)] +pub struct PyFluid { pub inner: rust_physics_engine::materials::fluids::Fluid } +#[pymethods] +impl PyFluid { + #[getter] + #[pyo3(name = "name")] + fn py_get_name(&self) -> PyResult { Ok(self.inner.name.to_string()) } + + #[getter] + #[pyo3(name = "density")] + fn py_get_density(&self) -> PyResult { Ok(self.inner.density) } + + #[setter] + #[pyo3(name = "density")] + fn py_set_density(&mut self, v: f64) { self.inner.density = v; } + + #[getter] + #[pyo3(name = "dynamic_viscosity")] + fn py_get_dynamic_viscosity(&self) -> PyResult { Ok(self.inner.dynamic_viscosity) } + + #[setter] + #[pyo3(name = "dynamic_viscosity")] + fn py_set_dynamic_viscosity(&mut self, v: f64) { self.inner.dynamic_viscosity = v; } + + #[getter] + #[pyo3(name = "kinematic_viscosity")] + fn py_get_kinematic_viscosity(&self) -> PyResult { Ok(self.inner.kinematic_viscosity) } + + #[setter] + #[pyo3(name = "kinematic_viscosity")] + fn py_set_kinematic_viscosity(&mut self, v: f64) { self.inner.kinematic_viscosity = v; } + + #[getter] + #[pyo3(name = "surface_tension")] + fn py_get_surface_tension(&self) -> PyResult { Ok(self.inner.surface_tension) } + + #[setter] + #[pyo3(name = "surface_tension")] + fn py_set_surface_tension(&mut self, v: f64) { self.inner.surface_tension = v; } + + #[getter] + #[pyo3(name = "specific_heat")] + fn py_get_specific_heat(&self) -> PyResult { Ok(self.inner.specific_heat) } + + #[setter] + #[pyo3(name = "specific_heat")] + fn py_set_specific_heat(&mut self, v: f64) { self.inner.specific_heat = v; } + + #[getter] + #[pyo3(name = "thermal_conductivity")] + fn py_get_thermal_conductivity(&self) -> PyResult { Ok(self.inner.thermal_conductivity) } + + #[setter] + #[pyo3(name = "thermal_conductivity")] + fn py_set_thermal_conductivity(&mut self, v: f64) { self.inner.thermal_conductivity = v; } + + #[getter] + #[pyo3(name = "boiling_point")] + fn py_get_boiling_point(&self) -> PyResult { Ok(self.inner.boiling_point) } + + #[setter] + #[pyo3(name = "boiling_point")] + fn py_set_boiling_point(&mut self, v: f64) { self.inner.boiling_point = v; } + + #[getter] + #[pyo3(name = "freezing_point")] + fn py_get_freezing_point(&self) -> PyResult { Ok(self.inner.freezing_point) } + + #[setter] + #[pyo3(name = "freezing_point")] + fn py_set_freezing_point(&mut self, v: f64) { self.inner.freezing_point = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Fluid", "Fluid", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Common gases. +/// +/// Molar mass, density at STP, specific heat at constant pressure and the +/// specific heat ratio `γ`, thermal conductivity, viscosity and the speed +/// of sound. +/// +/// `γ` is the entry most often needed: it fixes the adiabatic relations +/// and the speed of sound `c = √(γRT/M)`, and it follows the molecular +/// structure -- about 5/3 for a monatomic gas, 7/5 for a diatomic one. +/// A gas with its thermodynamic and transport properties at STP. +/// +/// All values use SI units: +/// - `molar_mass`: kg/mol +/// - `cp`, `cv`: J/(kg*K) +/// - `density_stp`: kg/m^3 +/// - `dynamic_viscosity`: Pa*s +/// - `thermal_conductivity`: W/(m*K) +/// +/// Rust: `materials::gases::Gas` +#[pyclass(name = "Gas", module = "numeria.materials.gases", from_py_object)] +#[derive(Clone)] +pub struct PyGas { pub inner: rust_physics_engine::materials::gases::Gas } +#[pymethods] +impl PyGas { + #[getter] + #[pyo3(name = "name")] + fn py_get_name(&self) -> PyResult { Ok(self.inner.name.to_string()) } + + #[getter] + #[pyo3(name = "formula")] + fn py_get_formula(&self) -> PyResult { Ok(self.inner.formula.to_string()) } + + #[getter] + #[pyo3(name = "molar_mass")] + fn py_get_molar_mass(&self) -> PyResult { Ok(self.inner.molar_mass) } + + #[setter] + #[pyo3(name = "molar_mass")] + fn py_set_molar_mass(&mut self, v: f64) { self.inner.molar_mass = v; } + + #[getter] + #[pyo3(name = "specific_heat_ratio")] + fn py_get_specific_heat_ratio(&self) -> PyResult { Ok(self.inner.specific_heat_ratio) } + + #[setter] + #[pyo3(name = "specific_heat_ratio")] + fn py_set_specific_heat_ratio(&mut self, v: f64) { self.inner.specific_heat_ratio = v; } + + #[getter] + #[pyo3(name = "cp")] + fn py_get_cp(&self) -> PyResult { Ok(self.inner.cp) } + + #[setter] + #[pyo3(name = "cp")] + fn py_set_cp(&mut self, v: f64) { self.inner.cp = v; } + + #[getter] + #[pyo3(name = "cv")] + fn py_get_cv(&self) -> PyResult { Ok(self.inner.cv) } + + #[setter] + #[pyo3(name = "cv")] + fn py_set_cv(&mut self, v: f64) { self.inner.cv = v; } + + #[getter] + #[pyo3(name = "density_stp")] + fn py_get_density_stp(&self) -> PyResult { Ok(self.inner.density_stp) } + + #[setter] + #[pyo3(name = "density_stp")] + fn py_set_density_stp(&mut self, v: f64) { self.inner.density_stp = v; } + + #[getter] + #[pyo3(name = "dynamic_viscosity")] + fn py_get_dynamic_viscosity(&self) -> PyResult { Ok(self.inner.dynamic_viscosity) } + + #[setter] + #[pyo3(name = "dynamic_viscosity")] + fn py_set_dynamic_viscosity(&mut self, v: f64) { self.inner.dynamic_viscosity = v; } + + #[getter] + #[pyo3(name = "thermal_conductivity")] + fn py_get_thermal_conductivity(&self) -> PyResult { Ok(self.inner.thermal_conductivity) } + + #[setter] + #[pyo3(name = "thermal_conductivity")] + fn py_set_thermal_conductivity(&mut self, v: f64) { self.inner.thermal_conductivity = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Gas", "Gas", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} diff --git a/bindings/python/src/generated/types/math.rs b/bindings/python/src/generated/types/math.rs new file mode 100644 index 0000000..8ffd9c3 --- /dev/null +++ b/bindings/python/src/generated/types/math.rs @@ -0,0 +1,462 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; + + +/// 2D vector (fluid grids, planar geometry). +/// +/// Rust: `math::Vec2` +#[pyclass(name = "Vec2", module = "numeria.math", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyVec2 { pub inner: rust_physics_engine::math::Vec2 } +#[pymethods] +impl PyVec2 { + /// Constructs a new 2D vector. + /// + /// Rust: `math::Vec2::new` + #[new] + #[pyo3(signature = (x, y))] + fn __new__(x: f64, y: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::math::Vec2::new(x, y)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) + } + + /// Euclidean length. + /// + /// Rust: `math::Vec2::magnitude` + #[pyo3(name = "magnitude")] + #[pyo3(signature = ())] + fn magnitude(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.magnitude()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Squared length. + /// + /// Rust: `math::Vec2::magnitude_squared` + #[pyo3(name = "magnitude_squared")] + #[pyo3(signature = ())] + fn magnitude_squared(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.magnitude_squared()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Unit vector in this direction (zero vector maps to zero). + /// + /// Rust: `math::Vec2::normalized` + #[pyo3(name = "normalized")] + #[pyo3(signature = ())] + fn normalized(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.normalized()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) + } + + /// Dot product. + /// + /// Rust: `math::Vec2::dot` + #[pyo3(name = "dot")] + #[pyo3(signature = (other))] + fn dot(&self, other: crate::generated::types::PyVec2Arg) -> PyResult { + let other = other.0; + let __r = crate::runtime::guard(|| self.inner.dot(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Scalar (z-component of the 3D) cross product. + /// + /// Rust: `math::Vec2::cross` + #[pyo3(name = "cross")] + #[pyo3(signature = (other))] + fn cross(&self, other: crate::generated::types::PyVec2Arg) -> PyResult { + let other = other.0; + let __r = crate::runtime::guard(|| self.inner.cross(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Counterclockwise perpendicular (-y, x). + /// + /// Rust: `math::Vec2::perp` + #[pyo3(name = "perp")] + #[pyo3(signature = ())] + fn perp(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.perp()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) + } + + /// Linear interpolation toward `other`. + /// + /// Rust: `math::Vec2::lerp` + #[pyo3(name = "lerp")] + #[pyo3(signature = (other, t))] + fn lerp(&self, other: crate::generated::types::PyVec2Arg, t: f64) -> PyResult { + let other = other.0; + let __r = crate::runtime::guard(|| self.inner.lerp(&other, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) + } + + /// Distance to another point. + /// + /// Rust: `math::Vec2::distance_to` + #[pyo3(name = "distance_to")] + #[pyo3(signature = (other))] + fn distance_to(&self, other: crate::generated::types::PyVec2Arg) -> PyResult { + let other = other.0; + let __r = crate::runtime::guard(|| self.inner.distance_to(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Unsigned angle to another vector in [0, pi]. + /// + /// Rust: `math::Vec2::angle_between` + #[pyo3(name = "angle_between")] + #[pyo3(signature = (other))] + fn angle_between(&self, other: crate::generated::types::PyVec2Arg) -> PyResult { + let other = other.0; + let __r = crate::runtime::guard(|| self.inner.angle_between(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Rotation by `angle` radians counter-clockwise about the origin. + /// + /// Rust: `math::Vec2::rotate` + #[pyo3(name = "rotate")] + #[pyo3(signature = (angle))] + fn rotate(&self, angle: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.rotate(angle)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) + } + + /// Embedding into 3-D with z = 0. + /// + /// Rust: `math::Vec2::to_vec3` + #[pyo3(name = "to_vec3")] + #[pyo3(signature = ())] + fn to_vec3(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.to_vec3()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + fn __add__(&self, rhs: crate::generated::types::PyVec2Arg) -> PyResult { + let rhs = rhs.0; + let __r = crate::runtime::guard(|| ::add(self.inner.clone(), rhs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) + } + + fn __sub__(&self, rhs: crate::generated::types::PyVec2Arg) -> PyResult { + let rhs = rhs.0; + let __r = crate::runtime::guard(|| ::sub(self.inner.clone(), rhs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) + } + + fn __mul__(&self, rhs: f64) -> PyResult { + let __r = crate::runtime::guard(|| >::mul(self.inner.clone(), rhs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) + } + + fn __neg__(&self) -> PyResult { + let __r = crate::runtime::guard(|| ::neg(self.inner.clone())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) + } + + #[getter] + #[pyo3(name = "x")] + fn py_get_x(&self) -> PyResult { Ok(self.inner.x) } + + #[setter] + #[pyo3(name = "x")] + fn py_set_x(&mut self, v: f64) { self.inner.x = v; } + + #[getter] + #[pyo3(name = "y")] + fn py_get_y(&self) -> PyResult { Ok(self.inner.y) } + + #[setter] + #[pyo3(name = "y")] + fn py_set_y(&mut self, v: f64) { self.inner.y = v; } + + #[classattr] + #[pyo3(name = "ZERO")] + fn const_zero() -> crate::generated::types::PyVec2 { crate::generated::types::PyVec2 { inner: rust_physics_engine::math::Vec2::ZERO } } + + fn __len__(&self) -> usize { 2 } + + fn __iter__(slf: pyo3::PyRef<'_, Self>) -> PyResult> { let v = vec![slf.inner.x, slf.inner.y]; Ok(v.into_pyobject(slf.py())?.try_iter()?.unbind().into_any()) } + + fn __getitem__(&self, i: isize) -> PyResult { match i { 0 | -2 => Ok(self.inner.x), 1 | -1 => Ok(self.inner.y), _ => Err(pyo3::exceptions::PyIndexError::new_err("Vec2 index out of range")) } } + + /// The components as a plain list. + fn tolist(&self) -> Vec { vec![self.inner.x, self.inner.y] } + + fn __repr__(&self) -> String { format!("Vec2(x={:?}, y={:?})", self.inner.x, self.inner.y) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A `Vec2` argument, or anything that can stand in for one. +pub struct PyVec2Arg(pub rust_physics_engine::math::Vec2); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyVec2Arg { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result { + if let Ok(__w) = obj.extract::() { + return Ok(PyVec2Arg(__w.inner)); + } + let __v = crate::runtime::coerce::floats_exact(obj, 2, "Vec2")?; + Ok(PyVec2Arg(rust_physics_engine::math::Vec2 { x: __v[0], y: __v[1] })) + } +} + + +/// 3D vector used throughout the physics engine. +/// +/// Rust: `math::Vec3` +#[pyclass(name = "Vec3", module = "numeria.math", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyVec3 { pub inner: rust_physics_engine::math::Vec3 } +#[pymethods] +impl PyVec3 { + /// Constructs a new 3D vector from x, y, z components. + /// + /// Rust: `math::Vec3::new` + #[new] + #[pyo3(signature = (x, y, z))] + fn __new__(x: f64, y: f64, z: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::math::Vec3::new(x, y, z)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Computes the Euclidean length of this vector: |v| = sqrt(x² + y² + z²). + /// + /// Rust: `math::Vec3::magnitude` + #[pyo3(name = "magnitude")] + #[pyo3(signature = ())] + fn magnitude(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.magnitude()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Computes the squared length of this vector: x² + y² + z² (avoids a sqrt). + /// + /// Rust: `math::Vec3::magnitude_squared` + #[pyo3(name = "magnitude_squared")] + #[pyo3(signature = ())] + fn magnitude_squared(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.magnitude_squared()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Returns the unit vector in the same direction: v / |v|. Returns ZERO for zero-length vectors. + /// + /// Rust: `math::Vec3::normalized` + #[pyo3(name = "normalized")] + #[pyo3(signature = ())] + fn normalized(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.normalized()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Computes the dot product of two vectors: a · b = ax*bx + ay*by + az*bz. + /// + /// Rust: `math::Vec3::dot` + #[pyo3(name = "dot")] + #[pyo3(signature = (other))] + fn dot(&self, other: crate::generated::types::PyVec3Arg) -> PyResult { + let other = other.0; + let __r = crate::runtime::guard(|| self.inner.dot(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Computes the cross product of two vectors: a × b, yielding a vector perpendicular to both. + /// + /// Rust: `math::Vec3::cross` + #[pyo3(name = "cross")] + #[pyo3(signature = (other))] + fn cross(&self, other: crate::generated::types::PyVec3Arg) -> PyResult { + let other = other.0; + let __r = crate::runtime::guard(|| self.inner.cross(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Computes the Euclidean distance between two points: |self - other|. + /// + /// Rust: `math::Vec3::distance_to` + #[pyo3(name = "distance_to")] + #[pyo3(signature = (other))] + fn distance_to(&self, other: crate::generated::types::PyVec3Arg) -> PyResult { + let other = other.0; + let __r = crate::runtime::guard(|| self.inner.distance_to(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Computes the angle in radians between two vectors: θ = acos((a · b) / (|a| |b|)). + /// + /// Rust: `math::Vec3::angle_between` + #[pyo3(name = "angle_between")] + #[pyo3(signature = (other))] + fn angle_between(&self, other: crate::generated::types::PyVec3Arg) -> PyResult { + let other = other.0; + let __r = crate::runtime::guard(|| self.inner.angle_between(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Linearly interpolates between two vectors: result = self*(1-t) + other*t. + /// + /// Rust: `math::Vec3::lerp` + #[pyo3(name = "lerp")] + #[pyo3(signature = (other, t))] + fn lerp(&self, other: crate::generated::types::PyVec3Arg, t: f64) -> PyResult { + let other = other.0; + let __r = crate::runtime::guard(|| self.inner.lerp(&other, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Projects this vector onto another: proj_b(a) = b * (a · b) / (b · b). + /// + /// Rust: `math::Vec3::project_onto` + #[pyo3(name = "project_onto")] + #[pyo3(signature = (other))] + fn project_onto(&self, other: crate::generated::types::PyVec3Arg) -> PyResult { + let other = other.0; + let __r = crate::runtime::guard(|| self.inner.project_onto(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Reflects this vector about a surface normal: r = v - 2(v · n)n. + /// + /// Rust: `math::Vec3::reflect` + #[pyo3(name = "reflect")] + #[pyo3(signature = (normal))] + fn reflect(&self, normal: crate::generated::types::PyVec3Arg) -> PyResult { + let normal = normal.0; + let __r = crate::runtime::guard(|| self.inner.reflect(&normal)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + fn __add__(&self, rhs: crate::generated::types::PyVec3Arg) -> PyResult { + let rhs = rhs.0; + let __r = crate::runtime::guard(|| ::add(self.inner.clone(), rhs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + fn __sub__(&self, rhs: crate::generated::types::PyVec3Arg) -> PyResult { + let rhs = rhs.0; + let __r = crate::runtime::guard(|| ::sub(self.inner.clone(), rhs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + fn __mul__(&self, rhs: f64) -> PyResult { + let __r = crate::runtime::guard(|| >::mul(self.inner.clone(), rhs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + fn __neg__(&self) -> PyResult { + let __r = crate::runtime::guard(|| ::neg(self.inner.clone())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + #[getter] + #[pyo3(name = "x")] + fn py_get_x(&self) -> PyResult { Ok(self.inner.x) } + + #[setter] + #[pyo3(name = "x")] + fn py_set_x(&mut self, v: f64) { self.inner.x = v; } + + #[getter] + #[pyo3(name = "y")] + fn py_get_y(&self) -> PyResult { Ok(self.inner.y) } + + #[setter] + #[pyo3(name = "y")] + fn py_set_y(&mut self, v: f64) { self.inner.y = v; } + + #[getter] + #[pyo3(name = "z")] + fn py_get_z(&self) -> PyResult { Ok(self.inner.z) } + + #[setter] + #[pyo3(name = "z")] + fn py_set_z(&mut self, v: f64) { self.inner.z = v; } + + #[classattr] + #[pyo3(name = "ZERO")] + fn const_zero() -> crate::generated::types::PyVec3 { crate::generated::types::PyVec3 { inner: rust_physics_engine::math::Vec3::ZERO } } + + fn __len__(&self) -> usize { 3 } + + fn __iter__(slf: pyo3::PyRef<'_, Self>) -> PyResult> { let v = vec![slf.inner.x, slf.inner.y, slf.inner.z]; Ok(v.into_pyobject(slf.py())?.try_iter()?.unbind().into_any()) } + + fn __getitem__(&self, i: isize) -> PyResult { match i { 0 | -3 => Ok(self.inner.x), 1 | -2 => Ok(self.inner.y), 2 | -1 => Ok(self.inner.z), _ => Err(pyo3::exceptions::PyIndexError::new_err("Vec3 index out of range")) } } + + /// The components as a plain list. + fn tolist(&self) -> Vec { vec![self.inner.x, self.inner.y, self.inner.z] } + + fn __repr__(&self) -> String { format!("Vec3(x={:?}, y={:?}, z={:?})", self.inner.x, self.inner.y, self.inner.z) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A `Vec3` argument, or anything that can stand in for one. +pub struct PyVec3Arg(pub rust_physics_engine::math::Vec3); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyVec3Arg { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result { + if let Ok(__w) = obj.extract::() { + return Ok(PyVec3Arg(__w.inner)); + } + let __v = crate::runtime::coerce::floats_exact(obj, 3, "Vec3")?; + Ok(PyVec3Arg(rust_physics_engine::math::Vec3 { x: __v[0], y: __v[1], z: __v[2] })) + } +} + diff --git a/bindings/python/src/generated/types/mesh.rs b/bindings/python/src/generated/types/mesh.rs new file mode 100644 index 0000000..88c272d --- /dev/null +++ b/bindings/python/src/generated/types/mesh.rs @@ -0,0 +1,1376 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; + + +/// Indexed triangle mesh with optional per-vertex normals and UVs. +/// +/// `normals` and `uvs`, when present, are parallel to `vertices`. +/// +/// Rust: `mesh::Mesh` +#[pyclass(name = "Mesh", module = "numeria.mesh", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyMeshMesh { pub inner: rust_physics_engine::mesh::Mesh } +#[pymethods] +impl PyMeshMesh { + /// Builds a mesh, validating that every index is in range. + /// + /// Errors: + /// Returns `GeomError::InvalidArgument` when a face references a + /// vertex index `>= vertices.len()`. + /// + /// Rust: `mesh::Mesh::new` + #[new] + #[pyo3(signature = (vertices, indices))] + fn __new__(vertices: Vec, indices: Vec>) -> PyResult { + let vertices = vertices.into_iter().map(|__e| __e.0).collect::>(); + let indices = indices.into_iter().map(|__e| -> PyResult<[usize; 3]> { Ok(<[usize; 3]>::try_from(__e).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 3 values, got {}", __v.len())))?) }).collect::>>()?; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::Mesh::new(vertices, indices)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyMeshMesh { inner: __v }) + } + + /// The i-th face as a `Triangle`. + /// + /// Panics: + /// Panics when `i >= self.indices.len()`. + /// + /// Rust: `mesh::Mesh::triangle` + #[pyo3(name = "triangle")] + #[pyo3(signature = (i))] + fn triangle(&self, i: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.triangle(i)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTriangle { inner: __v }) + } + + /// Iterator over all faces as triangles. + /// + /// Rust: `mesh::Mesh::triangles` + #[pyo3(name = "triangles")] + #[pyo3(signature = ())] + fn triangles(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.triangles()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.collect::>().into_iter().map(|__x| crate::generated::types::PyTriangle { inner: __x }).collect::>()) + } + + /// All faces collected as triangles. + /// + /// Rust: `mesh::Mesh::to_triangles` + #[pyo3(name = "to_triangles")] + #[pyo3(signature = ())] + fn to_triangles(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.to_triangles()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyTriangle { inner: __x }).collect::>()) + } + + /// Unit normal of every face (zero vector for degenerate faces). + /// + /// Rust: `mesh::Mesh::face_normals` + #[pyo3(name = "face_normals")] + #[pyo3(signature = ())] + fn face_normals(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.face_normals()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) + } + + /// Computes area-weighted per-vertex normals and stores them in + /// `self.normals`. + /// + /// Each face contributes its (unnormalized) cross product, whose + /// magnitude is twice the face area, so large faces dominate. + /// + /// Rust: `mesh::Mesh::compute_vertex_normals` + #[pyo3(name = "compute_vertex_normals")] + #[pyo3(signature = ())] + fn compute_vertex_normals(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.compute_vertex_normals()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Total surface area (sum of face areas). + /// + /// Rust: `mesh::Mesh::surface_area` + #[pyo3(name = "surface_area")] + #[pyo3(signature = ())] + fn surface_area(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.surface_area()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Signed enclosed volume via the divergence theorem: + /// V = Σ aᵢ · (bᵢ × cᵢ) / 6. Positive for a closed mesh with + /// outward-facing (counterclockwise) triangles. + /// + /// Rust: `mesh::Mesh::volume` + #[pyo3(name = "volume")] + #[pyo3(signature = ())] + fn volume(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.volume()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Centroid of the enclosed volume (center of mass at uniform + /// density), from the signed tetrahedron decomposition against the + /// origin. + /// + /// Panics: + /// Panics when the signed volume is zero. + /// + /// Rust: `mesh::Mesh::centroid` + #[pyo3(name = "centroid")] + #[pyo3(signature = ())] + fn centroid(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.centroid()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Area-weighted centroid of the surface (center of mass of a thin + /// shell of uniform surface density). + /// + /// Panics: + /// Panics when the total surface area is zero. + /// + /// Rust: `mesh::Mesh::center_of_mass_surface` + #[pyo3(name = "center_of_mass_surface")] + #[pyo3(signature = ())] + fn center_of_mass_surface(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.center_of_mass_surface()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Inertia tensor about the center of mass of the enclosed solid at + /// the given uniform density, by signed tetrahedron decomposition + /// (equivalent to Mirtich's polyhedral mass-property integrals). + /// + /// Each face forms the tet (0, a, b, c); its second-moment + /// (covariance) integral is det(J) · J C J^T where J = [a b c] and + /// C is the canonical tetrahedron covariance (1/60 diagonal, 1/120 + /// off-diagonal). Source: Mirtich, "Fast and Accurate Computation + /// of Polyhedral Mass Properties", JGT 1996. + /// + /// Panics: + /// Panics when the signed volume is zero. + /// + /// Rust: `mesh::Mesh::inertia_tensor` + #[pyo3(name = "inertia_tensor")] + #[pyo3(signature = (density))] + fn inertia_tensor(&self, density: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.inertia_tensor(density)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMat3 { inner: __v }) + } + + /// Principal moments of inertia (descending) and the rotation whose + /// columns are the principal axes. + /// + /// Panics: + /// Panics when the signed volume is zero. + /// + /// Rust: `mesh::Mesh::principal_inertia` + #[pyo3(name = "principal_inertia")] + #[pyo3(signature = (density))] + fn principal_inertia(&self, density: f64) -> PyResult<(Vec, crate::generated::types::PyMat3)> { + let __r = crate::runtime::guard(|| self.inner.principal_inertia(density)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0.to_vec(), crate::generated::types::PyMat3 { inner: __v.1 })) + } + + /// Axis-aligned bounding box of all vertices. + /// + /// Panics: + /// Panics when the mesh has no vertices. + /// + /// Rust: `mesh::Mesh::bounding_box` + #[pyo3(name = "bounding_box")] + #[pyo3(signature = ())] + fn bounding_box(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.bounding_box()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAabb { inner: __v }) + } + + /// Approximate minimal bounding sphere by Ritter's two-pass + /// algorithm (at most ~5% larger than optimal). + /// + /// Panics: + /// Panics when the mesh has no vertices. + /// + /// Rust: `mesh::Mesh::bounding_sphere` + #[pyo3(name = "bounding_sphere")] + #[pyo3(signature = ())] + fn bounding_sphere(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.bounding_sphere()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySphere { inner: __v }) + } + + /// Translates every vertex by `offset`. + /// + /// Rust: `mesh::Mesh::translate` + #[pyo3(name = "translate")] + #[pyo3(signature = (offset))] + fn translate(&mut self, offset: crate::generated::types::PyVec3Arg) -> PyResult<()> { + let offset = offset.0; + let __r = crate::runtime::guard(|| self.inner.translate(offset)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Uniformly scales every vertex about the origin. + /// + /// Rust: `mesh::Mesh::scale` + #[pyo3(name = "scale")] + #[pyo3(signature = (factor))] + fn scale(&mut self, factor: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.scale(factor)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Rotates vertices (and normals) about the origin. + /// + /// Rust: `mesh::Mesh::rotate` + #[pyo3(name = "rotate")] + #[pyo3(signature = (q))] + fn rotate(&mut self, q: crate::generated::types::PyQuaternionArg) -> PyResult<()> { + let q = q.0; + let __r = crate::runtime::guard(|| self.inner.rotate(&q)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Appends another mesh. Optional attributes are kept only when + /// both meshes carry them. + /// + /// Rust: `mesh::Mesh::merge` + #[pyo3(name = "merge")] + #[pyo3(signature = (other))] + fn merge(&mut self, other: crate::generated::types::PyMeshMesh) -> PyResult<()> { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.merge(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Reverses the winding of every face and negates stored normals. + /// + /// Rust: `mesh::Mesh::flip_normals` + #[pyo3(name = "flip_normals")] + #[pyo3(signature = ())] + fn flip_normals(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.flip_normals()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Merges vertices closer than `tol` (grid hashing with neighbor + /// search, so any pair within `tol` of a common representative + /// merges). Faces left with a repeated index are removed; stored + /// normals and UVs are dropped. Returns the number of vertices + /// removed. + /// + /// Panics: + /// Panics unless `tol > 0` and finite. + /// + /// Rust: `mesh::Mesh::weld_vertices` + #[pyo3(name = "weld_vertices")] + #[pyo3(signature = (tol))] + fn weld_vertices(&mut self, tol: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.weld_vertices(tol)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Removes vertices referenced by no face, compacting attributes. + /// + /// Rust: `mesh::Mesh::remove_unused_vertices` + #[pyo3(name = "remove_unused_vertices")] + #[pyo3(signature = ())] + fn remove_unused_vertices(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.remove_unused_vertices()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Removes faces with area below `area_tol` or with repeated + /// indices; returns how many were removed. + /// + /// Rust: `mesh::Mesh::remove_degenerate_triangles` + #[pyo3(name = "remove_degenerate_triangles")] + #[pyo3(signature = (area_tol))] + fn remove_degenerate_triangles(&mut self, area_tol: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.remove_degenerate_triangles(area_tol)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Unique undirected edges as sorted `(min, max)` index pairs, + /// lexicographically ordered. + /// + /// Rust: `mesh::Mesh::edges` + #[pyo3(name = "edges")] + #[pyo3(signature = ())] + fn edges<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.edges())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) + } + + /// Vertex-to-neighbor-vertices adjacency (each list sorted, + /// deduplicated). + /// + /// Rust: `mesh::Mesh::adjacency` + #[pyo3(name = "adjacency")] + #[pyo3(signature = ())] + fn adjacency<'py>(&self, py: Python<'py>) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.adjacency())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// For each face, the neighboring face across each of its edges + /// `(v0,v1), (v1,v2), (v2,v0)`, or `None` on a boundary. When an + /// edge is shared by more than two faces, an arbitrary neighbor is + /// reported. + /// + /// Rust: `mesh::Mesh::face_adjacency` + #[pyo3(name = "face_adjacency")] + #[pyo3(signature = ())] + fn face_adjacency<'py>(&self, py: Python<'py>) -> PyResult>>> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.face_adjacency())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.into_iter().map(|__x| __x.map(|__x| __x)).collect::>()).collect::>()) + } + + /// Builds a BVH over the faces (indices refer to face order). + /// + /// Panics: + /// Panics when the mesh has no faces. + /// + /// Rust: `mesh::Mesh::build_bvh` + #[pyo3(name = "build_bvh")] + #[pyo3(signature = ())] + fn build_bvh(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.build_bvh()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBvh { inner: __v }) + } + + /// Nearest ray hit as `(face index, hit)`. Pass a BVH built by + /// `Mesh::build_bvh` to accelerate; `None` falls back to brute + /// force. + /// + /// Rust: `mesh::Mesh::raycast` + #[pyo3(name = "raycast")] + #[pyo3(signature = (r, bvh=None))] + fn raycast(&self, r: crate::generated::types::PyRay, bvh: Option) -> PyResult> { + let r = r.inner; + let bvh = bvh.map(|__o| __o.inner); + let __r = crate::runtime::guard(|| self.inner.raycast(&r, bvh.as_ref().map(|__o| __o))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| (__x.0, crate::generated::types::PyIntersectRayHit { inner: __x.1 }))) + } + + /// Draws `n` points uniformly over the surface: faces are chosen + /// with probability proportional to area, positions by the + /// square-root barycentric warp. + /// + /// Panics: + /// Panics when the total surface area is zero. + /// + /// Rust: `mesh::Mesh::sample_surface` + #[pyo3(name = "sample_surface")] + #[pyo3(signature = (n, rng))] + fn sample_surface(&self, n: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| self.inner.sample_surface(n, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) + } + + /// Serializes to Wavefront OBJ (1-indexed; `vn`/`vt` written when + /// present, referenced with the same index as the position). + /// + /// Rust: `mesh::Mesh::to_obj` + #[pyo3(name = "to_obj")] + #[pyo3(signature = ())] + fn to_obj(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.to_obj()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_string()) + } + + /// Parses Wavefront OBJ. Faces with more than three corners are + /// fan-triangulated. Normals and UVs are kept only when every face + /// corner references the attribute with the same index as its + /// position and the counts match; otherwise they are dropped. + /// Negative (relative) indices are resolved against the counts seen + /// so far. + /// + /// Errors: + /// Returns `GeomError::InvalidArgument` on malformed numbers or + /// out-of-range indices. + /// + /// Rust: `mesh::Mesh::from_obj` + #[pyo3(name = "from_obj")] + #[staticmethod] + #[pyo3(signature = (s))] + fn from_obj(s: String) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::Mesh::from_obj(&s)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyMeshMesh { inner: __v }) + } + + /// Serializes to ASCII STL (facet normals recomputed from + /// geometry). + /// + /// Rust: `mesh::Mesh::to_stl_ascii` + #[pyo3(name = "to_stl_ascii")] + #[pyo3(signature = ())] + fn to_stl_ascii(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.to_stl_ascii()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_string()) + } + + #[getter] + #[pyo3(name = "vertices")] + fn py_get_vertices(&self) -> PyResult> { Ok(self.inner.vertices.clone().into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) } + + #[getter] + #[pyo3(name = "indices")] + fn py_get_indices(&self) -> PyResult>> { Ok(self.inner.indices.clone().into_iter().map(|__x| __x.to_vec()).collect::>()) } + + #[getter] + #[pyo3(name = "normals")] + fn py_get_normals(&self) -> PyResult>> { Ok(self.inner.normals.clone().map(|__x| __x.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>())) } + + #[getter] + #[pyo3(name = "uvs")] + fn py_get_uvs(&self) -> PyResult>> { Ok(self.inner.uvs.clone().map(|__x| __x.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>())) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Mesh", "Mesh", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Summary statistics of a mesh. +/// +/// Rust: `mesh::analyze::MeshStats` +#[pyclass(name = "MeshStats", module = "numeria.mesh.analyze", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyMeshStats { pub inner: rust_physics_engine::mesh::analyze::MeshStats } +#[pymethods] +impl PyMeshStats { + /// Builds a `MeshStats` from its fields. + #[new] + #[pyo3(signature = (vertices, edges, faces, euler, genus, boundary_loops, is_manifold, is_closed, is_oriented, min_angle_deg, max_angle_deg, min_edge, max_edge))] + fn __new__(vertices: usize, edges: usize, faces: usize, euler: i64, genus: Option, boundary_loops: usize, is_manifold: bool, is_closed: bool, is_oriented: bool, min_angle_deg: f64, max_angle_deg: f64, min_edge: f64, max_edge: f64) -> Self { + + Self { inner: rust_physics_engine::mesh::analyze::MeshStats { vertices: vertices, edges: edges, faces: faces, euler: euler, genus: genus, boundary_loops: boundary_loops, is_manifold: is_manifold, is_closed: is_closed, is_oriented: is_oriented, min_angle_deg: min_angle_deg, max_angle_deg: max_angle_deg, min_edge: min_edge, max_edge: max_edge } } + } + + #[getter] + #[pyo3(name = "vertices")] + fn py_get_vertices(&self) -> PyResult { Ok(self.inner.vertices) } + + #[setter] + #[pyo3(name = "vertices")] + fn py_set_vertices(&mut self, v: usize) { self.inner.vertices = v; } + + #[getter] + #[pyo3(name = "edges")] + fn py_get_edges(&self) -> PyResult { Ok(self.inner.edges) } + + #[setter] + #[pyo3(name = "edges")] + fn py_set_edges(&mut self, v: usize) { self.inner.edges = v; } + + #[getter] + #[pyo3(name = "faces")] + fn py_get_faces(&self) -> PyResult { Ok(self.inner.faces) } + + #[setter] + #[pyo3(name = "faces")] + fn py_set_faces(&mut self, v: usize) { self.inner.faces = v; } + + #[getter] + #[pyo3(name = "euler")] + fn py_get_euler(&self) -> PyResult { Ok(self.inner.euler) } + + #[setter] + #[pyo3(name = "euler")] + fn py_set_euler(&mut self, v: i64) { self.inner.euler = v; } + + #[getter] + #[pyo3(name = "genus")] + fn py_get_genus(&self) -> PyResult> { Ok(self.inner.genus.clone().map(|__x| __x)) } + + #[getter] + #[pyo3(name = "boundary_loops")] + fn py_get_boundary_loops(&self) -> PyResult { Ok(self.inner.boundary_loops) } + + #[setter] + #[pyo3(name = "boundary_loops")] + fn py_set_boundary_loops(&mut self, v: usize) { self.inner.boundary_loops = v; } + + #[getter] + #[pyo3(name = "is_manifold")] + fn py_get_is_manifold(&self) -> PyResult { Ok(self.inner.is_manifold) } + + #[setter] + #[pyo3(name = "is_manifold")] + fn py_set_is_manifold(&mut self, v: bool) { self.inner.is_manifold = v; } + + #[getter] + #[pyo3(name = "is_closed")] + fn py_get_is_closed(&self) -> PyResult { Ok(self.inner.is_closed) } + + #[setter] + #[pyo3(name = "is_closed")] + fn py_set_is_closed(&mut self, v: bool) { self.inner.is_closed = v; } + + #[getter] + #[pyo3(name = "is_oriented")] + fn py_get_is_oriented(&self) -> PyResult { Ok(self.inner.is_oriented) } + + #[setter] + #[pyo3(name = "is_oriented")] + fn py_set_is_oriented(&mut self, v: bool) { self.inner.is_oriented = v; } + + #[getter] + #[pyo3(name = "min_angle_deg")] + fn py_get_min_angle_deg(&self) -> PyResult { Ok(self.inner.min_angle_deg) } + + #[setter] + #[pyo3(name = "min_angle_deg")] + fn py_set_min_angle_deg(&mut self, v: f64) { self.inner.min_angle_deg = v; } + + #[getter] + #[pyo3(name = "max_angle_deg")] + fn py_get_max_angle_deg(&self) -> PyResult { Ok(self.inner.max_angle_deg) } + + #[setter] + #[pyo3(name = "max_angle_deg")] + fn py_set_max_angle_deg(&mut self, v: f64) { self.inner.max_angle_deg = v; } + + #[getter] + #[pyo3(name = "min_edge")] + fn py_get_min_edge(&self) -> PyResult { Ok(self.inner.min_edge) } + + #[setter] + #[pyo3(name = "min_edge")] + fn py_set_min_edge(&mut self, v: f64) { self.inner.min_edge = v; } + + #[getter] + #[pyo3(name = "max_edge")] + fn py_get_max_edge(&self) -> PyResult { Ok(self.inner.max_edge) } + + #[setter] + #[pyo3(name = "max_edge")] + fn py_set_max_edge(&mut self, v: f64) { self.inner.max_edge = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("MeshStats", "MeshStats", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Scalar samples on a regular 2-D grid (`width` x `height` samples, +/// x-fastest layout: `data[j * width + i]`). +/// +/// Rust: `mesh::isosurface::ScalarField2` +#[pyclass(name = "ScalarField2", module = "numeria.mesh.isosurface", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyIsosurfaceScalarField2 { pub inner: rust_physics_engine::mesh::isosurface::ScalarField2 } +#[pymethods] +impl PyIsosurfaceScalarField2 { + /// Builds a `ScalarField2` from its fields. + #[new] + #[pyo3(signature = (width, height, data, bounds))] + fn __new__(width: usize, height: usize, data: Vec, bounds: crate::generated::types::PyRect) -> Self { + let bounds = bounds.inner; + Self { inner: rust_physics_engine::mesh::isosurface::ScalarField2 { width: width, height: height, data: data, bounds: bounds } } + } + + /// Samples `f` on a `width` x `height` grid spanning `bounds`. + /// + /// Panics: + /// Panics unless `width >= 2` and `height >= 2`. + /// + /// Rust: `mesh::isosurface::ScalarField2::from_fn` + #[pyo3(name = "from_fn")] + #[staticmethod] + #[pyo3(signature = (bounds, width, height, f))] + fn from_fn(bounds: crate::generated::types::PyRect, width: usize, height: usize, f: pyo3::Py) -> PyResult { + let bounds = bounds.inner; + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: rust_physics_engine::math::Vec2| -> f64 { __cb.call::<_, f64>((crate::generated::types::PyVec2 { inner: __a0 },), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::isosurface::ScalarField2::from_fn(bounds, width, height, &f)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyIsosurfaceScalarField2 { inner: __v }) + } + + /// Sample value at grid coordinates. + /// + /// Panics: + /// Panics when out of range. + /// + /// Rust: `mesh::isosurface::ScalarField2::get` + #[pyo3(name = "get")] + #[pyo3(signature = (i, j))] + fn get(&self, i: usize, j: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.get(i, j)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// World position of the sample at grid coordinates. + /// + /// Panics: + /// Panics when out of range. + /// + /// Rust: `mesh::isosurface::ScalarField2::position` + #[pyo3(name = "position")] + #[pyo3(signature = (i, j))] + fn position(&self, i: usize, j: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.position(i, j)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) + } + + #[getter] + #[pyo3(name = "width")] + fn py_get_width(&self) -> PyResult { Ok(self.inner.width) } + + #[setter] + #[pyo3(name = "width")] + fn py_set_width(&mut self, v: usize) { self.inner.width = v; } + + #[getter] + #[pyo3(name = "height")] + fn py_get_height(&self) -> PyResult { Ok(self.inner.height) } + + #[setter] + #[pyo3(name = "height")] + fn py_set_height(&mut self, v: usize) { self.inner.height = v; } + + #[getter] + #[pyo3(name = "data")] + fn py_get_data(&self) -> PyResult> { Ok(self.inner.data.clone()) } + + #[getter] + #[pyo3(name = "bounds")] + fn py_get_bounds(&self) -> PyResult { Ok(crate::generated::types::PyRect { inner: self.inner.bounds.clone() }) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("ScalarField2", "ScalarField2", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Scalar samples on a regular 3-D grid (`nx` x `ny` x `nz` samples, +/// x-fastest layout: `data[(k * ny + j) * nx + i]`). +/// +/// Rust: `mesh::isosurface::ScalarField3` +#[pyclass(name = "ScalarField3", module = "numeria.mesh.isosurface", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyIsosurfaceScalarField3 { pub inner: rust_physics_engine::mesh::isosurface::ScalarField3 } +#[pymethods] +impl PyIsosurfaceScalarField3 { + /// Builds a `ScalarField3` from its fields. + #[new] + #[pyo3(signature = (nx, ny, nz, data, bounds))] + fn __new__(nx: usize, ny: usize, nz: usize, data: Vec, bounds: crate::generated::types::PyAabb) -> Self { + let bounds = bounds.inner; + Self { inner: rust_physics_engine::mesh::isosurface::ScalarField3 { nx: nx, ny: ny, nz: nz, data: data, bounds: bounds } } + } + + /// Samples `f` on an `nx` x `ny` x `nz` grid spanning `bounds`. + /// + /// Panics: + /// Panics unless every axis has at least 2 samples. + /// + /// Rust: `mesh::isosurface::ScalarField3::from_fn` + #[pyo3(name = "from_fn")] + #[staticmethod] + #[pyo3(signature = (bounds, nx, ny, nz, f))] + fn from_fn(bounds: crate::generated::types::PyAabb, nx: usize, ny: usize, nz: usize, f: pyo3::Py) -> PyResult { + let bounds = bounds.inner; + let __cb_f = std::rc::Rc::new(crate::runtime::Callback::new(f)); + let f = { let __cb = __cb_f.clone(); move |__a0: rust_physics_engine::math::Vec3| -> f64 { __cb.call::<_, f64>((crate::generated::types::PyVec3 { inner: __a0 },), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::isosurface::ScalarField3::from_fn(bounds, nx, ny, nz, &f)); + crate::runtime::callback::check(&[&__cb_f], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyIsosurfaceScalarField3 { inner: __v }) + } + + /// Samples a signed distance function (alias of `Self::from_fn`, + /// spelled out because SDF sampling is the common case). + /// + /// Rust: `mesh::isosurface::ScalarField3::from_sdf` + #[pyo3(name = "from_sdf")] + #[staticmethod] + #[pyo3(signature = (bounds, nx, ny, nz, sdf))] + fn from_sdf(bounds: crate::generated::types::PyAabb, nx: usize, ny: usize, nz: usize, sdf: pyo3::Py) -> PyResult { + let bounds = bounds.inner; + let __cb_sdf = std::rc::Rc::new(crate::runtime::Callback::new(sdf)); + let sdf = { let __cb = __cb_sdf.clone(); move |__a0: rust_physics_engine::math::Vec3| -> f64 { __cb.call::<_, f64>((crate::generated::types::PyVec3 { inner: __a0 },), f64::NAN) } }; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::isosurface::ScalarField3::from_sdf(bounds, nx, ny, nz, &sdf)); + crate::runtime::callback::check(&[&__cb_sdf], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyIsosurfaceScalarField3 { inner: __v }) + } + + /// Sample value at grid coordinates. + /// + /// Panics: + /// Panics when out of range. + /// + /// Rust: `mesh::isosurface::ScalarField3::get` + #[pyo3(name = "get")] + #[pyo3(signature = (i, j, k))] + fn get(&self, i: usize, j: usize, k: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.get(i, j, k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// World position of the sample at grid coordinates. + /// + /// Panics: + /// Panics when out of range. + /// + /// Rust: `mesh::isosurface::ScalarField3::position` + #[pyo3(name = "position")] + #[pyo3(signature = (i, j, k))] + fn position(&self, i: usize, j: usize, k: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.position(i, j, k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Trilinear interpolation of the field at a world position + /// (clamped to the grid). + /// + /// Rust: `mesh::isosurface::ScalarField3::sample_trilinear` + #[pyo3(name = "sample_trilinear")] + #[pyo3(signature = (p))] + fn sample_trilinear(&self, p: crate::generated::types::PyVec3Arg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.sample_trilinear(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Central-difference gradient at a grid point (one-sided on the + /// boundary), in world units. + /// + /// Panics: + /// Panics when out of range. + /// + /// Rust: `mesh::isosurface::ScalarField3::gradient` + #[pyo3(name = "gradient")] + #[pyo3(signature = (i, j, k))] + fn gradient(&self, i: usize, j: usize, k: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.gradient(i, j, k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + #[getter] + #[pyo3(name = "nx")] + fn py_get_nx(&self) -> PyResult { Ok(self.inner.nx) } + + #[setter] + #[pyo3(name = "nx")] + fn py_set_nx(&mut self, v: usize) { self.inner.nx = v; } + + #[getter] + #[pyo3(name = "ny")] + fn py_get_ny(&self) -> PyResult { Ok(self.inner.ny) } + + #[setter] + #[pyo3(name = "ny")] + fn py_set_ny(&mut self, v: usize) { self.inner.ny = v; } + + #[getter] + #[pyo3(name = "nz")] + fn py_get_nz(&self) -> PyResult { Ok(self.inner.nz) } + + #[setter] + #[pyo3(name = "nz")] + fn py_set_nz(&mut self, v: usize) { self.inner.nz = v; } + + #[getter] + #[pyo3(name = "data")] + fn py_get_data(&self) -> PyResult> { Ok(self.inner.data.clone()) } + + #[getter] + #[pyo3(name = "bounds")] + fn py_get_bounds(&self) -> PyResult { Ok(crate::generated::types::PyAabb { inner: self.inner.bounds.clone() }) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("ScalarField3", "ScalarField3", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Target shape for the fixed boundary of a harmonic +/// parameterization. +/// +/// Rust: `mesh::parameterize::BoundaryShape` +#[pyclass(name = "BoundaryShape", module = "numeria.mesh.parameterize", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyBoundaryShape { + Circle, + Square, + Free, +} +impl PyBoundaryShape { + pub fn to_rust(&self) -> rust_physics_engine::mesh::parameterize::BoundaryShape { match self { + Self::Circle => rust_physics_engine::mesh::parameterize::BoundaryShape::Circle, + Self::Square => rust_physics_engine::mesh::parameterize::BoundaryShape::Square, + Self::Free => rust_physics_engine::mesh::parameterize::BoundaryShape::Free, + } } + pub fn from_rust(v: &rust_physics_engine::mesh::parameterize::BoundaryShape) -> Self { match v { + rust_physics_engine::mesh::parameterize::BoundaryShape::Circle => Self::Circle, + rust_physics_engine::mesh::parameterize::BoundaryShape::Square => Self::Square, + rust_physics_engine::mesh::parameterize::BoundaryShape::Free => Self::Free, + } } +} +#[pymethods] +impl PyBoundaryShape { + fn __repr__(&self) -> &'static str { + match self { + Self::Circle => "BoundaryShape.Circle", + Self::Square => "BoundaryShape.Square", + Self::Free => "BoundaryShape.Free", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Quadrilateral mesh (faces as counterclockwise vertex quadruples). +/// +/// Rust: `mesh::subdivide::QuadMesh` +#[pyclass(name = "QuadMesh", module = "numeria.mesh.subdivide", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyQuadMesh { pub inner: rust_physics_engine::mesh::subdivide::QuadMesh } +#[pymethods] +impl PyQuadMesh { + /// Builds a `QuadMesh` from its fields. + #[new] + #[pyo3(signature = (vertices, quads))] + fn __new__(vertices: Vec, quads: Vec>) -> PyResult { + let vertices = vertices.into_iter().map(|__e| __e.0).collect::>(); + let quads = quads.into_iter().map(|__e| -> PyResult<[usize; 4]> { Ok(<[usize; 4]>::try_from(__e).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 4 values, got {}", __v.len())))?) }).collect::>>()?; + Ok(Self { inner: rust_physics_engine::mesh::subdivide::QuadMesh { vertices: vertices, quads: quads } }) + } + + /// Triangulates each quad along its 0-2 diagonal. + /// + /// Rust: `mesh::subdivide::QuadMesh::to_triangles` + #[pyo3(name = "to_triangles")] + #[pyo3(signature = ())] + fn to_triangles(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.to_triangles()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMeshMesh { inner: __v }) + } + + /// Axis-aligned box of the given half extents: 8 vertices, 6 + /// outward quads. + /// + /// Panics: + /// Panics unless all half extents are positive. + /// + /// Rust: `mesh::subdivide::QuadMesh::from_box` + #[pyo3(name = "from_box")] + #[staticmethod] + #[pyo3(signature = (half))] + fn from_box(half: crate::generated::types::PyVec3Arg) -> PyResult { + let half = half.0; + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::subdivide::QuadMesh::from_box(half)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuadMesh { inner: __v }) + } + + /// Flat grid of `nx` x `nz` quads in the xz plane (normals +y), + /// centered at the origin. + /// + /// Panics: + /// Panics unless `width, depth > 0` and `nx, nz >= 1`. + /// + /// Rust: `mesh::subdivide::QuadMesh::from_grid` + #[pyo3(name = "from_grid")] + #[staticmethod] + #[pyo3(signature = (width, depth, nx, nz))] + fn from_grid(width: f64, depth: f64, nx: usize, nz: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::subdivide::QuadMesh::from_grid(width, depth, nx, nz)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuadMesh { inner: __v }) + } + + #[getter] + #[pyo3(name = "vertices")] + fn py_get_vertices(&self) -> PyResult> { Ok(self.inner.vertices.clone().into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) } + + #[getter] + #[pyo3(name = "quads")] + fn py_get_quads(&self) -> PyResult>> { Ok(self.inner.quads.clone().into_iter().map(|__x| __x.to_vec()).collect::>()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("QuadMesh", "QuadMesh", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Tensor-product B-spline surface. Parameters range over +/// `[knots[degree], knots[len - degree - 1]]` in each direction. +/// +/// Rust: `mesh::surfaces::BSplineSurface` +#[pyclass(name = "BSplineSurface", module = "numeria.mesh.surfaces", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyBSplineSurface { pub inner: rust_physics_engine::mesh::surfaces::BSplineSurface } +#[pymethods] +impl PyBSplineSurface { + /// Builds a `BSplineSurface` from its fields. + #[new] + #[pyo3(signature = (degree_u, degree_v, knots_u, knots_v, control))] + fn __new__(degree_u: usize, degree_v: usize, knots_u: Vec, knots_v: Vec, control: Vec>) -> Self { + let control = control.into_iter().map(|__e| __e.into_iter().map(|__e| __e.0).collect::>()).collect::>(); + Self { inner: rust_physics_engine::mesh::surfaces::BSplineSurface { degree_u: degree_u, degree_v: degree_v, knots_u: knots_u, knots_v: knots_v, control: control } } + } + + /// Clamped surface with uniform interior knots on [0, 1]². + /// + /// Panics: + /// Panics unless the control net is rectangular with more than + /// `degree` points per direction and `degree >= 1`. + /// + /// Rust: `mesh::surfaces::BSplineSurface::uniform` + #[pyo3(name = "uniform")] + #[staticmethod] + #[pyo3(signature = (degree_u, degree_v, control))] + fn uniform(degree_u: usize, degree_v: usize, control: Vec>) -> PyResult { + let control = control.into_iter().map(|__e| __e.into_iter().map(|__e| __e.0).collect::>()).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::surfaces::BSplineSurface::uniform(degree_u, degree_v, control)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBSplineSurface { inner: __v }) + } + + /// Point at `(u, v)` (clamped to the domain). + /// + /// Rust: `mesh::surfaces::BSplineSurface::eval` + #[pyo3(name = "eval")] + #[pyo3(signature = (u, v))] + fn eval(&self, u: f64, v: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.eval(u, v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Unit normal by central differences (steps shrink at the domain + /// boundary). + /// + /// Rust: `mesh::surfaces::BSplineSurface::normal` + #[pyo3(name = "normal")] + #[pyo3(signature = (u, v))] + fn normal(&self, u: f64, v: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.normal(u, v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Tessellates on an `nu` x `nv` cell grid over the whole domain. + /// + /// Rust: `mesh::surfaces::BSplineSurface::to_mesh` + #[pyo3(name = "to_mesh")] + #[pyo3(signature = (nu, nv))] + fn to_mesh(&self, nu: usize, nv: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.to_mesh(nu, nv)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMeshMesh { inner: __v }) + } + + #[getter] + #[pyo3(name = "degree_u")] + fn py_get_degree_u(&self) -> PyResult { Ok(self.inner.degree_u) } + + #[setter] + #[pyo3(name = "degree_u")] + fn py_set_degree_u(&mut self, v: usize) { self.inner.degree_u = v; } + + #[getter] + #[pyo3(name = "degree_v")] + fn py_get_degree_v(&self) -> PyResult { Ok(self.inner.degree_v) } + + #[setter] + #[pyo3(name = "degree_v")] + fn py_set_degree_v(&mut self, v: usize) { self.inner.degree_v = v; } + + #[getter] + #[pyo3(name = "knots_u")] + fn py_get_knots_u(&self) -> PyResult> { Ok(self.inner.knots_u.clone()) } + + #[getter] + #[pyo3(name = "knots_v")] + fn py_get_knots_v(&self) -> PyResult> { Ok(self.inner.knots_v.clone()) } + + #[getter] + #[pyo3(name = "control")] + fn py_get_control(&self) -> PyResult>> { Ok(self.inner.control.clone().into_iter().map(|__x| __x.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()).collect::>()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("BSplineSurface", "BSplineSurface", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Bicubic Bézier patch; `control[i][j]` weights the Bernstein product +/// B_i(u) B_j(v). +/// +/// Rust: `mesh::surfaces::BezierPatch` +#[pyclass(name = "BezierPatch", module = "numeria.mesh.surfaces", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyBezierPatch { pub inner: rust_physics_engine::mesh::surfaces::BezierPatch } +#[pymethods] +impl PyBezierPatch { + /// Point at `(u, v)`, both in [0, 1]. + /// + /// Rust: `mesh::surfaces::BezierPatch::eval` + #[pyo3(name = "eval")] + #[pyo3(signature = (u, v))] + fn eval(&self, u: f64, v: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.eval(u, v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Partial derivative in u. + /// + /// Rust: `mesh::surfaces::BezierPatch::du` + #[pyo3(name = "du")] + #[pyo3(signature = (u, v))] + fn du(&self, u: f64, v: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.du(u, v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Partial derivative in v. + /// + /// Rust: `mesh::surfaces::BezierPatch::dv` + #[pyo3(name = "dv")] + #[pyo3(signature = (u, v))] + fn dv(&self, u: f64, v: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.dv(u, v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Unit normal `du × dv` (zero where the patch is degenerate). + /// + /// Rust: `mesh::surfaces::BezierPatch::normal` + #[pyo3(name = "normal")] + #[pyo3(signature = (u, v))] + fn normal(&self, u: f64, v: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.normal(u, v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Tessellates on an `nu` x `nv` cell grid. + /// + /// Rust: `mesh::surfaces::BezierPatch::to_mesh` + #[pyo3(name = "to_mesh")] + #[pyo3(signature = (nu, nv))] + fn to_mesh(&self, nu: usize, nv: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.to_mesh(nu, nv)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMeshMesh { inner: __v }) + } + + /// Splits into four subpatches at the parametric midpoint, ordered + /// `[u-low v-low, u-high v-low, u-low v-high, u-high v-high]`. + /// Their union reproduces the original surface exactly. + /// + /// Rust: `mesh::surfaces::BezierPatch::subdivide` + #[pyo3(name = "subdivide")] + #[pyo3(signature = ())] + fn subdivide(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.subdivide()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyBezierPatch { inner: __x }).collect::>()) + } + + #[getter] + #[pyo3(name = "control")] + fn py_get_control(&self) -> PyResult>> { Ok(self.inner.control.clone().into_iter().map(|__x| __x.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()).collect::>()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("BezierPatch", "BezierPatch", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// First (E, F, G) and second (L, M, N) fundamental form coefficients +/// of a parametric surface. +/// +/// Rust: `mesh::surfaces::FundamentalForms` +#[pyclass(name = "FundamentalForms", module = "numeria.mesh.surfaces", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyFundamentalForms { pub inner: rust_physics_engine::mesh::surfaces::FundamentalForms } +#[pymethods] +impl PyFundamentalForms { + /// Builds a `FundamentalForms` from its fields. + #[new] + #[pyo3(signature = (e, f, g, l, m, n))] + fn __new__(e: f64, f: f64, g: f64, l: f64, m: f64, n: f64) -> Self { + + Self { inner: rust_physics_engine::mesh::surfaces::FundamentalForms { e: e, f: f, g: g, l: l, m: m, n: n } } + } + + #[getter] + #[pyo3(name = "e")] + fn py_get_e(&self) -> PyResult { Ok(self.inner.e) } + + #[setter] + #[pyo3(name = "e")] + fn py_set_e(&mut self, v: f64) { self.inner.e = v; } + + #[getter] + #[pyo3(name = "f")] + fn py_get_f(&self) -> PyResult { Ok(self.inner.f) } + + #[setter] + #[pyo3(name = "f")] + fn py_set_f(&mut self, v: f64) { self.inner.f = v; } + + #[getter] + #[pyo3(name = "g")] + fn py_get_g(&self) -> PyResult { Ok(self.inner.g) } + + #[setter] + #[pyo3(name = "g")] + fn py_set_g(&mut self, v: f64) { self.inner.g = v; } + + #[getter] + #[pyo3(name = "l")] + fn py_get_l(&self) -> PyResult { Ok(self.inner.l) } + + #[setter] + #[pyo3(name = "l")] + fn py_set_l(&mut self, v: f64) { self.inner.l = v; } + + #[getter] + #[pyo3(name = "m")] + fn py_get_m(&self) -> PyResult { Ok(self.inner.m) } + + #[setter] + #[pyo3(name = "m")] + fn py_set_m(&mut self, v: f64) { self.inner.m = v; } + + #[getter] + #[pyo3(name = "n")] + fn py_get_n(&self) -> PyResult { Ok(self.inner.n) } + + #[setter] + #[pyo3(name = "n")] + fn py_set_n(&mut self, v: f64) { self.inner.n = v; } + + fn __repr__(&self) -> String { format!("FundamentalForms(e={:?}, f={:?}, g={:?}, l={:?}, m={:?}, n={:?})", self.inner.e, self.inner.f, self.inner.g, self.inner.l, self.inner.m, self.inner.n) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A `FundamentalForms` argument, or anything that can stand in for one. +pub struct PyFundamentalFormsArg(pub rust_physics_engine::mesh::surfaces::FundamentalForms); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyFundamentalFormsArg { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result { + if let Ok(__w) = obj.extract::() { + return Ok(PyFundamentalFormsArg(__w.inner)); + } + let __v = crate::runtime::coerce::floats_exact(obj, 6, "FundamentalForms")?; + Ok(PyFundamentalFormsArg(rust_physics_engine::mesh::surfaces::FundamentalForms { e: __v[0], f: __v[1], g: __v[2], l: __v[3], m: __v[4], n: __v[5] })) + } +} + + +/// Tensor-product NURBS surface (rational B-spline): projective +/// weights allow exact conics. +/// +/// Rust: `mesh::surfaces::NurbsSurface` +#[pyclass(name = "NurbsSurface", module = "numeria.mesh.surfaces", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyNurbsSurface { pub inner: rust_physics_engine::mesh::surfaces::NurbsSurface } +#[pymethods] +impl PyNurbsSurface { + /// Builds a `NurbsSurface` from its fields. + #[new] + #[pyo3(signature = (degree_u, degree_v, knots_u, knots_v, control, weights))] + fn __new__(degree_u: usize, degree_v: usize, knots_u: Vec, knots_v: Vec, control: Vec>, weights: Vec>) -> Self { + let control = control.into_iter().map(|__e| __e.into_iter().map(|__e| __e.0).collect::>()).collect::>(); + Self { inner: rust_physics_engine::mesh::surfaces::NurbsSurface { degree_u: degree_u, degree_v: degree_v, knots_u: knots_u, knots_v: knots_v, control: control, weights: weights } } + } + + /// Point at `(u, v)`: rational combination + /// Σ wᵢⱼ Nᵢ(u) Nⱼ(v) Pᵢⱼ / Σ wᵢⱼ Nᵢ(u) Nⱼ(v). + /// + /// Rust: `mesh::surfaces::NurbsSurface::eval` + #[pyo3(name = "eval")] + #[pyo3(signature = (u, v))] + fn eval(&self, u: f64, v: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.eval(u, v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Tessellates on an `nu` x `nv` cell grid over the whole domain. + /// + /// Rust: `mesh::surfaces::NurbsSurface::to_mesh` + #[pyo3(name = "to_mesh")] + #[pyo3(signature = (nu, nv))] + fn to_mesh(&self, nu: usize, nv: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.to_mesh(nu, nv)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMeshMesh { inner: __v }) + } + + /// Exact sphere of radius `r`: a semicircular profile revolved by + /// the exact NURBS circle. Every evaluated point lies exactly on + /// the sphere. + /// + /// Panics: + /// Panics unless `r > 0`. + /// + /// Rust: `mesh::surfaces::NurbsSurface::sphere` + #[pyo3(name = "sphere")] + #[staticmethod] + #[pyo3(signature = (r))] + fn sphere(r: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::surfaces::NurbsSurface::sphere(r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyNurbsSurface { inner: __v }) + } + + /// Exact torus: minor circle of radius `r` revolved at major + /// radius `big_r` around the y axis. + /// + /// Panics: + /// Panics unless `0 < r < big_r`. + /// + /// Rust: `mesh::surfaces::NurbsSurface::torus` + #[pyo3(name = "torus")] + #[staticmethod] + #[pyo3(signature = (big_r, r))] + fn torus(big_r: f64, r: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::surfaces::NurbsSurface::torus(big_r, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyNurbsSurface { inner: __v }) + } + + /// Exact cylinder of radius `r` and height `h` along the y axis, + /// base at y = 0. + /// + /// Panics: + /// Panics unless `r > 0` and `h > 0`. + /// + /// Rust: `mesh::surfaces::NurbsSurface::cylinder` + #[pyo3(name = "cylinder")] + #[staticmethod] + #[pyo3(signature = (r, h))] + fn cylinder(r: f64, h: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::mesh::surfaces::NurbsSurface::cylinder(r, h)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyNurbsSurface { inner: __v }) + } + + #[getter] + #[pyo3(name = "degree_u")] + fn py_get_degree_u(&self) -> PyResult { Ok(self.inner.degree_u) } + + #[setter] + #[pyo3(name = "degree_u")] + fn py_set_degree_u(&mut self, v: usize) { self.inner.degree_u = v; } + + #[getter] + #[pyo3(name = "degree_v")] + fn py_get_degree_v(&self) -> PyResult { Ok(self.inner.degree_v) } + + #[setter] + #[pyo3(name = "degree_v")] + fn py_set_degree_v(&mut self, v: usize) { self.inner.degree_v = v; } + + #[getter] + #[pyo3(name = "knots_u")] + fn py_get_knots_u(&self) -> PyResult> { Ok(self.inner.knots_u.clone()) } + + #[getter] + #[pyo3(name = "knots_v")] + fn py_get_knots_v(&self) -> PyResult> { Ok(self.inner.knots_v.clone()) } + + #[getter] + #[pyo3(name = "control")] + fn py_get_control(&self) -> PyResult>> { Ok(self.inner.control.clone().into_iter().map(|__x| __x.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()).collect::>()) } + + #[getter] + #[pyo3(name = "weights")] + fn py_get_weights(&self) -> PyResult>> { Ok(self.inner.weights.clone()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("NurbsSurface", "NurbsSurface", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} diff --git a/bindings/python/src/generated/types/mod.rs b/bindings/python/src/generated/types/mod.rs new file mode 100644 index 0000000..9a66353 --- /dev/null +++ b/bindings/python/src/generated/types/mod.rs @@ -0,0 +1,83 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + +mod astrophysics; +pub use astrophysics::*; +mod audio; +pub use audio::*; +mod biophysics; +pub use biophysics::*; +mod cfd; +pub use cfd::*; +mod codes; +pub use codes::*; +mod control_systems; +pub use control_systems::*; +mod core; +pub use core::*; +mod discrete; +pub use discrete::*; +mod dsp; +pub use dsp::*; +mod exact; +pub use exact::*; +mod fem; +pub use fem::*; +mod fields; +pub use fields::*; +mod finance; +pub use finance::*; +mod fractals; +pub use fractals::*; +mod geometry; +pub use geometry::*; +mod graph; +pub use graph::*; +mod learn; +pub use learn::*; +mod linalg; +pub use linalg::*; +mod manifold; +pub use manifold::*; +mod materials; +pub use materials::*; +mod math; +pub use math::*; +mod mesh; +pub use mesh::*; +mod monte_carlo; +pub use monte_carlo::*; +mod numerical; +pub use numerical::*; +mod optimization; +pub use optimization::*; +mod patterns; +pub use patterns::*; +mod quantum; +pub use quantum::*; +mod quaternion; +pub use quaternion::*; +mod resonance; +pub use resonance::*; +mod sim; +pub use sim::*; +mod spatial; +pub use spatial::*; +mod statistical_mechanics; +pub use statistical_mechanics::*; +mod statistics; +pub use statistics::*; +mod stochastic; +pub use stochastic::*; +mod transforms; +pub use transforms::*; +mod units; +pub use units::*; diff --git a/bindings/python/src/generated/types/monte_carlo.rs b/bindings/python/src/generated/types/monte_carlo.rs new file mode 100644 index 0000000..ce94814 --- /dev/null +++ b/bindings/python/src/generated/types/monte_carlo.rs @@ -0,0 +1,203 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; + + +/// Linear congruential pseudo-random number generator. +/// +/// Rust: `monte_carlo::Rng` +#[pyclass(name = "Rng", module = "numeria.monte_carlo")] +pub struct PyRng { pub inner: rust_physics_engine::monte_carlo::Rng } +#[pymethods] +impl PyRng { + /// Creates a new RNG seeded with the given value. + /// + /// Rust: `monte_carlo::Rng::new` + #[new] + #[pyo3(signature = (seed))] + fn __new__(seed: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::monte_carlo::Rng::new(seed)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyRng { inner: __v }) + } + + /// Advances the LCG state and returns the next pseudo-random u64. + /// + /// The low bits are not random: + /// + /// This is a plain linear congruential generator returning its raw + /// state, and for such a generator bit `k` has period at most + /// `2^(k+1)`. The bottom bit therefore alternates, the bottom two + /// cycle with period four, and so on. Taking `next_u64() % m` for a + /// **power of two** `m` reads exactly those bits and produces a + /// fixed repeating cycle -- `% 2` gives `0, 1, 0, 1, ...` and `% 4` + /// gives `0, 3, 2, 1, ...` for ever. Two such sequences drawn one + /// after another are perfectly correlated, which is not a subtle + /// statistical defect but a complete absence of randomness. + /// + /// A modulus with an odd factor mixes in higher bits and is fine. + /// Rather than remember which is which, use `Rng::below`, which + /// takes its answer from the top of the word. + /// + /// Rust: `monte_carlo::Rng::next_u64` + #[pyo3(name = "next_u64")] + #[pyo3(signature = ())] + fn next_u64(&mut self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.next_u64()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Returns a uniform random f64 in [0, 1) by extracting the top 53 mantissa bits. + /// + /// Rust: `monte_carlo::Rng::next_f64` + #[pyo3(name = "next_f64")] + #[pyo3(signature = ())] + fn next_f64(&mut self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.next_f64()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// A uniform integer in `0..n`, taken from the high bits. + /// + /// Use this rather than `next_u64() % n` whenever `n` might be a + /// power of two -- see the note on `Rng::next_u64` for why that + /// combination returns a short repeating cycle instead of a random + /// value. Returns zero for `n == 0`, there being no such range. + /// + /// Exact for `n` up to `2^53`, which is where the mantissa the + /// scaling goes through runs out. + /// + /// Rust: `monte_carlo::Rng::below` + #[pyo3(name = "below")] + #[pyo3(signature = (n))] + fn below(&mut self, n: u64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.below(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Returns a standard normal variate via the Box-Muller transform. + /// + /// Rust: `monte_carlo::Rng::next_gaussian` + #[pyo3(name = "next_gaussian")] + #[pyo3(signature = ())] + fn next_gaussian(&mut self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.next_gaussian()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// Halton low-discrepancy sequence: dimension d uses the radical +/// inverse in the d-th prime. +/// +/// Rust: `monte_carlo::quasi::Halton` +#[pyclass(name = "Halton", module = "numeria.monte_carlo.quasi")] +pub struct PyHalton { pub inner: rust_physics_engine::monte_carlo::quasi::Halton } +#[pymethods] +impl PyHalton { + /// Panics: + /// Panics unless 1 ≤ dim ≤ 25. + /// + /// Rust: `monte_carlo::quasi::Halton::new` + #[new] + #[pyo3(signature = (dim))] + fn __new__(dim: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::monte_carlo::quasi::Halton::new(dim)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyHalton { inner: __v }) + } + + /// The next point in the sequence (index starts at 1). + /// + /// Inherent rather than `Iterator` for the same reason as the Sobol + /// sequence above. + /// + /// Rust: `monte_carlo::quasi::Halton::next` + #[pyo3(name = "next")] + #[pyo3(signature = ())] + fn next<'py>(&mut self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.next())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// Sobol low-discrepancy sequence in `dim` dimensions (dim ≤ 21). +/// +/// Successive calls to `Sobol::next` return points x₁, x₂, … in +/// [0, 1)^dim (the origin point x₀ = 0 is skipped). +/// +/// Rust: `monte_carlo::quasi::Sobol` +#[pyclass(name = "Sobol", module = "numeria.monte_carlo.quasi")] +pub struct PySobol { pub inner: rust_physics_engine::monte_carlo::quasi::Sobol } +#[pymethods] +impl PySobol { + /// Panics: + /// Panics unless 1 ≤ dim ≤ 21. + /// + /// Rust: `monte_carlo::quasi::Sobol::new` + #[new] + #[pyo3(signature = (dim))] + fn __new__(dim: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::monte_carlo::quasi::Sobol::new(dim)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySobol { inner: __v }) + } + + /// The next point in the sequence. + /// + /// Named `next` to match the sequence vocabulary; it is deliberately an + /// inherent method rather than `Iterator`, which cannot borrow `self` + /// mutably and yield a fresh `Vec` without allocating an adapter. + /// + /// Rust: `monte_carlo::quasi::Sobol::next` + #[pyo3(name = "next")] + #[pyo3(signature = ())] + fn next<'py>(&mut self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.next())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Skips the next n points. + /// + /// Rust: `monte_carlo::quasi::Sobol::skip` + #[pyo3(name = "skip")] + #[pyo3(signature = (n))] + fn skip(&mut self, n: u64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.skip(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Dimensionality of the sequence. + /// + /// Rust: `monte_carlo::quasi::Sobol::dim` + #[pyo3(name = "dim")] + #[pyo3(signature = ())] + fn dim(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.dim()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + fn __repr__(&self) -> String { "".to_string() } +} diff --git a/bindings/python/src/generated/types/numerical.rs b/bindings/python/src/generated/types/numerical.rs new file mode 100644 index 0000000..9949f9f --- /dev/null +++ b/bindings/python/src/generated/types/numerical.rs @@ -0,0 +1,278 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; + + +/// Result of an error-estimating quadrature: the integral estimate, an +/// upper bound on its error, and the number of function evaluations. +/// +/// Rust: `numerical::integrate::QuadResult` +#[pyclass(name = "QuadResult", module = "numeria.numerical.integrate", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyQuadResult { pub inner: rust_physics_engine::numerical::integrate::QuadResult } +#[pymethods] +impl PyQuadResult { + /// Builds a `QuadResult` from its fields. + #[new] + #[pyo3(signature = (value, error, evals))] + fn __new__(value: f64, error: f64, evals: usize) -> Self { + + Self { inner: rust_physics_engine::numerical::integrate::QuadResult { value: value, error: error, evals: evals } } + } + + #[getter] + #[pyo3(name = "value")] + fn py_get_value(&self) -> PyResult { Ok(self.inner.value) } + + #[setter] + #[pyo3(name = "value")] + fn py_set_value(&mut self, v: f64) { self.inner.value = v; } + + #[getter] + #[pyo3(name = "error")] + fn py_get_error(&self) -> PyResult { Ok(self.inner.error) } + + #[setter] + #[pyo3(name = "error")] + fn py_set_error(&mut self, v: f64) { self.inner.error = v; } + + #[getter] + #[pyo3(name = "evals")] + fn py_get_evals(&self) -> PyResult { Ok(self.inner.evals) } + + #[setter] + #[pyo3(name = "evals")] + fn py_set_evals(&mut self, v: usize) { self.inner.evals = v; } + + fn __repr__(&self) -> String { format!("QuadResult(value={:?}, error={:?}, evals={:?})", self.inner.value, self.inner.error, self.inner.evals) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Clamped uniform B-spline curve evaluated by de Boor's algorithm +/// (de Boor, *A Practical Guide to Splines*). +/// +/// Rust: `numerical::interpolate::BSpline` +#[pyclass(name = "BSpline", module = "numeria.numerical.interpolate", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyBSpline { pub inner: rust_physics_engine::numerical::interpolate::BSpline } +#[pymethods] +impl PyBSpline { + /// Builds a `BSpline` from its fields. + #[new] + #[pyo3(signature = (degree, knots, control))] + fn __new__(degree: usize, knots: Vec, control: Vec) -> Self { + let control = control.into_iter().map(|__e| __e.0).collect::>(); + Self { inner: rust_physics_engine::numerical::interpolate::BSpline { degree: degree, knots: knots, control: control } } + } + + /// Clamped uniform knot vector: the curve interpolates the first + /// and last control points, with parameter domain [0, n − p]. + /// + /// Panics: + /// Panics unless degree ≥ 1 and there are more than `degree` + /// control points. + /// + /// Rust: `numerical::interpolate::BSpline::uniform` + #[pyo3(name = "uniform")] + #[staticmethod] + #[pyo3(signature = (degree, control))] + fn uniform(degree: usize, control: Vec) -> PyResult { + let control = control.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::numerical::interpolate::BSpline::uniform(degree, &control)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBSpline { inner: __v }) + } + + /// Domain of the curve parameter. + /// + /// Rust: `numerical::interpolate::BSpline::domain` + #[pyo3(name = "domain")] + #[pyo3(signature = ())] + fn domain(&self) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| self.inner.domain()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) + } + + /// Point on the curve at parameter u (clamped to the domain). + /// + /// Rust: `numerical::interpolate::BSpline::eval` + #[pyo3(name = "eval")] + #[pyo3(signature = (u))] + fn eval(&self, u: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.eval(u)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Derivative curve value at u: the degree p−1 B-spline with + /// control points p·(P_{i+1} − P_i)/(u_{i+p+1} − u_{i+1}). + /// + /// Rust: `numerical::interpolate::BSpline::derivative` + #[pyo3(name = "derivative")] + #[pyo3(signature = (u))] + fn derivative(&self, u: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.derivative(u)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + #[getter] + #[pyo3(name = "degree")] + fn py_get_degree(&self) -> PyResult { Ok(self.inner.degree) } + + #[setter] + #[pyo3(name = "degree")] + fn py_set_degree(&mut self, v: usize) { self.inner.degree = v; } + + #[getter] + #[pyo3(name = "knots")] + fn py_get_knots(&self) -> PyResult> { Ok(self.inner.knots.clone()) } + + #[getter] + #[pyo3(name = "control")] + fn py_get_control(&self) -> PyResult> { Ok(self.inner.control.clone().into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("BSpline", "BSpline", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Piecewise cubic spline S_i(t) = a_i + b_i·Δ + c_i·Δ² + d_i·Δ³ with +/// Δ = t − x_i on segment i (Burden & Faires, *Numerical Analysis*, +/// §3.5). Built with the Thomas tridiagonal solve; C² across knots. +/// +/// Rust: `numerical::interpolate::CubicSpline` +#[pyclass(name = "CubicSpline", module = "numeria.numerical.interpolate", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyCubicSpline { pub inner: rust_physics_engine::numerical::interpolate::CubicSpline } +#[pymethods] +impl PyCubicSpline { + /// Natural spline: zero second derivative at both ends. + /// + /// Rust: `numerical::interpolate::CubicSpline::natural` + #[pyo3(name = "natural")] + #[staticmethod] + #[pyo3(signature = (x, y))] + fn natural(x: Vec, y: Vec) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::numerical::interpolate::CubicSpline::natural(&x, &y)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(crate::generated::types::PyCubicSpline { inner: __v }) + } + + /// Clamped spline with prescribed end slopes dy0 = S'(x₀) and + /// dyn = S'(xₙ). + /// + /// Rust: `numerical::interpolate::CubicSpline::clamped` + #[pyo3(name = "clamped")] + #[staticmethod] + #[pyo3(signature = (x, y, dy0, dyn_))] + fn clamped(x: Vec, y: Vec, dy0: f64, dyn_: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::numerical::interpolate::CubicSpline::clamped(&x, &y, dy0, dyn_)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_solve)?; + Ok(crate::generated::types::PyCubicSpline { inner: __v }) + } + + /// Spline value S(t); outside the knot range the end polynomials + /// extrapolate. + /// + /// Rust: `numerical::interpolate::CubicSpline::eval` + #[pyo3(name = "eval")] + #[pyo3(signature = (t))] + fn eval(&self, t: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.eval(t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// First derivative S'(t). + /// + /// Rust: `numerical::interpolate::CubicSpline::derivative` + #[pyo3(name = "derivative")] + #[pyo3(signature = (t))] + fn derivative(&self, t: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.derivative(t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Definite integral ∫ₐᵇ S(t) dt, splitting at interior knots + /// (segments are integrated in closed form). + /// + /// Rust: `numerical::interpolate::CubicSpline::integrate` + #[pyo3(name = "integrate")] + #[pyo3(signature = (a, b))] + fn integrate(&self, a: f64, b: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.integrate(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("CubicSpline", "CubicSpline", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Result of an adaptive integration: accepted step times, states, and +/// the number of rejected trial steps. +/// +/// Rust: `numerical::ode::adaptive::AdaptiveResult` +#[pyclass(name = "AdaptiveResult", module = "numeria.numerical.ode.adaptive", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyAdaptiveResult { pub inner: rust_physics_engine::numerical::ode::adaptive::AdaptiveResult } +#[pymethods] +impl PyAdaptiveResult { + /// Builds a `AdaptiveResult` from its fields. + #[new] + #[pyo3(signature = (t, y, steps_rejected))] + fn __new__(t: Vec, y: Vec>, steps_rejected: usize) -> Self { + + Self { inner: rust_physics_engine::numerical::ode::adaptive::AdaptiveResult { t: t, y: y, steps_rejected: steps_rejected } } + } + + #[getter] + #[pyo3(name = "t")] + fn py_get_t(&self) -> PyResult> { Ok(self.inner.t.clone()) } + + #[getter] + #[pyo3(name = "y")] + fn py_get_y(&self) -> PyResult>> { Ok(self.inner.y.clone()) } + + #[getter] + #[pyo3(name = "steps_rejected")] + fn py_get_steps_rejected(&self) -> PyResult { Ok(self.inner.steps_rejected) } + + #[setter] + #[pyo3(name = "steps_rejected")] + fn py_set_steps_rejected(&mut self, v: usize) { self.inner.steps_rejected = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("AdaptiveResult", "AdaptiveResult", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} diff --git a/bindings/python/src/generated/types/optimization.rs b/bindings/python/src/generated/types/optimization.rs new file mode 100644 index 0000000..0aa2d19 --- /dev/null +++ b/bindings/python/src/generated/types/optimization.rs @@ -0,0 +1,619 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; + + +/// Always cooperate. +/// +/// Rust: `optimization::game_theory::AlwaysCooperate` +#[pyclass(name = "AlwaysCooperate", module = "numeria.optimization.game_theory")] +pub struct PyAlwaysCooperate { pub inner: rust_physics_engine::optimization::game_theory::AlwaysCooperate } +#[pymethods] +impl PyAlwaysCooperate { + fn __repr__(&self) -> String { "".to_string() } +} + +/// Always defect: the unique equilibrium of the one-shot game and of any +/// finitely repeated game with a commonly known end. +/// +/// Rust: `optimization::game_theory::AlwaysDefect` +#[pyclass(name = "AlwaysDefect", module = "numeria.optimization.game_theory")] +pub struct PyAlwaysDefect { pub inner: rust_physics_engine::optimization::game_theory::AlwaysDefect } +#[pymethods] +impl PyAlwaysDefect { + fn __repr__(&self) -> String { "".to_string() } +} + +/// A node of an extensive-form game tree. +/// +/// A leaf carries a payoff for each player; an internal node names the player +/// to move and its children. +/// +/// Rust: `optimization::game_theory::GameTree` +#[pyclass(name = "GameTree", module = "numeria.optimization.game_theory", from_py_object)] +#[derive(Clone)] +pub struct PyGameTree { pub inner: rust_physics_engine::optimization::game_theory::GameTree } +#[pymethods] +impl PyGameTree { + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("GameTree", "GameTree", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Tit for tat that forgives an occasional defection, which is what keeps two +/// copies of it from locking into mutual retaliation under noise. +/// +/// Rust: `optimization::game_theory::GenerousTitForTat` +#[pyclass(name = "GenerousTitForTat", module = "numeria.optimization.game_theory")] +pub struct PyGenerousTitForTat { pub inner: rust_physics_engine::optimization::game_theory::GenerousTitForTat } +#[pymethods] +impl PyGenerousTitForTat { + /// Builds a `GenerousTitForTat` from its fields. + #[new] + #[pyo3(signature = (forgiveness))] + fn __new__(forgiveness: f64) -> Self { + + Self { inner: rust_physics_engine::optimization::game_theory::GenerousTitForTat { forgiveness: forgiveness } } + } + + #[getter] + #[pyo3(name = "forgiveness")] + fn py_get_forgiveness(&self) -> PyResult { Ok(self.inner.forgiveness) } + + #[setter] + #[pyo3(name = "forgiveness")] + fn py_set_forgiveness(&mut self, v: f64) { self.inner.forgiveness = v; } + + fn __repr__(&self) -> String { format!("GenerousTitForTat(forgiveness={:?})", self.inner.forgiveness) } +} + +/// Cooperate until defected on once, then defect forever. +/// +/// Rust: `optimization::game_theory::Grim` +#[pyclass(name = "Grim", module = "numeria.optimization.game_theory")] +pub struct PyGrim { pub inner: rust_physics_engine::optimization::game_theory::Grim } +#[pymethods] +impl PyGrim { + fn __repr__(&self) -> String { "".to_string() } +} + +/// A move in the iterated prisoner's dilemma. +/// +/// Rust: `optimization::game_theory::Move` +#[pyclass(name = "Move", module = "numeria.optimization.game_theory", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyMove { + Cooperate, + Defect, +} +impl PyMove { + pub fn to_rust(&self) -> rust_physics_engine::optimization::game_theory::Move { match self { + Self::Cooperate => rust_physics_engine::optimization::game_theory::Move::Cooperate, + Self::Defect => rust_physics_engine::optimization::game_theory::Move::Defect, + } } + pub fn from_rust(v: &rust_physics_engine::optimization::game_theory::Move) -> Self { match v { + rust_physics_engine::optimization::game_theory::Move::Cooperate => Self::Cooperate, + rust_physics_engine::optimization::game_theory::Move::Defect => Self::Defect, + } } +} +#[pymethods] +impl PyMove { + fn __repr__(&self) -> &'static str { + match self { + Self::Cooperate => "Move.Cooperate", + Self::Defect => "Move.Defect", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Win-stay lose-shift: repeat the last move if it earned a good payoff, +/// switch if it did not. +/// +/// Rust: `optimization::game_theory::Pavlov` +#[pyclass(name = "Pavlov", module = "numeria.optimization.game_theory")] +pub struct PyPavlov { pub inner: rust_physics_engine::optimization::game_theory::Pavlov } +#[pymethods] +impl PyPavlov { + fn __repr__(&self) -> String { "".to_string() } +} + +/// Cooperate with fixed probability, ignoring the opponent. +/// +/// Rust: `optimization::game_theory::RandomPlayer` +#[pyclass(name = "RandomPlayer", module = "numeria.optimization.game_theory")] +pub struct PyRandomPlayer { pub inner: rust_physics_engine::optimization::game_theory::RandomPlayer } +#[pymethods] +impl PyRandomPlayer { + /// Builds a `RandomPlayer` from its fields. + #[new] + #[pyo3(signature = (cooperate_probability))] + fn __new__(cooperate_probability: f64) -> Self { + + Self { inner: rust_physics_engine::optimization::game_theory::RandomPlayer { cooperate_probability: cooperate_probability } } + } + + #[getter] + #[pyo3(name = "cooperate_probability")] + fn py_get_cooperate_probability(&self) -> PyResult { Ok(self.inner.cooperate_probability) } + + #[setter] + #[pyo3(name = "cooperate_probability")] + fn py_set_cooperate_probability(&mut self, v: f64) { self.inner.cooperate_probability = v; } + + fn __repr__(&self) -> String { format!("RandomPlayer(cooperate_probability={:?})", self.inner.cooperate_probability) } +} + +/// Cooperate first, then copy the opponent's last move. +/// +/// Rust: `optimization::game_theory::TitForTat` +#[pyclass(name = "TitForTat", module = "numeria.optimization.game_theory")] +pub struct PyTitForTat { pub inner: rust_physics_engine::optimization::game_theory::TitForTat } +#[pymethods] +impl PyTitForTat { + fn __repr__(&self) -> String { "".to_string() } +} + +/// One edit in a transformation from one sequence to another. +/// +/// Rust: `optimization::integer::EditOp` +#[pyclass(name = "EditOp", module = "numeria.optimization.integer", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyEditOp { pub inner: rust_physics_engine::optimization::integer::EditOp } +#[pymethods] +impl PyEditOp { + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("EditOp", "EditOp", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Result of a Levenberg-Marquardt fit. +/// +/// `residual` is the final sum of squared residuals ‖r(p)‖²; +/// `covariance` is s²·(JᵀJ)⁻¹ with s² = SSR/(m−n) when m > n and JᵀJ is +/// invertible, `None` otherwise. +/// +/// Rust: `optimization::least_squares::LmResult` +#[pyclass(name = "LmResult", module = "numeria.optimization.least_squares", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyLmResult { pub inner: rust_physics_engine::optimization::least_squares::LmResult } +#[pymethods] +impl PyLmResult { + /// Builds a `LmResult` from its fields. + #[new] + #[pyo3(signature = (params, residual, iters, covariance))] + fn __new__(params: Vec, residual: f64, iters: usize, covariance: Option) -> Self { + let covariance = covariance.map(|__o| __o.0); + Self { inner: rust_physics_engine::optimization::least_squares::LmResult { params: params, residual: residual, iters: iters, covariance: covariance } } + } + + #[getter] + #[pyo3(name = "params")] + fn py_get_params(&self) -> PyResult> { Ok(self.inner.params.clone()) } + + #[getter] + #[pyo3(name = "residual")] + fn py_get_residual(&self) -> PyResult { Ok(self.inner.residual) } + + #[setter] + #[pyo3(name = "residual")] + fn py_set_residual(&mut self, v: f64) { self.inner.residual = v; } + + #[getter] + #[pyo3(name = "iters")] + fn py_get_iters(&self) -> PyResult { Ok(self.inner.iters) } + + #[setter] + #[pyo3(name = "iters")] + fn py_set_iters(&mut self, v: usize) { self.inner.iters = v; } + + #[getter] + #[pyo3(name = "covariance")] + fn py_get_covariance(&self) -> PyResult> { Ok(self.inner.covariance.clone().map(|__x| crate::generated::types::PyMatrix { inner: __x })) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("LmResult", "LmResult", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// The sense of a constraint row. +/// +/// Rust: `optimization::lp::Cmp` +#[pyclass(name = "Cmp", module = "numeria.optimization.lp", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyCmp { + Le, + Ge, + Eq, +} +impl PyCmp { + pub fn to_rust(&self) -> rust_physics_engine::optimization::lp::Cmp { match self { + Self::Le => rust_physics_engine::optimization::lp::Cmp::Le, + Self::Ge => rust_physics_engine::optimization::lp::Cmp::Ge, + Self::Eq => rust_physics_engine::optimization::lp::Cmp::Eq, + } } + pub fn from_rust(v: &rust_physics_engine::optimization::lp::Cmp) -> Self { match v { + rust_physics_engine::optimization::lp::Cmp::Le => Self::Le, + rust_physics_engine::optimization::lp::Cmp::Ge => Self::Ge, + rust_physics_engine::optimization::lp::Cmp::Eq => Self::Eq, + } } +} +#[pymethods] +impl PyCmp { + fn __repr__(&self) -> &'static str { + match self { + Self::Le => "Cmp.Le", + Self::Ge => "Cmp.Ge", + Self::Eq => "Cmp.Eq", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A linear program. +/// +/// Minimises (or maximises) `c . x` subject to the rows of `a` compared +/// against `b` by `constraint_types`, with each variable confined to its +/// entry of `bounds`. A bound of `(0.0, f64::INFINITY)` is the default +/// non-negative variable; `(f64::NEG_INFINITY, f64::INFINITY)` makes a +/// variable free. +/// +/// Rust: `optimization::lp::LpProblem` +#[pyclass(name = "LpProblem", module = "numeria.optimization.lp", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyLpProblem { pub inner: rust_physics_engine::optimization::lp::LpProblem } +#[pymethods] +impl PyLpProblem { + /// A problem in the common shape: `A x <= b`, `x >= 0`. + /// + /// Errors: + /// Returns an error if the shapes disagree. + /// + /// Rust: `optimization::lp::LpProblem::new` + #[new] + #[pyo3(signature = (c, a, b, maximize))] + fn __new__(c: Vec, a: crate::generated::types::PyMatrixArg, b: Vec, maximize: bool) -> PyResult { + let a = a.0; + let __r = crate::runtime::guard(|| rust_physics_engine::optimization::lp::LpProblem::new(c, a, b, maximize)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyLpProblem { inner: __v }) + } + + /// Number of variables. + /// + /// Rust: `optimization::lp::LpProblem::n` + #[pyo3(name = "n")] + #[pyo3(signature = ())] + fn n(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.n()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Number of constraints. + /// + /// Rust: `optimization::lp::LpProblem::m` + #[pyo3(name = "m")] + #[pyo3(signature = ())] + fn m(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.m()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Checks that every part of the problem has a consistent shape. + /// + /// Errors: + /// Returns `GeomError::InvalidArgument` describing the first mismatch. + /// + /// Rust: `optimization::lp::LpProblem::validate` + #[pyo3(name = "validate")] + #[pyo3(signature = ())] + fn validate(&self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.validate()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(()) + } + + /// The objective value at a point, in the problem's own sense. + /// + /// Rust: `optimization::lp::LpProblem::objective_at` + #[pyo3(name = "objective_at")] + #[pyo3(signature = (x))] + fn objective_at<'py>(&self, py: Python<'py>, x: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.objective_at(&x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Whether `x` satisfies every constraint and bound to within `tol`. + /// + /// Rust: `optimization::lp::LpProblem::is_feasible` + #[pyo3(name = "is_feasible")] + #[pyo3(signature = (x, tol))] + fn is_feasible<'py>(&self, py: Python<'py>, x: Vec, tol: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.is_feasible(&x, tol))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "c")] + fn py_get_c(&self) -> PyResult> { Ok(self.inner.c.clone()) } + + #[getter] + #[pyo3(name = "a")] + fn py_get_a(&self) -> PyResult { Ok(crate::generated::types::PyMatrix { inner: self.inner.a.clone() }) } + + #[getter] + #[pyo3(name = "b")] + fn py_get_b(&self) -> PyResult> { Ok(self.inner.b.clone()) } + + #[getter] + #[pyo3(name = "constraint_types")] + fn py_get_constraint_types(&self) -> PyResult> { Ok(self.inner.constraint_types.clone().into_iter().map(|__x| crate::generated::types::PyCmp::from_rust(&__x)).collect::>()) } + + #[getter] + #[pyo3(name = "bounds")] + fn py_get_bounds(&self) -> PyResult> { Ok(self.inner.bounds.clone().into_iter().map(|__x| (__x.0, __x.1)).collect::>()) } + + #[getter] + #[pyo3(name = "maximize")] + fn py_get_maximize(&self) -> PyResult { Ok(self.inner.maximize) } + + #[setter] + #[pyo3(name = "maximize")] + fn py_set_maximize(&mut self, v: bool) { self.inner.maximize = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("LpProblem", "LpProblem", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// What a solver concluded. +/// +/// Rust: `optimization::lp::LpResult` +#[pyclass(name = "LpResult", module = "numeria.optimization.lp", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyLpResult { pub inner: rust_physics_engine::optimization::lp::LpResult } +#[pymethods] +impl PyLpResult { + /// The optimal objective, or `None` if the problem had no optimum. + /// + /// Rust: `optimization::lp::LpResult::objective` + #[pyo3(name = "objective")] + #[pyo3(signature = ())] + fn objective(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.objective()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) + } + + /// The optimal point, or `None`. + /// + /// Rust: `optimization::lp::LpResult::solution` + #[pyo3(name = "solution")] + #[pyo3(signature = ())] + fn solution(&self) -> PyResult>> { + let __r = crate::runtime::guard(|| self.inner.solution()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x.to_vec())) + } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("LpResult", "LpResult", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A benchmark landscape: name, function, per-coordinate bounds, and the +/// known global minimum value. +/// +/// Rust: `optimization::metaheuristics::Benchmark` +#[pyclass(name = "Benchmark", module = "numeria.optimization.metaheuristics")] +pub struct PyBenchmark { pub inner: rust_physics_engine::optimization::metaheuristics::Benchmark } +#[pymethods] +impl PyBenchmark { + #[getter] + #[pyo3(name = "name")] + fn py_get_name(&self) -> PyResult { Ok(self.inner.name.to_string()) } + + #[getter] + #[pyo3(name = "bounds")] + fn py_get_bounds(&self) -> PyResult> { Ok(self.inner.bounds.clone().into_iter().map(|__x| (__x.0, __x.1)).collect::>()) } + + #[getter] + #[pyo3(name = "optimum")] + fn py_get_optimum(&self) -> PyResult { Ok(self.inner.optimum) } + + #[setter] + #[pyo3(name = "optimum")] + fn py_set_optimum(&mut self, v: f64) { self.inner.optimum = v; } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// Settings for the real-valued genetic algorithm. +/// +/// Rust: `optimization::metaheuristics::GaConfig` +#[pyclass(name = "GaConfig", module = "numeria.optimization.metaheuristics", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyGaConfig { pub inner: rust_physics_engine::optimization::metaheuristics::GaConfig } +#[pymethods] +impl PyGaConfig { + /// Builds a `GaConfig` from its fields. + #[new] + #[pyo3(signature = (population, generations, mutation_rate, mutation_scale, elite))] + fn __new__(population: usize, generations: usize, mutation_rate: f64, mutation_scale: f64, elite: usize) -> Self { + + Self { inner: rust_physics_engine::optimization::metaheuristics::GaConfig { population: population, generations: generations, mutation_rate: mutation_rate, mutation_scale: mutation_scale, elite: elite } } + } + + #[getter] + #[pyo3(name = "population")] + fn py_get_population(&self) -> PyResult { Ok(self.inner.population) } + + #[setter] + #[pyo3(name = "population")] + fn py_set_population(&mut self, v: usize) { self.inner.population = v; } + + #[getter] + #[pyo3(name = "generations")] + fn py_get_generations(&self) -> PyResult { Ok(self.inner.generations) } + + #[setter] + #[pyo3(name = "generations")] + fn py_set_generations(&mut self, v: usize) { self.inner.generations = v; } + + #[getter] + #[pyo3(name = "mutation_rate")] + fn py_get_mutation_rate(&self) -> PyResult { Ok(self.inner.mutation_rate) } + + #[setter] + #[pyo3(name = "mutation_rate")] + fn py_set_mutation_rate(&mut self, v: f64) { self.inner.mutation_rate = v; } + + #[getter] + #[pyo3(name = "mutation_scale")] + fn py_get_mutation_scale(&self) -> PyResult { Ok(self.inner.mutation_scale) } + + #[setter] + #[pyo3(name = "mutation_scale")] + fn py_set_mutation_scale(&mut self, v: f64) { self.inner.mutation_scale = v; } + + #[getter] + #[pyo3(name = "elite")] + fn py_get_elite(&self) -> PyResult { Ok(self.inner.elite) } + + #[setter] + #[pyo3(name = "elite")] + fn py_set_elite(&mut self, v: usize) { self.inner.elite = v; } + + fn __repr__(&self) -> String { format!("GaConfig(population={:?}, generations={:?}, mutation_rate={:?}, mutation_scale={:?}, elite={:?})", self.inner.population, self.inner.generations, self.inner.mutation_rate, self.inner.mutation_scale, self.inner.elite) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// The four schedule times of one task: earliest start, earliest finish, +/// latest start, latest finish. +/// +/// Rust: `optimization::network::TaskTimes` +#[pyclass(name = "TaskTimes", module = "numeria.optimization.network", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyTaskTimes { pub inner: rust_physics_engine::optimization::network::TaskTimes } +#[pymethods] +impl PyTaskTimes { + /// Builds a `TaskTimes` from its fields. + #[new] + #[pyo3(signature = (early_start, early_finish, late_start, late_finish))] + fn __new__(early_start: f64, early_finish: f64, late_start: f64, late_finish: f64) -> Self { + + Self { inner: rust_physics_engine::optimization::network::TaskTimes { early_start: early_start, early_finish: early_finish, late_start: late_start, late_finish: late_finish } } + } + + /// How far the task can slip without delaying the project. + /// + /// Zero exactly on the critical path, which is what defines it. + /// + /// Rust: `optimization::network::TaskTimes::slack` + #[pyo3(name = "slack")] + #[pyo3(signature = ())] + fn slack(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.slack()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "early_start")] + fn py_get_early_start(&self) -> PyResult { Ok(self.inner.early_start) } + + #[setter] + #[pyo3(name = "early_start")] + fn py_set_early_start(&mut self, v: f64) { self.inner.early_start = v; } + + #[getter] + #[pyo3(name = "early_finish")] + fn py_get_early_finish(&self) -> PyResult { Ok(self.inner.early_finish) } + + #[setter] + #[pyo3(name = "early_finish")] + fn py_set_early_finish(&mut self, v: f64) { self.inner.early_finish = v; } + + #[getter] + #[pyo3(name = "late_start")] + fn py_get_late_start(&self) -> PyResult { Ok(self.inner.late_start) } + + #[setter] + #[pyo3(name = "late_start")] + fn py_set_late_start(&mut self, v: f64) { self.inner.late_start = v; } + + #[getter] + #[pyo3(name = "late_finish")] + fn py_get_late_finish(&self) -> PyResult { Ok(self.inner.late_finish) } + + #[setter] + #[pyo3(name = "late_finish")] + fn py_set_late_finish(&mut self, v: f64) { self.inner.late_finish = v; } + + fn __repr__(&self) -> String { format!("TaskTimes(early_start={:?}, early_finish={:?}, late_start={:?}, late_finish={:?})", self.inner.early_start, self.inner.early_finish, self.inner.late_start, self.inner.late_finish) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A `TaskTimes` argument, or anything that can stand in for one. +pub struct PyTaskTimesArg(pub rust_physics_engine::optimization::network::TaskTimes); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyTaskTimesArg { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result { + if let Ok(__w) = obj.extract::() { + return Ok(PyTaskTimesArg(__w.inner)); + } + let __v = crate::runtime::coerce::floats_exact(obj, 4, "TaskTimes")?; + Ok(PyTaskTimesArg(rust_physics_engine::optimization::network::TaskTimes { early_start: __v[0], early_finish: __v[1], late_start: __v[2], late_finish: __v[3] })) + } +} + diff --git a/bindings/python/src/generated/types/patterns.rs b/bindings/python/src/generated/types/patterns.rs new file mode 100644 index 0000000..4f07f56 --- /dev/null +++ b/bindings/python/src/generated/types/patterns.rs @@ -0,0 +1,1027 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; + + +/// Penrose tile kinds. +/// +/// Rust: `patterns::aperiodic::PenroseTile` +#[pyclass(name = "PenroseTile", module = "numeria.patterns.aperiodic", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyPenroseTile { + Kite, + Dart, + ThinRhomb, + ThickRhomb, +} +impl PyPenroseTile { + pub fn to_rust(&self) -> rust_physics_engine::patterns::aperiodic::PenroseTile { match self { + Self::Kite => rust_physics_engine::patterns::aperiodic::PenroseTile::Kite, + Self::Dart => rust_physics_engine::patterns::aperiodic::PenroseTile::Dart, + Self::ThinRhomb => rust_physics_engine::patterns::aperiodic::PenroseTile::ThinRhomb, + Self::ThickRhomb => rust_physics_engine::patterns::aperiodic::PenroseTile::ThickRhomb, + } } + pub fn from_rust(v: &rust_physics_engine::patterns::aperiodic::PenroseTile) -> Self { match v { + rust_physics_engine::patterns::aperiodic::PenroseTile::Kite => Self::Kite, + rust_physics_engine::patterns::aperiodic::PenroseTile::Dart => Self::Dart, + rust_physics_engine::patterns::aperiodic::PenroseTile::ThinRhomb => Self::ThinRhomb, + rust_physics_engine::patterns::aperiodic::PenroseTile::ThickRhomb => Self::ThickRhomb, + } } +} +#[pymethods] +impl PyPenroseTile { + fn __repr__(&self) -> &'static str { + match self { + Self::Kite => "PenroseTile.Kite", + Self::Dart => "PenroseTile.Dart", + Self::ThinRhomb => "PenroseTile.ThinRhomb", + Self::ThickRhomb => "PenroseTile.ThickRhomb", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A placed Penrose tile. Vertices are ordered so positions 1 and 3 +/// are the tile's internal axis/diagonal (the symmetry axis for +/// kites and darts, the splitting diagonal for rhombs). +/// +/// Rust: `patterns::aperiodic::PlacedTile` +#[pyclass(name = "PlacedTile", module = "numeria.patterns.aperiodic", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyPlacedTile { pub inner: rust_physics_engine::patterns::aperiodic::PlacedTile } +#[pymethods] +impl PyPlacedTile { + /// Builds a `PlacedTile` from its fields. + #[new] + #[pyo3(signature = (kind, vertices))] + fn __new__(kind: crate::generated::types::PyPenroseTile, vertices: Vec) -> Self { + let kind = kind.to_rust(); + let vertices = vertices.into_iter().map(|__e| __e.0).collect::>(); + Self { inner: rust_physics_engine::patterns::aperiodic::PlacedTile { kind: kind, vertices: vertices } } + } + + #[getter] + #[pyo3(name = "kind")] + fn py_get_kind(&self) -> PyResult { Ok(crate::generated::types::PyPenroseTile::from_rust(&self.inner.kind.clone())) } + + #[getter] + #[pyo3(name = "vertices")] + fn py_get_vertices(&self) -> PyResult> { Ok(self.inner.vertices.clone().into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("PlacedTile", "PlacedTile", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// How offset corners are joined. +/// +/// Rust: `patterns::polygon_ops::JoinStyle` +#[pyclass(name = "JoinStyle", module = "numeria.patterns.polygon_ops", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyJoinStyle { pub inner: rust_physics_engine::patterns::polygon_ops::JoinStyle } +#[pymethods] +impl PyJoinStyle { + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("JoinStyle", "JoinStyle", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// The 13 Archimedean solids. +/// +/// Rust: `patterns::polyhedra::ArchimedeanSolid` +#[pyclass(name = "ArchimedeanSolid", module = "numeria.patterns.polyhedra", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyArchimedeanSolid { + TruncatedTetrahedron, + Cuboctahedron, + TruncatedCube, + TruncatedOctahedron, + Rhombicuboctahedron, + TruncatedCuboctahedron, + SnubCube, + Icosidodecahedron, + TruncatedDodecahedron, + TruncatedIcosahedron, + Rhombicosidodecahedron, + TruncatedIcosidodecahedron, + SnubDodecahedron, +} +impl PyArchimedeanSolid { + pub fn to_rust(&self) -> rust_physics_engine::patterns::polyhedra::ArchimedeanSolid { match self { + Self::TruncatedTetrahedron => rust_physics_engine::patterns::polyhedra::ArchimedeanSolid::TruncatedTetrahedron, + Self::Cuboctahedron => rust_physics_engine::patterns::polyhedra::ArchimedeanSolid::Cuboctahedron, + Self::TruncatedCube => rust_physics_engine::patterns::polyhedra::ArchimedeanSolid::TruncatedCube, + Self::TruncatedOctahedron => rust_physics_engine::patterns::polyhedra::ArchimedeanSolid::TruncatedOctahedron, + Self::Rhombicuboctahedron => rust_physics_engine::patterns::polyhedra::ArchimedeanSolid::Rhombicuboctahedron, + Self::TruncatedCuboctahedron => rust_physics_engine::patterns::polyhedra::ArchimedeanSolid::TruncatedCuboctahedron, + Self::SnubCube => rust_physics_engine::patterns::polyhedra::ArchimedeanSolid::SnubCube, + Self::Icosidodecahedron => rust_physics_engine::patterns::polyhedra::ArchimedeanSolid::Icosidodecahedron, + Self::TruncatedDodecahedron => rust_physics_engine::patterns::polyhedra::ArchimedeanSolid::TruncatedDodecahedron, + Self::TruncatedIcosahedron => rust_physics_engine::patterns::polyhedra::ArchimedeanSolid::TruncatedIcosahedron, + Self::Rhombicosidodecahedron => rust_physics_engine::patterns::polyhedra::ArchimedeanSolid::Rhombicosidodecahedron, + Self::TruncatedIcosidodecahedron => rust_physics_engine::patterns::polyhedra::ArchimedeanSolid::TruncatedIcosidodecahedron, + Self::SnubDodecahedron => rust_physics_engine::patterns::polyhedra::ArchimedeanSolid::SnubDodecahedron, + } } + pub fn from_rust(v: &rust_physics_engine::patterns::polyhedra::ArchimedeanSolid) -> Self { match v { + rust_physics_engine::patterns::polyhedra::ArchimedeanSolid::TruncatedTetrahedron => Self::TruncatedTetrahedron, + rust_physics_engine::patterns::polyhedra::ArchimedeanSolid::Cuboctahedron => Self::Cuboctahedron, + rust_physics_engine::patterns::polyhedra::ArchimedeanSolid::TruncatedCube => Self::TruncatedCube, + rust_physics_engine::patterns::polyhedra::ArchimedeanSolid::TruncatedOctahedron => Self::TruncatedOctahedron, + rust_physics_engine::patterns::polyhedra::ArchimedeanSolid::Rhombicuboctahedron => Self::Rhombicuboctahedron, + rust_physics_engine::patterns::polyhedra::ArchimedeanSolid::TruncatedCuboctahedron => Self::TruncatedCuboctahedron, + rust_physics_engine::patterns::polyhedra::ArchimedeanSolid::SnubCube => Self::SnubCube, + rust_physics_engine::patterns::polyhedra::ArchimedeanSolid::Icosidodecahedron => Self::Icosidodecahedron, + rust_physics_engine::patterns::polyhedra::ArchimedeanSolid::TruncatedDodecahedron => Self::TruncatedDodecahedron, + rust_physics_engine::patterns::polyhedra::ArchimedeanSolid::TruncatedIcosahedron => Self::TruncatedIcosahedron, + rust_physics_engine::patterns::polyhedra::ArchimedeanSolid::Rhombicosidodecahedron => Self::Rhombicosidodecahedron, + rust_physics_engine::patterns::polyhedra::ArchimedeanSolid::TruncatedIcosidodecahedron => Self::TruncatedIcosidodecahedron, + rust_physics_engine::patterns::polyhedra::ArchimedeanSolid::SnubDodecahedron => Self::SnubDodecahedron, + } } +} +#[pymethods] +impl PyArchimedeanSolid { + fn __repr__(&self) -> &'static str { + match self { + Self::TruncatedTetrahedron => "ArchimedeanSolid.TruncatedTetrahedron", + Self::Cuboctahedron => "ArchimedeanSolid.Cuboctahedron", + Self::TruncatedCube => "ArchimedeanSolid.TruncatedCube", + Self::TruncatedOctahedron => "ArchimedeanSolid.TruncatedOctahedron", + Self::Rhombicuboctahedron => "ArchimedeanSolid.Rhombicuboctahedron", + Self::TruncatedCuboctahedron => "ArchimedeanSolid.TruncatedCuboctahedron", + Self::SnubCube => "ArchimedeanSolid.SnubCube", + Self::Icosidodecahedron => "ArchimedeanSolid.Icosidodecahedron", + Self::TruncatedDodecahedron => "ArchimedeanSolid.TruncatedDodecahedron", + Self::TruncatedIcosahedron => "ArchimedeanSolid.TruncatedIcosahedron", + Self::Rhombicosidodecahedron => "ArchimedeanSolid.Rhombicosidodecahedron", + Self::TruncatedIcosidodecahedron => "ArchimedeanSolid.TruncatedIcosidodecahedron", + Self::SnubDodecahedron => "ArchimedeanSolid.SnubDodecahedron", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A polyhedron with polygonal faces (counterclockwise seen from +/// outside). +/// +/// Rust: `patterns::polyhedra::Polyhedron` +#[pyclass(name = "Polyhedron", module = "numeria.patterns.polyhedra", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyPolyhedron { pub inner: rust_physics_engine::patterns::polyhedra::Polyhedron } +#[pymethods] +impl PyPolyhedron { + /// Builds a `Polyhedron` from its fields. + #[new] + #[pyo3(signature = (vertices, faces))] + fn __new__(vertices: Vec, faces: Vec>) -> Self { + let vertices = vertices.into_iter().map(|__e| __e.0).collect::>(); + Self { inner: rust_physics_engine::patterns::polyhedra::Polyhedron { vertices: vertices, faces: faces } } + } + + /// Fan-triangulates every face into a `Mesh`. + /// + /// Rust: `patterns::polyhedra::Polyhedron::to_mesh` + #[pyo3(name = "to_mesh")] + #[pyo3(signature = ())] + fn to_mesh(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.to_mesh()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMeshMesh { inner: __v }) + } + + /// Unique undirected edges, sorted. + /// + /// Rust: `patterns::polyhedra::Polyhedron::edges` + #[pyo3(name = "edges")] + #[pyo3(signature = ())] + fn edges<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.edges())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) + } + + /// Euler characteristic V − E + F. + /// + /// Rust: `patterns::polyhedra::Polyhedron::euler` + #[pyo3(name = "euler")] + #[pyo3(signature = ())] + fn euler(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.euler()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Centroid of every face. + /// + /// Rust: `patterns::polyhedra::Polyhedron::face_centroids` + #[pyo3(name = "face_centroids")] + #[pyo3(signature = ())] + fn face_centroids(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.face_centroids()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) + } + + /// Unit face normals (Newell's method, robust for near-planar + /// polygons). + /// + /// Rust: `patterns::polyhedra::Polyhedron::face_normals` + #[pyo3(name = "face_normals")] + #[pyo3(signature = ())] + fn face_normals(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.face_normals()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) + } + + /// Signed volume (positive for outward-facing faces). + /// + /// Rust: `patterns::polyhedra::Polyhedron::volume` + #[pyo3(name = "volume")] + #[pyo3(signature = ())] + fn volume(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.volume()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Total face area. + /// + /// Rust: `patterns::polyhedra::Polyhedron::surface_area` + #[pyo3(name = "surface_area")] + #[pyo3(signature = ())] + fn surface_area(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.surface_area()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// True when every vertex lies on or behind every face plane. + /// + /// Rust: `patterns::polyhedra::Polyhedron::is_convex` + #[pyo3(name = "is_convex")] + #[pyo3(signature = ())] + fn is_convex(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_convex()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Faces around vertex `v` in rotational order (walking across + /// shared edges). + /// + /// Panics: + /// Panics when the polyhedron is not closed (a directed edge has + /// no partner) or `v` is unused. + /// + /// Rust: `patterns::polyhedra::Polyhedron::faces_around_vertex` + #[pyo3(name = "faces_around_vertex")] + #[pyo3(signature = (v))] + fn faces_around_vertex<'py>(&self, py: Python<'py>, v: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.faces_around_vertex(v))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The neighbors of `v` in rotational order (the vertex figure). + /// + /// Panics: + /// Panics when the polyhedron is not closed or `v` is unused. + /// + /// Rust: `patterns::polyhedra::Polyhedron::vertex_figure` + #[pyo3(name = "vertex_figure")] + #[pyo3(signature = (v))] + fn vertex_figure<'py>(&self, py: Python<'py>, v: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.vertex_figure(v))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Scales every vertex to the given distance from the origin. + /// + /// Panics: + /// Panics unless `radius > 0`. + /// + /// Rust: `patterns::polyhedra::Polyhedron::normalize` + #[pyo3(name = "normalize")] + #[pyo3(signature = (radius))] + fn normalize(&self, radius: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.normalize(radius)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyhedron { inner: __v }) + } + + /// Hart's canonical form iteration: edges tangent to the unit + /// sphere, faces planar, centroid at the origin. Converges to the + /// canonical (maximally symmetric) shape for convex polyhedra. + /// + /// Rust: `patterns::polyhedra::Polyhedron::canonicalize` + #[pyo3(name = "canonicalize")] + #[pyo3(signature = (iterations))] + fn canonicalize(&self, iterations: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.canonicalize(iterations)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyhedron { inner: __v }) + } + + /// Dual polyhedron: face centroids become vertices, vertices + /// become faces. + /// + /// Rust: `patterns::polyhedra::Polyhedron::dual` + #[pyo3(name = "dual")] + #[pyo3(signature = ())] + fn dual(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.dual()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyhedron { inner: __v }) + } + + /// Detects the rotational symmetry group of the vertex set: + /// tetrahedral, octahedral, icosahedral, or a single-axis + /// cyclic/dihedral group. Returns `None` when no nontrivial + /// rotation (beyond identity, up to order 12 axes) fits. + /// + /// Rust: `patterns::polyhedra::Polyhedron::symmetry_group` + #[pyo3(name = "symmetry_group")] + #[pyo3(signature = ())] + fn symmetry_group(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.symmetry_group()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyPointGroup3 { inner: __x })) + } + + #[getter] + #[pyo3(name = "vertices")] + fn py_get_vertices(&self) -> PyResult> { Ok(self.inner.vertices.clone().into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) } + + #[getter] + #[pyo3(name = "faces")] + fn py_get_faces(&self) -> PyResult>> { Ok(self.inner.faces.clone()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Polyhedron", "Polyhedron", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// The 7 frieze groups (IUCr-style names). +/// +/// Rust: `patterns::symmetry::FriezeGroup` +#[pyclass(name = "FriezeGroup", module = "numeria.patterns.symmetry", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyFriezeGroup { + P1, + P11g, + P1m1, + P2, + P2mg, + P11m, + P2mm, +} +impl PyFriezeGroup { + pub fn to_rust(&self) -> rust_physics_engine::patterns::symmetry::FriezeGroup { match self { + Self::P1 => rust_physics_engine::patterns::symmetry::FriezeGroup::P1, + Self::P11g => rust_physics_engine::patterns::symmetry::FriezeGroup::P11g, + Self::P1m1 => rust_physics_engine::patterns::symmetry::FriezeGroup::P1m1, + Self::P2 => rust_physics_engine::patterns::symmetry::FriezeGroup::P2, + Self::P2mg => rust_physics_engine::patterns::symmetry::FriezeGroup::P2mg, + Self::P11m => rust_physics_engine::patterns::symmetry::FriezeGroup::P11m, + Self::P2mm => rust_physics_engine::patterns::symmetry::FriezeGroup::P2mm, + } } + pub fn from_rust(v: &rust_physics_engine::patterns::symmetry::FriezeGroup) -> Self { match v { + rust_physics_engine::patterns::symmetry::FriezeGroup::P1 => Self::P1, + rust_physics_engine::patterns::symmetry::FriezeGroup::P11g => Self::P11g, + rust_physics_engine::patterns::symmetry::FriezeGroup::P1m1 => Self::P1m1, + rust_physics_engine::patterns::symmetry::FriezeGroup::P2 => Self::P2, + rust_physics_engine::patterns::symmetry::FriezeGroup::P2mg => Self::P2mg, + rust_physics_engine::patterns::symmetry::FriezeGroup::P11m => Self::P11m, + rust_physics_engine::patterns::symmetry::FriezeGroup::P2mm => Self::P2mm, + } } +} +#[pymethods] +impl PyFriezeGroup { + fn __repr__(&self) -> &'static str { + match self { + Self::P1 => "FriezeGroup.P1", + Self::P11g => "FriezeGroup.P11g", + Self::P1m1 => "FriezeGroup.P1m1", + Self::P2 => "FriezeGroup.P2", + Self::P2mg => "FriezeGroup.P2mg", + Self::P11m => "FriezeGroup.P11m", + Self::P2mm => "FriezeGroup.P2mm", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A 2-D lattice spanned by two basis vectors. +/// +/// Rust: `patterns::symmetry::Lattice` +#[pyclass(name = "Lattice", module = "numeria.patterns.symmetry", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyLattice { pub inner: rust_physics_engine::patterns::symmetry::Lattice } +#[pymethods] +impl PyLattice { + /// Builds a `Lattice` from its fields. + #[new] + #[pyo3(signature = (a, b))] + fn __new__(a: crate::generated::types::PyVec2Arg, b: crate::generated::types::PyVec2Arg) -> Self { + let a = a.0; + let b = b.0; + Self { inner: rust_physics_engine::patterns::symmetry::Lattice { a: a, b: b } } + } + + /// + /// Rust: `patterns::symmetry::Lattice::square` + #[pyo3(name = "square")] + #[staticmethod] + #[pyo3(signature = (s))] + fn square(s: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::symmetry::Lattice::square(s)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLattice { inner: __v }) + } + + /// Hexagonal lattice with 120° between the basis vectors. + /// + /// Rust: `patterns::symmetry::Lattice::hexagonal` + #[pyo3(name = "hexagonal")] + #[staticmethod] + #[pyo3(signature = (s))] + fn hexagonal(s: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::symmetry::Lattice::hexagonal(s)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLattice { inner: __v }) + } + + /// + /// Rust: `patterns::symmetry::Lattice::rectangular` + #[pyo3(name = "rectangular")] + #[staticmethod] + #[pyo3(signature = (w, h))] + fn rectangular(w: f64, h: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::symmetry::Lattice::rectangular(w, h)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLattice { inner: __v }) + } + + /// Rhombic lattice: equal-length vectors at the given angle. + /// + /// Rust: `patterns::symmetry::Lattice::rhombic` + #[pyo3(name = "rhombic")] + #[staticmethod] + #[pyo3(signature = (s, angle))] + fn rhombic(s: f64, angle: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::symmetry::Lattice::rhombic(s, angle)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLattice { inner: __v }) + } + + /// + /// Rust: `patterns::symmetry::Lattice::oblique` + #[pyo3(name = "oblique")] + #[staticmethod] + #[pyo3(signature = (a, b))] + fn oblique(a: crate::generated::types::PyVec2Arg, b: crate::generated::types::PyVec2Arg) -> PyResult { + let a = a.0; + let b = b.0; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::symmetry::Lattice::oblique(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLattice { inner: __v }) + } + + /// Lagrange-Gauss reduction: returns an equivalent basis with the + /// two shortest vectors (|a| <= |b|, |b| minimal). + /// + /// Panics: + /// Panics for a degenerate (collinear) basis. + /// + /// Rust: `patterns::symmetry::Lattice::reduce` + #[pyo3(name = "reduce")] + #[pyo3(signature = ())] + fn reduce(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.reduce()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLattice { inner: __v }) + } + + /// World position of lattice coordinates (u, v). + /// + /// Rust: `patterns::symmetry::Lattice::to_world` + #[pyo3(name = "to_world")] + #[pyo3(signature = (p))] + fn to_world(&self, p: crate::generated::types::PyVec2Arg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.to_world(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) + } + + #[getter] + #[pyo3(name = "a")] + fn py_get_a(&self) -> PyResult { Ok(crate::generated::types::PyVec2 { inner: self.inner.a.clone() }) } + + #[getter] + #[pyo3(name = "b")] + fn py_get_b(&self) -> PyResult { Ok(crate::generated::types::PyVec2 { inner: self.inner.b.clone() }) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Lattice", "Lattice", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// 3-D point groups (rotation parts). +/// +/// Rust: `patterns::symmetry::PointGroup3` +#[pyclass(name = "PointGroup3", module = "numeria.patterns.symmetry", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyPointGroup3 { pub inner: rust_physics_engine::patterns::symmetry::PointGroup3 } +#[pymethods] +impl PyPointGroup3 { + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("PointGroup3", "PointGroup3", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// The 17 wallpaper groups. +/// +/// Rust: `patterns::symmetry::WallpaperGroup` +#[pyclass(name = "WallpaperGroup", module = "numeria.patterns.symmetry", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyWallpaperGroup { + P1, + P2, + Pm, + Pg, + Cm, + Pmm, + Pmg, + Pgg, + Cmm, + P4, + P4m, + P4g, + P3, + P3m1, + P31m, + P6, + P6m, +} +impl PyWallpaperGroup { + pub fn to_rust(&self) -> rust_physics_engine::patterns::symmetry::WallpaperGroup { match self { + Self::P1 => rust_physics_engine::patterns::symmetry::WallpaperGroup::P1, + Self::P2 => rust_physics_engine::patterns::symmetry::WallpaperGroup::P2, + Self::Pm => rust_physics_engine::patterns::symmetry::WallpaperGroup::Pm, + Self::Pg => rust_physics_engine::patterns::symmetry::WallpaperGroup::Pg, + Self::Cm => rust_physics_engine::patterns::symmetry::WallpaperGroup::Cm, + Self::Pmm => rust_physics_engine::patterns::symmetry::WallpaperGroup::Pmm, + Self::Pmg => rust_physics_engine::patterns::symmetry::WallpaperGroup::Pmg, + Self::Pgg => rust_physics_engine::patterns::symmetry::WallpaperGroup::Pgg, + Self::Cmm => rust_physics_engine::patterns::symmetry::WallpaperGroup::Cmm, + Self::P4 => rust_physics_engine::patterns::symmetry::WallpaperGroup::P4, + Self::P4m => rust_physics_engine::patterns::symmetry::WallpaperGroup::P4m, + Self::P4g => rust_physics_engine::patterns::symmetry::WallpaperGroup::P4g, + Self::P3 => rust_physics_engine::patterns::symmetry::WallpaperGroup::P3, + Self::P3m1 => rust_physics_engine::patterns::symmetry::WallpaperGroup::P3m1, + Self::P31m => rust_physics_engine::patterns::symmetry::WallpaperGroup::P31m, + Self::P6 => rust_physics_engine::patterns::symmetry::WallpaperGroup::P6, + Self::P6m => rust_physics_engine::patterns::symmetry::WallpaperGroup::P6m, + } } + pub fn from_rust(v: &rust_physics_engine::patterns::symmetry::WallpaperGroup) -> Self { match v { + rust_physics_engine::patterns::symmetry::WallpaperGroup::P1 => Self::P1, + rust_physics_engine::patterns::symmetry::WallpaperGroup::P2 => Self::P2, + rust_physics_engine::patterns::symmetry::WallpaperGroup::Pm => Self::Pm, + rust_physics_engine::patterns::symmetry::WallpaperGroup::Pg => Self::Pg, + rust_physics_engine::patterns::symmetry::WallpaperGroup::Cm => Self::Cm, + rust_physics_engine::patterns::symmetry::WallpaperGroup::Pmm => Self::Pmm, + rust_physics_engine::patterns::symmetry::WallpaperGroup::Pmg => Self::Pmg, + rust_physics_engine::patterns::symmetry::WallpaperGroup::Pgg => Self::Pgg, + rust_physics_engine::patterns::symmetry::WallpaperGroup::Cmm => Self::Cmm, + rust_physics_engine::patterns::symmetry::WallpaperGroup::P4 => Self::P4, + rust_physics_engine::patterns::symmetry::WallpaperGroup::P4m => Self::P4m, + rust_physics_engine::patterns::symmetry::WallpaperGroup::P4g => Self::P4g, + rust_physics_engine::patterns::symmetry::WallpaperGroup::P3 => Self::P3, + rust_physics_engine::patterns::symmetry::WallpaperGroup::P3m1 => Self::P3m1, + rust_physics_engine::patterns::symmetry::WallpaperGroup::P31m => Self::P31m, + rust_physics_engine::patterns::symmetry::WallpaperGroup::P6 => Self::P6, + rust_physics_engine::patterns::symmetry::WallpaperGroup::P6m => Self::P6m, + } } +} +#[pymethods] +impl PyWallpaperGroup { + fn __repr__(&self) -> &'static str { + match self { + Self::P1 => "WallpaperGroup.P1", + Self::P2 => "WallpaperGroup.P2", + Self::Pm => "WallpaperGroup.Pm", + Self::Pg => "WallpaperGroup.Pg", + Self::Cm => "WallpaperGroup.Cm", + Self::Pmm => "WallpaperGroup.Pmm", + Self::Pmg => "WallpaperGroup.Pmg", + Self::Pgg => "WallpaperGroup.Pgg", + Self::Cmm => "WallpaperGroup.Cmm", + Self::P4 => "WallpaperGroup.P4", + Self::P4m => "WallpaperGroup.P4m", + Self::P4g => "WallpaperGroup.P4g", + Self::P3 => "WallpaperGroup.P3", + Self::P3m1 => "WallpaperGroup.P3m1", + Self::P31m => "WallpaperGroup.P31m", + Self::P6 => "WallpaperGroup.P6", + Self::P6m => "WallpaperGroup.P6m", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// The 11 Archimedean (uniform) tilings by vertex configuration. +/// +/// Rust: `patterns::tilings::Archimedean` +#[pyclass(name = "Archimedean", module = "numeria.patterns.tilings", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyArchimedean { + T3_3_3_3_3_3, + T4_4_4_4, + T6_6_6, + T3_3_3_3_6, + T3_3_3_4_4, + T3_3_4_3_4, + T3_4_6_4, + T3_6_3_6, + T3_12_12, + T4_6_12, + T4_8_8, +} +impl PyArchimedean { + pub fn to_rust(&self) -> rust_physics_engine::patterns::tilings::Archimedean { match self { + Self::T3_3_3_3_3_3 => rust_physics_engine::patterns::tilings::Archimedean::T3_3_3_3_3_3, + Self::T4_4_4_4 => rust_physics_engine::patterns::tilings::Archimedean::T4_4_4_4, + Self::T6_6_6 => rust_physics_engine::patterns::tilings::Archimedean::T6_6_6, + Self::T3_3_3_3_6 => rust_physics_engine::patterns::tilings::Archimedean::T3_3_3_3_6, + Self::T3_3_3_4_4 => rust_physics_engine::patterns::tilings::Archimedean::T3_3_3_4_4, + Self::T3_3_4_3_4 => rust_physics_engine::patterns::tilings::Archimedean::T3_3_4_3_4, + Self::T3_4_6_4 => rust_physics_engine::patterns::tilings::Archimedean::T3_4_6_4, + Self::T3_6_3_6 => rust_physics_engine::patterns::tilings::Archimedean::T3_6_3_6, + Self::T3_12_12 => rust_physics_engine::patterns::tilings::Archimedean::T3_12_12, + Self::T4_6_12 => rust_physics_engine::patterns::tilings::Archimedean::T4_6_12, + Self::T4_8_8 => rust_physics_engine::patterns::tilings::Archimedean::T4_8_8, + } } + pub fn from_rust(v: &rust_physics_engine::patterns::tilings::Archimedean) -> Self { match v { + rust_physics_engine::patterns::tilings::Archimedean::T3_3_3_3_3_3 => Self::T3_3_3_3_3_3, + rust_physics_engine::patterns::tilings::Archimedean::T4_4_4_4 => Self::T4_4_4_4, + rust_physics_engine::patterns::tilings::Archimedean::T6_6_6 => Self::T6_6_6, + rust_physics_engine::patterns::tilings::Archimedean::T3_3_3_3_6 => Self::T3_3_3_3_6, + rust_physics_engine::patterns::tilings::Archimedean::T3_3_3_4_4 => Self::T3_3_3_4_4, + rust_physics_engine::patterns::tilings::Archimedean::T3_3_4_3_4 => Self::T3_3_4_3_4, + rust_physics_engine::patterns::tilings::Archimedean::T3_4_6_4 => Self::T3_4_6_4, + rust_physics_engine::patterns::tilings::Archimedean::T3_6_3_6 => Self::T3_6_3_6, + rust_physics_engine::patterns::tilings::Archimedean::T3_12_12 => Self::T3_12_12, + rust_physics_engine::patterns::tilings::Archimedean::T4_6_12 => Self::T4_6_12, + rust_physics_engine::patterns::tilings::Archimedean::T4_8_8 => Self::T4_8_8, + } } +} +#[pymethods] +impl PyArchimedean { + fn __repr__(&self) -> &'static str { + match self { + Self::T3_3_3_3_3_3 => "Archimedean.T3_3_3_3_3_3", + Self::T4_4_4_4 => "Archimedean.T4_4_4_4", + Self::T6_6_6 => "Archimedean.T6_6_6", + Self::T3_3_3_3_6 => "Archimedean.T3_3_3_3_6", + Self::T3_3_3_4_4 => "Archimedean.T3_3_3_4_4", + Self::T3_3_4_3_4 => "Archimedean.T3_3_4_3_4", + Self::T3_4_6_4 => "Archimedean.T3_4_6_4", + Self::T3_6_3_6 => "Archimedean.T3_6_3_6", + Self::T3_12_12 => "Archimedean.T3_12_12", + Self::T4_6_12 => "Archimedean.T4_6_12", + Self::T4_8_8 => "Archimedean.T4_8_8", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Axial hex-grid coordinate (Red Blob Games convention); the third +/// cube coordinate is `s = -q - r`. +/// +/// Rust: `patterns::tilings::Hex` +#[pyclass(name = "Hex", module = "numeria.patterns.tilings", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyHex { pub inner: rust_physics_engine::patterns::tilings::Hex } +#[pymethods] +impl PyHex { + /// + /// Rust: `patterns::tilings::Hex::new` + #[new] + #[pyo3(signature = (q, r))] + fn __new__(q: i32, r: i32) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::tilings::Hex::new(q, r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyHex { inner: __v }) + } + + /// Third cube coordinate. + /// + /// Rust: `patterns::tilings::Hex::s` + #[pyo3(name = "s")] + #[pyo3(signature = ())] + fn s(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.s()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// + /// Rust: `patterns::tilings::Hex::add` + #[pyo3(name = "add")] + #[pyo3(signature = (other))] + fn add(&self, other: crate::generated::types::PyHex) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.add(other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyHex { inner: __v }) + } + + /// + /// Rust: `patterns::tilings::Hex::sub` + #[pyo3(name = "sub")] + #[pyo3(signature = (other))] + fn sub(&self, other: crate::generated::types::PyHex) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.sub(other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyHex { inner: __v }) + } + + /// + /// Rust: `patterns::tilings::Hex::scale` + #[pyo3(name = "scale")] + #[pyo3(signature = (k))] + fn scale(&self, k: i32) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.scale(k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyHex { inner: __v }) + } + + /// The six neighbors, counterclockwise from +q. + /// + /// Rust: `patterns::tilings::Hex::neighbors` + #[pyo3(name = "neighbors")] + #[pyo3(signature = ())] + fn neighbors(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.neighbors()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyHex { inner: __x }).collect::>()) + } + + /// Hex (cube) distance. + /// + /// Rust: `patterns::tilings::Hex::distance` + #[pyo3(name = "distance")] + #[pyo3(signature = (other))] + fn distance(&self, other: crate::generated::types::PyHex) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.distance(other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Center position of the hex cell. + /// + /// Rust: `patterns::tilings::Hex::to_pixel` + #[pyo3(name = "to_pixel")] + #[pyo3(signature = (size, pointy))] + fn to_pixel(&self, size: f64, pointy: bool) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.to_pixel(size, pointy)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) + } + + /// Inverse of `Hex::to_pixel` with cube rounding. + /// + /// Rust: `patterns::tilings::Hex::from_pixel` + #[pyo3(name = "from_pixel")] + #[staticmethod] + #[pyo3(signature = (p, size, pointy))] + fn from_pixel(p: crate::generated::types::PyVec2Arg, size: f64, pointy: bool) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| rust_physics_engine::patterns::tilings::Hex::from_pixel(p, size, pointy)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyHex { inner: __v }) + } + + /// The ring of hexes at exactly the given radius. + /// + /// Panics: + /// Panics for negative radius. + /// + /// Rust: `patterns::tilings::Hex::ring` + #[pyo3(name = "ring")] + #[pyo3(signature = (radius))] + fn ring(&self, radius: i32) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.ring(radius)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyHex { inner: __x }).collect::>()) + } + + /// All hexes within the radius, spiraling outward ring by ring. + /// + /// Panics: + /// Panics for negative radius. + /// + /// Rust: `patterns::tilings::Hex::spiral` + #[pyo3(name = "spiral")] + #[pyo3(signature = (radius))] + fn spiral(&self, radius: i32) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.spiral(radius)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyHex { inner: __x }).collect::>()) + } + + /// Hexes on the line to `other` (inclusive), by cube + /// interpolation and rounding. + /// + /// Rust: `patterns::tilings::Hex::line_to` + #[pyo3(name = "line_to")] + #[pyo3(signature = (other))] + fn line_to(&self, other: crate::generated::types::PyHex) -> PyResult> { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.line_to(other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyHex { inner: __x }).collect::>()) + } + + /// Rotation by 60° counterclockwise about the origin. + /// + /// Rust: `patterns::tilings::Hex::rotate60` + #[pyo3(name = "rotate60")] + #[pyo3(signature = ())] + fn rotate60(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.rotate60()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyHex { inner: __v }) + } + + /// Reflection fixing the q axis. + /// + /// Rust: `patterns::tilings::Hex::reflect_q` + #[pyo3(name = "reflect_q")] + #[pyo3(signature = ())] + fn reflect_q(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.reflect_q()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyHex { inner: __v }) + } + + #[getter] + #[pyo3(name = "q")] + fn py_get_q(&self) -> PyResult { Ok(self.inner.q) } + + #[setter] + #[pyo3(name = "q")] + fn py_set_q(&mut self, v: i32) { self.inner.q = v; } + + #[getter] + #[pyo3(name = "r")] + fn py_get_r(&self) -> PyResult { Ok(self.inner.r) } + + #[setter] + #[pyo3(name = "r")] + fn py_set_r(&mut self, v: i32) { self.inner.r = v; } + + fn __repr__(&self) -> String { format!("Hex(q={:?}, r={:?})", self.inner.q, self.inner.r) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A tiling as an indexed face set: `faces` are counterclockwise +/// vertex loops, `edges` the unique undirected edges. +/// +/// Rust: `patterns::tilings::Tiling` +#[pyclass(name = "Tiling", module = "numeria.patterns.tilings", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyTiling { pub inner: rust_physics_engine::patterns::tilings::Tiling } +#[pymethods] +impl PyTiling { + /// Builds a `Tiling` from its fields. + #[new] + #[pyo3(signature = (vertices, faces, edges))] + fn __new__(vertices: Vec, faces: Vec>, edges: Vec<(usize, usize)>) -> Self { + let vertices = vertices.into_iter().map(|__e| __e.0).collect::>(); + let edges = edges.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + Self { inner: rust_physics_engine::patterns::tilings::Tiling { vertices: vertices, faces: faces, edges: edges } } + } + + /// Keeps only faces whose vertices all lie inside the rectangle + /// (closed), reindexing vertices. + /// + /// Rust: `patterns::tilings::Tiling::clip_to_rect` + #[pyo3(name = "clip_to_rect")] + #[pyo3(signature = (rect))] + fn clip_to_rect(&self, rect: crate::generated::types::PyRect) -> PyResult { + let rect = rect.inner; + let __r = crate::runtime::guard(|| self.inner.clip_to_rect(&rect)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTiling { inner: __v }) + } + + /// Faces as polygons. + /// + /// Rust: `patterns::tilings::Tiling::polygons` + #[pyo3(name = "polygons")] + #[pyo3(signature = ())] + fn polygons(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.polygons()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyPolygon2 { inner: __x }).collect::>()) + } + + /// Centroid of every face. + /// + /// Rust: `patterns::tilings::Tiling::face_centroids` + #[pyo3(name = "face_centroids")] + #[pyo3(signature = ())] + fn face_centroids(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.face_centroids()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) + } + + /// Dual tiling: one vertex per face (at its centroid), one face + /// per interior tiling vertex (a vertex is interior when its + /// incident face angles sum to 2π). Boundary vertices produce no + /// dual face. + /// + /// Rust: `patterns::tilings::Tiling::dual` + #[pyo3(name = "dual")] + #[pyo3(signature = ())] + fn dual(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.dual()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyTiling { inner: __v }) + } + + #[getter] + #[pyo3(name = "vertices")] + fn py_get_vertices(&self) -> PyResult> { Ok(self.inner.vertices.clone().into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) } + + #[getter] + #[pyo3(name = "faces")] + fn py_get_faces(&self) -> PyResult>> { Ok(self.inner.faces.clone()) } + + #[getter] + #[pyo3(name = "edges")] + fn py_get_edges(&self) -> PyResult> { Ok(self.inner.edges.clone().into_iter().map(|__x| (__x.0, __x.1)).collect::>()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Tiling", "Tiling", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} diff --git a/bindings/python/src/generated/types/quantum.rs b/bindings/python/src/generated/types/quantum.rs new file mode 100644 index 0000000..4d05c85 --- /dev/null +++ b/bindings/python/src/generated/types/quantum.rs @@ -0,0 +1,1768 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; + + +/// A sequence of operations on a fixed number of qubits. +/// +/// Rust: `quantum::circuit::Circuit` +#[pyclass(name = "Circuit", module = "numeria.quantum.circuit", from_py_object)] +#[derive(Clone)] +pub struct PyCircuit { pub inner: rust_physics_engine::quantum::circuit::Circuit } +#[pymethods] +impl PyCircuit { + /// An empty circuit. + /// + /// Errors: + /// Returns an error for a bad qubit count. + /// + /// Rust: `quantum::circuit::Circuit::new` + #[new] + #[pyo3(signature = (n))] + fn __new__(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::circuit::Circuit::new(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyCircuit { inner: __v }) + } + + /// Appends a one-qubit gate. + /// + /// Rust: `quantum::circuit::Circuit::gate` + #[pyo3(name = "gate")] + #[pyo3(signature = (q, gate))] + fn gate<'py>(mut slf: pyo3::PyRefMut<'py, Self>, q: usize, gate: crate::generated::types::PyGate) -> PyResult> { + let gate = gate.inner; + let __r = crate::runtime::guard(|| { slf.inner.gate(q, gate); }); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(slf) + } + + /// Appends an X. + /// + /// Rust: `quantum::circuit::Circuit::x` + #[pyo3(name = "x")] + #[pyo3(signature = (q))] + fn x<'py>(mut slf: pyo3::PyRefMut<'py, Self>, q: usize) -> PyResult> { + let __r = crate::runtime::guard(|| { slf.inner.x(q); }); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(slf) + } + + /// Appends a Y. + /// + /// Rust: `quantum::circuit::Circuit::y` + #[pyo3(name = "y")] + #[pyo3(signature = (q))] + fn y<'py>(mut slf: pyo3::PyRefMut<'py, Self>, q: usize) -> PyResult> { + let __r = crate::runtime::guard(|| { slf.inner.y(q); }); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(slf) + } + + /// Appends a Z. + /// + /// Rust: `quantum::circuit::Circuit::z` + #[pyo3(name = "z")] + #[pyo3(signature = (q))] + fn z<'py>(mut slf: pyo3::PyRefMut<'py, Self>, q: usize) -> PyResult> { + let __r = crate::runtime::guard(|| { slf.inner.z(q); }); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(slf) + } + + /// Appends a Hadamard. + /// + /// Rust: `quantum::circuit::Circuit::h` + #[pyo3(name = "h")] + #[pyo3(signature = (q))] + fn h<'py>(mut slf: pyo3::PyRefMut<'py, Self>, q: usize) -> PyResult> { + let __r = crate::runtime::guard(|| { slf.inner.h(q); }); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(slf) + } + + /// Appends an X rotation. + /// + /// Rust: `quantum::circuit::Circuit::rx` + #[pyo3(name = "rx")] + #[pyo3(signature = (q, theta))] + fn rx<'py>(mut slf: pyo3::PyRefMut<'py, Self>, q: usize, theta: f64) -> PyResult> { + let __r = crate::runtime::guard(|| { slf.inner.rx(q, theta); }); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(slf) + } + + /// Appends a Y rotation. + /// + /// Rust: `quantum::circuit::Circuit::ry` + #[pyo3(name = "ry")] + #[pyo3(signature = (q, theta))] + fn ry<'py>(mut slf: pyo3::PyRefMut<'py, Self>, q: usize, theta: f64) -> PyResult> { + let __r = crate::runtime::guard(|| { slf.inner.ry(q, theta); }); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(slf) + } + + /// Appends a Z rotation. + /// + /// Rust: `quantum::circuit::Circuit::rz` + #[pyo3(name = "rz")] + #[pyo3(signature = (q, theta))] + fn rz<'py>(mut slf: pyo3::PyRefMut<'py, Self>, q: usize, theta: f64) -> PyResult> { + let __r = crate::runtime::guard(|| { slf.inner.rz(q, theta); }); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(slf) + } + + /// Appends a phase. + /// + /// Rust: `quantum::circuit::Circuit::phase` + #[pyo3(name = "phase")] + #[pyo3(signature = (q, phi))] + fn phase<'py>(mut slf: pyo3::PyRefMut<'py, Self>, q: usize, phi: f64) -> PyResult> { + let __r = crate::runtime::guard(|| { slf.inner.phase(q, phi); }); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(slf) + } + + /// Appends a controlled NOT. + /// + /// Rust: `quantum::circuit::Circuit::cx` + #[pyo3(name = "cx")] + #[pyo3(signature = (control, target))] + fn cx<'py>(mut slf: pyo3::PyRefMut<'py, Self>, control: usize, target: usize) -> PyResult> { + let __r = crate::runtime::guard(|| { slf.inner.cx(control, target); }); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(slf) + } + + /// Appends a controlled Z. + /// + /// Rust: `quantum::circuit::Circuit::cz` + #[pyo3(name = "cz")] + #[pyo3(signature = (control, target))] + fn cz<'py>(mut slf: pyo3::PyRefMut<'py, Self>, control: usize, target: usize) -> PyResult> { + let __r = crate::runtime::guard(|| { slf.inner.cz(control, target); }); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(slf) + } + + /// Appends a controlled phase. + /// + /// Rust: `quantum::circuit::Circuit::cphase` + #[pyo3(name = "cphase")] + #[pyo3(signature = (control, target, phi))] + fn cphase<'py>(mut slf: pyo3::PyRefMut<'py, Self>, control: usize, target: usize, phi: f64) -> PyResult> { + let __r = crate::runtime::guard(|| { slf.inner.cphase(control, target, phi); }); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(slf) + } + + /// Appends a Toffoli. + /// + /// Rust: `quantum::circuit::Circuit::ccx` + #[pyo3(name = "ccx")] + #[pyo3(signature = (a, b, target))] + fn ccx<'py>(mut slf: pyo3::PyRefMut<'py, Self>, a: usize, b: usize, target: usize) -> PyResult> { + let __r = crate::runtime::guard(|| { slf.inner.ccx(a, b, target); }); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(slf) + } + + /// Appends a swap. + /// + /// Rust: `quantum::circuit::Circuit::swap` + #[pyo3(name = "swap")] + #[pyo3(signature = (a, b))] + fn swap<'py>(mut slf: pyo3::PyRefMut<'py, Self>, a: usize, b: usize) -> PyResult> { + let __r = crate::runtime::guard(|| { slf.inner.swap(a, b); }); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(slf) + } + + /// Appends a barrier. + /// + /// Rust: `quantum::circuit::Circuit::barrier` + #[pyo3(name = "barrier")] + #[pyo3(signature = ())] + fn barrier<'py>(mut slf: pyo3::PyRefMut<'py, Self>) -> PyResult> { + let __r = crate::runtime::guard(|| { slf.inner.barrier(); }); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(slf) + } + + /// Appends another circuit's operations. + /// + /// Errors: + /// Returns an error if the widths disagree. + /// + /// Rust: `quantum::circuit::Circuit::append` + #[pyo3(name = "append")] + #[pyo3(signature = (other))] + fn append<'py>(mut slf: pyo3::PyRefMut<'py, Self>, other: crate::generated::types::PyCircuit) -> PyResult> { + let other = other.inner; + let __r = crate::runtime::guard(|| slf.inner.append(&other).map(|_| ())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(slf) + } + + /// The inverse circuit: every gate adjointed, in reverse order. + /// + /// Reversing without adjointing, or adjointing without reversing, is the + /// classic error and gives the identity only for circuits of self-inverse + /// gates -- which is most textbook examples, so it survives casual + /// testing. + /// + /// Rust: `quantum::circuit::Circuit::inverse` + #[pyo3(name = "inverse")] + #[pyo3(signature = ())] + fn inverse(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.inverse()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCircuit { inner: __v }) + } + + /// The number of gates, ignoring barriers. + /// + /// Rust: `quantum::circuit::Circuit::gate_count` + #[pyo3(name = "gate_count")] + #[pyo3(signature = ())] + fn gate_count(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.gate_count()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The circuit depth: the number of layers when gates on disjoint qubits + /// are packed together. + /// + /// Depth rather than gate count is what sets the runtime on hardware, + /// because gates on disjoint qubits run at once, and it is what a + /// coherence time has to be compared against. + /// + /// Rust: `quantum::circuit::Circuit::depth` + #[pyo3(name = "depth")] + #[pyo3(signature = ())] + fn depth(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.depth()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Runs the circuit on a state. + /// + /// Errors: + /// Returns an error if the state has the wrong width or an operation + /// names a bad qubit. + /// + /// Rust: `quantum::circuit::Circuit::run` + #[pyo3(name = "run")] + #[pyo3(signature = (initial))] + fn run(&self, initial: crate::generated::types::PyQState) -> PyResult { + let initial = initial.inner; + let __r = crate::runtime::guard(|| self.inner.run(&initial)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyQState { inner: __v }) + } + + /// Runs from the all-zeros state and samples measurements. + /// + /// Errors: + /// Returns an error if the circuit cannot run. + /// + /// Rust: `quantum::circuit::Circuit::run_shots` + #[pyo3(name = "run_shots")] + #[pyo3(signature = (shots, rng))] + fn run_shots(&self, shots: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| self.inner.run_shots(shots, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) + } + + /// The full unitary, for small circuits. + /// + /// Costs `4^n` amplitudes, so it is capped at ten qubits. It is built by + /// running the circuit on each basis state in turn, which makes each + /// column the image of one basis vector -- the definition of the matrix. + /// + /// Errors: + /// Returns an error above ten qubits, or if the circuit cannot run. + /// + /// Rust: `quantum::circuit::Circuit::unitary_small` + #[pyo3(name = "unitary_small")] + #[pyo3(signature = ())] + fn unitary_small<'py>(&self, py: Python<'py>) -> PyResult>>> { + let __r = crate::runtime::guard(|| self.inner.unitary_small()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| __x.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()).collect::>()) + } + + /// A compact textual form, one line per operation. + /// + /// Rust: `quantum::circuit::Circuit::to_qasm_lite` + #[pyo3(name = "to_qasm_lite")] + #[pyo3(signature = ())] + fn to_qasm_lite(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.to_qasm_lite()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_string()) + } + + /// An ASCII diagram, one row per qubit. + /// + /// Rust: `quantum::circuit::Circuit::draw_ascii` + #[pyo3(name = "draw_ascii")] + #[pyo3(signature = ())] + fn draw_ascii(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.draw_ascii()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_string()) + } + + #[getter] + #[pyo3(name = "n")] + fn py_get_n(&self) -> PyResult { Ok(self.inner.n) } + + #[setter] + #[pyo3(name = "n")] + fn py_set_n(&mut self, v: usize) { self.inner.n = v; } + + #[getter] + #[pyo3(name = "ops")] + fn py_get_ops(&self) -> PyResult> { Ok(self.inner.ops.clone().into_iter().map(|__x| crate::generated::types::PyOp { inner: __x }).collect::>()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Circuit", "Circuit", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A mixed state of `n` qubits. +/// +/// Rust: `quantum::circuit::DensityMatrix` +#[pyclass(name = "DensityMatrix", module = "numeria.quantum.circuit", from_py_object)] +#[derive(Clone)] +pub struct PyDensityMatrix { pub inner: rust_physics_engine::quantum::circuit::DensityMatrix } +#[pymethods] +impl PyDensityMatrix { + /// Builds a `DensityMatrix` from its fields. + #[new] + #[pyo3(signature = (n, rho))] + fn __new__(n: usize, rho: Vec>) -> Self { + let rho = rho.into_iter().map(|__e| __e.into_iter().map(|__e| __e.0).collect::>()).collect::>(); + Self { inner: rust_physics_engine::quantum::circuit::DensityMatrix { n: n, rho: rho } } + } + + /// The density matrix of a pure state. + /// + /// Rust: `quantum::circuit::DensityMatrix::from_state` + #[pyo3(name = "from_state")] + #[staticmethod] + #[pyo3(signature = (state))] + fn from_state(state: crate::generated::types::PyQState) -> PyResult { + let state = state.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::circuit::DensityMatrix::from_state(&state)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyDensityMatrix { inner: __v }) + } + + /// A classical mixture of states. + /// + /// The distinction from a superposition is the whole of the difference + /// between quantum and classical uncertainty: a mixture of `|0>` and + /// `|1>` is diagonal and behaves like a coin, while their superposition + /// has off-diagonal terms and interferes. + /// + /// Errors: + /// Returns an error for mismatched lengths, differing widths, negative + /// weights, or weights that do not sum to one. + /// + /// Rust: `quantum::circuit::DensityMatrix::from_mixture` + #[pyo3(name = "from_mixture")] + #[staticmethod] + #[pyo3(signature = (states, weights))] + fn from_mixture(states: Vec, weights: Vec) -> PyResult { + let states = states.into_iter().map(|__e| __e.inner).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::circuit::DensityMatrix::from_mixture(&states, &weights)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyDensityMatrix { inner: __v }) + } + + /// The trace. + /// + /// Rust: `quantum::circuit::DensityMatrix::trace` + #[pyo3(name = "trace")] + #[pyo3(signature = ())] + fn trace<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.trace()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::complex_out(py, __v)) + } + + /// The purity `tr(rho^2)`: one for a pure state, `1 / d` for the maximally + /// mixed one. + /// + /// Rust: `quantum::circuit::DensityMatrix::purity` + #[pyo3(name = "purity")] + #[pyo3(signature = ())] + fn purity(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.purity()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The von Neumann entropy in bits. + /// + /// Errors: + /// Returns an error if the eigenproblem fails. + /// + /// Rust: `quantum::circuit::DensityMatrix::von_neumann_entropy` + #[pyo3(name = "von_neumann_entropy")] + #[pyo3(signature = ())] + fn von_neumann_entropy(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.von_neumann_entropy()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) + } + + /// Whether the matrix is Hermitian, unit trace, and positive + /// semi-definite -- the three conditions that make it a state. + /// + /// Rust: `quantum::circuit::DensityMatrix::is_valid` + #[pyo3(name = "is_valid")] + #[pyo3(signature = (tol))] + fn is_valid(&self, tol: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_valid(tol)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Applies a one-qubit gate by conjugation. + /// + /// Errors: + /// Returns an error if the qubit index is out of range. + /// + /// Rust: `quantum::circuit::DensityMatrix::apply_gate` + #[pyo3(name = "apply_gate")] + #[pyo3(signature = (q, gate))] + fn apply_gate(&mut self, q: usize, gate: crate::generated::types::PyGate) -> PyResult<()> { + let gate = gate.inner; + let __r = crate::runtime::guard(|| self.inner.apply_gate(q, &gate)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(()) + } + + /// Applies a quantum channel given by its Kraus operators. + /// + /// The Kraus form is what makes noise tractable: any physical evolution of + /// an open system, however complicated the environment, is + /// `sum_k K_k rho K_k^dagger` for some finite set of operators satisfying + /// `sum_k K_k^dagger K_k = I`. That completeness condition is exactly + /// trace preservation, which is why a channel cannot lose probability. + /// + /// Errors: + /// Returns an error for the wrong operator size or a set that is not + /// trace preserving. + /// + /// Rust: `quantum::circuit::DensityMatrix::apply_channel` + #[pyo3(name = "apply_channel")] + #[pyo3(signature = (kraus))] + fn apply_channel<'py>(&mut self, py: Python<'py>, kraus: Vec>>) -> PyResult<()> { + let kraus = kraus.into_iter().map(|__e| __e.into_iter().map(|__e| __e.into_iter().map(|__e| __e.0).collect::>()).collect::>()).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || self.inner.apply_channel(&kraus))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(()) + } + + /// Traces out every qubit but the kept ones. + /// + /// Errors: + /// Returns an error for a repeated or out-of-range index. + /// + /// Rust: `quantum::circuit::DensityMatrix::partial_trace` + #[pyo3(name = "partial_trace")] + #[pyo3(signature = (keep))] + fn partial_trace(&self, keep: Vec) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.partial_trace(&keep)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyDensityMatrix { inner: __v }) + } + + #[getter] + #[pyo3(name = "n")] + fn py_get_n(&self) -> PyResult { Ok(self.inner.n) } + + #[setter] + #[pyo3(name = "n")] + fn py_set_n(&mut self, v: usize) { self.inner.n = v; } + + #[getter] + #[pyo3(name = "rho")] + fn py_get_rho<'py>(&self, py: Python<'py>) -> PyResult>>> { Ok(self.inner.rho.clone().into_iter().map(|__x| __x.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()).collect::>()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("DensityMatrix", "DensityMatrix", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A one-qubit gate: a two-by-two unitary. +/// +/// Rust: `quantum::circuit::Gate` +#[pyclass(name = "Gate", module = "numeria.quantum.circuit", from_py_object)] +#[derive(Clone)] +pub struct PyGate { pub inner: rust_physics_engine::quantum::circuit::Gate } +#[pymethods] +impl PyGate { + /// Whether `U^dagger U` is the identity to the given tolerance. + /// + /// Rust: `quantum::circuit::Gate::is_unitary` + #[pyo3(name = "is_unitary")] + #[pyo3(signature = (tol))] + fn is_unitary(&self, tol: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_unitary(tol)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The adjoint, which is also the inverse. + /// + /// Rust: `quantum::circuit::Gate::dagger` + #[pyo3(name = "dagger")] + #[pyo3(signature = ())] + fn dagger(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.dagger()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGate { inner: __v }) + } + + /// The identity. + /// + /// Rust: `quantum::circuit::Gate::identity` + #[pyo3(name = "identity")] + #[staticmethod] + #[pyo3(signature = ())] + fn identity() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::circuit::Gate::identity()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGate { inner: __v }) + } + + /// The Pauli X, or bit flip. + /// + /// Rust: `quantum::circuit::Gate::x` + #[pyo3(name = "x")] + #[staticmethod] + #[pyo3(signature = ())] + fn x() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::circuit::Gate::x()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGate { inner: __v }) + } + + /// The Pauli Y. + /// + /// Rust: `quantum::circuit::Gate::y` + #[pyo3(name = "y")] + #[staticmethod] + #[pyo3(signature = ())] + fn y() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::circuit::Gate::y()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGate { inner: __v }) + } + + /// The Pauli Z, or phase flip. + /// + /// Rust: `quantum::circuit::Gate::z` + #[pyo3(name = "z")] + #[staticmethod] + #[pyo3(signature = ())] + fn z() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::circuit::Gate::z()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGate { inner: __v }) + } + + /// The Hadamard. + /// + /// Rust: `quantum::circuit::Gate::h` + #[pyo3(name = "h")] + #[staticmethod] + #[pyo3(signature = ())] + fn h() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::circuit::Gate::h()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGate { inner: __v }) + } + + /// The phase gate `S`. + /// + /// Rust: `quantum::circuit::Gate::s` + #[pyo3(name = "s")] + #[staticmethod] + #[pyo3(signature = ())] + fn s() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::circuit::Gate::s()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGate { inner: __v }) + } + + /// The inverse of `S`. + /// + /// Rust: `quantum::circuit::Gate::sdg` + #[pyo3(name = "sdg")] + #[staticmethod] + #[pyo3(signature = ())] + fn sdg() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::circuit::Gate::sdg()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGate { inner: __v }) + } + + /// The `T` gate, an eighth turn about `Z`. + /// + /// Rust: `quantum::circuit::Gate::t` + #[pyo3(name = "t")] + #[staticmethod] + #[pyo3(signature = ())] + fn t() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::circuit::Gate::t()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGate { inner: __v }) + } + + /// The inverse of `T`. + /// + /// Rust: `quantum::circuit::Gate::tdg` + #[pyo3(name = "tdg")] + #[staticmethod] + #[pyo3(signature = ())] + fn tdg() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::circuit::Gate::tdg()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGate { inner: __v }) + } + + /// A rotation about `X`. + /// + /// Rust: `quantum::circuit::Gate::rx` + #[pyo3(name = "rx")] + #[staticmethod] + #[pyo3(signature = (theta))] + fn rx(theta: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::circuit::Gate::rx(theta)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGate { inner: __v }) + } + + /// A rotation about `Y`. + /// + /// Rust: `quantum::circuit::Gate::ry` + #[pyo3(name = "ry")] + #[staticmethod] + #[pyo3(signature = (theta))] + fn ry(theta: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::circuit::Gate::ry(theta)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGate { inner: __v }) + } + + /// A rotation about `Z`. + /// + /// Rust: `quantum::circuit::Gate::rz` + #[pyo3(name = "rz")] + #[staticmethod] + #[pyo3(signature = (theta))] + fn rz(theta: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::circuit::Gate::rz(theta)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGate { inner: __v }) + } + + /// A relative phase on the one state. + /// + /// Rust: `quantum::circuit::Gate::phase` + #[pyo3(name = "phase")] + #[staticmethod] + #[pyo3(signature = (phi))] + fn phase(phi: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::circuit::Gate::phase(phi)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGate { inner: __v }) + } + + /// The general one-qubit gate. + /// + /// Every one-qubit unitary is this up to a global phase, which is the + /// content of the Euler decomposition: three real parameters, because the + /// group is three dimensional once the phase is quotiented out. + /// + /// Rust: `quantum::circuit::Gate::u3` + #[pyo3(name = "u3")] + #[staticmethod] + #[pyo3(signature = (theta, phi, lambda_))] + fn u3(theta: f64, phi: f64, lambda_: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::circuit::Gate::u3(theta, phi, lambda_)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGate { inner: __v }) + } + + /// The square root of `X`. + /// + /// Rust: `quantum::circuit::Gate::sqrt_x` + #[pyo3(name = "sqrt_x")] + #[staticmethod] + #[pyo3(signature = ())] + fn sqrt_x() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::circuit::Gate::sqrt_x()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGate { inner: __v }) + } + + #[getter] + #[pyo3(name = "matrix")] + fn py_get_matrix<'py>(&self, py: Python<'py>) -> PyResult>>> { Ok(self.inner.matrix.clone().into_iter().map(|__x| __x.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()).collect::>()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Gate", "Gate", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// One instruction in a circuit. +/// +/// Rust: `quantum::circuit::Op` +#[pyclass(name = "Op", module = "numeria.quantum.circuit", from_py_object)] +#[derive(Clone)] +pub struct PyOp { pub inner: rust_physics_engine::quantum::circuit::Op } +#[pymethods] +impl PyOp { + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Op", "Op", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A pure state of `n` qubits, as `2^n` amplitudes. +/// +/// Rust: `quantum::circuit::QState` +#[pyclass(name = "QState", module = "numeria.quantum.circuit", from_py_object)] +#[derive(Clone)] +pub struct PyQState { pub inner: rust_physics_engine::quantum::circuit::QState } +#[pymethods] +impl PyQState { + /// Builds a `QState` from its fields. + #[new] + #[pyo3(signature = (n, amps))] + fn __new__(n: usize, amps: Vec) -> Self { + let amps = amps.into_iter().map(|__e| __e.0).collect::>(); + Self { inner: rust_physics_engine::quantum::circuit::QState { n: n, amps: amps } } + } + + /// The all-zeros computational basis state. + /// + /// Errors: + /// Returns an error for zero qubits or more than `MAX_QUBITS` (26). + /// + /// Rust: `quantum::circuit::QState::zero` + #[pyo3(name = "zero")] + #[staticmethod] + #[pyo3(signature = (n))] + fn zero(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::circuit::QState::zero(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyQState { inner: __v }) + } + + /// A computational basis state. + /// + /// Errors: + /// Returns an error for a bad qubit count or an out-of-range index. + /// + /// Rust: `quantum::circuit::QState::basis` + #[pyo3(name = "basis")] + #[staticmethod] + #[pyo3(signature = (n, index))] + fn basis(n: usize, index: u64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::circuit::QState::basis(n, index)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyQState { inner: __v }) + } + + /// A state from explicit amplitudes, normalised on the way in. + /// + /// Errors: + /// Returns an error unless the length is a power of two in range, and the + /// amplitudes are not all zero. + /// + /// Rust: `quantum::circuit::QState::from_amps` + #[pyo3(name = "from_amps")] + #[staticmethod] + #[pyo3(signature = (amps))] + fn from_amps(amps: Vec) -> PyResult { + let amps = amps.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::circuit::QState::from_amps(amps)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyQState { inner: __v }) + } + + /// The equal superposition over every basis state. + /// + /// Errors: + /// Returns an error for a bad qubit count. + /// + /// Rust: `quantum::circuit::QState::plus_all` + #[pyo3(name = "plus_all")] + #[staticmethod] + #[pyo3(signature = (n))] + fn plus_all(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::circuit::QState::plus_all(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyQState { inner: __v }) + } + + /// The number of amplitudes. + /// + /// Rust: `quantum::circuit::QState::len` + #[pyo3(name = "len")] + #[pyo3(signature = ())] + fn len(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.len()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Always false: a state always has at least one qubit. + /// + /// Rust: `quantum::circuit::QState::is_empty` + #[pyo3(name = "is_empty")] + #[pyo3(signature = ())] + fn is_empty(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_empty()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The Euclidean norm. + /// + /// Rust: `quantum::circuit::QState::norm` + #[pyo3(name = "norm")] + #[pyo3(signature = ())] + fn norm(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.norm()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Rescales to unit norm, leaving a zero state alone. + /// + /// Rust: `quantum::circuit::QState::normalize` + #[pyo3(name = "normalize")] + #[pyo3(signature = ())] + fn normalize(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.normalize()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// The probability of a basis outcome. + /// + /// Rust: `quantum::circuit::QState::probability` + #[pyo3(name = "probability")] + #[pyo3(signature = (index))] + fn probability(&self, index: u64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.probability(index)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Every outcome probability. + /// + /// Rust: `quantum::circuit::QState::probabilities` + #[pyo3(name = "probabilities")] + #[pyo3(signature = ())] + fn probabilities<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.probabilities())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Samples one measurement of every qubit, returning the outcome as bits. + /// + /// Rust: `quantum::circuit::QState::measure_all` + #[pyo3(name = "measure_all")] + #[pyo3(signature = (rng))] + fn measure_all(&self, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| self.inner.measure_all(&mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Measures one qubit, returning the outcome and the collapsed state. + /// + /// The collapse is the projection onto the observed outcome, renormalised. + /// Note what survives: the *other* qubits keep whatever correlations they + /// had with this one, which is why measuring half of a Bell pair + /// determines the other half. + /// + /// Errors: + /// Returns an error if the qubit index is out of range. + /// + /// Rust: `quantum::circuit::QState::measure_qubit` + #[pyo3(name = "measure_qubit")] + #[pyo3(signature = (q, rng))] + fn measure_qubit(&self, q: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(bool, crate::generated::types::PyQState)> { + let mut rng = rng; + let __r = crate::runtime::guard(|| self.inner.measure_qubit(q, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, crate::generated::types::PyQState { inner: __v.1 })) + } + + /// Repeated measurement, returning `(outcome, count)` pairs sorted by + /// outcome. + /// + /// Rust: `quantum::circuit::QState::sample_counts` + #[pyo3(name = "sample_counts")] + #[pyo3(signature = (shots, rng))] + fn sample_counts(&self, shots: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| self.inner.sample_counts(shots, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) + } + + /// The expectation of `Z` on one qubit. + /// + /// Errors: + /// Returns an error if the qubit index is out of range. + /// + /// Rust: `quantum::circuit::QState::expectation_z` + #[pyo3(name = "expectation_z")] + #[pyo3(signature = (q))] + fn expectation_z(&self, q: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.expectation_z(q)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) + } + + /// The expectation of a Pauli string such as `"XIZY"`, whose leftmost + /// character is the highest-numbered qubit. + /// + /// Measuring a Pauli string is the primitive every variational algorithm + /// is built on, because any Hermitian operator decomposes into them. + /// + /// Errors: + /// Returns an error if the string has the wrong length or an unknown + /// character. + /// + /// Rust: `quantum::circuit::QState::expectation_pauli_string` + #[pyo3(name = "expectation_pauli_string")] + #[pyo3(signature = (pauli))] + fn expectation_pauli_string(&self, pauli: String) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.expectation_pauli_string(&pauli)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) + } + + /// The inner product ``. + /// + /// Errors: + /// Returns an error if the two states have different sizes. + /// + /// Rust: `quantum::circuit::QState::inner` + #[pyo3(name = "inner")] + #[pyo3(signature = (other))] + fn inner<'py>(&self, py: Python<'py>, other: crate::generated::types::PyQState) -> PyResult> { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.inner(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::runtime::coerce::complex_out(py, __v)) + } + + /// The fidelity `||^2`. + /// + /// Errors: + /// Returns an error if the two states have different sizes. + /// + /// Rust: `quantum::circuit::QState::fidelity` + #[pyo3(name = "fidelity")] + #[pyo3(signature = (other))] + fn fidelity(&self, other: crate::generated::types::PyQState) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.fidelity(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) + } + + /// Applies a one-qubit gate in place. + /// + /// Errors: + /// Returns an error if the qubit index is out of range. + /// + /// Rust: `quantum::circuit::QState::apply_single` + #[pyo3(name = "apply_single")] + #[pyo3(signature = (q, gate))] + fn apply_single(&mut self, q: usize, gate: crate::generated::types::PyGate) -> PyResult<()> { + let gate = gate.inner; + let __r = crate::runtime::guard(|| self.inner.apply_single(q, &gate)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(()) + } + + /// Applies a one-qubit gate conditioned on a control qubit. + /// + /// Errors: + /// Returns an error if either index is out of range, or they coincide. + /// + /// Rust: `quantum::circuit::QState::apply_controlled` + #[pyo3(name = "apply_controlled")] + #[pyo3(signature = (control, target, gate))] + fn apply_controlled(&mut self, control: usize, target: usize, gate: crate::generated::types::PyGate) -> PyResult<()> { + let gate = gate.inner; + let __r = crate::runtime::guard(|| self.inner.apply_controlled(control, target, &gate)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(()) + } + + /// The Toffoli gate. + /// + /// Errors: + /// Returns an error if any index is out of range or two coincide. + /// + /// Rust: `quantum::circuit::QState::apply_ccx` + #[pyo3(name = "apply_ccx")] + #[pyo3(signature = (a, b, target))] + fn apply_ccx(&mut self, a: usize, b: usize, target: usize) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.apply_ccx(a, b, target)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(()) + } + + /// Exchanges two qubits. + /// + /// Errors: + /// Returns an error if an index is out of range. + /// + /// Rust: `quantum::circuit::QState::apply_swap` + #[pyo3(name = "apply_swap")] + #[pyo3(signature = (a, b))] + fn apply_swap(&mut self, a: usize, b: usize) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.apply_swap(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(()) + } + + /// The reduced density matrix over the kept qubits, tracing out the rest. + /// + /// Errors: + /// Returns an error for a repeated or out-of-range index, or an empty + /// selection. + /// + /// Rust: `quantum::circuit::QState::reduced_density_matrix` + #[pyo3(name = "reduced_density_matrix")] + #[pyo3(signature = (keep))] + fn reduced_density_matrix<'py>(&self, py: Python<'py>, keep: Vec) -> PyResult>>> { + let __r = crate::runtime::guard(|| self.inner.reduced_density_matrix(&keep)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| __x.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()).collect::>()) + } + + /// The Schmidt coefficients across a bipartition: the square roots of the + /// reduced density matrix's eigenvalues, descending. + /// + /// Errors: + /// Returns an error for a bad partition or an eigensolver failure. + /// + /// Rust: `quantum::circuit::QState::schmidt_coefficients` + #[pyo3(name = "schmidt_coefficients")] + #[pyo3(signature = (partition))] + fn schmidt_coefficients<'py>(&self, py: Python<'py>, partition: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.schmidt_coefficients(&partition))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) + } + + /// The entanglement entropy across a bipartition, in bits. + /// + /// Zero exactly when the state factorises across the cut, and maximal -- + /// one bit per qubit of the smaller side -- for a maximally entangled + /// state. It is symmetric between the two sides, which is not obvious and + /// is the reason it is a property of the *cut* rather than of either + /// piece. + /// + /// Errors: + /// Returns an error for a bad partition. + /// + /// Rust: `quantum::circuit::QState::entanglement_entropy` + #[pyo3(name = "entanglement_entropy")] + #[pyo3(signature = (partition))] + fn entanglement_entropy<'py>(&self, py: Python<'py>, partition: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.entanglement_entropy(&partition))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) + } + + /// The Bloch vector of one qubit, as `(x, y, z)`. + /// + /// Its length is one exactly when that qubit is in a pure state, so it + /// shortens as the qubit becomes entangled with the others -- the + /// geometric statement of monogamy. + /// + /// Errors: + /// Returns an error if the qubit index is out of range. + /// + /// Rust: `quantum::circuit::QState::bloch_vector` + #[pyo3(name = "bloch_vector")] + #[pyo3(signature = (q))] + fn bloch_vector(&self, q: usize) -> PyResult<(f64, f64, f64)> { + let __r = crate::runtime::guard(|| self.inner.bloch_vector(q)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1, __v.2)) + } + + #[getter] + #[pyo3(name = "n")] + fn py_get_n(&self) -> PyResult { Ok(self.inner.n) } + + #[setter] + #[pyo3(name = "n")] + fn py_set_n(&mut self, v: usize) { self.inner.n = v; } + + #[getter] + #[pyo3(name = "amps")] + fn py_get_amps<'py>(&self, py: Python<'py>) -> PyResult>> { Ok(self.inner.amps.clone().into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("QState", "QState", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Which basis to expand the Hamiltonian in. +/// +/// Rust: `quantum::schrodinger::Basis` +#[pyclass(name = "Basis", module = "numeria.quantum.schrodinger", from_py_object)] +#[derive(Clone)] +pub struct PyBasis { pub inner: rust_physics_engine::quantum::schrodinger::Basis } +#[pymethods] +impl PyBasis { + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Basis", "Basis", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// An XXZ spin-1/2 chain in a longitudinal field. +/// +/// `H = sum_i [ j (Sx Sx + Sy Sy) + jz Sz Sz ] - h sum_i Sz`, with the spin +/// operators equal to half the Pauli matrices. +/// +/// Setting `j == jz` gives the isotropic Heisenberg model; `j == 0` gives the +/// classical Ising chain; and `jz == 0` gives the XX model, which is free +/// fermions in disguise. +/// +/// Rust: `quantum::spin::SpinChain` +#[pyclass(name = "SpinChain", module = "numeria.quantum.spin", from_py_object)] +#[derive(Clone)] +pub struct PySpinChain { pub inner: rust_physics_engine::quantum::spin::SpinChain } +#[pymethods] +impl PySpinChain { + /// A chain, checking the site count. + /// + /// Errors: + /// Returns an error for fewer than two sites or more than sixteen. + /// + /// Rust: `quantum::spin::SpinChain::new` + #[new] + #[pyo3(signature = (n, j, jz, h_field, periodic))] + fn __new__(n: usize, j: f64, jz: f64, h_field: f64, periodic: bool) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::spin::SpinChain::new(n, j, jz, h_field, periodic)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PySpinChain { inner: __v }) + } + + /// Applies the Hamiltonian to a state vector. + /// + /// This is the primitive everything else uses. Nothing is stored: each + /// term is applied on the fly, so the cost is `O(n 2^n)` in time and + /// `O(2^n)` in memory rather than the `O(4^n)` a stored matrix would + /// need. That difference is the whole reason a sixteen-site chain is + /// reachable and a stored one is not. + /// + /// Errors: + /// Returns an error if the vector has the wrong length. + /// + /// Rust: `quantum::spin::SpinChain::apply` + #[pyo3(name = "apply")] + #[pyo3(signature = (v))] + fn apply<'py>(&self, py: Python<'py>, v: Vec) -> PyResult>> { + let v = v.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| self.inner.apply(&v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) + } + + /// The Hamiltonian as a dense real symmetric matrix. + /// + /// The XXZ Hamiltonian in this basis has no imaginary part -- the `Sy Sy` + /// term's factors of `i` cancel against each other -- so it is stored + /// real, which halves the eigensolver's work. + /// + /// Errors: + /// Returns an error above ten sites, where the matrix stops being worth + /// forming. + /// + /// Rust: `quantum::spin::SpinChain::hamiltonian_dense` + #[pyo3(name = "hamiltonian_dense")] + #[pyo3(signature = ())] + fn hamiltonian_dense(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.hamiltonian_dense()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) + } + + /// The full spectrum, ascending. + /// + /// Errors: + /// Returns an error above ten sites or if the eigensolver fails. + /// + /// Rust: `quantum::spin::SpinChain::spectrum_small` + #[pyo3(name = "spectrum_small")] + #[pyo3(signature = ())] + fn spectrum_small<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.spectrum_small())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) + } + + /// The ground state by Lanczos, returning the energy and the vector. + /// + /// Errors: + /// Returns an error if the iteration fails to build a Krylov space. + /// + /// Rust: `quantum::spin::SpinChain::ground_state_lanczos` + #[pyo3(name = "ground_state_lanczos")] + #[pyo3(signature = (iterations, rng))] + fn ground_state_lanczos<'py>(&self, py: Python<'py>, iterations: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(f64, Vec>)> { + let mut rng = rng; + let __r = crate::runtime::guard(|| self.inner.ground_state_lanczos(iterations, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>())) + } + + /// The total magnetisation per site, ` / n`. + /// + /// Errors: + /// Returns an error if the state has the wrong length. + /// + /// Rust: `quantum::spin::SpinChain::magnetization` + #[pyo3(name = "magnetization")] + #[pyo3(signature = (state))] + fn magnetization<'py>(&self, py: Python<'py>, state: Vec) -> PyResult { + let state = state.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || self.inner.magnetization(&state))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) + } + + /// The spin-spin correlation ``. + /// + /// Errors: + /// Returns an error for a bad site index or state length. + /// + /// Rust: `quantum::spin::SpinChain::correlation` + #[pyo3(name = "correlation")] + #[pyo3(signature = (state, i, j))] + fn correlation<'py>(&self, py: Python<'py>, state: Vec, i: usize, j: usize) -> PyResult { + let state = state.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || self.inner.correlation(&state, i, j))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) + } + + /// The static structure factor at wavevector `k`. + /// + /// The Fourier transform of the correlations, and what a neutron + /// scattering experiment measures. A peak at `k = pi` is + /// antiferromagnetic order; a peak at zero is ferromagnetic. + /// + /// Errors: + /// Returns an error if the state has the wrong length. + /// + /// Rust: `quantum::spin::SpinChain::structure_factor` + #[pyo3(name = "structure_factor")] + #[pyo3(signature = (state, k))] + fn structure_factor<'py>(&self, py: Python<'py>, state: Vec, k: f64) -> PyResult { + let state = state.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || self.inner.structure_factor(&state, k))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) + } + + /// The entanglement entropy of the first `cut` sites, in bits. + /// + /// Errors: + /// Returns an error for a bad cut or state length. + /// + /// Rust: `quantum::spin::SpinChain::entanglement_entropy_cut` + #[pyo3(name = "entanglement_entropy_cut")] + #[pyo3(signature = (state, cut))] + fn entanglement_entropy_cut<'py>(&self, py: Python<'py>, state: Vec, cut: usize) -> PyResult { + let state = state.into_iter().map(|__e| __e.0).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || self.inner.entanglement_entropy_cut(&state, cut))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) + } + + /// Evolves a state under the chain's Hamiltonian for a time `t`, by + /// repeated Krylov steps. + /// + /// Each step builds a small Krylov space and exponentiates the + /// tridiagonal projection exactly, which is why the method is stable at + /// step sizes that would defeat a Taylor series -- the projection is + /// Hermitian, so its exponential is unitary whatever the step. + /// + /// Errors: + /// Returns an error for a bad state, a non-positive step, or a Krylov + /// breakdown. + /// + /// Rust: `quantum::spin::SpinChain::time_evolve_krylov` + #[pyo3(name = "time_evolve_krylov")] + #[pyo3(signature = (state, t, steps))] + fn time_evolve_krylov<'py>(&self, py: Python<'py>, state: Vec, t: f64, steps: usize) -> PyResult>> { + let state = state.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| self.inner.time_evolve_krylov(&state, t, steps)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) + } + + #[getter] + #[pyo3(name = "n")] + fn py_get_n(&self) -> PyResult { Ok(self.inner.n) } + + #[setter] + #[pyo3(name = "n")] + fn py_set_n(&mut self, v: usize) { self.inner.n = v; } + + #[getter] + #[pyo3(name = "j")] + fn py_get_j(&self) -> PyResult { Ok(self.inner.j) } + + #[setter] + #[pyo3(name = "j")] + fn py_set_j(&mut self, v: f64) { self.inner.j = v; } + + #[getter] + #[pyo3(name = "jz")] + fn py_get_jz(&self) -> PyResult { Ok(self.inner.jz) } + + #[setter] + #[pyo3(name = "jz")] + fn py_set_jz(&mut self, v: f64) { self.inner.jz = v; } + + #[getter] + #[pyo3(name = "h_field")] + fn py_get_h_field(&self) -> PyResult { Ok(self.inner.h_field) } + + #[setter] + #[pyo3(name = "h_field")] + fn py_set_h_field(&mut self, v: f64) { self.inner.h_field = v; } + + #[getter] + #[pyo3(name = "periodic")] + fn py_get_periodic(&self) -> PyResult { Ok(self.inner.periodic) } + + #[setter] + #[pyo3(name = "periodic")] + fn py_set_periodic(&mut self, v: bool) { self.inner.periodic = v; } + + fn __repr__(&self) -> String { format!("SpinChain(n={:?}, j={:?}, jz={:?}, h_field={:?}, periodic={:?})", self.inner.n, self.inner.j, self.inner.jz, self.inner.h_field, self.inner.periodic) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A complex wavefunction sampled on a uniform grid. +/// +/// The grid runs from `x0` in steps of `dx`, so sample `k` sits at +/// `x0 + k * dx`. +/// +/// Rust: `quantum::wavefunction::Wavefunction1D` +#[pyclass(name = "Wavefunction1D", module = "numeria.quantum.wavefunction", from_py_object)] +#[derive(Clone)] +pub struct PyWavefunction1D { pub inner: rust_physics_engine::quantum::wavefunction::Wavefunction1D } +#[pymethods] +impl PyWavefunction1D { + /// A wavefunction from explicit samples. + /// + /// Errors: + /// Returns an error for an empty sample vector or a non-positive spacing. + /// + /// Rust: `quantum::wavefunction::Wavefunction1D::new` + #[new] + #[pyo3(signature = (psi, dx, x0))] + fn __new__(psi: Vec, dx: f64, x0: f64) -> PyResult { + let psi = psi.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::wavefunction::Wavefunction1D::new(psi, dx, x0)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyWavefunction1D { inner: __v }) + } + + /// A normalised Gaussian wave packet centred at `centre` with mean + /// momentum `hbar * k0` and position spread `sigma`. + /// + /// The minimum-uncertainty state: it saturates `sigma_x sigma_p = hbar/2` + /// exactly, and it is the only state that does. Everything else in + /// quantum mechanics has a strictly larger product, so this is the + /// reference against which "how close to classical" is measured. + /// + /// Errors: + /// Returns an error for a non-positive width or an empty grid. + /// + /// Rust: `quantum::wavefunction::Wavefunction1D::gaussian_packet` + #[pyo3(name = "gaussian_packet")] + #[staticmethod] + #[pyo3(signature = (centre, k0, sigma, dx, x0, n))] + fn gaussian_packet(centre: f64, k0: f64, sigma: f64, dx: f64, x0: f64, n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::wavefunction::Wavefunction1D::gaussian_packet(centre, k0, sigma, dx, x0, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyWavefunction1D { inner: __v }) + } + + /// A plane wave `exp(i k x)` on the grid, normalised over it. + /// + /// Not normalisable on the whole line -- which is why momentum + /// eigenstates are not states -- so this is the box-normalised stand-in. + /// + /// Errors: + /// Returns an error for an empty grid or a non-positive spacing. + /// + /// Rust: `quantum::wavefunction::Wavefunction1D::plane_wave` + #[pyo3(name = "plane_wave")] + #[staticmethod] + #[pyo3(signature = (k, dx, x0, n))] + fn plane_wave(k: f64, dx: f64, x0: f64, n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quantum::wavefunction::Wavefunction1D::plane_wave(k, dx, x0, n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyWavefunction1D { inner: __v }) + } + + /// The number of grid points. + /// + /// Rust: `quantum::wavefunction::Wavefunction1D::len` + #[pyo3(name = "len")] + #[pyo3(signature = ())] + fn len(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.len()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Always false: a wavefunction cannot be constructed empty. + /// + /// Rust: `quantum::wavefunction::Wavefunction1D::is_empty` + #[pyo3(name = "is_empty")] + #[pyo3(signature = ())] + fn is_empty(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_empty()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The position of sample `k`. + /// + /// Rust: `quantum::wavefunction::Wavefunction1D::x` + #[pyo3(name = "x")] + #[pyo3(signature = (k))] + fn x(&self, k: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.x(k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// `sqrt(integral |psi|^2 dx)` on the grid. + /// + /// Rust: `quantum::wavefunction::Wavefunction1D::norm` + #[pyo3(name = "norm")] + #[pyo3(signature = ())] + fn norm(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.norm()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Scales the wavefunction to unit norm, leaving it alone if it is zero. + /// + /// Rust: `quantum::wavefunction::Wavefunction1D::normalize` + #[pyo3(name = "normalize")] + #[pyo3(signature = ())] + fn normalize(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.normalize()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// The probability density `|psi|^2`. + /// + /// Rust: `quantum::wavefunction::Wavefunction1D::probability_density` + #[pyo3(name = "probability_density")] + #[pyo3(signature = ())] + fn probability_density<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.probability_density())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The expected position. + /// + /// Rust: `quantum::wavefunction::Wavefunction1D::expectation_x` + #[pyo3(name = "expectation_x")] + #[pyo3(signature = ())] + fn expectation_x(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.expectation_x()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The variance of position. + /// + /// Rust: `quantum::wavefunction::Wavefunction1D::variance_x` + #[pyo3(name = "variance_x")] + #[pyo3(signature = ())] + fn variance_x(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.variance_x()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The grid's momentum values in FFT order, in units where the wavenumber + /// is `k` and the momentum `hbar k`. + /// + /// The second half of the array holds the negative frequencies, which is + /// the convention the FFT imposes and the one place a sign error hides + /// most easily. + /// + /// Rust: `quantum::wavefunction::Wavefunction1D::wavenumbers` + #[pyo3(name = "wavenumbers")] + #[pyo3(signature = ())] + fn wavenumbers<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.wavenumbers())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The momentum-space amplitudes, in FFT order. + /// + /// Errors: + /// Returns an error unless the grid length is a power of two. + /// + /// Rust: `quantum::wavefunction::Wavefunction1D::momentum_space` + #[pyo3(name = "momentum_space")] + #[pyo3(signature = ())] + fn momentum_space<'py>(&self, py: Python<'py>) -> PyResult>> { + let __r = crate::runtime::guard(|| self.inner.momentum_space()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) + } + + /// The expected momentum, in units of `hbar`. + /// + /// Computed spectrally rather than by differencing: the momentum operator + /// is exactly diagonal in the Fourier basis, so on a periodic grid this is + /// exact to rounding, while a finite difference carries an `O(dx^2)` + /// error that then contaminates the uncertainty product. + /// + /// Errors: + /// Returns an error unless the grid length is a power of two. + /// + /// Rust: `quantum::wavefunction::Wavefunction1D::expectation_k` + #[pyo3(name = "expectation_k")] + #[pyo3(signature = ())] + fn expectation_k(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.expectation_k()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) + } + + /// The variance of the wavenumber. + /// + /// Errors: + /// Returns an error unless the grid length is a power of two. + /// + /// Rust: `quantum::wavefunction::Wavefunction1D::variance_k` + #[pyo3(name = "variance_k")] + #[pyo3(signature = ())] + fn variance_k(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.variance_k()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) + } + + /// The uncertainty product `sigma_x sigma_p` with the given `hbar`. + /// + /// Bounded below by `hbar / 2`, with equality exactly for a Gaussian. + /// + /// Errors: + /// Returns an error unless the grid length is a power of two. + /// + /// Rust: `quantum::wavefunction::Wavefunction1D::uncertainty_product` + #[pyo3(name = "uncertainty_product")] + #[pyo3(signature = (hbar))] + fn uncertainty_product(&self, hbar: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.uncertainty_product(hbar)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) + } + + /// The overlap ``. + /// + /// Errors: + /// Returns an error if the two grids disagree. + /// + /// Rust: `quantum::wavefunction::Wavefunction1D::overlap` + #[pyo3(name = "overlap")] + #[pyo3(signature = (other))] + fn overlap<'py>(&self, py: Python<'py>, other: crate::generated::types::PyWavefunction1D) -> PyResult> { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.overlap(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::runtime::coerce::complex_out(py, __v)) + } + + /// The expected energy for the potential `v`, with the kinetic term + /// evaluated spectrally. + /// + /// Errors: + /// Returns an error if the potential has the wrong length or the grid is + /// not a power of two. + /// + /// Rust: `quantum::wavefunction::Wavefunction1D::energy` + #[pyo3(name = "energy")] + #[pyo3(signature = (v, hbar, mass))] + fn energy<'py>(&self, py: Python<'py>, v: Vec, hbar: f64, mass: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.energy(&v, hbar, mass))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) + } + + /// Applies the free-particle propagator for a time `t` spectrally. + /// + /// Exact for the free particle at any step size, since the kinetic + /// operator is diagonal in momentum -- there is no time-stepping error to + /// accumulate. That makes it the reference a split-operator integrator + /// should be measured against. + /// + /// Errors: + /// Returns an error unless the grid length is a power of two. + /// + /// Rust: `quantum::wavefunction::Wavefunction1D::propagate_free` + #[pyo3(name = "propagate_free")] + #[pyo3(signature = (t, hbar, mass))] + fn propagate_free(&self, t: f64, hbar: f64, mass: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.propagate_free(t, hbar, mass)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyWavefunction1D { inner: __v }) + } + + #[getter] + #[pyo3(name = "psi")] + fn py_get_psi<'py>(&self, py: Python<'py>) -> PyResult>> { Ok(self.inner.psi.clone().into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) } + + #[getter] + #[pyo3(name = "dx")] + fn py_get_dx(&self) -> PyResult { Ok(self.inner.dx) } + + #[setter] + #[pyo3(name = "dx")] + fn py_set_dx(&mut self, v: f64) { self.inner.dx = v; } + + #[getter] + #[pyo3(name = "x0")] + fn py_get_x0(&self) -> PyResult { Ok(self.inner.x0) } + + #[setter] + #[pyo3(name = "x0")] + fn py_set_x0(&mut self, v: f64) { self.inner.x0 = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Wavefunction1D", "Wavefunction1D", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} diff --git a/bindings/python/src/generated/types/quaternion.rs b/bindings/python/src/generated/types/quaternion.rs new file mode 100644 index 0000000..7363e72 --- /dev/null +++ b/bindings/python/src/generated/types/quaternion.rs @@ -0,0 +1,282 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; + + +/// +/// Rust: `quaternion::Quaternion` +#[pyclass(name = "Quaternion", module = "numeria.quaternion", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyQuaternion { pub inner: rust_physics_engine::quaternion::Quaternion } +#[pymethods] +impl PyQuaternion { + /// Create a quaternion from components (w, x, y, z). + /// + /// Rust: `quaternion::Quaternion::new` + #[new] + #[pyo3(signature = (w, x, y, z))] + fn __new__(w: f64, x: f64, y: f64, z: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quaternion::Quaternion::new(w, x, y, z)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuaternion { inner: __v }) + } + + /// Identity quaternion representing no rotation: (1, 0, 0, 0). + /// + /// Rust: `quaternion::Quaternion::identity` + #[pyo3(name = "identity")] + #[staticmethod] + #[pyo3(signature = ())] + fn identity() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quaternion::Quaternion::identity()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuaternion { inner: __v }) + } + + /// Create a rotation quaternion from an axis and angle: q = (cos(θ/2), sin(θ/2)·axis). + /// + /// Rust: `quaternion::Quaternion::from_axis_angle` + #[pyo3(name = "from_axis_angle")] + #[staticmethod] + #[pyo3(signature = (axis, angle))] + fn from_axis_angle(axis: crate::generated::types::PyVec3Arg, angle: f64) -> PyResult { + let axis = axis.0; + let __r = crate::runtime::guard(|| rust_physics_engine::quaternion::Quaternion::from_axis_angle(axis, angle)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuaternion { inner: __v }) + } + + /// ZYX convention: roll (X), pitch (Y), yaw (Z) applied as Z * Y * X. + /// + /// Rust: `quaternion::Quaternion::from_euler` + #[pyo3(name = "from_euler")] + #[staticmethod] + #[pyo3(signature = (roll, pitch, yaw))] + fn from_euler(roll: f64, pitch: f64, yaw: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::quaternion::Quaternion::from_euler(roll, pitch, yaw)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuaternion { inner: __v }) + } + + /// Quaternion norm: |q| = √(w² + x² + y² + z²) + /// + /// Rust: `quaternion::Quaternion::norm` + #[pyo3(name = "norm")] + #[pyo3(signature = ())] + fn norm(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.norm()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Return a unit quaternion (normalized to norm 1). + /// + /// Rust: `quaternion::Quaternion::normalize` + #[pyo3(name = "normalize")] + #[pyo3(signature = ())] + fn normalize(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.normalize()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuaternion { inner: __v }) + } + + /// Quaternion conjugate: q* = (w, -x, -y, -z). + /// + /// Rust: `quaternion::Quaternion::conjugate` + #[pyo3(name = "conjugate")] + #[pyo3(signature = ())] + fn conjugate(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.conjugate()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuaternion { inner: __v }) + } + + /// Quaternion inverse: q⁻¹ = q*/|q|². + /// + /// Rust: `quaternion::Quaternion::inverse` + #[pyo3(name = "inverse")] + #[pyo3(signature = ())] + fn inverse(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.inverse()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuaternion { inner: __v }) + } + + /// Dot product of two quaternions: q₁·q₂ = w₁w₂ + x₁x₂ + y₁y₂ + z₁z₂ + /// + /// Rust: `quaternion::Quaternion::dot` + #[pyo3(name = "dot")] + #[pyo3(signature = (other))] + fn dot(&self, other: crate::generated::types::PyQuaternionArg) -> PyResult { + let other = other.0; + let __r = crate::runtime::guard(|| self.inner.dot(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Rotate a vector by this quaternion: v' = q·v·q* + /// + /// Rust: `quaternion::Quaternion::rotate_vec` + #[pyo3(name = "rotate_vec")] + #[pyo3(signature = (v))] + fn rotate_vec(&self, v: crate::generated::types::PyVec3Arg) -> PyResult { + let v = v.0; + let __r = crate::runtime::guard(|| self.inner.rotate_vec(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Convert to a 3x3 rotation matrix. + /// + /// Rust: `quaternion::Quaternion::to_rotation_matrix` + #[pyo3(name = "to_rotation_matrix")] + #[pyo3(signature = ())] + fn to_rotation_matrix<'py>(&self, py: Python<'py>) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.to_rotation_matrix())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.to_vec()).collect::>()) + } + + /// Extract axis and angle from this rotation quaternion. + /// + /// Rust: `quaternion::Quaternion::to_axis_angle` + #[pyo3(name = "to_axis_angle")] + #[pyo3(signature = ())] + fn to_axis_angle(&self) -> PyResult<(crate::generated::types::PyVec3, f64)> { + let __r = crate::runtime::guard(|| self.inner.to_axis_angle()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((crate::generated::types::PyVec3 { inner: __v.0 }, __v.1)) + } + + /// Returns (roll, pitch, yaw) using ZYX convention. + /// + /// Rust: `quaternion::Quaternion::to_euler` + #[pyo3(name = "to_euler")] + #[pyo3(signature = ())] + fn to_euler(&self) -> PyResult<(f64, f64, f64)> { + let __r = crate::runtime::guard(|| self.inner.to_euler()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) + } + + /// Angle between two quaternion rotations: θ = 2·arccos(|q₁·q₂|) + /// + /// Rust: `quaternion::Quaternion::angle_between` + #[pyo3(name = "angle_between")] + #[pyo3(signature = (other))] + fn angle_between(&self, other: crate::generated::types::PyQuaternionArg) -> PyResult { + let other = other.0; + let __r = crate::runtime::guard(|| self.inner.angle_between(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Check if this quaternion has unit norm within the given tolerance. + /// + /// Rust: `quaternion::Quaternion::is_unit` + #[pyo3(name = "is_unit")] + #[pyo3(signature = (tolerance))] + fn is_unit(&self, tolerance: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_unit(tolerance)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + fn __mul__(&self, rhs: crate::generated::types::PyQuaternionArg) -> PyResult { + let rhs = rhs.0; + let __r = crate::runtime::guard(|| ::mul(self.inner.clone(), rhs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuaternion { inner: __v }) + } + + fn __add__(&self, rhs: crate::generated::types::PyQuaternionArg) -> PyResult { + let rhs = rhs.0; + let __r = crate::runtime::guard(|| ::add(self.inner.clone(), rhs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuaternion { inner: __v }) + } + + fn __sub__(&self, rhs: crate::generated::types::PyQuaternionArg) -> PyResult { + let rhs = rhs.0; + let __r = crate::runtime::guard(|| ::sub(self.inner.clone(), rhs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuaternion { inner: __v }) + } + + fn __neg__(&self) -> PyResult { + let __r = crate::runtime::guard(|| ::neg(self.inner.clone())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuaternion { inner: __v }) + } + + #[getter] + #[pyo3(name = "w")] + fn py_get_w(&self) -> PyResult { Ok(self.inner.w) } + + #[setter] + #[pyo3(name = "w")] + fn py_set_w(&mut self, v: f64) { self.inner.w = v; } + + #[getter] + #[pyo3(name = "x")] + fn py_get_x(&self) -> PyResult { Ok(self.inner.x) } + + #[setter] + #[pyo3(name = "x")] + fn py_set_x(&mut self, v: f64) { self.inner.x = v; } + + #[getter] + #[pyo3(name = "y")] + fn py_get_y(&self) -> PyResult { Ok(self.inner.y) } + + #[setter] + #[pyo3(name = "y")] + fn py_set_y(&mut self, v: f64) { self.inner.y = v; } + + #[getter] + #[pyo3(name = "z")] + fn py_get_z(&self) -> PyResult { Ok(self.inner.z) } + + #[setter] + #[pyo3(name = "z")] + fn py_set_z(&mut self, v: f64) { self.inner.z = v; } + + fn __len__(&self) -> usize { 4 } + + /// `(w, x, y, z)` as a plain list. + fn tolist(&self) -> Vec { vec![self.inner.w, self.inner.x, self.inner.y, self.inner.z] } + + fn __repr__(&self) -> String { format!("Quaternion(w={:?}, x={:?}, y={:?}, z={:?})", self.inner.w, self.inner.x, self.inner.y, self.inner.z) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A `Quaternion` argument, or anything that can stand in for one. +pub struct PyQuaternionArg(pub rust_physics_engine::quaternion::Quaternion); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyQuaternionArg { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result { + if let Ok(__w) = obj.extract::() { + return Ok(PyQuaternionArg(__w.inner)); + } + let __v = crate::runtime::coerce::floats_exact(obj, 4, "Quaternion")?; + Ok(PyQuaternionArg(rust_physics_engine::quaternion::Quaternion { w: __v[0], x: __v[1], y: __v[2], z: __v[3] })) + } +} + diff --git a/bindings/python/src/generated/types/resonance.rs b/bindings/python/src/generated/types/resonance.rs new file mode 100644 index 0000000..e9001b5 --- /dev/null +++ b/bindings/python/src/generated/types/resonance.rs @@ -0,0 +1,1194 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; + + +/// Beam boundary conditions. +/// +/// Rust: `resonance::cavity::BeamBc` +#[pyclass(name = "BeamBc", module = "numeria.resonance.cavity", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyBeamBc { + ClampedFree, + ClampedClamped, + SimplySupported, + FreeFree, +} +impl PyBeamBc { + pub fn to_rust(&self) -> rust_physics_engine::resonance::cavity::BeamBc { match self { + Self::ClampedFree => rust_physics_engine::resonance::cavity::BeamBc::ClampedFree, + Self::ClampedClamped => rust_physics_engine::resonance::cavity::BeamBc::ClampedClamped, + Self::SimplySupported => rust_physics_engine::resonance::cavity::BeamBc::SimplySupported, + Self::FreeFree => rust_physics_engine::resonance::cavity::BeamBc::FreeFree, + } } + pub fn from_rust(v: &rust_physics_engine::resonance::cavity::BeamBc) -> Self { match v { + rust_physics_engine::resonance::cavity::BeamBc::ClampedFree => Self::ClampedFree, + rust_physics_engine::resonance::cavity::BeamBc::ClampedClamped => Self::ClampedClamped, + rust_physics_engine::resonance::cavity::BeamBc::SimplySupported => Self::SimplySupported, + rust_physics_engine::resonance::cavity::BeamBc::FreeFree => Self::FreeFree, + } } +} +#[pymethods] +impl PyBeamBc { + fn __repr__(&self) -> &'static str { + match self { + Self::ClampedFree => "BeamBc.ClampedFree", + Self::ClampedClamped => "BeamBc.ClampedClamped", + Self::SimplySupported => "BeamBc.SimplySupported", + Self::FreeFree => "BeamBc.FreeFree", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Plate boundary conditions for `rectangular_plate_modes`. +/// +/// Rust: `resonance::cavity::PlateBc` +#[pyclass(name = "PlateBc", module = "numeria.resonance.cavity", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyPlateBc { + SimplySupported, + Clamped, +} +impl PyPlateBc { + pub fn to_rust(&self) -> rust_physics_engine::resonance::cavity::PlateBc { match self { + Self::SimplySupported => rust_physics_engine::resonance::cavity::PlateBc::SimplySupported, + Self::Clamped => rust_physics_engine::resonance::cavity::PlateBc::Clamped, + } } + pub fn from_rust(v: &rust_physics_engine::resonance::cavity::PlateBc) -> Self { match v { + rust_physics_engine::resonance::cavity::PlateBc::SimplySupported => Self::SimplySupported, + rust_physics_engine::resonance::cavity::PlateBc::Clamped => Self::Clamped, + } } +} +#[pymethods] +impl PyPlateBc { + fn __repr__(&self) -> &'static str { + match self { + Self::SimplySupported => "PlateBc.SimplySupported", + Self::Clamped => "PlateBc.Clamped", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Series/parallel RLC resonator. +/// +/// Rust: `resonance::cavity::Rlc` +#[pyclass(name = "Rlc", module = "numeria.resonance.cavity", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyRlc { pub inner: rust_physics_engine::resonance::cavity::Rlc } +#[pymethods] +impl PyRlc { + /// Builds a `Rlc` from its fields. + #[new] + #[pyo3(signature = (r, l, c))] + fn __new__(r: f64, l: f64, c: f64) -> Self { + + Self { inner: rust_physics_engine::resonance::cavity::Rlc { r: r, l: l, c: c } } + } + + /// Series impedance R + j(ωL − 1/(ωC)). + /// + /// Rust: `resonance::cavity::Rlc::series_impedance` + #[pyo3(name = "series_impedance")] + #[pyo3(signature = (omega))] + fn series_impedance<'py>(&self, py: Python<'py>, omega: f64) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.series_impedance(omega)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::complex_out(py, __v)) + } + + /// Parallel impedance 1/(1/R + 1/(jωL) + jωC). + /// + /// Rust: `resonance::cavity::Rlc::parallel_impedance` + #[pyo3(name = "parallel_impedance")] + #[pyo3(signature = (omega))] + fn parallel_impedance<'py>(&self, py: Python<'py>, omega: f64) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.parallel_impedance(omega)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::complex_out(py, __v)) + } + + /// Resonant frequency 1/(2π√(LC)) (Hz). + /// + /// Rust: `resonance::cavity::Rlc::resonant_frequency` + #[pyo3(name = "resonant_frequency")] + #[pyo3(signature = ())] + fn resonant_frequency(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.resonant_frequency()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Series quality factor (1/R)√(L/C). + /// + /// Rust: `resonance::cavity::Rlc::q_series` + #[pyo3(name = "q_series")] + #[pyo3(signature = ())] + fn q_series(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.q_series()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Parallel quality factor R√(C/L). + /// + /// Rust: `resonance::cavity::Rlc::q_parallel` + #[pyo3(name = "q_parallel")] + #[pyo3(signature = ())] + fn q_parallel(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.q_parallel()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Series half-power bandwidth f₀/Q (Hz). + /// + /// Rust: `resonance::cavity::Rlc::bandwidth` + #[pyo3(name = "bandwidth")] + #[pyo3(signature = ())] + fn bandwidth(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.bandwidth()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Series damping ratio ζ = (R/2)√(C/L). + /// + /// Rust: `resonance::cavity::Rlc::damping_ratio` + #[pyo3(name = "damping_ratio")] + #[pyo3(signature = ())] + fn damping_ratio(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.damping_ratio()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Voltage transfer across C in the series loop (low-pass). + /// + /// Rust: `resonance::cavity::Rlc::transfer_lowpass` + #[pyo3(name = "transfer_lowpass")] + #[pyo3(signature = (omega))] + fn transfer_lowpass<'py>(&self, py: Python<'py>, omega: f64) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.transfer_lowpass(omega)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::complex_out(py, __v)) + } + + /// Voltage transfer across R (band-pass). + /// + /// Rust: `resonance::cavity::Rlc::transfer_bandpass` + #[pyo3(name = "transfer_bandpass")] + #[pyo3(signature = (omega))] + fn transfer_bandpass<'py>(&self, py: Python<'py>, omega: f64) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.transfer_bandpass(omega)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::complex_out(py, __v)) + } + + /// Voltage transfer across L (high-pass). + /// + /// Rust: `resonance::cavity::Rlc::transfer_highpass` + #[pyo3(name = "transfer_highpass")] + #[pyo3(signature = (omega))] + fn transfer_highpass<'py>(&self, py: Python<'py>, omega: f64) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.transfer_highpass(omega)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::complex_out(py, __v)) + } + + /// Voltage transfer across the LC pair (notch). + /// + /// Rust: `resonance::cavity::Rlc::transfer_notch` + #[pyo3(name = "transfer_notch")] + #[pyo3(signature = (omega))] + fn transfer_notch<'py>(&self, py: Python<'py>, omega: f64) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.transfer_notch(omega)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::complex_out(py, __v)) + } + + /// Capacitor voltage after a step of amplitude v on the series loop. + /// + /// Rust: `resonance::cavity::Rlc::step_response` + #[pyo3(name = "step_response")] + #[pyo3(signature = (t, v))] + fn step_response(&self, t: f64, v: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.step_response(t, v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Stored energy ½Li² + ½Cv². + /// + /// Rust: `resonance::cavity::Rlc::energy` + #[pyo3(name = "energy")] + #[pyo3(signature = (i, v))] + fn energy(&self, i: f64, v: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.energy(i, v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "r")] + fn py_get_r(&self) -> PyResult { Ok(self.inner.r) } + + #[setter] + #[pyo3(name = "r")] + fn py_set_r(&mut self, v: f64) { self.inner.r = v; } + + #[getter] + #[pyo3(name = "l")] + fn py_get_l(&self) -> PyResult { Ok(self.inner.l) } + + #[setter] + #[pyo3(name = "l")] + fn py_set_l(&mut self, v: f64) { self.inner.l = v; } + + #[getter] + #[pyo3(name = "c")] + fn py_get_c(&self) -> PyResult { Ok(self.inner.c) } + + #[setter] + #[pyo3(name = "c")] + fn py_set_c(&mut self, v: f64) { self.inner.c = v; } + + fn __repr__(&self) -> String { format!("Rlc(r={:?}, l={:?}, c={:?})", self.inner.r, self.inner.l, self.inner.c) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A `Rlc` argument, or anything that can stand in for one. +pub struct PyRlcArg(pub rust_physics_engine::resonance::cavity::Rlc); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyRlcArg { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result { + if let Ok(__w) = obj.extract::() { + return Ok(PyRlcArg(__w.inner)); + } + let __v = crate::runtime::coerce::floats_exact(obj, 3, "Rlc")?; + Ok(PyRlcArg(rust_physics_engine::resonance::cavity::Rlc { r: __v[0], l: __v[1], c: __v[2] })) + } +} + + +/// N-degree-of-freedom system M·x″ + C·x′ + K·x = F. +/// +/// Rust: `resonance::coupled::CoupledOscillators` +#[pyclass(name = "CoupledOscillators", module = "numeria.resonance.coupled", from_py_object)] +#[derive(Clone)] +pub struct PyCoupledOscillators { pub inner: rust_physics_engine::resonance::coupled::CoupledOscillators } +#[pymethods] +impl PyCoupledOscillators { + /// Builds a `CoupledOscillators` from its fields. + #[new] + #[pyo3(signature = (masses, stiffness, damping))] + fn __new__(masses: Vec, stiffness: crate::generated::types::PyMatrixArg, damping: crate::generated::types::PyMatrixArg) -> Self { + let stiffness = stiffness.0; + let damping = damping.0; + Self { inner: rust_physics_engine::resonance::coupled::CoupledOscillators { masses: masses, stiffness: stiffness, damping: damping } } + } + + /// Chain of n masses, each grounded with stiffness k and coupled to + /// its neighbors by k_coupling (free ends). + /// + /// Rust: `resonance::coupled::CoupledOscillators::chain` + #[pyo3(name = "chain")] + #[staticmethod] + #[pyo3(signature = (n, m, k, k_coupling))] + fn chain(n: usize, m: f64, k: f64, k_coupling: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::coupled::CoupledOscillators::chain(n, m, k, k_coupling)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCoupledOscillators { inner: __v }) + } + + /// Chain of n masses joined in series by springs k with both ends + /// fixed to walls (the textbook fixed-fixed chain). + /// + /// Rust: `resonance::coupled::CoupledOscillators::chain_fixed_ends` + #[pyo3(name = "chain_fixed_ends")] + #[staticmethod] + #[pyo3(signature = (n, m, k))] + fn chain_fixed_ends(n: usize, m: f64, k: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::coupled::CoupledOscillators::chain_fixed_ends(n, m, k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCoupledOscillators { inner: __v }) + } + + /// Ring of n masses joined by springs k (periodic chain). + /// + /// Rust: `resonance::coupled::CoupledOscillators::ring` + #[pyo3(name = "ring")] + #[staticmethod] + #[pyo3(signature = (n, m, k))] + fn ring(n: usize, m: f64, k: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::coupled::CoupledOscillators::ring(n, m, k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCoupledOscillators { inner: __v }) + } + + /// Build from an explicit spring list: (i, j, k) couples masses i + /// and j; (i, i, k) grounds mass i with stiffness k. + /// + /// Rust: `resonance::coupled::CoupledOscillators::from_springs` + #[pyo3(name = "from_springs")] + #[staticmethod] + #[pyo3(signature = (masses, springs))] + fn from_springs(masses: Vec, springs: Vec<(usize, usize, f64)>) -> PyResult { + let springs = springs.into_iter().map(|__e| (__e.0, __e.1, __e.2)).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::coupled::CoupledOscillators::from_springs(&masses, &springs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCoupledOscillators { inner: __v }) + } + + /// Undamped normal modes: frequencies (rad/s, ascending) and + /// mass-orthonormal mode shapes as matrix columns (ΦᵀMΦ = I), from + /// the symmetric eigenproblem M^(−1/2)·K·M^(−1/2). + /// + /// Panics: + /// Panics if the eigen solve fails. + /// + /// Rust: `resonance::coupled::CoupledOscillators::normal_modes` + #[pyo3(name = "normal_modes")] + #[pyo3(signature = ())] + fn normal_modes(&self) -> PyResult<(Vec, crate::generated::types::PyMatrix)> { + let __r = crate::runtime::guard(|| self.inner.normal_modes()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, crate::generated::types::PyMatrix { inner: __v.1 })) + } + + /// Mode shape i (ascending frequency order). + /// + /// Rust: `resonance::coupled::CoupledOscillators::mode_shape` + #[pyo3(name = "mode_shape")] + #[pyo3(signature = (i))] + fn mode_shape<'py>(&self, py: Python<'py>, i: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.mode_shape(i))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Modal participation q = ΦᵀM·x₀ of an initial displacement. + /// + /// Rust: `resonance::coupled::CoupledOscillators::modal_participation` + #[pyo3(name = "modal_participation")] + #[pyo3(signature = (x0))] + fn modal_participation<'py>(&self, py: Python<'py>, x0: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.modal_participation(&x0))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Modal damping ratios ζᵢ = φᵢᵀCφᵢ/(2ωᵢ) (proportional-damping + /// assumption). + /// + /// Rust: `resonance::coupled::CoupledOscillators::modal_damping_ratios` + #[pyo3(name = "modal_damping_ratios")] + #[pyo3(signature = ())] + fn modal_damping_ratios<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.modal_damping_ratios())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Free response x(t) by modal superposition (proportional damping). + /// + /// Panics: + /// Panics on dimension mismatch. + /// + /// Rust: `resonance::coupled::CoupledOscillators::response` + #[pyo3(name = "response")] + #[pyo3(signature = (x0, v0, t))] + fn response<'py>(&self, py: Python<'py>, x0: Vec, v0: Vec, t: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.response(&x0, &v0, t))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Steady-state complex response X to harmonic forcing F·e^{jωt}, + /// solving (K + jωC − ω²M)X = F as a doubled real system. + /// + /// Panics: + /// Panics on dimension mismatch or singular dynamic stiffness. + /// + /// Rust: `resonance::coupled::CoupledOscillators::forced_response` + #[pyo3(name = "forced_response")] + #[pyo3(signature = (force, omega))] + fn forced_response<'py>(&self, py: Python<'py>, force: Vec, omega: f64) -> PyResult>> { + let __r = crate::runtime::guard(|| self.inner.forced_response(&force, omega)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()) + } + + /// Undamped receptance matrix (K − ω²M)^(−1). + /// + /// Panics: + /// Panics at an exact resonance (singular matrix). + /// + /// Rust: `resonance::coupled::CoupledOscillators::frequency_response_matrix` + #[pyo3(name = "frequency_response_matrix")] + #[pyo3(signature = (omega))] + fn frequency_response_matrix(&self, omega: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.frequency_response_matrix(omega)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) + } + + /// Beat (envelope) angular frequency ω₂ − ω₁ for a two-mode system. + /// + /// Rust: `resonance::coupled::CoupledOscillators::beat_frequency` + #[pyo3(name = "beat_frequency")] + #[pyo3(signature = ())] + fn beat_frequency(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.beat_frequency()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) + } + + /// Time for the energy to migrate fully between two identical + /// coupled oscillators: π/(ω₂ − ω₁). + /// + /// Rust: `resonance::coupled::CoupledOscillators::energy_transfer_time` + #[pyo3(name = "energy_transfer_time")] + #[pyo3(signature = ())] + fn energy_transfer_time(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.energy_transfer_time()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) + } + + /// Dispersion relation ω(q) of the uniform chain this system was + /// built as (q in radians per lattice site): + /// ω = √((k_ground + 4·k_c·sin²(q/2))/m), with k_c and k_ground read + /// off the stiffness matrix interior. + /// + /// Rust: `resonance::coupled::CoupledOscillators::dispersion_relation` + #[pyo3(name = "dispersion_relation")] + #[pyo3(signature = (k_wave))] + fn dispersion_relation(&self, k_wave: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.dispersion_relation(k_wave)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Velocity-Verlet simulation (with damping forces); returns the + /// position vector at every step, including t = 0. + /// + /// Panics: + /// Panics on dimension mismatch. + /// + /// Rust: `resonance::coupled::CoupledOscillators::simulate` + #[pyo3(name = "simulate")] + #[pyo3(signature = (x0, v0, t_end, dt))] + fn simulate<'py>(&self, py: Python<'py>, x0: Vec, v0: Vec, t_end: f64, dt: f64) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.simulate(&x0, &v0, t_end, dt))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Dunkerley lower-bound estimate of the fundamental frequency: + /// 1/ω² ≈ Σ mᵢ·(K⁻¹)ᵢᵢ. + /// + /// Panics: + /// Panics if K is singular. + /// + /// Rust: `resonance::coupled::CoupledOscillators::dunkerley_estimate` + #[pyo3(name = "dunkerley_estimate")] + #[pyo3(signature = ())] + fn dunkerley_estimate(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.dunkerley_estimate()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Rayleigh quotient ω² estimate φᵀKφ/φᵀMφ for a trial shape. + /// + /// Rust: `resonance::coupled::CoupledOscillators::rayleigh_quotient` + #[pyo3(name = "rayleigh_quotient")] + #[pyo3(signature = (shape))] + fn rayleigh_quotient<'py>(&self, py: Python<'py>, shape: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.rayleigh_quotient(&shape))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Anti-resonance frequencies of the receptance H_ij(ω): zeros + /// located by scanning for sign changes of the undamped H_ij up to + /// 1.2× the highest natural frequency. + /// + /// Rust: `resonance::coupled::CoupledOscillators::anti_resonance_frequencies` + #[pyo3(name = "anti_resonance_frequencies")] + #[pyo3(signature = (i, j))] + fn anti_resonance_frequencies<'py>(&self, py: Python<'py>, i: usize, j: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.anti_resonance_frequencies(i, j))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "masses")] + fn py_get_masses(&self) -> PyResult> { Ok(self.inner.masses.clone()) } + + #[getter] + #[pyo3(name = "stiffness")] + fn py_get_stiffness(&self) -> PyResult { Ok(crate::generated::types::PyMatrix { inner: self.inner.stiffness.clone() }) } + + #[getter] + #[pyo3(name = "damping")] + fn py_get_damping(&self) -> PyResult { Ok(crate::generated::types::PyMatrix { inner: self.inner.damping.clone() }) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("CoupledOscillators", "CoupledOscillators", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Mass-damper-spring oscillator m·x″ + c·x′ + k·x = F. +/// +/// Rust: `resonance::oscillator::DampedOscillator` +#[pyclass(name = "DampedOscillator", module = "numeria.resonance.oscillator", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyDampedOscillator { pub inner: rust_physics_engine::resonance::oscillator::DampedOscillator } +#[pymethods] +impl PyDampedOscillator { + /// Builds a `DampedOscillator` from its fields. + #[new] + #[pyo3(signature = (m, c, k))] + fn __new__(m: f64, c: f64, k: f64) -> Self { + + Self { inner: rust_physics_engine::resonance::oscillator::DampedOscillator { m: m, c: c, k: k } } + } + + /// Undamped natural frequency ω₀ = √(k/m) (rad/s). + /// + /// Rust: `resonance::oscillator::DampedOscillator::natural_frequency` + #[pyo3(name = "natural_frequency")] + #[pyo3(signature = ())] + fn natural_frequency(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.natural_frequency()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Damping ratio ζ = c/(2√(km)). + /// + /// Rust: `resonance::oscillator::DampedOscillator::damping_ratio` + #[pyo3(name = "damping_ratio")] + #[pyo3(signature = ())] + fn damping_ratio(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.damping_ratio()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Quality factor Q = 1/(2ζ). + /// + /// Rust: `resonance::oscillator::DampedOscillator::q_factor` + #[pyo3(name = "q_factor")] + #[pyo3(signature = ())] + fn q_factor(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.q_factor()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Damped oscillation frequency ω₀√(1−ζ²); None at or beyond + /// critical damping. + /// + /// Rust: `resonance::oscillator::DampedOscillator::damped_frequency` + #[pyo3(name = "damped_frequency")] + #[pyo3(signature = ())] + fn damped_frequency(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.damped_frequency()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) + } + + /// Damping regime (critical within 1 part in 10⁹ of ζ = 1). + /// + /// Rust: `resonance::oscillator::DampedOscillator::regime` + #[pyo3(name = "regime")] + #[pyo3(signature = ())] + fn regime(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.regime()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyDamping::from_rust(&__v)) + } + + /// Closed-form free response x(t) from initial position and + /// velocity, valid in all three regimes. + /// + /// Rust: `resonance::oscillator::DampedOscillator::free_response` + #[pyo3(name = "free_response")] + #[pyo3(signature = (x0, v0, t))] + fn free_response(&self, x0: f64, v0: f64, t: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.free_response(x0, v0, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Steady-state amplitude under forcing F = f₀·cos(ωt): + /// X = (f₀/m)/√((ω₀²−ω²)² + (2ζω₀ω)²). + /// + /// Rust: `resonance::oscillator::DampedOscillator::steady_state_amplitude` + #[pyo3(name = "steady_state_amplitude")] + #[pyo3(signature = (f0, omega))] + fn steady_state_amplitude(&self, f0: f64, omega: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.steady_state_amplitude(f0, omega)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Steady-state phase lag φ ∈ \[0, π\] of x behind the forcing: + /// x = X·cos(ωt − φ). + /// + /// Rust: `resonance::oscillator::DampedOscillator::steady_state_phase` + #[pyo3(name = "steady_state_phase")] + #[pyo3(signature = (omega))] + fn steady_state_phase(&self, omega: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.steady_state_phase(omega)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Transfer function X(s)/F(s) = 1/(m·s² + c·s + k). + /// + /// Rust: `resonance::oscillator::DampedOscillator::transfer_function` + #[pyo3(name = "transfer_function")] + #[pyo3(signature = (s))] + fn transfer_function<'py>(&self, py: Python<'py>, s: crate::runtime::coerce::ComplexArg) -> PyResult> { + let s = s.0; + let __r = crate::runtime::guard(|| self.inner.transfer_function(s)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::complex_out(py, __v)) + } + + /// H(jω). + /// + /// Rust: `resonance::oscillator::DampedOscillator::frequency_response` + #[pyo3(name = "frequency_response")] + #[pyo3(signature = (omega))] + fn frequency_response<'py>(&self, py: Python<'py>, omega: f64) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.frequency_response(omega)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::complex_out(py, __v)) + } + + /// Frequency of maximum amplitude ω₀√(1−2ζ²); None for ζ ≥ 1/√2 + /// (no resonant peak). + /// + /// Rust: `resonance::oscillator::DampedOscillator::resonant_frequency` + #[pyo3(name = "resonant_frequency")] + #[pyo3(signature = ())] + fn resonant_frequency(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.resonant_frequency()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| __x)) + } + + /// Half-power (full) bandwidth Δω = ω₀/Q = c/m (rad/s). + /// + /// Rust: `resonance::oscillator::DampedOscillator::bandwidth` + #[pyo3(name = "bandwidth")] + #[pyo3(signature = ())] + fn bandwidth(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.bandwidth()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Impulse response h(t): free response to a unit impulse (velocity + /// jump 1/m). + /// + /// Rust: `resonance::oscillator::DampedOscillator::impulse_response` + #[pyo3(name = "impulse_response")] + #[pyo3(signature = (t))] + fn impulse_response(&self, t: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.impulse_response(t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Response to a unit step force (final value 1/k). + /// + /// Rust: `resonance::oscillator::DampedOscillator::step_response` + #[pyo3(name = "step_response")] + #[pyo3(signature = (t))] + fn step_response(&self, t: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.step_response(t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// RK4 integration of the forced equation: samples of (t, x, v) + /// every dt up to t_end. + /// + /// Rust: `resonance::oscillator::DampedOscillator::forced_response_numeric` + #[pyo3(name = "forced_response_numeric")] + #[pyo3(signature = (force, x0, v0, t_end, dt))] + fn forced_response_numeric(&self, force: pyo3::Py, x0: f64, v0: f64, t_end: f64, dt: f64) -> PyResult> { + let __cb_force = std::rc::Rc::new(crate::runtime::Callback::new(force)); + let force = { let __cb = __cb_force.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let __r = crate::runtime::guard(|| self.inner.forced_response_numeric(&force, x0, v0, t_end, dt)); + crate::runtime::callback::check(&[&__cb_force], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1, __x.2)).collect::>()) + } + + /// Mechanical energy ½mv² + ½kx². + /// + /// Rust: `resonance::oscillator::DampedOscillator::energy` + #[pyo3(name = "energy")] + #[pyo3(signature = (x, v))] + fn energy(&self, x: f64, v: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.energy(x, v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Time for the displacement envelope to decay to `fraction` of its + /// initial value: t = −ln(fraction)/(ζω₀). + /// + /// Panics: + /// Panics unless 0 < fraction < 1. + /// + /// Rust: `resonance::oscillator::DampedOscillator::decay_time` + #[pyo3(name = "decay_time")] + #[pyo3(signature = (fraction))] + fn decay_time(&self, fraction: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.decay_time(fraction)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Logarithmic decrement δ = 2πζ/√(1−ζ²) (underdamped). + /// + /// Rust: `resonance::oscillator::DampedOscillator::logarithmic_decrement` + #[pyo3(name = "logarithmic_decrement")] + #[pyo3(signature = ())] + fn logarithmic_decrement(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.logarithmic_decrement()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Build from natural frequency, quality factor, and mass. + /// + /// Rust: `resonance::oscillator::DampedOscillator::from_q` + #[pyo3(name = "from_q")] + #[staticmethod] + #[pyo3(signature = (omega0, q, m))] + fn from_q(omega0: f64, q: f64, m: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::oscillator::DampedOscillator::from_q(omega0, q, m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyDampedOscillator { inner: __v }) + } + + #[getter] + #[pyo3(name = "m")] + fn py_get_m(&self) -> PyResult { Ok(self.inner.m) } + + #[setter] + #[pyo3(name = "m")] + fn py_set_m(&mut self, v: f64) { self.inner.m = v; } + + #[getter] + #[pyo3(name = "c")] + fn py_get_c(&self) -> PyResult { Ok(self.inner.c) } + + #[setter] + #[pyo3(name = "c")] + fn py_set_c(&mut self, v: f64) { self.inner.c = v; } + + #[getter] + #[pyo3(name = "k")] + fn py_get_k(&self) -> PyResult { Ok(self.inner.k) } + + #[setter] + #[pyo3(name = "k")] + fn py_set_k(&mut self, v: f64) { self.inner.k = v; } + + fn __repr__(&self) -> String { format!("DampedOscillator(m={:?}, c={:?}, k={:?})", self.inner.m, self.inner.c, self.inner.k) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A `DampedOscillator` argument, or anything that can stand in for one. +pub struct PyDampedOscillatorArg(pub rust_physics_engine::resonance::oscillator::DampedOscillator); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyDampedOscillatorArg { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result { + if let Ok(__w) = obj.extract::() { + return Ok(PyDampedOscillatorArg(__w.inner)); + } + let __v = crate::runtime::coerce::floats_exact(obj, 3, "DampedOscillator")?; + Ok(PyDampedOscillatorArg(rust_physics_engine::resonance::oscillator::DampedOscillator { m: __v[0], c: __v[1], k: __v[2] })) + } +} + + +/// Damping regime classification. +/// +/// Rust: `resonance::oscillator::Damping` +#[pyclass(name = "Damping", module = "numeria.resonance.oscillator", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyDamping { + Under, + Critical, + Over, +} +impl PyDamping { + pub fn to_rust(&self) -> rust_physics_engine::resonance::oscillator::Damping { match self { + Self::Under => rust_physics_engine::resonance::oscillator::Damping::Under, + Self::Critical => rust_physics_engine::resonance::oscillator::Damping::Critical, + Self::Over => rust_physics_engine::resonance::oscillator::Damping::Over, + } } + pub fn from_rust(v: &rust_physics_engine::resonance::oscillator::Damping) -> Self { match v { + rust_physics_engine::resonance::oscillator::Damping::Under => Self::Under, + rust_physics_engine::resonance::oscillator::Damping::Critical => Self::Critical, + rust_physics_engine::resonance::oscillator::Damping::Over => Self::Over, + } } +} +#[pymethods] +impl PyDamping { + fn __repr__(&self) -> &'static str { + match self { + Self::Under => "Damping.Under", + Self::Critical => "Damping.Critical", + Self::Over => "Damping.Over", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Structural model M·x″ + C·x′ + K·x = F(t) with full matrices. +/// +/// Rust: `resonance::structural::ModalModel` +#[pyclass(name = "ModalModel", module = "numeria.resonance.structural", from_py_object)] +#[derive(Clone)] +pub struct PyModalModel { pub inner: rust_physics_engine::resonance::structural::ModalModel } +#[pymethods] +impl PyModalModel { + /// Builds a `ModalModel` from its fields. + #[new] + #[pyo3(signature = (m, c, k))] + fn __new__(m: crate::generated::types::PyMatrixArg, c: crate::generated::types::PyMatrixArg, k: crate::generated::types::PyMatrixArg) -> Self { + let m = m.0; + let c = c.0; + let k = k.0; + Self { inner: rust_physics_engine::resonance::structural::ModalModel { m: m, c: c, k: k } } + } + + /// Clamped-free axial bar discretized with n_elem consistent-mass + /// two-node elements (dofs are the axial displacements of nodes + /// 1..=n_elem; node 0 is fixed). + /// + /// Rust: `resonance::structural::ModalModel::from_fem_1d_bar` + #[pyo3(name = "from_fem_1d_bar")] + #[staticmethod] + #[pyo3(signature = (n_elem, length, area, young, rho))] + fn from_fem_1d_bar(n_elem: usize, length: f64, area: f64, young: f64, rho: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::structural::ModalModel::from_fem_1d_bar(n_elem, length, area, young, rho)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyModalModel { inner: __v }) + } + + /// Cantilever Euler-Bernoulli beam with n_elem two-node elements + /// (dofs per free node: transverse deflection w and rotation θ). + /// + /// Rust: `resonance::structural::ModalModel::from_fem_beam` + #[pyo3(name = "from_fem_beam")] + #[staticmethod] + #[pyo3(signature = (n_elem, length, young, i_area, rho, area))] + fn from_fem_beam(n_elem: usize, length: f64, young: f64, i_area: f64, rho: f64, area: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::structural::ModalModel::from_fem_beam(n_elem, length, young, i_area, rho, area)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyModalModel { inner: __v }) + } + + /// Lumped-parameter model from mass, spring, and damper lists + /// ((i, i, v) grounds dof i; (i, j, v) couples i and j). + /// + /// Rust: `resonance::structural::ModalModel::from_lumped` + #[pyo3(name = "from_lumped")] + #[staticmethod] + #[pyo3(signature = (masses, springs, dampers))] + fn from_lumped(masses: Vec, springs: Vec<(usize, usize, f64)>, dampers: Vec<(usize, usize, f64)>) -> PyResult { + let springs = springs.into_iter().map(|__e| (__e.0, __e.1, __e.2)).collect::>(); + let dampers = dampers.into_iter().map(|__e| (__e.0, __e.1, __e.2)).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::resonance::structural::ModalModel::from_lumped(&masses, &springs, &dampers)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyModalModel { inner: __v }) + } + + /// Set C = αM + βK. + /// + /// Rust: `resonance::structural::ModalModel::rayleigh_damping` + #[pyo3(name = "rayleigh_damping")] + #[pyo3(signature = (alpha, beta))] + fn rayleigh_damping(&mut self, alpha: f64, beta: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.rayleigh_damping(alpha, beta)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Choose Rayleigh α, β to hit damping ratios ζ₁ at f₁ and ζ₂ at f₂ + /// (Hz): ζ = α/(2ω) + βω/2. + /// + /// Rust: `resonance::structural::ModalModel::rayleigh_from_ratios` + #[pyo3(name = "rayleigh_from_ratios")] + #[pyo3(signature = (zeta1, f1, zeta2, f2))] + fn rayleigh_from_ratios(&mut self, zeta1: f64, f1: f64, zeta2: f64, f2: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.rayleigh_from_ratios(zeta1, f1, zeta2, f2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Undamped modes of the generalized problem K·φ = ω²M·φ via the + /// Cholesky reduction L⁻¹KL⁻ᵀ: (frequencies rad/s ascending, + /// mass-orthonormal mode shapes as columns). + /// + /// Panics: + /// Panics if M is not positive definite or the eigen solve fails. + /// + /// Rust: `resonance::structural::ModalModel::modes` + #[pyo3(name = "modes")] + #[pyo3(signature = ())] + fn modes(&self) -> PyResult<(Vec, crate::generated::types::PyMatrix)> { + let __r = crate::runtime::guard(|| self.inner.modes()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, crate::generated::types::PyMatrix { inner: __v.1 })) + } + + /// State-space eigen solution of the damped system: eigenvalues λ + /// with Im λ ≥ 0 (one per underdamped mode) and their complex mode + /// shapes, from A = \[\[0, I\], \[−M⁻¹K, −M⁻¹C\]\] with eigenvectors + /// recovered by shifted inverse iteration. + /// + /// Panics: + /// Panics if the eigen machinery fails. + /// + /// Rust: `resonance::structural::ModalModel::damped_modes` + #[pyo3(name = "damped_modes")] + #[pyo3(signature = ())] + fn damped_modes<'py>(&self, py: Python<'py>) -> PyResult, Vec>)>> { + let __r = crate::runtime::guard(|| self.inner.damped_modes()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (crate::runtime::coerce::complex_out(py, __x.0), __x.1.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>())).collect::>()) + } + + /// Damping ratios ζᵢ = −Re λᵢ/|λᵢ| from the damped modes. + /// + /// Rust: `resonance::structural::ModalModel::modal_damping_ratios` + #[pyo3(name = "modal_damping_ratios")] + #[pyo3(signature = ())] + fn modal_damping_ratios<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.modal_damping_ratios())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Receptance FRF H_ij(ω) = \[(K + jωC − ω²M)⁻¹\]_ij. + /// + /// Rust: `resonance::structural::ModalModel::frf` + #[pyo3(name = "frf")] + #[pyo3(signature = (i, j, omega))] + fn frf<'py>(&self, py: Python<'py>, i: usize, j: usize, omega: f64) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.frf(i, j, omega)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::runtime::coerce::complex_out(py, __v)) + } + + /// Magnitude receptance matrix |H(ω)|. + /// + /// Rust: `resonance::structural::ModalModel::frf_matrix` + #[pyo3(name = "frf_matrix")] + #[pyo3(signature = (omega))] + fn frf_matrix(&self, omega: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.frf_matrix(omega)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) + } + + /// Newmark-β implicit integration under a time-varying force; + /// returns the displacement vector at every step (t = 0 included). + /// β = 1/4, γ = 1/2 is the unconditionally stable trapezoid rule. + /// + /// Panics: + /// Panics on dimension mismatches or a singular effective stiffness. + /// + /// Rust: `resonance::structural::ModalModel::newmark_beta` + #[pyo3(name = "newmark_beta")] + #[pyo3(signature = (force, x0, v0, t_end, dt, beta, gamma))] + fn newmark_beta(&self, force: pyo3::Py, x0: Vec, v0: Vec, t_end: f64, dt: f64, beta: f64, gamma: f64) -> PyResult>> { + let __cb_force = std::rc::Rc::new(crate::runtime::Callback::new(force)); + let force = { let __cb = __cb_force.clone(); move |__a0: f64| -> Vec { __cb.call::<_, Vec>((__a0,), Vec::new()) } }; + let __r = crate::runtime::guard(|| self.inner.newmark_beta(&force, &x0, &v0, t_end, dt, beta, gamma)); + crate::runtime::callback::check(&[&__cb_force], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// HHT-α integration (α ∈ [−1/3, 0]; β = (1−α)²/4, γ = 1/2 − α): + /// numerically damps spurious high-frequency content while keeping + /// second-order accuracy. + /// + /// Panics: + /// Panics on dimension mismatches or singular matrices. + /// + /// Rust: `resonance::structural::ModalModel::hht_alpha` + #[pyo3(name = "hht_alpha")] + #[pyo3(signature = (force, x0, v0, t_end, dt, alpha))] + fn hht_alpha(&self, force: pyo3::Py, x0: Vec, v0: Vec, t_end: f64, dt: f64, alpha: f64) -> PyResult>> { + let __cb_force = std::rc::Rc::new(crate::runtime::Callback::new(force)); + let force = { let __cb = __cb_force.clone(); move |__a0: f64| -> Vec { __cb.call::<_, Vec>((__a0,), Vec::new()) } }; + let __r = crate::runtime::guard(|| self.inner.hht_alpha(&force, &x0, &v0, t_end, dt, alpha)); + crate::runtime::callback::check(&[&__cb_force], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Reduced model keeping the lowest n_modes modal coordinates + /// (unit modal masses, diagonal stiffness ω², modal damping). + /// + /// Rust: `resonance::structural::ModalModel::modal_truncation` + #[pyo3(name = "modal_truncation")] + #[pyo3(signature = (n_modes))] + fn modal_truncation(&self, n_modes: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.modal_truncation(n_modes)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyModalModel { inner: __v }) + } + + /// Guyan (static) condensation onto the given master dofs. + /// + /// Panics: + /// Panics if the slave stiffness block is singular. + /// + /// Rust: `resonance::structural::ModalModel::guyan_reduction` + #[pyo3(name = "guyan_reduction")] + #[pyo3(signature = (master_dofs))] + fn guyan_reduction(&self, master_dofs: Vec) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.guyan_reduction(&master_dofs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyModalModel { inner: __v }) + } + + /// Modal response spectrum for a ground acceleration record: for + /// each mode, (natural frequency Hz, peak SDOF relative-displacement + /// response at damping ζ). + /// + /// Rust: `resonance::structural::ModalModel::response_spectrum` + #[pyo3(name = "response_spectrum")] + #[pyo3(signature = (ground_accel, dt, zeta))] + fn response_spectrum<'py>(&self, py: Python<'py>, ground_accel: Vec, dt: f64, zeta: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.response_spectrum(&ground_accel, dt, zeta))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) + } + + /// Modal assurance criterion between two mode shapes. + /// + /// Rust: `resonance::structural::ModalModel::mac` + #[pyo3(name = "mac")] + #[staticmethod] + #[pyo3(signature = (phi1, phi2))] + fn mac<'py>(py: Python<'py>, phi1: Vec, phi2: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::resonance::structural::ModalModel::mac(&phi1, &phi2))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Campbell diagram data: (rpm, natural frequencies in Hz) over a + /// speed range (structure frequencies here are speed-independent; + /// intersect with the order lines to find criticals). + /// + /// Rust: `resonance::structural::ModalModel::campbell_diagram` + #[pyo3(name = "campbell_diagram")] + #[pyo3(signature = (rpm_range, n, orders))] + fn campbell_diagram<'py>(&self, py: Python<'py>, rpm_range: (f64, f64), n: usize, orders: Vec) -> PyResult)>> { + let rpm_range = (rpm_range.0, rpm_range.1); + let __r = py.detach(move || crate::runtime::guard(move || self.inner.campbell_diagram(rpm_range, n, &orders))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) + } + + /// Critical speeds (rpm) where an excitation order line crosses a + /// natural frequency: rpm = 60·fᵢ/order. + /// + /// Rust: `resonance::structural::ModalModel::critical_speeds` + #[pyo3(name = "critical_speeds")] + #[pyo3(signature = (orders))] + fn critical_speeds<'py>(&self, py: Python<'py>, orders: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.critical_speeds(&orders))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Relative frequency margin of each excitation to the nearest + /// natural frequency: min |f_exc − fᵢ|/fᵢ. + /// + /// Rust: `resonance::structural::ModalModel::resonance_margins` + #[pyo3(name = "resonance_margins")] + #[pyo3(signature = (excitation_freqs))] + fn resonance_margins<'py>(&self, py: Python<'py>, excitation_freqs: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.resonance_margins(&excitation_freqs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "m")] + fn py_get_m(&self) -> PyResult { Ok(crate::generated::types::PyMatrix { inner: self.inner.m.clone() }) } + + #[getter] + #[pyo3(name = "c")] + fn py_get_c(&self) -> PyResult { Ok(crate::generated::types::PyMatrix { inner: self.inner.c.clone() }) } + + #[getter] + #[pyo3(name = "k")] + fn py_get_k(&self) -> PyResult { Ok(crate::generated::types::PyMatrix { inner: self.inner.k.clone() }) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("ModalModel", "ModalModel", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} diff --git a/bindings/python/src/generated/types/sim.rs b/bindings/python/src/generated/types/sim.rs new file mode 100644 index 0000000..6d695c4 --- /dev/null +++ b/bindings/python/src/generated/types/sim.rs @@ -0,0 +1,1996 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; + + +/// +/// Rust: `sim::cloth_sim::MassSpringSystem` +#[pyclass(name = "MassSpringSystem", module = "numeria.sim.cloth_sim", from_py_object)] +#[derive(Clone)] +pub struct PyMassSpringSystem { pub inner: rust_physics_engine::sim::cloth_sim::MassSpringSystem } +#[pymethods] +impl PyMassSpringSystem { + /// Create an empty mass-spring system with the given gravity vector. + /// + /// Rust: `sim::cloth_sim::MassSpringSystem::new` + #[new] + #[pyo3(signature = (gravity))] + fn __new__(gravity: crate::generated::types::PyVec3Arg) -> PyResult { + let gravity = gravity.0; + let __r = crate::runtime::guard(|| rust_physics_engine::sim::cloth_sim::MassSpringSystem::new(gravity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMassSpringSystem { inner: __v }) + } + + /// Add a particle to the system, returning its index. + /// + /// Rust: `sim::cloth_sim::MassSpringSystem::add_particle` + #[pyo3(name = "add_particle")] + #[pyo3(signature = (p))] + fn add_particle(&mut self, p: crate::generated::types::PyParticle) -> PyResult { + let p = p.inner; + let __r = crate::runtime::guard(|| self.inner.add_particle(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Add a spring constraint to the system. + /// + /// Rust: `sim::cloth_sim::MassSpringSystem::add_spring` + #[pyo3(name = "add_spring")] + #[pyo3(signature = (s))] + fn add_spring(&mut self, s: crate::generated::types::PySpring) -> PyResult<()> { + let s = s.inner; + let __r = crate::runtime::guard(|| self.inner.add_spring(s)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Verlet integration with Hooke's law spring forces and viscous damping. + /// + /// F_spring = -k (|dx| - L0) * dx_hat + /// F_damp = -d (v_rel . dx_hat) * dx_hat + /// F_grav = m * g + /// + /// Verlet update: new_pos = 2*pos - prev_pos + a*dt^2 + /// + /// Rust: `sim::cloth_sim::MassSpringSystem::step_verlet` + #[pyo3(name = "step_verlet")] + #[pyo3(signature = (dt))] + fn step_verlet(&mut self, dt: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step_verlet(dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Position-based dynamics (Jakobsen method). + /// + /// Performs a Verlet step with only gravity (no spring forces), then + /// iteratively projects particle positions to satisfy distance constraints. + /// + /// Rust: `sim::cloth_sim::MassSpringSystem::step_with_constraints` + #[pyo3(name = "step_with_constraints")] + #[pyo3(signature = (dt, iterations))] + fn step_with_constraints(&mut self, dt: f64, iterations: usize) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step_with_constraints(dt, iterations)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Total mechanical energy: KE + PE_gravity + PE_spring. + /// + /// KE = 0.5 * m * v^2 (velocity estimated from Verlet) + /// PE_gravity = m * (g . pos) (dot product to handle arbitrary gravity direction) + /// PE_spring = 0.5 * k * (|dx| - L0)^2 + /// + /// Rust: `sim::cloth_sim::MassSpringSystem::total_energy` + #[pyo3(name = "total_energy")] + #[pyo3(signature = (dt))] + fn total_energy(&self, dt: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.total_energy(dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Total linear momentum: sum of m * v for all non-pinned particles. + /// + /// Rust: `sim::cloth_sim::MassSpringSystem::total_momentum` + #[pyo3(name = "total_momentum")] + #[pyo3(signature = (dt))] + fn total_momentum(&self, dt: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.total_momentum(dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + #[getter] + #[pyo3(name = "particles")] + fn py_get_particles(&self) -> PyResult> { Ok(self.inner.particles.clone().into_iter().map(|__x| crate::generated::types::PyParticle { inner: __x }).collect::>()) } + + #[getter] + #[pyo3(name = "springs")] + fn py_get_springs(&self) -> PyResult> { Ok(self.inner.springs.clone().into_iter().map(|__x| crate::generated::types::PySpring { inner: __x }).collect::>()) } + + #[getter] + #[pyo3(name = "gravity")] + fn py_get_gravity(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.gravity.clone() }) } + + #[getter] + #[pyo3(name = "time")] + fn py_get_time(&self) -> PyResult { Ok(self.inner.time) } + + #[setter] + #[pyo3(name = "time")] + fn py_set_time(&mut self, v: f64) { self.inner.time = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("MassSpringSystem", "MassSpringSystem", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// +/// Rust: `sim::cloth_sim::Particle` +#[pyclass(name = "Particle", module = "numeria.sim.cloth_sim", from_py_object)] +#[derive(Clone)] +pub struct PyParticle { pub inner: rust_physics_engine::sim::cloth_sim::Particle } +#[pymethods] +impl PyParticle { + /// Create a free particle at the given position with the given mass. + /// + /// Rust: `sim::cloth_sim::Particle::new` + #[new] + #[pyo3(signature = (position, mass))] + fn __new__(position: crate::generated::types::PyVec3Arg, mass: f64) -> PyResult { + let position = position.0; + let __r = crate::runtime::guard(|| rust_physics_engine::sim::cloth_sim::Particle::new(position, mass)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyParticle { inner: __v }) + } + + /// Create a pinned (immovable) particle at the given position. + /// + /// Rust: `sim::cloth_sim::Particle::new_pinned` + #[pyo3(name = "new_pinned")] + #[staticmethod] + #[pyo3(signature = (position))] + fn new_pinned(position: crate::generated::types::PyVec3Arg) -> PyResult { + let position = position.0; + let __r = crate::runtime::guard(|| rust_physics_engine::sim::cloth_sim::Particle::new_pinned(position)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyParticle { inner: __v }) + } + + #[getter] + #[pyo3(name = "position")] + fn py_get_position(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.position.clone() }) } + + #[getter] + #[pyo3(name = "previous_position")] + fn py_get_previous_position(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.previous_position.clone() }) } + + #[getter] + #[pyo3(name = "acceleration")] + fn py_get_acceleration(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.acceleration.clone() }) } + + #[getter] + #[pyo3(name = "mass")] + fn py_get_mass(&self) -> PyResult { Ok(self.inner.mass) } + + #[setter] + #[pyo3(name = "mass")] + fn py_set_mass(&mut self, v: f64) { self.inner.mass = v; } + + #[getter] + #[pyo3(name = "pinned")] + fn py_get_pinned(&self) -> PyResult { Ok(self.inner.pinned) } + + #[setter] + #[pyo3(name = "pinned")] + fn py_set_pinned(&mut self, v: bool) { self.inner.pinned = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Particle", "Particle", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// +/// Rust: `sim::cloth_sim::Spring` +#[pyclass(name = "Spring", module = "numeria.sim.cloth_sim", from_py_object)] +#[derive(Clone)] +pub struct PySpring { pub inner: rust_physics_engine::sim::cloth_sim::Spring } +#[pymethods] +impl PySpring { + /// Create a spring connecting particles a and b with given rest length, stiffness, and damping. + /// + /// Rust: `sim::cloth_sim::Spring::new` + #[new] + #[pyo3(signature = (a, b, rest_length, stiffness, damping))] + fn __new__(a: usize, b: usize, rest_length: f64, stiffness: f64, damping: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::sim::cloth_sim::Spring::new(a, b, rest_length, stiffness, damping)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySpring { inner: __v }) + } + + #[getter] + #[pyo3(name = "particle_a")] + fn py_get_particle_a(&self) -> PyResult { Ok(self.inner.particle_a) } + + #[setter] + #[pyo3(name = "particle_a")] + fn py_set_particle_a(&mut self, v: usize) { self.inner.particle_a = v; } + + #[getter] + #[pyo3(name = "particle_b")] + fn py_get_particle_b(&self) -> PyResult { Ok(self.inner.particle_b) } + + #[setter] + #[pyo3(name = "particle_b")] + fn py_set_particle_b(&mut self, v: usize) { self.inner.particle_b = v; } + + #[getter] + #[pyo3(name = "rest_length")] + fn py_get_rest_length(&self) -> PyResult { Ok(self.inner.rest_length) } + + #[setter] + #[pyo3(name = "rest_length")] + fn py_set_rest_length(&mut self, v: f64) { self.inner.rest_length = v; } + + #[getter] + #[pyo3(name = "stiffness")] + fn py_get_stiffness(&self) -> PyResult { Ok(self.inner.stiffness) } + + #[setter] + #[pyo3(name = "stiffness")] + fn py_set_stiffness(&mut self, v: f64) { self.inner.stiffness = v; } + + #[getter] + #[pyo3(name = "damping")] + fn py_get_damping(&self) -> PyResult { Ok(self.inner.damping) } + + #[setter] + #[pyo3(name = "damping")] + fn py_set_damping(&mut self, v: f64) { self.inner.damping = v; } + + fn __repr__(&self) -> String { format!("Spring(particle_a={:?}, particle_b={:?}, rest_length={:?}, stiffness={:?}, damping={:?})", self.inner.particle_a, self.inner.particle_b, self.inner.rest_length, self.inner.stiffness, self.inner.damping) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// +/// Rust: `sim::em_sim::Fdtd1D` +#[pyclass(name = "Fdtd1D", module = "numeria.sim.em_sim")] +pub struct PyFdtd1D { pub inner: rust_physics_engine::sim::em_sim::Fdtd1D } +#[pymethods] +impl PyFdtd1D { + /// Create a 1D FDTD simulation domain with nx cells and spacing dx. + /// + /// Rust: `sim::em_sim::Fdtd1D::new` + #[new] + #[pyo3(signature = (nx, dx))] + fn __new__(nx: usize, dx: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::sim::em_sim::Fdtd1D::new(nx, dx)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyFdtd1D { inner: __v }) + } + + /// Set relative permittivity, permeability, and conductivity for a range of cells. + /// + /// Rust: `sim::em_sim::Fdtd1D::set_material` + #[pyo3(name = "set_material")] + #[pyo3(signature = (start, end, epsilon_r, mu_r, sigma))] + fn set_material(&mut self, start: usize, end: usize, epsilon_r: f64, mu_r: f64, sigma: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.set_material(start, end, epsilon_r, mu_r, sigma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Advance one FDTD leapfrog time step updating Hy then Ez with lossy material support. + /// + /// Rust: `sim::em_sim::Fdtd1D::step` + #[pyo3(name = "step")] + #[pyo3(signature = ())] + fn step(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Add value to Ez at position (soft source, allows wave passage). + /// + /// Rust: `sim::em_sim::Fdtd1D::add_source_soft` + #[pyo3(name = "add_source_soft")] + #[pyo3(signature = (position, value))] + fn add_source_soft(&mut self, position: usize, value: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.add_source_soft(position, value)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Set Ez at position to value (hard source, creates reflections). + /// + /// Rust: `sim::em_sim::Fdtd1D::add_source_hard` + #[pyo3(name = "add_source_hard")] + #[pyo3(signature = (position, value))] + fn add_source_hard(&mut self, position: usize, value: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.add_source_hard(position, value)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Gaussian pulse: exp(-((t - t0)/spread)²) + /// + /// Rust: `sim::em_sim::Fdtd1D::gaussian_pulse` + #[pyo3(name = "gaussian_pulse")] + #[staticmethod] + #[pyo3(signature = (t, t0, spread))] + fn gaussian_pulse(t: f64, t0: f64, spread: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::sim::em_sim::Fdtd1D::gaussian_pulse(t, t0, spread)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Sinusoidal source: sin(2πft) + /// + /// Rust: `sim::em_sim::Fdtd1D::sinusoidal_source` + #[pyo3(name = "sinusoidal_source")] + #[staticmethod] + #[pyo3(signature = (t, frequency))] + fn sinusoidal_source(t: f64, frequency: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::sim::em_sim::Fdtd1D::sinusoidal_source(t, frequency)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Apply first-order Mur absorbing boundary conditions at both ends. + /// + /// Rust: `sim::em_sim::Fdtd1D::apply_abc_mur` + #[pyo3(name = "apply_abc_mur")] + #[pyo3(signature = ())] + fn apply_abc_mur(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.apply_abc_mur()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Total electromagnetic energy: E = ½Σ(ε₀εᵣEz² + μ₀μᵣHy²)dx + /// + /// Rust: `sim::em_sim::Fdtd1D::total_energy` + #[pyo3(name = "total_energy")] + #[pyo3(signature = ())] + fn total_energy(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.total_energy()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// CFL-stable time step for 1D FDTD: dt = dx·Courant/(c) + /// + /// Rust: `sim::em_sim::Fdtd1D::stable_dt_for_dx` + #[pyo3(name = "stable_dt_for_dx")] + #[staticmethod] + #[pyo3(signature = (dx))] + fn stable_dt_for_dx(dx: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::sim::em_sim::Fdtd1D::stable_dt_for_dx(dx)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "ez")] + fn py_get_ez(&self) -> PyResult> { Ok(self.inner.ez.clone()) } + + #[getter] + #[pyo3(name = "hy")] + fn py_get_hy(&self) -> PyResult> { Ok(self.inner.hy.clone()) } + + #[getter] + #[pyo3(name = "nx")] + fn py_get_nx(&self) -> PyResult { Ok(self.inner.nx) } + + #[setter] + #[pyo3(name = "nx")] + fn py_set_nx(&mut self, v: usize) { self.inner.nx = v; } + + #[getter] + #[pyo3(name = "dx")] + fn py_get_dx(&self) -> PyResult { Ok(self.inner.dx) } + + #[setter] + #[pyo3(name = "dx")] + fn py_set_dx(&mut self, v: f64) { self.inner.dx = v; } + + #[getter] + #[pyo3(name = "dt")] + fn py_get_dt(&self) -> PyResult { Ok(self.inner.dt) } + + #[setter] + #[pyo3(name = "dt")] + fn py_set_dt(&mut self, v: f64) { self.inner.dt = v; } + + #[getter] + #[pyo3(name = "epsilon")] + fn py_get_epsilon(&self) -> PyResult> { Ok(self.inner.epsilon.clone()) } + + #[getter] + #[pyo3(name = "mu")] + fn py_get_mu(&self) -> PyResult> { Ok(self.inner.mu.clone()) } + + #[getter] + #[pyo3(name = "conductivity")] + fn py_get_conductivity(&self) -> PyResult> { Ok(self.inner.conductivity.clone()) } + + #[getter] + #[pyo3(name = "time")] + fn py_get_time(&self) -> PyResult { Ok(self.inner.time) } + + #[setter] + #[pyo3(name = "time")] + fn py_set_time(&mut self, v: f64) { self.inner.time = v; } + + #[getter] + #[pyo3(name = "time_step")] + fn py_get_time_step(&self) -> PyResult { Ok(self.inner.time_step) } + + #[setter] + #[pyo3(name = "time_step")] + fn py_set_time_step(&mut self, v: u64) { self.inner.time_step = v; } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// +/// Rust: `sim::em_sim::Fdtd2D` +#[pyclass(name = "Fdtd2D", module = "numeria.sim.em_sim")] +pub struct PyFdtd2D { pub inner: rust_physics_engine::sim::em_sim::Fdtd2D } +#[pymethods] +impl PyFdtd2D { + /// Create a 2D FDTD simulation domain with nx-by-ny cells. + /// + /// Rust: `sim::em_sim::Fdtd2D::new` + #[new] + #[pyo3(signature = (nx, ny, dx, dy))] + fn __new__(nx: usize, ny: usize, dx: f64, dy: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::sim::em_sim::Fdtd2D::new(nx, ny, dx, dy)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyFdtd2D { inner: __v }) + } + + /// Advance one 2D FDTD leapfrog time step updating Hx, Hy, then Ez. + /// + /// Rust: `sim::em_sim::Fdtd2D::step` + #[pyo3(name = "step")] + #[pyo3(signature = ())] + fn step(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Add a soft point source to Ez at grid point (i, j). + /// + /// Rust: `sim::em_sim::Fdtd2D::add_source` + #[pyo3(name = "add_source")] + #[pyo3(signature = (i, j, value))] + fn add_source(&mut self, i: usize, j: usize, value: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.add_source(i, j, value)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Total electromagnetic energy: E = ½Σ(ε₀εᵣEz² + μ₀(Hx² + Hy²))·dx·dy + /// + /// Rust: `sim::em_sim::Fdtd2D::total_energy` + #[pyo3(name = "total_energy")] + #[pyo3(signature = ())] + fn total_energy(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.total_energy()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// CFL-stable time step for 2D FDTD: dt = Courant/(c·√(1/dx² + 1/dy²)) + /// + /// Rust: `sim::em_sim::Fdtd2D::stable_dt_for_grid` + #[pyo3(name = "stable_dt_for_grid")] + #[staticmethod] + #[pyo3(signature = (dx, dy))] + fn stable_dt_for_grid(dx: f64, dy: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::sim::em_sim::Fdtd2D::stable_dt_for_grid(dx, dy)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "ez")] + fn py_get_ez(&self) -> PyResult> { Ok(self.inner.ez.clone()) } + + #[getter] + #[pyo3(name = "hx")] + fn py_get_hx(&self) -> PyResult> { Ok(self.inner.hx.clone()) } + + #[getter] + #[pyo3(name = "hy")] + fn py_get_hy(&self) -> PyResult> { Ok(self.inner.hy.clone()) } + + #[getter] + #[pyo3(name = "nx")] + fn py_get_nx(&self) -> PyResult { Ok(self.inner.nx) } + + #[setter] + #[pyo3(name = "nx")] + fn py_set_nx(&mut self, v: usize) { self.inner.nx = v; } + + #[getter] + #[pyo3(name = "ny")] + fn py_get_ny(&self) -> PyResult { Ok(self.inner.ny) } + + #[setter] + #[pyo3(name = "ny")] + fn py_set_ny(&mut self, v: usize) { self.inner.ny = v; } + + #[getter] + #[pyo3(name = "dx")] + fn py_get_dx(&self) -> PyResult { Ok(self.inner.dx) } + + #[setter] + #[pyo3(name = "dx")] + fn py_set_dx(&mut self, v: f64) { self.inner.dx = v; } + + #[getter] + #[pyo3(name = "dy")] + fn py_get_dy(&self) -> PyResult { Ok(self.inner.dy) } + + #[setter] + #[pyo3(name = "dy")] + fn py_set_dy(&mut self, v: f64) { self.inner.dy = v; } + + #[getter] + #[pyo3(name = "dt")] + fn py_get_dt(&self) -> PyResult { Ok(self.inner.dt) } + + #[setter] + #[pyo3(name = "dt")] + fn py_set_dt(&mut self, v: f64) { self.inner.dt = v; } + + #[getter] + #[pyo3(name = "epsilon")] + fn py_get_epsilon(&self) -> PyResult> { Ok(self.inner.epsilon.clone()) } + + #[getter] + #[pyo3(name = "time_step")] + fn py_get_time_step(&self) -> PyResult { Ok(self.inner.time_step) } + + #[setter] + #[pyo3(name = "time_step")] + fn py_set_time_step(&mut self, v: u64) { self.inner.time_step = v; } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// +/// Rust: `sim::fluid_sim::ColumnFluid` +#[pyclass(name = "ColumnFluid", module = "numeria.sim.fluid_sim")] +pub struct PyColumnFluid { pub inner: rust_physics_engine::sim::fluid_sim::ColumnFluid } +#[pymethods] +impl PyColumnFluid { + /// Create a column fluid with the given number of columns, spacing, density, and gravity. + /// + /// Rust: `sim::fluid_sim::ColumnFluid::new` + #[new] + #[pyo3(signature = (width, dx, density, g))] + fn __new__(width: usize, dx: f64, density: f64, g: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::sim::fluid_sim::ColumnFluid::new(width, dx, density, g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyColumnFluid { inner: __v }) + } + + /// Set the water height in a specific column. + /// + /// Rust: `sim::fluid_sim::ColumnFluid::set_height` + #[pyo3(name = "set_height")] + #[pyo3(signature = (col, h))] + fn set_height(&mut self, col: usize, h: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.set_height(col, h)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Advance one time step using pressure-driven orifice flow between + /// adjacent columns. + /// + /// Flow rate from column i to i+1 (Torricelli with discharge coeff): + /// Q = C_D × dx × sign(Δh) × √(2g|Δh|) + /// + /// Volume transferred per step = Q × dt. Height change: + /// Δh_i = -(Q_right - Q_left) × dt / dx + /// + /// Heights are clamped to zero to prevent negative water. + /// + /// Rust: `sim::fluid_sim::ColumnFluid::step` + #[pyo3(name = "step")] + #[pyo3(signature = (dt))] + fn step(&mut self, dt: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step(dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Total fluid volume: V = Σ h_i × dx (m² in 2D cross-section). + /// + /// Rust: `sim::fluid_sim::ColumnFluid::total_volume` + #[pyo3(name = "total_volume")] + #[pyo3(signature = ())] + fn total_volume(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.total_volume()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "heights")] + fn py_get_heights(&self) -> PyResult> { Ok(self.inner.heights.clone()) } + + #[getter] + #[pyo3(name = "width")] + fn py_get_width(&self) -> PyResult { Ok(self.inner.width) } + + #[setter] + #[pyo3(name = "width")] + fn py_set_width(&mut self, v: usize) { self.inner.width = v; } + + #[getter] + #[pyo3(name = "dx")] + fn py_get_dx(&self) -> PyResult { Ok(self.inner.dx) } + + #[setter] + #[pyo3(name = "dx")] + fn py_set_dx(&mut self, v: f64) { self.inner.dx = v; } + + #[getter] + #[pyo3(name = "density")] + fn py_get_density(&self) -> PyResult { Ok(self.inner.density) } + + #[setter] + #[pyo3(name = "density")] + fn py_set_density(&mut self, v: f64) { self.inner.density = v; } + + #[getter] + #[pyo3(name = "g")] + fn py_get_g(&self) -> PyResult { Ok(self.inner.g) } + + #[setter] + #[pyo3(name = "g")] + fn py_set_g(&mut self, v: f64) { self.inner.g = v; } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// +/// Rust: `sim::fluid_sim::EulerFluid2D` +#[pyclass(name = "EulerFluid2D", module = "numeria.sim.fluid_sim")] +pub struct PyEulerFluid2D { pub inner: rust_physics_engine::sim::fluid_sim::EulerFluid2D } +#[pymethods] +impl PyEulerFluid2D { + /// Create a 2D incompressible Euler fluid solver on an nx-by-ny grid. + /// + /// Rust: `sim::fluid_sim::EulerFluid2D::new` + #[new] + #[pyo3(signature = (nx, ny, dx, dy, density))] + fn __new__(nx: usize, ny: usize, dx: f64, dy: f64, density: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::sim::fluid_sim::EulerFluid2D::new(nx, ny, dx, dy, density)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyEulerFluid2D { inner: __v }) + } + + /// Set the velocity at grid point (i, j). + /// + /// Rust: `sim::fluid_sim::EulerFluid2D::set_velocity` + #[pyo3(name = "set_velocity")] + #[pyo3(signature = (i, j, vx, vy))] + fn set_velocity(&mut self, i: usize, j: usize, vx: f64, vy: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.set_velocity(i, j, vx, vy)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// One full time step via Chorin's projection method. + /// + /// 1. Advect with first-order upwind + add body force. + /// 2. Solve ∇²p = (ρ/dt)∇·u* via Jacobi iteration. + /// 3. Correct: u^{n+1} = u* - (dt/ρ)∇p. + /// + /// Rust: `sim::fluid_sim::EulerFluid2D::step` + #[pyo3(name = "step")] + #[pyo3(signature = (dt, gravity_x, gravity_y))] + fn step(&mut self, dt: f64, gravity_x: f64, gravity_y: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step(dt, gravity_x, gravity_y)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Maximum absolute divergence: max |∇·u|. + /// Should be near zero after a projection step. + /// + /// Rust: `sim::fluid_sim::EulerFluid2D::divergence` + #[pyo3(name = "divergence")] + #[pyo3(signature = ())] + fn divergence(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.divergence()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Total kinetic energy: KE = ½ρ Σ (vx² + vy²) dx dy. + /// + /// Rust: `sim::fluid_sim::EulerFluid2D::kinetic_energy` + #[pyo3(name = "kinetic_energy")] + #[pyo3(signature = ())] + fn kinetic_energy(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.kinetic_energy()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "vx")] + fn py_get_vx(&self) -> PyResult> { Ok(self.inner.vx.clone()) } + + #[getter] + #[pyo3(name = "vy")] + fn py_get_vy(&self) -> PyResult> { Ok(self.inner.vy.clone()) } + + #[getter] + #[pyo3(name = "pressure")] + fn py_get_pressure(&self) -> PyResult> { Ok(self.inner.pressure.clone()) } + + #[getter] + #[pyo3(name = "nx")] + fn py_get_nx(&self) -> PyResult { Ok(self.inner.nx) } + + #[setter] + #[pyo3(name = "nx")] + fn py_set_nx(&mut self, v: usize) { self.inner.nx = v; } + + #[getter] + #[pyo3(name = "ny")] + fn py_get_ny(&self) -> PyResult { Ok(self.inner.ny) } + + #[setter] + #[pyo3(name = "ny")] + fn py_set_ny(&mut self, v: usize) { self.inner.ny = v; } + + #[getter] + #[pyo3(name = "dx")] + fn py_get_dx(&self) -> PyResult { Ok(self.inner.dx) } + + #[setter] + #[pyo3(name = "dx")] + fn py_set_dx(&mut self, v: f64) { self.inner.dx = v; } + + #[getter] + #[pyo3(name = "dy")] + fn py_get_dy(&self) -> PyResult { Ok(self.inner.dy) } + + #[setter] + #[pyo3(name = "dy")] + fn py_set_dy(&mut self, v: f64) { self.inner.dy = v; } + + #[getter] + #[pyo3(name = "density")] + fn py_get_density(&self) -> PyResult { Ok(self.inner.density) } + + #[setter] + #[pyo3(name = "density")] + fn py_set_density(&mut self, v: f64) { self.inner.density = v; } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// +/// Rust: `sim::fluid_sim::ShallowWater1D` +#[pyclass(name = "ShallowWater1D", module = "numeria.sim.fluid_sim")] +pub struct PyShallowWater1D { pub inner: rust_physics_engine::sim::fluid_sim::ShallowWater1D } +#[pymethods] +impl PyShallowWater1D { + /// Create a 1D shallow water solver with nx cells, spacing dx, and gravity g. + /// + /// Rust: `sim::fluid_sim::ShallowWater1D::new` + #[new] + #[pyo3(signature = (nx, dx, g))] + fn __new__(nx: usize, dx: f64, g: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::sim::fluid_sim::ShallowWater1D::new(nx, dx, g)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyShallowWater1D { inner: __v }) + } + + /// Recover velocity u = hu/h, returning 0 for dry cells. + /// + /// Rust: `sim::fluid_sim::ShallowWater1D::velocity` + #[pyo3(name = "velocity")] + #[pyo3(signature = (i))] + fn velocity(&self, i: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.velocity(i)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// HLL (Harten-Lax-van Leer) time step with reflective boundary + /// conditions — the Part 3 rewire onto the CFD module's Riemann + /// machinery. Sharper than `Self::step_lax_friedrichs` on bores. + /// + /// Rust: `sim::fluid_sim::ShallowWater1D::step_hll` + #[pyo3(name = "step_hll")] + #[pyo3(signature = (dt))] + fn step_hll(&mut self, dt: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step_hll(dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Lax-Friedrichs time step with reflective boundary conditions. + /// + /// Scheme (interior, 1 ≤ i ≤ nx-2): + /// h_i^{n+1} = ½(h_{i-1} + h_{i+1}) - dt/(2dx)(hu_{i+1} - hu_{i-1}) + /// hu_i^{n+1} = ½(hu_{i-1}+ hu_{i+1}) - dt/(2dx)(F_{i+1} - F_{i-1}) + /// + /// Boundaries (i=0, i=nx-1): reflective ghost cells. + /// + /// Rust: `sim::fluid_sim::ShallowWater1D::step_lax_friedrichs` + #[pyo3(name = "step_lax_friedrichs")] + #[pyo3(signature = (dt))] + fn step_lax_friedrichs(&mut self, dt: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step_lax_friedrichs(dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Maximum wave speed: max_i(|u_i| + √(g h_i)). + /// This is the largest eigenvalue of the flux Jacobian across all cells. + /// + /// Rust: `sim::fluid_sim::ShallowWater1D::max_wave_speed` + #[pyo3(name = "max_wave_speed")] + #[pyo3(signature = ())] + fn max_wave_speed(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.max_wave_speed()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// CFL-limited stable time step: dt = CFL_SAFETY × dx / max_wave_speed. + /// + /// Rust: `sim::fluid_sim::ShallowWater1D::stable_dt` + #[pyo3(name = "stable_dt")] + #[pyo3(signature = ())] + fn stable_dt(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.stable_dt()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Total volume: V = Σ h_i × dx. + /// + /// Rust: `sim::fluid_sim::ShallowWater1D::total_volume` + #[pyo3(name = "total_volume")] + #[pyo3(signature = ())] + fn total_volume(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.total_volume()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Total energy (mechanical): E = Σ (½ h u² + ½ g h²) dx. + /// + /// First term is depth-integrated kinetic energy per unit width, + /// second is potential energy (∫₀ʰ ρg z dz = ½ρg h², with ρ=1 in + /// shallow water non-dimensionalization — we include g explicitly). + /// + /// Rust: `sim::fluid_sim::ShallowWater1D::total_energy` + #[pyo3(name = "total_energy")] + #[pyo3(signature = ())] + fn total_energy(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.total_energy()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "h")] + fn py_get_h(&self) -> PyResult> { Ok(self.inner.h.clone()) } + + #[getter] + #[pyo3(name = "hu")] + fn py_get_hu(&self) -> PyResult> { Ok(self.inner.hu.clone()) } + + #[getter] + #[pyo3(name = "nx")] + fn py_get_nx(&self) -> PyResult { Ok(self.inner.nx) } + + #[setter] + #[pyo3(name = "nx")] + fn py_set_nx(&mut self, v: usize) { self.inner.nx = v; } + + #[getter] + #[pyo3(name = "dx")] + fn py_get_dx(&self) -> PyResult { Ok(self.inner.dx) } + + #[setter] + #[pyo3(name = "dx")] + fn py_set_dx(&mut self, v: f64) { self.inner.dx = v; } + + #[getter] + #[pyo3(name = "g")] + fn py_get_g(&self) -> PyResult { Ok(self.inner.g) } + + #[setter] + #[pyo3(name = "g")] + fn py_set_g(&mut self, v: f64) { self.inner.g = v; } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// 1D convection-diffusion (advection-diffusion) solver. +/// +/// PDE: ∂T/∂t + v·∂T/∂x = α·∂²T/∂x² +/// +/// Uses first-order upwind for the advection term and central differencing +/// for the diffusion term. +/// +/// Rust: `sim::heat_sim::ConvectionDiffusion1D` +#[pyclass(name = "ConvectionDiffusion1D", module = "numeria.sim.heat_sim")] +pub struct PyConvectionDiffusion1D { pub inner: rust_physics_engine::sim::heat_sim::ConvectionDiffusion1D } +#[pymethods] +impl PyConvectionDiffusion1D { + /// Create a 1D convection-diffusion solver with the given parameters. + /// + /// Rust: `sim::heat_sim::ConvectionDiffusion1D::new` + #[new] + #[pyo3(signature = (nx, dx, velocity, diffusivity))] + fn __new__(nx: usize, dx: f64, velocity: f64, diffusivity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::sim::heat_sim::ConvectionDiffusion1D::new(nx, dx, velocity, diffusivity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyConvectionDiffusion1D { inner: __v }) + } + + /// Upwind advection + central diffusion explicit step. + /// Boundaries held fixed (Dirichlet). + /// + /// Stability requires dt < stable_dt(). + /// + /// Rust: `sim::heat_sim::ConvectionDiffusion1D::step_upwind` + #[pyo3(name = "step_upwind")] + #[pyo3(signature = (dt))] + fn step_upwind(&mut self, dt: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step_upwind(dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Grid Peclet number: Pe = v·dx/α. + /// For stability of central differencing the advection term, Pe < 2 is + /// required. The upwind scheme used here is stable for any Pe but + /// introduces numerical diffusion proportional to Pe. + /// + /// Rust: `sim::heat_sim::ConvectionDiffusion1D::peclet_number` + #[pyo3(name = "peclet_number")] + #[pyo3(signature = ())] + fn peclet_number(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.peclet_number()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Maximum stable time step. + /// CFL condition: dt < dx/|v| AND diffusive limit: dt < dx²/(2α). + /// Returns the minimum of both limits. + /// + /// Rust: `sim::heat_sim::ConvectionDiffusion1D::stable_dt` + #[pyo3(name = "stable_dt")] + #[pyo3(signature = ())] + fn stable_dt(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.stable_dt()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "field")] + fn py_get_field(&self) -> PyResult> { Ok(self.inner.field.clone()) } + + #[getter] + #[pyo3(name = "nx")] + fn py_get_nx(&self) -> PyResult { Ok(self.inner.nx) } + + #[setter] + #[pyo3(name = "nx")] + fn py_set_nx(&mut self, v: usize) { self.inner.nx = v; } + + #[getter] + #[pyo3(name = "dx")] + fn py_get_dx(&self) -> PyResult { Ok(self.inner.dx) } + + #[setter] + #[pyo3(name = "dx")] + fn py_set_dx(&mut self, v: f64) { self.inner.dx = v; } + + #[getter] + #[pyo3(name = "velocity")] + fn py_get_velocity(&self) -> PyResult { Ok(self.inner.velocity) } + + #[setter] + #[pyo3(name = "velocity")] + fn py_set_velocity(&mut self, v: f64) { self.inner.velocity = v; } + + #[getter] + #[pyo3(name = "diffusivity")] + fn py_get_diffusivity(&self) -> PyResult { Ok(self.inner.diffusivity) } + + #[setter] + #[pyo3(name = "diffusivity")] + fn py_set_diffusivity(&mut self, v: f64) { self.inner.diffusivity = v; } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// Heat conduction and convection-diffusion on a grid. +/// +/// Explicit finite differences in two and three dimensions, with Dirichlet +/// and Neumann boundaries, sources, and an advection term for +/// convection-diffusion. +/// +/// Explicit stepping is only conditionally stable: the step must satisfy +/// `α Δt / Δx² ≤ 1/4` in 2-D and `1/6` in 3-D, so halving the grid spacing +/// quarters the allowable time step. The stability limit is provided as a +/// function rather than left to the caller to remember. +/// 2D heat conduction on a uniform Cartesian grid with Dirichlet boundaries. +/// +/// Rust: `sim::heat_sim::HeatConduction2D` +#[pyclass(name = "HeatConduction2D", module = "numeria.sim.heat_sim")] +pub struct PyHeatConduction2D { pub inner: rust_physics_engine::sim::heat_sim::HeatConduction2D } +#[pymethods] +impl PyHeatConduction2D { + /// Create a grid initialized to uniform temperature 0. + /// + /// Rust: `sim::heat_sim::HeatConduction2D::new` + #[new] + #[pyo3(signature = (nx, ny, dx, dy, diffusivity))] + fn __new__(nx: usize, ny: usize, dx: f64, dy: f64, diffusivity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::sim::heat_sim::HeatConduction2D::new(nx, ny, dx, dy, diffusivity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyHeatConduction2D { inner: __v }) + } + + /// Set temperature at grid point (i, j). + /// + /// Rust: `sim::heat_sim::HeatConduction2D::set_temperature` + #[pyo3(name = "set_temperature")] + #[pyo3(signature = (i, j, temp))] + fn set_temperature(&mut self, i: usize, j: usize, temp: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.set_temperature(i, j, temp)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Get temperature at grid point (i, j). + /// + /// Rust: `sim::heat_sim::HeatConduction2D::get_temperature` + #[pyo3(name = "get_temperature")] + #[pyo3(signature = (i, j))] + fn get_temperature(&self, i: usize, j: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.get_temperature(i, j)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// FTCS explicit step. Boundary cells (i=0, i=nx-1, j=0, j=ny-1) are + /// held fixed (Dirichlet). + /// + /// Stability requires dt < stable_dt(). + /// + /// Rust: `sim::heat_sim::HeatConduction2D::step_explicit` + #[pyo3(name = "step_explicit")] + #[pyo3(signature = (dt))] + fn step_explicit(&mut self, dt: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step_explicit(dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Backward Euler solved via Jacobi iteration (unconditionally stable). + /// + /// Solves: + /// (1 + 2·rx + 2·ry)·T_ij^(n+1) + /// − rx·(T_{i±1,j}^(n+1)) − ry·(T_{i,j±1}^(n+1)) = T_ij^n + /// where rx = α·dt/dx², ry = α·dt/dy². + /// + /// Rust: `sim::heat_sim::HeatConduction2D::step_implicit_jacobi` + #[pyo3(name = "step_implicit_jacobi")] + #[pyo3(signature = (dt, iterations))] + fn step_implicit_jacobi(&mut self, dt: f64, iterations: usize) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step_implicit_jacobi(dt, iterations)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Maximum stable time step for the explicit FTCS scheme. + /// dt_max = 1 / (2α·(1/dx² + 1/dy²)) + /// + /// Rust: `sim::heat_sim::HeatConduction2D::stable_dt` + #[pyo3(name = "stable_dt")] + #[pyo3(signature = ())] + fn stable_dt(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.stable_dt()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Total thermal energy proxy: Σ T_ij · dx · dy. + /// Proportional to total thermal energy when ρcₚ is uniform. + /// + /// Rust: `sim::heat_sim::HeatConduction2D::total_energy` + #[pyo3(name = "total_energy")] + #[pyo3(signature = ())] + fn total_energy(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.total_energy()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Maximum temperature in the field. + /// + /// Rust: `sim::heat_sim::HeatConduction2D::max_temperature` + #[pyo3(name = "max_temperature")] + #[pyo3(signature = ())] + fn max_temperature(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.max_temperature()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Minimum temperature in the field. + /// + /// Rust: `sim::heat_sim::HeatConduction2D::min_temperature` + #[pyo3(name = "min_temperature")] + #[pyo3(signature = ())] + fn min_temperature(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.min_temperature()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Average temperature across the entire grid. + /// + /// Rust: `sim::heat_sim::HeatConduction2D::average_temperature` + #[pyo3(name = "average_temperature")] + #[pyo3(signature = ())] + fn average_temperature(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.average_temperature()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Explicit step with volumetric heat source. + /// + /// PDE: ∂T/∂t = α∇²T + Q(x,y)/(ρcₚ) + /// + /// `sources` is the Q/(ρcₚ) term at each grid point, same layout as + /// `temperature` (row-major, length nx*ny). Units: [K/s]. + /// + /// Rust: `sim::heat_sim::HeatConduction2D::step_with_source` + #[pyo3(name = "step_with_source")] + #[pyo3(signature = (dt, sources))] + fn step_with_source<'py>(&mut self, py: Python<'py>, dt: f64, sources: Vec) -> PyResult<()> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.step_with_source(dt, &sources))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + #[getter] + #[pyo3(name = "temperature")] + fn py_get_temperature(&self) -> PyResult> { Ok(self.inner.temperature.clone()) } + + #[getter] + #[pyo3(name = "nx")] + fn py_get_nx(&self) -> PyResult { Ok(self.inner.nx) } + + #[setter] + #[pyo3(name = "nx")] + fn py_set_nx(&mut self, v: usize) { self.inner.nx = v; } + + #[getter] + #[pyo3(name = "ny")] + fn py_get_ny(&self) -> PyResult { Ok(self.inner.ny) } + + #[setter] + #[pyo3(name = "ny")] + fn py_set_ny(&mut self, v: usize) { self.inner.ny = v; } + + #[getter] + #[pyo3(name = "dx")] + fn py_get_dx(&self) -> PyResult { Ok(self.inner.dx) } + + #[setter] + #[pyo3(name = "dx")] + fn py_set_dx(&mut self, v: f64) { self.inner.dx = v; } + + #[getter] + #[pyo3(name = "dy")] + fn py_get_dy(&self) -> PyResult { Ok(self.inner.dy) } + + #[setter] + #[pyo3(name = "dy")] + fn py_set_dy(&mut self, v: f64) { self.inner.dy = v; } + + #[getter] + #[pyo3(name = "diffusivity")] + fn py_get_diffusivity(&self) -> PyResult { Ok(self.inner.diffusivity) } + + #[setter] + #[pyo3(name = "diffusivity")] + fn py_set_diffusivity(&mut self, v: f64) { self.inner.diffusivity = v; } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// 3D heat conduction on a uniform Cartesian grid with Dirichlet boundaries. +/// +/// Rust: `sim::heat_sim::HeatConduction3D` +#[pyclass(name = "HeatConduction3D", module = "numeria.sim.heat_sim")] +pub struct PyHeatConduction3D { pub inner: rust_physics_engine::sim::heat_sim::HeatConduction3D } +#[pymethods] +impl PyHeatConduction3D { + /// Create a 3D heat conduction grid initialized to uniform temperature 0. + /// + /// Rust: `sim::heat_sim::HeatConduction3D::new` + #[new] + #[pyo3(signature = (nx, ny, nz, dx, dy, dz, diffusivity))] + fn __new__(nx: usize, ny: usize, nz: usize, dx: f64, dy: f64, dz: f64, diffusivity: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::sim::heat_sim::HeatConduction3D::new(nx, ny, nz, dx, dy, dz, diffusivity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyHeatConduction3D { inner: __v }) + } + + /// Set temperature at grid point (i, j, k). + /// + /// Rust: `sim::heat_sim::HeatConduction3D::set_temperature` + #[pyo3(name = "set_temperature")] + #[pyo3(signature = (i, j, k, temp))] + fn set_temperature(&mut self, i: usize, j: usize, k: usize, temp: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.set_temperature(i, j, k, temp)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Get temperature at grid point (i, j, k). + /// + /// Rust: `sim::heat_sim::HeatConduction3D::get_temperature` + #[pyo3(name = "get_temperature")] + #[pyo3(signature = (i, j, k))] + fn get_temperature(&self, i: usize, j: usize, k: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.get_temperature(i, j, k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// FTCS explicit step in 3D. Boundary cells held fixed (Dirichlet). + /// + /// Stability requires dt < stable_dt(). + /// + /// Rust: `sim::heat_sim::HeatConduction3D::step_explicit` + #[pyo3(name = "step_explicit")] + #[pyo3(signature = (dt))] + fn step_explicit(&mut self, dt: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step_explicit(dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// dt_max = 1 / (2α·(1/dx² + 1/dy² + 1/dz²)) + /// + /// Rust: `sim::heat_sim::HeatConduction3D::stable_dt` + #[pyo3(name = "stable_dt")] + #[pyo3(signature = ())] + fn stable_dt(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.stable_dt()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Total thermal energy proxy: Σ T·dx·dy·dz. + /// + /// Rust: `sim::heat_sim::HeatConduction3D::total_energy` + #[pyo3(name = "total_energy")] + #[pyo3(signature = ())] + fn total_energy(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.total_energy()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Average temperature across the entire 3D grid. + /// + /// Rust: `sim::heat_sim::HeatConduction3D::average_temperature` + #[pyo3(name = "average_temperature")] + #[pyo3(signature = ())] + fn average_temperature(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.average_temperature()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "temperature")] + fn py_get_temperature(&self) -> PyResult> { Ok(self.inner.temperature.clone()) } + + #[getter] + #[pyo3(name = "nx")] + fn py_get_nx(&self) -> PyResult { Ok(self.inner.nx) } + + #[setter] + #[pyo3(name = "nx")] + fn py_set_nx(&mut self, v: usize) { self.inner.nx = v; } + + #[getter] + #[pyo3(name = "ny")] + fn py_get_ny(&self) -> PyResult { Ok(self.inner.ny) } + + #[setter] + #[pyo3(name = "ny")] + fn py_set_ny(&mut self, v: usize) { self.inner.ny = v; } + + #[getter] + #[pyo3(name = "nz")] + fn py_get_nz(&self) -> PyResult { Ok(self.inner.nz) } + + #[setter] + #[pyo3(name = "nz")] + fn py_set_nz(&mut self, v: usize) { self.inner.nz = v; } + + #[getter] + #[pyo3(name = "dx")] + fn py_get_dx(&self) -> PyResult { Ok(self.inner.dx) } + + #[setter] + #[pyo3(name = "dx")] + fn py_set_dx(&mut self, v: f64) { self.inner.dx = v; } + + #[getter] + #[pyo3(name = "dy")] + fn py_get_dy(&self) -> PyResult { Ok(self.inner.dy) } + + #[setter] + #[pyo3(name = "dy")] + fn py_set_dy(&mut self, v: f64) { self.inner.dy = v; } + + #[getter] + #[pyo3(name = "dz")] + fn py_get_dz(&self) -> PyResult { Ok(self.inner.dz) } + + #[setter] + #[pyo3(name = "dz")] + fn py_set_dz(&mut self, v: f64) { self.inner.dz = v; } + + #[getter] + #[pyo3(name = "diffusivity")] + fn py_get_diffusivity(&self) -> PyResult { Ok(self.inner.diffusivity) } + + #[setter] + #[pyo3(name = "diffusivity")] + fn py_set_diffusivity(&mut self, v: f64) { self.inner.diffusivity = v; } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// +/// Rust: `sim::rigid_body::RigidBody` +#[pyclass(name = "RigidBody", module = "numeria.sim.rigid_body")] +pub struct PyRigidBody { pub inner: rust_physics_engine::sim::rigid_body::RigidBody } +#[pymethods] +impl PyRigidBody { + /// Create a rigid body with given mass and diagonal inertia tensor [Ix, Iy, Iz]. + /// + /// Rust: `sim::rigid_body::RigidBody::new` + #[new] + #[pyo3(signature = (mass, inertia))] + fn __new__(mass: f64, inertia: Vec) -> PyResult { + let inertia = <[f64; 3]>::try_from(inertia).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 3 values, got {}", __v.len())))?; + let __r = crate::runtime::guard(|| rust_physics_engine::sim::rigid_body::RigidBody::new(mass, inertia)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyRigidBody { inner: __v }) + } + + /// Create a rigid body with uniform sphere inertia: I = 2mr²/5 + /// + /// Rust: `sim::rigid_body::RigidBody::new_sphere` + #[pyo3(name = "new_sphere")] + #[staticmethod] + #[pyo3(signature = (mass, radius))] + fn new_sphere(mass: f64, radius: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::sim::rigid_body::RigidBody::new_sphere(mass, radius)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyRigidBody { inner: __v }) + } + + /// Create a rigid body with box inertia: Ix = m(wy² + wz²)/12, etc. + /// + /// Rust: `sim::rigid_body::RigidBody::new_box` + #[pyo3(name = "new_box")] + #[staticmethod] + #[pyo3(signature = (mass, wx, wy, wz))] + fn new_box(mass: f64, wx: f64, wy: f64, wz: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::sim::rigid_body::RigidBody::new_box(mass, wx, wy, wz)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyRigidBody { inner: __v }) + } + + /// Create a rigid body with cylinder inertia (z-axis is symmetry axis). + /// + /// Rust: `sim::rigid_body::RigidBody::new_cylinder` + #[pyo3(name = "new_cylinder")] + #[staticmethod] + #[pyo3(signature = (mass, radius, height))] + fn new_cylinder(mass: f64, radius: f64, height: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::sim::rigid_body::RigidBody::new_cylinder(mass, radius, height)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyRigidBody { inner: __v }) + } + + /// Accumulate a force (applied at center of mass, no torque). + /// + /// Rust: `sim::rigid_body::RigidBody::apply_force` + #[pyo3(name = "apply_force")] + #[pyo3(signature = (force))] + fn apply_force(&mut self, force: crate::generated::types::PyVec3Arg) -> PyResult<()> { + let force = force.0; + let __r = crate::runtime::guard(|| self.inner.apply_force(force)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Accumulate a force at a world-space point, generating torque τ = r × F. + /// + /// Rust: `sim::rigid_body::RigidBody::apply_force_at_point` + #[pyo3(name = "apply_force_at_point")] + #[pyo3(signature = (force, point))] + fn apply_force_at_point(&mut self, force: crate::generated::types::PyVec3Arg, point: crate::generated::types::PyVec3Arg) -> PyResult<()> { + let force = force.0; + let point = point.0; + let __r = crate::runtime::guard(|| self.inner.apply_force_at_point(force, point)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Accumulate a torque directly. + /// + /// Rust: `sim::rigid_body::RigidBody::apply_torque` + #[pyo3(name = "apply_torque")] + #[pyo3(signature = (torque))] + fn apply_torque(&mut self, torque: crate::generated::types::PyVec3Arg) -> PyResult<()> { + let torque = torque.0; + let __r = crate::runtime::guard(|| self.inner.apply_torque(torque)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Reset accumulated force and torque to zero. + /// + /// Rust: `sim::rigid_body::RigidBody::clear_forces` + #[pyo3(name = "clear_forces")] + #[pyo3(signature = ())] + fn clear_forces(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.clear_forces()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Symplectic Euler integration with full Euler rotation equations. + /// + /// Rust: `sim::rigid_body::RigidBody::step` + #[pyo3(name = "step")] + #[pyo3(signature = (dt))] + fn step(&mut self, dt: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step(dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Total kinetic energy: KE = ½mv² + ½ω·I·ω + /// + /// Rust: `sim::rigid_body::RigidBody::kinetic_energy` + #[pyo3(name = "kinetic_energy")] + #[pyo3(signature = ())] + fn kinetic_energy(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.kinetic_energy()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Angular momentum in world frame: L = R·(I·ω_body) + /// + /// Rust: `sim::rigid_body::RigidBody::angular_momentum` + #[pyo3(name = "angular_momentum")] + #[pyo3(signature = ())] + fn angular_momentum(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.angular_momentum()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Transform a point from body-local to world coordinates. + /// + /// Rust: `sim::rigid_body::RigidBody::local_to_world` + #[pyo3(name = "local_to_world")] + #[pyo3(signature = (local_point))] + fn local_to_world(&self, local_point: crate::generated::types::PyVec3Arg) -> PyResult { + let local_point = local_point.0; + let __r = crate::runtime::guard(|| self.inner.local_to_world(local_point)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Transform a point from world to body-local coordinates. + /// + /// Rust: `sim::rigid_body::RigidBody::world_to_local` + #[pyo3(name = "world_to_local")] + #[pyo3(signature = (world_point))] + fn world_to_local(&self, world_point: crate::generated::types::PyVec3Arg) -> PyResult { + let world_point = world_point.0; + let __r = crate::runtime::guard(|| self.inner.world_to_local(world_point)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Velocity at a world-space point: v_point = v_cm + ω × r + /// + /// Rust: `sim::rigid_body::RigidBody::velocity_at_point` + #[pyo3(name = "velocity_at_point")] + #[pyo3(signature = (world_point))] + fn velocity_at_point(&self, world_point: crate::generated::types::PyVec3Arg) -> PyResult { + let world_point = world_point.0; + let __r = crate::runtime::guard(|| self.inner.velocity_at_point(world_point)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + #[getter] + #[pyo3(name = "position")] + fn py_get_position(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.position.clone() }) } + + #[getter] + #[pyo3(name = "velocity")] + fn py_get_velocity(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.velocity.clone() }) } + + #[getter] + #[pyo3(name = "mass")] + fn py_get_mass(&self) -> PyResult { Ok(self.inner.mass) } + + #[setter] + #[pyo3(name = "mass")] + fn py_set_mass(&mut self, v: f64) { self.inner.mass = v; } + + #[getter] + #[pyo3(name = "orientation")] + fn py_get_orientation(&self) -> PyResult { Ok(crate::generated::types::PyQuaternion { inner: self.inner.orientation.clone() }) } + + #[getter] + #[pyo3(name = "angular_velocity")] + fn py_get_angular_velocity(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.angular_velocity.clone() }) } + + #[getter] + #[pyo3(name = "inertia")] + fn py_get_inertia(&self) -> PyResult> { Ok(self.inner.inertia.clone().to_vec()) } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// +/// Rust: `sim::rigid_body::RigidBodySystem` +#[pyclass(name = "RigidBodySystem", module = "numeria.sim.rigid_body")] +pub struct PyRigidBodySystem { pub inner: rust_physics_engine::sim::rigid_body::RigidBodySystem } +#[pymethods] +impl PyRigidBodySystem { + /// Create a new rigid body system with the given gravitational acceleration. + /// + /// Rust: `sim::rigid_body::RigidBodySystem::new` + #[new] + #[pyo3(signature = (gravity))] + fn __new__(gravity: crate::generated::types::PyVec3Arg) -> PyResult { + let gravity = gravity.0; + let __r = crate::runtime::guard(|| rust_physics_engine::sim::rigid_body::RigidBodySystem::new(gravity)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyRigidBodySystem { inner: __v }) + } + + /// Advance all bodies by dt, applying gravity and integrating with symplectic Euler. + /// + /// Rust: `sim::rigid_body::RigidBodySystem::step` + #[pyo3(name = "step")] + #[pyo3(signature = (dt))] + fn step(&mut self, dt: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step(dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Total mechanical energy: Σ(KE + PE_gravity) for all bodies. + /// + /// Rust: `sim::rigid_body::RigidBodySystem::total_energy` + #[pyo3(name = "total_energy")] + #[pyo3(signature = ())] + fn total_energy(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.total_energy()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Total linear momentum: Σ m·v for all bodies. + /// + /// Rust: `sim::rigid_body::RigidBodySystem::total_momentum` + #[pyo3(name = "total_momentum")] + #[pyo3(signature = ())] + fn total_momentum(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.total_momentum()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + #[getter] + #[pyo3(name = "gravity")] + fn py_get_gravity(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.gravity.clone() }) } + + #[getter] + #[pyo3(name = "time")] + fn py_get_time(&self) -> PyResult { Ok(self.inner.time) } + + #[setter] + #[pyo3(name = "time")] + fn py_set_time(&mut self, v: f64) { self.inner.time = v; } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// The wave equation in one and two dimensions. +/// +/// Explicit second-order finite differences on `∂²u/∂t² = c²∇²u`, with +/// fixed, free, and Mur first-order absorbing boundaries. The absorbing +/// condition passes a normally-incident wave out of the domain exactly and +/// degrades with the angle of incidence. +/// +/// Stability requires the Courant number `r = cΔt/Δx` to satisfy `r ≤ 1` +/// in 1-D and `r ≤ 1/√2` in 2-D. At exactly `r = 1` in one dimension the +/// scheme is an exact shift and has no dispersion error at all. +/// 1D wave equation solver on a uniform grid with fixed (Dirichlet) endpoints. +/// +/// Rust: `sim::wave_sim::WaveEquation1D` +#[pyclass(name = "WaveEquation1D", module = "numeria.sim.wave_sim")] +pub struct PyWaveEquation1D { pub inner: rust_physics_engine::sim::wave_sim::WaveEquation1D } +#[pymethods] +impl PyWaveEquation1D { + /// Create a new 1D wave equation solver. All displacements start at zero. + /// + /// Rust: `sim::wave_sim::WaveEquation1D::new` + #[new] + #[pyo3(signature = (nx, dx, wave_speed))] + fn __new__(nx: usize, dx: f64, wave_speed: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::sim::wave_sim::WaveEquation1D::new(nx, dx, wave_speed)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyWaveEquation1D { inner: __v }) + } + + /// Set initial displacement u(x, 0) and initial velocity ∂u/∂t(x, 0). + /// + /// The previous time-step field is computed from the Taylor expansion that + /// is second-order accurate in dt: + /// u_i^{-1} = u_i^0 - v_i dt + 0.5 c² dt² / dx² (u_{i+1}^0 - 2 u_i^0 + u_{i-1}^0) + /// + /// This avoids the first-order error that arises from the naive + /// u_prev = u_current - velocity * dt approximation. + /// + /// Rust: `sim::wave_sim::WaveEquation1D::set_initial` + #[pyo3(name = "set_initial")] + #[pyo3(signature = (displacement, velocity, dt))] + fn set_initial<'py>(&mut self, py: Python<'py>, displacement: Vec, velocity: Vec, dt: f64) -> PyResult<()> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.set_initial(&displacement, &velocity, dt))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Advance one time step using the leapfrog scheme with fixed endpoints + /// `u[0] = u[nx-1] = 0`. + /// + /// Rust: `sim::wave_sim::WaveEquation1D::step` + #[pyo3(name = "step")] + #[pyo3(signature = (dt))] + fn step(&mut self, dt: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step(dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Advance one time step with Mur's first-order absorbing boundary conditions. + /// + /// Interior update is identical to `step`. At the boundaries the outgoing + /// characteristic is approximated: + /// + /// + /// where `r = c dt / dx`. + /// + /// These conditions absorb normally-incident waves perfectly (first order + /// in angle of incidence for oblique waves). + /// + /// Rust: `sim::wave_sim::WaveEquation1D::step_absorbing` + #[pyo3(name = "step_absorbing")] + #[pyo3(signature = (dt))] + fn step_absorbing(&mut self, dt: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step_absorbing(dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Courant number r = c dt / dx. + /// + /// Rust: `sim::wave_sim::WaveEquation1D::courant_number` + #[pyo3(name = "courant_number")] + #[pyo3(signature = (dt))] + fn courant_number(&self, dt: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.courant_number(dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Maximum stable time step from the CFL condition: dt_max = dx / c. + /// + /// Rust: `sim::wave_sim::WaveEquation1D::stable_dt` + #[pyo3(name = "stable_dt")] + #[pyo3(signature = ())] + fn stable_dt(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.stable_dt()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Discrete total energy (kinetic + potential) of the grid. + /// + /// E = 0.5 dx Σ_i [ ((u_i^n - u_i^{n-1}) / dt)² + c² ((u_{i+1}^n - u_i^n) / dx)² ] + /// + /// The first term is the kinetic energy density (velocity squared), the + /// second is the potential / strain energy density (spatial gradient squared). + /// Both are summed with the cell volume dx. + /// + /// Rust: `sim::wave_sim::WaveEquation1D::total_energy` + #[pyo3(name = "total_energy")] + #[pyo3(signature = (dt))] + fn total_energy(&self, dt: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.total_energy(dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Inject a continuous point source by adding amplitude to the current + /// displacement field at the given grid index. + /// + /// Rust: `sim::wave_sim::WaveEquation1D::add_source` + #[pyo3(name = "add_source")] + #[pyo3(signature = (position, amplitude))] + fn add_source(&mut self, position: usize, amplitude: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.add_source(position, amplitude)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + #[getter] + #[pyo3(name = "u_current")] + fn py_get_u_current(&self) -> PyResult> { Ok(self.inner.u_current.clone()) } + + #[getter] + #[pyo3(name = "u_previous")] + fn py_get_u_previous(&self) -> PyResult> { Ok(self.inner.u_previous.clone()) } + + #[getter] + #[pyo3(name = "nx")] + fn py_get_nx(&self) -> PyResult { Ok(self.inner.nx) } + + #[setter] + #[pyo3(name = "nx")] + fn py_set_nx(&mut self, v: usize) { self.inner.nx = v; } + + #[getter] + #[pyo3(name = "dx")] + fn py_get_dx(&self) -> PyResult { Ok(self.inner.dx) } + + #[setter] + #[pyo3(name = "dx")] + fn py_set_dx(&mut self, v: f64) { self.inner.dx = v; } + + #[getter] + #[pyo3(name = "wave_speed")] + fn py_get_wave_speed(&self) -> PyResult { Ok(self.inner.wave_speed) } + + #[setter] + #[pyo3(name = "wave_speed")] + fn py_set_wave_speed(&mut self, v: f64) { self.inner.wave_speed = v; } + + #[getter] + #[pyo3(name = "time")] + fn py_get_time(&self) -> PyResult { Ok(self.inner.time) } + + #[setter] + #[pyo3(name = "time")] + fn py_set_time(&mut self, v: f64) { self.inner.time = v; } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// 2D wave equation solver on a uniform grid with fixed (Dirichlet) boundaries. +/// +/// Grid layout: row-major, index = j * nx + i (i along x, j along y). +/// +/// Rust: `sim::wave_sim::WaveEquation2D` +#[pyclass(name = "WaveEquation2D", module = "numeria.sim.wave_sim")] +pub struct PyWaveEquation2D { pub inner: rust_physics_engine::sim::wave_sim::WaveEquation2D } +#[pymethods] +impl PyWaveEquation2D { + /// Create a new 2D wave equation solver with all displacements at zero. + /// + /// Rust: `sim::wave_sim::WaveEquation2D::new` + #[new] + #[pyo3(signature = (nx, ny, dx, dy, wave_speed))] + fn __new__(nx: usize, ny: usize, dx: f64, dy: f64, wave_speed: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::sim::wave_sim::WaveEquation2D::new(nx, ny, dx, dy, wave_speed)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyWaveEquation2D { inner: __v }) + } + + /// Enable viscous damping (-γ ∂u/∂t). + /// + /// Rust: `sim::wave_sim::WaveEquation2D::set_damping` + #[pyo3(name = "set_damping")] + #[pyo3(signature = (damping))] + fn set_damping(&mut self, damping: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.set_damping(damping)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Advance one time step. + /// + /// Without damping (γ = 0): + /// u_{ij}^{n+1} = 2 u_{ij}^n - u_{ij}^{n-1} + c² dt² ∇²u_{ij}^n + /// + /// With damping (γ > 0): + /// u_{ij}^{n+1} = [ 2 u_{ij}^n - (1 - γ dt) u_{ij}^{n-1} + /// + c² dt² ∇²u_{ij}^n ] / (1 + γ dt) + /// + /// Boundary points (i=0, i=nx-1, j=0, j=ny-1) are held at zero. + /// + /// Rust: `sim::wave_sim::WaveEquation2D::step` + #[pyo3(name = "step")] + #[pyo3(signature = (dt))] + fn step(&mut self, dt: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step(dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Maximum stable time step from the 2D CFL condition: + /// dt_max = 1 / (c √(1/dx² + 1/dy²)) + /// + /// Rust: `sim::wave_sim::WaveEquation2D::stable_dt` + #[pyo3(name = "stable_dt")] + #[pyo3(signature = ())] + fn stable_dt(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.stable_dt()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Discrete total energy of the 2D field. + /// + /// E = 0.5 dx dy Σ_{i,j} [ ((u_{ij}^n - u_{ij}^{n-1})/dt)² + /// + c² ((u_{i+1,j} - u_{ij})²/dx² + (u_{i,j+1} - u_{ij})²/dy²) ] + /// + /// Rust: `sim::wave_sim::WaveEquation2D::total_energy` + #[pyo3(name = "total_energy")] + #[pyo3(signature = (dt))] + fn total_energy(&self, dt: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.total_energy(dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Set the displacement at grid point (i, j) in the current time step. + /// + /// Rust: `sim::wave_sim::WaveEquation2D::set_point` + #[pyo3(name = "set_point")] + #[pyo3(signature = (i, j, value))] + fn set_point(&mut self, i: usize, j: usize, value: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.set_point(i, j, value)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + #[getter] + #[pyo3(name = "u_current")] + fn py_get_u_current(&self) -> PyResult> { Ok(self.inner.u_current.clone()) } + + #[getter] + #[pyo3(name = "u_previous")] + fn py_get_u_previous(&self) -> PyResult> { Ok(self.inner.u_previous.clone()) } + + #[getter] + #[pyo3(name = "nx")] + fn py_get_nx(&self) -> PyResult { Ok(self.inner.nx) } + + #[setter] + #[pyo3(name = "nx")] + fn py_set_nx(&mut self, v: usize) { self.inner.nx = v; } + + #[getter] + #[pyo3(name = "ny")] + fn py_get_ny(&self) -> PyResult { Ok(self.inner.ny) } + + #[setter] + #[pyo3(name = "ny")] + fn py_set_ny(&mut self, v: usize) { self.inner.ny = v; } + + #[getter] + #[pyo3(name = "dx")] + fn py_get_dx(&self) -> PyResult { Ok(self.inner.dx) } + + #[setter] + #[pyo3(name = "dx")] + fn py_set_dx(&mut self, v: f64) { self.inner.dx = v; } + + #[getter] + #[pyo3(name = "dy")] + fn py_get_dy(&self) -> PyResult { Ok(self.inner.dy) } + + #[setter] + #[pyo3(name = "dy")] + fn py_set_dy(&mut self, v: f64) { self.inner.dy = v; } + + #[getter] + #[pyo3(name = "wave_speed")] + fn py_get_wave_speed(&self) -> PyResult { Ok(self.inner.wave_speed) } + + #[setter] + #[pyo3(name = "wave_speed")] + fn py_set_wave_speed(&mut self, v: f64) { self.inner.wave_speed = v; } + + #[getter] + #[pyo3(name = "time")] + fn py_get_time(&self) -> PyResult { Ok(self.inner.time) } + + #[setter] + #[pyo3(name = "time")] + fn py_set_time(&mut self, v: f64) { self.inner.time = v; } + + #[getter] + #[pyo3(name = "damping")] + fn py_get_damping(&self) -> PyResult { Ok(self.inner.damping) } + + #[setter] + #[pyo3(name = "damping")] + fn py_set_damping(&mut self, v: f64) { self.inner.damping = v; } + + fn __repr__(&self) -> String { "".to_string() } +} diff --git a/bindings/python/src/generated/types/spatial.rs b/bindings/python/src/generated/types/spatial.rs new file mode 100644 index 0000000..b70587b --- /dev/null +++ b/bindings/python/src/generated/types/spatial.rs @@ -0,0 +1,2542 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; + + +/// Binary BVH; query methods return primitive indices into the +/// original input slice. +/// +/// Rust: `spatial::bvh::Bvh` +#[pyclass(name = "Bvh", module = "numeria.spatial.bvh", from_py_object)] +#[derive(Clone)] +pub struct PyBvh { pub inner: rust_physics_engine::spatial::bvh::Bvh } +#[pymethods] +impl PyBvh { + /// Builds over one AABB per primitive. + /// + /// Panics: + /// Panics on empty input. + /// + /// Rust: `spatial::bvh::Bvh::build` + #[pyo3(name = "build")] + #[staticmethod] + #[pyo3(signature = (bounds))] + fn build(bounds: Vec) -> PyResult { + let bounds = bounds.into_iter().map(|__e| __e.inner).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::bvh::Bvh::build(&bounds)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBvh { inner: __v }) + } + + /// Convenience: builds over triangle bounds. + /// + /// Panics: + /// Panics on empty input. + /// + /// Rust: `spatial::bvh::Bvh::build_triangles` + #[pyo3(name = "build_triangles")] + #[staticmethod] + #[pyo3(signature = (tris))] + fn build_triangles(tris: Vec) -> PyResult { + let tris = tris.into_iter().map(|__e| __e.inner).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::bvh::Bvh::build_triangles(&tris)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBvh { inner: __v }) + } + + /// Primitive indices whose bounds a ray may hit within `max_t`. + /// + /// Rust: `spatial::bvh::Bvh::query_ray` + #[pyo3(name = "query_ray")] + #[pyo3(signature = (r, max_t))] + fn query_ray<'py>(&self, py: Python<'py>, r: crate::generated::types::PyRay, max_t: f64) -> PyResult> { + let r = r.inner; + let __r = py.detach(move || crate::runtime::guard(move || self.inner.query_ray(&r, max_t))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Primitive indices whose bounds overlap the box. + /// + /// Rust: `spatial::bvh::Bvh::query_aabb` + #[pyo3(name = "query_aabb")] + #[pyo3(signature = (b))] + fn query_aabb<'py>(&self, py: Python<'py>, b: crate::generated::types::PyAabb) -> PyResult> { + let b = b.inner; + let __r = py.detach(move || crate::runtime::guard(move || self.inner.query_aabb(&b))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Primitive indices whose bounds overlap the sphere. + /// + /// Rust: `spatial::bvh::Bvh::query_sphere` + #[pyo3(name = "query_sphere")] + #[pyo3(signature = (s))] + fn query_sphere<'py>(&self, py: Python<'py>, s: crate::generated::types::PySphere) -> PyResult> { + let s = s.inner; + let __r = py.detach(move || crate::runtime::guard(move || self.inner.query_sphere(&s))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Nearest ray-triangle hit over the indexed triangles. + /// + /// Rust: `spatial::bvh::Bvh::closest_hit` + #[pyo3(name = "closest_hit")] + #[pyo3(signature = (r, tris))] + fn closest_hit(&self, r: crate::generated::types::PyRay, tris: Vec) -> PyResult> { + let r = r.inner; + let tris = tris.into_iter().map(|__e| __e.inner).collect::>(); + let __r = crate::runtime::guard(|| self.inner.closest_hit(&r, &tris)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| (__x.0, crate::generated::types::PyIntersectRayHit { inner: __x.1 }))) + } + + /// Closest point on any indexed triangle: (index, point, distance). + /// + /// Rust: `spatial::bvh::Bvh::closest_point` + #[pyo3(name = "closest_point")] + #[pyo3(signature = (p, tris))] + fn closest_point(&self, p: crate::generated::types::PyVec3Arg, tris: Vec) -> PyResult<(usize, crate::generated::types::PyVec3, f64)> { + let p = p.0; + let tris = tris.into_iter().map(|__e| __e.inner).collect::>(); + let __r = crate::runtime::guard(|| self.inner.closest_point(p, &tris)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, crate::generated::types::PyVec3 { inner: __v.1 }, __v.2)) + } + + /// Broadphase: all unordered primitive pairs (i < j) whose bounds + /// overlap. + /// + /// Rust: `spatial::bvh::Bvh::self_overlaps` + #[pyo3(name = "self_overlaps")] + #[pyo3(signature = ())] + fn self_overlaps<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.self_overlaps())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) + } + + /// Updates leaf bounds in place (same topology) and recomputes + /// internal boxes bottom-up. + /// + /// Panics: + /// Panics unless `bounds.len()` matches the build-time count. + /// + /// Rust: `spatial::bvh::Bvh::refit` + #[pyo3(name = "refit")] + #[pyo3(signature = (bounds))] + fn refit<'py>(&mut self, py: Python<'py>, bounds: Vec) -> PyResult<()> { + let bounds = bounds.into_iter().map(|__e| __e.inner).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || self.inner.refit(&bounds))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Maximum node depth (root = 1). + /// + /// Rust: `spatial::bvh::Bvh::depth` + #[pyo3(name = "depth")] + #[pyo3(signature = ())] + fn depth(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.depth()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Bvh", "Bvh", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A rigid frame: position and orientation of a local coordinate +/// system expressed in world coordinates. +/// +/// Rust: `spatial::frame::Frame` +#[pyclass(name = "Frame", module = "numeria.spatial.frame", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyFrame { pub inner: rust_physics_engine::spatial::frame::Frame } +#[pymethods] +impl PyFrame { + /// World frame: origin at zero, identity rotation. + /// + /// Rust: `spatial::frame::Frame::identity` + #[pyo3(name = "identity")] + #[staticmethod] + #[pyo3(signature = ())] + fn identity() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::frame::Frame::identity()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyFrame { inner: __v }) + } + + /// Frame from origin and rotation (normalized). + /// + /// Rust: `spatial::frame::Frame::new` + #[new] + #[pyo3(signature = (origin, rotation))] + fn __new__(origin: crate::generated::types::PyVec3Arg, rotation: crate::generated::types::PyQuaternionArg) -> PyResult { + let origin = origin.0; + let rotation = rotation.0; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::frame::Frame::new(origin, rotation)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyFrame { inner: __v }) + } + + /// Frame whose x axis points along `x` and whose y axis is the + /// component of `y` perpendicular to it (Gram-Schmidt); z = x × y. + /// + /// Panics: + /// Panics when `x` is zero or `y` is parallel to `x`. + /// + /// Rust: `spatial::frame::Frame::from_axes` + #[pyo3(name = "from_axes")] + #[staticmethod] + #[pyo3(signature = (origin, x, y))] + fn from_axes(origin: crate::generated::types::PyVec3Arg, x: crate::generated::types::PyVec3Arg, y: crate::generated::types::PyVec3Arg) -> PyResult { + let origin = origin.0; + let x = x.0; + let y = y.0; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::frame::Frame::from_axes(origin, x, y)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyFrame { inner: __v }) + } + + /// World point → local coordinates. + /// + /// Rust: `spatial::frame::Frame::to_local` + #[pyo3(name = "to_local")] + #[pyo3(signature = (world_p))] + fn to_local(&self, world_p: crate::generated::types::PyVec3Arg) -> PyResult { + let world_p = world_p.0; + let __r = crate::runtime::guard(|| self.inner.to_local(world_p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Local point → world coordinates. + /// + /// Rust: `spatial::frame::Frame::to_world` + #[pyo3(name = "to_world")] + #[pyo3(signature = (local_p))] + fn to_world(&self, local_p: crate::generated::types::PyVec3Arg) -> PyResult { + let local_p = local_p.0; + let __r = crate::runtime::guard(|| self.inner.to_world(local_p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// World direction → local (rotation only). + /// + /// Rust: `spatial::frame::Frame::to_local_vector` + #[pyo3(name = "to_local_vector")] + #[pyo3(signature = (v))] + fn to_local_vector(&self, v: crate::generated::types::PyVec3Arg) -> PyResult { + let v = v.0; + let __r = crate::runtime::guard(|| self.inner.to_local_vector(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Local direction → world (rotation only). + /// + /// Rust: `spatial::frame::Frame::to_world_vector` + #[pyo3(name = "to_world_vector")] + #[pyo3(signature = (v))] + fn to_world_vector(&self, v: crate::generated::types::PyVec3Arg) -> PyResult { + let v = v.0; + let __r = crate::runtime::guard(|| self.inner.to_world_vector(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// A child frame expressed in `self` coordinates, re-expressed in + /// world coordinates. + /// + /// Rust: `spatial::frame::Frame::compose` + #[pyo3(name = "compose")] + #[pyo3(signature = (child))] + fn compose(&self, child: crate::generated::types::PyFrame) -> PyResult { + let child = child.inner; + let __r = crate::runtime::guard(|| self.inner.compose(&child)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyFrame { inner: __v }) + } + + /// The inverse rigid motion. + /// + /// Rust: `spatial::frame::Frame::inverse` + #[pyo3(name = "inverse")] + #[pyo3(signature = ())] + fn inverse(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.inverse()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyFrame { inner: __v }) + } + + /// This frame expressed relative to `other`: + /// other.compose(result) == self. + /// + /// Rust: `spatial::frame::Frame::relative_to` + #[pyo3(name = "relative_to")] + #[pyo3(signature = (other))] + fn relative_to(&self, other: crate::generated::types::PyFrame) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.relative_to(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyFrame { inner: __v }) + } + + /// Screw-free interpolation: lerp the origin, slerp the rotation. + /// + /// Rust: `spatial::frame::Frame::interpolate` + #[pyo3(name = "interpolate")] + #[pyo3(signature = (other, t))] + fn interpolate(&self, other: crate::generated::types::PyFrame, t: f64) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.interpolate(&other, t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyFrame { inner: __v }) + } + + /// Equivalent homogeneous matrix (unit scale). + /// + /// Rust: `spatial::frame::Frame::to_mat4` + #[pyo3(name = "to_mat4")] + #[pyo3(signature = ())] + fn to_mat4(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.to_mat4()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMat4Mat4 { inner: __v }) + } + + /// Local +X axis in world coordinates. + /// + /// Rust: `spatial::frame::Frame::x_axis` + #[pyo3(name = "x_axis")] + #[pyo3(signature = ())] + fn x_axis(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.x_axis()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Local +Y axis in world coordinates. + /// + /// Rust: `spatial::frame::Frame::y_axis` + #[pyo3(name = "y_axis")] + #[pyo3(signature = ())] + fn y_axis(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.y_axis()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Local +Z axis in world coordinates. + /// + /// Rust: `spatial::frame::Frame::z_axis` + #[pyo3(name = "z_axis")] + #[pyo3(signature = ())] + fn z_axis(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.z_axis()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + #[getter] + #[pyo3(name = "origin")] + fn py_get_origin(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.origin.clone() }) } + + #[getter] + #[pyo3(name = "rotation")] + fn py_get_rotation(&self) -> PyResult { Ok(crate::generated::types::PyQuaternion { inner: self.inner.rotation.clone() }) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Frame", "Frame", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A ray hit: parameter, position, and surface normal (facing the ray). +/// +/// Rust: `spatial::intersect::RayHit` +#[pyclass(name = "RayHit", module = "numeria.spatial.intersect", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyIntersectRayHit { pub inner: rust_physics_engine::spatial::intersect::RayHit } +#[pymethods] +impl PyIntersectRayHit { + /// Builds a `RayHit` from its fields. + #[new] + #[pyo3(signature = (t, point, normal))] + fn __new__(t: f64, point: crate::generated::types::PyVec3Arg, normal: crate::generated::types::PyVec3Arg) -> Self { + let point = point.0; + let normal = normal.0; + Self { inner: rust_physics_engine::spatial::intersect::RayHit { t: t, point: point, normal: normal } } + } + + #[getter] + #[pyo3(name = "t")] + fn py_get_t(&self) -> PyResult { Ok(self.inner.t) } + + #[setter] + #[pyo3(name = "t")] + fn py_set_t(&mut self, v: f64) { self.inner.t = v; } + + #[getter] + #[pyo3(name = "point")] + fn py_get_point(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.point.clone() }) } + + #[getter] + #[pyo3(name = "normal")] + fn py_get_normal(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.normal.clone() }) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("RayHit", "RayHit", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Median-split k-d tree over `Vec3` points. +/// +/// Rust: `spatial::kdtree::KdTree` +#[pyclass(name = "KdTree", module = "numeria.spatial.kdtree", from_py_object)] +#[derive(Clone)] +pub struct PyKdTree { pub inner: rust_physics_engine::spatial::kdtree::KdTree } +#[pymethods] +impl PyKdTree { + /// Builds by recursive median split. + /// + /// Rust: `spatial::kdtree::KdTree::build` + #[pyo3(name = "build")] + #[staticmethod] + #[pyo3(signature = (points))] + fn build(points: Vec) -> PyResult { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::kdtree::KdTree::build(&points)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyKdTree { inner: __v }) + } + + /// Index of and distance to the nearest stored point. + /// + /// Rust: `spatial::kdtree::KdTree::nearest` + #[pyo3(name = "nearest")] + #[pyo3(signature = (p))] + fn nearest(&self, p: crate::generated::types::PyVec3Arg) -> PyResult> { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.nearest(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| (__x.0, __x.1))) + } + + /// The `k` nearest stored points, nearest first. + /// + /// Rust: `spatial::kdtree::KdTree::k_nearest` + #[pyo3(name = "k_nearest")] + #[pyo3(signature = (p, k))] + fn k_nearest<'py>(&self, py: Python<'py>, p: crate::generated::types::PyVec3Arg, k: usize) -> PyResult> { + let p = p.0; + let __r = py.detach(move || crate::runtime::guard(move || self.inner.k_nearest(p, k))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) + } + + /// Every stored point within `r` of `p`. + /// + /// Rust: `spatial::kdtree::KdTree::within_radius` + #[pyo3(name = "within_radius")] + #[pyo3(signature = (p, r))] + fn within_radius<'py>(&self, py: Python<'py>, p: crate::generated::types::PyVec3Arg, r: f64) -> PyResult> { + let p = p.0; + let __r = py.detach(move || crate::runtime::guard(move || self.inner.within_radius(p, r))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) + } + + /// Every pair of stored points closer than `r`. + /// + /// Rust: `spatial::kdtree::KdTree::all_pairs_within` + #[pyo3(name = "all_pairs_within")] + #[pyo3(signature = (r))] + fn all_pairs_within<'py>(&self, py: Python<'py>, r: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.all_pairs_within(r))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) + } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("KdTree", "KdTree", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Median-split k-d tree over `Vec2` points. +/// +/// Rust: `spatial::kdtree::KdTree2` +#[pyclass(name = "KdTree2", module = "numeria.spatial.kdtree", from_py_object)] +#[derive(Clone)] +pub struct PyKdTree2 { pub inner: rust_physics_engine::spatial::kdtree::KdTree2 } +#[pymethods] +impl PyKdTree2 { + /// Builds by recursive median split. + /// + /// Rust: `spatial::kdtree::KdTree2::build` + #[pyo3(name = "build")] + #[staticmethod] + #[pyo3(signature = (points))] + fn build(points: Vec) -> PyResult { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::kdtree::KdTree2::build(&points)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyKdTree2 { inner: __v }) + } + + /// Index of and distance to the nearest stored point. + /// + /// Rust: `spatial::kdtree::KdTree2::nearest` + #[pyo3(name = "nearest")] + #[pyo3(signature = (p))] + fn nearest(&self, p: crate::generated::types::PyVec2Arg) -> PyResult> { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.nearest(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| (__x.0, __x.1))) + } + + /// The `k` nearest stored points, nearest first. + /// + /// Rust: `spatial::kdtree::KdTree2::k_nearest` + #[pyo3(name = "k_nearest")] + #[pyo3(signature = (p, k))] + fn k_nearest<'py>(&self, py: Python<'py>, p: crate::generated::types::PyVec2Arg, k: usize) -> PyResult> { + let p = p.0; + let __r = py.detach(move || crate::runtime::guard(move || self.inner.k_nearest(p, k))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) + } + + /// Every stored point within `r` of `p`. + /// + /// Rust: `spatial::kdtree::KdTree2::within_radius` + #[pyo3(name = "within_radius")] + #[pyo3(signature = (p, r))] + fn within_radius<'py>(&self, py: Python<'py>, p: crate::generated::types::PyVec2Arg, r: f64) -> PyResult> { + let p = p.0; + let __r = py.detach(move || crate::runtime::guard(move || self.inner.within_radius(p, r))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) + } + + /// Every pair of stored points closer than `r`. + /// + /// Rust: `spatial::kdtree::KdTree2::all_pairs_within` + #[pyo3(name = "all_pairs_within")] + #[pyo3(signature = (r))] + fn all_pairs_within<'py>(&self, py: Python<'py>, r: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.all_pairs_within(r))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) + } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("KdTree2", "KdTree2", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Uniform-grid spatial hash for 3-D points; O(1) insert, sphere +/// queries visit only overlapping cells. +/// +/// Rust: `spatial::kdtree::SpatialHash` +#[pyclass(name = "SpatialHash", module = "numeria.spatial.kdtree", from_py_object)] +#[derive(Clone)] +pub struct PyKdtreeSpatialHash { pub inner: rust_physics_engine::spatial::kdtree::SpatialHash } +#[pymethods] +impl PyKdtreeSpatialHash { + /// Panics: + /// Panics unless cell > 0. + /// + /// Rust: `spatial::kdtree::SpatialHash::new` + #[new] + #[pyo3(signature = (cell))] + fn __new__(cell: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::kdtree::SpatialHash::new(cell)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyKdtreeSpatialHash { inner: __v }) + } + + /// Registers item i at position p. + /// + /// Rust: `spatial::kdtree::SpatialHash::insert` + #[pyo3(name = "insert")] + #[pyo3(signature = (i, p))] + fn insert(&mut self, i: usize, p: crate::generated::types::PyVec3Arg) -> PyResult<()> { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.insert(i, p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Indices of items within r of p (exact, sorted). + /// + /// Rust: `spatial::kdtree::SpatialHash::query_sphere` + #[pyo3(name = "query_sphere")] + #[pyo3(signature = (p, r))] + fn query_sphere<'py>(&self, py: Python<'py>, p: crate::generated::types::PyVec3Arg, r: f64) -> PyResult> { + let p = p.0; + let __r = py.detach(move || crate::runtime::guard(move || self.inner.query_sphere(p, r))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Removes all items. + /// + /// Rust: `spatial::kdtree::SpatialHash::clear` + #[pyo3(name = "clear")] + #[pyo3(signature = ())] + fn clear(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.clear()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + #[getter] + #[pyo3(name = "cell")] + fn py_get_cell(&self) -> PyResult { Ok(self.inner.cell) } + + #[setter] + #[pyo3(name = "cell")] + fn py_set_cell(&mut self, v: f64) { self.inner.cell = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("SpatialHash", "SpatialHash", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// 4×4 matrix, row-major: `data[row][col]`. +/// +/// Rust: `spatial::mat4::Mat4` +#[pyclass(name = "Mat4", module = "numeria.spatial.mat4", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyMat4Mat4 { pub inner: rust_physics_engine::spatial::mat4::Mat4 } +#[pymethods] +impl PyMat4Mat4 { + /// Identity transform. + /// + /// Rust: `spatial::mat4::Mat4::identity` + #[pyo3(name = "identity")] + #[staticmethod] + #[pyo3(signature = ())] + fn identity() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::mat4::Mat4::identity()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMat4Mat4 { inner: __v }) + } + + /// Builds from four row arrays. + /// + /// Rust: `spatial::mat4::Mat4::from_rows` + #[pyo3(name = "from_rows")] + #[staticmethod] + #[pyo3(signature = (r0, r1, r2, r3))] + fn from_rows(r0: Vec, r1: Vec, r2: Vec, r3: Vec) -> PyResult { + let r0 = <[f64; 4]>::try_from(r0).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 4 values, got {}", __v.len())))?; + let r1 = <[f64; 4]>::try_from(r1).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 4 values, got {}", __v.len())))?; + let r2 = <[f64; 4]>::try_from(r2).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 4 values, got {}", __v.len())))?; + let r3 = <[f64; 4]>::try_from(r3).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 4 values, got {}", __v.len())))?; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::mat4::Mat4::from_rows(r0, r1, r2, r3)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMat4Mat4 { inner: __v }) + } + + /// Translation by t. + /// + /// Rust: `spatial::mat4::Mat4::translation` + #[pyo3(name = "translation")] + #[staticmethod] + #[pyo3(signature = (t))] + fn translation(t: crate::generated::types::PyVec3Arg) -> PyResult { + let t = t.0; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::mat4::Mat4::translation(t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMat4Mat4 { inner: __v }) + } + + /// Per-axis scaling. + /// + /// Rust: `spatial::mat4::Mat4::scaling` + #[pyo3(name = "scaling")] + #[staticmethod] + #[pyo3(signature = (s))] + fn scaling(s: crate::generated::types::PyVec3Arg) -> PyResult { + let s = s.0; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::mat4::Mat4::scaling(s)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMat4Mat4 { inner: __v }) + } + + /// Rotation from a unit quaternion. + /// + /// Rust: `spatial::mat4::Mat4::rotation` + #[pyo3(name = "rotation")] + #[staticmethod] + #[pyo3(signature = (q))] + fn rotation(q: crate::generated::types::PyQuaternionArg) -> PyResult { + let q = q.0; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::mat4::Mat4::rotation(&q)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMat4Mat4 { inner: __v }) + } + + /// Embeds a 3×3 linear map in the upper-left block. + /// + /// Rust: `spatial::mat4::Mat4::from_mat3` + #[pyo3(name = "from_mat3")] + #[staticmethod] + #[pyo3(signature = (m))] + fn from_mat3(m: crate::generated::types::PyMat3) -> PyResult { + let m = m.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::mat4::Mat4::from_mat3(&m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMat4Mat4 { inner: __v }) + } + + /// Composite transform T·R·S (scale, then rotate, then translate). + /// + /// Rust: `spatial::mat4::Mat4::from_trs` + #[pyo3(name = "from_trs")] + #[staticmethod] + #[pyo3(signature = (t, r, s))] + fn from_trs(t: crate::generated::types::PyVec3Arg, r: crate::generated::types::PyQuaternionArg, s: crate::generated::types::PyVec3Arg) -> PyResult { + let t = t.0; + let r = r.0; + let s = s.0; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::mat4::Mat4::from_trs(t, &r, s)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMat4Mat4 { inner: __v }) + } + + /// Right-handed view matrix: camera at `eye` looking toward + /// `target` with the −Z axis forward in view space. + /// + /// Panics: + /// Panics when eye == target or `up` is parallel to the view + /// direction. + /// + /// Rust: `spatial::mat4::Mat4::look_at` + #[pyo3(name = "look_at")] + #[staticmethod] + #[pyo3(signature = (eye, target, up))] + fn look_at(eye: crate::generated::types::PyVec3Arg, target: crate::generated::types::PyVec3Arg, up: crate::generated::types::PyVec3Arg) -> PyResult { + let eye = eye.0; + let target = target.0; + let up = up.0; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::mat4::Mat4::look_at(eye, target, up)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMat4Mat4 { inner: __v }) + } + + /// OpenGL-style perspective projection mapping the view frustum to + /// the clip cube [−1, 1]³ (near plane → z = −1, far → z = +1). + /// + /// Panics: + /// Panics unless 0 < near < far, 0 < fov_y < π, and aspect > 0. + /// + /// Rust: `spatial::mat4::Mat4::perspective` + #[pyo3(name = "perspective")] + #[staticmethod] + #[pyo3(signature = (fov_y_rad, aspect, near, far))] + fn perspective(fov_y_rad: f64, aspect: f64, near: f64, far: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::mat4::Mat4::perspective(fov_y_rad, aspect, near, far)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMat4Mat4 { inner: __v }) + } + + /// OpenGL-style orthographic projection onto the clip cube. + /// + /// Panics: + /// Panics on a zero-extent box. + /// + /// Rust: `spatial::mat4::Mat4::orthographic` + #[pyo3(name = "orthographic")] + #[staticmethod] + #[pyo3(signature = (l, r, b, t, near, far))] + fn orthographic(l: f64, r: f64, b: f64, t: f64, near: f64, far: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::mat4::Mat4::orthographic(l, r, b, t, near, far)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMat4Mat4 { inner: __v }) + } + + /// Matrix product self·other. + /// + /// Rust: `spatial::mat4::Mat4::mul` + #[pyo3(name = "mul")] + #[pyo3(signature = (other))] + fn mul(&self, other: crate::generated::types::PyMat4Mat4) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.mul(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMat4Mat4 { inner: __v }) + } + + /// Transforms a point (w = 1) with perspective division; a point + /// mapped to w' = 0 is returned undivided. + /// + /// Rust: `spatial::mat4::Mat4::transform_point` + #[pyo3(name = "transform_point")] + #[pyo3(signature = (p))] + fn transform_point(&self, p: crate::generated::types::PyVec3Arg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.transform_point(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Transforms a direction (w = 0): translation has no effect. + /// + /// Rust: `spatial::mat4::Mat4::transform_vector` + #[pyo3(name = "transform_vector")] + #[pyo3(signature = (v))] + fn transform_vector(&self, v: crate::generated::types::PyVec3Arg) -> PyResult { + let v = v.0; + let __r = crate::runtime::guard(|| self.inner.transform_vector(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Full homogeneous transform. + /// + /// Rust: `spatial::mat4::Mat4::transform_homogeneous` + #[pyo3(name = "transform_homogeneous")] + #[pyo3(signature = (p))] + fn transform_homogeneous<'py>(&self, py: Python<'py>, p: Vec) -> PyResult> { + let p = <[f64; 4]>::try_from(p).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 4 values, got {}", __v.len())))?; + let __r = py.detach(move || crate::runtime::guard(move || self.inner.transform_homogeneous(p))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_vec()) + } + + /// Transpose. + /// + /// Rust: `spatial::mat4::Mat4::transpose` + #[pyo3(name = "transpose")] + #[pyo3(signature = ())] + fn transpose(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.transpose()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMat4Mat4 { inner: __v }) + } + + /// Determinant by cofactor expansion over 2×2 sub-determinants. + /// + /// Rust: `spatial::mat4::Mat4::determinant` + #[pyo3(name = "determinant")] + #[pyo3(signature = ())] + fn determinant(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.determinant()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// General inverse via the 2×2-subdeterminant (Laplace) expansion; + /// `None` when the determinant is negligible. + /// + /// Rust: `spatial::mat4::Mat4::inverse` + #[pyo3(name = "inverse")] + #[pyo3(signature = ())] + fn inverse(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.inverse()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyMat4Mat4 { inner: __x })) + } + + /// Fast inverse for affine matrices (last row 0 0 0 1): + /// M⁻¹ = [A⁻¹, −A⁻¹·t]. `None` if the matrix is not affine or A is + /// singular. + /// + /// Rust: `spatial::mat4::Mat4::inverse_affine` + #[pyo3(name = "inverse_affine")] + #[pyo3(signature = ())] + fn inverse_affine(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.inverse_affine()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyMat4Mat4 { inner: __x })) + } + + /// Upper-left 3×3 block. + /// + /// Rust: `spatial::mat4::Mat4::to_mat3` + #[pyo3(name = "to_mat3")] + #[pyo3(signature = ())] + fn to_mat3(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.to_mat3()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMat3 { inner: __v }) + } + + /// Recovers (translation, rotation, scale) from an affine T·R·S + /// matrix without shear. `None` for non-affine input or a + /// degenerate (zero) scale. A negative determinant is folded into + /// the x scale. + /// + /// Rust: `spatial::mat4::Mat4::decompose_trs` + #[pyo3(name = "decompose_trs")] + #[pyo3(signature = ())] + fn decompose_trs(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.decompose_trs()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| (crate::generated::types::PyVec3 { inner: __x.0 }, crate::generated::types::PyQuaternion { inner: __x.1 }, crate::generated::types::PyVec3 { inner: __x.2 }))) + } + + /// Normal matrix: inverse-transpose of the upper-left 3×3 (falls + /// back to the block itself when singular). + /// + /// Rust: `spatial::mat4::Mat4::normal_matrix` + #[pyo3(name = "normal_matrix")] + #[pyo3(signature = ())] + fn normal_matrix(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.normal_matrix()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMat3 { inner: __v }) + } + + fn __mul__(&self, rhs: crate::generated::types::PyMat4Mat4) -> PyResult { + let rhs = rhs.inner; + let __r = crate::runtime::guard(|| ::mul(self.inner.clone(), rhs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMat4Mat4 { inner: __v }) + } + + #[getter] + #[pyo3(name = "data")] + fn py_get_data(&self) -> PyResult>> { Ok(self.inner.data.clone().into_iter().map(|__x| __x.to_vec()).collect::>()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Mat4", "Mat4", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// +/// Rust: `spatial::octree::Octree` +#[pyclass(name = "Octree", module = "numeria.spatial.octree")] +pub struct PyOctree { pub inner: rust_physics_engine::spatial::octree::Octree } +#[pymethods] +impl PyOctree { + /// Builds a Barnes-Hut octree from a set of bodies, computing bounding box and center-of-mass hierarchy. + /// + /// Rust: `spatial::octree::Octree::build` + #[pyo3(name = "build")] + #[staticmethod] + #[pyo3(signature = (bodies))] + fn build(bodies: Vec) -> PyResult { + let bodies = bodies.into_iter().map(|__e| __e.inner).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::octree::Octree::build(&bodies)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyOctree { inner: __v }) + } + + /// Computes gravitational acceleration on body `idx` using the Barnes-Hut tree walk with opening angle θ. + /// + /// Rust: `spatial::octree::Octree::compute_acceleration` + #[pyo3(name = "compute_acceleration")] + #[pyo3(signature = (bodies, idx, theta, softening))] + fn compute_acceleration(&self, bodies: Vec, idx: usize, theta: f64, softening: f64) -> PyResult { + let bodies = bodies.into_iter().map(|__e| __e.inner).collect::>(); + let __r = crate::runtime::guard(|| self.inner.compute_acceleration(&bodies, idx, theta, softening)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// Axis-aligned bounding box. +/// +/// Rust: `spatial::primitives::Aabb` +#[pyclass(name = "Aabb", module = "numeria.spatial.primitives", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyAabb { pub inner: rust_physics_engine::spatial::primitives::Aabb } +#[pymethods] +impl PyAabb { + /// Builds a `Aabb` from its fields. + #[new] + #[pyo3(signature = (min, max))] + fn __new__(min: crate::generated::types::PyVec3Arg, max: crate::generated::types::PyVec3Arg) -> Self { + let min = min.0; + let max = max.0; + Self { inner: rust_physics_engine::spatial::primitives::Aabb { min: min, max: max } } + } + + /// Smallest box containing all points. + /// + /// Panics: + /// Panics on an empty slice. + /// + /// Rust: `spatial::primitives::Aabb::from_points` + #[pyo3(name = "from_points")] + #[staticmethod] + #[pyo3(signature = (points))] + fn from_points(points: Vec) -> PyResult { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::primitives::Aabb::from_points(&points)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAabb { inner: __v }) + } + + /// Smallest box containing both. + /// + /// Rust: `spatial::primitives::Aabb::union` + #[pyo3(name = "union")] + #[pyo3(signature = (other))] + fn union(&self, other: crate::generated::types::PyAabb) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.union(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAabb { inner: __v }) + } + + /// Overlapping region, `None` when disjoint. + /// + /// Rust: `spatial::primitives::Aabb::intersection` + #[pyo3(name = "intersection")] + #[pyo3(signature = (other))] + fn intersection(&self, other: crate::generated::types::PyAabb) -> PyResult> { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.intersection(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyAabb { inner: __x })) + } + + /// Center point. + /// + /// Rust: `spatial::primitives::Aabb::center` + #[pyo3(name = "center")] + #[pyo3(signature = ())] + fn center(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.center()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Half-widths per axis. + /// + /// Rust: `spatial::primitives::Aabb::extents` + #[pyo3(name = "extents")] + #[pyo3(signature = ())] + fn extents(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.extents()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Total surface area. + /// + /// Rust: `spatial::primitives::Aabb::surface_area` + #[pyo3(name = "surface_area")] + #[pyo3(signature = ())] + fn surface_area(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.surface_area()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Volume. + /// + /// Rust: `spatial::primitives::Aabb::volume` + #[pyo3(name = "volume")] + #[pyo3(signature = ())] + fn volume(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.volume()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Closed containment test. + /// + /// Rust: `spatial::primitives::Aabb::contains_point` + #[pyo3(name = "contains_point")] + #[pyo3(signature = (p))] + fn contains_point(&self, p: crate::generated::types::PyVec3Arg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.contains_point(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Box grown by `margin` on every side. + /// + /// Rust: `spatial::primitives::Aabb::expand` + #[pyo3(name = "expand")] + #[pyo3(signature = (margin))] + fn expand(&self, margin: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.expand(margin)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAabb { inner: __v }) + } + + /// The eight corner points. + /// + /// Rust: `spatial::primitives::Aabb::corners` + #[pyo3(name = "corners")] + #[pyo3(signature = ())] + fn corners(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.corners()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) + } + + /// Axis-aligned bounds of the transformed corners. + /// + /// Rust: `spatial::primitives::Aabb::transform` + #[pyo3(name = "transform")] + #[pyo3(signature = (m))] + fn transform(&self, m: crate::generated::types::PyMat4Mat4) -> PyResult { + let m = m.inner; + let __r = crate::runtime::guard(|| self.inner.transform(&m)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAabb { inner: __v }) + } + + #[getter] + #[pyo3(name = "min")] + fn py_get_min(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.min.clone() }) } + + #[getter] + #[pyo3(name = "max")] + fn py_get_max(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.max.clone() }) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Aabb", "Aabb", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Capsule: segment swept by a sphere. +/// +/// Rust: `spatial::primitives::Capsule` +#[pyclass(name = "Capsule", module = "numeria.spatial.primitives", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyCapsule { pub inner: rust_physics_engine::spatial::primitives::Capsule } +#[pymethods] +impl PyCapsule { + /// Builds a `Capsule` from its fields. + #[new] + #[pyo3(signature = (a, b, radius))] + fn __new__(a: crate::generated::types::PyVec3Arg, b: crate::generated::types::PyVec3Arg, radius: f64) -> Self { + let a = a.0; + let b = b.0; + Self { inner: rust_physics_engine::spatial::primitives::Capsule { a: a, b: b, radius: radius } } + } + + #[getter] + #[pyo3(name = "a")] + fn py_get_a(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.a.clone() }) } + + #[getter] + #[pyo3(name = "b")] + fn py_get_b(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.b.clone() }) } + + #[getter] + #[pyo3(name = "radius")] + fn py_get_radius(&self) -> PyResult { Ok(self.inner.radius) } + + #[setter] + #[pyo3(name = "radius")] + fn py_set_radius(&mut self, v: f64) { self.inner.radius = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Capsule", "Capsule", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Circle in the plane. +/// +/// Rust: `spatial::primitives::Circle` +#[pyclass(name = "Circle", module = "numeria.spatial.primitives", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyCircle { pub inner: rust_physics_engine::spatial::primitives::Circle } +#[pymethods] +impl PyCircle { + /// Builds a `Circle` from its fields. + #[new] + #[pyo3(signature = (center, radius))] + fn __new__(center: crate::generated::types::PyVec2Arg, radius: f64) -> Self { + let center = center.0; + Self { inner: rust_physics_engine::spatial::primitives::Circle { center: center, radius: radius } } + } + + #[getter] + #[pyo3(name = "center")] + fn py_get_center(&self) -> PyResult { Ok(crate::generated::types::PyVec2 { inner: self.inner.center.clone() }) } + + #[getter] + #[pyo3(name = "radius")] + fn py_get_radius(&self) -> PyResult { Ok(self.inner.radius) } + + #[setter] + #[pyo3(name = "radius")] + fn py_set_radius(&mut self, v: f64) { self.inner.radius = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Circle", "Circle", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Finite cylinder between two cap centers. +/// +/// Rust: `spatial::primitives::Cylinder` +#[pyclass(name = "Cylinder", module = "numeria.spatial.primitives", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyCylinder { pub inner: rust_physics_engine::spatial::primitives::Cylinder } +#[pymethods] +impl PyCylinder { + /// Builds a `Cylinder` from its fields. + #[new] + #[pyo3(signature = (a, b, radius))] + fn __new__(a: crate::generated::types::PyVec3Arg, b: crate::generated::types::PyVec3Arg, radius: f64) -> Self { + let a = a.0; + let b = b.0; + Self { inner: rust_physics_engine::spatial::primitives::Cylinder { a: a, b: b, radius: radius } } + } + + #[getter] + #[pyo3(name = "a")] + fn py_get_a(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.a.clone() }) } + + #[getter] + #[pyo3(name = "b")] + fn py_get_b(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.b.clone() }) } + + #[getter] + #[pyo3(name = "radius")] + fn py_get_radius(&self) -> PyResult { Ok(self.inner.radius) } + + #[setter] + #[pyo3(name = "radius")] + fn py_set_radius(&mut self, v: f64) { self.inner.radius = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Cylinder", "Cylinder", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Oriented bounding box: rotation columns are the local axes. +/// +/// Rust: `spatial::primitives::Obb` +#[pyclass(name = "Obb", module = "numeria.spatial.primitives", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyObb { pub inner: rust_physics_engine::spatial::primitives::Obb } +#[pymethods] +impl PyObb { + /// Builds a `Obb` from its fields. + #[new] + #[pyo3(signature = (center, half_extents, rotation))] + fn __new__(center: crate::generated::types::PyVec3Arg, half_extents: crate::generated::types::PyVec3Arg, rotation: crate::generated::types::PyMat3) -> Self { + let center = center.0; + let half_extents = half_extents.0; + let rotation = rotation.inner; + Self { inner: rust_physics_engine::spatial::primitives::Obb { center: center, half_extents: half_extents, rotation: rotation } } + } + + /// Local axes (columns of the rotation). + /// + /// Rust: `spatial::primitives::Obb::axes` + #[pyo3(name = "axes")] + #[pyo3(signature = ())] + fn axes(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.axes()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) + } + + /// The eight corner points. + /// + /// Rust: `spatial::primitives::Obb::corners` + #[pyo3(name = "corners")] + #[pyo3(signature = ())] + fn corners(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.corners()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) + } + + /// Tight axis-aligned bounds: center ± Σ |axisᵢ|·hᵢ. + /// + /// Rust: `spatial::primitives::Obb::to_aabb` + #[pyo3(name = "to_aabb")] + #[pyo3(signature = ())] + fn to_aabb(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.to_aabb()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAabb { inner: __v }) + } + + /// PCA-fitted box: axes from the principal directions of the point + /// covariance (local 3×3 Jacobi), extents from the projections. + /// + /// Panics: + /// Panics on an empty slice. + /// + /// Rust: `spatial::primitives::Obb::from_points_pca` + #[pyo3(name = "from_points_pca")] + #[staticmethod] + #[pyo3(signature = (points))] + fn from_points_pca(points: Vec) -> PyResult { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::primitives::Obb::from_points_pca(&points)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyObb { inner: __v }) + } + + #[getter] + #[pyo3(name = "center")] + fn py_get_center(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.center.clone() }) } + + #[getter] + #[pyo3(name = "half_extents")] + fn py_get_half_extents(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.half_extents.clone() }) } + + #[getter] + #[pyo3(name = "rotation")] + fn py_get_rotation(&self) -> PyResult { Ok(crate::generated::types::PyMat3 { inner: self.inner.rotation.clone() }) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Obb", "Obb", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Plane n·p + d = 0 with unit normal. +/// +/// Rust: `spatial::primitives::Plane` +#[pyclass(name = "Plane", module = "numeria.spatial.primitives", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyPrimitivesPlane { pub inner: rust_physics_engine::spatial::primitives::Plane } +#[pymethods] +impl PyPrimitivesPlane { + /// Builds a `Plane` from its fields. + #[new] + #[pyo3(signature = (normal, d))] + fn __new__(normal: crate::generated::types::PyVec3Arg, d: f64) -> Self { + let normal = normal.0; + Self { inner: rust_physics_engine::spatial::primitives::Plane { normal: normal, d: d } } + } + + /// Plane through a point with the given (normalized) normal. + /// + /// Panics: + /// Panics on a zero normal. + /// + /// Rust: `spatial::primitives::Plane::from_point_normal` + #[pyo3(name = "from_point_normal")] + #[staticmethod] + #[pyo3(signature = (p, normal))] + fn from_point_normal(p: crate::generated::types::PyVec3Arg, normal: crate::generated::types::PyVec3Arg) -> PyResult { + let p = p.0; + let normal = normal.0; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::primitives::Plane::from_point_normal(p, normal)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPrimitivesPlane { inner: __v }) + } + + /// Plane through three points (CCW normal); `None` when collinear. + /// + /// Rust: `spatial::primitives::Plane::from_three_points` + #[pyo3(name = "from_three_points")] + #[staticmethod] + #[pyo3(signature = (a, b, c))] + fn from_three_points(a: crate::generated::types::PyVec3Arg, b: crate::generated::types::PyVec3Arg, c: crate::generated::types::PyVec3Arg) -> PyResult> { + let a = a.0; + let b = b.0; + let c = c.0; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::primitives::Plane::from_three_points(a, b, c)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyPrimitivesPlane { inner: __x })) + } + + /// Signed distance: positive on the normal side. + /// + /// Rust: `spatial::primitives::Plane::signed_distance` + #[pyo3(name = "signed_distance")] + #[pyo3(signature = (p))] + fn signed_distance(&self, p: crate::generated::types::PyVec3Arg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.signed_distance(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Orthogonal projection onto the plane. + /// + /// Rust: `spatial::primitives::Plane::project` + #[pyo3(name = "project")] + #[pyo3(signature = (p))] + fn project(&self, p: crate::generated::types::PyVec3Arg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.project(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// The same plane with the opposite orientation. + /// + /// Rust: `spatial::primitives::Plane::flip` + #[pyo3(name = "flip")] + #[pyo3(signature = ())] + fn flip(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.flip()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPrimitivesPlane { inner: __v }) + } + + #[getter] + #[pyo3(name = "normal")] + fn py_get_normal(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.normal.clone() }) } + + #[getter] + #[pyo3(name = "d")] + fn py_get_d(&self) -> PyResult { Ok(self.inner.d) } + + #[setter] + #[pyo3(name = "d")] + fn py_set_d(&mut self, v: f64) { self.inner.d = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Plane", "Plane", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Simple polygon in the plane (implicitly closed). +/// +/// Rust: `spatial::primitives::Polygon2` +#[pyclass(name = "Polygon2", module = "numeria.spatial.primitives", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyPolygon2 { pub inner: rust_physics_engine::spatial::primitives::Polygon2 } +#[pymethods] +impl PyPolygon2 { + /// Panics: + /// Panics with fewer than 3 vertices. + /// + /// Rust: `spatial::primitives::Polygon2::new` + #[new] + #[pyo3(signature = (vertices))] + fn __new__(vertices: Vec) -> PyResult { + let vertices = vertices.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::primitives::Polygon2::new(vertices)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolygon2 { inner: __v }) + } + + /// Shoelace signed area (positive when CCW). + /// + /// Rust: `spatial::primitives::Polygon2::area_signed` + #[pyo3(name = "area_signed")] + #[pyo3(signature = ())] + fn area_signed(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.area_signed()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Absolute area. + /// + /// Rust: `spatial::primitives::Polygon2::area` + #[pyo3(name = "area")] + #[pyo3(signature = ())] + fn area(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.area()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Boundary length. + /// + /// Rust: `spatial::primitives::Polygon2::perimeter` + #[pyo3(name = "perimeter")] + #[pyo3(signature = ())] + fn perimeter(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.perimeter()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Area centroid (falls back to the vertex mean for degenerate + /// zero-area polygons). + /// + /// Rust: `spatial::primitives::Polygon2::centroid` + #[pyo3(name = "centroid")] + #[pyo3(signature = ())] + fn centroid(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.centroid()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) + } + + /// Convexity: all cross products of consecutive edges share a sign + /// (collinear runs allowed). + /// + /// Rust: `spatial::primitives::Polygon2::is_convex` + #[pyo3(name = "is_convex")] + #[pyo3(signature = ())] + fn is_convex(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_convex()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// True when the vertex order is counter-clockwise. + /// + /// Rust: `spatial::primitives::Polygon2::is_ccw` + #[pyo3(name = "is_ccw")] + #[pyo3(signature = ())] + fn is_ccw(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_ccw()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Reverses the winding in place. + /// + /// Rust: `spatial::primitives::Polygon2::reverse` + #[pyo3(name = "reverse")] + #[pyo3(signature = ())] + fn reverse(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.reverse()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Axis-aligned bounding rectangle. + /// + /// Rust: `spatial::primitives::Polygon2::bounding_rect` + #[pyo3(name = "bounding_rect")] + #[pyo3(signature = ())] + fn bounding_rect(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.bounding_rect()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyRect { inner: __v }) + } + + /// Simplicity: no two non-adjacent edges intersect (O(n²) test). + /// + /// Rust: `spatial::primitives::Polygon2::is_simple` + #[pyo3(name = "is_simple")] + #[pyo3(signature = ())] + fn is_simple(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_simple()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "vertices")] + fn py_get_vertices(&self) -> PyResult> { Ok(self.inner.vertices.clone().into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Polygon2", "Polygon2", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// 3-D polyline (open or closed). +/// +/// Rust: `spatial::primitives::Polyline` +#[pyclass(name = "Polyline", module = "numeria.spatial.primitives", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyPolyline { pub inner: rust_physics_engine::spatial::primitives::Polyline } +#[pymethods] +impl PyPolyline { + /// Builds a `Polyline` from its fields. + #[new] + #[pyo3(signature = (points, closed))] + fn __new__(points: Vec, closed: bool) -> Self { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + Self { inner: rust_physics_engine::spatial::primitives::Polyline { points: points, closed: closed } } + } + + /// Number of segments (accounting for closure). + /// + /// Rust: `spatial::primitives::Polyline::segment_count` + #[pyo3(name = "segment_count")] + #[pyo3(signature = ())] + fn segment_count(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.segment_count()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Endpoints of segment i. + /// + /// Rust: `spatial::primitives::Polyline::segment` + #[pyo3(name = "segment")] + #[pyo3(signature = (i))] + fn segment(&self, i: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.segment(i)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PySegment { inner: __v }) + } + + /// Total arclength. + /// + /// Rust: `spatial::primitives::Polyline::length` + #[pyo3(name = "length")] + #[pyo3(signature = ())] + fn length(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.length()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Point at arclength s (clamped to [0, length]). + /// + /// Rust: `spatial::primitives::Polyline::point_at_arclength` + #[pyo3(name = "point_at_arclength")] + #[pyo3(signature = (s))] + fn point_at_arclength(&self, s: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.point_at_arclength(s)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Unit tangent of the segment containing arclength s. + /// + /// Rust: `spatial::primitives::Polyline::tangent_at` + #[pyo3(name = "tangent_at")] + #[pyo3(signature = (s))] + fn tangent_at(&self, s: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.tangent_at(s)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Resamples at (approximately) uniform arclength spacing, + /// preserving the endpoints. + /// + /// Panics: + /// Panics unless spacing > 0. + /// + /// Rust: `spatial::primitives::Polyline::resample` + #[pyo3(name = "resample")] + #[pyo3(signature = (spacing))] + fn resample(&self, spacing: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.resample(spacing)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPolyline { inner: __v }) + } + + /// Axis-aligned bounds of the points. + /// + /// Panics: + /// Panics on an empty polyline. + /// + /// Rust: `spatial::primitives::Polyline::bounding_box` + #[pyo3(name = "bounding_box")] + #[pyo3(signature = ())] + fn bounding_box(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.bounding_box()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAabb { inner: __v }) + } + + #[getter] + #[pyo3(name = "points")] + fn py_get_points(&self) -> PyResult> { Ok(self.inner.points.clone().into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) } + + #[getter] + #[pyo3(name = "closed")] + fn py_get_closed(&self) -> PyResult { Ok(self.inner.closed) } + + #[setter] + #[pyo3(name = "closed")] + fn py_set_closed(&mut self, v: bool) { self.inner.closed = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Polyline", "Polyline", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Ray with normalized direction. +/// +/// Rust: `spatial::primitives::Ray` +#[pyclass(name = "Ray", module = "numeria.spatial.primitives", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyRay { pub inner: rust_physics_engine::spatial::primitives::Ray } +#[pymethods] +impl PyRay { + /// Panics: + /// Panics on a zero direction (which cannot be normalized). + /// + /// Rust: `spatial::primitives::Ray::new` + #[new] + #[pyo3(signature = (origin, dir))] + fn __new__(origin: crate::generated::types::PyVec3Arg, dir: crate::generated::types::PyVec3Arg) -> PyResult { + let origin = origin.0; + let dir = dir.0; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::primitives::Ray::new(origin, dir)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyRay { inner: __v }) + } + + /// Point at parameter t: origin + t·dir. + /// + /// Rust: `spatial::primitives::Ray::at` + #[pyo3(name = "at")] + #[pyo3(signature = (t))] + fn at(&self, t: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.at(t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + #[getter] + #[pyo3(name = "origin")] + fn py_get_origin(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.origin.clone() }) } + + #[getter] + #[pyo3(name = "dir")] + fn py_get_dir(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.dir.clone() }) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Ray", "Ray", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Axis-aligned rectangle. +/// +/// Rust: `spatial::primitives::Rect` +#[pyclass(name = "Rect", module = "numeria.spatial.primitives", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyRect { pub inner: rust_physics_engine::spatial::primitives::Rect } +#[pymethods] +impl PyRect { + /// Builds a `Rect` from its fields. + #[new] + #[pyo3(signature = (min, max))] + fn __new__(min: crate::generated::types::PyVec2Arg, max: crate::generated::types::PyVec2Arg) -> Self { + let min = min.0; + let max = max.0; + Self { inner: rust_physics_engine::spatial::primitives::Rect { min: min, max: max } } + } + + /// Smallest rectangle containing all points. + /// + /// Panics: + /// Panics on an empty slice. + /// + /// Rust: `spatial::primitives::Rect::from_points` + #[pyo3(name = "from_points")] + #[staticmethod] + #[pyo3(signature = (points))] + fn from_points(points: Vec) -> PyResult { + let points = points.into_iter().map(|__e| __e.0).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::primitives::Rect::from_points(&points)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyRect { inner: __v }) + } + + /// Smallest rectangle containing both. + /// + /// Rust: `spatial::primitives::Rect::union` + #[pyo3(name = "union")] + #[pyo3(signature = (other))] + fn union(&self, other: crate::generated::types::PyRect) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.union(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyRect { inner: __v }) + } + + /// Overlapping region, `None` when disjoint. + /// + /// Rust: `spatial::primitives::Rect::intersection` + #[pyo3(name = "intersection")] + #[pyo3(signature = (other))] + fn intersection(&self, other: crate::generated::types::PyRect) -> PyResult> { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.intersection(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyRect { inner: __x })) + } + + /// Center point. + /// + /// Rust: `spatial::primitives::Rect::center` + #[pyo3(name = "center")] + #[pyo3(signature = ())] + fn center(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.center()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) + } + + /// Half-widths per axis. + /// + /// Rust: `spatial::primitives::Rect::extents` + #[pyo3(name = "extents")] + #[pyo3(signature = ())] + fn extents(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.extents()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) + } + + /// Area. + /// + /// Rust: `spatial::primitives::Rect::area` + #[pyo3(name = "area")] + #[pyo3(signature = ())] + fn area(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.area()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Perimeter length. + /// + /// Rust: `spatial::primitives::Rect::perimeter` + #[pyo3(name = "perimeter")] + #[pyo3(signature = ())] + fn perimeter(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.perimeter()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Closed containment test. + /// + /// Rust: `spatial::primitives::Rect::contains_point` + #[pyo3(name = "contains_point")] + #[pyo3(signature = (p))] + fn contains_point(&self, p: crate::generated::types::PyVec2Arg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.contains_point(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Rectangle grown by `margin` on every side. + /// + /// Rust: `spatial::primitives::Rect::expand` + #[pyo3(name = "expand")] + #[pyo3(signature = (margin))] + fn expand(&self, margin: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.expand(margin)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyRect { inner: __v }) + } + + /// The four corner points (CCW from min). + /// + /// Rust: `spatial::primitives::Rect::corners` + #[pyo3(name = "corners")] + #[pyo3(signature = ())] + fn corners(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.corners()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec2 { inner: __x }).collect::>()) + } + + #[getter] + #[pyo3(name = "min")] + fn py_get_min(&self) -> PyResult { Ok(crate::generated::types::PyVec2 { inner: self.inner.min.clone() }) } + + #[getter] + #[pyo3(name = "max")] + fn py_get_max(&self) -> PyResult { Ok(crate::generated::types::PyVec2 { inner: self.inner.max.clone() }) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Rect", "Rect", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// 3-D line segment. +/// +/// Rust: `spatial::primitives::Segment` +#[pyclass(name = "Segment", module = "numeria.spatial.primitives", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PySegment { pub inner: rust_physics_engine::spatial::primitives::Segment } +#[pymethods] +impl PySegment { + /// Builds a `Segment` from its fields. + #[new] + #[pyo3(signature = (a, b))] + fn __new__(a: crate::generated::types::PyVec3Arg, b: crate::generated::types::PyVec3Arg) -> Self { + let a = a.0; + let b = b.0; + Self { inner: rust_physics_engine::spatial::primitives::Segment { a: a, b: b } } + } + + #[getter] + #[pyo3(name = "a")] + fn py_get_a(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.a.clone() }) } + + #[getter] + #[pyo3(name = "b")] + fn py_get_b(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.b.clone() }) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Segment", "Segment", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// 2-D line segment. +/// +/// Rust: `spatial::primitives::Segment2` +#[pyclass(name = "Segment2", module = "numeria.spatial.primitives", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyPrimitivesSegment2 { pub inner: rust_physics_engine::spatial::primitives::Segment2 } +#[pymethods] +impl PyPrimitivesSegment2 { + /// Builds a `Segment2` from its fields. + #[new] + #[pyo3(signature = (a, b))] + fn __new__(a: crate::generated::types::PyVec2Arg, b: crate::generated::types::PyVec2Arg) -> Self { + let a = a.0; + let b = b.0; + Self { inner: rust_physics_engine::spatial::primitives::Segment2 { a: a, b: b } } + } + + #[getter] + #[pyo3(name = "a")] + fn py_get_a(&self) -> PyResult { Ok(crate::generated::types::PyVec2 { inner: self.inner.a.clone() }) } + + #[getter] + #[pyo3(name = "b")] + fn py_get_b(&self) -> PyResult { Ok(crate::generated::types::PyVec2 { inner: self.inner.b.clone() }) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Segment2", "Segment2", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Sphere. +/// +/// Rust: `spatial::primitives::Sphere` +#[pyclass(name = "Sphere", module = "numeria.spatial.primitives", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PySphere { pub inner: rust_physics_engine::spatial::primitives::Sphere } +#[pymethods] +impl PySphere { + /// Builds a `Sphere` from its fields. + #[new] + #[pyo3(signature = (center, radius))] + fn __new__(center: crate::generated::types::PyVec3Arg, radius: f64) -> Self { + let center = center.0; + Self { inner: rust_physics_engine::spatial::primitives::Sphere { center: center, radius: radius } } + } + + #[getter] + #[pyo3(name = "center")] + fn py_get_center(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.center.clone() }) } + + #[getter] + #[pyo3(name = "radius")] + fn py_get_radius(&self) -> PyResult { Ok(self.inner.radius) } + + #[setter] + #[pyo3(name = "radius")] + fn py_set_radius(&mut self, v: f64) { self.inner.radius = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Sphere", "Sphere", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// 3-D triangle. +/// +/// Rust: `spatial::primitives::Triangle` +#[pyclass(name = "Triangle", module = "numeria.spatial.primitives", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyTriangle { pub inner: rust_physics_engine::spatial::primitives::Triangle } +#[pymethods] +impl PyTriangle { + /// Builds a `Triangle` from its fields. + #[new] + #[pyo3(signature = (a, b, c))] + fn __new__(a: crate::generated::types::PyVec3Arg, b: crate::generated::types::PyVec3Arg, c: crate::generated::types::PyVec3Arg) -> Self { + let a = a.0; + let b = b.0; + let c = c.0; + Self { inner: rust_physics_engine::spatial::primitives::Triangle { a: a, b: b, c: c } } + } + + /// Unit normal of the CCW winding (ZERO for degenerate triangles). + /// + /// Rust: `spatial::primitives::Triangle::normal` + #[pyo3(name = "normal")] + #[pyo3(signature = ())] + fn normal(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.normal()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Area = ½|AB × AC|. + /// + /// Rust: `spatial::primitives::Triangle::area` + #[pyo3(name = "area")] + #[pyo3(signature = ())] + fn area(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.area()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Centroid (A + B + C)/3. + /// + /// Rust: `spatial::primitives::Triangle::centroid` + #[pyo3(name = "centroid")] + #[pyo3(signature = ())] + fn centroid(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.centroid()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Barycentric coordinates (u, v, w) of the projection of p onto + /// the triangle's plane, with u + v + w = 1 and p ≈ u·A + v·B + w·C + /// (Ericson, *Real-Time Collision Detection*, §3.4). + /// + /// Rust: `spatial::primitives::Triangle::barycentric` + #[pyo3(name = "barycentric")] + #[pyo3(signature = (p))] + fn barycentric(&self, p: crate::generated::types::PyVec3Arg) -> PyResult<(f64, f64, f64)> { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.barycentric(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) + } + + /// Point at barycentric coordinates (u, v, w). + /// + /// Rust: `spatial::primitives::Triangle::from_barycentric` + #[pyo3(name = "from_barycentric")] + #[pyo3(signature = (u, v, w))] + fn from_barycentric(&self, u: f64, v: f64, w: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.from_barycentric(u, v, w)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Circumcenter (equidistant from the three vertices). + /// + /// Rust: `spatial::primitives::Triangle::circumcenter` + #[pyo3(name = "circumcenter")] + #[pyo3(signature = ())] + fn circumcenter(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.circumcenter()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Incenter, weighted by opposite side lengths. + /// + /// Rust: `spatial::primitives::Triangle::incenter` + #[pyo3(name = "incenter")] + #[pyo3(signature = ())] + fn incenter(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.incenter()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// Degeneracy test: area below `tol`. + /// + /// Rust: `spatial::primitives::Triangle::is_degenerate` + #[pyo3(name = "is_degenerate")] + #[pyo3(signature = (tol))] + fn is_degenerate(&self, tol: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_degenerate(tol)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Supporting plane (`None` for degenerate triangles). + /// + /// Rust: `spatial::primitives::Triangle::to_plane` + #[pyo3(name = "to_plane")] + #[pyo3(signature = ())] + fn to_plane(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.to_plane()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyPrimitivesPlane { inner: __x })) + } + + #[getter] + #[pyo3(name = "a")] + fn py_get_a(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.a.clone() }) } + + #[getter] + #[pyo3(name = "b")] + fn py_get_b(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.b.clone() }) } + + #[getter] + #[pyo3(name = "c")] + fn py_get_c(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.c.clone() }) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Triangle", "Triangle", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// 2-D triangle. +/// +/// Rust: `spatial::primitives::Triangle2` +#[pyclass(name = "Triangle2", module = "numeria.spatial.primitives", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyTriangle2 { pub inner: rust_physics_engine::spatial::primitives::Triangle2 } +#[pymethods] +impl PyTriangle2 { + /// Builds a `Triangle2` from its fields. + #[new] + #[pyo3(signature = (a, b, c))] + fn __new__(a: crate::generated::types::PyVec2Arg, b: crate::generated::types::PyVec2Arg, c: crate::generated::types::PyVec2Arg) -> Self { + let a = a.0; + let b = b.0; + let c = c.0; + Self { inner: rust_physics_engine::spatial::primitives::Triangle2 { a: a, b: b, c: c } } + } + + /// Signed area (positive when CCW). + /// + /// Rust: `spatial::primitives::Triangle2::area_signed` + #[pyo3(name = "area_signed")] + #[pyo3(signature = ())] + fn area_signed(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.area_signed()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Centroid. + /// + /// Rust: `spatial::primitives::Triangle2::centroid` + #[pyo3(name = "centroid")] + #[pyo3(signature = ())] + fn centroid(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.centroid()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) + } + + /// Barycentric coordinates of p. + /// + /// Rust: `spatial::primitives::Triangle2::barycentric` + #[pyo3(name = "barycentric")] + #[pyo3(signature = (p))] + fn barycentric(&self, p: crate::generated::types::PyVec2Arg) -> PyResult<(f64, f64, f64)> { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.barycentric(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1, __v.2)) + } + + /// Circumcircle through the three vertices. + /// + /// Panics: + /// Panics on collinear vertices. + /// + /// Rust: `spatial::primitives::Triangle2::circumcircle` + #[pyo3(name = "circumcircle")] + #[pyo3(signature = ())] + fn circumcircle(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.circumcircle()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyCircle { inner: __v }) + } + + /// True when the winding is counter-clockwise. + /// + /// Rust: `spatial::primitives::Triangle2::is_ccw` + #[pyo3(name = "is_ccw")] + #[pyo3(signature = ())] + fn is_ccw(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_ccw()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "a")] + fn py_get_a(&self) -> PyResult { Ok(crate::generated::types::PyVec2 { inner: self.inner.a.clone() }) } + + #[getter] + #[pyo3(name = "b")] + fn py_get_b(&self) -> PyResult { Ok(crate::generated::types::PyVec2 { inner: self.inner.b.clone() }) } + + #[getter] + #[pyo3(name = "c")] + fn py_get_c(&self) -> PyResult { Ok(crate::generated::types::PyVec2 { inner: self.inner.c.clone() }) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Triangle2", "Triangle2", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Plane projective transform p' ~ H·p. +/// +/// Rust: `spatial::projective::Homography` +#[pyclass(name = "Homography", module = "numeria.spatial.projective", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyHomography { pub inner: rust_physics_engine::spatial::projective::Homography } +#[pymethods] +impl PyHomography { + /// Direct linear transform from four point correspondences + /// (h₃₃ normalized to 1). `None` for degenerate configurations + /// (three collinear source or destination points). + /// + /// Rust: `spatial::projective::Homography::from_four_points` + #[pyo3(name = "from_four_points")] + #[staticmethod] + #[pyo3(signature = (src, dst))] + fn from_four_points(src: Vec, dst: Vec) -> PyResult> { + let src = <[rust_physics_engine::math::Vec2; 4]>::try_from(src.into_iter().map(|__e| __e.0).collect::>()).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 4 values, got {}", __v.len())))?; + let dst = <[rust_physics_engine::math::Vec2; 4]>::try_from(dst.into_iter().map(|__e| __e.0).collect::>()).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 4 values, got {}", __v.len())))?; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::projective::Homography::from_four_points(src, dst)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyHomography { inner: __x })) + } + + /// Applies to a Euclidean point; `None` when the image lies at + /// infinity. + /// + /// Rust: `spatial::projective::Homography::apply` + #[pyo3(name = "apply")] + #[pyo3(signature = (p))] + fn apply(&self, p: crate::generated::types::PyVec2Arg) -> PyResult> { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.apply(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyVec2 { inner: __x })) + } + + /// Inverse homography via the adjugate; `None` when singular. + /// + /// Rust: `spatial::projective::Homography::inverse` + #[pyo3(name = "inverse")] + #[pyo3(signature = ())] + fn inverse(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.inverse()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyHomography { inner: __x })) + } + + /// Composition self ∘ other (apply `other` first). + /// + /// Rust: `spatial::projective::Homography::compose` + #[pyo3(name = "compose")] + #[pyo3(signature = (other))] + fn compose(&self, other: crate::generated::types::PyHomography) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.compose(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyHomography { inner: __v }) + } + + /// Image of the point at infinity in the given direction — the + /// vanishing point of all lines with that direction. `None` when + /// the direction stays at infinity (affine maps). + /// + /// Rust: `spatial::projective::Homography::vanishing_point` + #[pyo3(name = "vanishing_point")] + #[pyo3(signature = (direction))] + fn vanishing_point(&self, direction: crate::generated::types::PyVec2Arg) -> PyResult> { + let direction = direction.0; + let __r = crate::runtime::guard(|| self.inner.vanishing_point(direction)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyVec2 { inner: __x })) + } + + #[getter] + #[pyo3(name = "h")] + fn py_get_h(&self) -> PyResult>> { Ok(self.inner.h.clone().into_iter().map(|__x| __x.to_vec()).collect::>()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Homography", "Homography", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Affine map of the plane. +/// +/// Rust: `spatial::transform2d::Affine2` +#[pyclass(name = "Affine2", module = "numeria.spatial.transform2d", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyAffine2 { pub inner: rust_physics_engine::spatial::transform2d::Affine2 } +#[pymethods] +impl PyAffine2 { + /// Identity map. + /// + /// Rust: `spatial::transform2d::Affine2::identity` + #[pyo3(name = "identity")] + #[staticmethod] + #[pyo3(signature = ())] + fn identity() -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::transform2d::Affine2::identity()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAffine2 { inner: __v }) + } + + /// Translation by t. + /// + /// Rust: `spatial::transform2d::Affine2::translation` + #[pyo3(name = "translation")] + #[staticmethod] + #[pyo3(signature = (t))] + fn translation(t: crate::generated::types::PyVec2Arg) -> PyResult { + let t = t.0; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::transform2d::Affine2::translation(t)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAffine2 { inner: __v }) + } + + /// Counter-clockwise rotation about the origin. + /// + /// Rust: `spatial::transform2d::Affine2::rotation` + #[pyo3(name = "rotation")] + #[staticmethod] + #[pyo3(signature = (angle))] + fn rotation(angle: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::transform2d::Affine2::rotation(angle)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAffine2 { inner: __v }) + } + + /// Rotation about an arbitrary center: T(c)·R·T(−c). + /// + /// Rust: `spatial::transform2d::Affine2::rotation_about` + #[pyo3(name = "rotation_about")] + #[staticmethod] + #[pyo3(signature = (angle, center))] + fn rotation_about(angle: f64, center: crate::generated::types::PyVec2Arg) -> PyResult { + let center = center.0; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::transform2d::Affine2::rotation_about(angle, center)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAffine2 { inner: __v }) + } + + /// Axis-aligned scaling. + /// + /// Rust: `spatial::transform2d::Affine2::scaling` + #[pyo3(name = "scaling")] + #[staticmethod] + #[pyo3(signature = (sx, sy))] + fn scaling(sx: f64, sy: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::transform2d::Affine2::scaling(sx, sy)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAffine2 { inner: __v }) + } + + /// Shear: x' = x + kx·y, y' = y + ky·x. + /// + /// Rust: `spatial::transform2d::Affine2::shear` + #[pyo3(name = "shear")] + #[staticmethod] + #[pyo3(signature = (kx, ky))] + fn shear(kx: f64, ky: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::transform2d::Affine2::shear(kx, ky)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAffine2 { inner: __v }) + } + + /// Reflection across the line through the origin with the given + /// direction. + /// + /// Panics: + /// Panics on a zero direction vector. + /// + /// Rust: `spatial::transform2d::Affine2::reflection` + #[pyo3(name = "reflection")] + #[staticmethod] + #[pyo3(signature = (line_through_origin_dir))] + fn reflection(line_through_origin_dir: crate::generated::types::PyVec2Arg) -> PyResult { + let line_through_origin_dir = line_through_origin_dir.0; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::transform2d::Affine2::reflection(line_through_origin_dir)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAffine2 { inner: __v }) + } + + /// The unique affine map taking three source points to three + /// destination points: M = Q·P⁻¹ with homogeneous point columns. + /// `None` when the source triple is collinear. + /// + /// Rust: `spatial::transform2d::Affine2::from_three_points` + #[pyo3(name = "from_three_points")] + #[staticmethod] + #[pyo3(signature = (src, dst))] + fn from_three_points(src: Vec, dst: Vec) -> PyResult> { + let src = <[rust_physics_engine::math::Vec2; 3]>::try_from(src.into_iter().map(|__e| __e.0).collect::>()).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 3 values, got {}", __v.len())))?; + let dst = <[rust_physics_engine::math::Vec2; 3]>::try_from(dst.into_iter().map(|__e| __e.0).collect::>()).map_err(|__v: Vec| pyo3::exceptions::PyValueError::new_err(format!("expected 3 values, got {}", __v.len())))?; + let __r = crate::runtime::guard(|| rust_physics_engine::spatial::transform2d::Affine2::from_three_points(src, dst)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyAffine2 { inner: __x })) + } + + /// Composition self ∘ other (apply `other` first). + /// + /// Rust: `spatial::transform2d::Affine2::compose` + #[pyo3(name = "compose")] + #[pyo3(signature = (other))] + fn compose(&self, other: crate::generated::types::PyAffine2) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.compose(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyAffine2 { inner: __v }) + } + + /// Applies to a point (uses the translation column). + /// + /// Rust: `spatial::transform2d::Affine2::apply` + #[pyo3(name = "apply")] + #[pyo3(signature = (p))] + fn apply(&self, p: crate::generated::types::PyVec2Arg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.apply(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) + } + + /// Applies to a direction (ignores translation). + /// + /// Rust: `spatial::transform2d::Affine2::apply_vector` + #[pyo3(name = "apply_vector")] + #[pyo3(signature = (v))] + fn apply_vector(&self, v: crate::generated::types::PyVec2Arg) -> PyResult { + let v = v.0; + let __r = crate::runtime::guard(|| self.inner.apply_vector(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec2 { inner: __v }) + } + + /// Inverse map; `None` when the linear part is singular. + /// + /// Rust: `spatial::transform2d::Affine2::inverse` + #[pyo3(name = "inverse")] + #[pyo3(signature = ())] + fn inverse(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.inverse()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.map(|__x| crate::generated::types::PyAffine2 { inner: __x })) + } + + /// Decomposition A = R(rot)·[[sx, shear·sy], [0, sy]] plus the + /// translation: returns (t, rot, (sx, sy), shear). Recompose with + /// `translation(t)·rotation(rot)·[[sx, shear·sy],[0, sy]]`. + /// + /// Rust: `spatial::transform2d::Affine2::decompose` + #[pyo3(name = "decompose")] + #[pyo3(signature = ())] + fn decompose(&self) -> PyResult<(crate::generated::types::PyVec2, f64, crate::generated::types::PyVec2, f64)> { + let __r = crate::runtime::guard(|| self.inner.decompose()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((crate::generated::types::PyVec2 { inner: __v.0 }, __v.1, crate::generated::types::PyVec2 { inner: __v.2 }, __v.3)) + } + + /// True for a rigid motion (rotation + translation, no reflection): + /// AᵀA = I and det A = +1 within tol. + /// + /// Rust: `spatial::transform2d::Affine2::is_rigid` + #[pyo3(name = "is_rigid")] + #[pyo3(signature = (tol))] + fn is_rigid(&self, tol: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_rigid(tol)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// True for a similarity (uniform scale + rotation ± reflection): + /// AᵀA = s²·I within tol. + /// + /// Rust: `spatial::transform2d::Affine2::is_similarity` + #[pyo3(name = "is_similarity")] + #[pyo3(signature = (tol))] + fn is_similarity(&self, tol: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_similarity(tol)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "m")] + fn py_get_m(&self) -> PyResult>> { Ok(self.inner.m.clone().into_iter().map(|__x| __x.to_vec()).collect::>()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Affine2", "Affine2", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} diff --git a/bindings/python/src/generated/types/statistical_mechanics.rs b/bindings/python/src/generated/types/statistical_mechanics.rs new file mode 100644 index 0000000..f4843d7 --- /dev/null +++ b/bindings/python/src/generated/types/statistical_mechanics.rs @@ -0,0 +1,1557 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; + + +/// A square-lattice Ising model with nearest-neighbour coupling. +/// +/// `H = -j sum_ s_i s_j - h sum_i s_i` with spins `+/-1`, and `beta` the +/// inverse temperature in units where Boltzmann's constant is one. +/// +/// Rust: `statistical_mechanics::ising::Ising2D` +#[pyclass(name = "Ising2D", module = "numeria.statistical_mechanics.ising", from_py_object)] +#[derive(Clone)] +pub struct PyIsing2D { pub inner: rust_physics_engine::statistical_mechanics::ising::Ising2D } +#[pymethods] +impl PyIsing2D { + /// Builds a `Ising2D` from its fields. + #[new] + #[pyo3(signature = (n, spins, j, h, beta, periodic))] + fn __new__(n: usize, spins: Vec, j: f64, h: f64, beta: f64, periodic: bool) -> Self { + + Self { inner: rust_physics_engine::statistical_mechanics::ising::Ising2D { n: n, spins: spins, j: j, h: h, beta: beta, periodic: periodic } } + } + + /// A lattice with every spin up. + /// + /// Errors: + /// Returns an error for a lattice smaller than two or larger than 512 a + /// side, or a non-positive inverse temperature. + /// + /// Rust: `statistical_mechanics::ising::Ising2D::cold` + #[pyo3(name = "cold")] + #[staticmethod] + #[pyo3(signature = (n, j, h, beta, periodic))] + fn cold(n: usize, j: f64, h: f64, beta: f64, periodic: bool) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::ising::Ising2D::cold(n, j, h, beta, periodic)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyIsing2D { inner: __v }) + } + + /// A lattice with random spins. + /// + /// Errors: + /// Returns an error on the same conditions as `Ising2D::cold`. + /// + /// Rust: `statistical_mechanics::ising::Ising2D::random` + #[pyo3(name = "random")] + #[staticmethod] + #[pyo3(signature = (n, j, h, beta, periodic, rng))] + fn random(n: usize, j: f64, h: f64, beta: f64, periodic: bool, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::ising::Ising2D::random(n, j, h, beta, periodic, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyIsing2D { inner: __v }) + } + + /// The total energy. + /// + /// Rust: `statistical_mechanics::ising::Ising2D::energy` + #[pyo3(name = "energy")] + #[pyo3(signature = ())] + fn energy(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.energy()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The energy per site. + /// + /// Rust: `statistical_mechanics::ising::Ising2D::energy_per_site` + #[pyo3(name = "energy_per_site")] + #[pyo3(signature = ())] + fn energy_per_site(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.energy_per_site()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The magnetisation per site, signed. + /// + /// Rust: `statistical_mechanics::ising::Ising2D::magnetization` + #[pyo3(name = "magnetization")] + #[pyo3(signature = ())] + fn magnetization(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.magnetization()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// One Metropolis sweep: `n^2` attempted single-spin flips. + /// + /// The acceptance rule `min(1, exp(-beta dE))` satisfies detailed balance + /// with the Boltzmann distribution, which is what makes the chain sample + /// it. Note that a rejected move still counts as a step: the current + /// configuration is re-measured, and treating rejections as "nothing + /// happened" biases every average. + /// + /// Rust: `statistical_mechanics::ising::Ising2D::metropolis_sweep` + #[pyo3(name = "metropolis_sweep")] + #[pyo3(signature = (rng))] + fn metropolis_sweep(&mut self, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<()> { + let mut rng = rng; + let __r = crate::runtime::guard(|| self.inner.metropolis_sweep(&mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// One heat-bath sweep: each visited spin is redrawn from its conditional + /// distribution rather than proposed and accepted. + /// + /// Also correct, and it never rejects -- but it is not faster in any + /// useful sense, because a spin redrawn to its current value has moved + /// just as little as a rejected proposal. + /// + /// Rust: `statistical_mechanics::ising::Ising2D::heat_bath_sweep` + #[pyo3(name = "heat_bath_sweep")] + #[pyo3(signature = (rng))] + fn heat_bath_sweep(&mut self, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<()> { + let mut rng = rng; + let __r = crate::runtime::guard(|| self.inner.heat_bath_sweep(&mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// One Wolff cluster update, returning the cluster size. + /// + /// Grows a cluster of aligned spins by adding each neighbouring bond with + /// probability `1 - exp(-2 beta j)`, then flips the whole thing. The + /// acceptance is *one* -- the bond probability is chosen precisely so + /// that the construction's bias cancels the Boltzmann weight -- which is + /// why the method has no rejected moves at all. + /// + /// Only meaningful for a ferromagnetic coupling in zero field; the field + /// breaks the cancellation, and this implementation ignores it. + /// + /// Errors: + /// Returns an error for a non-positive coupling or a non-zero field. + /// + /// Rust: `statistical_mechanics::ising::Ising2D::wolff_cluster_step` + #[pyo3(name = "wolff_cluster_step")] + #[pyo3(signature = (rng))] + fn wolff_cluster_step(&mut self, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| self.inner.wolff_cluster_step(&mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) + } + + /// Runs the chain and returns summary statistics. + /// + /// One *update* is a Metropolis sweep of `n^2` attempted flips, or a + /// single Wolff cluster step. The two are not the same amount of work, + /// and deliberately so: a Wolff update must be a fixed number of cluster + /// steps rather than "however many it takes to flip a lattice's worth of + /// spins". That second rule looks like the natural way to equalise the + /// work and it silently biases every average, because it stops right + /// after a large cluster -- and a large cluster means an ordered + /// configuration, so measurements are taken preferentially at low + /// energies. Measuring at a *fixed* interval of a Markov chain is + /// unbiased; measuring when the chain reaches a state-dependent + /// condition is not. + /// + /// `thermalize` updates are discarded before measurement begins. That + /// discard is not optional either: the chain starts from a configuration + /// that is not a Boltzmann sample, and averaging over the approach to + /// equilibrium biases everything. + /// + /// Errors: + /// Returns an error for a zero measurement interval or no sweeps. + /// + /// Rust: `statistical_mechanics::ising::Ising2D::sample` + #[pyo3(name = "sample")] + #[pyo3(signature = (sweeps, thermalize, measure_every, use_wolff, rng))] + fn sample(&mut self, sweeps: usize, thermalize: usize, measure_every: usize, use_wolff: bool, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| self.inner.sample(sweeps, thermalize, measure_every, use_wolff, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyIsingStats { inner: __v }) + } + + /// The spin-spin correlation at separation `r` along a lattice axis. + /// + /// Errors: + /// Returns an error if the separation exceeds the lattice. + /// + /// Rust: `statistical_mechanics::ising::Ising2D::correlation_function` + #[pyo3(name = "correlation_function")] + #[pyo3(signature = (r))] + fn correlation_function(&self, r: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.correlation_function(r)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) + } + + /// The ensemble-averaged correlation function out to half the lattice, + /// together with the mean squared magnetisation. + /// + /// Averaging over the run is not a refinement. A single configuration's + /// correlation function is a sample of a random variable whose spread at + /// large separation is comparable to its mean, so a length fitted from + /// one snapshot is fitted to noise -- and the noise does not shrink as + /// the lattice grows, because the number of *independent* regions does + /// not either. + /// + /// Errors: + /// Returns an error for a lattice too small to fit on, or no updates. + /// + /// Rust: `statistical_mechanics::ising::Ising2D::sample_correlations` + #[pyo3(name = "sample_correlations")] + #[pyo3(signature = (updates, use_wolff, rng))] + fn sample_correlations(&mut self, updates: usize, use_wolff: bool, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(Vec, f64)> { + let mut rng = rng; + let __r = crate::runtime::guard(|| self.inner.sample_correlations(updates, use_wolff, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) + } + + /// Fits a correlation length to an averaged correlation function. + /// + /// The connected correlation `C(r) - ^2` decays as `exp(-r / xi)`, so + /// the length is minus the reciprocal slope of its logarithm. Only the + /// separations where the connected correlation is well clear of the + /// sampling noise are fitted: a threshold near zero admits points that + /// are pure noise, and the fitted slope is then noise too -- which reads + /// as a *long* correlation length in a hot lattice, exactly backwards. + /// + /// Returns zero when there is nothing resolvable to fit, and the lattice + /// size when the correlation does not decay within it -- which is the + /// honest answer near the critical point, where the true length exceeds + /// anything a finite lattice can report. + /// + /// Errors: + /// Returns an error for fewer than four separations. + /// + /// Rust: `statistical_mechanics::ising::Ising2D::correlation_length_estimate` + #[pyo3(name = "correlation_length_estimate")] + #[staticmethod] + #[pyo3(signature = (correlations, background))] + fn correlation_length_estimate<'py>(py: Python<'py>, correlations: Vec, background: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistical_mechanics::ising::Ising2D::correlation_length_estimate(&correlations, background))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) + } + + /// The integrated autocorrelation time of the magnetisation, together + /// with the mean number of spin flips an update costs. + /// + /// The time says how many updates a measurement is worth: `2 tau` + /// consecutive samples carry the information of one independent one, so + /// error bars computed as though samples were independent are too small + /// by a factor of `sqrt(2 tau)`. + /// + /// The work is reported alongside because the two algorithms' updates are + /// not comparable on their own. A Metropolis update attempts `n^2` flips; + /// a Wolff update flips one cluster, whose size varies with the + /// temperature. Comparing the two requires `tau` times the work, not + /// `tau` alone -- and a comparison in bare updates would flatter whichever + /// algorithm happened to define the larger one. + /// + /// Errors: + /// Returns an error for too few updates to estimate from. + /// + /// Rust: `statistical_mechanics::ising::Ising2D::autocorrelation_time` + #[pyo3(name = "autocorrelation_time")] + #[pyo3(signature = (updates, use_wolff, rng))] + fn autocorrelation_time(&mut self, updates: usize, use_wolff: bool, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(f64, f64)> { + let mut rng = rng; + let __r = crate::runtime::guard(|| self.inner.autocorrelation_time(updates, use_wolff, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0, __v.1)) + } + + #[getter] + #[pyo3(name = "n")] + fn py_get_n(&self) -> PyResult { Ok(self.inner.n) } + + #[setter] + #[pyo3(name = "n")] + fn py_set_n(&mut self, v: usize) { self.inner.n = v; } + + #[getter] + #[pyo3(name = "spins")] + fn py_get_spins(&self) -> PyResult> { Ok(self.inner.spins.clone()) } + + #[getter] + #[pyo3(name = "j")] + fn py_get_j(&self) -> PyResult { Ok(self.inner.j) } + + #[setter] + #[pyo3(name = "j")] + fn py_set_j(&mut self, v: f64) { self.inner.j = v; } + + #[getter] + #[pyo3(name = "h")] + fn py_get_h(&self) -> PyResult { Ok(self.inner.h) } + + #[setter] + #[pyo3(name = "h")] + fn py_set_h(&mut self, v: f64) { self.inner.h = v; } + + #[getter] + #[pyo3(name = "beta")] + fn py_get_beta(&self) -> PyResult { Ok(self.inner.beta) } + + #[setter] + #[pyo3(name = "beta")] + fn py_set_beta(&mut self, v: f64) { self.inner.beta = v; } + + #[getter] + #[pyo3(name = "periodic")] + fn py_get_periodic(&self) -> PyResult { Ok(self.inner.periodic) } + + #[setter] + #[pyo3(name = "periodic")] + fn py_set_periodic(&mut self, v: bool) { self.inner.periodic = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Ising2D", "Ising2D", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Summary statistics from a Monte Carlo run. +/// +/// Rust: `statistical_mechanics::ising::IsingStats` +#[pyclass(name = "IsingStats", module = "numeria.statistical_mechanics.ising", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyIsingStats { pub inner: rust_physics_engine::statistical_mechanics::ising::IsingStats } +#[pymethods] +impl PyIsingStats { + /// Builds a `IsingStats` from its fields. + #[new] + #[pyo3(signature = (e_mean, e_var, m_mean, m_abs, susceptibility, heat_capacity, binder_cumulant, samples))] + fn __new__(e_mean: f64, e_var: f64, m_mean: f64, m_abs: f64, susceptibility: f64, heat_capacity: f64, binder_cumulant: f64, samples: usize) -> Self { + + Self { inner: rust_physics_engine::statistical_mechanics::ising::IsingStats { e_mean: e_mean, e_var: e_var, m_mean: m_mean, m_abs: m_abs, susceptibility: susceptibility, heat_capacity: heat_capacity, binder_cumulant: binder_cumulant, samples: samples } } + } + + #[getter] + #[pyo3(name = "e_mean")] + fn py_get_e_mean(&self) -> PyResult { Ok(self.inner.e_mean) } + + #[setter] + #[pyo3(name = "e_mean")] + fn py_set_e_mean(&mut self, v: f64) { self.inner.e_mean = v; } + + #[getter] + #[pyo3(name = "e_var")] + fn py_get_e_var(&self) -> PyResult { Ok(self.inner.e_var) } + + #[setter] + #[pyo3(name = "e_var")] + fn py_set_e_var(&mut self, v: f64) { self.inner.e_var = v; } + + #[getter] + #[pyo3(name = "m_mean")] + fn py_get_m_mean(&self) -> PyResult { Ok(self.inner.m_mean) } + + #[setter] + #[pyo3(name = "m_mean")] + fn py_set_m_mean(&mut self, v: f64) { self.inner.m_mean = v; } + + #[getter] + #[pyo3(name = "m_abs")] + fn py_get_m_abs(&self) -> PyResult { Ok(self.inner.m_abs) } + + #[setter] + #[pyo3(name = "m_abs")] + fn py_set_m_abs(&mut self, v: f64) { self.inner.m_abs = v; } + + #[getter] + #[pyo3(name = "susceptibility")] + fn py_get_susceptibility(&self) -> PyResult { Ok(self.inner.susceptibility) } + + #[setter] + #[pyo3(name = "susceptibility")] + fn py_set_susceptibility(&mut self, v: f64) { self.inner.susceptibility = v; } + + #[getter] + #[pyo3(name = "heat_capacity")] + fn py_get_heat_capacity(&self) -> PyResult { Ok(self.inner.heat_capacity) } + + #[setter] + #[pyo3(name = "heat_capacity")] + fn py_set_heat_capacity(&mut self, v: f64) { self.inner.heat_capacity = v; } + + #[getter] + #[pyo3(name = "binder_cumulant")] + fn py_get_binder_cumulant(&self) -> PyResult { Ok(self.inner.binder_cumulant) } + + #[setter] + #[pyo3(name = "binder_cumulant")] + fn py_set_binder_cumulant(&mut self, v: f64) { self.inner.binder_cumulant = v; } + + #[getter] + #[pyo3(name = "samples")] + fn py_get_samples(&self) -> PyResult { Ok(self.inner.samples) } + + #[setter] + #[pyo3(name = "samples")] + fn py_set_samples(&mut self, v: usize) { self.inner.samples = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("IsingStats", "IsingStats", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// The `q`-state Potts model on a square lattice. +/// +/// Generalises Ising, which is the two-state case. The transition turns +/// first order above `q = 4` in two dimensions, which is why the model is the +/// standard example that the *order* of a transition is not a detail of the +/// interaction but a consequence of the symmetry. +/// +/// Rust: `statistical_mechanics::ising::Potts2D` +#[pyclass(name = "Potts2D", module = "numeria.statistical_mechanics.ising", from_py_object)] +#[derive(Clone)] +pub struct PyPotts2D { pub inner: rust_physics_engine::statistical_mechanics::ising::Potts2D } +#[pymethods] +impl PyPotts2D { + /// Builds a `Potts2D` from its fields. + #[new] + #[pyo3(signature = (q, n, states, j, beta))] + fn __new__(q: u8, n: usize, states: Vec, j: f64, beta: f64) -> Self { + + Self { inner: rust_physics_engine::statistical_mechanics::ising::Potts2D { q: q, n: n, states: states, j: j, beta: beta } } + } + + /// A random configuration. + /// + /// Errors: + /// Returns an error for fewer than two states, a bad lattice size, or a + /// non-positive beta. + /// + /// Rust: `statistical_mechanics::ising::Potts2D::random` + #[pyo3(name = "random")] + #[staticmethod] + #[pyo3(signature = (q, n, j, beta, rng))] + fn random(q: u8, n: usize, j: f64, beta: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::ising::Potts2D::random(q, n, j, beta, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyPotts2D { inner: __v }) + } + + /// The energy: minus the coupling for each agreeing bond. + /// + /// Rust: `statistical_mechanics::ising::Potts2D::energy` + #[pyo3(name = "energy")] + #[pyo3(signature = ())] + fn energy(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.energy()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// One Metropolis sweep. + /// + /// Rust: `statistical_mechanics::ising::Potts2D::metropolis_sweep` + #[pyo3(name = "metropolis_sweep")] + #[pyo3(signature = (rng))] + fn metropolis_sweep(&mut self, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<()> { + let mut rng = rng; + let __r = crate::runtime::guard(|| self.inner.metropolis_sweep(&mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// The order parameter: how far the most common state's share exceeds + /// what randomness would give. + /// + /// Rust: `statistical_mechanics::ising::Potts2D::order_parameter` + #[pyo3(name = "order_parameter")] + #[pyo3(signature = ())] + fn order_parameter(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.order_parameter()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "q")] + fn py_get_q(&self) -> PyResult { Ok(self.inner.q) } + + #[setter] + #[pyo3(name = "q")] + fn py_set_q(&mut self, v: u8) { self.inner.q = v; } + + #[getter] + #[pyo3(name = "n")] + fn py_get_n(&self) -> PyResult { Ok(self.inner.n) } + + #[setter] + #[pyo3(name = "n")] + fn py_set_n(&mut self, v: usize) { self.inner.n = v; } + + #[getter] + #[pyo3(name = "states")] + fn py_get_states(&self) -> PyResult> { Ok(self.inner.states.clone()) } + + #[getter] + #[pyo3(name = "j")] + fn py_get_j(&self) -> PyResult { Ok(self.inner.j) } + + #[setter] + #[pyo3(name = "j")] + fn py_set_j(&mut self, v: f64) { self.inner.j = v; } + + #[getter] + #[pyo3(name = "beta")] + fn py_get_beta(&self) -> PyResult { Ok(self.inner.beta) } + + #[setter] + #[pyo3(name = "beta")] + fn py_set_beta(&mut self, v: f64) { self.inner.beta = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Potts2D", "Potts2D", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// The two-dimensional XY model: continuous spins on a square lattice. +/// +/// It has no ordered phase at any positive temperature -- a continuous +/// symmetry cannot break in two dimensions -- and yet it has a transition, +/// where vortices unbind. That the transition exists without an order +/// parameter is what makes it interesting. +/// +/// Rust: `statistical_mechanics::ising::XyModel2D` +#[pyclass(name = "XyModel2D", module = "numeria.statistical_mechanics.ising", from_py_object)] +#[derive(Clone)] +pub struct PyXyModel2D { pub inner: rust_physics_engine::statistical_mechanics::ising::XyModel2D } +#[pymethods] +impl PyXyModel2D { + /// Builds a `XyModel2D` from its fields. + #[new] + #[pyo3(signature = (n, theta, j, beta))] + fn __new__(n: usize, theta: Vec, j: f64, beta: f64) -> Self { + + Self { inner: rust_physics_engine::statistical_mechanics::ising::XyModel2D { n: n, theta: theta, j: j, beta: beta } } + } + + /// A random configuration. + /// + /// Errors: + /// Returns an error for a bad lattice size or non-positive beta. + /// + /// Rust: `statistical_mechanics::ising::XyModel2D::random` + #[pyo3(name = "random")] + #[staticmethod] + #[pyo3(signature = (n, j, beta, rng))] + fn random(n: usize, j: f64, beta: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::ising::XyModel2D::random(n, j, beta, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyXyModel2D { inner: __v }) + } + + /// The energy: minus the coupling times the cosine of each bond angle. + /// + /// Rust: `statistical_mechanics::ising::XyModel2D::energy` + #[pyo3(name = "energy")] + #[pyo3(signature = ())] + fn energy(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.energy()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// One Metropolis sweep, proposing a bounded angle change. + /// + /// Rust: `statistical_mechanics::ising::XyModel2D::metropolis_sweep` + #[pyo3(name = "metropolis_sweep")] + #[pyo3(signature = (rng, step))] + fn metropolis_sweep(&mut self, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>, step: f64) -> PyResult<()> { + let mut rng = rng; + let __r = crate::runtime::guard(|| self.inner.metropolis_sweep(&mut rng.inner, step)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// The vorticity of the plaquette whose lower-left corner is `(row, + /// column)`, as an integer winding number. + /// + /// Summing the angle differences around a plaquette, each reduced to + /// `(-pi, pi]`, gives a multiple of `2 pi`. That the multiple is an + /// integer is not approximate -- it is a topological fact about the + /// configuration, and it is why vortices cannot be removed by a small + /// change. + /// + /// Rust: `statistical_mechanics::ising::XyModel2D::plaquette_vorticity` + #[pyo3(name = "plaquette_vorticity")] + #[pyo3(signature = (row, column))] + fn plaquette_vorticity(&self, row: usize, column: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.plaquette_vorticity(row, column)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The number of vortices and antivortices on the lattice. + /// + /// Rust: `statistical_mechanics::ising::XyModel2D::vortex_count` + #[pyo3(name = "vortex_count")] + #[pyo3(signature = ())] + fn vortex_count(&self) -> PyResult<(usize, usize)> { + let __r = crate::runtime::guard(|| self.inner.vortex_count()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) + } + + /// The Kosterlitz-Thouless transition temperature, about `0.893 j`. + /// + /// Not exactly known: unlike Ising, the XY model has no closed-form + /// solution, and this is the best numerical estimate. + /// + /// Rust: `statistical_mechanics::ising::XyModel2D::kt_transition_estimate` + #[pyo3(name = "kt_transition_estimate")] + #[staticmethod] + #[pyo3(signature = (j))] + fn kt_transition_estimate(j: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::ising::XyModel2D::kt_transition_estimate(j)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "n")] + fn py_get_n(&self) -> PyResult { Ok(self.inner.n) } + + #[setter] + #[pyo3(name = "n")] + fn py_set_n(&mut self, v: usize) { self.inner.n = v; } + + #[getter] + #[pyo3(name = "theta")] + fn py_get_theta(&self) -> PyResult> { Ok(self.inner.theta.clone()) } + + #[getter] + #[pyo3(name = "j")] + fn py_get_j(&self) -> PyResult { Ok(self.inner.j) } + + #[setter] + #[pyo3(name = "j")] + fn py_set_j(&mut self, v: f64) { self.inner.j = v; } + + #[getter] + #[pyo3(name = "beta")] + fn py_get_beta(&self) -> PyResult { Ok(self.inner.beta) } + + #[setter] + #[pyo3(name = "beta")] + fn py_set_beta(&mut self, v: f64) { self.inner.beta = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("XyModel2D", "XyModel2D", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Which way an inhibitor acts. +/// +/// Rust: `statistical_mechanics::kinetics::Inhibition` +#[pyclass(name = "Inhibition", module = "numeria.statistical_mechanics.kinetics", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyInhibition { + Competitive, + Uncompetitive, + NonCompetitive, +} +impl PyInhibition { + pub fn to_rust(&self) -> rust_physics_engine::statistical_mechanics::kinetics::Inhibition { match self { + Self::Competitive => rust_physics_engine::statistical_mechanics::kinetics::Inhibition::Competitive, + Self::Uncompetitive => rust_physics_engine::statistical_mechanics::kinetics::Inhibition::Uncompetitive, + Self::NonCompetitive => rust_physics_engine::statistical_mechanics::kinetics::Inhibition::NonCompetitive, + } } + pub fn from_rust(v: &rust_physics_engine::statistical_mechanics::kinetics::Inhibition) -> Self { match v { + rust_physics_engine::statistical_mechanics::kinetics::Inhibition::Competitive => Self::Competitive, + rust_physics_engine::statistical_mechanics::kinetics::Inhibition::Uncompetitive => Self::Uncompetitive, + rust_physics_engine::statistical_mechanics::kinetics::Inhibition::NonCompetitive => Self::NonCompetitive, + } } +} +#[pymethods] +impl PyInhibition { + fn __repr__(&self) -> &'static str { + match self { + Self::Competitive => "Inhibition.Competitive", + Self::Uncompetitive => "Inhibition.Uncompetitive", + Self::NonCompetitive => "Inhibition.NonCompetitive", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// One elementary reaction, as species indices with their stoichiometric +/// coefficients. +/// +/// Rust: `statistical_mechanics::kinetics::Reaction` +#[pyclass(name = "Reaction", module = "numeria.statistical_mechanics.kinetics", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyReaction { pub inner: rust_physics_engine::statistical_mechanics::kinetics::Reaction } +#[pymethods] +impl PyReaction { + /// A reaction from reactant and product lists. + /// + /// Rust: `statistical_mechanics::kinetics::Reaction::new` + #[new] + #[pyo3(signature = (reactants, products))] + fn __new__(reactants: Vec<(usize, u32)>, products: Vec<(usize, u32)>) -> PyResult { + let reactants = reactants.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let products = products.into_iter().map(|__e| (__e.0, __e.1)).collect::>(); + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::kinetics::Reaction::new(&reactants, &products)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyReaction { inner: __v }) + } + + /// The molecularity: how many molecules meet. + /// + /// Rust: `statistical_mechanics::kinetics::Reaction::order` + #[pyo3(name = "order")] + #[pyo3(signature = ())] + fn order(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.order()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The net change in each species, indexed by species. + /// + /// Rust: `statistical_mechanics::kinetics::Reaction::net_change` + #[pyo3(name = "net_change")] + #[pyo3(signature = (species))] + fn net_change<'py>(&self, py: Python<'py>, species: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.net_change(species))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "reactants")] + fn py_get_reactants(&self) -> PyResult> { Ok(self.inner.reactants.clone().into_iter().map(|__x| (__x.0, __x.1)).collect::>()) } + + #[getter] + #[pyo3(name = "products")] + fn py_get_products(&self) -> PyResult> { Ok(self.inner.products.clone().into_iter().map(|__x| (__x.0, __x.1)).collect::>()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Reaction", "Reaction", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// One record from a trajectory. +/// +/// Rust: `statistical_mechanics::md::MdSample` +#[pyclass(name = "MdSample", module = "numeria.statistical_mechanics.md", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyMdSample { pub inner: rust_physics_engine::statistical_mechanics::md::MdSample } +#[pymethods] +impl PyMdSample { + /// Builds a `MdSample` from its fields. + #[new] + #[pyo3(signature = (time, kinetic, potential, total, temperature, pressure))] + fn __new__(time: f64, kinetic: f64, potential: f64, total: f64, temperature: f64, pressure: f64) -> Self { + + Self { inner: rust_physics_engine::statistical_mechanics::md::MdSample { time: time, kinetic: kinetic, potential: potential, total: total, temperature: temperature, pressure: pressure } } + } + + #[getter] + #[pyo3(name = "time")] + fn py_get_time(&self) -> PyResult { Ok(self.inner.time) } + + #[setter] + #[pyo3(name = "time")] + fn py_set_time(&mut self, v: f64) { self.inner.time = v; } + + #[getter] + #[pyo3(name = "kinetic")] + fn py_get_kinetic(&self) -> PyResult { Ok(self.inner.kinetic) } + + #[setter] + #[pyo3(name = "kinetic")] + fn py_set_kinetic(&mut self, v: f64) { self.inner.kinetic = v; } + + #[getter] + #[pyo3(name = "potential")] + fn py_get_potential(&self) -> PyResult { Ok(self.inner.potential) } + + #[setter] + #[pyo3(name = "potential")] + fn py_set_potential(&mut self, v: f64) { self.inner.potential = v; } + + #[getter] + #[pyo3(name = "total")] + fn py_get_total(&self) -> PyResult { Ok(self.inner.total) } + + #[setter] + #[pyo3(name = "total")] + fn py_set_total(&mut self, v: f64) { self.inner.total = v; } + + #[getter] + #[pyo3(name = "temperature")] + fn py_get_temperature(&self) -> PyResult { Ok(self.inner.temperature) } + + #[setter] + #[pyo3(name = "temperature")] + fn py_set_temperature(&mut self, v: f64) { self.inner.temperature = v; } + + #[getter] + #[pyo3(name = "pressure")] + fn py_get_pressure(&self) -> PyResult { Ok(self.inner.pressure) } + + #[setter] + #[pyo3(name = "pressure")] + fn py_set_pressure(&mut self, v: f64) { self.inner.pressure = v; } + + fn __repr__(&self) -> String { format!("MdSample(time={:?}, kinetic={:?}, potential={:?}, total={:?}, temperature={:?}, pressure={:?})", self.inner.time, self.inner.kinetic, self.inner.potential, self.inner.total, self.inner.temperature, self.inner.pressure) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A `MdSample` argument, or anything that can stand in for one. +pub struct PyMdSampleArg(pub rust_physics_engine::statistical_mechanics::md::MdSample); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyMdSampleArg { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result { + if let Ok(__w) = obj.extract::() { + return Ok(PyMdSampleArg(__w.inner)); + } + let __v = crate::runtime::coerce::floats_exact(obj, 6, "MdSample")?; + Ok(PyMdSampleArg(rust_physics_engine::statistical_mechanics::md::MdSample { time: __v[0], kinetic: __v[1], potential: __v[2], total: __v[3], temperature: __v[4], pressure: __v[5] })) + } +} + + +/// A box of particles interacting through one pair potential. +/// +/// Rust: `statistical_mechanics::md::MdSystem` +#[pyclass(name = "MdSystem", module = "numeria.statistical_mechanics.md", from_py_object)] +#[derive(Clone)] +pub struct PyMdSystem { pub inner: rust_physics_engine::statistical_mechanics::md::MdSystem } +#[pymethods] +impl PyMdSystem { + /// A system from explicit state. + /// + /// Errors: + /// Returns an error for mismatched lengths, a non-positive mass, box or + /// cutoff, or a cutoff more than half the shortest box edge, which the + /// minimum-image convention cannot represent. + /// + /// Rust: `statistical_mechanics::md::MdSystem::new` + #[new] + #[pyo3(signature = (pos, vel, mass, box_size, periodic, potential, cutoff))] + fn __new__(pos: Vec, vel: Vec, mass: Vec, box_size: crate::generated::types::PyVec3Arg, periodic: bool, potential: crate::generated::types::PyPotential, cutoff: f64) -> PyResult { + let pos = pos.into_iter().map(|__e| __e.0).collect::>(); + let vel = vel.into_iter().map(|__e| __e.0).collect::>(); + let box_size = box_size.0; + let potential = potential.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::md::MdSystem::new(pos, vel, mass, box_size, periodic, potential, cutoff)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyMdSystem { inner: __v }) + } + + /// A face-centred cubic lattice of `cells^3` unit cells at a given + /// number density, with velocities drawn at the requested temperature. + /// + /// FCC rather than simple cubic because it is the Lennard-Jones ground + /// state: starting from a simple cubic lattice at liquid density puts + /// the system on a mechanically unstable configuration, and it melts + /// into a shock rather than into equilibrium. + /// + /// Errors: + /// Returns an error for no cells, a non-positive density or a negative + /// temperature. + /// + /// Rust: `statistical_mechanics::md::MdSystem::lattice_fcc` + #[pyo3(name = "lattice_fcc")] + #[staticmethod] + #[pyo3(signature = (cells, density, temperature, eps, sigma, rng))] + fn lattice_fcc(cells: usize, density: f64, temperature: f64, eps: f64, sigma: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::statistical_mechanics::md::MdSystem::lattice_fcc(cells, density, temperature, eps, sigma, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyMdSystem { inner: __v }) + } + + /// The number of particles. + /// + /// Rust: `statistical_mechanics::md::MdSystem::len` + #[pyo3(name = "len")] + #[pyo3(signature = ())] + fn len(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.len()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Whether the box is empty. Never true for a constructed system. + /// + /// Rust: `statistical_mechanics::md::MdSystem::is_empty` + #[pyo3(name = "is_empty")] + #[pyo3(signature = ())] + fn is_empty(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_empty()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The box volume. + /// + /// Rust: `statistical_mechanics::md::MdSystem::volume` + #[pyo3(name = "volume")] + #[pyo3(signature = ())] + fn volume(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.volume()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Wraps a position into the primary box. + /// + /// Rust: `statistical_mechanics::md::MdSystem::wrap` + #[pyo3(name = "wrap")] + #[pyo3(signature = (p))] + fn wrap(&self, p: crate::generated::types::PyVec3Arg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| self.inner.wrap(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// The shortest displacement between two points under the periodic + /// boundary: the *minimum image*. + /// + /// Rust: `statistical_mechanics::md::MdSystem::minimum_image` + #[pyo3(name = "minimum_image")] + #[pyo3(signature = (d))] + fn minimum_image(&self, d: crate::generated::types::PyVec3Arg) -> PyResult { + let d = d.0; + let __r = crate::runtime::guard(|| self.inner.minimum_image(d)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// The velocity-of-the-centre-of-mass subtracted from every particle. + /// + /// The total momentum is a constant of the motion, so a non-zero value + /// never decays: it sits in the kinetic energy for the whole run and + /// inflates every temperature reading by a fixed amount. + /// + /// Rust: `statistical_mechanics::md::MdSystem::remove_drift` + #[pyo3(name = "remove_drift")] + #[pyo3(signature = ())] + fn remove_drift(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.remove_drift()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Scales every velocity so the instantaneous temperature is `target`. + /// + /// Rust: `statistical_mechanics::md::MdSystem::rescale_to_temperature` + #[pyo3(name = "rescale_to_temperature")] + #[pyo3(signature = (target))] + fn rescale_to_temperature(&mut self, target: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.rescale_to_temperature(target)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// The force on every particle, computed afresh. + /// + /// Rust: `statistical_mechanics::md::MdSystem::forces` + #[pyo3(name = "forces")] + #[pyo3(signature = ())] + fn forces(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.forces()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) + } + + /// Recomputes the cached forces the integrator steps with. + /// + /// `MdSystem::step_velocity_verlet` reuses the force it computed at the + /// end of the previous step, which is what makes velocity Verlet one + /// force evaluation per step rather than two. Every method here that + /// changes a position keeps that cache current, but `pos`, `box_size`, + /// `charge`, `potential` and `cutoff` are public: **after writing to any + /// of them directly, call this before stepping.** Otherwise the next + /// step integrates the previous configuration's forces, and the symptom + /// is subtle rather than loud -- energy that almost conserves, and a + /// trajectory that is no longer reversible. + /// + /// Rust: `statistical_mechanics::md::MdSystem::refresh_forces` + #[pyo3(name = "refresh_forces")] + #[pyo3(signature = ())] + fn refresh_forces(&mut self) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.refresh_forces()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// The total potential energy. + /// + /// Rust: `statistical_mechanics::md::MdSystem::potential_energy` + #[pyo3(name = "potential_energy")] + #[pyo3(signature = ())] + fn potential_energy(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.potential_energy()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The total kinetic energy. + /// + /// Rust: `statistical_mechanics::md::MdSystem::kinetic_energy` + #[pyo3(name = "kinetic_energy")] + #[pyo3(signature = ())] + fn kinetic_energy(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.kinetic_energy()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The momentum of the whole box. + /// + /// Rust: `statistical_mechanics::md::MdSystem::total_momentum` + #[pyo3(name = "total_momentum")] + #[pyo3(signature = ())] + fn total_momentum(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.total_momentum()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyVec3 { inner: __v }) + } + + /// The count of translational degrees of freedom. + /// + /// Three fewer than `3 N` on a periodic box, because the total momentum + /// is conserved and carries no thermal energy. Dividing by `3 N` + /// instead reports a temperature low by a factor `1 - 1/N`, which is + /// invisible at ten thousand particles and a two per cent error at a + /// hundred. + /// + /// Rust: `statistical_mechanics::md::MdSystem::degrees_of_freedom` + #[pyo3(name = "degrees_of_freedom")] + #[pyo3(signature = ())] + fn degrees_of_freedom(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.degrees_of_freedom()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The instantaneous temperature from equipartition. + /// + /// Rust: `statistical_mechanics::md::MdSystem::temperature` + #[pyo3(name = "temperature")] + #[pyo3(signature = ())] + fn temperature(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.temperature()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The pressure from the virial theorem. + /// + /// Rust: `statistical_mechanics::md::MdSystem::pressure_virial` + #[pyo3(name = "pressure_virial")] + #[pyo3(signature = ())] + fn pressure_virial(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.pressure_virial()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// A snapshot of the thermodynamic state. + /// + /// Rust: `statistical_mechanics::md::MdSystem::sample` + #[pyo3(name = "sample")] + #[pyo3(signature = ())] + fn sample(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.sample()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMdSample { inner: __v }) + } + + /// One velocity-Verlet step. + /// + /// Symplectic, so the energy error stays bounded rather than + /// accumulating: the integrator conserves a shadow Hamiltonian close to + /// the true one, and the true energy oscillates around its initial value + /// forever instead of drifting away from it. That is the whole reason to + /// prefer it over a higher-order but non-symplectic scheme here, and + /// `energy_drift` is written to measure the distinction. + /// + /// Rust: `statistical_mechanics::md::MdSystem::step_velocity_verlet` + #[pyo3(name = "step_velocity_verlet")] + #[pyo3(signature = (dt))] + fn step_velocity_verlet(&mut self, dt: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.step_velocity_verlet(dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Berendsen velocity rescaling toward `t_target`. + /// + /// It reaches the right mean temperature and samples the wrong + /// ensemble: the fluctuations are suppressed, so a heat capacity taken + /// from a Berendsen run is too small. Use it to equilibrate and switch + /// to Nose-Hoover or Langevin before measuring anything that depends on + /// a fluctuation. + /// + /// Rust: `statistical_mechanics::md::MdSystem::thermostat_berendsen` + #[pyo3(name = "thermostat_berendsen")] + #[pyo3(signature = (t_target, tau, dt))] + fn thermostat_berendsen(&mut self, t_target: f64, tau: f64, dt: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.thermostat_berendsen(t_target, tau, dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// One Nose-Hoover step on the friction coordinate and the velocities. + /// + /// Unlike Berendsen this is derived from an extended Hamiltonian, so it + /// samples the canonical ensemble including the fluctuations -- the + /// friction is a dynamical variable with its own inertia `q`, and it + /// oscillates rather than clamping. + /// + /// Rust: `statistical_mechanics::md::MdSystem::thermostat_nose_hoover` + #[pyo3(name = "thermostat_nose_hoover")] + #[pyo3(signature = (t_target, q, dt))] + fn thermostat_nose_hoover(&mut self, t_target: f64, q: f64, dt: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.thermostat_nose_hoover(t_target, q, dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// One Langevin step: friction plus the matching noise. + /// + /// The two are not independent. The fluctuation-dissipation theorem + /// fixes the noise amplitude from the friction and the target + /// temperature, and any other amplitude thermostats to a different + /// temperature than the one requested. + /// + /// Rust: `statistical_mechanics::md::MdSystem::thermostat_langevin` + #[pyo3(name = "thermostat_langevin")] + #[pyo3(signature = (t_target, gamma, dt, rng))] + fn thermostat_langevin(&mut self, t_target: f64, gamma: f64, dt: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<()> { + let mut rng = rng; + let __r = crate::runtime::guard(|| self.inner.thermostat_langevin(t_target, gamma, dt, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Berendsen barostat: the box and every position scaled toward + /// `p_target`. + /// + /// Errors: + /// Returns an error for a non-positive time constant or compressibility, + /// or if the rescaling would shrink the box below twice the cutoff. + /// + /// Rust: `statistical_mechanics::md::MdSystem::barostat_berendsen` + #[pyo3(name = "barostat_berendsen")] + #[pyo3(signature = (p_target, compressibility, tau, dt))] + fn barostat_berendsen(&mut self, p_target: f64, compressibility: f64, tau: f64, dt: f64) -> PyResult<()> { + let __r = crate::runtime::guard(|| self.inner.barostat_berendsen(p_target, compressibility, tau, dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(()) + } + + /// Thermalises the system with a Langevin thermostat and returns the + /// drift-free result. + /// + /// Errors: + /// Returns an error for a non-positive step or a negative temperature. + /// + /// Rust: `statistical_mechanics::md::MdSystem::equilibrate` + #[pyo3(name = "equilibrate")] + #[pyo3(signature = (steps, dt, t_target, rng))] + fn equilibrate(&mut self, steps: usize, dt: f64, t_target: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<()> { + let mut rng = rng; + let __r = crate::runtime::guard(|| self.inner.equilibrate(steps, dt, t_target, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(()) + } + + /// A constant-energy run, sampled every step. + /// + /// Errors: + /// Returns an error for a non-positive step or no steps. + /// + /// Rust: `statistical_mechanics::md::MdSystem::run_nve` + #[pyo3(name = "run_nve")] + #[pyo3(signature = (steps, dt))] + fn run_nve(&mut self, steps: usize, dt: f64) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.run_nve(steps, dt)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyMdSample { inner: __x }).collect::>()) + } + + /// A constant-energy run recording positions and velocities every + /// `stride` steps, for the transport measurements. + /// + /// Errors: + /// Returns an error for a non-positive step, no steps, or a zero stride. + /// + /// Rust: `statistical_mechanics::md::MdSystem::run_trajectory` + #[pyo3(name = "run_trajectory")] + #[pyo3(signature = (steps, dt, stride))] + fn run_trajectory(&mut self, steps: usize, dt: f64, stride: usize) -> PyResult<(Vec>, Vec>)> { + let __r = crate::runtime::guard(|| self.inner.run_trajectory(steps, dt, stride)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok((__v.0.into_iter().map(|__x| __x.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()).collect::>(), __v.1.into_iter().map(|__x| __x.into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()).collect::>())) + } + + /// The radial distribution function `g(r)` in `bins` shells out to + /// `r_max`. + /// + /// Normalised by the *ideal gas* count in each shell, so `g(r) = 1` + /// means "no correlation at this separation" rather than "no + /// neighbours". A histogram normalised by the shell volume alone rises + /// as `r^2` and says nothing. + /// + /// Errors: + /// Returns an error for no bins, a non-positive range, or a range + /// exceeding half the shortest box edge on a periodic box. + /// + /// Rust: `statistical_mechanics::md::MdSystem::rdf` + #[pyo3(name = "rdf")] + #[pyo3(signature = (bins, r_max))] + fn rdf<'py>(&self, py: Python<'py>, bins: usize, r_max: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.rdf(bins, r_max))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) + } + + /// The static structure factor at each scalar wavenumber, by the Debye + /// formula `S(k) = 1 + (2/N) sum_{i(&self, py: Python<'py>, k_values: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.structure_factor(&k_values))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) + } + + /// A Kolmogorov-Smirnov test of the speeds against the Maxwell-Boltzmann + /// distribution at the system's own temperature. + /// + /// The check is worth making because equipartition alone does not pin + /// the distribution: a system with every particle at the same speed has + /// exactly the right temperature and entirely the wrong statistics, and + /// that is precisely the state a freshly rescaled lattice is in. + /// + /// Errors: + /// Returns an error for a zero temperature or fewer than five + /// particles, and for masses that are not all equal -- the speeds then + /// come from a mixture of distributions and a single-sample test does + /// not apply. + /// + /// Rust: `statistical_mechanics::md::MdSystem::maxwell_boltzmann_check` + #[pyo3(name = "maxwell_boltzmann_check")] + #[pyo3(signature = ())] + fn maxwell_boltzmann_check(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.maxwell_boltzmann_check()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyTestResult { inner: __v }) + } + + /// The Lindemann ratio: the root-mean-square displacement of each + /// particle from its own mean position, divided by the nearest-neighbour + /// distance. + /// + /// Above about 0.15 a crystal has melted. The ratio is taken about each + /// particle's *own* time-averaged site rather than about a lattice, so + /// it does not need to know the crystal structure -- but for the same + /// reason it only means something for a trajectory long enough for that + /// average to settle. + /// + /// Errors: + /// Returns an error for fewer than two frames or a frame of the wrong + /// length. + /// + /// Rust: `statistical_mechanics::md::MdSystem::melting_indicator_lindemann` + #[pyo3(name = "melting_indicator_lindemann")] + #[pyo3(signature = (traj))] + fn melting_indicator_lindemann<'py>(&self, py: Python<'py>, traj: Vec>) -> PyResult { + let traj = traj.into_iter().map(|__e| __e.into_iter().map(|__e| __e.0).collect::>()).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || self.inner.melting_indicator_lindemann(&traj))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) + } + + /// The mean squared displacement against lag, averaged over particles + /// and over every time origin. + /// + /// The trajectory must be *unwrapped*: a position folded back into the + /// box turns a steady drift into a sawtooth, and the resulting MSD + /// saturates at the box size and reports no diffusion at all. Use the + /// positions from `MdSystem::run_trajectory`, which are unwrapped for + /// this reason. + /// + /// Errors: + /// Returns an error for fewer than two frames or ragged frames. + /// + /// Rust: `statistical_mechanics::md::MdSystem::msd` + #[pyo3(name = "msd")] + #[staticmethod] + #[pyo3(signature = (traj))] + fn msd<'py>(py: Python<'py>, traj: Vec>) -> PyResult> { + let traj = traj.into_iter().map(|__e| __e.into_iter().map(|__e| __e.0).collect::>()).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistical_mechanics::md::MdSystem::msd(&traj))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) + } + + /// The diffusion coefficient from the Einstein relation ` = 6 D t`. + /// + /// Fitted over the middle half of the record. The two ends are excluded + /// deliberately: the short-lag part is ballistic rather than diffusive, + /// and the long-lag part is averaged over so few time origins that it is + /// mostly noise. Fitting the whole curve mixes both in. + /// + /// Errors: + /// Returns an error for fewer than eight lags or a non-positive step. + /// + /// Rust: `statistical_mechanics::md::MdSystem::diffusion_coefficient` + #[pyo3(name = "diffusion_coefficient")] + #[staticmethod] + #[pyo3(signature = (msd, dt))] + fn diffusion_coefficient<'py>(py: Python<'py>, msd: Vec, dt: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistical_mechanics::md::MdSystem::diffusion_coefficient(&msd, dt))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) + } + + /// The normalised velocity autocorrelation function. + /// + /// Errors: + /// Returns an error for fewer than two frames, ragged frames, or a + /// trajectory with no motion in it. + /// + /// Rust: `statistical_mechanics::md::MdSystem::vacf` + #[pyo3(name = "vacf")] + #[staticmethod] + #[pyo3(signature = (traj_vel))] + fn vacf<'py>(py: Python<'py>, traj_vel: Vec>) -> PyResult> { + let traj_vel = traj_vel.into_iter().map(|__e| __e.into_iter().map(|__e| __e.0).collect::>()).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistical_mechanics::md::MdSystem::vacf(&traj_vel))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) + } + + /// The vibrational density of states: the cosine transform of the + /// velocity autocorrelation. + /// + /// Returned on the frequency grid `omega_k = pi k / (N dt)`, so the + /// caller can label the axis without guessing. + /// + /// Errors: + /// Returns an error for fewer than two points or a non-positive step. + /// + /// Rust: `statistical_mechanics::md::MdSystem::vdos_from_vacf` + #[pyo3(name = "vdos_from_vacf")] + #[staticmethod] + #[pyo3(signature = (vacf, dt))] + fn vdos_from_vacf<'py>(py: Python<'py>, vacf: Vec, dt: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::statistical_mechanics::md::MdSystem::vdos_from_vacf(&vacf, dt))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "pos")] + fn py_get_pos(&self) -> PyResult> { Ok(self.inner.pos.clone().into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) } + + #[getter] + #[pyo3(name = "unwrapped")] + fn py_get_unwrapped(&self) -> PyResult> { Ok(self.inner.unwrapped.clone().into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) } + + #[getter] + #[pyo3(name = "vel")] + fn py_get_vel(&self) -> PyResult> { Ok(self.inner.vel.clone().into_iter().map(|__x| crate::generated::types::PyVec3 { inner: __x }).collect::>()) } + + #[getter] + #[pyo3(name = "mass")] + fn py_get_mass(&self) -> PyResult> { Ok(self.inner.mass.clone()) } + + #[getter] + #[pyo3(name = "charge")] + fn py_get_charge(&self) -> PyResult> { Ok(self.inner.charge.clone()) } + + #[getter] + #[pyo3(name = "box_size")] + fn py_get_box_size(&self) -> PyResult { Ok(crate::generated::types::PyVec3 { inner: self.inner.box_size.clone() }) } + + #[getter] + #[pyo3(name = "periodic")] + fn py_get_periodic(&self) -> PyResult { Ok(self.inner.periodic) } + + #[setter] + #[pyo3(name = "periodic")] + fn py_set_periodic(&mut self, v: bool) { self.inner.periodic = v; } + + #[getter] + #[pyo3(name = "potential")] + fn py_get_potential(&self) -> PyResult { Ok(crate::generated::types::PyPotential { inner: self.inner.potential.clone() }) } + + #[getter] + #[pyo3(name = "cutoff")] + fn py_get_cutoff(&self) -> PyResult { Ok(self.inner.cutoff) } + + #[setter] + #[pyo3(name = "cutoff")] + fn py_set_cutoff(&mut self, v: f64) { self.inner.cutoff = v; } + + #[getter] + #[pyo3(name = "time")] + fn py_get_time(&self) -> PyResult { Ok(self.inner.time) } + + #[setter] + #[pyo3(name = "time")] + fn py_set_time(&mut self, v: f64) { self.inner.time = v; } + + #[getter] + #[pyo3(name = "nose_hoover_zeta")] + fn py_get_nose_hoover_zeta(&self) -> PyResult { Ok(self.inner.nose_hoover_zeta) } + + #[setter] + #[pyo3(name = "nose_hoover_zeta")] + fn py_set_nose_hoover_zeta(&mut self, v: f64) { self.inner.nose_hoover_zeta = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("MdSystem", "MdSystem", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A pair potential, as a function of the separation alone. +/// +/// Each variant supplies both the energy and the force so that they cannot +/// drift apart: a force that is not the negative gradient of the energy in +/// use will conserve nothing, and the failure looks exactly like an +/// integrator bug. +/// +/// Rust: `statistical_mechanics::md::Potential` +#[pyclass(name = "Potential", module = "numeria.statistical_mechanics.md", from_py_object)] +#[derive(Clone)] +pub struct PyPotential { pub inner: rust_physics_engine::statistical_mechanics::md::Potential } +#[pymethods] +impl PyPotential { + /// The energy and the radial force `-du/dr` at separation `r` between + /// charges `qi` and `qj`. + /// + /// The force is returned rather than derived numerically so that the two + /// are guaranteed consistent; the tests check each variant's force + /// against a finite difference of its own energy. + /// + /// Rust: `statistical_mechanics::md::Potential::evaluate` + #[pyo3(name = "evaluate")] + #[pyo3(signature = (r, qi, qj))] + fn evaluate(&self, r: f64, qi: f64, qj: f64) -> PyResult<(f64, f64)> { + let __r = crate::runtime::guard(|| self.inner.evaluate(r, qi, qj)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) + } + + /// Whether the potential carries a charge term, and so cannot be + /// truncated at a cutoff without an Ewald correction. + /// + /// Rust: `statistical_mechanics::md::Potential::is_charged` + #[pyo3(name = "is_charged")] + #[pyo3(signature = ())] + fn is_charged(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_charged()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + fn __repr__(&self) -> String { "".to_string() } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} diff --git a/bindings/python/src/generated/types/statistics.rs b/bindings/python/src/generated/types/statistics.rs new file mode 100644 index 0000000..97f0d3c --- /dev/null +++ b/bindings/python/src/generated/types/statistics.rs @@ -0,0 +1,792 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; + + +/// Beta distribution on [0, 1] with shape parameters (a, b). +/// +/// Rust: `statistics::distributions::Beta` +#[pyclass(name = "Beta", module = "numeria.statistics.distributions", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyBeta { pub inner: rust_physics_engine::statistics::distributions::Beta } +#[pymethods] +impl PyBeta { + /// Panics: + /// Panics unless a > 0 and b > 0. + /// + /// Rust: `statistics::distributions::Beta::new` + #[new] + #[pyo3(signature = (a, b))] + fn __new__(a: f64, b: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistics::distributions::Beta::new(a, b)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBeta { inner: __v }) + } + + #[getter] + #[pyo3(name = "a")] + fn py_get_a(&self) -> PyResult { Ok(self.inner.a) } + + #[setter] + #[pyo3(name = "a")] + fn py_set_a(&mut self, v: f64) { self.inner.a = v; } + + #[getter] + #[pyo3(name = "b")] + fn py_get_b(&self) -> PyResult { Ok(self.inner.b) } + + #[setter] + #[pyo3(name = "b")] + fn py_set_b(&mut self, v: f64) { self.inner.b = v; } + + fn __repr__(&self) -> String { format!("Beta(a={:?}, b={:?})", self.inner.a, self.inner.b) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A `Beta` argument, or anything that can stand in for one. +pub struct PyBetaArg(pub rust_physics_engine::statistics::distributions::Beta); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyBetaArg { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result { + if let Ok(__w) = obj.extract::() { + return Ok(PyBetaArg(__w.inner)); + } + let __v = crate::runtime::coerce::floats_exact(obj, 2, "Beta")?; + Ok(PyBetaArg(rust_physics_engine::statistics::distributions::Beta { a: __v[0], b: __v[1] })) + } +} + + +/// Binomial distribution (discrete) with n trials and success +/// probability p; CDF via the regularized incomplete beta. +/// +/// Rust: `statistics::distributions::Binomial` +#[pyclass(name = "Binomial", module = "numeria.statistics.distributions", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyBinomial { pub inner: rust_physics_engine::statistics::distributions::Binomial } +#[pymethods] +impl PyBinomial { + /// Panics: + /// Panics unless 0 ≤ p ≤ 1 and n ≥ 1. + /// + /// Rust: `statistics::distributions::Binomial::new` + #[new] + #[pyo3(signature = (n, p))] + fn __new__(n: u64, p: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistics::distributions::Binomial::new(n, p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyBinomial { inner: __v }) + } + + /// Probability mass P(X = k), computed in log space. + /// + /// Rust: `statistics::distributions::Binomial::pmf` + #[pyo3(name = "pmf")] + #[pyo3(signature = (k))] + fn pmf(&self, k: u64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.pmf(k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "n")] + fn py_get_n(&self) -> PyResult { Ok(self.inner.n) } + + #[setter] + #[pyo3(name = "n")] + fn py_set_n(&mut self, v: u64) { self.inner.n = v; } + + #[getter] + #[pyo3(name = "p")] + fn py_get_p(&self) -> PyResult { Ok(self.inner.p) } + + #[setter] + #[pyo3(name = "p")] + fn py_set_p(&mut self, v: f64) { self.inner.p = v; } + + fn __repr__(&self) -> String { format!("Binomial(n={:?}, p={:?})", self.inner.n, self.inner.p) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Chi-squared distribution with k degrees of freedom; CDF via P(k/2, x/2). +/// +/// Rust: `statistics::distributions::ChiSquared` +#[pyclass(name = "ChiSquared", module = "numeria.statistics.distributions", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyChiSquared { pub inner: rust_physics_engine::statistics::distributions::ChiSquared } +#[pymethods] +impl PyChiSquared { + /// Panics: + /// Panics unless k > 0. + /// + /// Rust: `statistics::distributions::ChiSquared::new` + #[new] + #[pyo3(signature = (k))] + fn __new__(k: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistics::distributions::ChiSquared::new(k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyChiSquared { inner: __v }) + } + + #[getter] + #[pyo3(name = "k")] + fn py_get_k(&self) -> PyResult { Ok(self.inner.k) } + + #[setter] + #[pyo3(name = "k")] + fn py_set_k(&mut self, v: f64) { self.inner.k = v; } + + fn __repr__(&self) -> String { format!("ChiSquared(k={:?})", self.inner.k) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A `ChiSquared` argument, or anything that can stand in for one. +pub struct PyChiSquaredArg(pub rust_physics_engine::statistics::distributions::ChiSquared); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyChiSquaredArg { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result { + if let Ok(__w) = obj.extract::() { + return Ok(PyChiSquaredArg(__w.inner)); + } + let __v = crate::runtime::coerce::floats_exact(obj, 1, "ChiSquared")?; + Ok(PyChiSquaredArg(rust_physics_engine::statistics::distributions::ChiSquared { k: __v[0] })) + } +} + + +/// Exponential distribution with the given rate λ (mean 1/λ), wrapping +/// the module's free `exponential_pdf`/`exponential_cdf`. +/// +/// Rust: `statistics::distributions::Exponential` +#[pyclass(name = "Exponential", module = "numeria.statistics.distributions", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyExponential { pub inner: rust_physics_engine::statistics::distributions::Exponential } +#[pymethods] +impl PyExponential { + /// Panics: + /// Panics unless rate > 0. + /// + /// Rust: `statistics::distributions::Exponential::new` + #[new] + #[pyo3(signature = (rate))] + fn __new__(rate: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistics::distributions::Exponential::new(rate)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyExponential { inner: __v }) + } + + #[getter] + #[pyo3(name = "rate")] + fn py_get_rate(&self) -> PyResult { Ok(self.inner.rate) } + + #[setter] + #[pyo3(name = "rate")] + fn py_set_rate(&mut self, v: f64) { self.inner.rate = v; } + + fn __repr__(&self) -> String { format!("Exponential(rate={:?})", self.inner.rate) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A `Exponential` argument, or anything that can stand in for one. +pub struct PyExponentialArg(pub rust_physics_engine::statistics::distributions::Exponential); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyExponentialArg { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result { + if let Ok(__w) = obj.extract::() { + return Ok(PyExponentialArg(__w.inner)); + } + let __v = crate::runtime::coerce::floats_exact(obj, 1, "Exponential")?; + Ok(PyExponentialArg(rust_physics_engine::statistics::distributions::Exponential { rate: __v[0] })) + } +} + + +/// Fisher-Snedecor F distribution with (d1, d2) degrees of freedom. +/// +/// Rust: `statistics::distributions::FDist` +#[pyclass(name = "FDist", module = "numeria.statistics.distributions", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyFDist { pub inner: rust_physics_engine::statistics::distributions::FDist } +#[pymethods] +impl PyFDist { + /// Panics: + /// Panics unless d1 > 0 and d2 > 0. + /// + /// Rust: `statistics::distributions::FDist::new` + #[new] + #[pyo3(signature = (d1, d2))] + fn __new__(d1: f64, d2: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistics::distributions::FDist::new(d1, d2)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyFDist { inner: __v }) + } + + #[getter] + #[pyo3(name = "d1")] + fn py_get_d1(&self) -> PyResult { Ok(self.inner.d1) } + + #[setter] + #[pyo3(name = "d1")] + fn py_set_d1(&mut self, v: f64) { self.inner.d1 = v; } + + #[getter] + #[pyo3(name = "d2")] + fn py_get_d2(&self) -> PyResult { Ok(self.inner.d2) } + + #[setter] + #[pyo3(name = "d2")] + fn py_set_d2(&mut self, v: f64) { self.inner.d2 = v; } + + fn __repr__(&self) -> String { format!("FDist(d1={:?}, d2={:?})", self.inner.d1, self.inner.d2) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A `FDist` argument, or anything that can stand in for one. +pub struct PyFDistArg(pub rust_physics_engine::statistics::distributions::FDist); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyFDistArg { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result { + if let Ok(__w) = obj.extract::() { + return Ok(PyFDistArg(__w.inner)); + } + let __v = crate::runtime::coerce::floats_exact(obj, 2, "FDist")?; + Ok(PyFDistArg(rust_physics_engine::statistics::distributions::FDist { d1: __v[0], d2: __v[1] })) + } +} + + +/// Gamma distribution with shape α and rate β (mean α/β). +/// +/// Rust: `statistics::distributions::Gamma` +#[pyclass(name = "Gamma", module = "numeria.statistics.distributions", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyGamma { pub inner: rust_physics_engine::statistics::distributions::Gamma } +#[pymethods] +impl PyGamma { + /// Panics: + /// Panics unless shape > 0 and rate > 0. + /// + /// Rust: `statistics::distributions::Gamma::new` + #[new] + #[pyo3(signature = (shape, rate))] + fn __new__(shape: f64, rate: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistics::distributions::Gamma::new(shape, rate)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyGamma { inner: __v }) + } + + #[getter] + #[pyo3(name = "shape")] + fn py_get_shape(&self) -> PyResult { Ok(self.inner.shape) } + + #[setter] + #[pyo3(name = "shape")] + fn py_set_shape(&mut self, v: f64) { self.inner.shape = v; } + + #[getter] + #[pyo3(name = "rate")] + fn py_get_rate(&self) -> PyResult { Ok(self.inner.rate) } + + #[setter] + #[pyo3(name = "rate")] + fn py_set_rate(&mut self, v: f64) { self.inner.rate = v; } + + fn __repr__(&self) -> String { format!("Gamma(shape={:?}, rate={:?})", self.inner.shape, self.inner.rate) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A `Gamma` argument, or anything that can stand in for one. +pub struct PyGammaArg(pub rust_physics_engine::statistics::distributions::Gamma); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyGammaArg { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result { + if let Ok(__w) = obj.extract::() { + return Ok(PyGammaArg(__w.inner)); + } + let __v = crate::runtime::coerce::floats_exact(obj, 2, "Gamma")?; + Ok(PyGammaArg(rust_physics_engine::statistics::distributions::Gamma { shape: __v[0], rate: __v[1] })) + } +} + + +/// Log-normal distribution: ln X ~ N(μ, σ²). +/// +/// Rust: `statistics::distributions::LogNormal` +#[pyclass(name = "LogNormal", module = "numeria.statistics.distributions", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyLogNormal { pub inner: rust_physics_engine::statistics::distributions::LogNormal } +#[pymethods] +impl PyLogNormal { + /// Panics: + /// Panics unless σ > 0. + /// + /// Rust: `statistics::distributions::LogNormal::new` + #[new] + #[pyo3(signature = (mu, sigma))] + fn __new__(mu: f64, sigma: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistics::distributions::LogNormal::new(mu, sigma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyLogNormal { inner: __v }) + } + + #[getter] + #[pyo3(name = "mu")] + fn py_get_mu(&self) -> PyResult { Ok(self.inner.mu) } + + #[setter] + #[pyo3(name = "mu")] + fn py_set_mu(&mut self, v: f64) { self.inner.mu = v; } + + #[getter] + #[pyo3(name = "sigma")] + fn py_get_sigma(&self) -> PyResult { Ok(self.inner.sigma) } + + #[setter] + #[pyo3(name = "sigma")] + fn py_set_sigma(&mut self, v: f64) { self.inner.sigma = v; } + + fn __repr__(&self) -> String { format!("LogNormal(mu={:?}, sigma={:?})", self.inner.mu, self.inner.sigma) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A `LogNormal` argument, or anything that can stand in for one. +pub struct PyLogNormalArg(pub rust_physics_engine::statistics::distributions::LogNormal); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyLogNormalArg { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result { + if let Ok(__w) = obj.extract::() { + return Ok(PyLogNormalArg(__w.inner)); + } + let __v = crate::runtime::coerce::floats_exact(obj, 2, "LogNormal")?; + Ok(PyLogNormalArg(rust_physics_engine::statistics::distributions::LogNormal { mu: __v[0], sigma: __v[1] })) + } +} + + +/// Normal distribution N(μ, σ²); quantile via `erfinv`. +/// +/// Rust: `statistics::distributions::Normal` +#[pyclass(name = "Normal", module = "numeria.statistics.distributions", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyNormal { pub inner: rust_physics_engine::statistics::distributions::Normal } +#[pymethods] +impl PyNormal { + /// Panics: + /// Panics unless σ > 0. + /// + /// Rust: `statistics::distributions::Normal::new` + #[new] + #[pyo3(signature = (mu, sigma))] + fn __new__(mu: f64, sigma: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistics::distributions::Normal::new(mu, sigma)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyNormal { inner: __v }) + } + + #[getter] + #[pyo3(name = "mu")] + fn py_get_mu(&self) -> PyResult { Ok(self.inner.mu) } + + #[setter] + #[pyo3(name = "mu")] + fn py_set_mu(&mut self, v: f64) { self.inner.mu = v; } + + #[getter] + #[pyo3(name = "sigma")] + fn py_get_sigma(&self) -> PyResult { Ok(self.inner.sigma) } + + #[setter] + #[pyo3(name = "sigma")] + fn py_set_sigma(&mut self, v: f64) { self.inner.sigma = v; } + + fn __repr__(&self) -> String { format!("Normal(mu={:?}, sigma={:?})", self.inner.mu, self.inner.sigma) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A `Normal` argument, or anything that can stand in for one. +pub struct PyNormalArg(pub rust_physics_engine::statistics::distributions::Normal); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyNormalArg { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result { + if let Ok(__w) = obj.extract::() { + return Ok(PyNormalArg(__w.inner)); + } + let __v = crate::runtime::coerce::floats_exact(obj, 2, "Normal")?; + Ok(PyNormalArg(rust_physics_engine::statistics::distributions::Normal { mu: __v[0], sigma: __v[1] })) + } +} + + +/// Poisson distribution (discrete); `pdf` is the mass at round(x) and +/// `cdf` uses P(X ≤ k) = Q(k+1, λ). +/// +/// Rust: `statistics::distributions::Poisson` +#[pyclass(name = "Poisson", module = "numeria.statistics.distributions", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyPoisson { pub inner: rust_physics_engine::statistics::distributions::Poisson } +#[pymethods] +impl PyPoisson { + /// Panics: + /// Panics unless λ > 0. + /// + /// Rust: `statistics::distributions::Poisson::new` + #[new] + #[pyo3(signature = (lambda_))] + fn __new__(lambda_: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistics::distributions::Poisson::new(lambda_)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyPoisson { inner: __v }) + } + + /// Probability mass P(X = k), computed in log space. + /// + /// Rust: `statistics::distributions::Poisson::pmf` + #[pyo3(name = "pmf")] + #[pyo3(signature = (k))] + fn pmf(&self, k: u64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.pmf(k)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "lambda_")] + fn py_get_lambda(&self) -> PyResult { Ok(self.inner.lambda) } + + #[setter] + #[pyo3(name = "lambda_")] + fn py_set_lambda(&mut self, v: f64) { self.inner.lambda = v; } + + fn __repr__(&self) -> String { format!("Poisson(lambda={:?})", self.inner.lambda) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A `Poisson` argument, or anything that can stand in for one. +pub struct PyPoissonArg(pub rust_physics_engine::statistics::distributions::Poisson); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyPoissonArg { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result { + if let Ok(__w) = obj.extract::() { + return Ok(PyPoissonArg(__w.inner)); + } + let __v = crate::runtime::coerce::floats_exact(obj, 1, "Poisson")?; + Ok(PyPoissonArg(rust_physics_engine::statistics::distributions::Poisson { lambda: __v[0] })) + } +} + + +/// Student's t distribution with ν degrees of freedom; CDF via the +/// regularized incomplete beta function. +/// +/// Rust: `statistics::distributions::StudentT` +#[pyclass(name = "StudentT", module = "numeria.statistics.distributions", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyStudentT { pub inner: rust_physics_engine::statistics::distributions::StudentT } +#[pymethods] +impl PyStudentT { + /// Panics: + /// Panics unless ν > 0. + /// + /// Rust: `statistics::distributions::StudentT::new` + #[new] + #[pyo3(signature = (nu))] + fn __new__(nu: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistics::distributions::StudentT::new(nu)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyStudentT { inner: __v }) + } + + #[getter] + #[pyo3(name = "nu")] + fn py_get_nu(&self) -> PyResult { Ok(self.inner.nu) } + + #[setter] + #[pyo3(name = "nu")] + fn py_set_nu(&mut self, v: f64) { self.inner.nu = v; } + + fn __repr__(&self) -> String { format!("StudentT(nu={:?})", self.inner.nu) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A `StudentT` argument, or anything that can stand in for one. +pub struct PyStudentTArg(pub rust_physics_engine::statistics::distributions::StudentT); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyStudentTArg { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result { + if let Ok(__w) = obj.extract::() { + return Ok(PyStudentTArg(__w.inner)); + } + let __v = crate::runtime::coerce::floats_exact(obj, 1, "StudentT")?; + Ok(PyStudentTArg(rust_physics_engine::statistics::distributions::StudentT { nu: __v[0] })) + } +} + + +/// Weibull distribution with shape k and scale λ. +/// +/// Rust: `statistics::distributions::Weibull` +#[pyclass(name = "Weibull", module = "numeria.statistics.distributions", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyWeibull { pub inner: rust_physics_engine::statistics::distributions::Weibull } +#[pymethods] +impl PyWeibull { + /// Panics: + /// Panics unless k > 0 and λ > 0. + /// + /// Rust: `statistics::distributions::Weibull::new` + #[new] + #[pyo3(signature = (k, lambda_))] + fn __new__(k: f64, lambda_: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::statistics::distributions::Weibull::new(k, lambda_)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyWeibull { inner: __v }) + } + + #[getter] + #[pyo3(name = "k")] + fn py_get_k(&self) -> PyResult { Ok(self.inner.k) } + + #[setter] + #[pyo3(name = "k")] + fn py_set_k(&mut self, v: f64) { self.inner.k = v; } + + #[getter] + #[pyo3(name = "lambda_")] + fn py_get_lambda(&self) -> PyResult { Ok(self.inner.lambda) } + + #[setter] + #[pyo3(name = "lambda_")] + fn py_set_lambda(&mut self, v: f64) { self.inner.lambda = v; } + + fn __repr__(&self) -> String { format!("Weibull(k={:?}, lambda={:?})", self.inner.k, self.inner.lambda) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A `Weibull` argument, or anything that can stand in for one. +pub struct PyWeibullArg(pub rust_physics_engine::statistics::distributions::Weibull); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyWeibullArg { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result { + if let Ok(__w) = obj.extract::() { + return Ok(PyWeibullArg(__w.inner)); + } + let __v = crate::runtime::coerce::floats_exact(obj, 2, "Weibull")?; + Ok(PyWeibullArg(rust_physics_engine::statistics::distributions::Weibull { k: __v[0], lambda: __v[1] })) + } +} + + +/// Outcome of a hypothesis test. For tests with two df parameters +/// (ANOVA, independence tables) `df` is the numerator / primary df; +/// the p-value always accounts for the full parameterization. +/// +/// Rust: `statistics::inference::TestResult` +#[pyclass(name = "TestResult", module = "numeria.statistics.inference", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyTestResult { pub inner: rust_physics_engine::statistics::inference::TestResult } +#[pymethods] +impl PyTestResult { + /// Builds a `TestResult` from its fields. + #[new] + #[pyo3(signature = (statistic, p_value, df))] + fn __new__(statistic: f64, p_value: f64, df: f64) -> Self { + + Self { inner: rust_physics_engine::statistics::inference::TestResult { statistic: statistic, p_value: p_value, df: df } } + } + + #[getter] + #[pyo3(name = "statistic")] + fn py_get_statistic(&self) -> PyResult { Ok(self.inner.statistic) } + + #[setter] + #[pyo3(name = "statistic")] + fn py_set_statistic(&mut self, v: f64) { self.inner.statistic = v; } + + #[getter] + #[pyo3(name = "p_value")] + fn py_get_p_value(&self) -> PyResult { Ok(self.inner.p_value) } + + #[setter] + #[pyo3(name = "p_value")] + fn py_set_p_value(&mut self, v: f64) { self.inner.p_value = v; } + + #[getter] + #[pyo3(name = "df")] + fn py_get_df(&self) -> PyResult { Ok(self.inner.df) } + + #[setter] + #[pyo3(name = "df")] + fn py_set_df(&mut self, v: f64) { self.inner.df = v; } + + fn __repr__(&self) -> String { format!("TestResult(statistic={:?}, p_value={:?}, df={:?})", self.inner.statistic, self.inner.p_value, self.inner.df) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A `TestResult` argument, or anything that can stand in for one. +pub struct PyTestResultArg(pub rust_physics_engine::statistics::inference::TestResult); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyTestResultArg { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result { + if let Ok(__w) = obj.extract::() { + return Ok(PyTestResultArg(__w.inner)); + } + let __v = crate::runtime::coerce::floats_exact(obj, 3, "TestResult")?; + Ok(PyTestResultArg(rust_physics_engine::statistics::inference::TestResult { statistic: __v[0], p_value: __v[1], df: __v[2] })) + } +} + + +/// Bootstrap summary: point estimate on the original data, bootstrap +/// standard error, and the confidence interval bounds. +/// +/// Rust: `statistics::resampling::BootstrapResult` +#[pyclass(name = "BootstrapResult", module = "numeria.statistics.resampling", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyBootstrapResult { pub inner: rust_physics_engine::statistics::resampling::BootstrapResult } +#[pymethods] +impl PyBootstrapResult { + /// Builds a `BootstrapResult` from its fields. + #[new] + #[pyo3(signature = (estimate, se, ci_low, ci_high))] + fn __new__(estimate: f64, se: f64, ci_low: f64, ci_high: f64) -> Self { + + Self { inner: rust_physics_engine::statistics::resampling::BootstrapResult { estimate: estimate, se: se, ci_low: ci_low, ci_high: ci_high } } + } + + #[getter] + #[pyo3(name = "estimate")] + fn py_get_estimate(&self) -> PyResult { Ok(self.inner.estimate) } + + #[setter] + #[pyo3(name = "estimate")] + fn py_set_estimate(&mut self, v: f64) { self.inner.estimate = v; } + + #[getter] + #[pyo3(name = "se")] + fn py_get_se(&self) -> PyResult { Ok(self.inner.se) } + + #[setter] + #[pyo3(name = "se")] + fn py_set_se(&mut self, v: f64) { self.inner.se = v; } + + #[getter] + #[pyo3(name = "ci_low")] + fn py_get_ci_low(&self) -> PyResult { Ok(self.inner.ci_low) } + + #[setter] + #[pyo3(name = "ci_low")] + fn py_set_ci_low(&mut self, v: f64) { self.inner.ci_low = v; } + + #[getter] + #[pyo3(name = "ci_high")] + fn py_get_ci_high(&self) -> PyResult { Ok(self.inner.ci_high) } + + #[setter] + #[pyo3(name = "ci_high")] + fn py_set_ci_high(&mut self, v: f64) { self.inner.ci_high = v; } + + fn __repr__(&self) -> String { format!("BootstrapResult(estimate={:?}, se={:?}, ci_low={:?}, ci_high={:?})", self.inner.estimate, self.inner.se, self.inner.ci_low, self.inner.ci_high) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A `BootstrapResult` argument, or anything that can stand in for one. +pub struct PyBootstrapResultArg(pub rust_physics_engine::statistics::resampling::BootstrapResult); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyBootstrapResultArg { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result { + if let Ok(__w) = obj.extract::() { + return Ok(PyBootstrapResultArg(__w.inner)); + } + let __v = crate::runtime::coerce::floats_exact(obj, 4, "BootstrapResult")?; + Ok(PyBootstrapResultArg(rust_physics_engine::statistics::resampling::BootstrapResult { estimate: __v[0], se: __v[1], ci_low: __v[2], ci_high: __v[3] })) + } +} + diff --git a/bindings/python/src/generated/types/stochastic.rs b/bindings/python/src/generated/types/stochastic.rs new file mode 100644 index 0000000..2f96034 --- /dev/null +++ b/bindings/python/src/generated/types/stochastic.rs @@ -0,0 +1,2572 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; + + +/// The Archimedean and elliptical families supported here. +/// +/// Rust: `stochastic::extreme::CopulaFamily` +#[pyclass(name = "CopulaFamily", module = "numeria.stochastic.extreme", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyCopulaFamily { + Gaussian, + Clayton, + Gumbel, + Frank, +} +impl PyCopulaFamily { + pub fn to_rust(&self) -> rust_physics_engine::stochastic::extreme::CopulaFamily { match self { + Self::Gaussian => rust_physics_engine::stochastic::extreme::CopulaFamily::Gaussian, + Self::Clayton => rust_physics_engine::stochastic::extreme::CopulaFamily::Clayton, + Self::Gumbel => rust_physics_engine::stochastic::extreme::CopulaFamily::Gumbel, + Self::Frank => rust_physics_engine::stochastic::extreme::CopulaFamily::Frank, + } } + pub fn from_rust(v: &rust_physics_engine::stochastic::extreme::CopulaFamily) -> Self { match v { + rust_physics_engine::stochastic::extreme::CopulaFamily::Gaussian => Self::Gaussian, + rust_physics_engine::stochastic::extreme::CopulaFamily::Clayton => Self::Clayton, + rust_physics_engine::stochastic::extreme::CopulaFamily::Gumbel => Self::Gumbel, + rust_physics_engine::stochastic::extreme::CopulaFamily::Frank => Self::Frank, + } } +} +#[pymethods] +impl PyCopulaFamily { + fn __repr__(&self) -> &'static str { + match self { + Self::Gaussian => "CopulaFamily.Gaussian", + Self::Clayton => "CopulaFamily.Clayton", + Self::Gumbel => "CopulaFamily.Gumbel", + Self::Frank => "CopulaFamily.Frank", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// One step of a Kalman filter's output: the state estimate and its +/// covariance, before and after the measurement. +/// +/// Rust: `stochastic::hmm::FilterStep` +#[pyclass(name = "FilterStep", module = "numeria.stochastic.hmm", from_py_object)] +#[derive(Clone)] +pub struct PyFilterStep { pub inner: rust_physics_engine::stochastic::hmm::FilterStep } +#[pymethods] +impl PyFilterStep { + /// Builds a `FilterStep` from its fields. + #[new] + #[pyo3(signature = (predicted, predicted_cov, filtered, filtered_cov))] + fn __new__(predicted: Vec, predicted_cov: crate::generated::types::PyMatrixArg, filtered: Vec, filtered_cov: crate::generated::types::PyMatrixArg) -> Self { + let predicted_cov = predicted_cov.0; + let filtered_cov = filtered_cov.0; + Self { inner: rust_physics_engine::stochastic::hmm::FilterStep { predicted: predicted, predicted_cov: predicted_cov, filtered: filtered, filtered_cov: filtered_cov } } + } + + #[getter] + #[pyo3(name = "predicted")] + fn py_get_predicted(&self) -> PyResult> { Ok(self.inner.predicted.clone()) } + + #[getter] + #[pyo3(name = "predicted_cov")] + fn py_get_predicted_cov(&self) -> PyResult { Ok(crate::generated::types::PyMatrix { inner: self.inner.predicted_cov.clone() }) } + + #[getter] + #[pyo3(name = "filtered")] + fn py_get_filtered(&self) -> PyResult> { Ok(self.inner.filtered.clone()) } + + #[getter] + #[pyo3(name = "filtered_cov")] + fn py_get_filtered_cov(&self) -> PyResult { Ok(crate::generated::types::PyMatrix { inner: self.inner.filtered_cov.clone() }) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("FilterStep", "FilterStep", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A hidden Markov model whose emissions are one-dimensional Gaussians. +/// +/// Rust: `stochastic::hmm::GaussianHmm` +#[pyclass(name = "GaussianHmm", module = "numeria.stochastic.hmm", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyGaussianHmm { pub inner: rust_physics_engine::stochastic::hmm::GaussianHmm } +#[pymethods] +impl PyGaussianHmm { + /// The model with the given parameters. + /// + /// Errors: + /// Returns an error unless the shapes agree, the variances are positive, + /// and the rows are distributions. + /// + /// Rust: `stochastic::hmm::GaussianHmm::new` + #[new] + #[pyo3(signature = (a, means, vars, pi))] + fn __new__(a: crate::generated::types::PyMatrixArg, means: Vec, vars: Vec, pi: Vec) -> PyResult { + let a = a.0; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::hmm::GaussianHmm::new(a, means, vars, pi)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyGaussianHmm { inner: __v }) + } + + /// The number of hidden states. + /// + /// Rust: `stochastic::hmm::GaussianHmm::n_states` + #[pyo3(name = "n_states")] + #[pyo3(signature = ())] + fn n_states(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.n_states()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The emission density of state `i` at `x`. + /// + /// Rust: `stochastic::hmm::GaussianHmm::emission` + #[pyo3(name = "emission")] + #[pyo3(signature = (i, x))] + fn emission(&self, i: usize, x: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.emission(i, x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The forward recursion, scaled, with the log-likelihood. + /// + /// Rust: `stochastic::hmm::GaussianHmm::forward` + #[pyo3(name = "forward")] + #[pyo3(signature = (obs))] + fn forward(&self, obs: Vec) -> PyResult<(f64, crate::generated::types::PyMatrix)> { + let __r = crate::runtime::guard(|| self.inner.forward(&obs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, crate::generated::types::PyMatrix { inner: __v.1 })) + } + + /// The scaled backward recursion. + /// + /// Rust: `stochastic::hmm::GaussianHmm::backward` + #[pyo3(name = "backward")] + #[pyo3(signature = (obs))] + fn backward(&self, obs: Vec) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.backward(&obs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) + } + + /// The most likely state path and its log probability. + /// + /// Rust: `stochastic::hmm::GaussianHmm::viterbi` + #[pyo3(name = "viterbi")] + #[pyo3(signature = (obs))] + fn viterbi<'py>(&self, py: Python<'py>, obs: Vec) -> PyResult<(f64, Vec)> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.viterbi(&obs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) + } + + /// Baum-Welch for Gaussian emissions, returning the final + /// log-likelihood. + /// + /// The maximisation step is the posterior-weighted mean and variance of + /// the observations, which is the same closed form a Gaussian mixture + /// uses -- the only difference is where the weights come from. + /// + /// Rust: `stochastic::hmm::GaussianHmm::baum_welch` + #[pyo3(name = "baum_welch")] + #[pyo3(signature = (obs, iters, tol))] + fn baum_welch<'py>(&mut self, py: Python<'py>, obs: Vec, iters: usize, tol: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.baum_welch(&obs, iters, tol))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Draws a state path and the observations it produces. + /// + /// Rust: `stochastic::hmm::GaussianHmm::simulate` + #[pyo3(name = "simulate")] + #[pyo3(signature = (n, rng))] + fn simulate(&self, n: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(Vec, Vec)> { + let mut rng = rng; + let __r = crate::runtime::guard(|| self.inner.simulate(n, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) + } + + #[getter] + #[pyo3(name = "a")] + fn py_get_a(&self) -> PyResult { Ok(crate::generated::types::PyMatrix { inner: self.inner.a.clone() }) } + + #[getter] + #[pyo3(name = "means")] + fn py_get_means(&self) -> PyResult> { Ok(self.inner.means.clone()) } + + #[getter] + #[pyo3(name = "vars")] + fn py_get_vars(&self) -> PyResult> { Ok(self.inner.vars.clone()) } + + #[getter] + #[pyo3(name = "pi")] + fn py_get_pi(&self) -> PyResult> { Ok(self.inner.pi.clone()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("GaussianHmm", "GaussianHmm", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A hidden Markov model with discrete emissions. +/// +/// Rust: `stochastic::hmm::Hmm` +#[pyclass(name = "Hmm", module = "numeria.stochastic.hmm", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyHmm { pub inner: rust_physics_engine::stochastic::hmm::Hmm } +#[pymethods] +impl PyHmm { + /// The model with the given parameters. + /// + /// Errors: + /// Returns an error unless the shapes agree and every row of each matrix, + /// and the initial distribution, sums to one over non-negative entries. + /// + /// Rust: `stochastic::hmm::Hmm::new` + #[new] + #[pyo3(signature = (a, b, pi))] + fn __new__(a: crate::generated::types::PyMatrixArg, b: crate::generated::types::PyMatrixArg, pi: Vec) -> PyResult { + let a = a.0; + let b = b.0; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::hmm::Hmm::new(a, b, pi)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyHmm { inner: __v }) + } + + /// The number of hidden states. + /// + /// Rust: `stochastic::hmm::Hmm::n_states` + #[pyo3(name = "n_states")] + #[pyo3(signature = ())] + fn n_states(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.n_states()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The number of observable symbols. + /// + /// Rust: `stochastic::hmm::Hmm::n_symbols` + #[pyo3(name = "n_symbols")] + #[pyo3(signature = ())] + fn n_symbols(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.n_symbols()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// A model with random parameters, for Baum-Welch to start from. + /// + /// Panics: + /// Panics if either dimension is zero. + /// + /// Rust: `stochastic::hmm::Hmm::random_init` + #[pyo3(name = "random_init")] + #[staticmethod] + #[pyo3(signature = (n_states, n_symbols, rng))] + fn random_init(n_states: usize, n_symbols: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::hmm::Hmm::random_init(n_states, n_symbols, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyHmm { inner: __v }) + } + + /// The forward recursion, returning the log-likelihood and the scaled + /// forward probabilities. + /// + /// `alpha[t][i]` is the probability of state `i` at time `t` given the + /// observations up to `t`, rescaled to sum to one at each step. Scaling + /// is not an optimisation: without it the raw forward variables shrink by + /// roughly the observation's probability at every step and underflow a + /// double within a few hundred symbols. The scale factors, summed as + /// logs, are exactly the log-likelihood. + /// + /// Panics: + /// Panics if an observation is outside the alphabet. + /// + /// Rust: `stochastic::hmm::Hmm::forward` + #[pyo3(name = "forward")] + #[pyo3(signature = (obs))] + fn forward(&self, obs: Vec) -> PyResult<(f64, crate::generated::types::PyMatrix)> { + let __r = crate::runtime::guard(|| self.inner.forward(&obs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, crate::generated::types::PyMatrix { inner: __v.1 })) + } + + /// The backward recursion, scaled to match `forward`. + /// + /// `beta[t][i]` is proportional to the probability of the observations + /// after `t` given state `i` at `t`, under the same per-step scaling. + /// + /// Panics: + /// Panics if an observation is outside the alphabet. + /// + /// Rust: `stochastic::hmm::Hmm::backward` + #[pyo3(name = "backward")] + #[pyo3(signature = (obs))] + fn backward(&self, obs: Vec) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.backward(&obs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) + } + + /// The log-likelihood of an observation sequence. + /// + /// Panics: + /// Panics if an observation is outside the alphabet. + /// + /// Rust: `stochastic::hmm::Hmm::log_likelihood` + #[pyo3(name = "log_likelihood")] + #[pyo3(signature = (obs))] + fn log_likelihood<'py>(&self, py: Python<'py>, obs: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.log_likelihood(&obs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The single most likely state path, and its log probability. + /// + /// The forward recursion with the sum replaced by a maximum, in logs. It + /// answers a different question from decoding each state separately: the + /// best path need not contain any individual state's most likely value, + /// and unlike posterior decoding its answer is always a path the model + /// can actually produce. + /// + /// Panics: + /// Panics if an observation is outside the alphabet. + /// + /// Rust: `stochastic::hmm::Hmm::viterbi` + #[pyo3(name = "viterbi")] + #[pyo3(signature = (obs))] + fn viterbi<'py>(&self, py: Python<'py>, obs: Vec) -> PyResult<(f64, Vec)> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.viterbi(&obs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) + } + + /// The state posteriors: `gamma[t][i]` is the probability of state `i` at + /// time `t` given the whole sequence. + /// + /// Panics: + /// Panics if an observation is outside the alphabet. + /// + /// Rust: `stochastic::hmm::Hmm::posteriors` + #[pyo3(name = "posteriors")] + #[pyo3(signature = (obs))] + fn posteriors(&self, obs: Vec) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.posteriors(&obs)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) + } + + /// The most likely state at each time, taken separately. + /// + /// Maximises the expected number of correct states, which is a different + /// objective from Viterbi's. The path it returns can have probability + /// zero -- if two consecutive states are each individually likeliest but + /// the transition between them is impossible, this will happily report + /// both. + /// + /// Panics: + /// Panics if an observation is outside the alphabet. + /// + /// Rust: `stochastic::hmm::Hmm::posterior_decode` + #[pyo3(name = "posterior_decode")] + #[pyo3(signature = (obs))] + fn posterior_decode<'py>(&self, py: Python<'py>, obs: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.posterior_decode(&obs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Baum-Welch training, returning the final total log-likelihood. + /// + /// Expectation-maximisation: compute the expected transition and emission + /// counts under the current parameters, then set the parameters to their + /// maximum-likelihood values given those counts. Each round is guaranteed + /// not to lower the likelihood, which is what makes it safe to run + /// without a line search -- and also all it guarantees, since it climbs + /// to a local optimum that depends entirely on where it started. + /// + /// Stops early when the improvement falls below `tol`. + /// + /// Panics: + /// Panics if a sequence contains an observation outside the alphabet. + /// + /// Rust: `stochastic::hmm::Hmm::baum_welch` + #[pyo3(name = "baum_welch")] + #[pyo3(signature = (sequences, iters, tol))] + fn baum_welch<'py>(&mut self, py: Python<'py>, sequences: Vec>, iters: usize, tol: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.baum_welch(&sequences, iters, tol))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Draws a state path and the observations it produces. + /// + /// Rust: `stochastic::hmm::Hmm::simulate` + #[pyo3(name = "simulate")] + #[pyo3(signature = (n, rng))] + fn simulate(&self, n: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(Vec, Vec)> { + let mut rng = rng; + let __r = crate::runtime::guard(|| self.inner.simulate(n, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) + } + + #[getter] + #[pyo3(name = "a")] + fn py_get_a(&self) -> PyResult { Ok(crate::generated::types::PyMatrix { inner: self.inner.a.clone() }) } + + #[getter] + #[pyo3(name = "b")] + fn py_get_b(&self) -> PyResult { Ok(crate::generated::types::PyMatrix { inner: self.inner.b.clone() }) } + + #[getter] + #[pyo3(name = "pi")] + fn py_get_pi(&self) -> PyResult> { Ok(self.inner.pi.clone()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Hmm", "Hmm", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A bootstrap particle filter: a cloud of weighted samples standing in for +/// the state distribution. +/// +/// Where the Kalman filter propagates a mean and a covariance -- which is +/// exactly right if everything is linear and Gaussian and wrong otherwise -- +/// this propagates samples, so it can represent any shape at all. The price +/// is variance, and the need to resample: without it the weight concentrates +/// on one particle and the rest of the cloud stops contributing. +/// +/// Rust: `stochastic::hmm::ParticleFilter` +#[pyclass(name = "ParticleFilter", module = "numeria.stochastic.hmm", from_py_object)] +#[derive(Clone)] +pub struct PyParticleFilter { pub inner: rust_physics_engine::stochastic::hmm::ParticleFilter } +#[pymethods] +impl PyParticleFilter { + /// Reweights the particles by how well each explains a measurement. + /// + /// The weights are multiplied by the likelihood and renormalised. If + /// every particle is impossible the cloud is reset to uniform weights, + /// since the alternative is dividing by zero. + /// + /// Rust: `stochastic::hmm::ParticleFilter::update` + #[pyo3(name = "update")] + #[pyo3(signature = (likelihood))] + fn update(&mut self, likelihood: pyo3::Py) -> PyResult<()> { + let __cb_likelihood = std::rc::Rc::new(crate::runtime::Callback::new(likelihood)); + let likelihood = { let __cb = __cb_likelihood.clone(); move |__a0: &[f64]| -> f64 { __cb.call::<_, f64>((__a0.to_vec(),), f64::NAN) } }; + let __r = crate::runtime::guard(|| self.inner.update(&likelihood)); + crate::runtime::callback::check(&[&__cb_likelihood], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// Systematic resampling: draw a single uniform and take evenly spaced + /// points from the cumulative weights. + /// + /// One random number for the whole cloud rather than one per particle, + /// which gives lower variance than independent draws and guarantees that + /// a particle with weight `w` is copied either `floor(nw)` or + /// `ceil(nw)` times -- never zero when it deserves several. + /// + /// Rust: `stochastic::hmm::ParticleFilter::resample_systematic` + #[pyo3(name = "resample_systematic")] + #[pyo3(signature = (rng))] + fn resample_systematic(&mut self, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<()> { + let mut rng = rng; + let __r = crate::runtime::guard(|| self.inner.resample_systematic(&mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(()) + } + + /// The weighted mean of the cloud. + /// + /// Panics: + /// Panics if the cloud is empty. + /// + /// Rust: `stochastic::hmm::ParticleFilter::estimate` + #[pyo3(name = "estimate")] + #[pyo3(signature = ())] + fn estimate<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.estimate())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The effective number of particles: the reciprocal of the sum of + /// squared weights. + /// + /// Equal to the particle count when the weights are uniform and one when + /// a single particle holds everything. Falling below about half the count + /// is the usual signal to resample. + /// + /// Rust: `stochastic::hmm::ParticleFilter::effective_n` + #[pyo3(name = "effective_n")] + #[pyo3(signature = ())] + fn effective_n(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.effective_n()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "particles")] + fn py_get_particles(&self) -> PyResult>> { Ok(self.inner.particles.clone()) } + + #[getter] + #[pyo3(name = "weights")] + fn py_get_weights(&self) -> PyResult> { Ok(self.inner.weights.clone()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("ParticleFilter", "ParticleFilter", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A finite Markov chain, held as its row-stochastic transition matrix. +/// +/// Rust: `stochastic::markov::MarkovChain` +#[pyclass(name = "MarkovChain", module = "numeria.stochastic.markov", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyMarkovChain { pub inner: rust_physics_engine::stochastic::markov::MarkovChain } +#[pymethods] +impl PyMarkovChain { + /// The chain with the given transition matrix. + /// + /// Errors: + /// Returns an error unless the matrix is square, non-empty, has no + /// negative entries, and every row sums to one. + /// + /// Rust: `stochastic::markov::MarkovChain::new` + #[new] + #[pyo3(signature = (p))] + fn __new__(p: crate::generated::types::PyMatrixArg) -> PyResult { + let p = p.0; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::markov::MarkovChain::new(p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyMarkovChain { inner: __v }) + } + + /// The number of states. + /// + /// Rust: `stochastic::markov::MarkovChain::n` + #[pyo3(name = "n")] + #[pyo3(signature = ())] + fn n(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.n()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// A chain estimated from a matrix of observed transition counts. + /// + /// Each row is normalised by its total, which is the maximum likelihood + /// estimate. A row with no observations is made absorbing, since the data + /// says nothing about where that state goes and any other choice would be + /// an invention. + /// + /// Errors: + /// Returns an error unless the counts form a non-empty square matrix with + /// no negative entries. + /// + /// Rust: `stochastic::markov::MarkovChain::from_counts` + #[pyo3(name = "from_counts")] + #[staticmethod] + #[pyo3(signature = (transitions))] + fn from_counts(transitions: crate::generated::types::PyMatrixArg) -> PyResult { + let transitions = transitions.0; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::markov::MarkovChain::from_counts(&transitions)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyMarkovChain { inner: __v }) + } + + /// A chain estimated from one observed sequence of states. + /// + /// Errors: + /// Returns an error if `n_states` is zero or a state is out of range. + /// + /// Rust: `stochastic::markov::MarkovChain::from_sequence` + #[pyo3(name = "from_sequence")] + #[staticmethod] + #[pyo3(signature = (states, n_states))] + fn from_sequence(states: Vec, n_states: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::markov::MarkovChain::from_sequence(&states, n_states)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyMarkovChain { inner: __v }) + } + + /// The distribution one step on from `dist`. + /// + /// Panics: + /// Panics unless `dist` has one entry per state. + /// + /// Rust: `stochastic::markov::MarkovChain::step_dist` + #[pyo3(name = "step_dist")] + #[pyo3(signature = (dist))] + fn step_dist<'py>(&self, py: Python<'py>, dist: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.step_dist(&dist))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The `n`-step transition matrix, by repeated squaring. + /// + /// Rust: `stochastic::markov::MarkovChain::n_step` + #[pyo3(name = "n_step")] + #[pyo3(signature = (n))] + fn n_step(&self, n: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.n_step(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) + } + + /// A stationary distribution: a row vector left fixed by the matrix. + /// + /// Solved as a linear system rather than found by iteration, so a + /// periodic chain -- where the powers of the matrix never converge -- + /// still gives its stationary distribution. The system is `pi (P - I) = + /// 0` with the normalisation `sum pi = 1` substituted for one of the + /// redundant equations. + /// + /// Panics: + /// Panics if the linear system is singular, which happens only when the + /// matrix is not stochastic. + /// + /// Rust: `stochastic::markov::MarkovChain::stationary` + #[pyo3(name = "stationary")] + #[pyo3(signature = ())] + fn stationary<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.stationary())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Runs the chain, returning the states visited including the start. + /// + /// Panics: + /// Panics unless `start` is a valid state. + /// + /// Rust: `stochastic::markov::MarkovChain::simulate` + #[pyo3(name = "simulate")] + #[pyo3(signature = (start, steps, rng))] + fn simulate(&self, start: usize, steps: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| self.inner.simulate(start, steps, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Whether every state can reach every other. + /// + /// Rust: `stochastic::markov::MarkovChain::is_irreducible` + #[pyo3(name = "is_irreducible")] + #[pyo3(signature = ())] + fn is_irreducible(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_irreducible()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The period of a state: the greatest common divisor of the lengths of + /// the loops through it. + /// + /// One means aperiodic. A chain with a period above one cycles through + /// classes of states and its matrix powers never settle, which is why + /// aperiodicity is a hypothesis of every convergence theorem here. + /// + /// Panics: + /// Panics unless `state` is valid. + /// + /// Rust: `stochastic::markov::MarkovChain::period` + #[pyo3(name = "period")] + #[pyo3(signature = (state))] + fn period(&self, state: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.period(state)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Whether every state has period one. + /// + /// Rust: `stochastic::markov::MarkovChain::is_aperiodic` + #[pyo3(name = "is_aperiodic")] + #[pyo3(signature = ())] + fn is_aperiodic(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_aperiodic()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Each state's long-run behaviour. + /// + /// A state is recurrent when everything it can reach can reach it back, + /// and transient otherwise; it is absorbing when it goes nowhere else. + /// + /// Rust: `stochastic::markov::MarkovChain::classify_states` + #[pyo3(name = "classify_states")] + #[pyo3(signature = ())] + fn classify_states(&self) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.classify_states()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyStateClass::from_rust(&__x)).collect::>()) + } + + /// The probability of ending in each absorbing state, one row per + /// transient state. + /// + /// The fundamental matrix `N = (I - Q)^-1` counts expected visits to each + /// transient state before absorption -- its `(i, j)` entry is the sum + /// over path lengths of the chance of being at `j` at that step -- and + /// `N R` then routes those visits into the absorbing states. Columns + /// follow the order the absorbing states appear in. + /// + /// Panics: + /// Panics if the chain has no absorbing state, or if `I - Q` is singular, + /// which means some transient state cannot reach absorption. + /// + /// Rust: `stochastic::markov::MarkovChain::absorbing_probabilities` + #[pyo3(name = "absorbing_probabilities")] + #[pyo3(signature = ())] + fn absorbing_probabilities(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.absorbing_probabilities()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) + } + + /// The fundamental matrix `N = (I - Q)^-1` over the transient states. + /// + /// Panics: + /// Panics if `I - Q` is singular. + /// + /// Rust: `stochastic::markov::MarkovChain::fundamental_matrix` + #[pyo3(name = "fundamental_matrix")] + #[pyo3(signature = ())] + fn fundamental_matrix(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.fundamental_matrix()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) + } + + /// Expected steps to absorption from each state, zero for the absorbing + /// ones. + /// + /// The row sums of the fundamental matrix: total expected visits to all + /// transient states is total expected time before leaving them. + /// + /// Panics: + /// Panics if the chain has no absorbing state. + /// + /// Rust: `stochastic::markov::MarkovChain::expected_steps_to_absorption` + #[pyo3(name = "expected_steps_to_absorption")] + #[pyo3(signature = ())] + fn expected_steps_to_absorption<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.expected_steps_to_absorption())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The expected number of steps to first reach any state in `target`. + /// + /// Infinite when the target cannot be reached. Solved as the linear + /// system `h_i = 1 + sum_j p_ij h_j` over the states outside the target, + /// which is the first-step decomposition written down. + /// + /// Panics: + /// Panics unless the states are valid. + /// + /// Rust: `stochastic::markov::MarkovChain::hitting_time` + #[pyo3(name = "hitting_time")] + #[pyo3(signature = (from_, target))] + fn hitting_time<'py>(&self, py: Python<'py>, from_: usize, target: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.hitting_time(from_, &target))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The probability of ever reaching `target` from `from`. + /// + /// Panics: + /// Panics unless the states are valid. + /// + /// Rust: `stochastic::markov::MarkovChain::hitting_probability` + #[pyo3(name = "hitting_probability")] + #[pyo3(signature = (from_, target))] + fn hitting_probability<'py>(&self, py: Python<'py>, from_: usize, target: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.hitting_probability(from_, &target))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The expected number of steps to return to a state, starting from it. + /// + /// Kac's formula: the reciprocal of that state's stationary probability. + /// It is one of the most useful facts about a chain -- the long-run share + /// of time spent somewhere and the average wait between visits are + /// reciprocals of each other, with no further hypothesis than + /// irreducibility. + /// + /// Panics: + /// Panics unless `state` is valid. + /// + /// Rust: `stochastic::markov::MarkovChain::return_time` + #[pyo3(name = "return_time")] + #[pyo3(signature = (state))] + fn return_time(&self, state: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.return_time(state)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The mean first passage time from every state to every other. + /// + /// The diagonal holds the return times. + /// + /// Panics: + /// Panics if the chain has fewer than one state. + /// + /// Rust: `stochastic::markov::MarkovChain::mfpt_matrix` + #[pyo3(name = "mfpt_matrix")] + #[pyo3(signature = ())] + fn mfpt_matrix(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.mfpt_matrix()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) + } + + /// Total variation distance between two distributions: half the sum of + /// the absolute differences. + /// + /// The largest difference in probability the two assign to any event, + /// which is why it is the metric convergence is measured in. + /// + /// Panics: + /// Panics unless the two have the same length. + /// + /// Rust: `stochastic::markov::MarkovChain::total_variation_distance` + #[pyo3(name = "total_variation_distance")] + #[staticmethod] + #[pyo3(signature = (a, b))] + fn total_variation_distance<'py>(py: Python<'py>, a: Vec, b: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::markov::MarkovChain::total_variation_distance(&a, &b))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The number of steps until every start is within `eps` of stationary in + /// total variation. + /// + /// Infinite for a chain that does not converge -- one that is reducible or + /// periodic. + /// + /// Panics: + /// Panics unless `eps` is in `(0, 1)`. + /// + /// Rust: `stochastic::markov::MarkovChain::mixing_time` + #[pyo3(name = "mixing_time")] + #[pyo3(signature = (eps))] + fn mixing_time(&self, eps: f64) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.mixing_time(eps)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The spectral gap: one minus the second-largest eigenvalue modulus. + /// + /// What sets the rate of convergence, since the distance to stationary + /// falls like the second eigenvalue's magnitude raised to the step count. + /// Zero for a chain that does not converge. Computed here through the + /// symmetrised chain, so it is exact for reversible chains and a + /// reasonable proxy otherwise. + /// + /// Rust: `stochastic::markov::MarkovChain::spectral_gap` + #[pyo3(name = "spectral_gap")] + #[pyo3(signature = ())] + fn spectral_gap(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.spectral_gap()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Whether the chain satisfies detailed balance against `pi`. + /// + /// `pi_i p_ij = pi_j p_ji` for every pair: the flow between any two + /// states is the same in both directions. It is much stronger than + /// stationarity, which needs only that the total flow into each state + /// balances the total flow out, and it is what every Metropolis-Hastings + /// sampler arranges because it is far easier to arrange. + /// + /// Panics: + /// Panics unless `pi` has one entry per state. + /// + /// Rust: `stochastic::markov::MarkovChain::reversible_check` + #[pyo3(name = "reversible_check")] + #[pyo3(signature = (pi, tol))] + fn reversible_check<'py>(&self, py: Python<'py>, pi: Vec, tol: f64) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.reversible_check(&pi, tol))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The entropy rate: the average uncertainty per step in the long run, in + /// bits. + /// + /// The stationary-weighted average of each row's entropy. It is the + /// compression limit for a stream generated by the chain, and it is what + /// separates a chain from a memoryless source with the same marginal: + /// the marginal entropy is an upper bound and the difference is what the + /// dependence saves. + /// + /// Rust: `stochastic::markov::MarkovChain::entropy_rate` + #[pyo3(name = "entropy_rate")] + #[pyo3(signature = ())] + fn entropy_rate(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.entropy_rate()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// An exact sample from the stationary distribution, by coupling from the + /// past. + /// + /// Ordinary simulation gives a sample that is only approximately + /// stationary, with no way to tell how close. Propp and Wilson's + /// construction instead runs every possible start from further and + /// further back until they all coalesce by time zero; the common value is + /// then *exactly* stationary, because whatever the chain was doing + /// infinitely far back, it would have ended up there too. + /// + /// Panics: + /// Panics if coalescence does not occur, which for an irreducible + /// aperiodic chain means only that the bound was too small. + /// + /// Rust: `stochastic::markov::MarkovChain::coupling_from_the_past_small` + #[pyo3(name = "coupling_from_the_past_small")] + #[pyo3(signature = (rng))] + fn coupling_from_the_past_small(&self, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult { + let mut rng = rng; + let __r = crate::runtime::guard(|| self.inner.coupling_from_the_past_small(&mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The PageRank chain of a graph: follow a random out-edge with + /// probability `damping`, and teleport to a uniform vertex otherwise. + /// + /// The teleportation is what makes the chain irreducible and aperiodic + /// whatever the graph looks like, so a stationary distribution exists and + /// is unique. A vertex with no out-edges teleports always, which spreads + /// its mass rather than letting it vanish. + /// + /// Panics: + /// Panics unless the graph is non-empty and `damping` is in `[0, 1]`. + /// + /// Rust: `stochastic::markov::MarkovChain::pagerank_chain` + #[pyo3(name = "pagerank_chain")] + #[staticmethod] + #[pyo3(signature = (g, damping))] + fn pagerank_chain(g: crate::generated::types::PyGraph, damping: f64) -> PyResult { + let g = g.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::markov::MarkovChain::pagerank_chain(&g, damping)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyMarkovChain { inner: __v }) + } + + #[getter] + #[pyo3(name = "p")] + fn py_get_p(&self) -> PyResult { Ok(crate::generated::types::PyMatrix { inner: self.inner.p.clone() }) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("MarkovChain", "MarkovChain", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Samplers that build a chain whose stationary distribution is a target you +/// can evaluate but not sample from directly. +/// +/// Every method here takes the *log* of the target, unnormalised. Logs +/// because the density of anything interesting underflows; unnormalised +/// because the normalising constant is exactly the thing that is usually +/// impossible to compute, and none of these methods needs it -- they see the +/// target only through ratios, in which it cancels. +/// +/// Rust: `stochastic::markov::Mcmc` +#[pyclass(name = "Mcmc", module = "numeria.stochastic.markov", from_py_object)] +#[derive(Clone)] +pub struct PyMcmc { pub inner: rust_physics_engine::stochastic::markov::Mcmc } +#[pymethods] +impl PyMcmc { + /// Metropolis-Hastings with a symmetric Gaussian proposal. + /// + /// Propose a move, accept it outright if it goes uphill, and accept it + /// with probability equal to the density ratio if it goes down. That rule + /// makes detailed balance hold against the target, so the target is + /// stationary; the downhill moves are not a concession but the mechanism, + /// since a sampler that only climbed would sit at the mode forever. + /// + /// Returns the chain after discarding `burn` samples. + /// + /// Panics: + /// Panics on an empty start, a non-positive proposal width, or a burn-in + /// at or beyond the requested length. + /// + /// Rust: `stochastic::markov::Mcmc::metropolis_hastings` + #[pyo3(name = "metropolis_hastings")] + #[staticmethod] + #[pyo3(signature = (log_target, x0, proposal_std, n, burn, rng))] + fn metropolis_hastings(log_target: pyo3::Py, x0: Vec, proposal_std: f64, n: usize, burn: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult>> { + let __cb_log_target = std::rc::Rc::new(crate::runtime::Callback::new(log_target)); + let log_target = { let __cb = __cb_log_target.clone(); move |__a0: &[f64]| -> f64 { __cb.call::<_, f64>((__a0.to_vec(),), f64::NAN) } }; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::markov::Mcmc::metropolis_hastings(&log_target, &x0, proposal_std, n, burn, &mut rng.inner)); + crate::runtime::callback::check(&[&__cb_log_target], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Metropolis-Hastings that tunes its own proposal width towards an + /// acceptance rate of about a quarter. + /// + /// Too wide a proposal is rejected constantly and the chain stands still; + /// too narrow a one is always accepted and the chain crawls. The optimum + /// for a high-dimensional Gaussian target is famously near 0.234, and + /// adapting towards it costs nothing. Adaptation stops at the end of + /// burn-in, because a proposal that keeps changing breaks the Markov + /// property and the chain is no longer guaranteed to have the right + /// stationary distribution. + /// + /// Panics: + /// Panics under the same conditions as + /// `metropolis_hastings`. + /// + /// Rust: `stochastic::markov::Mcmc::adaptive_metropolis` + #[pyo3(name = "adaptive_metropolis")] + #[staticmethod] + #[pyo3(signature = (log_target, x0, proposal_std, n, burn, rng))] + fn adaptive_metropolis(log_target: pyo3::Py, x0: Vec, proposal_std: f64, n: usize, burn: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult>> { + let __cb_log_target = std::rc::Rc::new(crate::runtime::Callback::new(log_target)); + let log_target = { let __cb = __cb_log_target.clone(); move |__a0: &[f64]| -> f64 { __cb.call::<_, f64>((__a0.to_vec(),), f64::NAN) } }; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::markov::Mcmc::adaptive_metropolis(&log_target, &x0, proposal_std, n, burn, &mut rng.inner)); + crate::runtime::callback::check(&[&__cb_log_target], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Hamiltonian Monte Carlo: give the point a momentum and follow the + /// resulting trajectory. + /// + /// Treat the negative log density as a potential energy, draw a random + /// momentum, and integrate the equations of motion. The trajectory + /// conserves energy, so a proposal at the far end is accepted with + /// probability near one however far it has travelled -- which is what + /// lets the chain cross the whole distribution in one move instead of + /// diffusing across it. The leapfrog integrator is used because it is + /// *symplectic*: its error does not accumulate, so energy stays nearly + /// conserved over long trajectories, and it is reversible, which the + /// acceptance rule requires. + /// + /// Panics: + /// Panics on an empty start, a non-positive step, no leapfrog steps, or a + /// burn-in at or beyond the run. + /// + /// Rust: `stochastic::markov::Mcmc::hamiltonian_mc` + #[pyo3(name = "hamiltonian_mc")] + #[staticmethod] + #[pyo3(signature = (log_target, grad, x0, step, n_leapfrog, n, burn, rng))] + fn hamiltonian_mc(log_target: pyo3::Py, grad: pyo3::Py, x0: Vec, step: f64, n_leapfrog: usize, n: usize, burn: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult>> { + let __cb_log_target = std::rc::Rc::new(crate::runtime::Callback::new(log_target)); + let log_target = { let __cb = __cb_log_target.clone(); move |__a0: &[f64]| -> f64 { __cb.call::<_, f64>((__a0.to_vec(),), f64::NAN) } }; + let __cb_grad = std::rc::Rc::new(crate::runtime::Callback::new(grad)); + let grad = { let __cb = __cb_grad.clone(); move |__a0: &[f64]| -> Vec { __cb.call::<_, Vec>((__a0.to_vec(),), Vec::new()) } }; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::markov::Mcmc::hamiltonian_mc(&log_target, &grad, &x0, step, n_leapfrog, n, burn, &mut rng.inner)); + crate::runtime::callback::check(&[&__cb_log_target, &__cb_grad], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The no-U-turn sampler: Hamiltonian trajectories whose length the + /// algorithm chooses by watching for the path to double back. + /// + /// Hoffman and Gelman's naive scheme. The trajectory is grown by + /// repeated doubling, forwards or backwards at random, and stops when the + /// two ends of *any* sub-trajectory start approaching each other; the + /// next state is drawn uniformly from the states the slice variable + /// admits. The doubling and the sub-tree stopping check are not + /// decoration -- simply running until the path turns and taking the last + /// point is not reversible, and gives the wrong stationary distribution. + /// Getting that right removes trajectory length from the list of things a + /// user must tune, which was the practical obstacle to Hamiltonian + /// methods. + /// + /// Panics: + /// Panics on an empty start, a non-positive step, a zero depth, or a + /// burn-in at or beyond the run. + /// + /// Rust: `stochastic::markov::Mcmc::nuts_lite` + #[pyo3(name = "nuts_lite")] + #[staticmethod] + #[pyo3(signature = (log_target, grad, x0, step, max_depth, n, burn, rng))] + fn nuts_lite(log_target: pyo3::Py, grad: pyo3::Py, x0: Vec, step: f64, max_depth: usize, n: usize, burn: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult>> { + let __cb_log_target = std::rc::Rc::new(crate::runtime::Callback::new(log_target)); + let log_target = { let __cb = __cb_log_target.clone(); move |__a0: &[f64]| -> f64 { __cb.call::<_, f64>((__a0.to_vec(),), f64::NAN) } }; + let __cb_grad = std::rc::Rc::new(crate::runtime::Callback::new(grad)); + let grad = { let __cb = __cb_grad.clone(); move |__a0: &[f64]| -> Vec { __cb.call::<_, Vec>((__a0.to_vec(),), Vec::new()) } }; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::markov::Mcmc::nuts_lite(&log_target, &grad, &x0, step, max_depth, n, burn, &mut rng.inner)); + crate::runtime::callback::check(&[&__cb_log_target, &__cb_grad], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Slice sampling in one dimension. + /// + /// Draw a height uniformly below the density, then draw a point uniformly + /// from the slice at that height. Every move is accepted and there is no + /// proposal width to tune -- the stepping-out procedure finds the slice's + /// extent on its own, so `w` only affects speed and not correctness. + /// + /// Panics: + /// Panics on a non-positive width. + /// + /// Rust: `stochastic::markov::Mcmc::slice_sampler` + #[pyo3(name = "slice_sampler")] + #[staticmethod] + #[pyo3(signature = (log_target_1d, x0, w, n, rng))] + fn slice_sampler(log_target_1d: pyo3::Py, x0: f64, w: f64, n: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let __cb_log_target_1d = std::rc::Rc::new(crate::runtime::Callback::new(log_target_1d)); + let log_target_1d = { let __cb = __cb_log_target_1d.clone(); move |__a0: f64| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::markov::Mcmc::slice_sampler(&log_target_1d, x0, w, n, &mut rng.inner)); + crate::runtime::callback::check(&[&__cb_log_target_1d], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Parallel tempering: run several chains at different temperatures and + /// let them swap. + /// + /// A hot chain sees a flattened version of the target and crosses between + /// modes easily; a cold chain samples the target itself but can be + /// trapped. Swapping states between neighbouring temperatures, with an + /// acceptance rule that preserves each chain's own stationary + /// distribution, lets the cold chain inherit the hot one's mobility. + /// Returns the samples from the coldest chain. + /// + /// Panics: + /// Panics unless the temperatures are positive with the first equal to + /// one, and the burn-in is shorter than the run. + /// + /// Rust: `stochastic::markov::Mcmc::parallel_tempering` + #[pyo3(name = "parallel_tempering")] + #[staticmethod] + #[pyo3(signature = (log_target, temps, x0, proposal_std, n, burn, rng))] + fn parallel_tempering(log_target: pyo3::Py, temps: Vec, x0: Vec, proposal_std: f64, n: usize, burn: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult>> { + let __cb_log_target = std::rc::Rc::new(crate::runtime::Callback::new(log_target)); + let log_target = { let __cb = __cb_log_target.clone(); move |__a0: &[f64]| -> f64 { __cb.call::<_, f64>((__a0.to_vec(),), f64::NAN) } }; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::markov::Mcmc::parallel_tempering(&log_target, &temps, &x0, proposal_std, n, burn, &mut rng.inner)); + crate::runtime::callback::check(&[&__cb_log_target], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The autocorrelation time of a chain: one plus twice the sum of the + /// autocorrelations, truncated where they first turn negative. + /// + /// How many steps the chain takes to forget where it was. The truncation + /// is Geyer's initial positive sequence rule: past that point the + /// estimates are dominated by noise, and summing them adds variance + /// rather than information. + /// + /// Rust: `stochastic::markov::Mcmc::autocorrelation_time` + #[pyo3(name = "autocorrelation_time")] + #[staticmethod] + #[pyo3(signature = (chain))] + fn autocorrelation_time<'py>(py: Python<'py>, chain: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::markov::Mcmc::autocorrelation_time(&chain))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The effective sample size: the number of independent draws a + /// correlated chain is worth. + /// + /// The run length divided by the autocorrelation time. Always at most the + /// run length, and usually far less -- a Metropolis chain with a + /// well-tuned proposal might be worth a tenth of its length, which is the + /// honest denominator for any Monte Carlo error estimate. + /// + /// Rust: `stochastic::markov::Mcmc::effective_sample_size` + #[pyo3(name = "effective_sample_size")] + #[staticmethod] + #[pyo3(signature = (chain))] + fn effective_sample_size<'py>(py: Python<'py>, chain: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::markov::Mcmc::effective_sample_size(&chain))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The Gelman-Rubin statistic: the ratio of the pooled variance estimate + /// to the within-chain one. + /// + /// Several chains from different starts should, once converged, look like + /// draws from the same distribution -- so the spread between chains + /// should match the spread within them and the ratio should approach one. + /// A value well above one is the clearest evidence available that a run + /// has not converged. It cannot prove that one has. + /// + /// Panics: + /// Panics unless there are at least two chains of at least two samples + /// each, all the same length. + /// + /// Rust: `stochastic::markov::Mcmc::gelman_rubin` + #[pyo3(name = "gelman_rubin")] + #[staticmethod] + #[pyo3(signature = (chains))] + fn gelman_rubin<'py>(py: Python<'py>, chains: Vec>) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::stochastic::markov::Mcmc::gelman_rubin(&chains))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Simulated annealing: Metropolis on an energy, with the temperature + /// falling on a schedule. + /// + /// At a high temperature almost every move is accepted and the search + /// wanders; as the temperature falls it becomes a hill descent. Returns + /// the best point found and its energy -- the best, not the last, because + /// the walk can and does step away from an optimum it has found. + /// + /// Panics: + /// Panics on an empty start. + /// + /// Rust: `stochastic::markov::Mcmc::simulated_annealing` + #[pyo3(name = "simulated_annealing")] + #[staticmethod] + #[pyo3(signature = (energy, x0, schedule, n, rng))] + fn simulated_annealing(energy: pyo3::Py, x0: Vec, schedule: pyo3::Py, n: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult<(Vec, f64)> { + let __cb_energy = std::rc::Rc::new(crate::runtime::Callback::new(energy)); + let energy = { let __cb = __cb_energy.clone(); move |__a0: &[f64]| -> f64 { __cb.call::<_, f64>((__a0.to_vec(),), f64::NAN) } }; + let __cb_schedule = std::rc::Rc::new(crate::runtime::Callback::new(schedule)); + let schedule = { let __cb = __cb_schedule.clone(); move |__a0: usize| -> f64 { __cb.call::<_, f64>((__a0,), f64::NAN) } }; + let mut rng = rng; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::markov::Mcmc::simulated_annealing(&energy, &x0, &schedule, n, &mut rng.inner)); + crate::runtime::callback::check(&[&__cb_energy, &__cb_schedule], ())?; + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) + } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Mcmc", "Mcmc", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// How a state behaves in the long run. +/// +/// Rust: `stochastic::markov::StateClass` +#[pyclass(name = "StateClass", module = "numeria.stochastic.markov", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyStateClass { + Transient, + Recurrent, + Absorbing, +} +impl PyStateClass { + pub fn to_rust(&self) -> rust_physics_engine::stochastic::markov::StateClass { match self { + Self::Transient => rust_physics_engine::stochastic::markov::StateClass::Transient, + Self::Recurrent => rust_physics_engine::stochastic::markov::StateClass::Recurrent, + Self::Absorbing => rust_physics_engine::stochastic::markov::StateClass::Absorbing, + } } + pub fn from_rust(v: &rust_physics_engine::stochastic::markov::StateClass) -> Self { match v { + rust_physics_engine::stochastic::markov::StateClass::Transient => Self::Transient, + rust_physics_engine::stochastic::markov::StateClass::Recurrent => Self::Recurrent, + rust_physics_engine::stochastic::markov::StateClass::Absorbing => Self::Absorbing, + } } +} +#[pymethods] +impl PyStateClass { + fn __repr__(&self) -> &'static str { + match self { + Self::Transient => "StateClass.Transient", + Self::Recurrent => "StateClass.Recurrent", + Self::Absorbing => "StateClass.Absorbing", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A continuous-time Markov chain, held as its generator matrix. +/// +/// Rows of `q` sum to zero: the off-diagonal entries are transition rates and +/// the diagonal is minus their total, so `-q_ii` is the rate of leaving state +/// `i`. Where a discrete chain asks "what is the next state", a generator +/// asks "how long until something happens, and what". +/// +/// Rust: `stochastic::queueing::Ctmc` +#[pyclass(name = "Ctmc", module = "numeria.stochastic.queueing", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyCtmc { pub inner: rust_physics_engine::stochastic::queueing::Ctmc } +#[pymethods] +impl PyCtmc { + /// Wraps a generator after checking its shape. + /// + /// Errors: + /// Returns `GeomError::InvalidArgument` if the matrix is not square, + /// has a negative off-diagonal rate, or has a row that does not sum to + /// zero within `1e-9`. + /// + /// Rust: `stochastic::queueing::Ctmc::new` + #[new] + #[pyo3(signature = (q))] + fn __new__(q: crate::generated::types::PyMatrixArg) -> PyResult { + let q = q.0; + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::queueing::Ctmc::new(q)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyCtmc { inner: __v }) + } + + /// Number of states. + /// + /// Rust: `stochastic::queueing::Ctmc::n` + #[pyo3(name = "n")] + #[pyo3(signature = ())] + fn n(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.n()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Mean time spent in each state per visit, `1 / (-q_ii)`. + /// + /// Infinite for an absorbing state, which is never left. + /// + /// Rust: `stochastic::queueing::Ctmc::mean_holding_times` + #[pyo3(name = "mean_holding_times")] + #[pyo3(signature = ())] + fn mean_holding_times<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.mean_holding_times())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The jump chain: where the process goes, ignoring how long it waits. + /// + /// `P_ij = q_ij / (-q_ii)`. An absorbing state becomes a self-loop so the + /// result is a valid stochastic matrix. + /// + /// Errors: + /// Returns an error if the resulting matrix is rejected as a chain. + /// + /// Rust: `stochastic::queueing::Ctmc::embedded_chain` + #[pyo3(name = "embedded_chain")] + #[pyo3(signature = ())] + fn embedded_chain(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.embedded_chain()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyMarkovChain { inner: __v }) + } + + /// The stationary distribution, solving `pi Q = 0` with `sum pi = 1`. + /// + /// Solved as a linear system rather than by iterating, so periodicity in + /// the jump chain is irrelevant -- a continuous-time chain has no period + /// to speak of, and the linear solve reflects that. + /// + /// Errors: + /// Returns `GeomError::Degenerate` if the balance equations are + /// singular, which happens when the chain is reducible. + /// + /// Rust: `stochastic::queueing::Ctmc::stationary` + #[pyo3(name = "stationary")] + #[pyo3(signature = ())] + fn stationary<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.stationary())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) + } + + /// Simulates one trajectory by the Gillespie construction: hold in the + /// current state for an exponential time set by its exit rate, then jump + /// according to the embedded chain. + /// + /// Returns `(time of entry, state)` pairs, beginning at `(0, start)`. + /// Stops early at an absorbing state. + /// + /// Panics: + /// Panics if `start` is out of range or `t_end` is not positive. + /// + /// Rust: `stochastic::queueing::Ctmc::simulate` + #[pyo3(name = "simulate")] + #[pyo3(signature = (start, t_end, rng))] + fn simulate(&self, start: usize, t_end: f64, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| self.inner.simulate(start, t_end, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| (__x.0, __x.1)).collect::>()) + } + + /// Mean time to reach `to` starting from `from`. + /// + /// Solves `m_i = 1/(-q_ii) + sum_{j != to} P_ij m_j` over the jump chain, + /// which is the continuous-time analogue of a first-step decomposition: + /// wait out the holding time, then start again from wherever you land. + /// + /// Errors: + /// Returns an error if the system is singular, which means `to` is not + /// reachable from every transient state. + /// + /// Rust: `stochastic::queueing::Ctmc::first_passage` + #[pyo3(name = "first_passage")] + #[pyo3(signature = (from_, to))] + fn first_passage(&self, from_: usize, to: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.first_passage(from_, to)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "q")] + fn py_get_q(&self) -> PyResult { Ok(crate::generated::types::PyMatrix { inner: self.inner.q.clone() }) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Ctmc", "Ctmc", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// The standard summary of a queue in steady state. +/// +/// `l` and `lq` count customers, `w` and `wq` measure time. The two pairs are +/// linked by Little's law at the *effective* arrival rate, which differs from +/// the offered rate whenever the system turns customers away. +/// +/// Rust: `stochastic::queueing::QueueMetrics` +#[pyclass(name = "QueueMetrics", module = "numeria.stochastic.queueing", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyQueueMetrics { pub inner: rust_physics_engine::stochastic::queueing::QueueMetrics } +#[pymethods] +impl PyQueueMetrics { + /// Builds a `QueueMetrics` from its fields. + #[new] + #[pyo3(signature = (rho, l, lq, w, wq, p0, lambda_eff, model))] + fn __new__(rho: f64, l: f64, lq: f64, w: f64, wq: f64, p0: f64, lambda_eff: f64, model: crate::generated::types::PyQueueModel) -> Self { + let model = model.inner; + Self { inner: rust_physics_engine::stochastic::queueing::QueueMetrics { rho: rho, l: l, lq: lq, w: w, wq: wq, p0: p0, lambda_eff: lambda_eff, model: model } } + } + + /// Stationary probability of exactly `n` customers in the system. + /// + /// Returns NaN for `QueueModel::MeanValueOnly`, where only the means + /// are determined by the inputs. + /// + /// Rust: `stochastic::queueing::QueueMetrics::pn` + #[pyo3(name = "pn")] + #[pyo3(signature = (n))] + fn pn(&self, n: usize) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.pn(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "rho")] + fn py_get_rho(&self) -> PyResult { Ok(self.inner.rho) } + + #[setter] + #[pyo3(name = "rho")] + fn py_set_rho(&mut self, v: f64) { self.inner.rho = v; } + + #[getter] + #[pyo3(name = "l")] + fn py_get_l(&self) -> PyResult { Ok(self.inner.l) } + + #[setter] + #[pyo3(name = "l")] + fn py_set_l(&mut self, v: f64) { self.inner.l = v; } + + #[getter] + #[pyo3(name = "lq")] + fn py_get_lq(&self) -> PyResult { Ok(self.inner.lq) } + + #[setter] + #[pyo3(name = "lq")] + fn py_set_lq(&mut self, v: f64) { self.inner.lq = v; } + + #[getter] + #[pyo3(name = "w")] + fn py_get_w(&self) -> PyResult { Ok(self.inner.w) } + + #[setter] + #[pyo3(name = "w")] + fn py_set_w(&mut self, v: f64) { self.inner.w = v; } + + #[getter] + #[pyo3(name = "wq")] + fn py_get_wq(&self) -> PyResult { Ok(self.inner.wq) } + + #[setter] + #[pyo3(name = "wq")] + fn py_set_wq(&mut self, v: f64) { self.inner.wq = v; } + + #[getter] + #[pyo3(name = "p0")] + fn py_get_p0(&self) -> PyResult { Ok(self.inner.p0) } + + #[setter] + #[pyo3(name = "p0")] + fn py_set_p0(&mut self, v: f64) { self.inner.p0 = v; } + + #[getter] + #[pyo3(name = "lambda_eff")] + fn py_get_lambda_eff(&self) -> PyResult { Ok(self.inner.lambda_eff) } + + #[setter] + #[pyo3(name = "lambda_eff")] + fn py_set_lambda_eff(&mut self, v: f64) { self.inner.lambda_eff = v; } + + #[getter] + #[pyo3(name = "model")] + fn py_get_model(&self) -> PyResult { Ok(crate::generated::types::PyQueueModel { inner: self.inner.model.clone() }) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("QueueMetrics", "QueueMetrics", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Which birth-death chain a set of metrics came from. +/// +/// Carried alongside the means so that `QueueMetrics::pn` can report the +/// exact stationary probability of `n` in the system. Models with no +/// product-form state distribution report `QueueModel::MeanValueOnly`. +/// +/// Rust: `stochastic::queueing::QueueModel` +#[pyclass(name = "QueueModel", module = "numeria.stochastic.queueing", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyQueueModel { pub inner: rust_physics_engine::stochastic::queueing::QueueModel } +#[pymethods] +impl PyQueueModel { + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("QueueModel", "QueueModel", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// What an event-driven run measured. +/// +/// The time averages come from integrating the sample path over a merged +/// list of arrival and departure events, independently of the customer +/// averages, so `l` and `lambda_eff * w` are two separate measurements of the +/// same quantity rather than one derived from the other. +/// +/// Rust: `stochastic::queueing::QueueSimResult` +#[pyclass(name = "QueueSimResult", module = "numeria.stochastic.queueing", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyQueueSimResult { pub inner: rust_physics_engine::stochastic::queueing::QueueSimResult } +#[pymethods] +impl PyQueueSimResult { + /// Builds a `QueueSimResult` from its fields. + #[new] + #[pyo3(signature = (l, lq, w, wq, rho, lambda_eff, served))] + fn __new__(l: f64, lq: f64, w: f64, wq: f64, rho: f64, lambda_eff: f64, served: usize) -> Self { + + Self { inner: rust_physics_engine::stochastic::queueing::QueueSimResult { l: l, lq: lq, w: w, wq: wq, rho: rho, lambda_eff: lambda_eff, served: served } } + } + + #[getter] + #[pyo3(name = "l")] + fn py_get_l(&self) -> PyResult { Ok(self.inner.l) } + + #[setter] + #[pyo3(name = "l")] + fn py_set_l(&mut self, v: f64) { self.inner.l = v; } + + #[getter] + #[pyo3(name = "lq")] + fn py_get_lq(&self) -> PyResult { Ok(self.inner.lq) } + + #[setter] + #[pyo3(name = "lq")] + fn py_set_lq(&mut self, v: f64) { self.inner.lq = v; } + + #[getter] + #[pyo3(name = "w")] + fn py_get_w(&self) -> PyResult { Ok(self.inner.w) } + + #[setter] + #[pyo3(name = "w")] + fn py_set_w(&mut self, v: f64) { self.inner.w = v; } + + #[getter] + #[pyo3(name = "wq")] + fn py_get_wq(&self) -> PyResult { Ok(self.inner.wq) } + + #[setter] + #[pyo3(name = "wq")] + fn py_set_wq(&mut self, v: f64) { self.inner.wq = v; } + + #[getter] + #[pyo3(name = "rho")] + fn py_get_rho(&self) -> PyResult { Ok(self.inner.rho) } + + #[setter] + #[pyo3(name = "rho")] + fn py_set_rho(&mut self, v: f64) { self.inner.rho = v; } + + #[getter] + #[pyo3(name = "lambda_eff")] + fn py_get_lambda_eff(&self) -> PyResult { Ok(self.inner.lambda_eff) } + + #[setter] + #[pyo3(name = "lambda_eff")] + fn py_set_lambda_eff(&mut self, v: f64) { self.inner.lambda_eff = v; } + + #[getter] + #[pyo3(name = "served")] + fn py_get_served(&self) -> PyResult { Ok(self.inner.served) } + + #[setter] + #[pyo3(name = "served")] + fn py_set_served(&mut self, v: usize) { self.inner.served = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("QueueSimResult", "QueueSimResult", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Parameters of the Heston stochastic volatility model. +/// +/// Rust: `stochastic::sde::HestonParams` +#[pyclass(name = "HestonParams", module = "numeria.stochastic.sde", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyHestonParams { pub inner: rust_physics_engine::stochastic::sde::HestonParams } +#[pymethods] +impl PyHestonParams { + /// Builds a `HestonParams` from its fields. + #[new] + #[pyo3(signature = (mu, kappa, theta, xi, rho))] + fn __new__(mu: f64, kappa: f64, theta: f64, xi: f64, rho: f64) -> Self { + + Self { inner: rust_physics_engine::stochastic::sde::HestonParams { mu: mu, kappa: kappa, theta: theta, xi: xi, rho: rho } } + } + + #[getter] + #[pyo3(name = "mu")] + fn py_get_mu(&self) -> PyResult { Ok(self.inner.mu) } + + #[setter] + #[pyo3(name = "mu")] + fn py_set_mu(&mut self, v: f64) { self.inner.mu = v; } + + #[getter] + #[pyo3(name = "kappa")] + fn py_get_kappa(&self) -> PyResult { Ok(self.inner.kappa) } + + #[setter] + #[pyo3(name = "kappa")] + fn py_set_kappa(&mut self, v: f64) { self.inner.kappa = v; } + + #[getter] + #[pyo3(name = "theta")] + fn py_get_theta(&self) -> PyResult { Ok(self.inner.theta) } + + #[setter] + #[pyo3(name = "theta")] + fn py_set_theta(&mut self, v: f64) { self.inner.theta = v; } + + #[getter] + #[pyo3(name = "xi")] + fn py_get_xi(&self) -> PyResult { Ok(self.inner.xi) } + + #[setter] + #[pyo3(name = "xi")] + fn py_set_xi(&mut self, v: f64) { self.inner.xi = v; } + + #[getter] + #[pyo3(name = "rho")] + fn py_get_rho(&self) -> PyResult { Ok(self.inner.rho) } + + #[setter] + #[pyo3(name = "rho")] + fn py_set_rho(&mut self, v: f64) { self.inner.rho = v; } + + fn __repr__(&self) -> String { format!("HestonParams(mu={:?}, kappa={:?}, theta={:?}, xi={:?}, rho={:?})", self.inner.mu, self.inner.kappa, self.inner.theta, self.inner.xi, self.inner.rho) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A `HestonParams` argument, or anything that can stand in for one. +pub struct PyHestonParamsArg(pub rust_physics_engine::stochastic::sde::HestonParams); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyHestonParamsArg { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result { + if let Ok(__w) = obj.extract::() { + return Ok(PyHestonParamsArg(__w.inner)); + } + let __v = crate::runtime::coerce::floats_exact(obj, 5, "HestonParams")?; + Ok(PyHestonParamsArg(rust_physics_engine::stochastic::sde::HestonParams { mu: __v[0], kappa: __v[1], theta: __v[2], xi: __v[3], rho: __v[4] })) + } +} + + +/// An ARIMA model: an `Arma` fitted to the `d`-th difference. +/// +/// Rust: `stochastic::timeseries::Arima` +#[pyclass(name = "Arima", module = "numeria.stochastic.timeseries", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyArima { pub inner: rust_physics_engine::stochastic::timeseries::Arima } +#[pymethods] +impl PyArima { + /// Differences `d` times, then fits an ARMA(`p`, `q`) by conditional sum + /// of squares. + /// + /// Errors: + /// Returns an error if the series is too short or the ARMA fit fails. + /// + /// Rust: `stochastic::timeseries::Arima::fit` + #[pyo3(name = "fit")] + #[staticmethod] + #[pyo3(signature = (x, p, d, q))] + fn fit(x: Vec, p: usize, d: usize, q: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::timeseries::Arima::fit(&x, p, d, q)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyArima { inner: __v }) + } + + /// `h`-step forecasts on the original scale, with standard errors. + /// + /// The ARMA part forecasts the differenced series; integrating those + /// forecasts back up is a cumulative sum, so the errors accumulate too -- + /// the standard error of the `h`-step forecast of an integrated series is + /// the norm of the *partial sums* of the psi weights, not of the weights + /// themselves. That is why an ARIMA forecast interval keeps widening + /// without bound while a stationary ARMA one levels off. + /// + /// Panics: + /// Panics if `h` is zero. + /// + /// Rust: `stochastic::timeseries::Arima::forecast` + #[pyo3(name = "forecast")] + #[pyo3(signature = (h))] + fn forecast(&self, h: usize) -> PyResult<(Vec, Vec)> { + let __r = crate::runtime::guard(|| self.inner.forecast(h)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) + } + + #[getter] + #[pyo3(name = "d")] + fn py_get_d(&self) -> PyResult { Ok(self.inner.d) } + + #[setter] + #[pyo3(name = "d")] + fn py_set_d(&mut self, v: usize) { self.inner.d = v; } + + #[getter] + #[pyo3(name = "arma")] + fn py_get_arma(&self) -> PyResult { Ok(crate::generated::types::PyArma { inner: self.inner.arma.clone() }) } + + #[getter] + #[pyo3(name = "initial")] + fn py_get_initial(&self) -> PyResult> { Ok(self.inner.initial.clone()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Arima", "Arima", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// An autoregressive moving-average model. +/// +/// The process is written around its mean: +/// `(x_t - mu) = sum_i phi_i (x_{t-i} - mu) + e_t + sum_j theta_j e_{t-j}`, +/// with `e_t` white noise of variance `sigma2`. The sign convention on the +/// moving-average side is the additive one, matching the Box-Jenkins form. +/// +/// Rust: `stochastic::timeseries::Arma` +#[pyclass(name = "Arma", module = "numeria.stochastic.timeseries", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyArma { pub inner: rust_physics_engine::stochastic::timeseries::Arma } +#[pymethods] +impl PyArma { + /// A model with the given coefficients. + /// + /// Rust: `stochastic::timeseries::Arma::new` + #[new] + #[pyo3(signature = (ar, ma, sigma2, mean))] + fn __new__(ar: Vec, ma: Vec, sigma2: f64, mean: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::timeseries::Arma::new(ar, ma, sigma2, mean)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyArma { inner: __v }) + } + + /// Autoregressive order. + /// + /// Rust: `stochastic::timeseries::Arma::p` + #[pyo3(name = "p")] + #[pyo3(signature = ())] + fn p(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.p()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Moving-average order. + /// + /// Rust: `stochastic::timeseries::Arma::q` + #[pyo3(name = "q")] + #[pyo3(signature = ())] + fn q(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.q()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The conditional innovations implied by the model and the data. + /// + /// Pre-sample observations are replaced by the mean and pre-sample + /// innovations by zero, which is the "conditional" in conditional sum of + /// squares. The first few residuals therefore carry that assumption, and + /// its influence dies out at the rate the moving-average part is + /// invertible. + /// + /// Rust: `stochastic::timeseries::Arma::residuals` + #[pyo3(name = "residuals")] + #[pyo3(signature = (x))] + fn residuals<'py>(&self, py: Python<'py>, x: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.residuals(&x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Fits by conditional sum of squares: choose the mean and the + /// coefficients that make the conditional innovations as small as + /// possible in the least-squares sense. + /// + /// The innovations *are* the residual vector, so this is a plain + /// nonlinear least-squares problem and Levenberg-Marquardt solves it + /// directly. `sigma2` is then the residual mean square on `n - k` degrees + /// of freedom. + /// + /// Errors: + /// Returns an error if the series is too short or the optimiser fails to + /// converge. + /// + /// Rust: `stochastic::timeseries::Arma::fit_css` + #[pyo3(name = "fit_css")] + #[staticmethod] + #[pyo3(signature = (x, p, q))] + fn fit_css(x: Vec, p: usize, q: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::timeseries::Arma::fit_css(&x, p, q)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyArma { inner: __v }) + } + + /// Fits by the Hannan-Rissanen two-stage procedure. + /// + /// A long autoregression approximates the innovations, and those + /// estimated innovations then enter a second regression as if they were + /// observed, turning a nonlinear problem into two linear ones. It costs + /// some efficiency against `Arma::fit_css` but needs no starting values + /// and no iteration, which makes it a good source of starting values. + /// + /// Errors: + /// Returns an error if the series is too short or either regression is + /// rank deficient. + /// + /// Rust: `stochastic::timeseries::Arma::fit_hannan_rissanen` + #[pyo3(name = "fit_hannan_rissanen")] + #[staticmethod] + #[pyo3(signature = (x, p, q))] + fn fit_hannan_rissanen(x: Vec, p: usize, q: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::timeseries::Arma::fit_hannan_rissanen(&x, p, q)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyArma { inner: __v }) + } + + /// Generates `n` observations, discarding a burn-in long enough for the + /// transient from the zero start to decay. + /// + /// Panics: + /// Panics if `n` is zero or `sigma2` is negative. + /// + /// Rust: `stochastic::timeseries::Arma::simulate` + #[pyo3(name = "simulate")] + #[pyo3(signature = (n, rng))] + fn simulate(&self, n: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| self.inner.simulate(n, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The impulse-response (psi) weights: the coefficients of the model's + /// infinite moving-average representation. + /// + /// `psi_0 = 1` and `psi_j = theta_j + sum_i phi_i psi_{j-i}`. These are + /// the single most useful derived quantity in the module -- forecast + /// error variances, the autocovariances, and the spectral density are all + /// expressible in them. + /// + /// Returns `n` weights, `psi_0` first. + /// + /// Panics: + /// Panics if `n` is zero. + /// + /// Rust: `stochastic::timeseries::Arma::impulse_response` + #[pyo3(name = "impulse_response")] + #[pyo3(signature = (n))] + fn impulse_response<'py>(&self, py: Python<'py>, n: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.impulse_response(n))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The spectral density at each supplied angular frequency. + /// + /// `f(w) = (sigma2 / 2 pi) |theta(e^{-iw})|^2 / |phi(e^{-iw})|^2`. It + /// integrates over `[-pi, pi]` to the process variance, which is the + /// frequency-domain statement of the same second-order structure the + /// autocovariances describe. + /// + /// Rust: `stochastic::timeseries::Arma::spectral_density` + #[pyo3(name = "spectral_density")] + #[pyo3(signature = (freqs))] + fn spectral_density<'py>(&self, py: Python<'py>, freqs: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.spectral_density(&freqs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// `(stationary, invertible)`. + /// + /// Stationarity asks that every root of `1 - phi_1 z - ... - phi_p z^p` + /// lie outside the unit circle; invertibility asks the same of + /// `1 + theta_1 z + ... + theta_q z^q`. An empty side is trivially both. + /// + /// A root exactly on the circle counts as failing, since the boundary is + /// where stationarity breaks down. + /// + /// Rust: `stochastic::timeseries::Arma::roots_check` + #[pyo3(name = "roots_check")] + #[pyo3(signature = ())] + fn roots_check(&self) -> PyResult<(bool, bool)> { + let __r = crate::runtime::guard(|| self.inner.roots_check()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) + } + + /// The conditional (Gaussian) log-likelihood of `x` under this model. + /// + /// Conditional because the pre-sample values are fixed rather than + /// integrated out; it is the quantity `Arma::fit_css` maximises, and the + /// one `Arma::aic` and `Arma::bic` penalise. + /// + /// Rust: `stochastic::timeseries::Arma::log_likelihood` + #[pyo3(name = "log_likelihood")] + #[pyo3(signature = (x))] + fn log_likelihood<'py>(&self, py: Python<'py>, x: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.log_likelihood(&x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Number of free parameters: the coefficients, the mean, and the + /// innovation variance. + /// + /// Rust: `stochastic::timeseries::Arma::n_params` + #[pyo3(name = "n_params")] + #[pyo3(signature = ())] + fn n_params(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.n_params()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Akaike's information criterion, `-2 ln L + 2k`. Lower is better. + /// + /// Rust: `stochastic::timeseries::Arma::aic` + #[pyo3(name = "aic")] + #[pyo3(signature = (x))] + fn aic<'py>(&self, py: Python<'py>, x: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.aic(&x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The Bayesian information criterion, `-2 ln L + k ln n`. Penalises + /// extra parameters harder than `Arma::aic` for any sample past + /// `n = e^2`, so it selects more parsimonious models. + /// + /// Rust: `stochastic::timeseries::Arma::bic` + #[pyo3(name = "bic")] + #[pyo3(signature = (x))] + fn bic<'py>(&self, py: Python<'py>, x: Vec) -> PyResult { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.bic(&x))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// `h`-step-ahead forecasts and their standard errors. + /// + /// Point forecasts run the model recursion forward with future + /// innovations set to their expectation of zero. The standard errors are + /// `sigma sqrt(sum_{j(&self, py: Python<'py>, x: Vec, h: usize) -> PyResult<(Vec, Vec)> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.forecast(&x, h))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok((__v.0, __v.1)) + } + + #[getter] + #[pyo3(name = "ar")] + fn py_get_ar(&self) -> PyResult> { Ok(self.inner.ar.clone()) } + + #[getter] + #[pyo3(name = "ma")] + fn py_get_ma(&self) -> PyResult> { Ok(self.inner.ma.clone()) } + + #[getter] + #[pyo3(name = "sigma2")] + fn py_get_sigma2(&self) -> PyResult { Ok(self.inner.sigma2) } + + #[setter] + #[pyo3(name = "sigma2")] + fn py_set_sigma2(&mut self, v: f64) { self.inner.sigma2 = v; } + + #[getter] + #[pyo3(name = "mean")] + fn py_get_mean(&self) -> PyResult { Ok(self.inner.mean) } + + #[setter] + #[pyo3(name = "mean")] + fn py_set_mean(&mut self, v: f64) { self.inner.mean = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Arma", "Arma", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A GARCH(1,1) volatility model: +/// `sigma_t^2 = omega + alpha r_{t-1}^2 + beta sigma_{t-1}^2`. +/// +/// The single most used model in the family, because two parameters are +/// enough to reproduce the two features that matter: volatility clusters, and +/// it mean-reverts. `alpha + beta` is the persistence, and the model is +/// stationary only while that sum is below one. +/// +/// Rust: `stochastic::timeseries::Garch11` +#[pyclass(name = "Garch11", module = "numeria.stochastic.timeseries", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyGarch11 { pub inner: rust_physics_engine::stochastic::timeseries::Garch11 } +#[pymethods] +impl PyGarch11 { + /// Builds a `Garch11` from its fields. + #[new] + #[pyo3(signature = (omega, alpha, beta))] + fn __new__(omega: f64, alpha: f64, beta: f64) -> Self { + + Self { inner: rust_physics_engine::stochastic::timeseries::Garch11 { omega: omega, alpha: alpha, beta: beta } } + } + + /// `alpha + beta`: how much of today's variance shock survives to + /// tomorrow. At 1 the process has a unit root in variance and no + /// unconditional variance exists. + /// + /// Rust: `stochastic::timeseries::Garch11::persistence` + #[pyo3(name = "persistence")] + #[pyo3(signature = ())] + fn persistence(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.persistence()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// `omega / (1 - alpha - beta)`, the level volatility reverts to. + /// + /// Infinite once persistence reaches one. + /// + /// Rust: `stochastic::timeseries::Garch11::unconditional_variance` + #[pyo3(name = "unconditional_variance")] + #[pyo3(signature = ())] + fn unconditional_variance(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.unconditional_variance()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// The filtered conditional variance for each return, seeded at the + /// unconditional variance where one exists and at the sample variance + /// otherwise. + /// + /// Panics: + /// Panics if `returns` is empty. + /// + /// Rust: `stochastic::timeseries::Garch11::conditional_variance` + #[pyo3(name = "conditional_variance")] + #[pyo3(signature = (returns))] + fn conditional_variance<'py>(&self, py: Python<'py>, returns: Vec) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.conditional_variance(&returns))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Fits by maximising the Gaussian quasi-likelihood over a Nelder-Mead + /// simplex, parameterised so the constraints hold by construction. + /// + /// `omega` is optimised on the log scale, keeping it positive, and + /// `(alpha, beta)` through a softmax-style map onto the simplex + /// `alpha, beta > 0`, `alpha + beta < 1`, which is where the model is + /// stationary. An unconstrained fit routinely wanders to a negative + /// variance, where the likelihood is not merely bad but undefined. + /// + /// Errors: + /// Returns an error for a series too short to identify three parameters. + /// + /// Rust: `stochastic::timeseries::Garch11::fit` + #[pyo3(name = "fit")] + #[staticmethod] + #[pyo3(signature = (returns))] + fn fit(returns: Vec) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::timeseries::Garch11::fit(&returns)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyGarch11 { inner: __v }) + } + + /// Simulates `n` returns with Gaussian innovations. + /// + /// Panics: + /// Panics if `n` is zero or the parameters are not non-negative. + /// + /// Rust: `stochastic::timeseries::Garch11::simulate` + #[pyo3(name = "simulate")] + #[pyo3(signature = (n, rng))] + fn simulate(&self, n: usize, rng: pyo3::PyRefMut<'_, crate::generated::types::PyRng>) -> PyResult> { + let mut rng = rng; + let __r = crate::runtime::guard(|| self.inner.simulate(n, &mut rng.inner)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Variance forecasts `1..=h` steps ahead from the end of `returns`. + /// + /// Each step pulls the forecast toward the unconditional variance at rate + /// `persistence`, so the sequence is monotone and converges there + /// geometrically. + /// + /// Panics: + /// Panics if `h` is zero or `returns` is empty. + /// + /// Rust: `stochastic::timeseries::Garch11::forecast_variance` + #[pyo3(name = "forecast_variance")] + #[pyo3(signature = (returns, h))] + fn forecast_variance<'py>(&self, py: Python<'py>, returns: Vec, h: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.forecast_variance(&returns, h))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "omega")] + fn py_get_omega(&self) -> PyResult { Ok(self.inner.omega) } + + #[setter] + #[pyo3(name = "omega")] + fn py_set_omega(&mut self, v: f64) { self.inner.omega = v; } + + #[getter] + #[pyo3(name = "alpha")] + fn py_get_alpha(&self) -> PyResult { Ok(self.inner.alpha) } + + #[setter] + #[pyo3(name = "alpha")] + fn py_set_alpha(&mut self, v: f64) { self.inner.alpha = v; } + + #[getter] + #[pyo3(name = "beta")] + fn py_get_beta(&self) -> PyResult { Ok(self.inner.beta) } + + #[setter] + #[pyo3(name = "beta")] + fn py_set_beta(&mut self, v: f64) { self.inner.beta = v; } + + fn __repr__(&self) -> String { format!("Garch11(omega={:?}, alpha={:?}, beta={:?})", self.inner.omega, self.inner.alpha, self.inner.beta) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A `Garch11` argument, or anything that can stand in for one. +pub struct PyGarch11Arg(pub rust_physics_engine::stochastic::timeseries::Garch11); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyGarch11Arg { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result { + if let Ok(__w) = obj.extract::() { + return Ok(PyGarch11Arg(__w.inner)); + } + let __v = crate::runtime::coerce::floats_exact(obj, 3, "Garch11")?; + Ok(PyGarch11Arg(rust_physics_engine::stochastic::timeseries::Garch11 { omega: __v[0], alpha: __v[1], beta: __v[2] })) + } +} + + +/// The smoothing state left behind by `holt_winters`, enough to continue +/// the recursion or to forecast forward. +/// +/// Rust: `stochastic::timeseries::HwState` +#[pyclass(name = "HwState", module = "numeria.stochastic.timeseries", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyHwState { pub inner: rust_physics_engine::stochastic::timeseries::HwState } +#[pymethods] +impl PyHwState { + /// Builds a `HwState` from its fields. + #[new] + #[pyo3(signature = (level, trend, seasonal, multiplicative))] + fn __new__(level: f64, trend: f64, seasonal: Vec, multiplicative: bool) -> Self { + + Self { inner: rust_physics_engine::stochastic::timeseries::HwState { level: level, trend: trend, seasonal: seasonal, multiplicative: multiplicative } } + } + + /// `h`-step forecasts continuing from this state. + /// + /// Panics: + /// Panics if `h` is zero or the seasonal vector is empty. + /// + /// Rust: `stochastic::timeseries::HwState::forecast` + #[pyo3(name = "forecast")] + #[pyo3(signature = (h))] + fn forecast<'py>(&self, py: Python<'py>, h: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.forecast(h))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "level")] + fn py_get_level(&self) -> PyResult { Ok(self.inner.level) } + + #[setter] + #[pyo3(name = "level")] + fn py_set_level(&mut self, v: f64) { self.inner.level = v; } + + #[getter] + #[pyo3(name = "trend")] + fn py_get_trend(&self) -> PyResult { Ok(self.inner.trend) } + + #[setter] + #[pyo3(name = "trend")] + fn py_set_trend(&mut self, v: f64) { self.inner.trend = v; } + + #[getter] + #[pyo3(name = "seasonal")] + fn py_get_seasonal(&self) -> PyResult> { Ok(self.inner.seasonal.clone()) } + + #[getter] + #[pyo3(name = "multiplicative")] + fn py_get_multiplicative(&self) -> PyResult { Ok(self.inner.multiplicative) } + + #[setter] + #[pyo3(name = "multiplicative")] + fn py_set_multiplicative(&mut self, v: bool) { self.inner.multiplicative = v; } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("HwState", "HwState", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A seasonal ARIMA model, `(p, d, q) x (P, D, Q)_s`. +/// +/// Fitted by applying the seasonal difference `D` times and the ordinary +/// difference `d` times, then estimating the non-seasonal and seasonal +/// polynomials on the doubly differenced series. The seasonal part is modelled +/// as an ARMA in lags that are multiples of `s`. +/// +/// Rust: `stochastic::timeseries::Sarima` +#[pyclass(name = "Sarima", module = "numeria.stochastic.timeseries", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PySarima { pub inner: rust_physics_engine::stochastic::timeseries::Sarima } +#[pymethods] +impl PySarima { + /// Fits a `(p, d, q) x (P, D, Q)_s` model. + /// + /// The combined autoregressive polynomial has non-zero coefficients at + /// lags `1..=p` and at `s, 2s, ...` up to `P s`; the moving-average side + /// likewise. Cross-product terms of the multiplicative form are omitted, + /// which makes this the additive rather than the strictly multiplicative + /// SARIMA -- the difference is second order and the additive form is what + /// conditional least squares can identify without a much longer series. + /// + /// Errors: + /// Returns an error if the series is too short after differencing or the + /// fit fails. + /// + /// Rust: `stochastic::timeseries::Sarima::fit` + #[pyo3(name = "fit")] + #[staticmethod] + #[pyo3(signature = (x, p, d, q, seasonal_p, seasonal_d, seasonal_q, s))] + fn fit(x: Vec, p: usize, d: usize, q: usize, seasonal_p: usize, seasonal_d: usize, seasonal_q: usize, s: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::timeseries::Sarima::fit(&x, p, d, q, seasonal_p, seasonal_d, seasonal_q, s)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PySarima { inner: __v }) + } + + /// `h`-step forecasts on the original scale. + /// + /// Panics: + /// Panics if `h` is zero. + /// + /// Rust: `stochastic::timeseries::Sarima::forecast` + #[pyo3(name = "forecast")] + #[pyo3(signature = (h))] + fn forecast<'py>(&self, py: Python<'py>, h: usize) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.forecast(h))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "d")] + fn py_get_d(&self) -> PyResult { Ok(self.inner.d) } + + #[setter] + #[pyo3(name = "d")] + fn py_set_d(&mut self, v: usize) { self.inner.d = v; } + + #[getter] + #[pyo3(name = "seasonal_d")] + fn py_get_seasonal_d(&self) -> PyResult { Ok(self.inner.seasonal_d) } + + #[setter] + #[pyo3(name = "seasonal_d")] + fn py_set_seasonal_d(&mut self, v: usize) { self.inner.seasonal_d = v; } + + #[getter] + #[pyo3(name = "s")] + fn py_get_s(&self) -> PyResult { Ok(self.inner.s) } + + #[setter] + #[pyo3(name = "s")] + fn py_set_s(&mut self, v: usize) { self.inner.s = v; } + + #[getter] + #[pyo3(name = "arma")] + fn py_get_arma(&self) -> PyResult { Ok(crate::generated::types::PyArma { inner: self.inner.arma.clone() }) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Sarima", "Sarima", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A vector autoregression: each series regressed on `p` lags of every series. +/// +/// Rust: `stochastic::timeseries::Var` +#[pyclass(name = "Var", module = "numeria.stochastic.timeseries", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyVar { pub inner: rust_physics_engine::stochastic::timeseries::Var } +#[pymethods] +impl PyVar { + /// Number of series. + /// + /// Rust: `stochastic::timeseries::Var::k` + #[pyo3(name = "k")] + #[pyo3(signature = ())] + fn k(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.k()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Lag order. + /// + /// Rust: `stochastic::timeseries::Var::p` + #[pyo3(name = "p")] + #[pyo3(signature = ())] + fn p(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.p()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Fits by equation-by-equation least squares. + /// + /// Every equation has the same right-hand side, so the seemingly + /// unrelated regression collapses to ordinary least squares run + /// separately -- there is nothing to gain from estimating them jointly. + /// + /// `data[t]` holds all series at time `t`. + /// + /// Errors: + /// Returns an error if the series are ragged, too short, or the design is + /// rank deficient. + /// + /// Rust: `stochastic::timeseries::Var::fit` + #[pyo3(name = "fit")] + #[staticmethod] + #[pyo3(signature = (data, p))] + fn fit(data: Vec>, p: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::stochastic::timeseries::Var::fit(&data, p)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyVar { inner: __v }) + } + + /// `h`-step forecasts, each row one time step. + /// + /// Errors: + /// Returns an error if `data` is too short or shaped wrongly. + /// + /// Rust: `stochastic::timeseries::Var::forecast` + #[pyo3(name = "forecast")] + #[pyo3(signature = (data, h))] + fn forecast<'py>(&self, py: Python<'py>, data: Vec>, h: usize) -> PyResult>> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.forecast(&data, h))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(__v) + } + + /// The moving-average (impulse-response) matrices `Psi_0 ..= Psi_{h}`. + /// + /// `Psi_0` is the identity and `Psi_m = sum_l A_l Psi_{m-l}`. Entry + /// `(r, c)` of `Psi_m` is the response of series `r` at horizon `m` to a + /// unit shock in series `c` now. + /// + /// Panics: + /// Panics if the coefficient matrices are not square and conformable. + /// + /// Rust: `stochastic::timeseries::Var::impulse_response` + #[pyo3(name = "impulse_response")] + #[pyo3(signature = (h))] + fn impulse_response(&self, h: usize) -> PyResult> { + let __r = crate::runtime::guard(|| self.inner.impulse_response(h)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| crate::generated::types::PyMatrix { inner: __x }).collect::>()) + } + + /// A matrix of Granger-causality p-values: entry `(i, j)` tests whether + /// series `j` helps predict series `i` given the rest of the system. + /// + /// The diagonal is set to 1: a series always predicts itself, so the + /// question is not meaningful there. + /// + /// Errors: + /// Returns an error if a restricted regression is degenerate. + /// + /// Rust: `stochastic::timeseries::Var::granger_matrix` + #[pyo3(name = "granger_matrix")] + #[pyo3(signature = (data))] + fn granger_matrix(&self, data: Vec>) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.granger_matrix(&data)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::map_geom)?; + Ok(crate::generated::types::PyMatrix { inner: __v }) + } + + #[getter] + #[pyo3(name = "coeffs")] + fn py_get_coeffs(&self) -> PyResult> { Ok(self.inner.coeffs.clone().into_iter().map(|__x| crate::generated::types::PyMatrix { inner: __x }).collect::>()) } + + #[getter] + #[pyo3(name = "intercept")] + fn py_get_intercept(&self) -> PyResult> { Ok(self.inner.intercept.clone()) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Var", "Var", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} diff --git a/bindings/python/src/generated/types/transforms.rs b/bindings/python/src/generated/types/transforms.rs new file mode 100644 index 0000000..4416610 --- /dev/null +++ b/bindings/python/src/generated/types/transforms.rs @@ -0,0 +1,400 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; + + +/// Boundary condition for `dct_poisson_1d`. +/// +/// Rust: `transforms::dct::Bc` +#[pyclass(name = "Bc", module = "numeria.transforms.dct", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyDctBc { + Dirichlet, + Neumann, +} +impl PyDctBc { + pub fn to_rust(&self) -> rust_physics_engine::transforms::dct::Bc { match self { + Self::Dirichlet => rust_physics_engine::transforms::dct::Bc::Dirichlet, + Self::Neumann => rust_physics_engine::transforms::dct::Bc::Neumann, + } } + pub fn from_rust(v: &rust_physics_engine::transforms::dct::Bc) -> Self { match v { + rust_physics_engine::transforms::dct::Bc::Dirichlet => Self::Dirichlet, + rust_physics_engine::transforms::dct::Bc::Neumann => Self::Neumann, + } } +} +#[pymethods] +impl PyDctBc { + fn __repr__(&self) -> &'static str { + match self { + Self::Dirichlet => "Bc.Dirichlet", + Self::Neumann => "Bc.Neumann", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Precomputed twiddle factors and bit-reversal permutation for repeated +/// power-of-two FFTs of one size. +/// +/// Rust: `transforms::fft::FftPlan` +#[pyclass(name = "FftPlan", module = "numeria.transforms.fft")] +pub struct PyFftPlan { pub inner: rust_physics_engine::transforms::fft::FftPlan } +#[pymethods] +impl PyFftPlan { + /// Build a plan for length n. + /// + /// Panics: + /// Panics unless n is a power of two. + /// + /// Rust: `transforms::fft::FftPlan::new` + #[new] + #[pyo3(signature = (n))] + fn __new__(n: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::transforms::fft::FftPlan::new(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyFftPlan { inner: __v }) + } + + /// Planned transform length. + /// + /// Rust: `transforms::fft::FftPlan::len` + #[pyo3(name = "len")] + #[pyo3(signature = ())] + fn len(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.len()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// True for the (degenerate) length-0 plan; present for clippy's + /// `len_without_is_empty` convention. + /// + /// Rust: `transforms::fft::FftPlan::is_empty` + #[pyo3(name = "is_empty")] + #[pyo3(signature = ())] + fn is_empty(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_empty()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// In-place forward FFT of the planned length. + /// + /// Panics: + /// Panics unless `x.len()` equals the planned length. + /// + /// Rust: `transforms::fft::FftPlan::execute` + #[pyo3(name = "execute")] + #[pyo3(signature = (x))] + fn execute<'py>(&self, x: pyo3::Bound<'py, pyo3::PyAny>) -> PyResult<()> { + let mut x__v: Vec = x.extract::>()?.into_iter().map(|__e| __e.0).collect(); + let __r = crate::runtime::guard(|| self.inner.execute(&mut x__v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + crate::runtime::coerce::write_back_objects(&x, x__v.into_iter().map(|__e| crate::runtime::coerce::Cx(__e)).collect::>())?; + Ok(()) + } + + /// In-place inverse FFT of the planned length (with 1/n scaling). + /// + /// Panics: + /// Panics unless `x.len()` equals the planned length. + /// + /// Rust: `transforms::fft::FftPlan::execute_inverse` + #[pyo3(name = "execute_inverse")] + #[pyo3(signature = (x))] + fn execute_inverse<'py>(&self, x: pyo3::Bound<'py, pyo3::PyAny>) -> PyResult<()> { + let mut x__v: Vec = x.extract::>()?.into_iter().map(|__e| __e.0).collect(); + let __r = crate::runtime::guard(|| self.inner.execute_inverse(&mut x__v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + crate::runtime::coerce::write_back_objects(&x, x__v.into_iter().map(|__e| crate::runtime::coerce::Cx(__e)).collect::>())?; + Ok(()) + } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// Filter kernels for filtered back-projection. +/// +/// Rust: `transforms::radon::FbpFilter` +#[pyclass(name = "FbpFilter", module = "numeria.transforms.radon", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyFbpFilter { + RamLak, + SheppLogan, + Cosine, + Hamming, + Hann, +} +impl PyFbpFilter { + pub fn to_rust(&self) -> rust_physics_engine::transforms::radon::FbpFilter { match self { + Self::RamLak => rust_physics_engine::transforms::radon::FbpFilter::RamLak, + Self::SheppLogan => rust_physics_engine::transforms::radon::FbpFilter::SheppLogan, + Self::Cosine => rust_physics_engine::transforms::radon::FbpFilter::Cosine, + Self::Hamming => rust_physics_engine::transforms::radon::FbpFilter::Hamming, + Self::Hann => rust_physics_engine::transforms::radon::FbpFilter::Hann, + } } + pub fn from_rust(v: &rust_physics_engine::transforms::radon::FbpFilter) -> Self { match v { + rust_physics_engine::transforms::radon::FbpFilter::RamLak => Self::RamLak, + rust_physics_engine::transforms::radon::FbpFilter::SheppLogan => Self::SheppLogan, + rust_physics_engine::transforms::radon::FbpFilter::Cosine => Self::Cosine, + rust_physics_engine::transforms::radon::FbpFilter::Hamming => Self::Hamming, + rust_physics_engine::transforms::radon::FbpFilter::Hann => Self::Hann, + } } +} +#[pymethods] +impl PyFbpFilter { + fn __repr__(&self) -> &'static str { + match self { + Self::RamLak => "FbpFilter.RamLak", + Self::SheppLogan => "FbpFilter.SheppLogan", + Self::Cosine => "FbpFilter.Cosine", + Self::Hamming => "FbpFilter.Hamming", + Self::Hann => "FbpFilter.Hann", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Short-time Fourier transform configuration: an analysis window, hop +/// size in samples, and FFT length (≥ window length; frames are +/// zero-padded up to it). +/// +/// Rust: `transforms::stft::Stft` +#[pyclass(name = "Stft", module = "numeria.transforms.stft")] +pub struct PyStft { pub inner: rust_physics_engine::transforms::stft::Stft } +#[pymethods] +impl PyStft { + /// Build an STFT plan. + /// + /// Panics: + /// Panics if the hop is zero, the window is empty, or longer than n_fft. + /// + /// Rust: `transforms::stft::Stft::new` + #[new] + #[pyo3(signature = (window, hop, n_fft))] + fn __new__(window: Vec, hop: usize, n_fft: usize) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::transforms::stft::Stft::new(window, hop, n_fft)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyStft { inner: __v }) + } + + /// Forward STFT: one spectrum of n_fft/2 + 1 bins per frame. Frames + /// start at k·hop and the last full frame ends within the signal. + /// + /// Rust: `transforms::stft::Stft::forward` + #[pyo3(name = "forward")] + #[pyo3(signature = (x))] + fn forward<'py>(&self, py: Python<'py>, x: Vec) -> PyResult>>> { + let __r = crate::runtime::guard(|| self.inner.forward(&x)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.into_iter().map(|__x| __x.into_iter().map(|__x| crate::runtime::coerce::complex_out(py, __x)).collect::>()).collect::>()) + } + + /// Inverse STFT by weighted overlap-add: each frame is inverse + /// transformed, windowed again, accumulated, and normalized by the + /// accumulated squared window. Exact wherever the window overlap + /// covers the signal (COLA-satisfying window/hop pairs). + /// + /// Rust: `transforms::stft::Stft::inverse` + #[pyo3(name = "inverse")] + #[pyo3(signature = (frames))] + fn inverse<'py>(&self, py: Python<'py>, frames: Vec>) -> PyResult> { + let frames = frames.into_iter().map(|__e| __e.into_iter().map(|__e| __e.0).collect::>()).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || self.inner.inverse(&frames))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Magnitude of each bin per frame. + /// + /// Rust: `transforms::stft::Stft::magnitude` + #[pyo3(name = "magnitude")] + #[staticmethod] + #[pyo3(signature = (frames))] + fn magnitude<'py>(py: Python<'py>, frames: Vec>) -> PyResult>> { + let frames = frames.into_iter().map(|__e| __e.into_iter().map(|__e| __e.0).collect::>()).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::stft::Stft::magnitude(&frames))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Power in dB (10 log₁₀|X|², floored at −300 dB). + /// + /// Rust: `transforms::stft::Stft::power_db` + #[pyo3(name = "power_db")] + #[staticmethod] + #[pyo3(signature = (frames))] + fn power_db<'py>(py: Python<'py>, frames: Vec>) -> PyResult>> { + let frames = frames.into_iter().map(|__e| __e.into_iter().map(|__e| __e.0).collect::>()).collect::>(); + let __r = py.detach(move || crate::runtime::guard(move || rust_physics_engine::transforms::stft::Stft::power_db(&frames))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Frame-center times (seconds) for a signal of `n_samples` at `fs`. + /// + /// Rust: `transforms::stft::Stft::times` + #[pyo3(name = "times")] + #[pyo3(signature = (n_samples, fs))] + fn times<'py>(&self, py: Python<'py>, n_samples: usize, fs: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.times(n_samples, fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Bin center frequencies (Hz) for sample rate `fs`. + /// + /// Rust: `transforms::stft::Stft::freqs` + #[pyo3(name = "freqs")] + #[pyo3(signature = (fs))] + fn freqs<'py>(&self, py: Python<'py>, fs: f64) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.freqs(fs))); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// True when the squared window overlap-adds to a constant at this + /// hop (perfect weighted-OLA reconstruction in the interior). + /// + /// Rust: `transforms::stft::Stft::is_cola` + #[pyo3(name = "is_cola")] + #[pyo3(signature = ())] + fn is_cola(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_cola()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + #[getter] + #[pyo3(name = "window")] + fn py_get_window(&self) -> PyResult> { Ok(self.inner.window.clone()) } + + #[getter] + #[pyo3(name = "hop")] + fn py_get_hop(&self) -> PyResult { Ok(self.inner.hop) } + + #[setter] + #[pyo3(name = "hop")] + fn py_set_hop(&mut self, v: usize) { self.inner.hop = v; } + + #[getter] + #[pyo3(name = "n_fft")] + fn py_get_n_fft(&self) -> PyResult { Ok(self.inner.n_fft) } + + #[setter] + #[pyo3(name = "n_fft")] + fn py_set_n_fft(&mut self, v: usize) { self.inner.n_fft = v; } + + fn __repr__(&self) -> String { "".to_string() } +} + +/// Mother wavelets for the CWT (Torrence & Compo definitions): +/// Morlet(ω₀), Mexican hat (DOG order 2), Paul(m), DOG(m). +/// +/// Rust: `transforms::wavelet::Mother` +#[pyclass(name = "Mother", module = "numeria.transforms.wavelet", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyMother { pub inner: rust_physics_engine::transforms::wavelet::Mother } +#[pymethods] +impl PyMother { + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Mother", "Mother", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Signal extension at the boundaries. +/// +/// Rust: `transforms::wavelet::PadMode` +#[pyclass(name = "PadMode", module = "numeria.transforms.wavelet", from_py_object, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum PyPadMode { + Zero, + Symmetric, + Periodic, + Reflect, +} +impl PyPadMode { + pub fn to_rust(&self) -> rust_physics_engine::transforms::wavelet::PadMode { match self { + Self::Zero => rust_physics_engine::transforms::wavelet::PadMode::Zero, + Self::Symmetric => rust_physics_engine::transforms::wavelet::PadMode::Symmetric, + Self::Periodic => rust_physics_engine::transforms::wavelet::PadMode::Periodic, + Self::Reflect => rust_physics_engine::transforms::wavelet::PadMode::Reflect, + } } + pub fn from_rust(v: &rust_physics_engine::transforms::wavelet::PadMode) -> Self { match v { + rust_physics_engine::transforms::wavelet::PadMode::Zero => Self::Zero, + rust_physics_engine::transforms::wavelet::PadMode::Symmetric => Self::Symmetric, + rust_physics_engine::transforms::wavelet::PadMode::Periodic => Self::Periodic, + rust_physics_engine::transforms::wavelet::PadMode::Reflect => Self::Reflect, + } } +} +#[pymethods] +impl PyPadMode { + fn __repr__(&self) -> &'static str { + match self { + Self::Zero => "PadMode.Zero", + Self::Symmetric => "PadMode.Symmetric", + Self::Periodic => "PadMode.Periodic", + Self::Reflect => "PadMode.Reflect", + } + } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Detail-coefficient thresholding rules for `wavelet_denoise`. +/// +/// Rust: `transforms::wavelet::Threshold` +#[pyclass(name = "Threshold", module = "numeria.transforms.wavelet", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyThreshold { pub inner: rust_physics_engine::transforms::wavelet::Threshold } +#[pymethods] +impl PyThreshold { + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Threshold", "Threshold", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// Wavelet families: Haar, Daubechies 1–20, symlets 2–20, coiflets 1–5, +/// and the biorthogonal spline family (bior p.q as in PyWavelets). +/// +/// Rust: `transforms::wavelet::Wavelet` +#[pyclass(name = "Wavelet", module = "numeria.transforms.wavelet", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyWavelet { pub inner: rust_physics_engine::transforms::wavelet::Wavelet } +#[pymethods] +impl PyWavelet { + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Wavelet", "Wavelet", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} diff --git a/bindings/python/src/generated/types/units.rs b/bindings/python/src/generated/types/units.rs new file mode 100644 index 0000000..99c7b08 --- /dev/null +++ b/bindings/python/src/generated/types/units.rs @@ -0,0 +1,774 @@ +// @generated by bindings/python/generate.py -- do not edit. +// +// Regenerate with: python3 bindings/python/generate.py + + +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(deprecated)] +#![allow(rustdoc::all)] +#![allow(unused_imports)] +#![allow(non_snake_case)] + + +use pyo3::prelude::*; + + +/// The seven SI base exponents. +/// +/// Rust: `units::quantity::Dim` +#[pyclass(name = "Dim", module = "numeria.units.quantity", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyDim { pub inner: rust_physics_engine::units::quantity::Dim } +#[pymethods] +impl PyDim { + /// Builds a dimension from its seven exponents. + /// + /// Rust: `units::quantity::Dim::new` + #[new] + #[pyo3(signature = (m, kg, s, a, k, mol, cd))] + fn __new__(m: i8, kg: i8, s: i8, a: i8, k: i8, mol: i8, cd: i8) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::quantity::Dim::new(m, kg, s, a, k, mol, cd)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyDim { inner: __v }) + } + + /// The exponents as an array, in the order metre, kilogram, second, + /// ampere, kelvin, mole, candela. + /// + /// Rust: `units::quantity::Dim::exponents` + #[pyo3(name = "exponents")] + #[pyo3(signature = ())] + fn exponents<'py>(&self, py: Python<'py>) -> PyResult> { + let __r = py.detach(move || crate::runtime::guard(move || self.inner.exponents())); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_vec()) + } + + /// Whether every exponent is zero. + /// + /// Rust: `units::quantity::Dim::is_dimensionless` + #[pyo3(name = "is_dimensionless")] + #[pyo3(signature = ())] + fn is_dimensionless(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.is_dimensionless()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v) + } + + /// Adds two exponent vectors, which is what multiplying does. + /// + /// Errors: + /// + /// `DimError::Overflow` if an exponent leaves `i8`. + /// + /// Rust: `units::quantity::Dim::mul` + #[pyo3(name = "mul")] + #[pyo3(signature = (other))] + fn mul(&self, other: crate::generated::types::PyDim) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.mul(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::errors::map_dim)?; + Ok(crate::generated::types::PyDim { inner: __v }) + } + + /// Subtracts two exponent vectors, which is what dividing does. + /// + /// Errors: + /// + /// As `Dim::mul`. + /// + /// Rust: `units::quantity::Dim::div` + #[pyo3(name = "div")] + #[pyo3(signature = (other))] + fn div(&self, other: crate::generated::types::PyDim) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.div(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::errors::map_dim)?; + Ok(crate::generated::types::PyDim { inner: __v }) + } + + /// Multiplies every exponent by `n`. + /// + /// Errors: + /// + /// As `Dim::mul`. + /// + /// Rust: `units::quantity::Dim::pow` + #[pyo3(name = "pow")] + #[pyo3(signature = (n))] + fn pow(&self, n: i8) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.pow(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::errors::map_dim)?; + Ok(crate::generated::types::PyDim { inner: __v }) + } + + /// Halves every exponent. + /// + /// Errors: + /// + /// `DimError::NotAPerfectRoot` unless every exponent is even. A + /// dimension with an odd exponent has no square root at all -- there + /// is no such thing as the square root of a metre -- so this is a + /// refusal rather than a rounding decision. + /// + /// Rust: `units::quantity::Dim::sqrt` + #[pyo3(name = "sqrt")] + #[pyo3(signature = ())] + fn sqrt(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.sqrt()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::errors::map_dim)?; + Ok(crate::generated::types::PyDim { inner: __v }) + } + + #[getter] + #[pyo3(name = "m")] + fn py_get_m(&self) -> PyResult { Ok(self.inner.m) } + + #[setter] + #[pyo3(name = "m")] + fn py_set_m(&mut self, v: i8) { self.inner.m = v; } + + #[getter] + #[pyo3(name = "kg")] + fn py_get_kg(&self) -> PyResult { Ok(self.inner.kg) } + + #[setter] + #[pyo3(name = "kg")] + fn py_set_kg(&mut self, v: i8) { self.inner.kg = v; } + + #[getter] + #[pyo3(name = "s")] + fn py_get_s(&self) -> PyResult { Ok(self.inner.s) } + + #[setter] + #[pyo3(name = "s")] + fn py_set_s(&mut self, v: i8) { self.inner.s = v; } + + #[getter] + #[pyo3(name = "a")] + fn py_get_a(&self) -> PyResult { Ok(self.inner.a) } + + #[setter] + #[pyo3(name = "a")] + fn py_set_a(&mut self, v: i8) { self.inner.a = v; } + + #[getter] + #[pyo3(name = "k")] + fn py_get_k(&self) -> PyResult { Ok(self.inner.k) } + + #[setter] + #[pyo3(name = "k")] + fn py_set_k(&mut self, v: i8) { self.inner.k = v; } + + #[getter] + #[pyo3(name = "mol")] + fn py_get_mol(&self) -> PyResult { Ok(self.inner.mol) } + + #[setter] + #[pyo3(name = "mol")] + fn py_set_mol(&mut self, v: i8) { self.inner.mol = v; } + + #[getter] + #[pyo3(name = "cd")] + fn py_get_cd(&self) -> PyResult { Ok(self.inner.cd) } + + #[setter] + #[pyo3(name = "cd")] + fn py_set_cd(&mut self, v: i8) { self.inner.cd = v; } + + #[classattr] + #[pyo3(name = "NONE")] + fn const_none() -> crate::generated::types::PyDim { crate::generated::types::PyDim { inner: rust_physics_engine::units::quantity::Dim::NONE } } + + #[classattr] + #[pyo3(name = "LENGTH")] + fn const_length() -> crate::generated::types::PyDim { crate::generated::types::PyDim { inner: rust_physics_engine::units::quantity::Dim::LENGTH } } + + #[classattr] + #[pyo3(name = "MASS")] + fn const_mass() -> crate::generated::types::PyDim { crate::generated::types::PyDim { inner: rust_physics_engine::units::quantity::Dim::MASS } } + + #[classattr] + #[pyo3(name = "TIME")] + fn const_time() -> crate::generated::types::PyDim { crate::generated::types::PyDim { inner: rust_physics_engine::units::quantity::Dim::TIME } } + + #[classattr] + #[pyo3(name = "CURRENT")] + fn const_current() -> crate::generated::types::PyDim { crate::generated::types::PyDim { inner: rust_physics_engine::units::quantity::Dim::CURRENT } } + + #[classattr] + #[pyo3(name = "TEMPERATURE")] + fn const_temperature() -> crate::generated::types::PyDim { crate::generated::types::PyDim { inner: rust_physics_engine::units::quantity::Dim::TEMPERATURE } } + + #[classattr] + #[pyo3(name = "AMOUNT")] + fn const_amount() -> crate::generated::types::PyDim { crate::generated::types::PyDim { inner: rust_physics_engine::units::quantity::Dim::AMOUNT } } + + #[classattr] + #[pyo3(name = "LUMINOUS")] + fn const_luminous() -> crate::generated::types::PyDim { crate::generated::types::PyDim { inner: rust_physics_engine::units::quantity::Dim::LUMINOUS } } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Dim", "Dim", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} + +/// A value together with its dimension. +/// +/// Rust: `units::quantity::Quantity` +#[pyclass(name = "Quantity", module = "numeria.units.quantity", from_py_object, eq)] +#[derive(Clone, PartialEq)] +pub struct PyQuantity { pub inner: rust_physics_engine::units::quantity::Quantity } +#[pymethods] +impl PyQuantity { + /// A pure number. + /// + /// Rust: `units::quantity::Quantity::number` + #[pyo3(name = "number")] + #[staticmethod] + #[pyo3(signature = (v))] + fn number(v: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::quantity::Quantity::number(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuantity { inner: __v }) + } + + /// A value with an explicit dimension, already in SI. + /// + /// Rust: `units::quantity::Quantity::new` + #[new] + #[pyo3(signature = (value, dim))] + fn __new__(value: f64, dim: crate::generated::types::PyDim) -> PyResult { + let dim = dim.inner; + let __r = crate::runtime::guard(|| rust_physics_engine::units::quantity::Quantity::new(value, dim)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuantity { inner: __v }) + } + + /// Adds two quantities. + /// + /// Errors: + /// + /// `DimError::Mismatch` if they do not measure the same thing. + /// + /// Rust: `units::quantity::Quantity::add` + #[pyo3(name = "add")] + #[pyo3(signature = (other))] + fn add(&self, other: crate::generated::types::PyQuantity) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.add(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::errors::map_dim)?; + Ok(crate::generated::types::PyQuantity { inner: __v }) + } + + /// Subtracts two quantities. + /// + /// Errors: + /// + /// As `Quantity::add`. + /// + /// Rust: `units::quantity::Quantity::sub` + #[pyo3(name = "sub")] + #[pyo3(signature = (other))] + fn sub(&self, other: crate::generated::types::PyQuantity) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.sub(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::errors::map_dim)?; + Ok(crate::generated::types::PyQuantity { inner: __v }) + } + + /// Multiplies two quantities, adding their exponents. + /// + /// Errors: + /// + /// `DimError::Overflow` if an exponent leaves `i8`. + /// + /// Rust: `units::quantity::Quantity::mul` + #[pyo3(name = "mul")] + #[pyo3(signature = (other))] + fn mul(&self, other: crate::generated::types::PyQuantity) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.mul(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::errors::map_dim)?; + Ok(crate::generated::types::PyQuantity { inner: __v }) + } + + /// Divides two quantities, subtracting their exponents. + /// + /// Errors: + /// + /// As `Quantity::mul`. + /// + /// Rust: `units::quantity::Quantity::div` + #[pyo3(name = "div")] + #[pyo3(signature = (other))] + fn div(&self, other: crate::generated::types::PyQuantity) -> PyResult { + let other = other.inner; + let __r = crate::runtime::guard(|| self.inner.div(&other)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::errors::map_dim)?; + Ok(crate::generated::types::PyQuantity { inner: __v }) + } + + /// Raises to an integer power. + /// + /// Errors: + /// + /// As `Quantity::mul`. + /// + /// Rust: `units::quantity::Quantity::pow` + #[pyo3(name = "pow")] + #[pyo3(signature = (n))] + fn pow(&self, n: i8) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.pow(n)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::errors::map_dim)?; + Ok(crate::generated::types::PyQuantity { inner: __v }) + } + + /// Takes the square root. + /// + /// Errors: + /// + /// `DimError::NotAPerfectRoot` unless every exponent is even. + /// + /// Rust: `units::quantity::Quantity::sqrt` + #[pyo3(name = "sqrt")] + #[pyo3(signature = ())] + fn sqrt(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.sqrt()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::errors::map_dim)?; + Ok(crate::generated::types::PyQuantity { inner: __v }) + } + + /// The magnitude expressed in the named unit. + /// + /// Errors: + /// + /// `DimError::UnknownUnit` or `DimError::Mismatch` if the unit + /// measures something else. + /// + /// Rust: `units::quantity::Quantity::to` + #[pyo3(name = "to")] + #[pyo3(signature = (unit))] + fn to(&self, unit: String) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.to(&unit)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + let __v = __v.map_err(crate::runtime::errors::map_dim)?; + Ok(__v) + } + + /// The value and its SI dimension as text. + /// + /// Rust: `units::quantity::Quantity::format_si` + #[pyo3(name = "format_si")] + #[pyo3(signature = ())] + fn format_si(&self) -> PyResult { + let __r = crate::runtime::guard(|| self.inner.format_si()); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(__v.to_string()) + } + + /// Metres. + /// + /// Rust: `units::quantity::Quantity::meters` + #[pyo3(name = "meters")] + #[staticmethod] + #[pyo3(signature = (v))] + fn meters(v: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::quantity::Quantity::meters(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuantity { inner: __v }) + } + + /// Kilometres. + /// + /// Rust: `units::quantity::Quantity::kilometers` + #[pyo3(name = "kilometers")] + #[staticmethod] + #[pyo3(signature = (v))] + fn kilometers(v: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::quantity::Quantity::kilometers(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuantity { inner: __v }) + } + + /// Millimetres. + /// + /// Rust: `units::quantity::Quantity::millimeters` + #[pyo3(name = "millimeters")] + #[staticmethod] + #[pyo3(signature = (v))] + fn millimeters(v: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::quantity::Quantity::millimeters(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuantity { inner: __v }) + } + + /// Feet, exactly 0.3048 m. + /// + /// Rust: `units::quantity::Quantity::feet` + #[pyo3(name = "feet")] + #[staticmethod] + #[pyo3(signature = (v))] + fn feet(v: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::quantity::Quantity::feet(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuantity { inner: __v }) + } + + /// Inches, exactly 25.4 mm. + /// + /// Rust: `units::quantity::Quantity::inches` + #[pyo3(name = "inches")] + #[staticmethod] + #[pyo3(signature = (v))] + fn inches(v: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::quantity::Quantity::inches(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuantity { inner: __v }) + } + + /// Statute miles. + /// + /// Rust: `units::quantity::Quantity::miles` + #[pyo3(name = "miles")] + #[staticmethod] + #[pyo3(signature = (v))] + fn miles(v: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::quantity::Quantity::miles(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuantity { inner: __v }) + } + + /// Kilograms. + /// + /// Rust: `units::quantity::Quantity::kg` + #[pyo3(name = "kg")] + #[staticmethod] + #[pyo3(signature = (v))] + fn kg(v: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::quantity::Quantity::kg(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuantity { inner: __v }) + } + + /// Grams. + /// + /// Rust: `units::quantity::Quantity::grams` + #[pyo3(name = "grams")] + #[staticmethod] + #[pyo3(signature = (v))] + fn grams(v: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::quantity::Quantity::grams(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuantity { inner: __v }) + } + + /// Pounds, exactly 0.45359237 kg. + /// + /// Rust: `units::quantity::Quantity::pounds` + #[pyo3(name = "pounds")] + #[staticmethod] + #[pyo3(signature = (v))] + fn pounds(v: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::quantity::Quantity::pounds(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuantity { inner: __v }) + } + + /// Seconds. + /// + /// Rust: `units::quantity::Quantity::seconds` + #[pyo3(name = "seconds")] + #[staticmethod] + #[pyo3(signature = (v))] + fn seconds(v: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::quantity::Quantity::seconds(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuantity { inner: __v }) + } + + /// Minutes. + /// + /// Rust: `units::quantity::Quantity::minutes` + #[pyo3(name = "minutes")] + #[staticmethod] + #[pyo3(signature = (v))] + fn minutes(v: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::quantity::Quantity::minutes(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuantity { inner: __v }) + } + + /// Hours. + /// + /// Rust: `units::quantity::Quantity::hours` + #[pyo3(name = "hours")] + #[staticmethod] + #[pyo3(signature = (v))] + fn hours(v: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::quantity::Quantity::hours(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuantity { inner: __v }) + } + + /// Days of exactly 86400 s. + /// + /// Rust: `units::quantity::Quantity::days` + #[pyo3(name = "days")] + #[staticmethod] + #[pyo3(signature = (v))] + fn days(v: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::quantity::Quantity::days(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuantity { inner: __v }) + } + + /// Amperes. + /// + /// Rust: `units::quantity::Quantity::amperes` + #[pyo3(name = "amperes")] + #[staticmethod] + #[pyo3(signature = (v))] + fn amperes(v: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::quantity::Quantity::amperes(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuantity { inner: __v }) + } + + /// Kelvin. + /// + /// Rust: `units::quantity::Quantity::kelvin` + #[pyo3(name = "kelvin")] + #[staticmethod] + #[pyo3(signature = (v))] + fn kelvin(v: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::quantity::Quantity::kelvin(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuantity { inner: __v }) + } + + /// Moles. + /// + /// Rust: `units::quantity::Quantity::moles` + #[pyo3(name = "moles")] + #[staticmethod] + #[pyo3(signature = (v))] + fn moles(v: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::quantity::Quantity::moles(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuantity { inner: __v }) + } + + /// Candela. + /// + /// Rust: `units::quantity::Quantity::candela` + #[pyo3(name = "candela")] + #[staticmethod] + #[pyo3(signature = (v))] + fn candela(v: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::quantity::Quantity::candela(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuantity { inner: __v }) + } + + /// Hertz. + /// + /// Rust: `units::quantity::Quantity::hertz` + #[pyo3(name = "hertz")] + #[staticmethod] + #[pyo3(signature = (v))] + fn hertz(v: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::quantity::Quantity::hertz(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuantity { inner: __v }) + } + + /// Newtons. + /// + /// Rust: `units::quantity::Quantity::newtons` + #[pyo3(name = "newtons")] + #[staticmethod] + #[pyo3(signature = (v))] + fn newtons(v: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::quantity::Quantity::newtons(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuantity { inner: __v }) + } + + /// Pascals. + /// + /// Rust: `units::quantity::Quantity::pascals` + #[pyo3(name = "pascals")] + #[staticmethod] + #[pyo3(signature = (v))] + fn pascals(v: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::quantity::Quantity::pascals(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuantity { inner: __v }) + } + + /// Joules. + /// + /// Rust: `units::quantity::Quantity::joules` + #[pyo3(name = "joules")] + #[staticmethod] + #[pyo3(signature = (v))] + fn joules(v: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::quantity::Quantity::joules(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuantity { inner: __v }) + } + + /// Watts. + /// + /// Rust: `units::quantity::Quantity::watts` + #[pyo3(name = "watts")] + #[staticmethod] + #[pyo3(signature = (v))] + fn watts(v: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::quantity::Quantity::watts(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuantity { inner: __v }) + } + + /// Coulombs. + /// + /// Rust: `units::quantity::Quantity::coulombs` + #[pyo3(name = "coulombs")] + #[staticmethod] + #[pyo3(signature = (v))] + fn coulombs(v: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::quantity::Quantity::coulombs(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuantity { inner: __v }) + } + + /// Volts. + /// + /// Rust: `units::quantity::Quantity::volts` + #[pyo3(name = "volts")] + #[staticmethod] + #[pyo3(signature = (v))] + fn volts(v: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::quantity::Quantity::volts(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuantity { inner: __v }) + } + + /// Farads. + /// + /// Rust: `units::quantity::Quantity::farads` + #[pyo3(name = "farads")] + #[staticmethod] + #[pyo3(signature = (v))] + fn farads(v: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::quantity::Quantity::farads(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuantity { inner: __v }) + } + + /// Ohms. + /// + /// Rust: `units::quantity::Quantity::ohms` + #[pyo3(name = "ohms")] + #[staticmethod] + #[pyo3(signature = (v))] + fn ohms(v: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::quantity::Quantity::ohms(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuantity { inner: __v }) + } + + /// Teslas. + /// + /// Rust: `units::quantity::Quantity::teslas` + #[pyo3(name = "teslas")] + #[staticmethod] + #[pyo3(signature = (v))] + fn teslas(v: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::quantity::Quantity::teslas(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuantity { inner: __v }) + } + + /// Webers. + /// + /// Rust: `units::quantity::Quantity::webers` + #[pyo3(name = "webers")] + #[staticmethod] + #[pyo3(signature = (v))] + fn webers(v: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::quantity::Quantity::webers(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuantity { inner: __v }) + } + + /// Henries. + /// + /// Rust: `units::quantity::Quantity::henries` + #[pyo3(name = "henries")] + #[staticmethod] + #[pyo3(signature = (v))] + fn henries(v: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::quantity::Quantity::henries(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuantity { inner: __v }) + } + + /// Electron volts, exact since the 2019 redefinition. + /// + /// Rust: `units::quantity::Quantity::electron_volts` + #[pyo3(name = "electron_volts")] + #[staticmethod] + #[pyo3(signature = (v))] + fn electron_volts(v: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::quantity::Quantity::electron_volts(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuantity { inner: __v }) + } + + /// Kilowatt hours. + /// + /// Rust: `units::quantity::Quantity::kilowatt_hours` + #[pyo3(name = "kilowatt_hours")] + #[staticmethod] + #[pyo3(signature = (v))] + fn kilowatt_hours(v: f64) -> PyResult { + let __r = crate::runtime::guard(|| rust_physics_engine::units::quantity::Quantity::kilowatt_hours(v)); + let __v = __r.map_err(crate::runtime::errors::InvalidArgumentError::new_err)?; + Ok(crate::generated::types::PyQuantity { inner: __v }) + } + + #[getter] + #[pyo3(name = "value")] + fn py_get_value(&self) -> PyResult { Ok(self.inner.value) } + + #[setter] + #[pyo3(name = "value")] + fn py_set_value(&mut self, v: f64) { self.inner.value = v; } + + #[getter] + #[pyo3(name = "dim")] + fn py_get_dim(&self) -> PyResult { Ok(crate::generated::types::PyDim { inner: self.inner.dim.clone() }) } + + fn __repr__(&self) -> String { format!("{:?}", self.inner).replacen("Quantity", "Quantity", 1) } + + fn __copy__(&self) -> Self { self.clone() } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__(&self, _memo: Option>) -> Self { self.clone() } +} diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs new file mode 100644 index 0000000..87c4e2e --- /dev/null +++ b/bindings/python/src/lib.rs @@ -0,0 +1,26 @@ +//! Python bindings for `rust_physics_engine`. +//! +//! Almost everything under `generated/` is written by +//! `bindings/python/generate.py`, which reads the library's source and +//! emits a wrapper for each item it can bind. This file and `runtime/` +//! are the parts a generator cannot write: the module's entry point, the +//! exception hierarchy, the coercions that let a Python tuple stand in +//! for a `Vec3`, and the adapter that carries a Python callable into a +//! routine expecting `&dyn Fn`. + +use pyo3::prelude::*; +use pyo3::types::PyModule; + +mod generated; +mod runtime; + +/// The extension module. `numeria/__init__.py` re-exports +/// from here and installs the submodules into `sys.modules`. +#[pymodule] +fn _core(m: &Bound<'_, PyModule>) -> PyResult<()> { + runtime::install_panic_hook(); + m.add("__version__", env!("CARGO_PKG_VERSION"))?; + runtime::errors::register(m)?; + generated::register(m.py(), m)?; + Ok(()) +} diff --git a/bindings/python/src/runtime/callback.rs b/bindings/python/src/runtime/callback.rs new file mode 100644 index 0000000..a347027 --- /dev/null +++ b/bindings/python/src/runtime/callback.rs @@ -0,0 +1,80 @@ +//! Passing a Python callable into a routine that wants `&dyn Fn`. +//! +//! Roughly two hundred functions in the library take a function: an +//! integrand, a residual, the right-hand side of an ODE. Rust's signature +//! for those has no room for failure -- `&dyn Fn(f64) -> f64` returns an +//! `f64` and nothing else -- but a Python callable can raise, and can +//! return something that is not a number. +//! +//! [`Callback`] holds the first such error instead of discarding it. When +//! the callable fails, the call returns a fallback so the Rust routine can +//! unwind normally on its own terms, every later call short-circuits to +//! that same fallback rather than compounding the failure, and the wrapper +//! re-raises the stored exception once the routine returns. What the +//! caller sees is their own `ZeroDivisionError`, with their traceback, +//! rather than a NaN that came from nowhere. + +use std::cell::RefCell; + +use pyo3::prelude::*; + +/// A Python callable, adapted for use as a Rust `Fn`. +pub struct Callback { + obj: Py, + err: RefCell>, +} + +impl Callback { + /// Wraps a Python object. It is not checked for callability here; a + /// non-callable raises `TypeError` on first use, which is where the + /// traceback is most useful. + pub fn new(obj: Py) -> Self { + Self { obj, err: RefCell::new(None) } + } + + /// Calls the object with `args`, returning `fallback` if anything goes + /// wrong -- and remembering why. + pub fn call(&self, args: A, fallback: R) -> R + where + A: for<'py> pyo3::call::PyCallArgs<'py>, + R: for<'a, 'py> FromPyObject<'a, 'py>, + for<'a, 'py> >::Error: Into, + { + if self.err.borrow().is_some() { + return fallback; + } + Python::attach(|py| { + let outcome = self + .obj + .call1(py, args) + .and_then(|v| v.bind(py).extract::().map_err(Into::into)); + match outcome { + Ok(v) => v, + Err(e) => { + *self.err.borrow_mut() = Some(e); + fallback + } + } + }) + } + + /// The first exception raised inside the callable, if there was one. + pub fn take_err(&self) -> Option { + self.err.borrow_mut().take() + } +} + +/// Re-raises the first error from any of `callbacks`, else returns `value`. +/// +/// The generated wrappers call this before returning. It runs before the +/// panic check, because a panic that follows a failed callback is a +/// consequence of the fallback value, not the thing the caller needs to +/// see. +pub fn check(callbacks: &[&Callback], value: T) -> PyResult { + for cb in callbacks { + if let Some(e) = cb.take_err() { + return Err(e); + } + } + Ok(value) +} diff --git a/bindings/python/src/runtime/coerce.rs b/bindings/python/src/runtime/coerce.rs new file mode 100644 index 0000000..d5e6def --- /dev/null +++ b/bindings/python/src/runtime/coerce.rs @@ -0,0 +1,264 @@ +//! Letting Python values stand in for Rust ones. +//! +//! Two kinds of conversion live here. The first is coercion: a `Vec3` +//! argument accepts `(1.0, 2.0, 3.0)`, a `Matrix` accepts a list of rows. +//! Requiring `Vec3(1, 2, 3)` everywhere would be faithful to the Rust API +//! and unpleasant to use; accepting both costs one `extract` attempt and +//! reads the way a Python caller expects. +//! +//! The second is identification: three Rust types have exact Python +//! counterparts, and are translated rather than wrapped. +//! +//! | Rust | Python | +//! |---|---| +//! | `fractals::Complex` | `complex` | +//! | `exact::bigint::BigInt` | `int` | +//! | `exact::rational::Rational` | `fractions.Fraction` | +//! +//! `BigInt` and `Fraction` are both arbitrary-precision and both exact, so +//! the round trip loses nothing -- which is the test a translation has to +//! pass before it is worth doing. Everything else gets a wrapper class. + +use pyo3::exceptions::PyTypeError; +use pyo3::prelude::*; +use std::sync::OnceLock; +use pyo3::types::{PyComplex, PyComplexMethods, PyList, PyListMethods, PyString, PyTuple}; +use pyo3::Borrowed; + +use rust_physics_engine::exact::bigint::BigInt; +use rust_physics_engine::exact::rational::Rational; +use rust_physics_engine::fractals::Complex; + +/// Extracts exactly `n` floats from a sequence. +/// +/// Used by the generated adapters for the small fixed-width value types -- +/// `Vec2`, `Vec3`, `Vec4`, `Mat3` and friends -- so that any sequence of +/// the right length is accepted. +pub fn floats_exact(obj: Borrowed<'_, '_, PyAny>, n: usize, what: &str) -> PyResult> { + let v: Vec = obj.extract().map_err(|_| { + PyTypeError::new_err(format!("{what} expects {n} floats or a {what} instance")) + })?; + if v.len() != n { + return Err(PyTypeError::new_err(format!( + "{what} expects {n} floats, got {}", + v.len() + ))); + } + Ok(v) +} + +/// Extracts a rectangular list of rows, checking that the rows agree. +pub fn rows(obj: Borrowed<'_, '_, PyAny>, what: &str) -> PyResult>> { + let v: Vec> = obj + .extract() + .map_err(|_| PyTypeError::new_err(format!("{what} expects a sequence of rows")))?; + if v.is_empty() || v[0].is_empty() { + return Err(PyTypeError::new_err(format!("{what} needs at least one non-empty row"))); + } + let cols = v[0].len(); + for (i, r) in v.iter().enumerate() { + if r.len() != cols { + return Err(PyTypeError::new_err(format!( + "{what}: row {i} has {} entries, row 0 has {cols}", + r.len() + ))); + } + } + Ok(v) +} + +// ── Complex ───────────────────────────────────────────────────────────── + +/// A `Complex` argument. Accepts `complex`, `float` and `int`. +pub struct ComplexArg(pub Complex); + +impl<'a, 'py> FromPyObject<'a, 'py> for ComplexArg { + type Error = PyErr; + + fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result { + let any = obj.as_any(); + if let Ok(c) = any.cast::() { + return Ok(ComplexArg(Complex::new(c.real(), c.imag()))); + } + if let Ok(x) = any.extract::() { + return Ok(ComplexArg(Complex::new(x, 0.0))); + } + // `complex()` on an object with `__complex__`. + let called = any.call_method0("__complex__").map_err(|_| { + PyTypeError::new_err("expected a complex, a float, or an object with __complex__") + })?; + let c = called + .cast::() + .map_err(|_| PyTypeError::new_err("__complex__ did not return a complex"))?; + Ok(ComplexArg(Complex::new(c.real(), c.imag()))) + } +} + +/// Builds a Python `complex`. +pub fn complex_out(py: Python<'_>, c: Complex) -> Bound<'_, PyComplex> { + PyComplex::from_doubles(py, c.re, c.im) +} + +// ── BigInt ────────────────────────────────────────────────────────────── + +static INT_CTOR: OnceLock> = OnceLock::new(); + +fn int_ctor(py: Python<'_>) -> PyResult<&'static Py> { + if let Some(v) = INT_CTOR.get() { + return Ok(v); + } + let ctor = py.import("builtins")?.getattr("int")?.unbind(); + Ok(INT_CTOR.get_or_init(|| ctor)) +} + +/// A `BigInt` argument: any Python `int`, of any size. +pub struct BigIntArg(pub BigInt); + +impl<'a, 'py> FromPyObject<'a, 'py> for BigIntArg { + type Error = PyErr; + + fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result { + Ok(BigIntArg(bigint_from(obj.as_any())?)) + } +} + +/// Converts a Python `int` to a [`BigInt`]. +/// +/// Values inside `i64` take a direct path. Anything larger goes via a +/// hexadecimal string, which is exact -- four bits per character -- and is +/// the only conversion CPython offers that does not depend on the +/// internal digit layout. +pub fn bigint_from(obj: &Bound<'_, PyAny>) -> PyResult { + if let Ok(n) = obj.extract::() { + return Ok(BigInt::from_i64(n)); + } + let s: String = obj + .call_method1("__format__", ("x",)) + .map_err(|_| PyTypeError::new_err("expected an int"))? + .extract()?; + BigInt::from_str_radix(&s, 16).map_err(super::errors::map_geom) +} + +/// Converts a [`BigInt`] to a Python `int`. +pub fn bigint_out<'py>(py: Python<'py>, v: &BigInt) -> PyResult> { + if let Some(n) = v.to_i64() { + return Ok(n.into_pyobject(py)?.into_any()); + } + let s = v.to_string_radix(16); + let text = PyString::new(py, &s); + int_ctor(py)?.bind(py).call1((text, 16)) +} + +// ── Rational ──────────────────────────────────────────────────────────── + +static FRACTION: OnceLock> = OnceLock::new(); + +fn fraction(py: Python<'_>) -> PyResult<&'static Py> { + if let Some(v) = FRACTION.get() { + return Ok(v); + } + let cls = py.import("fractions")?.getattr("Fraction")?.unbind(); + Ok(FRACTION.get_or_init(|| cls)) +} + +/// A `Rational` argument: a `Fraction`, an `int`, or a `(numerator, +/// denominator)` pair. A `float` is *not* accepted -- 0.1 is not one +/// tenth, and silently pretending otherwise in a module whose whole point +/// is exactness would be the wrong kindness. Use +/// `Fraction(1, 10)`, or `exact.rational.Rational.from_f64_approx`. +pub struct RationalArg(pub Rational); + +impl<'a, 'py> FromPyObject<'a, 'py> for RationalArg { + type Error = PyErr; + + fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result { + let any = obj.as_any(); + if let (Ok(n), Ok(d)) = (any.getattr("numerator"), any.getattr("denominator")) { + let num = bigint_from(&n)?; + let den = bigint_from(&d)?; + return Rational::new(num, den) + .map(RationalArg) + .ok_or_else(|| PyTypeError::new_err("a Rational may not have a zero denominator")); + } + if let Ok(t) = any.cast::() { + if t.len() == 2 { + let num = bigint_from(&t.get_item(0)?)?; + let den = bigint_from(&t.get_item(1)?)?; + return Rational::new(num, den).map(RationalArg).ok_or_else(|| { + PyTypeError::new_err("a Rational may not have a zero denominator") + }); + } + } + Err(PyTypeError::new_err( + "expected a Fraction, an int, or a (numerator, denominator) pair", + )) + } +} + +/// Converts a [`Rational`] to a `fractions.Fraction`. +pub fn rational_out<'py>(py: Python<'py>, v: &Rational) -> PyResult> { + let num = bigint_out(py, &v.num)?; + let den = bigint_out(py, &v.den)?; + fraction(py)?.bind(py).call1((num, den)) +} + +// ── Arguments written through ─────────────────────────────────────────── + +/// Writes a mutated slice back into the Python list it came from. +/// +/// A handful of routines take `&mut [f64]` and work in place. Copying the +/// values in and dropping the results on the floor would compile and give +/// wrong answers silently, which is the one outcome worth ruling out; so +/// the values go back where they came from, and an argument that cannot +/// receive them -- a tuple, a generator -- is a `TypeError` rather than a +/// quiet no-op. +pub fn write_back(obj: &Bound<'_, PyAny>, values: &[T]) -> PyResult<()> +where + T: Copy + for<'py> IntoPyObject<'py>, +{ + let list = obj.cast::().map_err(|_| { + PyTypeError::new_err("this argument is modified in place, so it must be a list") + })?; + let fresh = PyList::empty(obj.py()); + for v in values { + fresh.append(*v)?; + } + list.set_slice(0, list.len(), fresh.as_any()) +} + +/// A [`Complex`] on its way *to* Python, as a `complex`. +/// +/// Needed where the value is handed to a Python callable rather than +/// returned: `Callback::call` takes a tuple of things that convert +/// themselves, and a bare `Complex` is not one. +pub struct Cx(pub Complex); + +impl<'py> IntoPyObject<'py> for Cx { + type Target = PyComplex; + type Output = Bound<'py, PyComplex>; + type Error = std::convert::Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + Ok(PyComplex::from_doubles(py, self.0.re, self.0.im)) + } +} + +/// Writes a mutated slice of wrapped values back into its Python list. +/// +/// The counterpart of [`write_back`] for element types that are objects +/// rather than numbers -- `&mut [Vec2]`, `&mut [Complex]`. The values are +/// rebuilt as Python objects, so the caller sees the new ones rather than +/// stale wrappers around the old. +pub fn write_back_objects<'py, T>(obj: &Bound<'py, PyAny>, values: Vec) -> PyResult<()> +where + T: IntoPyObject<'py>, +{ + let list = obj.cast::().map_err(|_| { + PyTypeError::new_err("this argument is modified in place, so it must be a list") + })?; + let fresh = PyList::empty(obj.py()); + for v in values { + fresh.append(v)?; + } + list.set_slice(0, list.len(), fresh.as_any()) +} diff --git a/bindings/python/src/runtime/errors.rs b/bindings/python/src/runtime/errors.rs new file mode 100644 index 0000000..b07ef4b --- /dev/null +++ b/bindings/python/src/runtime/errors.rs @@ -0,0 +1,237 @@ +//! The exception hierarchy, and the two things that produce it: the +//! library's error enums, and panics. +//! +//! Every exception raised by these bindings derives from `PhysicsError`, +//! so `except PhysicsError` catches all of them and nothing else. Below +//! that the tree follows the library's own two error enums, plus the +//! dimensional errors from `units`. Where a variant carries data -- +//! `NoConvergence { iters, residual }`, `DimensionMismatch { expected, +//! got }` -- that data is set as attributes on the exception instance +//! rather than only formatted into its message, so a caller can branch on +//! the residual instead of parsing a string. +//! +//! Panics are the second source. Much of the library validates arguments +//! with `assert!`, which is right for Rust -- a negative mass is a +//! programming error, not a runtime condition -- but a panic crossing the +//! FFI boundary would abort the interpreter or, at best, surface as +//! `pyo3_runtime.PanicException` with a backtrace printed to stderr. +//! [`guard`] catches them and raises `InvalidArgumentError` carrying the +//! assertion's own message, which is the message the library author +//! wrote for exactly this case. + +use std::cell::{Cell, RefCell}; +use std::panic::AssertUnwindSafe; +use std::sync::Once; + +use pyo3::prelude::*; +use pyo3::types::PyModule; + +use rust_physics_engine::error::{GeomError, SolveError}; +use rust_physics_engine::units::quantity::DimError; + +pyo3::create_exception!( + numeria, + PhysicsError, + pyo3::exceptions::PyException, + "Base class for every exception raised by rust_physics_engine." +); +pyo3::create_exception!( + numeria, + InvalidArgumentError, + PhysicsError, + "An argument violated a documented precondition." +); +pyo3::create_exception!( + numeria, + SolverError, + PhysicsError, + "Base class for failures reported by the numerical solvers." +); +pyo3::create_exception!( + numeria, + SingularMatrixError, + SolverError, + "The matrix is singular, or a pivot fell below the safe threshold." +); +pyo3::create_exception!( + numeria, + NotPositiveDefiniteError, + SolverError, + "The matrix is not (numerically) symmetric positive definite." +); +pyo3::create_exception!( + numeria, + ConvergenceError, + SolverError, + "An iteration did not converge. Carries `iterations` and `residual`." +); +pyo3::create_exception!( + numeria, + DimensionMismatchError, + SolverError, + "Operand shapes are incompatible. Carries `expected` and `got`." +); +pyo3::create_exception!( + numeria, + GeometryError, + PhysicsError, + "Base class for failures reported by the geometric algorithms." +); +pyo3::create_exception!( + numeria, + DegenerateGeometryError, + GeometryError, + "Degenerate input: a zero-area triangle, coincident points, and so on." +); +pyo3::create_exception!( + numeria, + NotManifoldError, + GeometryError, + "The mesh is not manifold where the operation requires it." +); +pyo3::create_exception!( + numeria, + EmptyInputError, + GeometryError, + "The input contains no elements." +); +pyo3::create_exception!( + numeria, + UnitsError, + PhysicsError, + "A dimensional check failed in `units`." +); + +/// Registers the exception classes on the extension module. +pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { + let py = m.py(); + m.add("PhysicsError", py.get_type::())?; + m.add("InvalidArgumentError", py.get_type::())?; + m.add("SolverError", py.get_type::())?; + m.add("SingularMatrixError", py.get_type::())?; + m.add("NotPositiveDefiniteError", py.get_type::())?; + m.add("ConvergenceError", py.get_type::())?; + m.add("DimensionMismatchError", py.get_type::())?; + m.add("GeometryError", py.get_type::())?; + m.add("DegenerateGeometryError", py.get_type::())?; + m.add("NotManifoldError", py.get_type::())?; + m.add("EmptyInputError", py.get_type::())?; + m.add("UnitsError", py.get_type::())?; + Ok(()) +} + +fn with_attrs(err: PyErr, attrs: &[(&str, f64)], ints: &[(&str, usize)]) -> PyErr { + Python::attach(|py| { + let value = err.value(py); + for (k, v) in attrs { + let _ = value.setattr(*k, *v); + } + for (k, v) in ints { + let _ = value.setattr(*k, *v); + } + }); + err +} + +/// Maps a [`SolveError`] onto the matching Python exception. +pub fn map_solve(e: SolveError) -> PyErr { + let msg = e.to_string(); + match e { + SolveError::Singular => SingularMatrixError::new_err(msg), + SolveError::NotPositiveDefinite => NotPositiveDefiniteError::new_err(msg), + SolveError::NoConvergence { iters, residual } => with_attrs( + ConvergenceError::new_err(msg), + &[("residual", residual)], + &[("iterations", iters)], + ), + SolveError::DimensionMismatch { expected, got } => with_attrs( + DimensionMismatchError::new_err(msg), + &[], + &[("expected", expected), ("got", got)], + ), + SolveError::InvalidArgument(_) => InvalidArgumentError::new_err(msg), + } +} + +/// Maps a [`GeomError`] onto the matching Python exception. +pub fn map_geom(e: GeomError) -> PyErr { + let msg = e.to_string(); + match e { + GeomError::Degenerate(_) => DegenerateGeometryError::new_err(msg), + GeomError::NotManifold => NotManifoldError::new_err(msg), + GeomError::Empty => EmptyInputError::new_err(msg), + GeomError::InvalidArgument(_) => InvalidArgumentError::new_err(msg), + } +} + +/// Maps a [`DimError`] onto `UnitsError`. +pub fn map_dim(e: DimError) -> PyErr { + UnitsError::new_err(e.to_string()) +} + +/// Fallback for the handful of one-off error types that are plain structs +/// (`TooManyErrors`, `NegativeCycle`): anything with a `Display`. +pub fn map_display(e: E) -> PyErr { + PhysicsError::new_err(e.to_string()) +} + +/// Last resort, for an error type that is data rather than a message -- +/// `graph::matching` reports an odd cycle as the `Vec` of its +/// vertices. `Debug` is what such a type has, so `Debug` is what the +/// exception carries. +pub fn map_debug(e: E) -> PyErr { + PhysicsError::new_err(format!("{e:?}")) +} + +thread_local! { + static PANIC_MESSAGE: RefCell> = const { RefCell::new(None) }; + static SUPPRESS: Cell = const { Cell::new(false) }; +} + +static HOOK: Once = Once::new(); + +/// Installs a panic hook that stays quiet inside [`guard`] and defers to +/// whatever hook was already in place everywhere else. +/// +/// The suppression flag is thread-local, so a panic on some other thread +/// -- one this extension knows nothing about -- still prints and still +/// reaches the previous hook. Only a panic raised inside a call that +/// [`guard`] is watching is captured, and that panic is about to become an +/// exception carrying the same message. +pub fn install_panic_hook() { + HOOK.call_once(|| { + let previous = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |info| { + if SUPPRESS.with(Cell::get) { + let payload = info.payload(); + let msg = payload + .downcast_ref::<&str>() + .map(|s| (*s).to_string()) + .or_else(|| payload.downcast_ref::().cloned()) + .unwrap_or_else(|| "panicked".to_string()); + PANIC_MESSAGE.with(|slot| *slot.borrow_mut() = Some(msg)); + } else { + previous(info); + } + })); + }); +} + +/// Runs `f`, converting a panic into `InvalidArgumentError`. +/// +/// The library asserts its preconditions -- `assert!(mass > 0.0, "mass +/// must be positive")` -- and those assertions are the argument checks a +/// Python caller should see. The message the assertion carries becomes +/// the exception's message. +pub fn guard(f: impl FnOnce() -> T) -> Result { + let previous = SUPPRESS.with(|s| s.replace(true)); + PANIC_MESSAGE.with(|slot| *slot.borrow_mut() = None); + let result = std::panic::catch_unwind(AssertUnwindSafe(f)); + SUPPRESS.with(|s| s.set(previous)); + match result { + Ok(v) => Ok(v), + Err(_) => Err(PANIC_MESSAGE + .with(|slot| slot.borrow_mut().take()) + .unwrap_or_else(|| "the underlying Rust routine panicked".to_string())), + } +} diff --git a/bindings/python/src/runtime/mod.rs b/bindings/python/src/runtime/mod.rs new file mode 100644 index 0000000..f5d9086 --- /dev/null +++ b/bindings/python/src/runtime/mod.rs @@ -0,0 +1,18 @@ +//! The hand-written half of the bindings: everything the generator calls +//! but does not itself produce. +//! +//! Three concerns live here. [`errors`] turns the library's two error +//! enums, and any panic that escapes an argument assertion, into a Python +//! exception hierarchy. [`coerce`] lets a Python tuple stand in for a +//! `Vec3` and a list of lists for a `Matrix`, so callers are not forced to +//! construct wrapper objects for values that have an obvious literal form. +//! [`callback`] passes a Python callable into a routine that expects +//! `&dyn Fn`, and carries an exception raised inside it back out to the +//! caller instead of swallowing it. + +pub mod callback; +pub mod coerce; +pub mod errors; + +pub use callback::Callback; +pub use errors::{guard, install_panic_hook, map_geom, map_solve}; diff --git a/bindings/python/tests/test_callables.py b/bindings/python/tests/test_callables.py new file mode 100644 index 0000000..becb0d6 --- /dev/null +++ b/bindings/python/tests/test_callables.py @@ -0,0 +1,90 @@ +"""Passing a Python function where the library wants `&dyn Fn`. + +About two hundred routines take a function: an integrand, a residual, the +right-hand side of an ODE. Rust's signature has no room for failure -- +`&dyn Fn(f64) -> f64` returns an `f64` and nothing else -- so the adapter +has to hold any exception and re-raise it after the routine returns. These +tests check both halves: that the results are right when nothing goes +wrong, and that the caller gets their own exception when something does. +""" + +import math + +import pytest + +import numeria as nm + + +def test_integrating_a_python_function(): + got = nm.numerical.integrate.simpson(math.sin, 0.0, math.pi, 1000) + assert got == pytest.approx(2.0, abs=1e-9) + assert nm.numerical.integrate.trapezoid(lambda x: x * x, 0.0, 3.0, 10_000) == pytest.approx( + 9.0, rel=1e-6 + ) + + +def test_a_closure_over_python_state_works(): + power = 3 + + def f(x): + return x**power + + # ∫₀¹ x³ dx = 1/4 + assert nm.numerical.integrate.simpson(f, 0.0, 1.0, 1000) == pytest.approx(0.25, abs=1e-9) + + +def test_root_finding_with_two_callbacks(): + root = nm.numerical.roots.newton_raphson( + lambda x: x * x - 2.0, lambda x: 2.0 * x, 1.0, 1e-14, 100 + ) + assert root == pytest.approx(math.sqrt(2.0)) + + +def test_a_method_that_cannot_converge_returns_None_not_a_wrong_answer(): + # No sign change on [2, 3] for x² − 2, so bisection has nothing to do. + assert nm.numerical.roots.bisection(lambda x: x * x - 2.0, 2.0, 3.0, 1e-12, 100) is None + + +def test_a_two_argument_callback_integrates_an_ode(): + # y' = -y, y(0) = 1 -> y(1) = 1/e. The result is a list of (t, y). + trace = nm.numerical.ode.explicit.rk4_solve(lambda t, y: -y, 0.0, 1.0, 1.0, 1e-3) + assert trace[0] == (0.0, 1.0) + t_end, y_end = trace[-1] + assert t_end == pytest.approx(1.0) + assert y_end == pytest.approx(math.exp(-1.0), rel=1e-9) + + +def test_a_callback_taking_and_returning_a_sequence(): + """`&dyn Fn(f64, &[f64]) -> Vec` -- a vector-valued right-hand side.""" + # The harmonic oscillator: y'' = -y, as y' = v, v' = -y. + step = nm.numerical.ode.explicit.rk4_step_vec( + lambda t, y: [y[1], -y[0]], 0.0, [1.0, 0.0], 0.01 + ) + assert len(step) == 2 + assert step[0] == pytest.approx(math.cos(0.01), abs=1e-9) + assert step[1] == pytest.approx(-math.sin(0.01), abs=1e-9) + + +def test_the_callback_sees_the_arguments_in_the_right_order(): + seen = [] + + def rhs(t, y): + seen.append((t, y)) + return 1.0 + + nm.numerical.ode.explicit.rk4_solve(rhs, 0.0, 5.0, 1.0, 0.25) + # First call is at the initial condition: t = 0, y = 5. + assert seen[0] == (0.0, 5.0) + + +def test_many_callback_invocations_do_not_leak_or_slow_to_a_halt(): + calls = 0 + + def f(x): + nonlocal calls + calls += 1 + return math.exp(-x * x) + + got = nm.numerical.integrate.simpson(f, -5.0, 5.0, 20_000) + assert got == pytest.approx(math.sqrt(math.pi), abs=1e-6) + assert calls > 20_000 diff --git a/bindings/python/tests/test_errors.py b/bindings/python/tests/test_errors.py new file mode 100644 index 0000000..601b3b9 --- /dev/null +++ b/bindings/python/tests/test_errors.py @@ -0,0 +1,128 @@ +"""Failures arrive as exceptions, and carry what the caller needs. + +Three things have to hold. A `Result::Err` becomes the matching exception +rather than a sentinel value. An `assert!` inside the library -- which is +how it validates arguments -- becomes an exception rather than an aborted +interpreter. And an exception raised inside a Python callable passed into +the library comes back out with its own traceback rather than turning into +a NaN. +""" + +import math + +import pytest + +import numeria as nm + + +def test_the_hierarchy_is_what_the_readme_says(): + E = nm + assert issubclass(E.InvalidArgumentError, E.PhysicsError) + assert issubclass(E.SolverError, E.PhysicsError) + assert issubclass(E.SingularMatrixError, E.SolverError) + assert issubclass(E.NotPositiveDefiniteError, E.SolverError) + assert issubclass(E.ConvergenceError, E.SolverError) + assert issubclass(E.DimensionMismatchError, E.SolverError) + assert issubclass(E.GeometryError, E.PhysicsError) + assert issubclass(E.DegenerateGeometryError, E.GeometryError) + assert issubclass(E.NotManifoldError, E.GeometryError) + assert issubclass(E.EmptyInputError, E.GeometryError) + assert issubclass(E.UnitsError, E.PhysicsError) + assert issubclass(E.PhysicsError, Exception) + + +def test_a_singular_matrix_raises_rather_than_returning_nonsense(): + singular = [[1.0, 2.0], [2.0, 4.0]] + with pytest.raises(nm.SingularMatrixError) as excinfo: + nm.linalg.lu.solve(singular, [1.0, 2.0]) + assert "singular" in str(excinfo.value) + # And it is catchable at every level above it. + with pytest.raises(nm.SolverError): + nm.linalg.lu.solve(singular, [1.0, 2.0]) + with pytest.raises(nm.PhysicsError): + nm.linalg.lu.solve(singular, [1.0, 2.0]) + + +def test_a_shape_mismatch_carries_the_two_shapes(): + with pytest.raises(nm.DimensionMismatchError) as excinfo: + nm.linalg.lu.solve([[1.0, 2.0], [3.0, 4.0]], [1.0, 2.0, 3.0]) + err = excinfo.value + assert err.expected == 2 + assert err.got == 3 + + +def test_an_assertion_in_the_library_becomes_an_exception_with_its_message(): + """`assert!(mass > 0.0, "mass must be positive")` is an argument check. + + In Rust it is the right one: a negative mass is a programming error. + Crossing into Python it must not abort the interpreter, and the + message the library author wrote is exactly the message a caller + wants. + """ + with pytest.raises(nm.InvalidArgumentError) as excinfo: + nm.classical.acceleration(force=10.0, mass=-1.0) + assert "mass must be positive" in str(excinfo.value) + + +def test_a_panic_does_not_leave_the_next_call_broken(): + with pytest.raises(nm.InvalidArgumentError): + nm.classical.acceleration(force=10.0, mass=0.0) + # The guard has to reset its state, or every later call inherits it. + assert nm.classical.acceleration(force=10.0, mass=2.0) == 5.0 + + +def test_an_exception_inside_a_callback_comes_back_out(): + def bad(x): + raise ZeroDivisionError("from the integrand") + + with pytest.raises(ZeroDivisionError) as excinfo: + nm.numerical.integrate.simpson(bad, 0.0, 1.0, 100) + assert "from the integrand" in str(excinfo.value) + + +def test_a_callback_returning_the_wrong_type_is_a_TypeError(): + with pytest.raises(TypeError): + nm.numerical.integrate.simpson(lambda x: "not a number", 0.0, 1.0, 10) + + +def test_a_callback_that_is_not_callable_is_a_TypeError(): + with pytest.raises(TypeError): + nm.numerical.integrate.simpson(42, 0.0, 1.0, 10) + + +def test_the_integrand_error_wins_over_whatever_it_caused(): + """A failed callback returns NaN so the Rust routine can finish. + + That NaN may then trip an assertion further in. The caller needs the + original exception, not the consequence, so the callback's error is + checked first. + """ + + calls = [] + + def bad(x): + calls.append(x) + raise ValueError("mine") + + with pytest.raises(ValueError, match="mine"): + nm.numerical.integrate.simpson(bad, 0.0, 1.0, 1000) + # And it short-circuits rather than calling a thousand times after the + # first failure. + assert len(calls) < 10 + + +def test_a_geometry_failure_maps_to_the_geometry_branch(): + with pytest.raises(nm.EmptyInputError): + # A triangulation needs at least three points. + nm.fem.fem2d.FemMesh2.from_delaunay([(0.0, 0.0), (1.0, 0.0)]) + with pytest.raises(nm.DegenerateGeometryError) as excinfo: + # Three collinear points enclose no area. + nm.fem.fem2d.FemMesh2.from_delaunay([(0.0, 0.0), (1.0, 0.0), (2.0, 0.0)]) + assert "collinear" in str(excinfo.value) or "degenerate" in str(excinfo.value) + + +def test_wrong_python_types_are_TypeError_not_a_crash(): + with pytest.raises(TypeError): + nm.classical.displacement("fast", 1.0, 1.0) + with pytest.raises(TypeError): + nm.statistics.descriptive.mean("abc") diff --git a/bindings/python/tests/test_generator.py b/bindings/python/tests/test_generator.py new file mode 100644 index 0000000..d3a8187 --- /dev/null +++ b/bindings/python/tests/test_generator.py @@ -0,0 +1,180 @@ +"""The generator itself, and the promises the package makes about itself. + +The bindings are derived from the library's source, so the parts worth +testing directly are the ones a bad derivation would get wrong quietly: +the Rust reader's view of the source, and whether the shipped stubs and +the shipped module still agree. +""" + +import os +import subprocess +import sys + +import pytest + +HERE = os.path.dirname(os.path.abspath(__file__)) +BINDINGS = os.path.dirname(HERE) +sys.path.insert(0, BINDINGS) + +import rustscan # noqa: E402 + + +def scan(source: str): + """Run the scanner over a snippet, as if it were a file.""" + import tempfile + + crate = rustscan.Crate() + with tempfile.NamedTemporaryFile("w", suffix=".rs", delete=False) as fh: + fh.write(source) + path = fh.name + try: + rustscan.parse_file(path, "demo", crate) + finally: + os.unlink(path) + return crate + + +def test_the_scanner_finds_a_function_and_its_doc(): + crate = scan( + """ + /// Adds two numbers. + pub fn add(a: f64, b: f64) -> f64 { a + b } + """ + ) + assert len(crate.funcs) == 1 + fn = crate.funcs[0] + assert fn.name == "add" + assert fn.args == [("a", "f64"), ("b", "f64")] + assert fn.ret == "f64" + assert fn.doc == "Adds two numbers." + + +def test_a_brace_inside_a_doctest_does_not_unbalance_the_item(): + """Doc comments are masked before brace matching. + + A `{` inside a doctest would otherwise close the wrong block and lose + every item after it -- silently, because the file still parses. + """ + crate = scan( + """ + /// ``` + /// let m = Foo { x: 1 }; + /// ``` + pub fn first() -> f64 { 1.0 } + + pub fn second() -> f64 { 2.0 } + """ + ) + assert [f.name for f in crate.funcs] == ["first", "second"] + + +def test_a_semicolon_in_a_return_type_is_not_the_end_of_a_declaration(): + crate = scan("pub fn corners() -> [f64; 6] { [0.0; 6] }\npub fn after() -> f64 { 1.0 }") + assert [f.name for f in crate.funcs] == ["corners", "after"] + assert crate.funcs[0].ret == "[f64; 6]" + + +def test_test_modules_are_not_part_of_the_public_api(): + crate = scan( + """ + pub fn real() -> f64 { 1.0 } + + #[cfg(test)] + mod tests { + pub fn helper() -> f64 { 2.0 } + } + """ + ) + assert [f.name for f in crate.funcs] == ["real"] + + +def test_a_string_containing_a_brace_does_not_confuse_the_scanner(): + crate = scan( + """ + pub fn label() -> String { "}{".to_string() } + pub fn after() -> f64 { 1.0 } + """ + ) + assert [f.name for f in crate.funcs] == ["label", "after"] + + +def test_methods_are_attributed_to_their_impl_type(): + crate = scan( + """ + pub struct Thing { pub x: f64 } + + impl Thing { + /// Doubles it. + pub fn double(&self) -> f64 { self.x * 2.0 } + } + """ + ) + assert crate.structs[0].name == "Thing" + assert crate.structs[0].fields[0].name == "x" + method = crate.funcs[0] + assert method.impl_type == "Thing" + assert method.self_kind == "&self" + + +def test_trait_impls_are_recorded_so_operators_can_be_found(): + crate = scan( + """ + pub struct V { pub x: f64 } + + impl std::ops::Add for V { + type Output = V; + fn add(self, rhs: V) -> V { V { x: self.x + rhs.x } } + } + """ + ) + add = [f for f in crate.funcs if f.name == "add"][0] + assert add.impl_trait.endswith("Add") + assert add.impl_type == "V" + + +def test_use_statements_resolve_short_names(): + crate = scan("use crate::math::{Vec2, Vec3 as V3};\npub fn f(a: Vec3) -> f64 { 0.0 }") + uses = list(crate.uses.values())[0] + assert uses["Vec2"] == "math::Vec2" + assert uses["V3"] == "math::Vec3" + + +# ── the shipped package ───────────────────────────────────────────────── + + +def test_the_package_declares_itself_typed(): + assert os.path.exists(os.path.join(BINDINGS, "python", "numeria", "py.typed")) + + +def test_every_module_has_a_stub_and_the_stub_agrees(): + """`check_stubs.py` is the real check; this runs it.""" + result = subprocess.run( + [sys.executable, os.path.join(BINDINGS, "check_stubs.py")], + capture_output=True, + text=True, + ) + assert result.returncode == 0, result.stdout + result.stderr + + +def test_the_committed_bindings_match_the_library_source(): + """Regenerating must be a no-op, or what is committed is stale.""" + result = subprocess.run( + [sys.executable, os.path.join(BINDINGS, "generate.py"), "--check"], + capture_output=True, + text=True, + ) + assert result.returncode == 0, result.stdout + result.stderr + + +def test_the_coverage_report_exists_and_reports_most_of_the_api(): + path = os.path.join(BINDINGS, "COVERAGE.md") + assert os.path.exists(path) + text = open(path, encoding="utf-8").read() + assert "Free functions" in text + # Pull the bound/total counts out of the totals table. + import re + + row = re.search(r"\| Free functions \| (\d+) \| (\d+) \|", text) + assert row, "the totals table changed shape" + total, bound = int(row.group(1)), int(row.group(2)) + assert bound / total > 0.95, f"only {bound} of {total} free functions are bound" diff --git a/bindings/python/tests/test_identified_types.py b/bindings/python/tests/test_identified_types.py new file mode 100644 index 0000000..3a1afa4 --- /dev/null +++ b/bindings/python/tests/test_identified_types.py @@ -0,0 +1,82 @@ +"""Three Rust types that are Python types. + +`Complex`, `BigInt` and `Rational` have exact Python counterparts, so they +are translated rather than wrapped. The test a translation has to pass is +that the round trip loses nothing -- these check it at sizes and precisions +where a lossy conversion would show. +""" + +from fractions import Fraction + +import pytest + +import numeria as nm + + +def test_complex_goes_both_ways_as_the_builtin(): + spectrum = nm.transforms.fft.fft([1.0, 2.0, 3.0, 4.0]) + assert all(isinstance(z, complex) for z in spectrum) + # DC bin is the sum. + assert spectrum[0] == pytest.approx(10 + 0j) + # And a Python complex is accepted on the way in. + back = nm.transforms.fft.ifft([complex(10, 0), -2 + 2j, -2 + 0j, -2 - 2j]) + assert [z.real for z in back] == pytest.approx([1.0, 2.0, 3.0, 4.0]) + + +def test_a_float_is_accepted_where_a_complex_is_expected(): + assert nm.transforms.fft.fft([1, 0, 0, 0])[0] == pytest.approx(1 + 0j) + + +def test_bigint_is_a_python_int_of_any_size(): + small = nm.exact.bigint.factorial(10) + assert isinstance(small, int) + assert small == 3_628_800 + + # 100! has 158 digits: far past anything an f64 or an i64 can hold, so + # a conversion that went through either would be visibly wrong. + big = nm.exact.bigint.factorial(100) + import math + + assert big == math.factorial(100) + assert len(str(big)) == 158 + + +def test_a_huge_python_int_survives_the_trip_in_and_out(): + n = (1 << 4000) - 1 + # gcd(n, n) == n exercises the conversion in both directions. + assert nm.exact.bigint.gcd(n, n) == n + assert nm.exact.bigint.gcd(-n, n) == n + + +def test_modular_exponentiation_agrees_with_python(): + base, exp, mod = 3, 10**40 + 7, 2**61 - 1 + assert nm.exact.bigint.mod_pow(base, exp, mod) == pow(base, exp, mod) + + +def test_rational_is_a_fraction_and_stays_exact(): + third = Fraction(1, 3) + doubled = nm.exact.rational.add(third, third) + assert isinstance(doubled, Fraction) + assert doubled == Fraction(2, 3) + + # A tenth is exactly a tenth, which it would not be through a float. + tenth = nm.exact.rational.add(Fraction(1, 20), Fraction(1, 20)) + assert tenth == Fraction(1, 10) + assert tenth != 0.1 + + +def test_a_plain_int_is_a_rational(): + assert nm.exact.rational.add(2, Fraction(1, 2)) == Fraction(5, 2) + + +def test_a_float_is_refused_where_a_rational_is_expected(): + """0.1 is not one tenth, and a module whose point is exactness should + not pretend otherwise.""" + with pytest.raises(TypeError): + nm.exact.rational.add(0.1, Fraction(1, 10)) + + +def test_continued_fractions_round_trip(): + cf = nm.exact.rational.to_continued_fraction(Fraction(415, 93)) + assert cf == [4, 2, 6, 7] + assert all(isinstance(x, int) for x in cf) diff --git a/bindings/python/tests/test_module_layout.py b/bindings/python/tests/test_module_layout.py new file mode 100644 index 0000000..cd94854 --- /dev/null +++ b/bindings/python/tests/test_module_layout.py @@ -0,0 +1,101 @@ +"""The shape of the package: what is importable, and by what name. + +The Rust module tree is the map users navigate by. If `linalg.lu` is a +module in Rust it should be one here, reachable by attribute *and* by +`import`, and holding the same names. These tests fix that correspondence +so a change to the generator cannot quietly rearrange it. +""" + +import importlib +import sys + +import pytest + +import numeria as nm + + +def test_version_is_reported(): + assert nm.__version__ + assert nm.__version__[0].isdigit() + + +def test_every_top_level_rust_module_is_an_attribute(): + # A representative spread rather than all 71: enough that a wholesale + # regression shows up, few enough that adding a module to the library + # does not fail this test. + for name in ( + "classical", "linalg", "numerical", "statistics", "quantum", + "transforms", "geometry", "spatial", "exact", "units", "math", + "optimization", "graph", "audio", "fem", "manifold", "codes", + ): + assert hasattr(nm, name), name + + +def test_submodules_are_importable_by_name(): + """`import numeria.linalg.lu` has to work. + + PyO3 builds the tree as attributes of the extension module, which is + enough for attribute access and not enough for `import`: that goes + through `sys.modules`. `__init__.py` installs them; this is the check + that it did. + """ + mod = importlib.import_module("numeria.linalg.lu") + assert hasattr(mod, "solve") + from numeria.numerical import integrate + + assert hasattr(integrate, "simpson") + + +def test_attribute_and_import_give_the_same_object(): + from numeria import linalg + + assert linalg.lu is sys.modules["numeria.linalg.lu"] + assert nm.linalg.lu is linalg.lu + + +def test_module_docstrings_come_from_the_rust_headers(): + assert "Newtonian" in nm.classical.__doc__ + assert nm.linalg.__doc__ + + +def test_function_docstrings_survive_the_crossing(): + doc = nm.classical.projectile_range.__doc__ + assert "Range of a projectile" in doc + # Every docstring names the Rust item it came from, so a reader can + # follow it back to the source. + assert "classical::projectile_range" in doc + + +def test_signatures_are_introspectable(): + import inspect + + sig = inspect.signature(nm.classical.projectile_range) + assert list(sig.parameters) == ["speed", "angle_rad", "g"] + + +def test_keyword_arguments_work(): + a = nm.classical.projectile_range(20.0, 0.7853981633974483, 9.80665) + b = nm.classical.projectile_range(g=9.80665, speed=20.0, angle_rad=0.7853981633974483) + assert a == b + + +def test_constants_are_exposed_both_ways(): + assert nm.constants is sys.modules["numeria.math.constants"] + assert nm.math.constants.C == 299_792_458.0 + # The 2019 SI redefinition made these exact, and the crate says so. + assert nm.constants.H == 6.626_070_15e-34 + assert nm.constants.K_B == 1.380_649e-23 + + +def test_the_package_docstring_example_actually_runs(): + """The example in `__init__.py` is a doctest, so it cannot go stale.""" + import doctest + + result = doctest.testmod(nm, verbose=False) + assert result.attempted > 0 + assert result.failed == 0 + + +def test_a_module_that_does_not_exist_raises(): + with pytest.raises(ImportError): + importlib.import_module("numeria.not_a_module") diff --git a/bindings/python/tests/test_mutation_and_threads.py b/bindings/python/tests/test_mutation_and_threads.py new file mode 100644 index 0000000..256d12f --- /dev/null +++ b/bindings/python/tests/test_mutation_and_threads.py @@ -0,0 +1,130 @@ +"""Two things that are easy to get quietly wrong. + +`&mut [f64]` is an output written through an argument. A binding that +copied the values in and dropped the results would compile, run, and give +the wrong answer with no error anywhere -- so it is checked directly. + +Releasing the GIL is the other. It is done for calls that take or return +arrays, which is where it pays; the test is that the results are still +right when several threads do it at once, and that it actually overlaps. +""" + +import math +import threading + +import pytest + +import numeria as nm + + +def test_an_in_place_argument_is_written_back(): + """`velocity_verlet` advances position and velocity through `&mut [f64]`. + + It returns nothing: the whole result is the mutation. A binding that + copied the lists in and threw the results away would run cleanly and + leave both lists untouched. + """ + dt = 0.01 + x = [1.0] + v = [0.0] + nm.numerical.ode.symplectic.velocity_verlet(lambda pos: [-p for p in pos], x, v, dt) + + # v½ = -dt/2; x₁ = x + v½·dt; v₁ = v½ - (dt/2)·x₁. + half = -0.5 * dt + expected_x = 1.0 + half * dt + expected_v = half - 0.5 * dt * expected_x + assert x == [pytest.approx(expected_x)] + assert v == [pytest.approx(expected_v)] + + +def test_stepping_repeatedly_traces_the_oscillator(): + """Successive calls have to see the previous call's writes.""" + x, v = [1.0], [0.0] + dt = 0.001 + for _ in range(1000): + nm.numerical.ode.symplectic.velocity_verlet( + lambda pos: [-p for p in pos], x, v, dt + ) + # One radian of a unit-frequency oscillator: x = cos(1), v = -sin(1). + assert x[0] == pytest.approx(math.cos(1.0), abs=1e-6) + assert v[0] == pytest.approx(-math.sin(1.0), abs=1e-6) + + +def test_an_in_place_argument_must_be_something_that_can_receive_the_result(): + with pytest.raises(TypeError) as excinfo: + nm.numerical.ode.symplectic.velocity_verlet( + lambda pos: [-p for p in pos], (1.0,), [0.0], 0.01 + ) + assert "in place" in str(excinfo.value) + + +def test_results_are_right_under_concurrency(): + """Array calls release the GIL. Doing that wrong corrupts results.""" + data = [math.sin(i / 7.0) for i in range(20_000)] + expected = nm.statistics.descriptive.mean(data) + results = [] + errors = [] + + def work(): + try: + for _ in range(20): + results.append(nm.statistics.descriptive.mean(data)) + except Exception as exc: # pragma: no cover - a failure is the point + errors.append(exc) + + threads = [threading.Thread(target=work) for _ in range(8)] + for t in threads: + t.start() + for t in threads: + t.join() + + assert not errors + assert len(results) == 160 + assert all(r == expected for r in results) + + +def test_a_callback_from_several_threads_stays_correct(): + """Callbacks hold the GIL; each call still has to see its own state.""" + outcomes = {} + lock = threading.Lock() + + def work(k): + got = nm.numerical.integrate.simpson(lambda x: x**k, 0.0, 1.0, 2000) + with lock: + outcomes[k] = got + + threads = [threading.Thread(target=work, args=(k,)) for k in range(1, 7)] + for t in threads: + t.start() + for t in threads: + t.join() + + # ∫₀¹ xᵏ dx = 1/(k+1) + for k, got in outcomes.items(): + assert got == pytest.approx(1.0 / (k + 1), abs=1e-9) + + +def test_an_exception_on_one_thread_does_not_disturb_another(): + """The panic guard's state is thread-local; it had better be.""" + ok = [] + caught = [] + + def good(): + for _ in range(200): + ok.append(nm.classical.acceleration(10.0, 2.0)) + + def bad(): + for _ in range(200): + try: + nm.classical.acceleration(10.0, -1.0) + except nm.InvalidArgumentError: + caught.append(1) + + threads = [threading.Thread(target=good), threading.Thread(target=bad)] + for t in threads: + t.start() + for t in threads: + t.join() + + assert len(caught) == 200 + assert ok == [5.0] * 200 diff --git a/bindings/python/tests/test_numerics.py b/bindings/python/tests/test_numerics.py new file mode 100644 index 0000000..80743fd --- /dev/null +++ b/bindings/python/tests/test_numerics.py @@ -0,0 +1,120 @@ +"""The numbers that come back are the right numbers. + +The library tests its own mathematics thoroughly; these tests are not a +second opinion on that. They check the crossing: that arguments arrive in +the order and the units the Rust function expects, that a `Vec` comes +back as a list of the same length in the same order, and that nothing is +transposed, truncated or scaled on the way through. Each expected value is +a closed form or an exact identity, so a wrong answer is a wrong binding +rather than a stale golden file. +""" + +import math + +import pytest + +import numeria as nm + + +def test_projectile_range_matches_the_closed_form(): + speed, angle, g = 20.0, math.radians(45.0), 9.80665 + got = nm.classical.projectile_range(speed, angle, g) + assert got == pytest.approx(speed**2 * math.sin(2 * angle) / g) + # 45 degrees maximises the range, which is a fact about the physics + # and so a check that the angle is in radians and in the right place. + for other in (30.0, 60.0, 44.0, 46.0): + assert nm.classical.projectile_range(speed, math.radians(other), g) < got + + +def test_arguments_are_not_silently_reordered(): + # displacement(v0, a, t) = v0*t + a*t²/2. Asymmetric in its arguments, + # so any permutation gives a different number. + assert nm.classical.displacement(3.0, 2.0, 4.0) == pytest.approx(3 * 4 + 0.5 * 2 * 16) + + +def test_orbital_and_escape_velocities_are_related_by_root_two(): + m, r = 5.972e24, 6.371e6 + v_orbit = nm.gravitation.orbital_velocity(m, r) + v_escape = nm.gravitation.escape_velocity(m, r) + assert v_escape == pytest.approx(v_orbit * math.sqrt(2.0)) + # And low Earth orbit really is about 7.9 km/s. + assert 7_800 < v_orbit < 8_000 + + +def test_a_linear_system_solves_to_the_known_answer(): + # [[2,1],[1,3]] x = [5,10] -> x = (1, 3) + x = nm.linalg.lu.solve([[2.0, 1.0], [1.0, 3.0]], [5.0, 10.0]) + assert x == pytest.approx([1.0, 3.0]) + + +def test_a_list_of_rows_and_a_Matrix_are_interchangeable(): + rows = [[4.0, 1.0], [1.0, 3.0]] + m = nm.linalg.Matrix.from_rows(rows) + assert nm.linalg.lu.solve(rows, [1.0, 2.0]) == pytest.approx( + nm.linalg.lu.solve(m, [1.0, 2.0]) + ) + + +def test_the_fft_of_a_delta_is_flat_and_inverts(): + x = [1.0, 0.0, 0.0, 0.0] + spectrum = nm.transforms.fft.fft(x) + assert len(spectrum) == 4 + assert all(isinstance(z, complex) for z in spectrum) + assert all(z == pytest.approx(1 + 0j) for z in spectrum) + back = nm.transforms.fft.ifft(spectrum) + assert [z.real for z in back] == pytest.approx(x, abs=1e-12) + + +def test_a_pure_tone_lands_in_one_bin(): + n = 64 + k = 5 + signal = [complex(math.cos(2 * math.pi * k * i / n), 0.0) for i in range(n)] + mags = [abs(z) for z in nm.transforms.fft.fft(signal)] + peak = max(range(n), key=lambda i: mags[i]) + assert peak in (k, n - k) + + +def test_descriptive_statistics_on_a_known_sample(): + data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0] + assert nm.statistics.descriptive.mean(data) == pytest.approx(5.0) + # Population variance of this textbook sample is exactly 4. + assert nm.statistics.descriptive.variance(data) == pytest.approx(4.0) + assert nm.statistics.descriptive.std_deviation(data) == pytest.approx(2.0) + + +def test_gamma_of_a_half_integer_is_root_pi(): + assert nm.special.gamma.gamma(0.5) == pytest.approx(math.sqrt(math.pi)) + assert nm.special.gamma.gamma(5.0) == pytest.approx(24.0) + + +def test_a_long_vector_survives_the_crossing_intact(): + """The GIL is released for array calls; the data must still be right.""" + n = 4096 + data = [math.sin(i / 10.0) for i in range(n)] + out = nm.statistics.descriptive.mean(data) + assert out == pytest.approx(sum(data) / n) + + +def test_geodesy_reaches_an_associated_constant_and_a_tuple_return(): + """`Ellipsoid::WGS84` is a Rust associated constant, and the return is + a three-tuple. Both have to survive.""" + wgs84 = nm.geometry.geodesy.Ellipsoid.WGS84 + assert wgs84.a == pytest.approx(6_378_137.0) + assert wgs84.f == pytest.approx(1.0 / 298.257223563) + + # London to New York is about 5,570 km along the geodesic. + dist, az_fwd, az_rev = nm.geometry.geodesy.vincenty_inverse( + math.radians(51.5074), math.radians(-0.1278), + math.radians(40.7128), math.radians(-74.0060), + wgs84, + ) + assert 5_500_000 < dist < 5_600_000 + assert -math.pi <= az_fwd <= math.pi or 0 <= az_fwd <= 2 * math.pi + + # A bare `(a, f)` pair stands in for the Ellipsoid. + same, _, _ = nm.geometry.geodesy.vincenty_inverse( + math.radians(51.5074), math.radians(-0.1278), + math.radians(40.7128), math.radians(-74.0060), + (6_378_137.0, 1.0 / 298.257223563), + ) + assert same == pytest.approx(dist) diff --git a/bindings/python/tests/test_shapes.py b/bindings/python/tests/test_shapes.py new file mode 100644 index 0000000..e2bd70e --- /dev/null +++ b/bindings/python/tests/test_shapes.py @@ -0,0 +1,98 @@ +"""Signatures that need more than a value copied across. + +Four shapes in the Rust API do not map to a Python argument by themselves, +and each gets its own treatment in the generator. If any of them regresses +the failure is quiet -- the call still runs, it just does nothing, or does +it to a copy -- so each has a test that would notice. +""" + +import math + +import pytest + +import numeria as nm + + +def test_a_builder_returns_the_same_object_so_calls_chain(): + """`&mut self -> &mut Self` is Rust's chaining idiom. + + Handing back a copy would compile and read the same, and would throw + away every gate after the first. + """ + c = nm.quantum.circuit.Circuit(2) + returned = c.h(0) + assert returned is c + + c.cx(0, 1) + state = c.run(nm.quantum.circuit.QState.zero(2)) + probabilities = [round(abs(z) ** 2, 6) for z in state.amps] + # A Bell pair: |00> and |11> at one half each, nothing in between. + assert probabilities == [0.5, 0.0, 0.0, 0.5] + + +def test_chaining_in_one_expression_keeps_every_gate(): + c = nm.quantum.circuit.Circuit(3) + c.h(0).cx(0, 1).cx(1, 2) + state = c.run(nm.quantum.circuit.QState.zero(3)) + probabilities = [round(abs(z) ** 2, 6) for z in state.amps] + assert probabilities[0] == 0.5 + assert probabilities[7] == 0.5 + assert sum(probabilities[1:7]) == 0.0 + + +def test_a_mutable_slice_of_objects_is_written_back(): + """`&mut [Vec2]` -- the list is the output, and the elements are + wrapper objects rather than numbers.""" + points = [ + nm.math.Vec2(3.0, 3.0), + nm.math.Vec2(0.0, 0.0), + nm.math.Vec2(1.0, 1.0), + ] + nm.patterns.space_filling.sort_by_hilbert(points, 4) + ordered = [p.tolist() for p in points] + # Every point is still there, as a Vec2, and the order has changed: + # the Hilbert index rises along this diagonal. + assert sorted(ordered) == [[0.0, 0.0], [1.0, 1.0], [3.0, 3.0]] + assert ordered[0] == [0.0, 0.0] + assert ordered[-1] == [3.0, 3.0] + assert all(isinstance(p, nm.math.Vec2) for p in points) + + +def test_a_borrowed_argument_whose_type_is_not_clone(): + """Some types have no `Clone`, so the wrapper is borrowed rather than + copied. The call still has to work.""" + grid = nm.cfd.grid.MacGrid2(8, 8, 1.0) + field = nm.cfd.grid.CellField2(8, 8, 1.0) + out = nm.cfd.advection.advect_semi_lagrangian_2d(field, grid, 0.1) + assert out is not None + + +def test_an_associated_constant_is_a_class_attribute(): + assert nm.math.Vec3.ZERO.tolist() == [0.0, 0.0, 0.0] + assert nm.units.quantity.Dim.LENGTH.exponents() == [1, 0, 0, 0, 0, 0, 0] + + +def test_a_reexport_is_reachable_by_its_short_path(): + """`linalg` re-exports `Matrix` and `solve`; the crate's own docs use + the short names.""" + assert nm.linalg.Matrix is nm.linalg.matrix.Matrix + assert nm.linalg.solve is nm.linalg.lu.solve + + +def test_a_submodule_wins_a_name_it_shares_with_a_reexport(): + """`special::gamma` is both a module and a re-exported function. + + Python has one namespace, so the module keeps the name and the + function stays reachable one level down. COVERAGE.md says so too. + """ + import types + + assert isinstance(nm.special.gamma, types.ModuleType) + assert nm.special.gamma.gamma(0.5) == pytest.approx(math.sqrt(math.pi)) + assert isinstance(nm.transforms.fft, types.ModuleType) + assert nm.transforms.fft.fft([1.0, 0.0]) + + +def test_an_optional_argument_defaults_to_none(): + sig = nm.mesh.analyze.self_intersections.__text_signature__ + assert "bvh=None" in sig diff --git a/bindings/python/tests/test_surface.py b/bindings/python/tests/test_surface.py new file mode 100644 index 0000000..2fa29cd --- /dev/null +++ b/bindings/python/tests/test_surface.py @@ -0,0 +1,158 @@ +"""A sweep over the whole bound surface. + +The other tests check particular functions carefully. This one checks +every function shallowly: that the module tree registered without gaps, +that nothing is missing a docstring or a signature, and that no name in it +raises on mere access. A registration bug -- a module attached to the +wrong parent, a class registered twice, a getter that panics -- shows up +here and nowhere else, because no hand-written test would ever call the +function it broke. +""" + +import inspect +import sys + +import pytest + +import numeria as nm + + +def _all_modules(): + return [ + sys.modules[f"numeria.{d}"] for d in nm._core.__submodules__ + ] + + +def test_the_tree_is_large_and_complete(): + mods = _all_modules() + assert len(mods) > 250 + # Every module the extension advertises really is in sys.modules. + assert all(m is not None for m in mods) + + +def test_every_module_is_attached_under_the_right_parent(): + for dotted in nm._core.__submodules__: + parent_name, _, leaf = dotted.rpartition(".") + parent = ( + sys.modules[f"numeria.{parent_name}"] if parent_name else nm + ) + assert getattr(parent, leaf) is sys.modules[f"numeria.{dotted}"] + + +def test_no_public_name_raises_on_access(): + """Getters run Rust code. One that panics would only show up here.""" + failures = [] + for mod in _all_modules(): + for name in dir(mod): + if name.startswith("_"): + continue + try: + getattr(mod, name) + except Exception as exc: # pragma: no cover - a failure is the point + failures.append(f"{mod.__name__}.{name}: {exc!r}") + assert not failures, failures[:20] + + +def test_thousands_of_functions_are_bound(): + total = 0 + for mod in _all_modules(): + for name in dir(mod): + if name.startswith("_"): + continue + if inspect.isbuiltin(getattr(mod, name)): + total += 1 + assert total > 3500, f"only {total} functions bound" + + +def test_every_function_has_a_docstring_and_a_signature(): + missing_doc = [] + missing_sig = [] + for mod in _all_modules(): + for name in dir(mod): + if name.startswith("_"): + continue + obj = getattr(mod, name) + if not inspect.isbuiltin(obj): + continue + if not (obj.__doc__ or "").strip(): + missing_doc.append(f"{mod.__name__}.{name}") + if not (obj.__text_signature__ or ""): + missing_sig.append(f"{mod.__name__}.{name}") + assert not missing_doc, missing_doc[:20] + assert not missing_sig, missing_sig[:20] + + +def test_every_docstring_names_its_rust_origin(): + """So a reader can follow any function back to the source.""" + sampled = 0 + for mod in _all_modules(): + seen_here = 0 + for name in sorted(dir(mod)): + if name.startswith("_") or seen_here >= 3: + continue + obj = getattr(mod, name) + if not inspect.isbuiltin(obj): + continue + assert "Rust: `" in obj.__doc__, f"{mod.__name__}.{name}" + seen_here += 1 + sampled += 1 + assert sampled > 200 + + +def test_hundreds_of_classes_are_bound_and_printable(): + classes = [] + for mod in _all_modules(): + for name in dir(mod): + if name.startswith("_"): + continue + obj = getattr(mod, name) + if isinstance(obj, type) and getattr(obj, "__module__", "").startswith( + "numeria" + ): + classes.append(obj) + # Aliases mean a class can appear twice; count the distinct ones. + assert len({(c.__module__, c.__name__) for c in classes}) > 350 + for cls in classes: + assert cls.__doc__, cls.__name__ + + +def test_no_name_collides_with_a_submodule(): + """A function and a submodule of the same name would shadow each other.""" + for dotted in nm._core.__submodules__: + mod = sys.modules[f"numeria.{dotted}"] + children = { + d.rsplit(".", 1)[1] + for d in nm._core.__submodules__ + if d.startswith(dotted + ".") and d.count(".") == dotted.count(".") + 1 + } + for child in children: + attr = getattr(mod, child) + assert attr is sys.modules[f"numeria.{dotted}.{child}"], ( + f"{dotted}.{child} is shadowed" + ) + + +def test_calling_a_representative_function_from_every_top_level_module(): + """Cheap end-to-end proof that each module's registration works.""" + import math + + checks = { + "acoustics": lambda: nm.acoustics.sabine_reverberation(100.0, 20.0), + "chemistry": lambda: nm.chemistry.half_life_first_order(0.1), + "classical": lambda: nm.classical.force(2.0, 3.0), + "electromagnetism": lambda: nm.electromagnetism.coulomb_force(1e-6, 1e-6, 1.0), + "gravitation": lambda: nm.gravitation.escape_velocity(5.972e24, 6.371e6), + "information_theory": lambda: nm.information_theory.shannon_entropy([0.5, 0.5]), + "linalg": lambda: nm.linalg.lu.solve([[1.0, 0.0], [0.0, 1.0]], [1.0, 2.0]), + "math": lambda: nm.math.Vec3(1.0, 0.0, 0.0).magnitude(), + "numerical": lambda: nm.numerical.integrate.simpson(math.sin, 0.0, 1.0, 100), + "optics": lambda: nm.optics.snells_law(1.0, 0.5, 1.5), + "quantum": lambda: nm.quantum.photon_energy(5e14), + "relativity": lambda: nm.relativity.lorentz_factor(0.5 * 299_792_458.0), + "statistics": lambda: nm.statistics.descriptive.mean([1.0, 2.0, 3.0]), + "thermodynamics": lambda: nm.thermodynamics.carnot_efficiency(300.0, 600.0), + "transforms": lambda: nm.transforms.fft.fft([1.0, 0.0]), + } + for name, call in checks.items(): + value = call() + assert value is not None, name diff --git a/bindings/python/tests/test_value_types.py b/bindings/python/tests/test_value_types.py new file mode 100644 index 0000000..df5a62f --- /dev/null +++ b/bindings/python/tests/test_value_types.py @@ -0,0 +1,134 @@ +"""Wrapper classes, and the literals that may stand in for them. + +`Vec3(1, 2, 3)` and `(1, 2, 3)` should be interchangeable as arguments, +because requiring the constructor everywhere would make a Rust API read +like a Rust API. The classes still have to behave as Python objects: they +index, iterate, compare, copy and print. +""" + +import copy +import math + +import pytest + +import numeria as nm + + +def test_a_tuple_stands_in_for_a_vector(): + a = nm.classical.position_3d((0.0, 0.0, 100.0), (5.0, 0.0, 0.0), (0.0, 0.0, -9.81), 2.0) + b = nm.classical.position_3d( + nm.math.Vec3(0.0, 0.0, 100.0), + nm.math.Vec3(5.0, 0.0, 0.0), + nm.math.Vec3(0.0, 0.0, -9.81), + 2.0, + ) + assert a.tolist() == pytest.approx(b.tolist()) + assert a.tolist() == pytest.approx([10.0, 0.0, 100.0 - 0.5 * 9.81 * 4]) + + +def test_a_list_stands_in_too_and_the_wrong_length_is_rejected(): + assert nm.math.Vec3(1, 0, 0).dot([0, 1, 0]) == 0.0 + with pytest.raises(TypeError) as excinfo: + nm.math.Vec3(1, 0, 0).dot([0, 1]) + assert "Vec3" in str(excinfo.value) + + +def test_vectors_behave_like_sequences(): + v = nm.math.Vec3(1.0, 2.0, 2.0) + assert len(v) == 3 + assert v[0] == 1.0 and v[-1] == 2.0 + assert list(v) == [1.0, 2.0, 2.0] + assert v.tolist() == [1.0, 2.0, 2.0] + with pytest.raises(IndexError): + v[3] + + +def test_vector_algebra_through_operators(): + a = nm.math.Vec3(1.0, 2.0, 3.0) + b = nm.math.Vec3(4.0, 5.0, 6.0) + assert (a + b).tolist() == [5.0, 7.0, 9.0] + assert (b - a).tolist() == [3.0, 3.0, 3.0] + assert (a * 2.0).tolist() == [2.0, 4.0, 6.0] + assert (-a).tolist() == [-1.0, -2.0, -3.0] + assert a.dot(b) == 32.0 + assert a.cross(b).tolist() == [-3.0, 6.0, -3.0] + assert nm.math.Vec3(3.0, 4.0, 0.0).magnitude() == 5.0 + + +def test_equality_repr_and_copying(): + v = nm.math.Vec3(1.0, 2.0, 3.0) + assert v == nm.math.Vec3(1.0, 2.0, 3.0) + assert v != nm.math.Vec3(1.0, 2.0, 4.0) + assert repr(v) == "Vec3(x=1.0, y=2.0, z=3.0)" + assert copy.copy(v) == v + assert copy.deepcopy(v) == v + assert type(v).__module__ == "numeria.math" + + +def test_fields_are_readable_and_writable(): + v = nm.math.Vec3(1.0, 2.0, 3.0) + assert (v.x, v.y, v.z) == (1.0, 2.0, 3.0) + v.x = 10.0 + assert v.x == 10.0 + assert v.tolist() == [10.0, 2.0, 3.0] + + +def test_matrix_indexing_and_shape(): + m = nm.linalg.Matrix.from_rows([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]) + assert m.shape == (2, 3) + assert len(m) == 2 + assert m[0, 2] == 3.0 + assert m[1] == [4.0, 5.0, 6.0] + assert m[-1, -1] == 6.0 + m[0, 0] = 9.0 + assert m[0, 0] == 9.0 + assert m.tolist() == [[9.0, 2.0, 3.0], [4.0, 5.0, 6.0]] + with pytest.raises(IndexError): + m[5, 0] + + +def test_quaternion_rotation_is_a_rotation(): + q = nm.quaternion.Quaternion.from_axis_angle((0.0, 0.0, 1.0), math.pi / 2) + turned = q.rotate_vec((1.0, 0.0, 0.0)) + assert turned.tolist() == pytest.approx([0.0, 1.0, 0.0], abs=1e-12) + assert q.normalize().tolist() == pytest.approx(q.tolist()) + + +def test_a_stateful_object_keeps_its_state_across_calls(): + """A wrapper holds the Rust value, so `&mut self` methods really mutate.""" + rng = nm.monte_carlo.Rng(12345) + first = [rng.next_f64() for _ in range(5)] + assert all(0.0 <= x < 1.0 for x in first) + assert len(set(first)) == 5 + + # And the same seed replays exactly. + again = nm.monte_carlo.Rng(12345) + assert [again.next_f64() for _ in range(5)] == first + + +def test_an_rng_passed_into_a_free_function_advances(): + """`&mut Rng` arguments are the wrapper itself, not a copy of it.""" + rng = nm.monte_carlo.Rng(7) + walk = nm.monte_carlo.random_walk_1d(50, 1.0, rng) + assert len(walk) == 51 + # The walk moved: if the RNG had been copied in and thrown away, every + # step would repeat the same draw. + assert len(set(walk)) > 5 + + # A second walk from the advanced generator differs from the first. + assert nm.monte_carlo.random_walk_1d(50, 1.0, rng) != walk + + # And pi comes out near pi, which needs both the draws and the state. + fresh = nm.monte_carlo.Rng(99) + assert nm.monte_carlo.mc_estimate_pi(200_000, fresh) == pytest.approx(math.pi, abs=0.02) + + +def test_a_unit_variant_enum_crosses_as_a_class_with_members(): + """Fieldless Rust enums become Python enums: comparable, printable, + and accepted wherever the Rust function wants the enum.""" + compounding = nm.finance.rates.Compounding + members = [n for n in dir(compounding) if not n.startswith("_")] + assert members + first = getattr(compounding, members[0]) + assert first == getattr(compounding, members[0]) + assert repr(first).startswith("Compounding.")