diff --git a/.artifacts/docs/leaky.md b/.artifacts/docs/leaky.md new file mode 100644 index 00000000..136d787d --- /dev/null +++ b/.artifacts/docs/leaky.md @@ -0,0 +1 @@ +# Leaky Integrate-and-Fire (LIF) Neuron diff --git a/.envrc b/.envrc index bd53aca3..b10b3541 100644 --- a/.envrc +++ b/.envrc @@ -1 +1,3 @@ CARGO_TERM_COLOR=always +RUST_BACKTRACE=full +RUST_LOG=debug,concision=info \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 426cdbba..476181c4 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,11 +1,23 @@ version: 2 updates: + - package-ecosystem: devcontainers + directory: / + schedule: + interval: monthly + - package-ecosystem: github-actions + directory: / + schedule: + interval: monthly + - package-ecosystem: rust-toolchain + directory: / + schedule: + interval: monthly - package-ecosystem: cargo schedule: interval: monthly directories: - - . + - / - /concision - /core - /data @@ -15,11 +27,3 @@ updates: - /macros - /params - /traits - - package-ecosystem: devcontainers - directory: / - schedule: - interval: monthly - - package-ecosystem: github-actions - directory: / - schedule: - interval: monthly diff --git a/.github/workflows/cargo-bench.yml b/.github/workflows/cargo-bench.yml new file mode 100644 index 00000000..a6633422 --- /dev/null +++ b/.github/workflows/cargo-bench.yml @@ -0,0 +1,68 @@ +# This workflow is used to benchmark a Rust project +name: cargo-bench +# The workflow may be manually triggered and is run automatically when: +# - a pull request is closed on the default branch (main/master) +# - a release is published +# - a repository_dispatch event with the type "cargo-bench" is received + +concurrency: + cancel-in-progress: false + group: ${{ github.workflow }}-${{ github.ref }} + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: full + +on: + pull_request: + branches: [main, master] + types: [closed] + push: + tags: [latest, v*.*.*, "*-nightly"] + repository_dispatch: + types: [cargo-bench] + workflow_dispatch: + +permissions: + contents: write + checks: write + +jobs: + cargo-bench: + name: Benchmark + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + features: [full] + target: [x86_64-unknown-linux-gnu] + outputs: + id: ${{ steps.artifacts.outputs.artifact-id }} + digest: ${{ steps.artifacts.outputs.artifact-digest }} + url: ${{ steps.artifacts.outputs.artifact-url }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + repository: ${{ github.repository }} + ref: ${{ github.event.client_payload.ref || github.ref }} + token: ${{ github.token }} + - name: Setup Rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + cache-key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + target: ${{ matrix.target }} + - name: Install cargo-criterion + uses: taiki-e/install-action@v2 + with: + tool: cargo-criterion + - name: Benchmark the workspace (${{ matrix.target }}) + run: cargo criterion -v --benches --locked --workspace --features ${{ matrix.features }} --target ${{ matrix.target }} + - name: Upload the results + id: artifacts + uses: actions/upload-artifact@v6 + with: + name: Benchmark Report (${{ github.run_id }}) + if-no-files-found: error + overwrite: true + path: target/criterion/ diff --git a/.github/workflows/cargo-clippy.yml b/.github/workflows/cargo-clippy.yml index 7ffd2685..eff51934 100644 --- a/.github/workflows/cargo-clippy.yml +++ b/.github/workflows/cargo-clippy.yml @@ -1,4 +1,4 @@ -name: Clippy +name: clippy concurrency: cancel-in-progress: false @@ -6,45 +6,46 @@ concurrency: on: pull_request: - branches: [main, master] - types: [opened, ready_for_review, reopened] + types: [synchronize] push: branches: [main, master] - tags: [v*.*.*, "*-nightly"] - release: - types: [created, edited] + tags: [latest, v*.*.*, "*-nightly"] repository_dispatch: types: [clippy, cargo-clippy] workflow_dispatch: +permissions: + actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status + contents: read + security-events: write + statuses: write + jobs: clippy: runs-on: ubuntu-latest - permissions: - actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status - contents: read - security-events: write - statuses: write steps: - name: Checkout uses: actions/checkout@v6 + with: + fetch-depth: 0 + repository: ${{ github.repository }} + ref: ${{ github.event.client_payload.ref || github.ref }} + token: ${{ github.token }} - name: Setup Rust uses: actions-rust-lang/setup-rust-toolchain@v1 with: cache-key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - components: clippy, rustfmt - toolchain: nightly - override: true - - name: Setup the for sarif output - run: cargo install clippy-sarif sarif-fmt + components: clippy,rustfmt + - uses: taiki-e/install-action@v2 + with: + tool: clippy-sarif sarif-fmt - name: Run Clippy - run: cargo clippy - --all-features - --workspace - --message-format=json | clippy-sarif | tee rust-clippy-results.sarif | sarif-fmt + run: | + cargo clippy \ + --features full \ + --message-format=json | clippy-sarif | tee rust-clippy-results.sarif | sarif-fmt - name: Upload analysis uses: github/codeql-action/upload-sarif@v4 - continue-on-error: true with: sarif_file: rust-clippy-results.sarif wait-for-processing: true diff --git a/.github/workflows/cargo-publish.yml b/.github/workflows/cargo-publish.yml index 25a28670..b55ee491 100644 --- a/.github/workflows/cargo-publish.yml +++ b/.github/workflows/cargo-publish.yml @@ -1,22 +1,21 @@ -name: Publish +name: cargo-publish concurrency: cancel-in-progress: false group: ${{ github.workflow }}-${{ github.ref }} +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: full + on: repository_dispatch: - types: [deploy, publish, cargo-publish, crates-io] + types: [crates-io, cargo-publish] workflow_dispatch: - inputs: - publish: - default: true - description: "Publish the crate(s) to crates.io?" - type: boolean -env: - CARGO_TERM_COLOR: always - RUST_BACKTRACE: full +permissions: + contents: read + deployments: write jobs: crates-io: @@ -24,12 +23,6 @@ jobs: runs-on: ubuntu-latest env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} - outputs: - url: ${{ steps.results.outputs.url }} - permissions: - contents: read - deployments: write - packages: write strategy: fail-fast: false max-parallel: 1 @@ -45,19 +38,15 @@ jobs: - concision - concision-ext steps: - - name: Checkout - uses: actions/checkout@v6 + - uses: actions/checkout@v6 with: fetch-depth: 0 - ref: ${{ github.ref }} + repository: ${{ github.repository }} + ref: ${{ github.event.client_payload.ref || github.ref }} token: ${{ secrets.GITHUB_TOKEN }} - name: Setup Rust uses: actions-rust-lang/setup-rust-toolchain@v1 with: cache-key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - name: Publish (${{ matrix.package }}) - id: publish run: cargo publish --locked --package ${{ matrix.package }} - - name: Set output(s) - id: results - run: echo "url=https://crates.io/crates/${{ matrix.package }}" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/cleanup.yml b/.github/workflows/cleanup.yml index f442853c..d7dc99d1 100644 --- a/.github/workflows/cleanup.yml +++ b/.github/workflows/cleanup.yml @@ -1,12 +1,11 @@ -name: Cleanup +name: cleanup on: pull_request: types: [closed] jobs: - pr_cache_cleanup: - name: Cleanup PR Cache(s) + cache_cleanup: runs-on: ubuntu-latest permissions: actions: write diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index 78f41832..a2d3351c 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -1,4 +1,4 @@ -name: "Nix" +name: nix concurrency: cancel-in-progress: false @@ -7,12 +7,10 @@ concurrency: on: pull_request: branches: [main, master] - types: [opened, ready_for_review, reopened] + types: [auto_merge_enabled, ready_for_review] push: branches: [main, master] - tags: [v*.*.*, "*-nightly"] - release: - types: [created, edited] + tags: [latest, v*.*.*, "*-nightly"] repository_dispatch: types: [nix, nix-build] workflow_dispatch: @@ -25,8 +23,7 @@ jobs: continue-on-error: true runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v6 + - uses: actions/checkout@v6 - name: Setup Nix uses: cachix/install-nix-action@v31 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8741cfe1..4b5be747 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -name: Release +name: release on: release: @@ -22,40 +22,34 @@ permissions: contents: write discussions: write -env: - IS_DRAFT: ${{ github.event.inputs.draft || github.event.release.draft || false }} - IS_PRERELEASE: ${{ github.event.inputs.prerelease || github.event.release.prerelease || false }} - jobs: - publish: - environment: - name: crates-io - url: https://crates.io/crates/concision + release: runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v6 + - uses: actions/checkout@v6 with: fetch-depth: 0 ref: ${{ github.ref }} repository: ${{ github.repository }} - - name: Publish to crates.io + token: ${{ github.token }} + - name: Dispatch & Publish to crates.io uses: peter-evans/repository-dispatch@v4 with: event-type: cargo-publish client-payload: '{"ref": "${{ github.ref }}", "sha": "${{ github.sha }}"}' token: ${{ github.token }} - - name: Create release + - name: Create GitHub Release uses: softprops/action-gh-release@v2 + continue-on-error: true with: append_body: false - draft: ${{ env.IS_DRAFT }} - prerelease: ${{ env.IS_PRERELEASE }} + draft: ${{ inputs.prerelease || github.event.release.prerelease || false }} + prerelease: ${{ inputs.draft || github.event.release.draft || false }} tag_name: ${{ github.event.release.tag_name }} body: | ${{ github.event.release.body }} ## Links - - [crates.io](https://crates.io/crates/${{ github.event.repository.name }}) - - [docs.rs](https://docs.rs/${{ github.event.repository.name }}) + - [Crates.io](https://crates.io/crates/${{ github.event.repository.name }}) + - [Documentation](https://docs.rs/${{ github.event.repository.name }}) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 412ceb9d..ad9d9768 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -1,34 +1,35 @@ -name: Rust +name: rust concurrency: cancel-in-progress: false group: ${{ github.workflow }}-${{ github.ref }} -env: - CARGO_TERM_COLOR: always - RUST_BACKTRACE: full - on: pull_request: branches: [main, master] - types: [edited, opened, ready_for_review, reopened, synchronize] + types: [synchronize] paths: - - '**.rs' - - 'Cargo.toml' - - 'Cargo.lock' - - '.github/workflows/rust.yml' + - ".github/workflows/rust.yml" + - "Cargo.toml" + - "Cargo.lock" + - "**/*.rs" push: branches: [main, master] - tags: [v*.*.*, "*-nightly"] + tags: [latest, v*.*.*, "*-nightly"] repository_dispatch: - types: [rust] + types: [rust, rust-ci] workflow_dispatch: inputs: - benchmark: - default: false - description: "Run benchmarks" - required: true - type: boolean + toolchain: + description: 'Toolchain to test (stable or nightly)' + required: false + default: 'stable' + type: choice + options: [stable, nightly] + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: full jobs: build: @@ -37,29 +38,26 @@ jobs: fail-fast: false matrix: target: [x86_64-unknown-linux-gnu] + toolchain: [stable] steps: - - name: Checkout - uses: actions/checkout@v6 + - uses: actions/checkout@v6 - name: Setup Rust uses: actions-rust-lang/setup-rust-toolchain@v1 with: cache-key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} target: ${{ matrix.target }} + toolchain: ${{ matrix.toolchain }} + override: true - name: Build the workspace - run: cargo build --release --locked --workspace --features full --target ${{ matrix.target }} - benchmark: - if: ${{ inputs.benchmark || github.event_name != 'pull_request' }} + run: cargo build -r --locked --workspace --features full --target ${{ matrix.target }} + test: + if: github.event_name != 'workflow_dispatch' || github.event.inputs.toolchain == 'nightly' + needs: build runs-on: ubuntu-latest - outputs: - digest: ${{ steps.artifacts.outputs.artifact-digest }} - id: ${{ steps.artifacts.outputs.artifact-id }} - url: ${{ steps.artifacts.outputs.artifact-url }} - permissions: - actions: read - contents: write strategy: fail-fast: false matrix: + features: [full, default] target: [x86_64-unknown-linux-gnu] steps: - name: Checkout @@ -69,35 +67,49 @@ jobs: with: cache-key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} target: ${{ matrix.target }} - - name: Benchmark the workspace - run: cargo bench --locked --verbose --workspace --target ${{ matrix.target }} --features full - - name: Upload the benchmarks - id: artifacts - uses: actions/upload-artifact@v5 - with: - name: Benchmark Report (${{ github.event.repository.name }}) - if-no-files-found: error - overwrite: true - path: target/criterion/ - test: + toolchain: stable + override: true + - name: Test (all-features) + if: matrix.features == 'all' + run: cargo test -r --locked --workspace --target ${{ matrix.target}} --all-features + - name: Test (default) + if: matrix.features == 'default' + run: cargo test -r --locked --workspace --target ${{ matrix.target}} + - name: Test (${{ matrix.features }}) + if: matrix.features != 'default' + run: cargo test -r --locked --workspace --target ${{ matrix.target}} --features ${{ matrix.features }} + test_nightly: + if: github.event.inputs.toolchain == 'nightly' || false + continue-on-error: true needs: build runs-on: ubuntu-latest strategy: fail-fast: false matrix: - features: [full, default] - target: [x86_64-unknown-linux-gnu] + features: + - all + - no_std + - "alloc,nightly" steps: - - name: Checkout - uses: actions/checkout@v6 + - uses: actions/checkout@v6 - name: Setup Rust uses: actions-rust-lang/setup-rust-toolchain@v1 with: cache-key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - target: ${{ matrix.target }} + toolchain: nightly + override: true + - name: Test (all-features) + if: matrix.features == 'all' + run: cargo test -r --locked --workspace --all-features + - name: Test (no_std) + continue-on-error: true + if: matrix.features == 'no_std' + run: cargo test -r --locked --workspace --no-default-features --features nightly + env: + RUSTFLAGS: "-C panic=abort -Z panic_abort_tests" - name: Test (${{ matrix.features }}) - if: matrix.features != 'default' && matrix.features != 'all' - run: cargo test -r --locked --workspace --target ${{ matrix.target }} --features ${{ matrix.features }} - - name: Test (default) - if: matrix.features == 'default' - run: cargo test -r --locked --workspace --target ${{ matrix.target }} + continue-on-error: true + if: matrix.features != 'all' && matrix.features != 'no_std' + run: cargo test -r --locked --workspace --no-default-features --features ${{ matrix.features }} + env: + RUSTFLAGS: "-C panic=abort -Z panic_abort_tests" diff --git a/Cargo.lock b/Cargo.lock index b670e9cf..9b45c716 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -74,6 +74,28 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-lc-rs" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a88aab2464f1f25453baa7a07c84c5b7684e274054ba06817f382357f77a288" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45afffdee1e7c9126814751f88dddc747f41d91da16c9551a0f1e8a11e788a1" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "base64" version = "0.22.1" @@ -88,9 +110,9 @@ checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "bytes" @@ -115,20 +137,34 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.49" +version = "1.2.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" +checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.42" @@ -170,18 +206,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.53" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.53" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" dependencies = [ "anstyle", "clap_lex", @@ -189,13 +225,32 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.6" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" + +[[package]] +name = "cmake" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] + +[[package]] +name = "combine" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] [[package]] name = "concision" -version = "0.3.0" +version = "0.3.1" dependencies = [ "anyhow", "approx", @@ -211,7 +266,7 @@ dependencies = [ [[package]] name = "concision-core" -version = "0.3.0" +version = "0.3.1" dependencies = [ "anyhow", "approx", @@ -226,39 +281,44 @@ dependencies = [ "num-traits", "paste", "rayon", + "rspace-traits", "rustfft", "serde", "serde_derive", "serde_json", "smart-default", "strum", - "thiserror", + "thiserror 2.0.17", "tracing", "variants", + "wasm-bindgen", ] [[package]] name = "concision-data" -version = "0.3.0" +version = "0.3.1" dependencies = [ + "anyhow", "approx", "concision-core", + "hashbrown 0.16.1", "ndarray", - "num", "num-complex", "num-traits", "rayon", "reqwest", "serde", + "serde_derive", "serde_json", - "thiserror", + "thiserror 2.0.17", "tracing", "variants", + "wasm-bindgen", ] [[package]] name = "concision-derive" -version = "0.3.0" +version = "0.3.1" dependencies = [ "proc-macro2", "quote", @@ -267,7 +327,7 @@ dependencies = [ [[package]] name = "concision-ext" -version = "0.3.0" +version = "0.3.1" dependencies = [ "anyhow", "approx", @@ -277,6 +337,7 @@ dependencies = [ "num", "num-complex", "num-traits", + "paste", "rayon", "rustfft", "serde", @@ -289,13 +350,14 @@ dependencies = [ [[package]] name = "concision-init" -version = "0.3.0" +version = "0.3.1" dependencies = [ + "anyhow", "approx", "getrandom 0.3.4", + "hashbrown 0.16.1", "lazy_static", "ndarray", - "num", "num-complex", "num-traits", "paste", @@ -303,16 +365,15 @@ dependencies = [ "rand_distr", "serde", "serde_derive", - "serde_json", "smart-default", "strum", - "thiserror", - "tracing", + "thiserror 2.0.17", + "wasm-bindgen", ] [[package]] name = "concision-macros" -version = "0.3.0" +version = "0.3.1" dependencies = [ "proc-macro2", "quote", @@ -321,42 +382,41 @@ dependencies = [ [[package]] name = "concision-params" -version = "0.3.0" +version = "0.3.1" dependencies = [ "anyhow", "approx", "concision-init", "concision-traits", - "getrandom 0.3.4", "ndarray", "num-complex", "num-traits", - "rand 0.9.2", - "rand_distr", - "rayon-core", + "rayon", + "rspace-traits", "serde", "serde_derive", "serde_json", - "thiserror", + "thiserror 2.0.17", "variants", + "wasm-bindgen", ] [[package]] name = "concision-traits" -version = "0.3.0" +version = "0.3.1" dependencies = [ - "anyhow", "approx", - "getrandom 0.3.4", + "hashbrown 0.16.1", "ndarray", "num-complex", "num-integer", "num-traits", "paste", - "rand 0.9.2", - "rand_distr", - "thiserror", - "variants", + "rand_core 0.9.5", + "rspace-traits", + "serde", + "serde_derive", + "wasm-bindgen", ] [[package]] @@ -369,6 +429,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -497,6 +567,12 @@ dependencies = [ "syn", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "either" version = "1.15.0" @@ -518,27 +594,11 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" [[package]] name = "fnv" @@ -552,21 +612,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "form_urlencoded" version = "1.2.2" @@ -576,6 +621,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures-channel" version = "0.3.31" @@ -617,13 +668,15 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -642,9 +695,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ "atomic-waker", "bytes", @@ -652,7 +705,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.12.1", + "indexmap 2.13.0", "slab", "tokio", "tokio-util", @@ -780,22 +833,6 @@ dependencies = [ "tower-service", ] -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - [[package]] name = "hyper-util" version = "0.1.19" @@ -894,9 +931,9 @@ checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ "icu_collections", "icu_locale_core", @@ -908,9 +945,9 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" @@ -967,9 +1004,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown 0.16.1", @@ -985,9 +1022,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "iri-string" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" dependencies = [ "memchr", "serde", @@ -1004,9 +1041,41 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] [[package]] name = "js-sys" @@ -1026,9 +1095,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.178" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "libm" @@ -1036,12 +1105,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" -[[package]] -name = "linux-raw-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" - [[package]] name = "litemap" version = "0.8.1" @@ -1054,6 +1117,12 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "matchers" version = "0.2.0" @@ -1096,28 +1165,11 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "native-tls" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - [[package]] name = "ndarray" -version = "0.17.1" +version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c7c9125e8f6f10c9da3aad044cc918cf8784fa34de857b1aa68038eb05a50a9" +checksum = "520080814a7a6b4a6e9070823bb24b4531daac8c4627e08ba5de8c5ef2f2752d" dependencies = [ "approx", "cblas-sys", @@ -1239,49 +1291,11 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" -[[package]] -name = "openssl" -version = "0.10.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" -dependencies = [ - "bitflags", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - -[[package]] -name = "openssl-sys" -version = "0.9.111" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] +checksum = "9f50d9b3dabb09ecd771ad0aa242ca6894994c130308ca3d7684634df8037391" [[package]] name = "page_size" @@ -1317,12 +1331,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - [[package]] name = "plotters" version = "0.3.7" @@ -1353,9 +1361,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" [[package]] name = "portable-atomic-util" @@ -1401,18 +1409,74 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" dependencies = [ "unicode-ident", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.17", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.17", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" -version = "1.0.42" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" dependencies = [ "proc-macro2", ] @@ -1439,7 +1503,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha", - "rand_core 0.9.3", + "rand_core 0.9.5", "serde", ] @@ -1450,7 +1514,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -1461,9 +1525,9 @@ checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "getrandom 0.3.4", "serde", @@ -1540,9 +1604,9 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" -version = "0.12.25" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6eff9328d40131d43bd911d42d79eb6a47312002a4daefc9e37f17e74a7701a" +checksum = "04e9018c9d814e5f30cc16a0f03271aeab3571e609612d9fe78c1aa8d11c2f62" dependencies = [ "base64", "bytes", @@ -1554,21 +1618,21 @@ dependencies = [ "http-body-util", "hyper", "hyper-rustls", - "hyper-tls", "hyper-util", "js-sys", "log", "mime", - "native-tls", "percent-encoding", "pin-project-lite", + "quinn", + "rustls", "rustls-pki-types", + "rustls-platform-verifier", "serde", "serde_json", - "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-native-tls", + "tokio-rustls", "tower", "tower-http", "tower-service", @@ -1586,12 +1650,32 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", ] +[[package]] +name = "rspace-traits" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "309a27b5c3e2c889fa140ac0fef65c5ac79be6e22d84048ec6148f6842c2d327" +dependencies = [ + "hashbrown 0.16.1", + "ndarray", + "num-complex", + "num-traits", + "paste", + "wasm-bindgen", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustc-std-workspace-alloc" version = "1.0.1" @@ -1612,25 +1696,13 @@ dependencies = [ "transpose", ] -[[package]] -name = "rustix" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - [[package]] name = "rustls" -version = "0.23.35" +version = "0.23.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ + "aws-lc-rs", "once_cell", "rustls-pki-types", "rustls-webpki", @@ -1638,21 +1710,62 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pki-types" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" +checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" dependencies = [ + "web-time", "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -1664,12 +1777,6 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - [[package]] name = "same-file" version = "1.0.6" @@ -1690,12 +1797,12 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.11.1" +version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" dependencies = [ "bitflags", - "core-foundation", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -1743,27 +1850,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", + "zmij", ] [[package]] @@ -1776,7 +1871,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.12.1", + "indexmap 2.13.0", "serde", "serde_derive", "serde_json", @@ -1891,9 +1986,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.111" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", @@ -1927,7 +2022,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ "bitflags", - "core-foundation", + "core-foundation 0.9.4", "system-configuration-sys", ] @@ -1942,16 +2037,12 @@ dependencies = [ ] [[package]] -name = "tempfile" -version = "3.23.0" +name = "thiserror" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "fastrand", - "getrandom 0.3.4", - "once_cell", - "rustix", - "windows-sys 0.61.2", + "thiserror-impl 1.0.69", ] [[package]] @@ -1960,7 +2051,18 @@ version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1985,30 +2087,30 @@ dependencies = [ [[package]] name = "time" -version = "0.3.44" +version = "0.3.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" [[package]] name = "time-macros" -version = "0.2.24" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd" dependencies = [ "num-conv", "time-core", @@ -2034,11 +2136,26 @@ dependencies = [ "serde_json", ] +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" -version = "1.48.0" +version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ "bytes", "libc", @@ -2048,16 +2165,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.26.4" @@ -2070,9 +2177,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.17" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -2083,9 +2190,9 @@ dependencies = [ [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", @@ -2128,9 +2235,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -2151,9 +2258,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.35" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -2218,9 +2325,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", @@ -2247,7 +2354,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d2f539cc5b8d5442f7558e2981adabd7462d6f318df8a5a12860a7242587e3" dependencies = [ "anyhow", - "thiserror", + "thiserror 2.0.17", "variants-derive", "variants-macros", ] @@ -2274,12 +2381,6 @@ dependencies = [ "syn", ] -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - [[package]] name = "walkdir" version = "2.5.0" @@ -2393,6 +2494,25 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36a29fc0408b113f68cf32637857ab740edfafdf460c326cd2afaa2d84cc05dc" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "winapi" version = "0.3.9" @@ -2494,6 +2614,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -2521,6 +2650,21 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -2554,6 +2698,12 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -2566,6 +2716,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -2578,6 +2734,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -2602,6 +2764,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -2614,6 +2782,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -2626,6 +2800,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -2638,6 +2818,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -2687,18 +2873,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.31" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.31" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" dependencies = [ "proc-macro2", "quote", @@ -2764,3 +2950,9 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zmij" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea" diff --git a/Cargo.toml b/Cargo.toml index f6cb91dd..844e9f6c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,34 +19,38 @@ categories = ["algorithms", "mathematics", "science"] description = "Concision is a toolkit for designing machine-learning models in Rust." edition = "2024" homepage = "https://github.com/FL03/concision/wiki" -keywords = ["data-science", "machine-learning", "scsys", "toolkit"] +keywords = ["data-science", "machine-learning", "toolkit"] license = "Apache-2.0" readme = "README.md" repository = "https://github.com/FL03/concision.git" rust-version = "1.85.0" -version = "0.3.0" +version = "0.3.1" [workspace.dependencies] -concision = { default-features = false, path = "concision", version = "0.3.0" } +concision = { default-features = false, path = "concision", version = "0.3.1" } # sdk -concision-core = { default-features = false, path = "core", version = "0.3.0" } -concision-data = { default-features = false, path = "data", version = "0.3.0" } -concision-derive = { default-features = false, path = "derive", version = "0.3.0" } -concision-init = { default-features = false, path = "init", version = "0.3.0" } -concision-macros = { default-features = false, path = "macros", version = "0.3.0" } -concision-neural = { default-features = false, path = "neural", version = "0.3.0" } -concision-params = { default-features = false, path = "params", version = "0.3.0" } -concision-traits = { default-features = false, path = "traits", version = "0.3.0" } +concision-core = { default-features = false, path = "core", version = "0.3.1" } +concision-data = { default-features = false, path = "data", version = "0.3.1" } +concision-derive = { default-features = false, path = "derive", version = "0.3.1" } +concision-init = { default-features = false, path = "init", version = "0.3.1" } +concision-macros = { default-features = false, path = "macros", version = "0.3.1" } +concision-neural = { default-features = false, path = "neural", version = "0.3.1" } +concision-params = { default-features = false, path = "params", version = "0.3.1" } +concision-traits = { default-features = false, path = "traits", version = "0.3.1" } # custom models & extras -concision-ext = { default-features = false, path = "ext", version = "0.3.0" } +concision-ext = { default-features = false, path = "ext", version = "0.3.1" } # custom +contained = { default-features = false, features = ["derive"], version = "0.2.2" } +rspace = { default-features = false, features = ["macros"], version = "0.0.8" } +rspace-traits = { default-features = false, version = "0.0.8" } variants = { default-features = false, features = ["derive"], version = "0.0.1" } # data structures hashbrown = { default-features = false, version = "0.16" } # tensors & arrays ndarray = { default-features = false, version = "0.17" } ndarray-linalg = { default-features = false, version = "0.18" } -ndarray-stats = "0.6" +ndarray-rand = { default-features = false, version = "0.16" } +ndarray-stats = { version = "0.7" } # benchmarking criterion = { version = "0.8" } # concurrency & parallelism @@ -64,6 +68,8 @@ num-complex = { default-features = false, version = "0.4" } num-integer = { default-features = false, version = "0.1" } num-traits = { default-features = false, version = "0.2" } rustfft = { version = "6" } +# networking +reqwest = { default-features = false, version = "0.13" } # random getrandom = { default-features = false, version = "0.3" } rand = { default-features = false, version = "0.9" } @@ -86,6 +92,8 @@ lazy_static = { version = "1" } paste = { version = "1" } smart-default = "0.7" strum = { default-features = false, features = ["derive"], version = "0.27" } +# WebAssembly (wasm) +wasm-bindgen = { default-features = false, version = "0.2" } # ************* [Profiles] ************* [profile.dev] diff --git a/README.md b/README.md index 099fbef3..4be20c83 100644 --- a/README.md +++ b/README.md @@ -23,36 +23,28 @@ _**Warning: The library still in development and is not yet ready for production ### Roadmap -- [x] **v1**: - - [x] **`ParamsBase`**: Design a basic structure for storing model parameters. - - [x] **Traits**: Create a set of traits for defining the basics of a neural network model. - - `Forward` and `Backward`: traits defining forward and backward propagation - - `Model`: A trait for defining a neural network model. - - `Predict`: A trait extending the basic [`Forward`](cnc::Forward) pass. - - `Train`: A trait for training a neural network model. -- [x] **v2**: - - [x] **`DeepModelParams`**: Extend the `ParamsBase` structure to support deep neural networks with multiple layers. - - [x] **Models**: Implement standard model configurations and parameters. - - `StandardModelConfig`: A standard configuration for neural network models. - - `ModelFeatures`: A structure to define the features of a model (e.g., number of layers, neurons per layer). - - [x] **Activation Functions**: Implement and refine various activation functions (`ReLU`, `Sigmoid`, `Tanh`, etc.) - - [x] **Loss Functions**: Implement common loss functions such as `MeanSquaredError` and `CrossEntropy` -- [ ] **v3**: - - [ ] **Optimizers**: Implement optimization algorithms like `SGD` and `Adam`. - - [ ] **Scheduler**: Learning rate schedulers to adjust the learning rate during training. - - [ ] **Layers**: Refine a more functional layer-based architecture. - - [ ] **Utilities**: Additional utilities for data preprocessing, model evaluation, and visualization - -## Usage +- Implement additional optimization algorithms (e.g., Adam, RMSProp). +- Add support for convolutional and recurrent neural networks. +- Expand the set of built-in layers and activation functions. +- Improve documentation and provide more examples and tutorials. +- Implement support for automatic differentiation using the `autodiff` crate. + +## Getting Started ### Adding to your project -To use `concision` in your project, add the following to your `Cargo.toml`: +To use `concision` in your project, run the following command: + +```bash +cargo add concision --features full +``` + +or add the following to your `Cargo.toml`: ```toml [dependencies.concision] features = ["full"] -version = "0.2.x" +version = "0.3.x" ``` ### Examples @@ -60,139 +52,293 @@ version = "0.2.x" #### **Example (1):** Simple Model ```rust - extern crate concision as cnc; - - use cnc::activate::{ReLU, Sigmoid}; - use cnc::nn::{Model, ModelFeatures, DeepModelParams, StandardModelConfig}; - use ndarray::{Array1, ScalarOperand}; - use num::Float; - - pub struct SimpleModel { - pub config: StandardModelConfig, - pub features: ModelFeatures, - pub params: DeepModelParams, - } - - impl SimpleModel { - pub fn new(config: StandardModelConfig, features: ModelFeatures) -> Self - where - T: Clone + num::Zero - { - let params = DeepModelParams::zeros(features); - SimpleModel { - config, - features, - params, - } - } - } - - impl cnc::Forward> for SimpleModel - where - T: Float + ScalarOperand, - cnc::Params: cnc::Forward, Output = Array1>, - { - type Output = Array1; - - fn forward(&self, input: &Array1) -> Result - where - T: Clone, - { - let mut output = self.params().input().forward(input)?.relu(); - - for layer in self.params().hidden() { - output = layer.forward(&output)?.sigmoid(); - } - - let res = self.params().output().forward(&output)?; - Ok(res.relu()) - } - } - - impl Model for SimpleModel { - type Config = StandardModelConfig; - - fn config(&self) -> &StandardModelConfig { - &self.config - } - - fn config_mut(&mut self) -> &mut StandardModelConfig { - &mut self.config - } - - fn features(&self) -> ModelFeatures { - self.features - } - - fn params(&self) -> &DeepModelParams { - &self.params - } - - fn params_mut(&mut self) -> &mut DeepModelParams { - &mut self.params - } - } -``` - -## Getting Started - -### Prerequisites - -To use `concision`, you need to have the following installed: - -- [Rust](https://www.rust-lang.org/tools/install) (version 1.85 or later) - -### Installation - -You can install the `rustup` toolchain using the following command: - -```bash -curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -``` - -After installing `rustup`, you can install the latest stable version of Rust with: - -```bash -rustup install stable -``` + use crate::activate::{ReLUActivation, SigmoidActivation}; + use crate::{ + DeepModelParams, Error, Forward, Model, ModelFeatures, Norm, Params, StandardModelConfig, Train, + }; + #[cfg(feature = "rand")] + use concision_init::{ + InitTensor, + rand_distr::{Distribution, StandardNormal}, + }; + + use ndarray::prelude::*; + use ndarray::{Data, ScalarOperand}; + use num_traits::{Float, FromPrimitive, NumAssign, Zero}; + + #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] + pub struct TestModel { + pub config: StandardModelConfig, + pub features: ModelFeatures, + pub params: DeepModelParams, + } + + impl TestModel { + pub fn new(config: StandardModelConfig, features: ModelFeatures) -> Self + where + T: Clone + Zero, + { + let params = DeepModelParams::zeros(features); + TestModel { + config, + features, + params, + } + } + /// returns an immutable reference to the model configuration + pub const fn config(&self) -> &StandardModelConfig { + &self.config + } + /// returns a reference to the model layout + pub const fn features(&self) -> ModelFeatures { + self.features + } + /// returns a reference to the model params + pub const fn params(&self) -> &DeepModelParams { + &self.params + } + /// returns a mutable reference to the model params + pub const fn params_mut(&mut self) -> &mut DeepModelParams { + &mut self.params + } + #[cfg(feature = "rand")] + /// consumes the current instance to initalize another with random parameters + pub fn init(self) -> Self + where + StandardNormal: Distribution, + T: Float, + { + let TestModel { + mut params, + config, + features, + } = self; + params.set_input(Params::::lecun_normal(( + features.input(), + features.hidden(), + ))); + for layer in params.hidden_mut() { + *layer = Params::::lecun_normal((features.hidden(), features.hidden())); + } + params.set_output(Params::::lecun_normal(( + features.hidden(), + features.output(), + ))); + TestModel { + config, + features, + params, + } + } + } + + impl Model for TestModel { + type Config = StandardModelConfig; + + type Layout = ModelFeatures; + + fn config(&self) -> &StandardModelConfig { + &self.config + } + + fn config_mut(&mut self) -> &mut StandardModelConfig { + &mut self.config + } + + fn layout(&self) -> &ModelFeatures { + &self.features + } + + fn params(&self) -> &DeepModelParams { + &self.params + } + + fn params_mut(&mut self) -> &mut DeepModelParams { + &mut self.params + } + } + + impl Forward> for TestModel + where + A: Float + FromPrimitive + ScalarOperand, + D: Dimension, + S: Data, + Params: Forward, Output = Array> + + Forward, Output = Array>, + { + type Output = Array; + + fn forward(&self, input: &ArrayBase) -> Self::Output { + // complete the first forward pass using the input layer + let mut output = self.params().input().forward(input).relu(); + // complete the forward pass for each hidden layer + for layer in self.params().hidden() { + output = layer.forward(&output).relu(); + } + + self.params().output().forward(&output).sigmoid() + } + } + + impl Train, ArrayBase> for TestModel + where + A: Float + FromPrimitive + NumAssign + ScalarOperand + core::fmt::Debug, + S: Data, + T: Data, + { + type Error = Error; + type Output = A; + + fn train( + &mut self, + input: &ArrayBase, + target: &ArrayBase, + ) -> Result { + if input.len() != self.layout().input() { + return Err(Error::InvalidInputFeatures( + input.len(), + self.layout().input(), + )); + } + if target.len() != self.layout().output() { + return Err(Error::InvalidTargetFeatures( + target.len(), + self.layout().output(), + )); + } + // get the learning rate from the model's configuration + let lr = self + .config() + .learning_rate() + .copied() + .unwrap_or(A::from_f32(0.01).unwrap()); + // Normalize the input and target + let input = input / input.l2_norm(); + let target_norm = target.l2_norm(); + let target = target / target_norm; + // self.prev_target_norm = Some(target_norm); + // Forward pass to collect activations + let mut activations = Vec::new(); + activations.push(input.to_owned()); + + let mut output = self.params().input().forward_then(&input, |y| y.relu()); + activations.push(output.to_owned()); + // collect the activations of the hidden + for layer in self.params().hidden() { + output = layer.forward(&output).relu(); + activations.push(output.to_owned()); + } + + output = self.params().output().forward(&output).sigmoid(); + activations.push(output.to_owned()); + + // Calculate output layer error + let error = &target - &output; + let loss = error.pow2().mean().unwrap_or(A::zero()); + #[cfg(feature = "tracing")] + tracing::trace!("Training loss: {loss:?}"); + let mut delta = error * output.sigmoid_derivative(); + delta /= delta.l2_norm(); // Normalize the delta to prevent exploding gradients + + // Update output weights + self.params_mut() + .output_mut() + .backward(activations.last().unwrap(), &delta, lr); + + let num_hidden = self.layout().layers(); + // Iterate through hidden layers in reverse order + for i in (0..num_hidden).rev() { + // Calculate error for this layer + delta = if i == num_hidden - 1 { + // use the output activations for the final hidden layer + self.params().output().weights().dot(&delta) * activations[i + 1].relu_derivative() + } else { + // else; backpropagate using the previous hidden layer + self.params().hidden()[i + 1].weights().t().dot(&delta) + * activations[i + 1].relu_derivative() + }; + // Normalize delta to prevent exploding gradients + delta /= delta.l2_norm(); + self.params_mut().hidden_mut()[i].backward(&activations[i + 1], &delta, lr); + } + /* + The delta for the input layer is computed using the weights of the first hidden layer + and the derivative of the activation function of the first hidden layer. + */ + delta = self.params().hidden()[0].weights().dot(&delta) * activations[1].relu_derivative(); + delta /= delta.l2_norm(); // Normalize the delta to prevent exploding gradients + self.params_mut() + .input_mut() + .backward(&activations[1], &delta, lr); + + Ok(loss) + } + } + + impl Train, ArrayBase> for TestModel + where + A: Float + FromPrimitive + NumAssign + ScalarOperand + core::fmt::Debug, + S: Data, + T: Data, + { + type Error = Error; + type Output = A; + + fn train( + &mut self, + input: &ArrayBase, + target: &ArrayBase, + ) -> Result { + if input.nrows() == 0 || target.nrows() == 0 { + return Err(anyhow::anyhow!("Input and target batches must be non-empty").into()); + } + if input.ncols() != self.layout().input() { + return Err(Error::InvalidInputFeatures( + input.ncols(), + self.layout().input(), + )); + } + if target.ncols() != self.layout().output() || target.nrows() != input.nrows() { + return Err(Error::InvalidTargetFeatures( + target.ncols(), + self.layout().output(), + )); + } + let batch_size = input.nrows(); + let mut loss = A::zero(); + + for (i, (x, e)) in input.rows().into_iter().zip(target.rows()).enumerate() { + loss += match Train::, ArrayView1>::train(self, &x, &e) { + Ok(l) => l, + Err(err) => { + #[cfg(not(feature = "tracing"))] + eprintln!( + "Training failed for batch {}/{}: {:?}", + i + 1, + batch_size, + err + ); + #[cfg(feature = "tracing")] + tracing::error!( + "Training failed for batch {}/{}: {:?}", + i + 1, + batch_size, + err + ); + return Err(err); + } + }; + } + + Ok(loss) + } + } -You can also install the latest nightly version of Rust with: - -```bash -rustup install nightly -``` - -### Building from the source - -Start by cloning the repository - -```bash -git clone https://github.com/FL03/concision.git -``` - -Then, navigate to the `concision` directory: - -```bash -cd concision -``` - -#### _Using the `cargo` tool_ - -To build the crate, you can use the `cargo` tool. The following command will build the crate with all features enabled: - -```bash -cargo build -r --locked --workspace --features full -``` - -To run the tests, you can use the following command: - -```bash -cargo test -r --locked --workspace --features full ``` ## Contributing -Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. +Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. View the [quickstart guide](QUICKSTART.md) for more information on setting up your environment to develop the `concision` framework. Please make sure to update tests as appropriate. diff --git a/SECURITY.md b/SECURITY.md index a298596c..d8a7a16e 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,6 +1,6 @@ # Security Policy - Current Version: 0.3.0 + Current Version: 0.3.1 ## Supported Versions @@ -8,7 +8,7 @@ Checkout the table below for information on which versions are currently support | Version | Supported | |:-----------------|:-------------------| -| 0.2.9,<=0.3.0 | :white_check_mark: | +| 0.2.9,<=0.3.1 | :white_check_mark: | | <0.2.9 | :x: | ## Reporting a Vulnerability diff --git a/clippy.toml b/clippy.toml index 1a78622e..4972822f 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1 +1 @@ -msrv = "1.35.0" +msrv = "1.85.0" diff --git a/concision/Cargo.toml b/concision/Cargo.toml index 4a591402..90ea99d0 100644 --- a/concision/Cargo.toml +++ b/concision/Cargo.toml @@ -24,28 +24,21 @@ no-dev-version = true tag-name = "v{{version}}" [lib] -bench = true -crate-type = ["cdylib", "rlib"] -doc = true -doctest = true -name = "concision" path = "lib.rs" -test = true [[bench]] harness = false name = "params" path = "benches/params.rs" -required-features = [ - "approx", - "rand", - "std", - "tracing", -] +required-features = ["approx", "rand", "std", "tracing"] + +[[example]] +name = "mnist" +required-features = ["full"] [[example]] name = "params" -required-features = ["rand", "std", "tracing"] +required-features = ["default", "tracing"] [[example]] name = "simple" @@ -67,34 +60,39 @@ tracing-subscriber = { workspace = true } [features] default = [ - "rand", + "data", + "derive", + "macros", "std", ] full = [ - "approx", - "data", "default", - "derive", - "macros", + "approx", "rand", "serde", "tracing", ] -nightly = [ - "concision-core/nightly", - "concision-data?/nightly", +# ********* [FF] Features ********* +autodiff = [ + "concision-core/autodiff", + "concision-data?/autodiff", ] -# ************* [FF:Features] ************* derive = ["dep:concision-derive"] macros = ["concision-core/macros", "dep:concision-macros"] data = ["dep:concision-data"] -# ************* [FF:Environments] ************* +# ********* [FF] Environments ********* + +nightly = [ + "concision-core/nightly", + "concision-data?/nightly", +] + std = [ "alloc", "concision-core/std", @@ -111,7 +109,8 @@ wasm = [ "concision-data?/wasm", ] -# ************* [FF:Dependencies] ************* +# ********* [FF] Dependencies ********* + alloc = [ "concision-core/alloc", "concision-data?/alloc", @@ -137,11 +136,6 @@ rand = [ "concision-data?/rand", ] -rng = [ - "concision-core/rng", - "concision-data?/rng", -] - rayon = [ "concision-core/rayon", "concision-data?/rayon", @@ -156,11 +150,21 @@ serde = [ "concision-data?/serde", ] +serde_json = [ + "concision-core/serde_json", + "concision-data?/serde_json", +] + tracing = [ "concision-core/tracing", "concision-data?/tracing", ] +wasm_bindgen = [ + "concision-core/wasm_bindgen", + "concision-data?/wasm_bindgen", +] + # ********* [FF] Blas ********* blas = [ "concision-core/blas", diff --git a/concision/benches/params.rs b/concision/benches/params.rs index 33b14d16..db257fcb 100644 --- a/concision/benches/params.rs +++ b/concision/benches/params.rs @@ -4,8 +4,8 @@ */ extern crate concision as cnc; -use cnc::init::NdInit; - +use cnc::Params; +use cnc::init::NdRandom; use core::hint::black_box; use criterion::{BatchSize, BenchmarkId, Criterion}; use ndarray::Array1; @@ -26,13 +26,12 @@ fn bench_params_forward(c: &mut Criterion) { group.bench_with_input(BenchmarkId::new("Params::forward", n), &n, |b, &x| { b.iter_batched( || { - let params = cnc::Params::::glorot_normal((n, 64)); - let input = Array1::::linspace(0.0, 1.0, x); - // return the configured parameters - (params, input) + let params = Params::::glorot_normal((n, 64)); + let x = Array1::::linspace(0.0, 1.0, x); + (params, x) }, - |(params, input)| { - params.forward(black_box(&input)); + |(params, x)| { + black_box(params.forward(&x)); }, BatchSize::SmallInput, ); diff --git a/concision/examples/mnist.rs b/concision/examples/mnist.rs new file mode 100644 index 00000000..f82b3ea7 --- /dev/null +++ b/concision/examples/mnist.rs @@ -0,0 +1,19 @@ +/* + Appellation: mnist + Created At: 2026.01.13:22:06:22 + Contrib: @FL03 +*/ +extern crate concision as cnc; + +fn main() -> cnc::Result<()> { + tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::filter::EnvFilter::from_default_env()) + .with_max_level(tracing::Level::TRACE) + .with_target(false) + .with_timer(tracing_subscriber::fmt::time::uptime()) + .init(); + + // load the dataset + // let dataset = + Ok(()) +} diff --git a/concision/examples/params.rs b/concision/examples/params.rs index 6dc386bc..8f0961d4 100644 --- a/concision/examples/params.rs +++ b/concision/examples/params.rs @@ -4,38 +4,37 @@ */ extern crate concision as cnc; -use cnc::init::NdInit; +#[cfg(feature = "rand")] +use cnc::init::NdRandom; use cnc::params::Params; use ndarray::prelude::*; -fn main() -> anyhow::Result<()> { +fn main() -> cnc::Result<()> { tracing_subscriber::fmt() - .with_max_level(tracing::Level::DEBUG) + .with_env_filter(tracing_subscriber::filter::EnvFilter::from_default_env()) + .with_max_level(tracing::Level::TRACE) .with_target(false) - .with_ansi(true) + .with_timer(tracing_subscriber::fmt::time::uptime()) .init(); - tracing::info!("Initializing basic example..."); + tracing::info! { "Initialize some params..." } let (m, n) = (8, 9); let inputs = Array1::linspace(0.0, 1.0, m); // initialize a 2-dimensional parameter set with 8 samples and 9 features - let mut params = Params::::default((m, n)); - tracing::info!("Initial Values: {params:?}"); + #[cfg(feature = "rand")] + let params = Params::::glorot_normal((m, n)); + #[cfg(not(feature = "rand"))] + let params = Params::::ones((m, n)); + // log the initial values of the parameters + tracing::info! { "Initial Values: {params:?}" } // validate the shape of the parameters - assert_eq!(params.weights().shape(), &[m, n]); - assert_eq!(params.bias().shape(), &[n]); - // initialize the parameters with random values - params.assign_weights(&Array2::glorot_normal((m, n))); - params.assign_bias(&Array1::glorot_normal((n,))); - // validate the shape of the parameters - assert_eq!(params.weights().shape(), &[m, n]); - assert_eq!(params.bias().shape(), &[n]); - tracing::info!("Randomized parameters: {params:?}"); - + assert_eq! { params.weights().shape(), &[m, n] } + assert_eq! { params.bias().shape(), &[n] } + // perform a forward pass through the parameters let y = params.forward(&inputs); - assert_eq!(y.shape(), &[n]); - tracing::info!("Forward pass: {y:?}"); + assert_eq! { y.shape(), &[n] } + tracing::info! { "Forward pass: {y:?}" } Ok(()) } diff --git a/concision/examples/simple.rs b/concision/examples/simple.rs index f8a0e46b..9d972727 100644 --- a/concision/examples/simple.rs +++ b/concision/examples/simple.rs @@ -11,9 +11,10 @@ use ndarray::prelude::*; fn main() -> anyhow::Result<()> { tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::filter::EnvFilter::from_default_env()) .with_max_level(tracing::Level::TRACE) .with_target(false) - .without_time() + .with_timer(tracing_subscriber::fmt::time::uptime()) .init(); tracing::info!("Setting up the model..."); // define the models features @@ -39,10 +40,13 @@ fn main() -> anyhow::Result<()> { let training_input = Array2::from_shape_vec((1, model.features().input()), input.to_vec()).unwrap(); let expected_output = Array2::from_elem((1, model.features().output()), 0.235); + let training_span = tracing::info_span!("Starting training...").entered(); // train the model - for _ in 0..model.config().epochs() { + for e in 0..model.config().epochs() { + training_span.in_scope(|| tracing::trace!("Training epoch {}...", e)); model.train(&training_input, &expected_output)?; } + drop(training_span); // forward the input through the model let output = model.predict(&input); tracing::info!("output: {:?}", output); diff --git a/concision/lib.rs b/concision/lib.rs index aa974afc..1a8169dd 100644 --- a/concision/lib.rs +++ b/concision/lib.rs @@ -1,7 +1,3 @@ -/* - Appellation: concision - Contrib: FL03 -*/ //! # concision (cnc) //! //! [![crates.io](https://img.shields.io/crates/v/concision?style=for-the-badge&logo=rust)](https://crates.io/crates/concision) @@ -22,12 +18,14 @@ //! //! - `data`: Provides utilities for data loading, preprocessing, and augmentation. //! - `derive`: Custom derive macros for automatic implementation of traits -//! - `init`: Enables various initialization strategies for model parameters. -//! - `macros`: Procedural macros for simplifying common tasks in machine learning. -//! - `neural`: A neural network module that includes layers, optimizers, and training -//! utilities. +//! - `macros`: Procedural macros focused on facilitating the creation of new neural networks //! -//! ### _Extensions_ +//! ### Experimental Features +//! +//! - `autodiff`: toggle the use of Rust's nightly `autodiff` feature for automatic +//! differentiation. +//! +//! ### Optional //! //! The crate is integrated with several optional externcal crates that are commonly used in //! Rust development; listed below are some of the most relevant of these _extensions_ as they @@ -35,7 +33,8 @@ //! //! - [`approx`](https://docs.rs/approx): Enables approximate equality checks for //! floating-point arithmetic, useful for testing and validation of model outputs. -//! - `json`: Enables JSON serialization and deserialization for models and data. +//! - [`json`](https://docs.rs/serde_json): Enables JSON serialization and deserialization for models and data. +//! - [`rand`](https://docs.rs/rand): Enable random number generation and associated initialization routines. //! - [`rayon`](https://docs.rs/rayon): Enables parallel processing for data loading and //! training. //! - [`serde`](https://serde.rs): Enables the `serde` crate for the serialization and @@ -43,22 +42,20 @@ //! - [`tracing`](https://docs.rs/tracing): Enables the `tracing` crate for structured logging //! and diagnostics. //! -//! ## Roadmap -//! -//! - **DSL**: Create a pseudo-DSL for defining machine learning models and training processes. -//! - **GPU**: Support for GPU acceleration to speed up training and inference. -//! - **Interoperability**: Integrate with other libraries and frameworks (TensorFlow, PyTorch) -//! - **Visualization**: Utilities for visualizing model architectures and training progress -//! - **WASM**: Native support for WebAssembly enabling models to be run in web browsers. -//! #![crate_type = "lib"] #![allow( + clippy::missing_docs_in_private_items, + clippy::missing_errors_doc, + clippy::missing_panics_doc, clippy::missing_safety_doc, clippy::module_inception, clippy::needless_doctest_main, + clippy::non_canonical_partial_ord_impl, clippy::upper_case_acronyms )] #![cfg_attr(not(feature = "std"), no_std)] +#![cfg_attr(all(feature = "nightly", feature = "alloc"), feature(allocator_api))] +#![cfg_attr(all(feature = "nightly", feature = "autodiff"), feature(autodiff))] #[doc(inline)] pub use concision_core::*; diff --git a/core/Cargo.toml b/core/Cargo.toml index 3bdf8da4..e8b78dbf 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -16,7 +16,7 @@ version.workspace = true [package.metadata.docs.rs] all-features = false -features = ["full"] +features = ["default"] rustc-args = ["--cfg", "docsrs"] [package.metadata.release] @@ -25,15 +25,13 @@ tag-name = "v{{version}}" [lib] bench = false -crate-type = ["cdylib", "rlib"] -doctest = true -test = true [dependencies] -concision-init = { workspace = true } +concision-init = { optional = true, workspace = true } concision-params = { workspace = true } -concision-traits = { workspace = true } +concision-traits = { features = ["hashbrown"], workspace = true } # custom +rspace-traits = { workspace = true } variants = { workspace = true } # concurrency & parallelism rayon = { optional = true, workspace = true } @@ -59,6 +57,8 @@ num-complex = { optional = true, workspace = true } num-integer = { workspace = true } num-traits = { workspace = true } rustfft = { optional = true, workspace = true } +# WebAssembly (wasm) +wasm-bindgen = { optional = true, workspace = true } [dev-dependencies] lazy_static = { workspace = true } @@ -67,37 +67,49 @@ lazy_static = { workspace = true } default = ["macros", "std"] full = [ + "default", "approx", "complex", - "default", - "json", "rand", + "json", "serde", "signal", "tracing", ] nightly = [ - "concision-init/nightly", + "concision-init?/nightly", "concision-params/nightly", "concision-traits/nightly", "hashbrown/nightly", ] -# ************* [FF:Features] ************* -init = ["rand"] +# ********* [FF] Flags ********* +autodiff = [ + "nightly", + "concision-params/autodiff", + "concision-traits/autodiff", +] macros = [] -json = ["alloc", "serde", "serde_json"] +json = [ + "alloc", + "serde", + "serde_json", +] + +signal = [ + "complex", + "rustfft" +] -signal = ["complex", "rustfft"] +# ********* [FF] Environments ********* -# ************* [FF:Dependencies] ************* std = [ "alloc", "anyhow/std", - "concision-init/std", + "concision-init?/std", "concision-params/std", "concision-traits/std", "hashbrown/default", @@ -105,7 +117,7 @@ std = [ "num-complex?/std", "num-integer/std", "num-traits/std", - "serde/std", + "serde?/std", "strum/std", "thiserror/std", "tracing?/std", @@ -113,20 +125,22 @@ std = [ ] wasi = [ - "concision-init/wasi", + "concision-init?/wasi", "concision-params/wasi", "concision-traits/wasi", ] wasm = [ - "concision-init/wasm", + "wasm_bindgen", + "concision-init?/wasm", "concision-params/wasm", "concision-traits/wasm", ] -# ************* [FF:Dependencies] ************* +# ********* [FF] Dependencies ********* + alloc = [ - "concision-init/alloc", + "concision-init?/alloc", "concision-params/alloc", "concision-traits/alloc", "hashbrown/alloc", @@ -137,56 +151,67 @@ alloc = [ approx = [ "dep:approx", - "concision-init/approx", + "concision-init?/approx", "concision-params/approx", "ndarray/approx", ] blas = [ - "concision-init/blas", + "concision-init?/blas", "concision-params/blas", "ndarray/blas", ] complex = [ "dep:num-complex", - "concision-init/complex", + "concision-init?/complex", "concision-params/complex", "concision-traits/complex", ] +concision_init = ["dep:concision-init"] + rand = [ - "concision-init/rand", + "concision_init", "concision-params/rand", "concision-traits/rand", + "num-complex?/rand", + "rspace-traits/rand", ] rayon = [ "dep:rayon", + "concision-init?/rayon", "concision-params/rayon", "concision-traits/rayon", "hashbrown/rayon", "ndarray/rayon", ] -rng = [ - "concision-init/rng", - "concision-params/rng", - "concision-traits/rand", -] - rustfft = ["dep:rustfft"] serde = [ "dep:serde", "dep:serde_derive", - "concision-init/serde", + "serde?/derive", + "concision-init?/serde", "concision-params/serde", + "concision-traits/serde", "hashbrown/serde", "ndarray/serde", "num-complex?/serde", ] -serde_json = ["dep:serde_json"] +serde_json = [ + "dep:serde_json", + "concision-params/serde_json", +] + +tracing = ["dep:tracing"] -tracing = ["concision-init/tracing", "dep:tracing"] +wasm_bindgen = [ + "dep:wasm-bindgen", + "concision-init?/wasm_bindgen", + "concision-params/wasm_bindgen", + "concision-traits/wasm_bindgen", +] diff --git a/core/src/activate/impls/impl_activate_linear.rs b/core/src/activate/impls/impl_activate_linear.rs new file mode 100644 index 00000000..517a0591 --- /dev/null +++ b/core/src/activate/impls/impl_activate_linear.rs @@ -0,0 +1,115 @@ +/* + Appellation: impl_linear + Created At: 2025.12.14:11:14:22 + Contrib: @FL03 +*/ +use crate::activate::{HeavysideActivation, LinearActivation}; +use ndarray::{Array, ArrayBase, Data, DataMut, Dimension}; +use num_traits::{One, Zero}; + +macro_rules! impl_heavyside { + ($($T:ty),* $(,)*) => { + $( + impl $crate::activate::HeavysideActivation for $T { + type Output = $T; + + fn heavyside(self) -> Self::Output { + if self > <$T>::zero() { + <$T>::one() + } else { + <$T>::zero() + } + } + + fn heavyside_derivative(self) -> Self::Output { + if self > <$T>::zero() { + <$T>::one() + } else { + <$T>::zero() + } + } + } + )* + }; +} + +macro_rules! impl_linear { + ($($T:ty),* $(,)*) => { + $( + impl $crate::activate::LinearActivation for $T { + type Output = $T; + + fn linear(self) -> Self::Output { + self + } + + fn linear_derivative(self) -> Self::Output { + <$T>::one() + } + } + )* + }; +} + +impl_heavyside! { + i8, i16, i32, i64, i128, isize, + u8, u16, u32, u64, u128, usize, + f32, f64, +} + +impl_linear! { + i8, i16, i32, i64, i128, isize, + u8, u16, u32, u64, u128, usize, + f32, f64, +} + +impl HeavysideActivation for ArrayBase +where + A: Clone + HeavysideActivation, + D: Dimension, + S: Data, +{ + type Output = Array; + + fn heavyside(self) -> Self::Output { + self.mapv(HeavysideActivation::heavyside) + } + + fn heavyside_derivative(self) -> Self::Output { + self.mapv(HeavysideActivation::heavyside_derivative) + } +} + +impl HeavysideActivation for &ArrayBase +where + A: Clone + HeavysideActivation, + D: Dimension, + S: Data, +{ + type Output = Array; + + fn heavyside(self) -> Self::Output { + self.mapv(HeavysideActivation::heavyside) + } + + fn heavyside_derivative(self) -> Self::Output { + self.mapv(HeavysideActivation::heavyside_derivative) + } +} + +impl LinearActivation for ArrayBase +where + A: Clone + One, + D: Dimension, + S: DataMut, +{ + type Output = ArrayBase; + + fn linear(self) -> Self::Output { + self + } + + fn linear_derivative(self) -> Self::Output { + self.mapv_into(|_| ::one()) + } +} diff --git a/core/src/activate/impls/impl_nonlinear.rs b/core/src/activate/impls/impl_activate_nonlinear.rs similarity index 69% rename from core/src/activate/impls/impl_nonlinear.rs rename to core/src/activate/impls/impl_activate_nonlinear.rs index 2272e635..ee896da7 100644 --- a/core/src/activate/impls/impl_nonlinear.rs +++ b/core/src/activate/impls/impl_activate_nonlinear.rs @@ -1,11 +1,9 @@ /* - Appellation: sigmoid - Contrib: FL03 + Appellation: impl_nonlinear + Created At: 2025.12.14:11:13:15 + Contrib: @FL03 */ -use crate::activate::{ - ReLUActivation, SigmoidActivation, SoftmaxActivation, TanhActivation, utils::sigmoid_derivative, -}; - +use crate::activate::{ReLUActivation, SigmoidActivation, SoftmaxActivation, TanhActivation}; use ndarray::{Array, ArrayBase, Data, Dimension, ScalarOperand}; use num_traits::{Float, One, Zero}; @@ -28,7 +26,7 @@ where impl SigmoidActivation for ArrayBase where - A: ScalarOperand + Float, + A: 'static + Float, S: Data, D: Dimension, { @@ -38,31 +36,32 @@ where let dim = self.dim(); let ones = Array::::ones(dim); - (ones + self.map(|&i| i.neg().exp())).recip() + (ones + self.signum().exp()).recip() } fn sigmoid_derivative(self) -> Self::Output { - self.mapv(|i| sigmoid_derivative(i)) + self.mapv(|i| { + let s = (A::one() + i.neg().exp()).recip(); + s * (A::one() - s) + }) } } impl SoftmaxActivation for ArrayBase where - A: ScalarOperand + Float, + A: Float + ScalarOperand, S: Data, D: Dimension, { type Output = Array; fn softmax(&self) -> Self::Output { - let e = self.exp(); - &e / e.sum() + let exp = self.exp(); + &exp / exp.sum() } fn softmax_derivative(&self) -> Self::Output { - let e = self.exp(); - let sum = e.sum(); - let softmax = &e / sum; + let softmax = self.softmax(); let ones = Array::::ones(self.dim()); &softmax * (&ones - &softmax) @@ -71,17 +70,17 @@ where impl TanhActivation for ArrayBase where - A: ScalarOperand + Float, + A: 'static + Float, S: Data, D: Dimension, { type Output = Array; fn tanh(&self) -> Self::Output { - self.map(|i| i.tanh()) + self.mapv(|i| i.tanh()) } fn tanh_derivative(&self) -> Self::Output { - self.map(|i| A::one() - i.tanh().powi(2)) + self.mapv(|i| A::one() - i.tanh().powi(2)) } } diff --git a/core/src/activate/impls/impl_activator.rs b/core/src/activate/impls/impl_activator.rs new file mode 100644 index 00000000..cda73163 --- /dev/null +++ b/core/src/activate/impls/impl_activator.rs @@ -0,0 +1,38 @@ +/* + appellation: activate + authors: @FL03 +*/ +use crate::activate::Activator; + +impl Activator for F +where + F: Fn(X) -> Y, +{ + type Output = Y; + + fn activate(&self, rhs: X) -> Self::Output { + self(rhs) + } +} + +// impl Activator> for F +// where +// F: Activator, +// S: Data, +// D: Dimension, +// { +// type Output = Array; + +// fn activate(&self, rhs: ArrayBase) -> Self::Output { +// rhs.mapv(|x| self.activate(x)) +// } +// } + +#[cfg(feature = "alloc")] +impl Activator for alloc::boxed::Box> { + type Output = Y; + + fn activate(&self, rhs: X) -> Self::Output { + self.as_ref().activate(rhs) + } +} diff --git a/core/src/activate/impls/impl_binary.rs b/core/src/activate/impls/impl_binary.rs deleted file mode 100644 index 46e77e28..00000000 --- a/core/src/activate/impls/impl_binary.rs +++ /dev/null @@ -1,68 +0,0 @@ -/* - Appellation: binary - Contrib: FL03 -*/ -use crate::activate::{HeavysideActivation, utils::heavyside}; -use ndarray::{Array, ArrayBase, Data, Dimension}; -use num_traits::{One, Zero}; - -macro_rules! impl_heavyside { - ($($ty:ty),* $(,)*) => { - $(impl_heavyside!(@impl $ty);)* - }; - (@impl $ty:ty) => { - impl HeavysideActivation for $ty { - type Output = $ty; - - fn heavyside(self) -> Self::Output { - heavyside(self) - } - - fn heavyside_derivative(self) -> Self::Output { - if self > <$ty>::zero() { - <$ty>::one() - } else { - <$ty>::zero() - } - } - } - }; -} - -impl_heavyside!( - f32, f64, i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, -); - -impl HeavysideActivation for ArrayBase -where - A: Clone + HeavysideActivation, - D: Dimension, - S: Data, -{ - type Output = Array; - - fn heavyside(self) -> Self::Output { - self.mapv(HeavysideActivation::heavyside) - } - - fn heavyside_derivative(self) -> Self::Output { - self.mapv(HeavysideActivation::heavyside_derivative) - } -} - -impl HeavysideActivation for &ArrayBase -where - A: Clone + HeavysideActivation, - D: Dimension, - S: Data, -{ - type Output = Array; - - fn heavyside(self) -> Self::Output { - self.mapv(HeavysideActivation::heavyside) - } - - fn heavyside_derivative(self) -> Self::Output { - self.mapv(HeavysideActivation::heavyside_derivative) - } -} diff --git a/core/src/activate/impls/impl_linear.rs b/core/src/activate/impls/impl_linear.rs deleted file mode 100644 index d26f5370..00000000 --- a/core/src/activate/impls/impl_linear.rs +++ /dev/null @@ -1,19 +0,0 @@ -/* - Appellation: linear - Contrib: FL03 -*/ - -impl crate::activate::LinearActivation for T -where - T: Clone + Default, -{ - type Output = T; - - fn linear(self) -> Self::Output { - self.clone() - } - - fn linear_derivative(self) -> Self::Output { - ::default() - } -} diff --git a/core/src/activate/mod.rs b/core/src/activate/mod.rs index 9c463931..97e7539b 100644 --- a/core/src/activate/mod.rs +++ b/core/src/activate/mod.rs @@ -1,50 +1,38 @@ /* Appellation: activate - Contrib: FL03 + Created At: 2026.01.13:17:07:42 + Contrib: @FL03 */ -//! Activation functions for neural networks and their components. These functions are often -//! used to introduce non-linearity into the model, allowing it to learn more complex patterns -//! in the data. -//! -//! ## Overview -//! -//! This module works to provide a complete set of activation utilities for neural networks, -//! manifesting in a number of traits, utilities, and other primitives used to define various -//! approaches to activation functions. -//! -//! - [`HeavysideActivation`] -//! - [`LinearActivation`] -//! - [`SigmoidActivation`] -//! - [`SoftmaxActivation`] -//! - [`ReLUActivation`] -//! - [`TanhActivation`] -//! +//! this module provides the [`Activate`] trait alongside additional primitives and utilities +//! for activating neurons within a neural network. #[doc(inline)] -pub use self::{traits::*, utils::*}; +pub use self::{rho::*, traits::*, utils::*}; -pub(crate) mod utils; +pub mod rho; -pub(crate) mod traits { +mod impls { + mod impl_activate_linear; + mod impl_activate_nonlinear; + mod impl_activator; +} + +mod traits { #[doc(inline)] - pub use self::prelude::*; + pub use self::{activate::*, ops::*}; mod activate; - mod unary; - - mod prelude { - #[doc(inline)] - pub use super::activate::*; - #[doc(inline)] - pub use super::unary::*; - } + mod ops; } -mod impls { - mod impl_binary; - mod impl_linear; - mod impl_nonlinear; +pub mod utils { + #[doc(inline)] + pub use self::funcs::*; + + mod funcs; } +#[doc(hidden)] +#[allow(unused_imports)] pub(crate) mod prelude { pub use super::traits::*; pub use super::utils::*; diff --git a/core/src/activate/rho.rs b/core/src/activate/rho.rs new file mode 100644 index 00000000..0f487826 --- /dev/null +++ b/core/src/activate/rho.rs @@ -0,0 +1,69 @@ +/* + Appellation: rho + Created At: 2026.01.13:18:09:21 + Contrib: @FL03 +*/ +//! this module defines _structural_ implementations of various activation functions + +/// An [`Activator`] defines an interface for _structural_ activation functions that can be +/// applied onto various types. +pub trait Activator { + type Output; + + /// Applies the activation function to the input tensor. + fn activate(&self, input: T) -> Self::Output; +} +/// The [`ActivatorGradient`] trait extends the [`Activator`] trait to include a method for +/// computing the gradient of the activation function. +pub trait ActivatorGradient { + type Rel: Activator; + type Delta; + + /// compute the gradient of some input + fn activate_gradient(&self, input: T) -> Self::Delta; +} + +macro_rules! activator { + ($($vis:vis struct $name:ident::<$T:ident>::$method:ident $({where $($where:tt)*})?),* $(,)?) => { + $(activator! { + @impl $vis struct $name::<$T>::$method $({where $($where)*})? + })* + }; + (@impl $vis:vis struct $name:ident::<$T:ident>::$method:ident $({where $($where:tt)*})? ) => { + #[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + $vis struct $name; + + impl<$T> Activator<$T> for $name + $(where $($where)*)? + { + type Output = <$T>::Output; + + fn activate(&self, x: $T) -> Self::Output { + x.$method() + } + } + + paste::paste! { + impl<$T> ActivatorGradient<$T> for $name + $(where $($where)*)?, + { + type Rel = Self; + type Delta = <$T>::Output; + + fn activate_gradient(&self, inputs: $T) -> Self::Delta { + inputs.[<$method _derivative>]() + } + } + } + }; +} + +activator! { + pub struct Linear::::linear { where T: crate::activate::LinearActivation }, + pub struct ReLU::::relu { where T: crate::activate::ReLUActivation }, + pub struct Sigmoid::::sigmoid { where T: crate::activate::SigmoidActivation }, + pub struct HyperbolicTangent::::tanh { where T: crate::activate::TanhActivation }, + pub struct HeavySide::::heavyside { where T: crate::activate::HeavysideActivation }, + pub struct Softmax::::softmax { where T: crate::activate::SoftmaxActivation }, +} diff --git a/core/src/activate/traits/activate.rs b/core/src/activate/traits/activate.rs index f91761b4..b7fd56ad 100644 --- a/core/src/activate/traits/activate.rs +++ b/core/src/activate/traits/activate.rs @@ -1,157 +1,64 @@ /* - appellation: activate - authors: @FL03 + Appellation: rho + Created At: 2026.01.12:09:50:26 + Contrib: @FL03 */ -use super::unary::*; +use concision_traits::Tanh; +use num_traits::{Float, One, Zero}; -use concision_traits::Apply; -#[cfg(feature = "complex")] -use num_complex::ComplexFloat; -use num_traits::One; +/// [`Activate`] is a higher-kinded trait that provides a mechanism to apply a function over the +/// elements within a container or structure. +pub trait Activate { + type Cont; -/// The [`Rho`] trait defines a set of activation functions that can be applied to an -/// implementor of the [`Apply`] trait. It provides methods for common activation functions -/// such as linear, heavyside, ReLU, sigmoid, and tanh, along with their derivatives. -/// The trait is generic over a type `U`, which represents the data type of the input to the -/// activation functions. The trait also inherits a type alias `Cont` to allow for variance -/// w.r.t. the outputs of defined methods. -pub trait Rho { - type Cont<_V>; - - fn rho(&self, f: F) -> Self::Cont - where - F: Fn(T) -> V; - /// the linear activation function is essentially a passthrough function, simply cloning - /// the content. - fn linear(&self) -> Self::Cont { - self.rho(|x| x) - } - - fn linear_derivative(&self) -> Self::Cont - where - T: One, - { - self.rho(|_| ::one()) - } - - fn heavyside(&self) -> Self::Cont - where - T: HeavysideActivation, - { - self.rho(|x| x.heavyside()) - } - - fn heavyside_derivative(&self) -> Self::Cont - where - T: HeavysideActivation, - { - self.rho(|x| x.heavyside_derivative()) - } - - fn relu(&self) -> Self::Cont - where - T: ReLUActivation, - { - self.rho(|x| x.relu()) - } - - fn relu_derivative(&self) -> Self::Cont + fn rho(&self, f: F) -> Self::Cont where - T: ReLUActivation, - { - self.rho(|x| x.relu_derivative()) - } + F: FnMut(T) -> U; - fn sigmoid(&self) -> Self::Cont + fn heavyside(&self) -> Self::Cont where - T: SigmoidActivation, + T: PartialOrd + One + Zero, { - self.rho(|x| x.sigmoid()) + self.rho(|i| if i > T::zero() { T::one() } else { T::zero() }) } - - fn sigmoid_derivative(&self) -> Self::Cont + fn relu(&self) -> Self::Cont where - T: SigmoidActivation, + T: PartialOrd + Zero, { - self.rho(|x| x.sigmoid_derivative()) + self.rho(|i| if i > T::zero() { i } else { T::zero() }) } - fn tanh(&self) -> Self::Cont + fn sinh(&self) -> Self::Cont where - T: TanhActivation, + T: Float, { - self.rho(|x| x.tanh()) + self.rho(|i| i.sinh()) } - fn tanh_derivative(&self) -> Self::Cont + fn tanh(&self) -> Self::Cont<::Output> where - T: TanhActivation, + T: Tanh, { - self.rho(|x| x.tanh_derivative()) - } -} - -#[cfg(feature = "complex")] -/// The [`RhoComplex`] trait is similar to the [`Rho`] trait in that it provides various -/// activation functions for implementos of the [`Apply`] trait, however, instead of being -/// truly generic over a type `U`, it is generic over a type `U` that implements the -/// [`ComplexFloat`] trait. This enables the use of complex numbers in the activation -/// functions, something particularly useful for signal-based workloads. -/// -/// **note**: The [`Rho`] and [`RhoComplex`] traits are not intended to be used together, hence -/// why the implemented methods are not given alternative or unique name between the two -/// traits. If you happen to import both within the same file, you will more than likely need -/// to use a fully qualified syntax to disambiguate the two traits. If this becomes a problem, -/// we may consider renaming the _complex_ methods accordingly to differentiate them from the -/// _standard_ methods. -pub trait RhoComplex: Rho -where - U: ComplexFloat, -{ - fn sigmoid(&self) -> Self::Cont { - self.rho(|x| U::one() / (U::one() + (-x).exp())) - } - - fn sigmoid_derivative(&self) -> Self::Cont { - self.rho(|x| { - let s = U::one() / (U::one() + (-x).exp()); - s * (U::one() - s) - }) - } - - fn tanh(&self) -> Self::Cont { - self.rho(|x| x.tanh()) - } - - fn tanh_derivative(&self) -> Self::Cont { - self.rho(|x| { - let s = x.tanh(); - U::one() - s * s - }) + self.rho(|i| i.tanh()) } } - /* ************* Implementations ************* */ -impl Rho for S +use ndarray::{Array, ArrayBase, Data, Dimension}; + +impl Activate for ArrayBase where - S: Apply, + S: Data, + D: Dimension, + A: Clone, { - type Cont<_V> = S::Cont<_V>; + type Cont = Array; - fn rho(&self, f: F) -> Self::Cont + fn rho(&self, f: F) -> Self::Cont where - F: Fn(T) -> V, + F: FnMut(A) -> U, { - self.apply(|x| f(x)) + self.mapv(f) } } - -#[cfg(feature = "complex")] -impl RhoComplex for S -where - S: Apply, - U: ComplexFloat, -{ -} diff --git a/core/src/activate/traits/ops.rs b/core/src/activate/traits/ops.rs new file mode 100644 index 00000000..54a8c8cf --- /dev/null +++ b/core/src/activate/traits/ops.rs @@ -0,0 +1,37 @@ +/* + Appellation: ops + Created At: 2026.01.13:17:12:10 + Contrib: @FL03 +*/ +/// Compute the softmax activation along a specified axis. +pub trait SoftmaxAxis: SoftmaxActivation { + fn softmax_axis(self, axis: usize) -> Self::Output; +} + +macro_rules! unary { + (@impl $name:ident::$call:ident($($rest:tt)*)) => { + paste::paste! { + pub trait $name { + type Output; + + fn $call($($rest)*) -> Self::Output; + + fn [<$call _derivative>]($($rest)*) -> Self::Output; + } + } + }; + ($($name:ident::$call:ident($($rest:tt)*)),* $(,)?) => { + $( + unary!(@impl $name::$call($($rest)*)); + )* + }; +} + +unary! { + HeavysideActivation::heavyside(self), + LinearActivation::linear(self), + SigmoidActivation::sigmoid(self), + SoftmaxActivation::softmax(&self), + ReLUActivation::relu(&self), + TanhActivation::tanh(&self), +} diff --git a/core/src/activate/traits/unary.rs b/core/src/activate/traits/unary.rs deleted file mode 100644 index cd482a3c..00000000 --- a/core/src/activate/traits/unary.rs +++ /dev/null @@ -1,61 +0,0 @@ -/* - appellation: unary - authors: @FL03 -*/ - -macro_rules! unary { - (@impl $name:ident::$call:ident(self)) => { - paste::paste! { - pub trait $name { - type Output; - - fn $call(self) -> Self::Output; - - fn [<$call _derivative>](self) -> Self::Output; - } - } - - }; - (@impl $name:ident::$call:ident(&self)) => { - paste::paste! { - pub trait $name { - type Output; - - fn $call(&self) -> Self::Output; - - fn [<$call _derivative>](&self) -> Self::Output; - } - } - }; - (@impl $name:ident::$call:ident(&mut self)) => { - paste::paste! { - pub trait $name { - type Output; - - fn $call(&mut self) -> Self::Output; - - fn [<$call _derivative>](&mut self) -> Self::Output; - } - } - }; - ($( - $name:ident::$call:ident($($rest:tt)*) - ),* $(,)?) => { - $( - unary!(@impl $name::$call($($rest)*)); - )* - }; -} - -unary! { - HeavysideActivation::heavyside(self), - LinearActivation::linear(self), - SigmoidActivation::sigmoid(self), - SoftmaxActivation::softmax(&self), - ReLUActivation::relu(&self), - TanhActivation::tanh(&self), -} - -pub trait SoftmaxAxis: SoftmaxActivation { - fn softmax_axis(self, axis: usize) -> Self::Output; -} diff --git a/core/src/activate/utils.rs b/core/src/activate/utils/funcs.rs similarity index 90% rename from core/src/activate/utils.rs rename to core/src/activate/utils/funcs.rs index dc78948e..5a6f6f58 100644 --- a/core/src/activate/utils.rs +++ b/core/src/activate/utils/funcs.rs @@ -34,7 +34,7 @@ where /// the sigmoid activation function: /// /// ```math -/// \mbox{f}(x)=\frac{1}{1+\exp(-x)} +/// f(x)=(1+e^{-x})^{-1} /// ``` pub fn sigmoid(args: T) -> T where @@ -67,7 +67,7 @@ where /// Softmax function along a specific axis: /// /// ```math -/// f(x_i) = \frac{e^{x_i}}{\sum_j e^{x_j}} +/// f(x_i) = \frac{e^{x_i}}{\sum_{j} e^{x_j}} /// ``` pub fn softmax_axis(args: &ArrayBase, axis: usize) -> Array where @@ -79,7 +79,7 @@ where let e = args.exp(); &e / &e.sum_axis(axis) } -/// the tanh activation function: +/// Hyperbolic tangent /// /// ```math /// f(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}} @@ -117,13 +117,7 @@ where /// Heaviside activation function: /// /// ```math -/// H(x) = -/// \left\{ -/// \begin{array}{rcl} -/// 1 & \mbox{if} & x\gt{0} \\ -/// 0 & \mbox{if} & x\leq{0} -/// \end{array} -/// \right. +/// H(x)=\begin{cases}1 &x\gt{0} \\ 0 &x\leq{0} \end{cases} /// ``` pub fn heavyside(x: T) -> T where diff --git a/core/src/config/hyper_params.rs b/core/src/config/hyper_params.rs index a4d490a8..4e579d78 100644 --- a/core/src/config/hyper_params.rs +++ b/core/src/config/hyper_params.rs @@ -2,9 +2,6 @@ Appellation: hyper_params Contrib: @FL03 */ -#[cfg(feature = "alloc")] -use alloc::string::{String, ToString}; - /// An enumeration of common HyperParams used in neural network configurations. #[derive( Clone, @@ -14,9 +11,12 @@ use alloc::string::{String, ToString}; Ord, PartialEq, PartialOrd, + strum::AsRefStr, + strum::Display, strum::EnumCount, strum::EnumIs, strum::EnumIter, + strum::EnumString, strum::VariantNames, )] #[cfg_attr( @@ -24,11 +24,13 @@ use alloc::string::{String, ToString}; derive(serde::Deserialize, serde::Serialize), serde(rename_all = "snake_case", untagged) )] +#[strum(serialize_all = "snake_case")] +#[non_exhaustive] pub enum HyperParam { Decay, - #[serde(alias = "drop_out", alias = "p")] + #[cfg_attr(feature = "serde", serde(alias = "drop_out", alias = "p"))] Dropout, - #[serde(alias = "lr", alias = "gamma")] + #[cfg_attr(feature = "serde", serde(alias = "lr", alias = "gamma"))] LearningRate, Momentum, Temperature, @@ -36,16 +38,9 @@ pub enum HyperParam { Beta1, Beta2, Epsilon, - #[cfg(feature = "alloc")] - Custom(String), } impl HyperParam { - #[cfg(feature = "alloc")] - /// creates a custom hyperparameter variant - pub fn custom(name: T) -> Self { - HyperParam::Custom(name.to_string()) - } /// returns a list of variants as strings pub const fn variants() -> &'static [&'static str] { use strum::VariantNames; @@ -53,65 +48,12 @@ impl HyperParam { } } -impl core::fmt::Display for HyperParam { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.write_str(self.as_ref()) - } -} - -impl AsRef for HyperParam { - fn as_ref(&self) -> &str { - match self { - HyperParam::Decay => "decay", - HyperParam::Dropout => "dropout", - HyperParam::LearningRate => "learning_rate", - HyperParam::Momentum => "momentum", - HyperParam::Temperature => "temperature", - HyperParam::WeightDecay => "weight_decay", - HyperParam::Beta1 => "beta1", - HyperParam::Beta2 => "beta2", - HyperParam::Epsilon => "epsilon", - HyperParam::Custom(s) => s.as_ref(), - } - } -} - impl core::borrow::Borrow for HyperParam { fn borrow(&self) -> &str { self.as_ref() } } -#[cfg(feature = "alloc")] -impl core::convert::From for HyperParam { - fn from(s: String) -> Self { - core::str::FromStr::from_str(&s).expect("Failed to convert String to HyperParams") - } -} - -impl From<&str> for HyperParam { - fn from(s: &str) -> Self { - core::str::FromStr::from_str(s).expect("Failed to convert &str to HyperParams") - } -} - -impl core::str::FromStr for HyperParam { - type Err = core::convert::Infallible; - - fn from_str(s: &str) -> Result { - match s { - "decay" => Ok(HyperParam::Decay), - "dropout" => Ok(HyperParam::Dropout), - "learning_rate" => Ok(HyperParam::LearningRate), - "momentum" => Ok(HyperParam::Momentum), - "temperature" => Ok(HyperParam::Temperature), - "weight_decay" => Ok(HyperParam::WeightDecay), - #[cfg(feature = "alloc")] - other => Ok(HyperParam::Custom(other.to_string())), - } - } -} - #[cfg(test)] mod tests { use super::HyperParam; @@ -119,18 +61,21 @@ mod tests { #[test] fn test_hyper_params() { use HyperParam::*; + use core::str::FromStr; - assert_eq!(HyperParam::from("learning_rate"), LearningRate); - - assert_eq!(HyperParam::from("weight_decay"), WeightDecay); - - for v in ["something", "another_param", "custom_hyperparam"] { - let param = HyperParam::from(v); - assert!(param.is_custom()); - match param { - HyperParam::Custom(s) => assert_eq!(s, v), - _ => panic!("Expected Custom variant"), - } + let tests = [ + ("decay", Decay), + ("dropout", Dropout), + ("momentum", Momentum), + ("temperature", Temperature), + ("beta1", Beta1), + ("beta2", Beta2), + ("epsilon", Epsilon), + ("learning_rate", LearningRate), + ("weight_decay", WeightDecay), + ]; + for (s, param) in tests { + assert_eq!(HyperParam::from_str(s).ok(), Some(param)); } } } diff --git a/core/src/config/mod.rs b/core/src/config/mod.rs index a13bf3d9..5b15d0fe 100644 --- a/core/src/config/mod.rs +++ b/core/src/config/mod.rs @@ -9,6 +9,8 @@ pub use self::{hyper_params::HyperParam, model_config::StandardModelConfig}; pub mod hyper_params; pub mod model_config; +// prelude (local) +#[doc(hidden)] pub(crate) mod prelude { pub use super::hyper_params::HyperParam; pub use super::model_config::*; @@ -41,7 +43,7 @@ pub trait ModelConfiguration: RawConfig { where K: AsRef; - fn keys(&self) -> Vec; + fn keys(&self) -> Vec<&str>; } macro_rules! hyperparam_method { diff --git a/core/src/config/model_config.rs b/core/src/config/model_config.rs index acfc28e2..6a7fe624 100644 --- a/core/src/config/model_config.rs +++ b/core/src/config/model_config.rs @@ -4,9 +4,11 @@ */ use super::HyperParam; use super::{ExtendedModelConfig, ModelConfiguration, RawConfig}; +use alloc::string::{String, ToString}; use hashbrown::DefaultHashBuilder; use hashbrown::hash_map::{self, HashMap}; +/// The [`StandardModelConfig`] struct is a standard implementation of the #[derive(Clone, Debug)] #[cfg_attr( feature = "serde", @@ -16,7 +18,7 @@ use hashbrown::hash_map::{self, HashMap}; pub struct StandardModelConfig { pub batch_size: usize, pub epochs: usize, - pub hyperspace: HashMap, + pub hyperspace: HashMap, } impl StandardModelConfig { @@ -44,37 +46,37 @@ impl StandardModelConfig { &mut self.epochs } /// returns a reference to the hyperparameters map - pub const fn hyperparameters(&self) -> &HashMap { + pub const fn hyperparameters(&self) -> &HashMap { &self.hyperspace } /// returns a mutable reference to the hyperparameters map - pub const fn hyperparameters_mut(&mut self) -> &mut HashMap { + pub const fn hyperparameters_mut(&mut self) -> &mut HashMap { &mut self.hyperspace } /// inserts a hyperparameter into the map, returning the previous value if it exists - pub fn add_parameter>(&mut self, key: P, value: T) -> Option { - self.hyperparameters_mut().insert(key.into(), value) + pub fn add_parameter(&mut self, key: K, value: T) -> Option { + self.hyperparameters_mut().insert(key.to_string(), value) } /// gets a reference to a hyperparameter by key, returning None if it does not exist - pub fn get_parameter(&self, key: &Q) -> Option<&T> + pub fn get(&self, key: &Q) -> Option<&T> where Q: ?Sized + Eq + core::hash::Hash, - HyperParam: core::borrow::Borrow, + String: core::borrow::Borrow, { self.hyperparameters().get(key) } /// returns an entry for the hyperparameter, allowing for insertion or modification - pub fn parameter(&mut self, key: Q) -> hash_map::Entry<'_, HyperParam, T, DefaultHashBuilder> - where - Q: AsRef, - { - self.hyperparameters_mut().entry(key.as_ref().into()) + pub fn parameter( + &mut self, + key: Q, + ) -> hash_map::Entry<'_, String, T, DefaultHashBuilder> { + self.hyperparameters_mut().entry(key.to_string()) } /// removes a hyperparameter from the map, returning the value if it exists pub fn remove_hyperparameter(&mut self, key: &Q) -> Option where Q: ?Sized + core::hash::Hash + Eq, - HyperParam: core::borrow::Borrow, + String: core::borrow::Borrow, { self.hyperparameters_mut().remove(key) } @@ -118,19 +120,19 @@ impl StandardModelConfig { } /// returns a reference to the learning rate hyperparameter, if it exists pub fn learning_rate(&self) -> Option<&T> { - self.get_parameter(&LearningRate) + self.get("learning_rate") } /// returns a reference to the momentum hyperparameter, if it exists pub fn momentum(&self) -> Option<&T> { - self.get_parameter(&Momentum) + self.get("momentum") } /// returns a reference to the decay hyperparameter, if it exists pub fn decay(&self) -> Option<&T> { - self.get_parameter(&Decay) + self.get("decay") } /// returns a reference to the weight decay hyperparameter, if it exists pub fn weight_decay(&self) -> Option<&T> { - self.get_parameter(&WeightDecay) + self.get("weight_decay") } } @@ -144,6 +146,18 @@ unsafe impl Send for StandardModelConfig where T: Send {} unsafe impl Sync for StandardModelConfig where T: Sync {} +impl crate::nn::NetworkConfig for StandardModelConfig { + type Store = HashMap; + + fn store(&self) -> &Self::Store { + &self.hyperspace + } + + fn store_mut(&mut self) -> &mut Self::Store { + &mut self.hyperspace + } +} + impl RawConfig for StandardModelConfig { type Ctx = T; } @@ -185,8 +199,8 @@ impl ModelConfiguration for StandardModelConfig { self.hyperparameters().contains_key(key.as_ref()) } - fn keys(&self) -> Vec { - self.hyperparameters().keys().cloned().collect() + fn keys(&self) -> Vec<&str> { + self.hyperparameters().keys().map(|k| k.as_str()).collect() } } diff --git a/core/src/error.rs b/core/src/error.rs index 56386a40..1357af88 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -14,6 +14,8 @@ use alloc::{boxed::Box, string::String}; #[derive(Debug, thiserror::Error)] #[non_exhaustive] pub enum Error { + #[error("The provided batch is empty")] + EmptyBatch, #[error("Invalid model configuration")] InvalidModelConfig, #[error("The model is not supported for the given input")] @@ -42,13 +44,10 @@ pub enum Error { #[error(transparent)] BoxError(#[from] Box), #[error(transparent)] - PadError(#[from] crate::utils::pad::PadError), - #[error(transparent)] ParamError(#[from] concision_params::ParamsError), #[error(transparent)] + #[cfg(feature = "concision_init")] InitError(#[from] concision_init::InitError), - #[error(transparent)] - TraitError(#[from] concision_traits::Error), #[cfg(feature = "serde")] #[error(transparent)] DeserializeError(#[from] serde::de::value::Error), diff --git a/core/src/ex/sample.rs b/core/src/ex/sample.rs index 4016f6d1..c6fa55fb 100644 --- a/core/src/ex/sample.rs +++ b/core/src/ex/sample.rs @@ -2,26 +2,32 @@ appellation: model authors: @FL03 */ -#![cfg(feature = "std")] use crate::activate::{ReLUActivation, SigmoidActivation}; -use crate::{ - DeepModelParams, Error, Forward, Model, ModelFeatures, Norm, Params, StandardModelConfig, Train, -}; +use crate::config::StandardModelConfig; +use crate::error::Error; +use crate::models::{DeepModelParams, ModelFeatures}; +use crate::nn::Model; #[cfg(feature = "rand")] use concision_init::{ - NdInit, + NdRandom, rand_distr::{Distribution, StandardNormal}, }; - +use concision_params::Params; +use concision_traits::{Forward, Norm, Train}; use ndarray::prelude::*; use ndarray::{Data, ScalarOperand}; use num_traits::{Float, FromPrimitive, NumAssign, Zero}; +#[cfg(not(feature = "tracing"))] +use eprintln as error; +#[cfg(feature = "tracing")] +use tracing::error; + #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct TestModel { pub config: StandardModelConfig, pub features: ModelFeatures, - pub params: DeepModelParams, + pub store: DeepModelParams, } impl TestModel { @@ -29,11 +35,11 @@ impl TestModel { where T: Clone + Zero, { - let params = DeepModelParams::zeros(features); + let store = DeepModelParams::zeros(features); TestModel { config, features, - params, + store, } } /// returns an immutable reference to the model configuration @@ -45,12 +51,16 @@ impl TestModel { self.features } /// returns a reference to the model params - pub const fn params(&self) -> &DeepModelParams { - &self.params + pub const fn store(&self) -> &DeepModelParams { + &self.store } /// returns a mutable reference to the model params - pub const fn params_mut(&mut self) -> &mut DeepModelParams { - &mut self.params + pub const fn store_mut(&mut self) -> &mut DeepModelParams { + &mut self.store + } + #[cfg(not(feature = "rand"))] + pub fn init(self) -> Self { + self } #[cfg(feature = "rand")] /// consumes the current instance to initalize another with random parameters @@ -60,25 +70,25 @@ impl TestModel { T: Float, { let TestModel { - mut params, + mut store, config, features, } = self; - params.set_input(Params::::lecun_normal(( + store.set_input(Params::::lecun_normal(( features.input(), features.hidden(), ))); - for layer in params.hidden_mut() { + for layer in store.hidden_mut() { *layer = Params::::lecun_normal((features.hidden(), features.hidden())); } - params.set_output(Params::::lecun_normal(( + store.set_output(Params::::lecun_normal(( features.hidden(), features.output(), ))); TestModel { config, features, - params, + store, } } } @@ -101,11 +111,11 @@ impl Model for TestModel { } fn params(&self) -> &DeepModelParams { - &self.params + &self.store } fn params_mut(&mut self) -> &mut DeepModelParams { - &mut self.params + &mut self.store } } @@ -121,13 +131,13 @@ where fn forward(&self, input: &ArrayBase) -> Self::Output { // complete the first forward pass using the input layer - let mut output = self.params().input().forward(input).relu(); + let mut output = self.store().input().forward(input).relu(); // complete the forward pass for each hidden layer - for layer in self.params().hidden() { + for layer in self.store().hidden() { output = layer.forward(&output).relu(); } - self.params().output().forward(&output).sigmoid() + self.store().output().forward(&output).sigmoid() } } @@ -172,15 +182,15 @@ where let mut activations = Vec::new(); activations.push(input.to_owned()); - let mut output = self.params().input().forward_then(&input, |y| y.relu()); + let mut output = self.store().input().forward_then(&input, |y| y.relu()); activations.push(output.to_owned()); // collect the activations of the hidden - for layer in self.params().hidden() { + for layer in self.store().hidden() { output = layer.forward(&output).relu(); activations.push(output.to_owned()); } - output = self.params().output().forward(&output).sigmoid(); + output = self.store().output().forward(&output).sigmoid(); activations.push(output.to_owned()); // Calculate output layer error @@ -192,7 +202,7 @@ where delta /= delta.l2_norm(); // Normalize the delta to prevent exploding gradients // Update output weights - self.params_mut() + self.store_mut() .output_mut() .backward(activations.last().unwrap(), &delta, lr); @@ -202,23 +212,23 @@ where // Calculate error for this layer delta = if i == num_hidden - 1 { // use the output activations for the final hidden layer - self.params().output().weights().dot(&delta) * activations[i + 1].relu_derivative() + self.store().output().weights().dot(&delta) * activations[i + 1].relu_derivative() } else { // else; backpropagate using the previous hidden layer - self.params().hidden()[i + 1].weights().t().dot(&delta) + self.store().hidden()[i + 1].weights().t().dot(&delta) * activations[i + 1].relu_derivative() }; // Normalize delta to prevent exploding gradients delta /= delta.l2_norm(); - self.params_mut().hidden_mut()[i].backward(&activations[i + 1], &delta, lr); + self.store_mut().hidden_mut()[i].backward(&activations[i + 1], &delta, lr); } /* The delta for the input layer is computed using the weights of the first hidden layer and the derivative of the activation function of the first hidden layer. */ - delta = self.params().hidden()[0].weights().dot(&delta) * activations[1].relu_derivative(); + delta = self.store().hidden()[0].weights().dot(&delta) * activations[1].relu_derivative(); delta /= delta.l2_norm(); // Normalize the delta to prevent exploding gradients - self.params_mut() + self.store_mut() .input_mut() .backward(&activations[1], &delta, lr); @@ -262,15 +272,7 @@ where loss += match Train::, ArrayView1>::train(self, &x, &e) { Ok(l) => l, Err(err) => { - #[cfg(not(feature = "tracing"))] - eprintln!( - "Training failed for batch {}/{}: {:?}", - i + 1, - batch_size, - err - ); - #[cfg(feature = "tracing")] - tracing::error!( + error!( "Training failed for batch {}/{}: {:?}", i + 1, batch_size, diff --git a/core/src/layers/layer/impl_layer.rs b/core/src/layers/layer/impl_layer.rs deleted file mode 100644 index 6c90fca8..00000000 --- a/core/src/layers/layer/impl_layer.rs +++ /dev/null @@ -1,61 +0,0 @@ -/* - appellation: impl_layer - authors: @FL03 -*/ -use crate::layers::Layer; - -use crate::layers::{Activator, RawLayer}; -use concision_params::{ParamsBase, RawParam}; -use concision_traits::Forward; -use ndarray::{DataOwned, Dimension, RawData, RemoveAxis, ShapeBuilder}; - -impl Layer> -where - F: Activator, - D: Dimension, - S: RawData, -{ - /// create a new [`LayerBase`] from the given activation function and shape. - pub fn from_rho_with_shape(rho: F, shape: Sh) -> Self - where - A: Clone + Default, - S: DataOwned, - D: RemoveAxis, - Sh: ShapeBuilder, - { - Self { - rho, - params: ParamsBase::default(shape), - } - } -} - -impl Forward for Layer -where - F: Activator, - P: RawParam + Forward, -{ - type Output = Y; - - fn forward(&self, input: &X) -> Self::Output { - self.rho().activate(self.params().forward(input)) - } -} - -impl RawLayer for Layer -where - F: Activator

, - P: RawParam, -{ - fn rho(&self) -> &F { - &self.rho - } - - fn params(&self) -> &P { - &self.params - } - - fn params_mut(&mut self) -> &mut P { - &mut self.params - } -} diff --git a/core/src/layers/layer/impl_layer_deprecated.rs b/core/src/layers/layer/impl_layer_deprecated.rs deleted file mode 100644 index 96d12c65..00000000 --- a/core/src/layers/layer/impl_layer_deprecated.rs +++ /dev/null @@ -1,10 +0,0 @@ -/* - appellation: impl_layer_deprecated - authors: @FL03 -*/ -#![allow(deprecated)] - -use crate::layers::Layer; - -#[doc(hidden)] -impl Layer {} diff --git a/core/src/layers/layer/impl_layer_repr.rs b/core/src/layers/layer/impl_layer_repr.rs deleted file mode 100644 index 79ede5a1..00000000 --- a/core/src/layers/layer/impl_layer_repr.rs +++ /dev/null @@ -1,44 +0,0 @@ -/* - appellation: impl_layer_repr - authors: @FL03 -*/ -use super::Layer; - -use crate::layers::{Linear, ReLU, Sigmoid, Tanh}; - -impl Layer { - /// initialize a new [`LayerBase`] using a [`Linear`] activation function and the given - /// parameters. - pub const fn linear(params: T) -> Self { - Self { - rho: Linear, - params, - } - } -} - -impl Layer { - /// initialize a new [`LayerBase`] using a [`Sigmoid`] activation function and the given - /// parameters. - pub const fn sigmoid(params: T) -> Self { - Self { - rho: Sigmoid, - params, - } - } -} - -impl Layer { - /// initialize a new [`LayerBase`] using a [`Tanh`] activation function and the given - /// parameters. - pub const fn tanh(params: T) -> Self { - Self { rho: Tanh, params } - } -} - -impl Layer { - /// initialize a new [`LayerBase`] using a [`ReLU`] activation function and the given - pub const fn relu(params: T) -> Self { - Self { rho: ReLU, params } - } -} diff --git a/core/src/layers/mod.rs b/core/src/layers/mod.rs deleted file mode 100644 index a8aa7ca5..00000000 --- a/core/src/layers/mod.rs +++ /dev/null @@ -1,81 +0,0 @@ -/* - Appellation: layers - Contrib: @FL03 -*/ -//! This module provides the [`Layer`] implementation along with supporting traits and types. -//! -//! struct, a generic representation of a neural network -//! layer by associating -//! -#[doc(inline)] -pub use self::{layer::Layer, traits::*, types::*}; - -mod layer; - -pub mod seq; - -pub(crate) mod traits { - #[doc(inline)] - pub use self::{activator::*, layers::*}; - - mod activator; - mod layers; - mod store; -} - -pub(crate) mod types { - #[doc(inline)] - pub use self::aliases::*; - - mod aliases; -} - -pub(crate) mod prelude { - pub use super::layer::*; - pub use super::types::*; -} - -#[cfg(test)] -mod tests { - use super::*; - use concision_params::Params; - use ndarray::{Array1, array}; - - #[test] - fn test_linear_layer() { - let params = Params::from_elem((3, 2), 0.5_f32); - let layer = Layer::linear(params); - - assert_eq!(layer.params().shape(), &[3, 2]); - - let inputs = Array1::linspace(1.0_f32, 2.0_f32, 3); - println!("{:?}", inputs); - assert_eq!(layer.forward(&inputs), array![2.75, 2.75]); - } - - #[test] - fn test_relu_layer() { - let params = Params::from_elem((3, 2), 0.5_f32); - let layer = Layer::relu(params); - - assert_eq!(layer.params().shape(), &[3, 2]); - - let inputs = Array1::linspace(1.0_f32, 2.0_f32, 3); - assert_eq!(layer.forward(&inputs), array![2.75, 2.75]); - } - #[cfg(feature = "approx")] - #[test] - fn test_tanh_layer() { - let params = Params::from_elem((3, 2), 0.5_f32); - let layer = Layer::tanh(params); - - assert_eq!(layer.params().shape(), &[3, 2]); - - let inputs = Array1::linspace(1.0_f32, 2.0_f32, 3); - approx::assert_abs_diff_eq!( - layer.forward(&inputs), - Array1::from_elem(2, 0.99185973), - epsilon = 1e-6 - ); - } -} diff --git a/core/src/layers/seq.rs b/core/src/layers/seq.rs deleted file mode 100644 index 98d5ce88..00000000 --- a/core/src/layers/seq.rs +++ /dev/null @@ -1,15 +0,0 @@ -/* - appellation: sequential - authors: @FL03 -*/ -use concision_params::ParamsBase; -use ndarray::{Dimension, RawData}; - -#[allow(dead_code)] -pub struct Sequential -where - D: Dimension, - S: RawData, -{ - pub(crate) layers: Vec>, -} diff --git a/core/src/layers/traits/activator.rs b/core/src/layers/traits/activator.rs deleted file mode 100644 index 27849177..00000000 --- a/core/src/layers/traits/activator.rs +++ /dev/null @@ -1,114 +0,0 @@ -/* - appellation: activate - authors: @FL03 -*/ -/// The [`Activator`] trait defines a method for applying an activation function to an input -/// tensor. -pub trait Activator { - type Output; - - /// Applies the activation function to the input tensor. - fn activate(&self, input: T) -> Self::Output; -} -/// The [`ActivatorGradient`] trait extends the [`Activator`] trait to include a method for -/// computing the gradient of the activation function. -pub trait ActivatorGradient: Activator { - type Delta; - - /// compute the gradient of some input - fn activate_gradient(&self, input: T) -> Self::Delta; -} - -/* - ************* Implementations ************* -*/ -impl Activator for &T -where - T: Activator, -{ - type Output = B; - - fn activate(&self, rhs: A) -> Self::Output { - (*self).activate(rhs) - } -} - -impl ActivatorGradient for &U -where - U: ActivatorGradient, -{ - type Delta = G; - - fn activate_gradient(&self, inputs: A) -> Self::Delta { - (*self).activate_gradient(inputs) - } -} - -impl Activator for dyn Fn(X) -> Y { - type Output = Y; - - fn activate(&self, rhs: X) -> Self::Output { - self(rhs) - } -} - -#[cfg(feature = "alloc")] -impl Activator for alloc::boxed::Box> { - type Output = Y; - - fn activate(&self, rhs: X) -> Self::Output { - self.as_ref().activate(rhs) - } -} - -/* - ************* Implementations ************* -*/ -macro_rules! activator { - ($( - $vis:vis struct $name:ident.$method:ident where $T:ident: $($trait:ident)::* - );* $(;)?) => { - $( - activator!(@impl $vis struct $name.$method where $T: $($trait)::* ); - )* - }; - (@impl $vis:vis struct $name:ident.$method:ident where $T:ident: $($trait:ident)::* ) => { - - #[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)] - #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] - $vis struct $name; - - impl Activator for $name - where - U: $($trait)::*, - { - type Output = U::Output; - - fn activate(&self, x: U) -> Self::Output { - x.$method() - } - } - - paste::paste! { - impl ActivatorGradient for $name - where - U: $($trait)::*, - { - type Delta = U::Output; - - fn activate_gradient(&self, inputs: U) -> Self::Delta { - inputs.[<$method _derivative>]() - } - } - } - }; -} - -activator! { - pub struct Linear.linear where T: crate::activate::LinearActivation; - pub struct ReLU.relu where T: crate::activate::ReLUActivation; - pub struct Sigmoid.sigmoid where T: crate::activate::SigmoidActivation; - pub struct Tanh.tanh where T: crate::activate::TanhActivation; - pub struct HeavySide.heavyside where T: crate::activate::HeavysideActivation; - pub struct Softmax.softmax where T: crate::activate::SoftmaxActivation; -} diff --git a/core/src/layers/traits/layers.rs b/core/src/layers/traits/layers.rs deleted file mode 100644 index 5a109921..00000000 --- a/core/src/layers/traits/layers.rs +++ /dev/null @@ -1,61 +0,0 @@ -/* - appellation: layers - authors: @FL03 -*/ -use super::{Activator, ActivatorGradient}; - -use concision_params::{ParamsBase, RawParam}; -use concision_traits::{Backward, Forward}; -use ndarray::{Data, Dimension, RawData}; - -pub trait RawLayer::Elem> -where - F: Activator, - X: RawParam, -{ - /// the activation function of the layer - fn rho(&self) -> &F; - /// returns an immutable reference to the parameters of the layer - fn params(&self) -> &X; - /// returns a mutable reference to the parameters of the layer - fn params_mut(&mut self) -> &mut X; -} -/// A generic trait defining the composition of a _layer_ within a neural network. -pub trait NdLayer::Elem> -where - D: Dimension, - S: RawData, -{ - /// The type of activator used by the layer; the type must implement [`ActivatorGradient`] - type Rho: Activator; - - fn rho(&self) -> &Self::Rho; - /// returns an immutable reference to the parameters of the layer - fn params(&self) -> &ParamsBase; - /// returns a mutable reference to the parameters of the layer - fn params_mut(&mut self) -> &mut ParamsBase; - - /// update the layer parameters - fn set_params(&mut self, params: ParamsBase) { - *self.params_mut() = params; - } - /// backward propagate error through the layer - fn backward(&mut self, input: X, error: Y, gamma: A) - where - S: Data, - Self: ActivatorGradient, - A: Clone, - ParamsBase: Backward, - { - let delta = self.activate_gradient(error); - self.params_mut().backward(&input, &delta, gamma) - } - /// complete a forward pass through the layer - fn forward(&self, input: &X) -> Y - where - ParamsBase: Forward, - Self: Activator, - { - self.params().forward_then(input, |y| self.activate(y)) - } -} diff --git a/core/src/layers/traits/store.rs b/core/src/layers/traits/store.rs deleted file mode 100644 index 8b137891..00000000 --- a/core/src/layers/traits/store.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/core/src/layers/types/aliases.rs b/core/src/layers/types/aliases.rs deleted file mode 100644 index 198297a2..00000000 --- a/core/src/layers/types/aliases.rs +++ /dev/null @@ -1,35 +0,0 @@ -/* - appellation: aliases - authors: @FL03 -*/ -#[cfg(feature = "alloc")] -pub use self::impl_alloc::*; - -use crate::layers::{HeavySide, Layer, Linear, ReLU, Sigmoid, Tanh}; -use concision_params::{Params, ParamsBase}; -use ndarray::Ix2; - -pub type LayerParamsBase = Layer>; - -pub type LayerParams = Layer>; - -/// A type alias for a [`Layer`] configured with a [`Linear`] activation function. -pub type LinearLayer = Layer; -/// A type alias for a [`Layer`] configured with a [`Sigmoid`] activation function. -pub type SigmoidLayer = Layer; -/// A type alias for a [`Layer`] configured with a [`Tanh`] activation function. -pub type TanhLayer = Layer; -/// A type alias for a [`Layer`] configured with a [`ReLU`] activation function. -pub type ReluLayer = Layer; -/// A type alias for a [`Layer`] configured with a [`HeavySide`] activation function. -/// This activation function is also known as the step function. -pub type HeavysideLayer = Layer; - -#[cfg(feature = "alloc")] -mod impl_alloc { - use crate::layers::{Activator, Layer}; - use alloc::boxed::Box; - - /// A type alias for a [`Layer`] configured with a dynamic [`Activator`]. - pub type LayerDyn = Layer + 'static>, T>; -} diff --git a/core/src/lib.rs b/core/src/lib.rs index 3c2ec8eb..f4b7ab3a 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -10,7 +10,6 @@ //! The crate is heavily feature-gate, enabling users to customize their experience based on //! their needs. //! -//! - `init`: Enables (random) initialization routines for models, parameters, and tensors. //! - `utils`: Provides various utilities for developing machine learning models. //! //! ### Dependency-specific Features @@ -21,7 +20,7 @@ //! - `approx`: Enables approximate equality checks for floating-point numbers. //! - `complex`: Enables complex number support. //! - `json`: Enables JSON serialization and deserialization capabilities. -//! - `rand`: Enables random number generation capabilities. +//! - `rand`: Enables random number generation //! - `serde`: Enables serialization and deserialization capabilities. //! - `tracing`: Enables tracing capabilities for debugging and logging. //! @@ -34,35 +33,27 @@ rustdoc::redundant_explicit_links )] #![cfg_attr(not(feature = "std"), no_std)] -#![cfg_attr(feature = "nightly", feature(allocator_api))] - +#![cfg_attr(all(feature = "nightly", feature = "alloc"), feature(allocator_api))] +#![cfg_attr(all(feature = "nightly", feature = "autodiff"), feature(autodiff))] +// compile-time checks #[cfg(not(any(feature = "std", feature = "alloc")))] -compiler_error! { - "Either the \"std\" feature or the \"alloc\" feature must be enabled." -} - +compiler_error! { "Either the \"std\" feature or the \"alloc\" feature must be enabled." } +// external crates #[cfg(feature = "alloc")] extern crate alloc; - -/// this module establishes generic random initialization routines for models, params, and -/// tensors. +// re-definitions #[doc(inline)] +#[cfg(feature = "rand")] pub use concision_init as init; #[doc(inline)] pub use concision_params as params; - -#[doc(inline)] -pub use concision_init::prelude::*; -#[doc(inline)] -pub use concision_params::prelude::*; -#[doc(inline)] -pub use concision_traits::prelude::*; - #[macro_use] pub(crate) mod macros { #[macro_use] pub mod config; #[macro_use] + pub mod gsw; + #[macro_use] pub mod seal; #[macro_use] pub mod units; @@ -71,8 +62,6 @@ pub(crate) mod macros { pub mod activate; pub mod config; pub mod error; -pub mod layers; -pub mod layout; pub mod models; pub mod nn; pub mod utils; @@ -82,24 +71,42 @@ pub mod ex { pub mod sample; } +mod types { + #[doc(inline)] + pub use self::parameters::*; + + mod parameters; +} // re-exports #[doc(inline)] pub use self::{ - activate::prelude::*, config::prelude::*, error::*, layers::Layer, layout::*, - models::prelude::*, utils::prelude::*, + activate::{Activate, Activator, ActivatorGradient}, + config::StandardModelConfig, + error::*, + models::prelude::*, + nn::prelude::*, + types::*, + utils::*, }; +#[doc(inline)] +#[cfg(feature = "rand")] +pub use concision_init::prelude::*; +#[doc(inline)] +pub use concision_params::prelude::*; +#[doc(inline)] +pub use concision_traits::prelude::*; + // prelude #[doc(hidden)] pub mod prelude { + #[cfg(feature = "rand")] pub use concision_init::prelude::*; pub use concision_params::prelude::*; pub use concision_traits::prelude::*; - pub use crate::activate::prelude::*; pub use crate::config::prelude::*; - pub use crate::layers::prelude::*; - pub use crate::layout::*; pub use crate::models::prelude::*; pub use crate::nn::prelude::*; - pub use crate::utils::prelude::*; + pub use crate::types::*; + pub use crate::utils::*; } diff --git a/core/src/macros/config.rs b/core/src/macros/config.rs index fe3a053e..aa13142e 100644 --- a/core/src/macros/config.rs +++ b/core/src/macros/config.rs @@ -12,7 +12,7 @@ macro_rules! config { ( $(#[$attr:meta])* - $vis:vis struct $name:ident {$($field:ident: $type:ty),* $(,)?} + $name:ident {$($field:ident: $T:ty $(= $val:expr)?),* $(,)?} ) => { $(#[$attr])* #[cfg_attr( @@ -21,44 +21,44 @@ macro_rules! config { serde(rename_all = "snake_case"), )] #[repr(C)] - #{derive(Clone, Debug, Default, PartialEq, PartialOrd)} - $vis struct $name { - $($field: $type),* + #[derive(Clone, Debug, Default, PartialEq, PartialOrd)] + pub struct $name { + $(pub $field: $T),* } - config!(@impl $vis struct $name {$($field: $type),*}); - }; - (@impl $vis:vis struct $name:ident {$($field:ident: $type:ty),* $(,)?}) => { impl $name { - pub fn new() -> Self { - Self { - $($field: Default::default()),* - } + config!(@impl $($field: $T $(= $val)?),*); + } + }; + (@impl $($field:ident: $T:ty $(= $val:expr)?),* $(,)?) => { + pub fn new() -> Self { + Self { + $($field: Default::default()),* } - paste::paste! { - $( - /// returns an immutable reference to the field - pub const fn $field(&self) -> &$type { - &self.$field - } - /// return a mutable reference to the field - pub const fn [<$field _mut>](&mut self) -> &mut $type { - &mut self.$field - } - /// update the current value of the field and return a mutable reference to self - pub const fn [](&mut self, value: $type) -> &mut Self { - self.$field = value; - self - } - /// consume the current instance to create another with the given value - pub fn [](self, $field: $type) -> Self { - Self { - $field, - ..self - } + } + paste::paste! { + $( + /// returns an immutable reference to the field + pub const fn $field(&self) -> &$T { + &self.$field + } + /// return a mutable reference to the field + pub const fn [<$field _mut>](&mut self) -> &mut $T { + &mut self.$field + } + /// update the current value of the field and return a mutable reference to self + pub fn [](&mut self, value: $T) -> &mut Self { + self.$field = value; + self + } + /// consume the current instance to create another with the given value + pub fn [](self, $field: $T) -> Self { + Self { + $field, + ..self } - )* - } + } + )* } }; } diff --git a/core/src/macros/gsw.rs b/core/src/macros/gsw.rs new file mode 100644 index 00000000..ce7b2aee --- /dev/null +++ b/core/src/macros/gsw.rs @@ -0,0 +1,78 @@ +/* + appellation: gsw + authors: @FL03 +*/ + +#[allow(unused_macros)] +/// [`get!`] is a macro used to generate getter methods for a struct's fields. +macro_rules! get { + ($($field:ident: $T:ty),* $(,)?) => { + paste::paste! { + $( + /// returns an immutable reference to the field + pub const fn $field(&self) -> $T { + self.$field + } + /// return a mutable reference to the field + pub const fn [<$field _mut>](&mut self) -> &mut $T { + &mut self.$field + } + )* + } + }; + ($($field:ident: &$T:ty),* $(,)?) => { + paste::paste! { + $( + /// returns an immutable reference to the field + pub const fn $field(&self) -> &$T { + &self.$field + } + /// return a mutable reference to the field + pub const fn [<$field _mut>](&mut self) -> &mut $T { + &mut self.$field + } + )* + } + }; +} + +#[allow(unused_macros)] +/// [`set_with!`] is a macro used to generate `set_` and `with_` methods for a struct's fields. +/// +/// ```ignore +/// #[derive(Default)] +/// pub struct Something { +/// field1: T, +/// field2: u8, +/// } +/// +/// impl Something { +/// set_with! { +/// field1: T, +/// field2: u8, +/// } +/// } +/// +/// let mut something = Something::::default().with_field1(core::f32::consts::PI); +/// something.set_field2(42); +/// ``` +macro_rules! set_with { + ($($field:ident: $F:ty),* $(,)?) => { + paste::paste! { + $( + /// update the current value of the field and return a mutable reference to self + pub fn [](&mut self, value: $T) { + self.$field = value + } + /// consume the current instance to create another with the given value + pub fn [](self, $field: $T) -> Self { + Self { + $field, + ..self + } + } + )* + } + + }; +} diff --git a/core/src/macros/units.rs b/core/src/macros/units.rs index 2a8fc7ab..1efd7749 100644 --- a/core/src/macros/units.rs +++ b/core/src/macros/units.rs @@ -3,29 +3,33 @@ Created At: 2025.12.08:19:44:33 Contrib: @FL03 */ - +#[allow(unused_macros)] macro_rules! type_tags { - (#[$tgt:ident] $vis:vis $s:ident {$($name:ident),* $(,)?}) => { + ( #[$tgt:ident] $vis:vis $s:ident {$($name:ident $({$($rest:tt)*})?),* $(,)?}) => { $( - type_tags!(@impl #[$tgt] $vis $s $name); + type_tags!(@impl #[$tgt] $vis $s $name $({$($rest)*})?); )* }; - (@impl #[$tgt:ident] $vis:vis enum $name:ident) => { + (@impl #[$tgt:ident] $vis:vis enum $name:ident $({$($rest:tt)*})?) => { #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] $vis enum $name {} impl $tgt for $name { seal!(); + + $($($rest)*)? } }; - (@impl #[$tgt:ident] $vis:vis struct $name:ident) => { - #[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)] + (@impl #[$tgt:ident] $vis:vis struct $name:ident $({$($rest:tt)*})?) => { + #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] $vis struct $name; impl $tgt for $name { seal!(); + + $($($rest)*)? } }; } diff --git a/core/src/models/impls/impl_model_params.rs b/core/src/models/impls/impl_model_params.rs index 0822c800..96a1eeb2 100644 --- a/core/src/models/impls/impl_model_params.rs +++ b/core/src/models/impls/impl_model_params.rs @@ -6,7 +6,142 @@ use crate::models::ModelParamsBase; use crate::{DeepModelRepr, RawHidden}; use concision_params::ParamsBase; -use ndarray::{Data, Dimension, RawDataClone}; +use ndarray::{ArrayBase, Data, Dimension, RawData, RawDataClone}; + +/// The base implementation for the [`ModelParamsBase`] type, which is generic over the +/// storage type `S`, the dimension `D`, and the hidden layer type `H`. This implementation +/// focuses on providing basic initialization routines and accessors for the various layers +/// within the model. +impl ModelParamsBase +where + D: Dimension, + S: RawData, + H: RawHidden, +{ + /// create a new instance of the [`ModelParamsBase`] instance + pub const fn new(input: ParamsBase, hidden: H, output: ParamsBase) -> Self { + Self { + input, + hidden, + output, + } + } + /// returns an immutable reference to the input layer of the model + pub const fn input(&self) -> &ParamsBase { + &self.input + } + /// returns a mutable reference to the input layer of the model + pub const fn input_mut(&mut self) -> &mut ParamsBase { + &mut self.input + } + /// returns an immutable reference to the hidden layers of the model + pub const fn hidden(&self) -> &H { + &self.hidden + } + /// returns a mutable reference to the hidden layers of the model + pub const fn hidden_mut(&mut self) -> &mut H { + &mut self.hidden + } + /// returns an immutable reference to the output layer of the model + pub const fn output(&self) -> &ParamsBase { + &self.output + } + /// returns a mutable reference to the output layer of the model + pub const fn output_mut(&mut self) -> &mut ParamsBase { + &mut self.output + } + /// set the input layer of the model + #[inline] + pub fn set_input(&mut self, input: ParamsBase) { + *self.input_mut() = input + } + /// set the hidden layers of the model + #[inline] + pub fn set_hidden(&mut self, hidden: H) { + *self.hidden_mut() = hidden + } + /// set the output layer of the model + #[inline] + pub fn set_output(&mut self, output: ParamsBase) { + *self.output_mut() = output + } + /// consumes the current instance and returns another with the specified input layer + #[inline] + pub fn with_input(self, input: ParamsBase) -> Self { + Self { input, ..self } + } + /// consumes the current instance and returns another with the specified hidden + /// layer(s) + #[inline] + pub fn with_hidden(self, hidden: H) -> Self { + Self { hidden, ..self } + } + /// consumes the current instance and returns another with the specified output layer + #[inline] + pub fn with_output(self, output: ParamsBase) -> Self { + Self { output, ..self } + } + /// returns an immutable reference to the hidden layers of the model as a slice + #[inline] + pub fn hidden_as_slice(&self) -> &[ParamsBase] + where + H: DeepModelRepr, + { + self.hidden().as_slice() + } + /// returns an immutable reference to the input bias + pub const fn input_bias(&self) -> &ArrayBase { + self.input().bias() + } + /// returns a mutable reference to the input bias + pub const fn input_bias_mut(&mut self) -> &mut ArrayBase { + self.input_mut().bias_mut() + } + /// returns an immutable reference to the input weights + pub const fn input_weights(&self) -> &ArrayBase { + self.input().weights() + } + /// returns an mutable reference to the input weights + pub const fn input_weights_mut(&mut self) -> &mut ArrayBase { + self.input_mut().weights_mut() + } + /// returns an immutable reference to the output bias + pub const fn output_bias(&self) -> &ArrayBase { + self.output().bias() + } + /// returns a mutable reference to the output bias + pub const fn output_bias_mut(&mut self) -> &mut ArrayBase { + self.output_mut().bias_mut() + } + /// returns an immutable reference to the output weights + pub const fn output_weights(&self) -> &ArrayBase { + self.output().weights() + } + /// returns an mutable reference to the output weights + pub const fn output_weights_mut(&mut self) -> &mut ArrayBase { + self.output_mut().weights_mut() + } + /// returns the total number of layers in the model, including input, hidden, and output + pub fn layers(&self) -> usize { + 2 + self.count_hidden() + } + /// returns the number of hidden layers in the model + pub fn count_hidden(&self) -> usize { + self.hidden().count() + } + /// returns true if the stack is shallow; a neural network is considered to be _shallow_ if + /// it has at most one hidden layer (`n <= 1`). + #[inline] + pub fn is_shallow(&self) -> bool { + self.count_hidden() <= 1 + } + /// returns true if the model stack of parameters is considered to be _deep_, meaning that + /// there the number of hidden layers is greater than one. + #[inline] + pub fn is_deep(&self) -> bool { + self.count_hidden() > 1 + } +} impl Clone for ModelParamsBase where @@ -62,18 +197,16 @@ impl core::ops::Index for ModelParamsBase where D: Dimension, S: Data, - H: DeepModelRepr, + H: RawHidden + core::ops::Index>, A: Clone, { type Output = ParamsBase; fn index(&self, index: usize) -> &Self::Output { - if index == 0 { - self.input() - } else if index == self.count_hidden() + 1 { - self.output() - } else { - &self.hidden().as_slice()[index - 1] + match index % self.layers() { + 0 => self.input(), + i if i == self.count_hidden() + 1 => self.output(), + _ => &self.hidden()[index - 1], } } } @@ -82,16 +215,14 @@ impl core::ops::IndexMut for ModelParamsBase where D: Dimension, S: Data, - H: DeepModelRepr, + H: RawHidden + core::ops::IndexMut>, A: Clone, { fn index_mut(&mut self, index: usize) -> &mut Self::Output { - if index == 0 { - self.input_mut() - } else if index == self.count_hidden() + 1 { - self.output_mut() - } else { - &mut self.hidden_mut().as_mut_slice()[index - 1] + match index % self.layers() { + 0 => self.input_mut(), + i if i == self.count_hidden() + 1 => self.output_mut(), + _ => &mut self.hidden_mut()[index - 1], } } } diff --git a/core/src/models/impls/impl_model_params_rand.rs b/core/src/models/impls/impl_model_params_rand.rs index 519ac380..70eda223 100644 --- a/core/src/models/impls/impl_model_params_rand.rs +++ b/core/src/models/impls/impl_model_params_rand.rs @@ -6,7 +6,7 @@ use crate::models::{DeepParamsBase, ShallowParamsBase}; use crate::ModelFeatures; use concision_init::distr as init; -use concision_init::{NdInit, rand_distr}; +use concision_init::{NdRandom, rand_distr}; use concision_params::ParamsBase; use ndarray::{DataOwned, Ix2}; use num_traits::{Float, FromPrimitive}; @@ -18,28 +18,9 @@ impl ShallowParamsBase where S: DataOwned, { - /// consumes the controller to initialize the various parameters with random values - pub fn init(self) -> Self - where - A: Float + num_traits::FromPrimitive + rand_distr::uniform::SampleUniform, - rand_distr::StandardNormal: rand_distr::Distribution, - { - // initialize the hidden layer(s) - let hidden = ParamsBase::glorot_normal(self.hidden().dim()); - // Controller input weights (alphabet + state -> hidden) - let input = ParamsBase::glorot_normal(self.input().dim()); - // initialize the output layers - let output = ParamsBase::glorot_normal(self.output().dim()); - // return a new instance with the initialized layers - Self { - hidden, - input, - output, - } - } /// returns a new instance of the model initialized with the given features and random /// distribution - pub fn init_rand(features: ModelFeatures, distr: G) -> Self + pub fn rand_with(features: ModelFeatures, distr: G) -> Self where G: Fn((usize, usize)) -> Ds, Ds: Clone + Distribution, @@ -57,7 +38,7 @@ where A: Float + FromPrimitive, StandardNormal: Distribution, { - Self::init_rand(features, |(rows, cols)| init::XavierNormal::new(rows, cols)) + Self::rand_with(features, |(rows, cols)| init::XavierNormal::new(rows, cols)) } /// initialize the model parameters using a glorot uniform distribution pub fn glorot_uniform(features: ModelFeatures) -> Self @@ -66,10 +47,29 @@ where ::Sampler: Clone, Uniform: Distribution, { - Self::init_rand(features, |(rows, cols)| { + Self::rand_with(features, |(rows, cols)| { init::XavierUniform::new(rows, cols).expect("failed to create distribution") }) } + /// consumes the controller to initialize the various parameters with random values + pub fn init(self) -> Self + where + A: Float + num_traits::FromPrimitive + rand_distr::uniform::SampleUniform, + rand_distr::StandardNormal: rand_distr::Distribution, + { + // initialize the hidden layer(s) + let hidden = ParamsBase::glorot_normal(self.hidden().dim()); + // Controller input weights (alphabet + state -> hidden) + let input = ParamsBase::glorot_normal(self.input().dim()); + // initialize the output layers + let output = ParamsBase::glorot_normal(self.output().dim()); + // return a new instance with the initialized layers + Self { + hidden, + input, + output, + } + } } impl DeepParamsBase diff --git a/core/src/models/impls/impl_params_deep.rs b/core/src/models/impls/impl_params_deep.rs index 8740e642..ffde5557 100644 --- a/core/src/models/impls/impl_params_deep.rs +++ b/core/src/models/impls/impl_params_deep.rs @@ -109,7 +109,7 @@ where ParamsBase: Forward + Forward, { let mut output = self.input().forward(input); - self.hidden().into_iter().for_each(|layer| { + self.hidden().iter().for_each(|layer| { output = layer.forward(&output); }); self.output().forward(&output) diff --git a/core/src/models/layout.rs b/core/src/models/layout.rs new file mode 100644 index 00000000..f3342c68 --- /dev/null +++ b/core/src/models/layout.rs @@ -0,0 +1,109 @@ +/* + appellation: layout + authors: @FL03 +*/ +use super::{Deep, NetworkDepth, RawModelLayout}; + +mod impl_model_features; +mod impl_model_format; +mod impl_model_layout; + +/// A trait that consumes the caller to create a new instance of [`ModelFeatures`] object. +pub trait IntoModelFeatures { + fn into_model_features(self) -> ModelFeatures; +} + +/// The [`ModelFormat`] type enumerates the various formats a neural network may take, either +/// shallow or deep, providing a unified interface for accessing the number of hidden features +/// and layers in the model. This is primarily used to generalize the allowed formats of a +/// neural network without introducing any additional complexity with typing or other +/// constructs. +#[derive( + Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, strum::EnumCount, strum::EnumIs, +)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum ModelFormat { + Layer, + Shallow { hidden: usize }, + Deep { hidden: usize, layers: usize }, +} + +/// The [`ModelFeatures`] provides a common way of defining the layout of a model. This is +/// used to define the number of input features, the number of hidden layers, the number of +/// hidden features, and the number of output features. +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct ModelFeatures { + /// the number of input features + pub(crate) input: usize, + /// the features of the "inner" layers + pub(crate) inner: ModelFormat, + /// the number of output features + pub(crate) output: usize, +} + +/// In contrast to the [`ModelFeatures`] type, the [`ModelLayout`] implementation aims to +/// provide a generic foundation for using type-based features / layouts within neural network. +/// Our goal with this struct is to eventually push the implementation to the point of being +/// able to sufficiently describe everything about a model's layout (similar to what the +/// [`ndarray`] developers have attained with the [`LayoutRef`](ndarray::LayoutRef)). +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct ModelLayout +where + D: NetworkDepth, + F: RawModelLayout, +{ + pub(crate) features: F, + pub(crate) _marker: core::marker::PhantomData, +} + +/* + ************* Implementations ************* +*/ + +impl IntoModelFeatures for (usize, usize, usize) { + fn into_model_features(self) -> ModelFeatures { + ModelFeatures { + input: self.0, + inner: ModelFormat::Shallow { hidden: self.1 }, + output: self.2, + } + } +} + +impl IntoModelFeatures for (usize, usize, usize, usize) { + fn into_model_features(self) -> ModelFeatures { + ModelFeatures { + input: self.0, + inner: ModelFormat::Deep { + hidden: self.1, + layers: self.3, + }, + output: self.2, + } + } +} + +impl IntoModelFeatures for [usize; 3] { + fn into_model_features(self) -> ModelFeatures { + ModelFeatures { + input: self[0], + inner: ModelFormat::Shallow { hidden: self[1] }, + output: self[2], + } + } +} + +impl IntoModelFeatures for [usize; 4] { + fn into_model_features(self) -> ModelFeatures { + ModelFeatures { + input: self[0], + inner: ModelFormat::Deep { + hidden: self[1], + layers: self[3], + }, + output: self[2], + } + } +} diff --git a/core/src/layout/impl_model_features.rs b/core/src/models/layout/impl_model_features.rs similarity index 92% rename from core/src/layout/impl_model_features.rs rename to core/src/models/layout/impl_model_features.rs index b089213f..fe3fa099 100644 --- a/core/src/layout/impl_model_features.rs +++ b/core/src/models/layout/impl_model_features.rs @@ -2,7 +2,8 @@ Appellation: layout Contrib: @FL03 */ -use super::{ModelFeatures, ModelFormat, RawModelLayout, RawModelLayoutMut}; +use super::ModelFeatures; +use crate::models::{ModelFormat, RawModelLayout, RawModelLayoutMut}; /// verify if the input and hidden dimensions are compatible by checking: /// @@ -22,6 +23,17 @@ where } impl ModelFeatures { + pub fn from_shape_and_size(shape: &[usize], size: usize) -> Self { + let input = shape[0]; + let output = *shape.last().unwrap(); + let hidden = if shape.len() > 2 { + shape[1] + } else { + (size - (input * output)) / (input + output) + }; + let layers = if shape.len() > 2 { shape.len() - 2 } else { 1 }; + Self::new(input, hidden, output, layers) + } /// creates a new instance of [`ModelFeatures`] for a neural network with `n` layers. If /// the number of layers is `<=1` then the [`ModelFormat`] is automatically /// configured as a _shallow_ neural network. @@ -57,7 +69,7 @@ impl ModelFeatures { { Self { input: layout.input(), - inner: ModelFormat::new(layout.hidden(), layout.layers()), + inner: ModelFormat::new(layout.hidden(), layout.depth()), output: layout.output(), } } @@ -189,7 +201,7 @@ impl RawModelLayout for ModelFeatures { self.hidden() } - fn layers(&self) -> usize { + fn depth(&self) -> usize { self.layers() } diff --git a/core/src/layout/impl_model_format.rs b/core/src/models/layout/impl_model_format.rs similarity index 76% rename from core/src/layout/impl_model_format.rs rename to core/src/models/layout/impl_model_format.rs index bf8ed4ca..72047c41 100644 --- a/core/src/layout/impl_model_format.rs +++ b/core/src/models/layout/impl_model_format.rs @@ -7,10 +7,15 @@ use super::ModelFormat; impl ModelFormat { pub const fn new(hidden: usize, layers: usize) -> Self { match layers { - 0 | 1 => ModelFormat::Shallow { hidden }, + 0 => ModelFormat::Layer, + 1 => ModelFormat::Shallow { hidden }, _ => ModelFormat::Deep { hidden, layers }, } } + + pub const fn layer() -> Self { + ModelFormat::Layer + } /// initialize a new [`Deep`](ModelFormat::Deep) variant for a deep neural network with the /// given number of hidden features and layers pub const fn deep(hidden: usize, layers: usize) -> Self { @@ -26,6 +31,7 @@ impl ModelFormat { match self { ModelFormat::Shallow { hidden } => *hidden, ModelFormat::Deep { hidden, .. } => *hidden, + ModelFormat::Layer => 0, } } /// returns a mutable reference to the hidden features for the model @@ -33,6 +39,7 @@ impl ModelFormat { match self { ModelFormat::Shallow { hidden } => hidden, ModelFormat::Deep { hidden, .. } => hidden, + ModelFormat::Layer => panic!("Cannot mutate hidden features of a layout model"), } } /// returns a copy of the number of layers for the model; if the variant is @@ -42,6 +49,7 @@ impl ModelFormat { match self { ModelFormat::Shallow { .. } => 1, ModelFormat::Deep { layers, .. } => *layers, + ModelFormat::Layer => 0, } } /// returns a mutable reference to the number of layers for the model; this will panic on @@ -50,6 +58,7 @@ impl ModelFormat { match self { ModelFormat::Shallow { .. } => panic!("Cannot mutate layers of a shallow model"), ModelFormat::Deep { layers, .. } => layers, + ModelFormat::Layer => panic!("Cannot mutate layers of a layout model"), } } /// update the number of hidden features for the model @@ -61,6 +70,9 @@ impl ModelFormat { ModelFormat::Deep { hidden, .. } => { *hidden = value; } + ModelFormat::Layer => { + panic!("Cannot mutate hidden features of a layout model"); + } } self } @@ -83,6 +95,9 @@ impl ModelFormat { ModelFormat::Deep { layers, .. } => { *layers = value; } + ModelFormat::Layer => { + panic!("Cannot mutate layers of a layout model"); + } } self } @@ -92,6 +107,7 @@ impl ModelFormat { match self { ModelFormat::Shallow { .. } => ModelFormat::Shallow { hidden }, ModelFormat::Deep { layers, .. } => ModelFormat::Deep { hidden, layers }, + ModelFormat::Layer => ModelFormat::Shallow { hidden }, } } /// consumes the current instance and returns a new instance with the given number of @@ -101,15 +117,18 @@ impl ModelFormat { /// variant if it is currently a [`Shallow`](ModelFormat::Shallow) variant and the number /// of layers becomes greater than 1 pub fn with_layers(self, layers: usize) -> Self { - match self { - ModelFormat::Shallow { hidden } => { - if layers > 1 { - ModelFormat::Deep { hidden, layers } - } else { - ModelFormat::Shallow { hidden } - } - } - ModelFormat::Deep { hidden, .. } => ModelFormat::Deep { hidden, layers }, + match layers { + 0 => ModelFormat::Layer, + 1 => match self { + ModelFormat::Shallow { hidden } => ModelFormat::Shallow { hidden }, + ModelFormat::Deep { hidden, .. } => ModelFormat::Shallow { hidden }, + ModelFormat::Layer => ModelFormat::Layer, + }, + _ => match self { + ModelFormat::Shallow { hidden } => ModelFormat::Deep { hidden, layers }, + ModelFormat::Deep { hidden, .. } => ModelFormat::Deep { hidden, layers }, + ModelFormat::Layer => ModelFormat::Deep { hidden: 16, layers }, + }, } } } diff --git a/core/src/layout/impl_model_layout.rs b/core/src/models/layout/impl_model_layout.rs similarity index 95% rename from core/src/layout/impl_model_layout.rs rename to core/src/models/layout/impl_model_layout.rs index bee305f1..c0b1024e 100644 --- a/core/src/layout/impl_model_layout.rs +++ b/core/src/models/layout/impl_model_layout.rs @@ -5,7 +5,7 @@ */ use super::ModelLayout; -use crate::layout::{Deep, NetworkDepth, RawModelLayout, Shallow}; +use crate::models::{Deep, NetworkDepth, RawModelLayout, Shallow}; impl ModelLayout where @@ -41,7 +41,7 @@ where } /// returns a reference to the depth, or number of hidden layers, of the model pub fn layers(&self) -> usize { - self.features().layers() + self.features().depth() } } diff --git a/core/src/models/mod.rs b/core/src/models/mod.rs index b3e6bde0..4c06e570 100644 --- a/core/src/models/mod.rs +++ b/core/src/models/mod.rs @@ -2,12 +2,14 @@ appellation: params authors: @FL03 */ -//! this module provides the [`ModelParamsBase`] type and its associated aliases. The -//! implementation focuses on providing a generic container for the parameters of a neural -//! network. +//! this module works to provide a common interface for storing sets of parameters within a +//! given model. The [`ModelParamsBase`] implementation generically captures the behavior of +//! parameter storage, relying on the [`ParamsBase`](concision_params::ParamsBase) instance to represent +//! individual layers within the network. #[doc(inline)] -pub use self::{model_params::*, traits::*, types::*}; +pub use self::{layout::*, model_params::*, traits::*, types::*}; +pub mod layout; pub mod model_params; mod impls { @@ -22,21 +24,21 @@ mod impls { } mod types { - #[doc(inline)] pub use self::aliases::*; mod aliases; } mod traits { - #[doc(inline)] - pub use self::{hidden::*, model::*}; + pub use self::{format::*, hidden::*}; + mod format; mod hidden; - mod model; } +#[doc(hidden)] pub(crate) mod prelude { + pub use super::layout::*; pub use super::model_params::*; pub use super::traits::*; pub use super::types::*; diff --git a/core/src/models/model_params.rs b/core/src/models/model_params.rs index c3d9c197..d5f81868 100644 --- a/core/src/models/model_params.rs +++ b/core/src/models/model_params.rs @@ -4,9 +4,15 @@ */ use concision_params::ParamsBase; -use ndarray::{ArrayBase, Dimension, RawData}; +use ndarray::{Dimension, RawData}; -use crate::{DeepModelRepr, RawHidden}; +use crate::RawHidden; + +pub struct DeepNeuralNetworkStore { + pub input: X, + pub hidden: Y, + pub output: Z, +} /// The [`ModelParamsBase`] object is a generic container for storing the parameters of a /// neural network, regardless of the layout (e.g. shallow or deep). This is made possible @@ -23,147 +29,9 @@ where H: RawHidden, { /// the input layer of the model - pub(crate) input: ParamsBase, + pub(crate) input: ParamsBase, /// a sequential stack of params for the model's hidden layers pub(crate) hidden: H, /// the output layer of the model - pub(crate) output: ParamsBase, -} -/// The base implementation for the [`ModelParamsBase`] type, which is generic over the -/// storage type `S`, the dimension `D`, and the hidden layer type `H`. This implementation -/// focuses on providing basic initialization routines and accessors for the various layers -/// within the model. -impl ModelParamsBase -where - D: Dimension, - S: RawData, - H: RawHidden, -{ - /// create a new instance of the [`ModelParamsBase`] instance - pub const fn new(input: ParamsBase, hidden: H, output: ParamsBase) -> Self { - Self { - input, - hidden, - output, - } - } - /// returns an immutable reference to the input layer of the model - pub const fn input(&self) -> &ParamsBase { - &self.input - } - /// returns a mutable reference to the input layer of the model - pub const fn input_mut(&mut self) -> &mut ParamsBase { - &mut self.input - } - /// returns an immutable reference to the hidden layers of the model - pub const fn hidden(&self) -> &H { - &self.hidden - } - /// returns a mutable reference to the hidden layers of the model - pub const fn hidden_mut(&mut self) -> &mut H { - &mut self.hidden - } - /// returns an immutable reference to the output layer of the model - pub const fn output(&self) -> &ParamsBase { - &self.output - } - /// returns a mutable reference to the output layer of the model - pub const fn output_mut(&mut self) -> &mut ParamsBase { - &mut self.output - } - /// set the input layer of the model - #[inline] - pub fn set_input(&mut self, input: ParamsBase) -> &mut Self { - *self.input_mut() = input; - self - } - /// set the hidden layers of the model - #[inline] - pub fn set_hidden(&mut self, hidden: H) -> &mut Self { - *self.hidden_mut() = hidden; - self - } - /// set the output layer of the model - #[inline] - pub fn set_output(&mut self, output: ParamsBase) -> &mut Self { - *self.output_mut() = output; - self - } - /// consumes the current instance and returns another with the specified input layer - #[inline] - pub fn with_input(self, input: ParamsBase) -> Self { - Self { input, ..self } - } - /// consumes the current instance and returns another with the specified hidden - /// layer(s) - #[inline] - pub fn with_hidden(self, hidden: H) -> Self { - Self { hidden, ..self } - } - /// consumes the current instance and returns another with the specified output layer - #[inline] - pub fn with_output(self, output: ParamsBase) -> Self { - Self { output, ..self } - } - /// returns an immutable reference to the hidden layers of the model as a slice - #[inline] - pub fn hidden_as_slice(&self) -> &[ParamsBase] - where - H: DeepModelRepr, - { - self.hidden().as_slice() - } - /// returns an immutable reference to the input bias - pub const fn input_bias(&self) -> &ArrayBase { - self.input().bias() - } - /// returns a mutable reference to the input bias - pub const fn input_bias_mut(&mut self) -> &mut ArrayBase { - self.input_mut().bias_mut() - } - /// returns an immutable reference to the input weights - pub const fn input_weights(&self) -> &ArrayBase { - self.input().weights() - } - /// returns an mutable reference to the input weights - pub const fn input_weights_mut(&mut self) -> &mut ArrayBase { - self.input_mut().weights_mut() - } - /// returns an immutable reference to the output bias - pub const fn output_bias(&self) -> &ArrayBase { - self.output().bias() - } - /// returns a mutable reference to the output bias - pub const fn output_bias_mut(&mut self) -> &mut ArrayBase { - self.output_mut().bias_mut() - } - /// returns an immutable reference to the output weights - pub const fn output_weights(&self) -> &ArrayBase { - self.output().weights() - } - /// returns an mutable reference to the output weights - pub const fn output_weights_mut(&mut self) -> &mut ArrayBase { - self.output_mut().weights_mut() - } - /// returns the number of hidden layers in the model - pub fn count_hidden(&self) -> usize { - self.hidden().count() - } - /// returns true if the stack is shallow; a neural network is considered to be _shallow_ if - /// it has at most one hidden layer (`n <= 1`). - #[inline] - pub fn is_shallow(&self) -> bool { - self.count_hidden() <= 1 - } - /// returns true if the model stack of parameters is considered to be _deep_, meaning that - /// there the number of hidden layers is greater than one. - #[inline] - pub fn is_deep(&self) -> bool { - self.count_hidden() > 1 - } - /// returns the total number of layers within the model, including the input and output layers - #[inline] - pub fn len(&self) -> usize { - self.count_hidden() + 2 // +2 for input and output layers - } + pub(crate) output: ParamsBase, } diff --git a/core/src/models/traits.rs b/core/src/models/traits.rs deleted file mode 100644 index b9d6369a..00000000 --- a/core/src/models/traits.rs +++ /dev/null @@ -1,146 +0,0 @@ -/* - appellation: models - authors: @FL03 -*/ -use crate::config::NetworkConfig; -use crate::{DeepModelParams, ModelLayout}; -use crate::{Predict, Train}; -use concision_core::params::Params; -use concision_data::DatasetBase; - -/// The [`Model`] trait defines the core interface for all models; implementors will need to -/// provide the type of configuration used by the model, the type of layout used by the model, -/// and the type of parameters used by the model. The crate provides standard, or default, -/// definitions of both the configuration and layout types, however, for -pub trait Model { - /// The type of configuration used for the model - type Config: NetworkConfig; - /// The type of [`ModelLayout`] used by the model for this implementation. - type Layout: ModelLayout; - /// returns an immutable reference to the models configuration; this is typically used to - /// access the models hyperparameters (i.e. learning rate, momentum, etc.) and other - /// related control parameters. - fn config(&self) -> &Self::Config; - /// returns a mutable reference to the models configuration; useful for setting hyperparams - fn config_mut(&mut self) -> &mut Self::Config; - /// returns a copy of the model's current layout (features); a type providing the model - /// with a particular number of features for the various layers of a deep neural network. - /// - /// the layout is used in everything from creation and initialization routines to - /// validating the dimensionality of the model's inputs, outputs, training data, etc. - fn layout(&self) -> Self::Layout; - /// returns an immutable reference to the model parameters - fn params(&self) -> &DeepModelParams; - /// returns a mutable reference to the model's parameters - fn params_mut(&mut self) -> &mut DeepModelParams; - /// propagates the input through the model; each layer is applied in sequence meaning that - /// the output of each previous layer is the input to the next layer. This pattern - /// repeats until the output layer returns the final result. - /// - /// By default, the trait simply passes each output from one layer to the next, however, - /// custom models will likely override this method to inject activation methods and other - /// related logic - fn predict(&self, inputs: &U) -> Option - where - Self: Predict, - { - Predict::predict(self, inputs) - } - /// a convience method that trains the model using the provided dataset; this method - /// requires that the model implements the [`Train`] trait and that the dataset - fn train(&mut self, dataset: &DatasetBase) -> crate::Result - where - Self: Train, - { - Train::train(self, dataset.records(), dataset.targets()) - } -} - -pub trait ModelExt: Model { - /// [`replace`](core::mem::replace) the current configuration and returns the old one; - fn replace_config(&mut self, config: Self::Config) -> Self::Config { - core::mem::replace(self.config_mut(), config) - } - /// [`replace`](core::mem::replace) the current model parameters and returns the old one - fn replace_params(&mut self, params: DeepModelParams) -> DeepModelParams { - core::mem::replace(self.params_mut(), params) - } - /// overrides the current configuration and returns a mutable reference to the model - fn set_config(&mut self, config: Self::Config) -> &mut Self { - *self.config_mut() = config; - self - } - /// overrides the current model parameters and returns a mutable reference to the model - fn set_params(&mut self, params: DeepModelParams) -> &mut Self { - *self.params_mut() = params; - self - } - /// returns an immutable reference to the input layer; - #[inline] - fn input_layer(&self) -> &Params { - self.params().input() - } - /// returns a mutable reference to the input layer; - #[inline] - fn input_layer_mut(&mut self) -> &mut Params { - self.params_mut().input_mut() - } - /// returns an immutable reference to the hidden layer(s); - #[inline] - fn hidden_layers(&self) -> &Vec> { - self.params().hidden() - } - /// returns a mutable reference to the hidden layer(s); - #[inline] - fn hidden_layers_mut(&mut self) -> &mut Vec> { - self.params_mut().hidden_mut() - } - /// returns an immutable reference to the output layer; - #[inline] - fn output_layer(&self) -> &Params { - self.params().output() - } - /// returns a mutable reference to the output layer; - #[inline] - fn output_layer_mut(&mut self) -> &mut Params { - self.params_mut().output_mut() - } - #[inline] - fn set_input_layer(&mut self, layer: Params) -> &mut Self { - self.params_mut().set_input(layer); - self - } - #[inline] - fn set_hidden_layers(&mut self, layers: Vec>) -> &mut Self { - self.params_mut().set_hidden(layers); - self - } - #[inline] - fn set_output_layer(&mut self, layer: Params) -> &mut Self { - self.params_mut().set_output(layer); - self - } - /// returns a 2-tuple representing the dimensions of the input layer; (input, hidden) - fn input_dim(&self) -> (usize, usize) { - self.layout().dim_input() - } - /// returns a 2-tuple representing the dimensions of the hidden layers; (hidden, hidden) - fn hidden_dim(&self) -> (usize, usize) { - self.layout().dim_hidden() - } - /// returns the total number of hidden layers in the model; - fn hidden_layers_count(&self) -> usize { - self.layout().layers() - } - /// returns a 2-tuple representing the dimensions of the output layer; (hidden, output) - fn output_dim(&self) -> (usize, usize) { - self.layout().dim_output() - } -} - -impl ModelExt for M -where - M: Model, - M::Layout: ModelLayout, -{ -} diff --git a/core/src/layout.rs b/core/src/models/traits/format.rs similarity index 62% rename from core/src/layout.rs rename to core/src/models/traits/format.rs index 989317e2..e3cc5382 100644 --- a/core/src/layout.rs +++ b/core/src/models/traits/format.rs @@ -1,16 +1,8 @@ /* - appellation: layout - authors: @FL03 + Appellation: format + Created At: 2026.01.06:14:38:32 + Contrib: @FL03 */ -mod impl_model_features; -mod impl_model_format; -mod impl_model_layout; - -/// A trait that consumes the caller to create a new instance of [`ModelFeatures`] object. -pub trait IntoModelFeatures { - fn into_model_features(self) -> ModelFeatures; -} - /// The [`RawModelLayout`] trait defines a minimal interface for objects capable of representing /// the _layout_; i.e. the number of input, hidden, and output features of a neural network /// model containing some number of hidden layers. @@ -27,7 +19,7 @@ pub trait RawModelLayout { /// the number of output features for the model fn output(&self) -> usize; /// returns the number of hidden layers within the network - fn layers(&self) -> usize; + fn depth(&self) -> usize; /// the dimension of the input layer; (input, hidden) fn dim_input(&self) -> (usize, usize) { @@ -51,7 +43,7 @@ pub trait RawModelLayout { } /// the total number of hidden parameters in the model fn size_hidden(&self) -> usize { - self.hidden() * self.hidden() * self.layers() + self.hidden() * self.hidden() * self.depth() } /// the total number of output parameters in the model fn size_output(&self) -> usize { @@ -108,57 +100,41 @@ pub trait LayoutExt: RawModelLayout + RawModelLayoutMut + Clone + core::fmt::Deb /// The [`NetworkDepth`] trait is used to define the depth/kind of a neural network model. pub trait NetworkDepth { private!(); -} -type_tags! { - #[NetworkDepth] - pub enum { - Deep, - Shallow, + fn is_deep(&self) -> bool { + false } } -/// The [`ModelFormat`] type enumerates the various formats a neural network may take, either -/// shallow or deep, providing a unified interface for accessing the number of hidden features -/// and layers in the model. This is done largely for simplicity, as it eliminates the need to -/// define a particular _type_ of network as its composition has little impact on the actual -/// requirements / algorithms used to train or evaluate the model (that is, outside of the -/// obvious need to account for additional hidden layers in deep configurations). In other -/// words, both shallow and deep networks are requried to implement the same traits and -/// fulfill the same requirements, so it makes sense to treat them as a single type with -/// different configurations. The differences between the networks are largely left to the -/// developer and their choice of activation functions, optimizers, and other considerations. -#[derive( - Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, strum::EnumCount, strum::EnumIs, -)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub enum ModelFormat { - Shallow { hidden: usize }, - Deep { hidden: usize, layers: usize }, +macro_rules! impl_network_depth { + ( #[$tgt:ident] $vis:vis $s:ident {$($name:ident $({$($rest:tt)*})?),* $(,)?}) => { + $( + impl_network_depth!(@impl #[$tgt] $vis $s $name $({$($rest)*})?); + )* + }; + (@impl #[$tgt:ident] $vis:vis enum $name:ident $({$($rest:tt)*})?) => { + #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] + #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] + $vis enum $name {} + + impl $tgt for $name { + seal!(); + + $($($rest)*)? + } + }; } -/// The [`ModelFeatures`] provides a common way of defining the layout of a model. This is -/// used to define the number of input features, the number of hidden layers, the number of -/// hidden features, and the number of output features. -#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct ModelFeatures { - /// the number of input features - pub(crate) input: usize, - /// the features of the "inner" layers - pub(crate) inner: ModelFormat, - /// the number of output features - pub(crate) output: usize, -} -#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct ModelLayout -where - D: NetworkDepth, - F: RawModelLayout, -{ - pub(crate) features: F, - pub(crate) _marker: core::marker::PhantomData, +impl_network_depth! { + #[NetworkDepth] + pub enum { + Deep { + fn is_deep(&self) -> bool { + true + } + }, + Shallow, + } } /* @@ -175,8 +151,8 @@ where fn hidden(&self) -> usize { ::hidden(self) } - fn layers(&self) -> usize { - ::layers(self) + fn depth(&self) -> usize { + ::depth(self) } fn output(&self) -> usize { ::output(self) @@ -193,8 +169,8 @@ where fn hidden(&self) -> usize { ::hidden(self) } - fn layers(&self) -> usize { - ::layers(self) + fn depth(&self) -> usize { + ::depth(self) } fn output(&self) -> usize { ::output(self) @@ -210,7 +186,7 @@ impl RawModelLayout for (usize, usize, usize) { fn hidden(&self) -> usize { self.1 } - fn layers(&self) -> usize { + fn depth(&self) -> usize { 1 } fn output(&self) -> usize { @@ -229,7 +205,7 @@ impl RawModelLayout for (usize, usize, usize, usize) { self.2 } - fn layers(&self) -> usize { + fn depth(&self) -> usize { self.3 } } @@ -265,7 +241,7 @@ impl RawModelLayout for [usize; 3] { self[2] } - fn layers(&self) -> usize { + fn depth(&self) -> usize { 1 } } @@ -283,7 +259,7 @@ impl RawModelLayout for [usize; 4] { self[2] } - fn layers(&self) -> usize { + fn depth(&self) -> usize { self[3] } } @@ -302,49 +278,3 @@ impl RawModelLayoutMut for [usize; 4] { &mut self[3] } } - -impl IntoModelFeatures for (usize, usize, usize) { - fn into_model_features(self) -> ModelFeatures { - ModelFeatures { - input: self.0, - inner: ModelFormat::Shallow { hidden: self.1 }, - output: self.2, - } - } -} - -impl IntoModelFeatures for (usize, usize, usize, usize) { - fn into_model_features(self) -> ModelFeatures { - ModelFeatures { - input: self.0, - inner: ModelFormat::Deep { - hidden: self.1, - layers: self.3, - }, - output: self.2, - } - } -} - -impl IntoModelFeatures for [usize; 3] { - fn into_model_features(self) -> ModelFeatures { - ModelFeatures { - input: self[0], - inner: ModelFormat::Shallow { hidden: self[1] }, - output: self[2], - } - } -} - -impl IntoModelFeatures for [usize; 4] { - fn into_model_features(self) -> ModelFeatures { - ModelFeatures { - input: self[0], - inner: ModelFormat::Deep { - hidden: self[1], - layers: self[3], - }, - output: self[2], - } - } -} diff --git a/core/src/models/traits/hidden.rs b/core/src/models/traits/hidden.rs index 7f8bd502..5c99d708 100644 --- a/core/src/models/traits/hidden.rs +++ b/core/src/models/traits/hidden.rs @@ -11,7 +11,7 @@ where S: RawData, D: Dimension, { - private!(); + private! {} fn count(&self) -> usize; } @@ -22,16 +22,17 @@ where S: RawData, D: Dimension, { - private!(); + private! {} } /// The [`DeepModelRepr`] trait for deep neural networks pub trait DeepModelRepr: RawHidden where S: RawData, D: Dimension, - Self: IntoIterator>, + Self: + IntoIterator> + core::ops::Index>, { - private!(); + private! {} /// returns the hidden layers as a slice fn as_slice(&self) -> &[ParamsBase]; @@ -51,7 +52,8 @@ where X: RawHidden + IntoIterator> + AsRef<[ParamsBase]> - + AsMut<[ParamsBase]>, + + AsMut<[ParamsBase]> + + core::ops::Index>, { seal!(); diff --git a/core/src/nn/layer.rs b/core/src/nn/layer.rs new file mode 100644 index 00000000..e67b8ad3 --- /dev/null +++ b/core/src/nn/layer.rs @@ -0,0 +1,112 @@ +/* + Appellation: layer + Created At: 2026.01.12:09:34:59 + Contrib: @FL03 +*/ +mod impl_layer; +mod impl_layer_ext; +mod impl_layer_repr; + +#[doc(inline)] +pub use self::types::*; + +/// The [`LayerBase`] implementation works to provide a generic interface for layers within a +/// neural network by associating an activation function `F` with a set of parameters `P`. +#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct LayerBase { + /// the activation function of the layer + pub rho: F, + /// the parameters of the layer; often weights and biases + pub params: P, +} + +mod types { + use super::LayerBase; + use crate::activate::{HeavySide, HyperbolicTangent, Linear, ReLU, Sigmoid}; + #[cfg(feature = "alloc")] + use alloc::boxed::Box; + use concision_params::{Params, ParamsBase}; + + /// A type alias for a layer configured to use the [`ParamsBase`] instance + pub type LayerParamsBase = LayerBase>; + /// A type alias for an owned [`Layer`] configured to use the standard [`Params`] instance + pub type LayerParams = LayerBase>; + /// A type alias for a layer using a linear activation function. + pub type LinearLayer = LayerBase; + /// A type alias for a [`Layer`] using a sigmoid activation function. + pub type SigmoidLayer = LayerBase; + /// An alias for a [`Layer`] that uses the hyperbolic tangent function. + pub type TanhLayer = LayerBase; + /// A [`Layer`] type using the ReLU activation function. + pub type ReluLayer = LayerBase; + /// A [`Layer`] type using the heavyside activation function. + pub type HeavySideLayer = LayerBase; + + #[cfg(feature = "alloc")] + /// A dynamic instance of the layer using a boxed activator. + pub type LayerDyn<'a, T> = + LayerBase + 'a>, T>; + #[cfg(feature = "alloc")] + /// A dynamic, functional alias of the [`Layer`] implementation leveraging boxed closures. + pub type FnLayer<'a, T> = LayerBase T + 'a>, T>; +} + +#[cfg(test)] +mod tests { + use super::*; + use concision_params::Params; + use ndarray::Array1; + + #[test] + #[ignore = "need to fix the test"] + fn test_func_layer() { + let params = Params::::from_elem((3, 2), 0.5); + let layer = LayerBase::new(|x: Array1| x.pow2(), params); + // initialize some inputs + let inputs = Array1::::linspace(1.0, 2.0, 3); + // verify the shape of the layer's parameters + assert_eq!(layer.params().shape(), &[3, 2]); + // compare the actual output against the expected output + assert_eq!(layer.forward(&inputs), Array1::from_elem(2, 7.5625).pow2()); + } + + #[test] + fn test_linear_layer() { + let params = Params::from_elem((3, 2), 0.5_f32); + let layer = LayerBase::linear(params); + // verify the shape of the layer's parameters + assert_eq!(layer.params().shape(), &[3, 2]); + // initialize some inputs + let inputs = Array1::::linspace(1.0, 2.0, 3); + // compare the actual output against the expected output + assert_eq!(layer.forward(&inputs), Array1::from_elem(2, 2.75)); + } + + #[test] + fn test_relu_layer() { + let params = Params::from_elem((3, 2), 0.5_f32); + let layer = LayerBase::relu(params); + // initialize some inputs + let inputs = Array1::::linspace(1.0, 2.0, 3); + // verify the shape of the layer's parameters + assert_eq!(layer.params().shape(), &[3, 2]); + // compare the actual output against the expected output + assert_eq!(layer.forward(&inputs), Array1::from_elem(2, 2.75)); + } + + #[test] + #[ignore = "need to fix the test"] + fn test_tanh_layer() { + let params = Params::from_elem((3, 2), 0.5_f32); + let layer = LayerBase::tanh(params); + // initialize some inputs + let inputs = Array1::::linspace(1.0, 2.0, 3); + // verify the shape of the layer's parameters + assert_eq!(layer.params().shape(), &[3, 2]); + // compare the actual output against the expected output + let y = layer.forward(&inputs); + let exp = Array1::from_elem(2, 0.99185973).tanh(); + assert!((y - exp).abs().iter().all(|&i| i < 1e-4)); + } +} diff --git a/core/src/layers/layer.rs b/core/src/nn/layer/impl_layer.rs similarity index 53% rename from core/src/layers/layer.rs rename to core/src/nn/layer/impl_layer.rs index 687348a2..0fbf8ecb 100644 --- a/core/src/layers/layer.rs +++ b/core/src/nn/layer/impl_layer.rs @@ -1,49 +1,34 @@ /* - Appellation: layer - Contrib: @FL03 + appellation: impl_layer + authors: @FL03 */ -mod impl_layer; -mod impl_layer_deprecated; -mod impl_layer_repr; - -use super::Activator; +use crate::activate::Activator; +use crate::nn::layer::LayerBase; +use concision_params::RawParams; use concision_traits::Forward; -/// The [`Layer`] implementation works to provide a generic interface for layers within a -/// neural network. It associates an activation function of type `F` with parameters of -/// type `P`. -pub struct Layer { - /// the activation function of the layer - pub(crate) rho: F, - /// the parameters of the layer is an object consisting of both a weight and a bias tensor. - pub(crate) params: P, -} - -impl Layer { - /// create a new [`Layer`] from the given activation function and parameters. +impl LayerBase +where + P: RawParams, +{ + /// create a new [`LayerBase`] from the given activation function and parameters. pub const fn new(rho: F, params: P) -> Self { Self { rho, params } } - /// create a new [`Layer`] from the given parameters assuming the logical default for + /// create a new [`LayerBase`] from the given parameters assuming the logical default for /// the activation of type `F`. pub fn from_params(params: P) -> Self where F: Default, { - Self { - rho: F::default(), - params, - } + Self::new(::default(), params) } - /// create a new [`Layer`] from the given activation function and shape. + /// create a new [`LayerBase`] from the given activation function and shape. pub fn from_rho(rho: F) -> Self where P: Default, { - Self { - rho, - params:

::default(), - } + Self::new(rho,

::default()) } /// returns an immutable reference to the layer's parameters pub const fn params(&self) -> &P { @@ -61,36 +46,42 @@ impl Layer { pub const fn rho_mut(&mut self) -> &mut F { &mut self.rho } + #[inline] /// consumes the current instance and returns another with the given parameters. - pub fn with_params(self, params: Y) -> Layer + pub fn with_params(self, params: Y) -> LayerBase where F: Activator, { - Layer { + LayerBase { rho: self.rho, params, } } + #[inline] /// consumes the current instance and returns another with the given activation function. /// This is useful during the creation of the model, when the activation function is not known yet. - pub fn with_rho(self, rho: G) -> Layer + pub fn with_rho(self, rho: G) -> LayerBase where G: Activator

, - F: Activator

, { - Layer { + LayerBase { rho, params: self.params, } } + #[inline] + /// apply the configured activation function onto some input, producing some output + pub fn activate(&self, input: X) -> Y + where + F: Activator, + { + self.rho().activate(input) + } /// given some input, complete a single forward pass through the layer pub fn forward(&self, input: &U) -> V where - P: Forward, - F: Activator, - V: Clone, + Self: Forward, { - self.params() - .forward_then(input, |y| self.rho().activate(y)) + >::forward(self, input) } } diff --git a/core/src/nn/layer/impl_layer_ext.rs b/core/src/nn/layer/impl_layer_ext.rs new file mode 100644 index 00000000..17a426a5 --- /dev/null +++ b/core/src/nn/layer/impl_layer_ext.rs @@ -0,0 +1,59 @@ +/* + Appellation: impl_layer_ext + Created At: 2026.01.12:09:33:36 + Contrib: @FL03 +*/ +use crate::activate::Activator; +use crate::nn::layer::LayerBase; +use crate::nn::{RawLayer, RawLayerMut}; +use concision_params::RawParams; +use concision_traits::Forward; + +impl Activator for LayerBase +where + F: Activator, + P: RawParams, +{ + type Output = F::Output; + + fn activate(&self, input: X) -> Self::Output { + self.rho().activate(input) + } +} + +impl Forward for LayerBase +where + F: Activator, + P: RawParams + Forward, +{ + type Output = F::Output; + + fn forward(&self, input: &X) -> Self::Output { + self.rho().activate(self.params().forward(input)) + } +} + +impl RawLayer for LayerBase +where + F: Activator, + P: RawParams, +{ + type Params<_T> = P; + + fn rho(&self) -> &F { + &self.rho + } + + fn params(&self) -> &P { + &self.params + } +} +impl RawLayerMut for LayerBase +where + F: Activator, + P: RawParams, +{ + fn params_mut(&mut self) -> &mut P { + &mut self.params + } +} diff --git a/core/src/nn/layer/impl_layer_repr.rs b/core/src/nn/layer/impl_layer_repr.rs new file mode 100644 index 00000000..08eb0c58 --- /dev/null +++ b/core/src/nn/layer/impl_layer_repr.rs @@ -0,0 +1,148 @@ +/* + appellation: impl_layer_repr + authors: @FL03 +*/ +use crate::activate::{Activator, HyperbolicTangent, Linear, ReLU, Sigmoid}; +use crate::nn::layer::LayerBase; +use concision_params::{ParamsBase, RawParams}; +use ndarray::{ArrayBase, DataOwned, Dimension, RawData, RemoveAxis, ShapeBuilder}; + +impl LayerBase> +where + F: Activator, + D: Dimension, + S: RawData, +{ + /// create a new instance from the given activation function and shape. + pub fn from_rho_with_shape(rho: F, shape: Sh) -> Self + where + A: Clone + Default, + S: DataOwned, + D: RemoveAxis, + Sh: ShapeBuilder, + { + Self { + rho, + params: ArrayBase::default(shape), + } + } + + pub fn dim(&self) -> D::Pattern { + self.params().dim() + } + + pub fn raw_dim(&self) -> D { + self.params().raw_dim() + } + + pub fn shape(&self) -> &[usize] { + self.params().shape() + } +} + +impl LayerBase> +where + F: Activator, + D: Dimension, + E: Dimension, + S: RawData, +{ + /// create a new layer from the given activation function and shape. + pub fn from_rho_with_shape(rho: F, shape: Sh) -> Self + where + A: Clone + Default, + S: DataOwned, + D: RemoveAxis, + Sh: ShapeBuilder, + { + Self { + rho, + params: ParamsBase::default(shape), + } + } + + pub const fn bias(&self) -> &ArrayBase { + self.params().bias() + } + + pub const fn bias_mut(&mut self) -> &mut ArrayBase { + self.params_mut().bias_mut() + } + + pub const fn weights(&self) -> &ArrayBase { + self.params().weights() + } + + pub const fn weights_mut(&mut self) -> &mut ArrayBase { + self.params_mut().weights_mut() + } + + pub fn dim(&self) -> D::Pattern { + self.params().dim() + } + + pub fn raw_dim(&self) -> D { + self.params().raw_dim() + } + + pub fn shape(&self) -> &[usize] { + self.params().shape() + } +} + +impl LayerBase +where + F: Fn(A) -> A, + P: RawParams, +{ +} + +impl LayerBase +where + P: RawParams, +{ + /// initialize a layer using the [`Linear`] activation function and the given params. + pub const fn linear(params: P) -> Self { + Self { + rho: Linear, + params, + } + } +} + +impl LayerBase +where + P: RawParams, +{ + /// initialize a layer using the [`Sigmoid`] activation function and the given params. + pub const fn sigmoid(params: P) -> Self { + Self { + rho: Sigmoid, + params, + } + } +} + +impl LayerBase +where + P: RawParams, +{ + /// initialize a new layer using a [`TanhActivator`] activation function and the given + /// parameters. + pub const fn tanh(params: P) -> Self { + Self { + rho: HyperbolicTangent, + params, + } + } +} + +impl LayerBase +where + P: RawParams, +{ + /// initialize a layer using the [`Sigmoid`] activation function and the given params. + pub const fn relu(params: P) -> Self { + Self { rho: ReLU, params } + } +} diff --git a/core/src/nn/mod.rs b/core/src/nn/mod.rs index 1773c040..bbe71591 100644 --- a/core/src/nn/mod.rs +++ b/core/src/nn/mod.rs @@ -6,47 +6,25 @@ //! This module provides network specific implementations and traits supporting the development //! of neural network models. //! +#[doc(inline)] +pub use self::{layer::*, traits::*}; -pub(crate) mod prelude { - pub use super::{NetworkConsts, NeuralNetwork, NeuralNetworkParams}; -} +pub mod layer; -use ndarray::{Dimension, RawData}; +mod traits { + #[doc(inline)] + pub use self::{context::*, layer::*, model::*, neural_network::*}; -pub trait NeuralNetworkParams::Elem> -where - D: Dimension, - S: RawData, -{ + mod context; + mod layer; + mod model; + mod neural_network; } -/// The [`NeuralNetwork`] trait is used to define the network itself as well as each of its -/// constituent parts. -pub trait NeuralNetwork::Elem> -where - D: Dimension, - S: RawData, -{ - /// The context of the neural network defines any additional information required for its operation. - type Ctx; - /// The configuration of the neural network defines its architecture and hyperparameters. - type Config; - /// The parameters of the neural network define its weights and biases. - type Params<_S, _D>: NeuralNetworkParams<_S, _D, A> - where - _S: RawData, - _D: Dimension; - - /// returns a reference to the network configuration; - fn config(&self) -> &Self::Config; - - fn params(&self) -> &Self::Params; - - fn params_mut(&mut self) -> &mut Self::Params; +pub(crate) mod prelude { + pub use super::layer::*; + pub use super::traits::*; } -/// A trait defining common constants for neural networks. -pub trait NetworkConsts { - const NAME: &'static str; - const VERSION: &'static str; -} +#[cfg(test)] +mod tests {} diff --git a/core/src/nn/traits/context.rs b/core/src/nn/traits/context.rs new file mode 100644 index 00000000..d7f2ac94 --- /dev/null +++ b/core/src/nn/traits/context.rs @@ -0,0 +1,17 @@ +/* + Appellation: context + Created At: 2025.12.14:06:17:36 + Contrib: @FL03 +*/ + +pub trait RawContext { + private! {} +} + +/* + ************* Implementations ************* +*/ + +impl RawContext for () { + seal! {} +} diff --git a/core/src/nn/traits/layer.rs b/core/src/nn/traits/layer.rs new file mode 100644 index 00000000..df6935ae --- /dev/null +++ b/core/src/nn/traits/layer.rs @@ -0,0 +1,65 @@ +/* + Appellation: layer + Created At: 2025.12.10:16:50:03 + Contrib: @FL03 +*/ +use crate::activate::{Activator, ActivatorGradient}; +use concision_params::RawParams; +use concision_traits::{Backward, Forward}; + +/// The [`RawLayer`] trait establishes a common interface for all _layers_ within a given +/// model. Implementors will need to define the type of parameters they utilize, as well as +/// provide methods to access both the activation function and the parameters of the layer. +pub trait RawLayer +where + F: Activator, + Self::Params: RawParams, +{ + type Params<_T>; + /// the activation function of the layer + fn rho(&self) -> &F; + /// returns an immutable reference to the parameters of the layer + fn params(&self) -> &Self::Params; + /// complete a forward pass through the layer + fn forward(&self, input: &X) -> Z + where + F: Activator, + Self::Params: Forward, + { + let y = self.params().forward(input); + self.rho().activate(y) + } +} +/// The [`RawLayerMut`] trait extends the [`RawLayer`] trait by providing mutable access to the +/// layer's parameters and additional methods for training the layer, such as backward +/// propagation and parameter updates. +pub trait RawLayerMut: RawLayer +where + F: Activator, + Self::Params: RawParams, +{ + /// returns a mutable reference to the parameters of the layer + fn params_mut(&mut self) -> &mut Self::Params; + /// backward propagate error through the layer + fn backward(&mut self, input: X, error: Y, gamma: A) + where + A: Clone, + F: ActivatorGradient, + Self::Params: Backward, + { + let delta = self.rho().activate_gradient(error); + self.params_mut().backward(&input, &delta, gamma) + } + /// update the layer parameters + fn set_params(&mut self, params: Self::Params) { + *self.params_mut() = params; + } + /// [`replace`](core::mem::replace) the params of the layer, returning the previous value + fn replace_params(&mut self, params: Self::Params) -> Self::Params { + core::mem::replace(self.params_mut(), params) + } + /// [`swap`](core::mem::swap) the params of the layer with another + fn swap_params(&mut self, other: &mut Self::Params) { + core::mem::swap(self.params_mut(), other); + } +} diff --git a/core/src/models/traits/model.rs b/core/src/nn/traits/model.rs similarity index 99% rename from core/src/models/traits/model.rs rename to core/src/nn/traits/model.rs index 7083cf2d..62d908d4 100644 --- a/core/src/models/traits/model.rs +++ b/core/src/nn/traits/model.rs @@ -121,7 +121,7 @@ pub trait ModelExt: Model { } /// returns the total number of hidden layers in the model; fn hidden_layers_count(&self) -> usize { - self.layout().layers() + self.layout().depth() } /// returns a 2-tuple representing the dimensions of the output layer; (hidden, output) fn output_dim(&self) -> (usize, usize) { diff --git a/core/src/nn/traits/neural_network.rs b/core/src/nn/traits/neural_network.rs new file mode 100644 index 00000000..73426f90 --- /dev/null +++ b/core/src/nn/traits/neural_network.rs @@ -0,0 +1,72 @@ +/* + Appellation: neural_network + Created At: 2025.12.10:16:20:19 + Contrib: @FL03 +*/ +use concision_params::RawParams; +use concision_traits::{RawStore, RawStoreMut, Store}; +use ndarray::{Dimension, RawData}; + +pub trait NetworkParams::Elem> +where + D: Dimension, + S: RawData, +{ +} +/// The [`NetworkConfig`] trait defines an interface for compatible configurations within the +/// framework, providing a layout and a key-value store to manage hyperparameters. +pub trait NetworkConfig { + /// the type of key-value store used to handle the hyperparameters of the network + type Store: RawStore; + + /// returns a reference to the key-value store + fn store(&self) -> &Self::Store; + /// returns a mutable reference to the key-value store + fn store_mut(&mut self) -> &mut Self::Store; + /// get a reference to a value in the store by key + fn get<'a>(&'a self, key: &K) -> Option<&'a V> + where + Self::Store: 'a, + { + self.store().get(key) + } + /// returns a mutable reference to a value in the store by key + fn get_mut<'a>(&'a mut self, key: &K) -> Option<&'a mut V> + where + Self::Store: 'a + RawStoreMut, + { + self.store_mut().get_mut(key) + } + /// returns the entry associated with the given key + fn hyperparam<'a>(&'a mut self, key: K) -> >::Entry<'a> + where + Self::Store: 'a + Store, + { + self.store_mut().entry(key) + } +} + +/// The [`NeuralNetwork`] trait is used to define the network itself as well as each of its +/// constituent parts. +pub trait NeuralNetwork +where + Self::Params: RawParams, +{ + /// The configuration of the neural network defines its architecture and hyperparameters. + type Config: NetworkConfig; + /// The parameters of the neural network define its weights and biases. + type Params<_A>; + + /// returns a reference to the network configuration; + fn config(&self) -> &Self::Config; + /// returns a reference to the network parameters + fn params(&self) -> &Self::Params; + /// returns a mutable reference to the network parameters + fn params_mut(&mut self) -> &mut Self::Params; +} + +/// A trait defining common constants for neural networks. +pub trait NetworkConsts { + const NAME: &'static str; + const VERSION: &'static str; +} diff --git a/core/src/types/parameters.rs b/core/src/types/parameters.rs new file mode 100644 index 00000000..619771bf --- /dev/null +++ b/core/src/types/parameters.rs @@ -0,0 +1,46 @@ +/* + Appellation: parameters + Created At: 2025.12.14:07:14:46 + Contrib: @FL03 +*/ + +pub trait HyperParamKey: + Eq + AsRef + core::borrow::Borrow + core::fmt::Debug + core::hash::Hash +{ + private! {} +} + +impl HyperParamKey for str { + seal! {} +} + +#[cfg(feature = "alloc")] +impl HyperParamKey for alloc::string::String { + seal! {} +} + +/// The [`Parameter`] struct represents a key-value pair used for configuration +/// settings within the neural network framework. +pub struct Parameter { + pub key: K, + pub value: V, +} + +impl Parameter { + /// creates a new parameter instance + pub const fn new(key: K, value: V) -> Self { + Self { key, value } + } + /// returns a reference to the key + pub const fn key(&self) -> &K { + &self.key + } + /// returns a reference to the value + pub const fn value(&self) -> &V { + &self.value + } + /// returns a mutable reference to the value + pub fn value_mut(&mut self) -> &mut V { + &mut self.value + } +} diff --git a/core/src/utils.rs b/core/src/utils.rs index 3dfe5762..b4732f00 100644 --- a/core/src/utils.rs +++ b/core/src/utils.rs @@ -5,22 +5,12 @@ */ //! Additional utilities for creating, manipulating, and managing tensors and models. #[doc(inline)] -pub use self::prelude::*; +pub use self::{arith::*, dropout::*, gradient::*, norm::*, pad::*, patterns::*, tensor::*}; mod arith; mod dropout; mod gradient; mod norm; -pub mod pad; +mod pad; mod patterns; mod tensor; - -pub(crate) mod prelude { - pub use super::arith::*; - pub use super::dropout::*; - pub use super::gradient::*; - pub use super::norm::*; - pub use super::pad::*; - pub use super::patterns::*; - pub use super::tensor::*; -} diff --git a/core/src/utils/dropout.rs b/core/src/utils/dropout.rs index 99240852..3a7043f7 100644 --- a/core/src/utils/dropout.rs +++ b/core/src/utils/dropout.rs @@ -50,7 +50,7 @@ impl Default for Dropout { #[cfg(feature = "rand")] mod impl_rand { use super::*; - use concision_init::NdInit; + use concision_init::NdRandom; use concision_traits::Forward; use ndarray::{Array, ArrayBase, DataOwned, Dimension, ScalarOperand}; use num_traits::Num; diff --git a/core/src/utils/pad.rs b/core/src/utils/pad.rs index f4728949..31804c1b 100644 --- a/core/src/utils/pad.rs +++ b/core/src/utils/pad.rs @@ -6,8 +6,6 @@ mod impl_pad; mod impl_pad_mode; mod impl_padding; -pub type PadResult = Result; - /// The [`Pad`] trait defines a padding operation for tensors. pub trait Pad { type Output; @@ -93,14 +91,3 @@ pub enum PadMode { #[default] Wrap, } - -#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, thiserror::Error)] -#[cfg_attr( - feature = "serde", - derive(serde::Deserialize, serde::Serialize), - serde(rename_all = "snake_case") -)] -pub enum PadError { - #[error("Inconsistent Dimensions")] - InconsistentDimensions, -} diff --git a/core/src/utils/pad/impl_pad.rs b/core/src/utils/pad/impl_pad.rs index 1a37ad89..b2fc9767 100644 --- a/core/src/utils/pad/impl_pad.rs +++ b/core/src/utils/pad/impl_pad.rs @@ -41,7 +41,7 @@ where match mode.into_pad_action() { PadAction::StopAfterCopy => { // Do nothing - return Some(true); + Some(true) } _ => unimplemented!(), } diff --git a/core/tests/macros.rs b/core/tests/macros.rs new file mode 100644 index 00000000..455902de --- /dev/null +++ b/core/tests/macros.rs @@ -0,0 +1,21 @@ +/* + Appellation: macros + Created At: 2025.12.13:07:32:40 + Contrib: @FL03 +*/ +#![cfg(feature = "macros")] + +use concision_core::config; + +config! { + TestConfig { + test_key: f32, + number_key: usize, + } +} + +#[test] +fn test_config_macro() { + let cfg = TestConfig::new().with_test_key(3.14); + assert_eq!(cfg.test_key(), &3.14); +} diff --git a/core/tests/models.rs b/core/tests/models.rs index de132be0..061fd36c 100644 --- a/core/tests/models.rs +++ b/core/tests/models.rs @@ -3,37 +3,27 @@ Created At: 2025.12.07:11:02:49 Contrib: @FL03 */ -#![allow(unused_mut)] - use concision_core::ex::sample::TestModel; use concision_core::{Model, ModelFeatures, StandardModelConfig}; use ndarray::prelude::*; #[test] fn test_simple_model() { + // define the model features + let features = ModelFeatures::deep(3, 9, 1, 8); + // initialize some input data + let input = Array1::linspace(1.0, 9.0, features.input()); + // initialize a model configuration let mut config = StandardModelConfig::new() .with_epochs(1000) .with_batch_size(32); config.set_learning_rate(0.01); config.set_momentum(0.9); config.set_decay(0.0001); - // define the model features - let features = ModelFeatures::deep(3, 9, 1, 8); - // initialize the model with the given features and configuration - let mut model = TestModel::::new(config, features); - #[cfg(feature = "rand")] - { - model = model.init(); - } - // initialize some input data - let input = Array1::linspace(1.0, 9.0, model.layout().input()); + // define and initialize a new model + let model = TestModel::::new(config, features).init(); // forward the input through the model let output = model.predict(&input); - // verify the output shape - assert_eq!(output.dim(), (features.output())); - // compare the results to what we expected - #[cfg(not(feature = "rand"))] - { - assert_eq!(output, Array1::from_elem(model.layout().output(), 0.5)); - } + // verify the shape of the output + assert_eq! { output.dim(), (features.output()) } } diff --git a/data/Cargo.toml b/data/Cargo.toml index fd9325e6..b7f7c21c 100644 --- a/data/Cargo.toml +++ b/data/Cargo.toml @@ -1,13 +1,14 @@ [package] -authors.workspace = true build = "build.rs" -categories.workspace = true description = "this crate provides additional tools for working with datasets" +name = "concision-data" + +authors.workspace = true +categories.workspace = true edition.workspace = true homepage.workspace = true keywords.workspace = true license.workspace = true -name = "concision-data" readme.workspace = true repository.workspace = true rust-version.workspace = true @@ -15,7 +16,7 @@ version.workspace = true [package.metadata.docs.rs] all-features = false -features = ["full"] +features = ["default"] rustc-args = ["--cfg", "docsrs"] [package.metadata.release] @@ -24,13 +25,6 @@ tag-name = "v{{version}}" [lib] bench = false -crate-type = ["cdylib", "rlib"] -doc = true -doctest = true -test = true - -[[test]] -name = "default" [[test]] name = "loader" @@ -40,35 +34,38 @@ required-features = ["loader"] concision-core = { workspace = true } # custom variants = { workspace = true } +# data-structures +hashbrown = { optional = true, workspace = true } # error handling +anyhow = { workspace = true } thiserror = { workspace = true } # mathematics approx = { optional = true, workspace = true } ndarray = { workspace = true } -num = { workspace = true } num-complex = { optional = true, workspace = true } num-traits = { workspace = true } # concurrency & parallelism rayon = { optional = true, workspace = true } # networking -reqwest = { optional = true, version = "0.12" } +reqwest = { optional = true, features = ["default"], workspace = true } # data & serialization serde = { optional = true, workspace = true } +serde_derive = { optional = true, workspace = true } serde_json = { optional = true, workspace = true } # logging tracing = { optional = true, workspace = true } +# WebAssembly +wasm-bindgen = { optional = true, workspace = true } [features] -default = [ - "std", -] +default = ["std"] full = [ + "default", "approx", "complex", - "default", - "json", "rand", + "json", "serde", "tracing", ] @@ -78,6 +75,16 @@ nightly = [ ] # ************* [FF:Features] ************* +autodiff = [ + "nightly", + "concision-core/autodiff", +] + +rand = [ + "concision-core/rand", + "num-complex?/rand", +] + loader = [ "json", "reqwest", @@ -86,10 +93,12 @@ loader = [ # ************* [FF:Environments] ************* std = [ "alloc", + "anyhow/std", "concision-core/std", + "hashbrown?/default", "ndarray/std", "num-complex?/std", - "num/std", + "num-traits/std", "serde?/std", "serde_json?/std", "tracing?/std", @@ -102,19 +111,21 @@ wasi = [ wasm = [ "concision-core/wasm", + "rayon?/web_spin_lock", ] # ************* [FF:Dependencies] ************* alloc = [ "concision-core/alloc", - "num/alloc", + "hashbrown?/alloc", "serde?/alloc", + "serde_json?/alloc", "variants/alloc", ] approx = [ - "concision-core/approx", "dep:approx", + "concision-core/approx", "ndarray/approx", ] @@ -124,45 +135,52 @@ blas = [ ] complex = [ - "concision-core/complex", "dep:num-complex", + "concision-core/complex", +] + +hashbrown = [ + "dep:hashbrown", + "alloc" ] json = [ "alloc", - "concision-core/json", - "reqwest?/json", "serde", "serde_json", + "concision-core/json", + "reqwest?/json", ] rayon = [ - "concision-core/rayon", "dep:rayon", -] - -rand = [ - "concision-core/rand", - "rng", -] - -rng = [ - "concision-core/rng", + "concision-core/rayon", + "hashbrown?/rayon", ] reqwest = ["dep:reqwest"] serde = [ - "concision-core/serde", "dep:serde", + "dep:serde_derive", + "serde?/derive", + "concision-core/serde", + "hashbrown?/serde", "ndarray/serde", "num-complex?/serde", - "num/serde", ] -serde_json = ["dep:serde_json"] +serde_json = [ + "dep:serde_json", + "concision-core/serde_json", +] tracing = [ - "concision-core/tracing", "dep:tracing", + "concision-core/tracing", +] + +wasm_bindgen = [ + "dep:wasm-bindgen", + "concision-core/wasm_bindgen", ] diff --git a/data/src/lib.rs b/data/src/lib.rs index 225fcdc9..45306eae 100644 --- a/data/src/lib.rs +++ b/data/src/lib.rs @@ -11,28 +11,27 @@ clippy::upper_case_acronyms )] #![cfg_attr(not(feature = "std"), no_std)] -#![cfg_attr(feature = "nightly", feature(allocator_api))] +#![cfg_attr(all(feature = "nightly", feature = "alloc"), feature(allocator_api))] +#![cfg_attr(all(feature = "nightly", feature = "autodiff"), feature(autodiff))] #![crate_type = "lib"] - +// compile-time checks #[cfg(not(any(feature = "std", feature = "alloc")))] -compiler_error! { - "Either the \"std\" feature or the \"alloc\" feature must be enabled." -} - +compiler_error! { "Either the \"std\" feature or the \"alloc\" feature must be enabled." } +// external crates #[cfg(feature = "alloc")] extern crate alloc; - -pub mod dataset; -pub mod error; -#[cfg(feature = "loader")] -pub mod loader; -pub mod trainer; - +// macros #[macro_use] pub(crate) mod macros { #[macro_use] pub mod seal; } +// modules +pub mod dataset; +pub mod error; +#[cfg(feature = "loader")] +pub mod loader; +pub mod trainer; pub mod traits { //! Additional traits and interfaces for working with datasets and data loaders. diff --git a/data/src/loader.rs b/data/src/loader.rs index 4336c989..38bb68cd 100644 --- a/data/src/loader.rs +++ b/data/src/loader.rs @@ -1,3 +1,9 @@ +/* + Appellation: loader + Created At: 2026.01.13:14:05:18 + Contrib: @FL03 +*/ +//! this module provides loading mechanisms for datasets and models. #[doc(inline)] pub use self::dataloader::Dataloader; @@ -10,4 +16,6 @@ pub(crate) mod prelude { pub trait Compile {} +/// The [`Loader`] trait establishes a common interface for all implemented loading mechanisms +/// within the framework. pub trait Loader {} diff --git a/data/src/traits/convert.rs b/data/src/traits/convert.rs index 9925f6a7..53b47ef8 100644 --- a/data/src/traits/convert.rs +++ b/data/src/traits/convert.rs @@ -1,8 +1,15 @@ +/* + Appellation: convert + Created At: 2026.01.13:14:06:50 + Contrib: @FL03 +*/ use crate::dataset::DatasetBase; - +/// The [`AsDataset`] trait defines the conversion from some reference into a dataset. pub trait AsDataset { fn as_dataset(&self) -> DatasetBase; } +/// Thge [`IntoDataset`] trait defines a method for consuming the caller to convert it into a +/// dataset. pub trait IntoDataset { fn into_dataset(self) -> DatasetBase; } @@ -20,6 +27,7 @@ where self.as_ref().clone() } } + impl IntoDataset for A where A: Into>, diff --git a/default.nix b/default.nix new file mode 100644 index 00000000..b7e02189 --- /dev/null +++ b/default.nix @@ -0,0 +1,43 @@ +{ pkgs, nixpkgs, system, makeRustPlatform, rust-overlay }: +let + rustPkgs = import nixpkgs { + inherit system; + overlays = [ (import rust-overlay) ]; + }; + + rustVersion = "1.85.0"; + wasmUnknownUknown = "wasm32-unknown-unknown"; + wasm32Wasi = "wasm32-wasi"; + + rustDefaultTarget = rustPkgs.rust-bin.stable.${rustVersion}.default; + + rustWithWasmTarget = rustPkgs.rust-bin.nightly.${rustVersion}.default.override { + targets = [ wasmUnknownUknown ]; + }; + + rustPlatform = makeRustPlatform { + cargo = rustDefaultTarget; + rustc = rustDefaultTarget; + }; + + rustPlatformWasm = makeRustPlatform { + cargo = rustWithWasmTarget; + rustc = rustWithWasmTarget; + }; + + common = { + version = "0.3.1"; + src = self; # ./.; + + cargoLock = { + lockFile = ./Cargo.lock; + }; + + nativeBuildInputs = [ pkgs.pkg-config ]; + PKG_CONFIG_PATH = "${pkgs.openssl.dev}/lib/pkgconfig"; + }; +in { + workspace = pkgs.rustPlatformWasm.buildRustPackage (common // { + cargoBuildFlags = "--release --workspace"; + }); +} \ No newline at end of file diff --git a/derive/src/impls.rs b/derive/src/impls.rs index c19c760e..89b8f6c7 100644 --- a/derive/src/impls.rs +++ b/derive/src/impls.rs @@ -8,41 +8,19 @@ mod impl_config; mod impl_keys; use proc_macro2::TokenStream; -use quote::{format_ident, quote}; -use syn::{Data, DataStruct, DeriveInput}; +use syn::{Data, DeriveInput}; pub fn impl_config(DeriveInput { ident, data, .. }: &DeriveInput) -> TokenStream { - // ensure the target object is a struct - let out = match &data { - Data::Struct(s) => impl_config::derive_config_from_struct(&s, &ident), + match &data { + Data::Struct(s) => impl_config::derive_config_from_struct(s, ident), _ => panic!("Only structs are supported"), - }; - - // Combine the generated code - quote! { - #out } } -pub fn impl_keys(input: &DeriveInput) -> TokenStream { - // Get the name of the struct - let struct_name = &input.ident; - let store_name = format_ident!("{}Key", struct_name); - - // Generate the parameter struct definition +pub fn impl_keys(DeriveInput { data, ident, .. }: &DeriveInput) -> TokenStream { + match &data { + Data::Struct(s) => impl_keys::generate_keys_for_struct(s, ident), - // Generate the parameter keys enum - let param_keys_enum = match &input.data { - Data::Struct(s) => { - let DataStruct { fields, .. } = s; - - impl_keys::generate_keys(fields, &store_name) - } _ => panic!("Only structs are supported"), - }; - - // Combine the generated code - quote! { - #param_keys_enum } } diff --git a/derive/src/impls/impl_keys.rs b/derive/src/impls/impl_keys.rs index 80c4e038..8c5ed30e 100644 --- a/derive/src/impls/impl_keys.rs +++ b/derive/src/impls/impl_keys.rs @@ -5,16 +5,20 @@ use crate::utils::capitalize_first; use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use syn::{Fields, FieldsNamed, Ident, Variant}; +use syn::{DataStruct, Fields, FieldsNamed, Ident, Variant}; -pub fn generate_keys(fields: &Fields, name: &Ident) -> TokenStream { - match fields { - Fields::Named(inner) => handle_named(inner, name), +pub fn generate_keys_for_struct( + DataStruct { fields, .. }: &DataStruct, + ident: &Ident, +) -> TokenStream { + let store_name = format_ident!("{}Key", ident); + match &fields { + Fields::Named(inner) => handle_named(inner, &store_name), _ => panic!("Only named fields are supported"), } } -pub fn handle_named(fields: &FieldsNamed, name: &Ident) -> TokenStream { +fn handle_named(fields: &FieldsNamed, name: &Ident) -> TokenStream { let FieldsNamed { named, .. } = fields; let methods = named.iter().cloned().map(|f| { let ident = f.ident.unwrap(); diff --git a/ext/Cargo.toml b/ext/Cargo.toml index ac8ea512..de9ae7de 100644 --- a/ext/Cargo.toml +++ b/ext/Cargo.toml @@ -16,7 +16,6 @@ version.workspace = true [package.metadata.docs.rs] all-features = false -doc-scrape-examples = true features = ["full"] rustc-args = ["--cfg", "docsrs"] @@ -26,9 +25,6 @@ tag-name = "v{{version}}" [lib] bench = false -crate-type = ["cdylib", "rlib"] -doctest = false -test = true [[example]] name = "attention" @@ -38,16 +34,8 @@ required-features = ["attention", "rand", "std"] name = "snn" required-features = ["approx", "snn", "std"] -[[test]] -name = "attention" -required-features = ["approx", "attention", "rand", "std"] - -[[test]] -name = "snn" -required-features = ["approx", "snn", "std"] - [dependencies] -concision = { workspace = true } +concision = { features = ["macros"], workspace = true } # custom variants = { workspace = true } # error handling @@ -68,13 +56,15 @@ serde_derive = { optional = true, workspace = true } serde_json = { optional = true, workspace = true } # logging tracing = { optional = true, workspace = true } +# macros & utilities +paste = { workspace = true } [dev-dependencies] lazy_static = { workspace = true } tracing-subscriber = { features = ["std"], workspace = true } [features] -default = ["attention", "std"] +default = ["attention", "snn", "std"] full = [ "default", @@ -87,33 +77,38 @@ full = [ "tracing", ] -nightly = ["concision/nightly"] +nightly = [ + "concision/nightly" +] # ************* [FF:Toggles] ************* -json = ["alloc", "serde", "serde_json"] +autodiff = ["concision/autodiff"] + +rand = [ + "concision/rand", + "num/rand", + "num-complex?/rand", +] + +json = [ + "alloc", + "serde", + "serde_json", + "concision/json", +] signal = ["complex", "rustfft"] # ********* [FF:Models] ********* models = [ - "s4", "snn", - "transformer", ] attention = [] -cnn = [] - -kan = [] - -rnn = [] - -s4 = [] - snn = [] -transformer = ["attention"] +transformer = [] # ************* [FF:Environments] ************* std = [ @@ -131,13 +126,18 @@ std = [ "variants/std", ] -wasi = ["concision/wasi"] +wasi = [ + "concision/wasi" +] wasm = [ + "wasm_bindgen", "concision/wasm", "rayon?/web_spin_lock", ] -# ************* [FF:Dependencies] ************* + +# ********* [FF] Dependencies ********* + alloc = [ "concision/alloc", "num/alloc", @@ -154,13 +154,15 @@ approx = [ blas = ["concision/blas", "ndarray/blas"] -complex = ["dep:num-complex"] - -rand = ["concision/rand", "num-complex?/rand", "num/rand"] - -rayon = ["concision/rayon", "ndarray/rayon"] +complex = [ + "dep:num-complex", + "concision/complex", +] -rng = ["concision/rng"] +rayon = [ + "concision/rayon", + "ndarray/rayon" +] rustfft = ["dep:rustfft"] @@ -173,9 +175,16 @@ serde = [ "num/serde", ] -serde_json = ["dep:serde_json"] +serde_json = [ + "dep:serde_json", + "concision/serde_json", +] tracing = [ "dep:tracing", "concision/tracing", ] + +wasm_bindgen = [ + "concision/wasm_bindgen" +] \ No newline at end of file diff --git a/ext/examples/attention.rs b/ext/examples/attention.rs index 76bd16f8..a7f558c3 100644 --- a/ext/examples/attention.rs +++ b/ext/examples/attention.rs @@ -3,17 +3,18 @@ Created At: 2025.11.28:13:41:41 Contrib: @FL03 */ -use concision_ext::attention::{Qkv, ScaledDotProductAttention}; +use concision_ext::attention::{Qkv, SDPA}; fn main() -> anyhow::Result<()> { tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) .with_max_level(tracing::Level::TRACE) .with_timer(tracing_subscriber::fmt::time::Uptime::default()) .init(); let (m, n) = (7, 10); let qkv = Qkv::::ones((m, n)); // initialize the scaled dot-product attention layer - let layer = ScaledDotProductAttention::::new(0.1, 1.0); + let layer = SDPA::::new(0.1, 1.0); // compute the attention scores let z_score = layer.attention(&qkv); println!("z_score: {:?}", z_score); diff --git a/ext/examples/snn.rs b/ext/examples/snn.rs index 19f8bd3b..00fc3293 100644 --- a/ext/examples/snn.rs +++ b/ext/examples/snn.rs @@ -1,40 +1,38 @@ -/* - Appellation: snn - Created At: 2025.12.08:15:27:07 - Contrib: @FL03 -*/ -//! Minimal demonstration of neuron usage. Simulates a neuron for `t_sim` ms with dt, -//! injects a constant external current `i_ext`, and injects discrete synaptic events at specified times. -use concision_ext::snn::{LIFNeuron, SynapticEvent}; +//! # Example: Leaky Integrate-and-Fire Neuron +//! +//! This example demonstrates the usage of a Leaky Integrate-and-Fire (LIF) neuron model, +//! simulating its behavior over time with constant external current injection and discrete +//! synaptic events. +use concision_ext::snn::{Leaky, SynapticEvent}; fn main() -> anyhow::Result<()> { tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) .with_max_level(tracing::Level::TRACE) .with_timer(tracing_subscriber::fmt::time::Uptime::default()) .init(); // Simulation parameters - let dt = 0.1; // ms - let t_sim = 5000.0; // ms + let dt: f64 = 0.1; // ms + let t_sim: f64 = 5000.0; // ms let steps = (t_sim / dt) as usize; // Create neuron with defaults - let mut neuron = LIFNeuron::default(); + let mut neuron = Leaky::::default(); // Example external current (constant) // Increase drive so steady-state v can reach threshold (v_rest + R*i_ext > v_thresh). // With default params: v_rest = -65, v_thresh = -50 → need i_ext >= 15. - let i_ext = 16.0; // stronger constant drive to induce spiking + let i_ext = 15.0; // stronger constant drive to induce spiking // Example presynaptic spike times (ms) and weights - let presyn_spikes: Vec<(f64, f64)> = - vec![(50.0, 2.0), (100.0, 1.5), (150.0, 2.2), (300.0, 3.0)]; + let presyn_spikes = [(50.0, 2.0), (100.0, 1.5), (150.0, 2.2), (300.0, 3.0)]; // Convert into an index-able event list let mut events: Vec> = vec![Vec::new(); steps + 1]; for (t_spike, weight) in presyn_spikes { let idx = (t_spike / dt).round() as isize; if idx >= 0 && (idx as usize) < events.len() { - events[idx as usize].push(SynapticEvent { weight }); + events[idx as usize].push(SynapticEvent::new(weight)); } } @@ -54,11 +52,7 @@ fn main() -> anyhow::Result<()> { if res.is_spiked() { spike_times.push(t); // print the pre-spike membrane potential from the step result - println!( - "Spike at {:.3} ms (pre-spike v = {:.3})", - t, - res.membrane_potential() - ); + tracing::info!("Spike at {:.3} ms (pre-spike v = {:.3})", t, res.get()); } // optionally, record v, w, s for analysis (omitted here for brevity) @@ -69,10 +63,10 @@ fn main() -> anyhow::Result<()> { // small example of printing membrane potential every 50 ms if step % ((50.0 / dt) as usize) == 0 { // show the step result potential (pre-reset when a spike occurred) - tracing::info!( + tracing::trace!( "t={:.1} ms, v={:.3} mV, w={:.3}, s={:.3}", t, - res.membrane_potential(), + res.get(), _w, _s ); diff --git a/ext/src/attention/mod.rs b/ext/src/attention/mod.rs index b80e8950..759c85cf 100644 --- a/ext/src/attention/mod.rs +++ b/ext/src/attention/mod.rs @@ -24,7 +24,7 @@ //! expensive. //! #[doc(inline)] -pub use self::{multi_head::MultiHeadAttention, qkv::*, scaled::ScaledDotProductAttention}; +pub use self::{multi_head::MultiHeadAttention, qkv::*, scaled::SDPA}; #[cfg(feature = "signal")] pub mod fft; @@ -47,5 +47,24 @@ pub(crate) mod prelude { #[doc(inline)] pub use super::qkv::QkvParamsBase; #[doc(inline)] - pub use super::scaled::ScaledDotProductAttention; + pub use super::scaled::SDPA; +} + +#[cfg(test)] +mod tests { + use super::{Qkv, SDPA}; + + #[test] + fn test_scaled_dot_product_attention() { + // define the shape of the params + let [m, n] = [7, 10]; + // initialize some params + let qkv = Qkv::::ones((m, n)); + // initialize the scaled dot-product attention layer + let layer = SDPA::::new(0.1, 1.0); + // compute the attention scores + let z_score = layer.attention(&qkv); + // verify the output dimensions + assert_eq!(z_score.shape(), &[m, n]); + } } diff --git a/ext/src/attention/scaled.rs b/ext/src/attention/scaled.rs index 6a3b95cc..946f2c7c 100644 --- a/ext/src/attention/scaled.rs +++ b/ext/src/attention/scaled.rs @@ -16,7 +16,7 @@ use num_traits::Float; /// /// This implementation follows the original paper /// ["Attention is All You Need"](https://arxiv.org/pdf/1706.03762) by Vaswani et al. -pub struct ScaledDotProductAttention> +pub struct SDPA> where D: Dimension, S: RawData, @@ -28,7 +28,7 @@ where pub(crate) mask: Option>, } -impl ScaledDotProductAttention +impl SDPA where D: Dimension, S: RawData, diff --git a/ext/src/kan/mod.rs b/ext/src/kan/mod.rs deleted file mode 100644 index 53ea88b6..00000000 --- a/ext/src/kan/mod.rs +++ /dev/null @@ -1,20 +0,0 @@ -/* - Appellation: kan - Created At: 2025.11.26:13:58:50 - Contrib: @FL03 -*/ -//! This library provides an implementation of the Kolmogorov–Arnold Networks (kan) model using -//! the [`concision`](https://docs.rs/concision) framework. -//! -//! ## References -//! -//! - [KAN: Kolmogorov–Arnold Networks](https://arxiv.org/html/2404.19756v1) -//! -#[doc(inline)] -pub use self::model::*; - -mod model; - -pub(crate) mod prelude { - pub use super::model::*; -} diff --git a/ext/src/kan/model.rs b/ext/src/kan/model.rs deleted file mode 100644 index 5a6a11d1..00000000 --- a/ext/src/kan/model.rs +++ /dev/null @@ -1,122 +0,0 @@ -/* - appellation: model - authors: @FL03 -*/ -use cnc::config::StandardModelConfig; -use cnc::prelude::{DeepModelParams, Model, ModelFeatures}; - -#[cfg(feature = "rand")] -use cnc::init::rand_distr::{Distribution, StandardNormal}; -use num_traits::{Float, FromPrimitive}; - -#[derive(Clone, Debug)] -pub struct KanModel { - pub config: StandardModelConfig, - pub features: ModelFeatures, - pub params: DeepModelParams, -} - -impl KanModel { - pub fn new(config: StandardModelConfig, features: ModelFeatures) -> Self - where - T: Clone + Default, - { - let params = DeepModelParams::default(features); - KanModel { - config, - features, - params, - } - } - /// returns a reference to the model configuration - pub const fn config(&self) -> &StandardModelConfig { - &self.config - } - /// returns a mutable reference to the model configuration - pub const fn config_mut(&mut self) -> &mut StandardModelConfig { - &mut self.config - } - /// returns the model features - pub const fn features(&self) -> ModelFeatures { - self.features - } - /// returns a mutable reference to the model features - pub const fn features_mut(&mut self) -> &mut ModelFeatures { - &mut self.features - } - /// returns a reference to the model parameters - pub const fn params(&self) -> &DeepModelParams { - &self.params - } - /// returns a mutable reference to the model parameters - pub const fn params_mut(&mut self) -> &mut DeepModelParams { - &mut self.params - } - /// set the current configuration and return a mutable reference to the model - pub fn set_config(&mut self, config: StandardModelConfig) -> &mut Self { - self.config = config; - self - } - /// set the current features and return a mutable reference to the model - pub fn set_features(&mut self, features: ModelFeatures) -> &mut Self { - self.features = features; - self - } - /// set the current parameters and return a mutable reference to the model - pub fn set_params(&mut self, params: DeepModelParams) -> &mut Self { - self.params = params; - self - } - /// consumes the current instance to create another with the given configuration - pub fn with_config(self, config: StandardModelConfig) -> Self { - Self { config, ..self } - } - /// consumes the current instance to create another with the given features - pub fn with_features(self, features: ModelFeatures) -> Self { - Self { features, ..self } - } - /// consumes the current instance to create another with the given parameters - pub fn with_params(self, params: DeepModelParams) -> Self { - Self { params, ..self } - } -} - -impl KanModel -where - T: 'static + Float + FromPrimitive, -{ - #[cfg(feature = "rand")] - pub fn init(self) -> Self - where - T: 'static + Float + FromPrimitive, - rand_distr::StandardNormal: rand_distr::Distribution, - { - let params = DeepModelParams::glorot_normal(self.features()); - KanModel { params, ..self } - } -} - -impl Model for KanModel { - type Config = StandardModelConfig; - type Layout = ModelFeatures; - - fn config(&self) -> &StandardModelConfig { - &self.config - } - - fn config_mut(&mut self) -> &mut StandardModelConfig { - &mut self.config - } - - fn layout(&self) -> &ModelFeatures { - &self.features - } - - fn params(&self) -> &DeepModelParams { - &self.params - } - - fn params_mut(&mut self) -> &mut DeepModelParams { - &mut self.params - } -} diff --git a/ext/src/lib.rs b/ext/src/lib.rs index 5f9c97b3..c156b4d3 100644 --- a/ext/src/lib.rs +++ b/ext/src/lib.rs @@ -2,46 +2,53 @@ Appellation: concision-ext Contrib: @FL03 */ -//! An extension of the [`concision`](https://crates.io/crates/concision) library, focusing on -//! providing additional layers and other non-essential features for building more complex -//! neural networks and machine learning models. +//! An extension of the [`concision`](https://docs.rs/concision) library, focusing on +//! implementing additional layers, models, and utilities for deep learning applications. //! //! ## Features //! -//! - **attention**: Enables various attention mechanisms from scaled dot-product and -//! multi-headed attention to FFT-based attention. +//! - `attention`: Enables attention mechanisms commonly used in transformer architectures. +//! - `snn`: Introduces spiking neural network components for neuromorphic computing. //! -#![allow(clippy::module_inception, clippy::needless_doctest_main)] +#![allow( + clippy::missing_errors_doc, + clippy::module_inception, + clippy::needless_doctest_main, + clippy::upper_case_acronyms +)] #![cfg_attr(not(feature = "std"), no_std)] - +#![cfg_attr(all(feature = "nightly", feature = "alloc"), feature(allocator_api))] +#![cfg_attr(all(feature = "nightly", feature = "autodiff"), feature(autodiff))] +// compile-time checks +#[cfg(not(any(feature = "std", feature = "alloc")))] +compiler_error! { + "At least one of the \"std\" or \"alloc\" features must be enabled for the crate to compile." +} +// external crates #[cfg(feature = "alloc")] extern crate alloc; extern crate concision as cnc; +#[macro_use] +mod macros { + #[macro_use] + pub(crate) mod seal; + #[macro_use] + mod gsw; +} #[cfg(feature = "attention")] pub mod attention; -#[cfg(feature = "kan")] -pub mod kan; -#[cfg(feature = "s4")] -pub mod s4; #[cfg(feature = "snn")] pub mod snn; -#[cfg(feature = "transformer")] -pub mod transformer; /// re-exports #[cfg(feature = "attention")] -pub use self::attention::prelude::*; +pub use self::prelude::*; +// prelude #[doc(hidden)] pub mod prelude { #[cfg(feature = "attention")] pub use crate::attention::prelude::*; - #[cfg(feature = "kan")] - pub use crate::kan::prelude::*; - #[cfg(feature = "s4")] - pub use crate::s4::prelude::*; #[cfg(feature = "snn")] pub use crate::snn::prelude::*; - #[cfg(feature = "transformer")] - pub use crate::transformer::prelude::*; } diff --git a/ext/src/macros/gsw.rs b/ext/src/macros/gsw.rs new file mode 100644 index 00000000..7d3e4cd5 --- /dev/null +++ b/ext/src/macros/gsw.rs @@ -0,0 +1,25 @@ +/* + appellation: gsw + authors: @FL03 +*/ + +#[allow(unused_macros)] +/// [`set_with!`] is a macro used to generate `set_` and `with_` methods for a struct's fields. +macro_rules! set_with { + ($($field:ident: $T:ty),* $(,)?) => { + $(paste::paste! { + /// update the current value of the field and return a mutable reference to self + pub fn [](&mut self, value: $T) -> &mut Self { + self.$field = value; + self + } + /// consume the current instance to create another with the given value + pub fn [](self, $field: $T) -> Self { + Self { + $field, + ..self + } + } + })* + }; +} diff --git a/ext/src/macros/seal.rs b/ext/src/macros/seal.rs new file mode 100644 index 00000000..05f2bbf6 --- /dev/null +++ b/ext/src/macros/seal.rs @@ -0,0 +1,50 @@ +/* + Appellation: seal + Contrib: FL03 +*/ +//! The public parts of this private module are used to create traits +//! that cannot be implemented outside of our own crate. This way we +//! can feel free to extend those traits without worrying about it +//! being a breaking change for other implementations. +//! +//! ## Usage +//! +//! To define a private trait, you can use the [`private!`] macro, which will define a hidden +//! method `__private__` that can only be implemented within the crate. + +/// If this type is pub but not publicly reachable, third parties +/// can't name it and can't implement traits using it. +#[allow(dead_code)] +pub struct Seal; +/// the [`private!`] macro is used to seal a particular trait, defining a hidden method that +/// may only be implemented within the bounds of the crate. +#[allow(unused_macros)] +macro_rules! private { + () => { + /// This trait is private to implement; this method exists to make it + /// impossible to implement outside the crate. + #[doc(hidden)] + fn __private__(&self) -> $crate::macros::seal::Seal; + }; +} +/// the [`seal!`] macro is used to implement a private method on a type, which is used to seal +/// the type so that it cannot be implemented outside of the crate. +#[allow(unused_macros)] +macro_rules! seal { + () => { + fn __private__(&self) -> $crate::macros::seal::Seal { + $crate::macros::seal::Seal + } + }; +} +/// this macros is used to implement a trait for a type, sealing it so that +/// it cannot be implemented outside of the crate. This is most usefuly for creating other +/// macros that can be used to implement some raw, sealed trait on the given _types_. +#[allow(unused_macros)] +macro_rules! sealed { + (impl$(<$($T:ident),*>)? $trait:ident for $name:ident$(<$($V:ident),*>)? $(where $($rest:tt)*)?) => { + impl$(<$($T),*>)? $trait for $name$(<$($V),*>)? $(where $($rest)*)? { + seal!(); + } + }; +} diff --git a/ext/src/s4/mod.rs b/ext/src/s4/mod.rs deleted file mode 100644 index b02ac779..00000000 --- a/ext/src/s4/mod.rs +++ /dev/null @@ -1,19 +0,0 @@ -/* - appellation: s4 - authors: @FL03 -*/ -//! This library provides the sequential, structured state-space (S4) model implementation for the Concision framework. -//! -//! ## References -//! -//! - [Structured State Spaces for Sequence Modeling](https://arxiv.org/abs/2106.08084) -//! - [Efficiently Modeling Long Sequences with Structured State Spaces](https://arxiv.org/abs/2111.00396) -//! -#[doc(inline)] -pub use self::model::*; - -mod model; - -pub(crate) mod prelude { - pub use super::model::*; -} diff --git a/ext/src/s4/model.rs b/ext/src/s4/model.rs deleted file mode 100644 index 142df60c..00000000 --- a/ext/src/s4/model.rs +++ /dev/null @@ -1,123 +0,0 @@ -/* - appellation: model - authors: @FL03 -*/ -use cnc::config::StandardModelConfig; -use cnc::prelude::{DeepModelParams, Model, ModelFeatures}; - -#[cfg(feature = "rand")] -use cnc::init::rand_distr::{Distribution, StandardNormal}; -use num_traits::{Float, FromPrimitive}; - -#[derive(Clone, Debug)] -pub struct S4Model { - pub config: StandardModelConfig, - pub features: ModelFeatures, - pub params: DeepModelParams, -} - -impl S4Model { - pub fn new(config: StandardModelConfig, features: ModelFeatures) -> Self - where - T: Clone + Default, - { - let params = DeepModelParams::default(features); - S4Model { - config, - features, - params, - } - } - /// returns a reference to the model configuration - pub const fn config(&self) -> &StandardModelConfig { - &self.config - } - /// returns a mutable reference to the model configuration - pub const fn config_mut(&mut self) -> &mut StandardModelConfig { - &mut self.config - } - /// returns the model features - pub const fn features(&self) -> ModelFeatures { - self.features - } - /// returns a mutable reference to the model features - pub const fn features_mut(&mut self) -> &mut ModelFeatures { - &mut self.features - } - /// returns a reference to the model parameters - pub const fn params(&self) -> &DeepModelParams { - &self.params - } - /// returns a mutable reference to the model parameters - pub const fn params_mut(&mut self) -> &mut DeepModelParams { - &mut self.params - } - /// set the current configuration and return a mutable reference to the model - pub fn set_config(&mut self, config: StandardModelConfig) -> &mut Self { - self.config = config; - self - } - /// set the current features and return a mutable reference to the model - pub fn set_features(&mut self, features: ModelFeatures) -> &mut Self { - self.features = features; - self - } - /// set the current parameters and return a mutable reference to the model - pub fn set_params(&mut self, params: DeepModelParams) -> &mut Self { - self.params = params; - self - } - /// consumes the current instance to create another with the given configuration - pub fn with_config(self, config: StandardModelConfig) -> Self { - Self { config, ..self } - } - /// consumes the current instance to create another with the given features - pub fn with_features(self, features: ModelFeatures) -> Self { - Self { features, ..self } - } - /// consumes the current instance to create another with the given parameters - pub fn with_params(self, params: DeepModelParams) -> Self { - Self { params, ..self } - } -} - -impl S4Model -where - T: 'static + Float + FromPrimitive, -{ - /// initialize the model parameters using Glorot normal initialization - #[cfg(feature = "rand")] - pub fn init(self) -> Self - where - T: 'static + Float + FromPrimitive, - StandardNormal: Distribution, - { - let params = DeepModelParams::glorot_normal(self.features()); - S4Model { params, ..self } - } -} -impl Model for S4Model { - type Config = StandardModelConfig; - - type Layout = ModelFeatures; - - fn config(&self) -> &StandardModelConfig { - &self.config - } - - fn config_mut(&mut self) -> &mut StandardModelConfig { - &mut self.config - } - - fn layout(&self) -> &ModelFeatures { - &self.features - } - - fn params(&self) -> &DeepModelParams { - &self.params - } - - fn params_mut(&mut self) -> &mut DeepModelParams { - &mut self.params - } -} diff --git a/ext/src/snn/complex_neuron.rs b/ext/src/snn/complex_neuron.rs new file mode 100644 index 00000000..f07a3380 --- /dev/null +++ b/ext/src/snn/complex_neuron.rs @@ -0,0 +1,72 @@ +/* + Appellation: complex_neuron + Created At: 2025.12.14:14:17:56 + Contrib: @FL03 +*/ +//! A minimal complex-valued leaky integrate-and-fire oscillator with continuous-time dynamics +//! (rotation-stable, radial leakage): +//! ```math +//! \frac{dz}{dt}=(i * \omega - \gamma)\cdot z + I(t) +//! ``` +//! - $`\omega`$ : intrinsic angular velocity (rad/s) +//! - $`\gamma`$ : radial decay rate (1/s), gamma > 0 ensures boundedness +//! - $`I(t)`$ : complex-valued input (excitation) +//! +//! Exact discrete update for constant input over a step dt: +//! +//! ```math +//! \begin{equation}\begin{aligned} +//! \phi &= e\cdot\Big((i\cdot\omega - \gamma)\cdot{dt}\Big) +//! z_{n+1} &= \phi\cdot{z_n} + I\cdot\Big(\frac{1 - \phi}{i\cdot\omega - \gamma}\Big) +//! \end{aligned}\end{equation} +//! ``` +//! Phase-based spike condition (example policy): fire when the unwrapped phase crosses +//! $`\theta_{spike}^{+}`$ in the positive direction and magnitude $`|z| >= r_{thresh}`$. On spike, +//! radius may be reset to $`r_{reset}`$ while preserving phase. +//! +use ndarray::{ArrayBase, Dimension, RawData}; +use num_complex::{Complex, ComplexFloat}; +use num_traits::{Float, FloatConst, FromPrimitive}; + +/// Parameters for the complex LIF oscillator. +#[derive(Copy, Clone, Debug)] +pub struct ComplexNeuronParams { + pub omega: T, // intrinsic angular velocity (rad/s) + pub gamma: T, // radial decay (1/s), must be > 0 for boundedness + pub r_threshold: T, // minimum radius to allow a spike + pub r_reset: T, // radius after spike (if reset policy used) + pub theta_spike: T, // phase angle (radians) at which to trigger a spike +} + +/// Lightweight state for the neuron. +#[derive(Copy, Clone, Debug)] +pub struct ComplexNeuronState { + pub z: Complex, // complex state z = r * e^{i theta} + pub last_phase: Option, // last observed phase (radians), used for crossing detection +} + +/// Minimal complex-valued LIF neuron. +pub struct ComplexNeuron::Elem> +where + D: Dimension, + S: RawData>, +{ + pub params: ArrayBase>, + pub state: ComplexNeuronState, +} + +impl Default for ComplexNeuronParams +where + Complex: ComplexFloat, + T: Float + FloatConst + FromPrimitive, +{ + fn default() -> Self { + Self { + omega: T::from_usize(2).unwrap() * ::PI(), + gamma: T::one(), + r_threshold: T::from_f32(1.0).unwrap(), + r_reset: T::from_f32(0.05).unwrap(), + theta_spike: ::PI(), + } + } +} diff --git a/ext/src/snn/leaky.rs b/ext/src/snn/leaky.rs new file mode 100644 index 00000000..b918e816 --- /dev/null +++ b/ext/src/snn/leaky.rs @@ -0,0 +1,107 @@ +/* + Appellation: leaky + Created At: 2025.11.25:09:33:30 + Contrib: @FL03 +*/ + +mod impl_leaky; +mod impl_leaky_params; +mod impl_leaky_state; + +/// An implementation of a leaky integrate-and-fire (LIF) neuron with an adaptation term and +/// exponential synaptic current. Generally speaking, an intergate-and-fire neuron integrates +/// the input current over time until the membrane potential reaches a certain threshold, +/// at which point it emits a spike and resets its membrane potential. The _leaky_ term speaks +/// to the decay of the membrane potential over time, simulating the effect of a leaky +/// capacitor. +/// +/// ## Model +/// +/// Here, we describe the dynamics of a leaky integrate-and-fire (LIF) neuron with an +/// adaptation term and exponential synaptic current. The neuron's behavior is governed by the +/// following set of equations: +/// +/// ```math +/// \begin{aligned} +/// \tau_{m}\cdot{\frac{dv}{dt}} &= -(v - v_{rest}) + R\cdot{(I_{ext} + I_{syn})} - \omega \\ +/// \tau_{w}\cdot{\frac{d\omega}{dt}} &= -\omega \\ +/// \tau_{s}\cdot{\frac{ds}{dt}} &= -s +/// \end{aligned} +/// ``` +/// +/// ## Design +/// +/// The implementation consists of three main components: a parameter struct (`LeakyParams`), +/// a state struct (`LeakyState`), and the main neuron struct (`Leaky`) that combines both +/// parameters and state. The `Leaky` struct provides methods to update the neuron's state based +/// on the input current and time step, check for spikes, and reset the neuron after a spike. +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[cfg_attr( + feature = "serde", + derive(serde::Deserialize, serde::Serialize), + serde(rename_all = "snake_case") +)] +#[repr(C)] +pub struct Leaky { + #[cfg_attr(feature = "serde", serde(flatten))] + pub params: LeakyParams, + #[cfg_attr(feature = "serde", serde(flatten))] + pub state: LeakyState, + /// Minimum allowed dt for integration [ms] + pub min_dt: T, +} + +/// The params of a leaky integrate-and-fire (LIF) neuron +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[cfg_attr( + feature = "serde", + derive(serde::Deserialize, serde::Serialize), + serde(rename_all = "snake_case") +)] +#[repr(C)] +pub struct LeakyParams { + /// the adaptation increment `b` added to `w` on spike [same units as the current] + #[cfg_attr(feature = "serde", serde(alias = "adaptation_increment"))] + pub b: T, + /// the membrane time constant [ms] + pub tau_m: T, + /// the synaptic time constant [ms] + pub tau_s: T, + /// the time constant for adaptation [ms] + pub tau_w: T, + /// the resistance, `R` of the membrane [MΩ or equivalent units] + #[cfg_attr(feature = "serde", serde(alias = "r"))] + pub resistance: T, + /// the resting potential of the neuron [mV] + #[cfg_attr(feature = "serde", serde(alias = "resting_potential"))] + pub v_rest: T, + /// the spike reset potential [mV] + #[cfg_attr(feature = "serde", serde(alias = "reset_potential"))] + pub v_reset: T, + /// threshold potential for a spike [mV] + #[cfg_attr(feature = "serde", serde(alias = "threshold_potential"))] + pub v_thresh: T, +} + +/// The state of a leaky integrate-and-fire (LIF) neuron +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[cfg_attr( + feature = "serde", + derive(serde::Deserialize, serde::Serialize), + serde(rename_all = "snake_case") +)] +#[repr(C)] +pub struct LeakyState { + /// the neuron's membrane potential + #[cfg_attr(feature = "serde", serde(alias = "membrane_potential"))] + pub v: T, + /// the adaptation variable of the neuron + #[cfg_attr(feature = "serde", serde(alias = "adaptation"))] + pub w: T, + /// total synaptic current + #[cfg_attr( + feature = "serde", + serde(alias = "synaptic_current", alias = "synaptic_state") + )] + pub s: T, +} diff --git a/ext/src/snn/leaky/impl_leaky.rs b/ext/src/snn/leaky/impl_leaky.rs new file mode 100644 index 00000000..b7e9b5ef --- /dev/null +++ b/ext/src/snn/leaky/impl_leaky.rs @@ -0,0 +1,203 @@ +/* + Appellation: impl_leaky + Created At: 2025.12.10:13:05:53 + Contrib: @FL03 +*/ +use super::Leaky; + +use crate::snn::StepResult; +use crate::snn::leaky::{LeakyParams, LeakyState}; +use num_traits::{Float, FromPrimitive, Zero}; + +impl Leaky { + /// Create a neuron with explicit parameters and initial state. + pub fn new( + tau_m: T, + resistance: T, + v_rest: T, + v_thresh: T, + v_reset: T, + tau_w: T, + b: T, + tau_s: T, + v_init: Option, + ) -> Self + where + T: Copy + FromPrimitive + Zero, + { + let params = LeakyParams { + b, + tau_m, + tau_s, + tau_w, + resistance, + v_rest, + v_reset, + v_thresh, + }; + Self::from_params(params, v_init) + } + /// create a new instance of the LIF neuron from parameters and optional initial membrane + /// potential + pub fn from_params(params: LeakyParams, v_init: Option) -> Self + where + T: Copy + FromPrimitive + Zero, + { + let v0 = if let Some(vi) = v_init { + vi + } else { + params.v_rest + }; + let state = LeakyState::from_v(v0); + let min_dt = T::from_f32(1e-6).unwrap(); + Self { + params, + state, + min_dt, + } + } + /// returns a reference to the neuron's parameters + pub const fn params(&self) -> &LeakyParams { + &self.params + } + /// returns a mutable reference to the neuron's parameters + pub const fn params_mut(&mut self) -> &mut LeakyParams { + &mut self.params + } + /// returns a reference to the neuron's state + pub const fn state(&self) -> &LeakyState { + &self.state + } + pub const fn state_mut(&mut self) -> &mut LeakyState { + &mut self.state + } + /// returns a reference to the neuron's adaptation variable (`w`) + pub const fn adaptation(&self) -> &T { + self.state().w() + } + /// returns a reference to the membrane potential, `v`, of the neuron + pub const fn membrane_potential(&self) -> &T { + self.state().v() + } + /// returns a reference to the current value, or synaptic state, of the neuron (`s`) + pub const fn synaptic_state(&self) -> &T { + self.state().s() + } + /// returns a reference to the adaptation increment, `b`, of the neuron + pub const fn b(&self) -> &T { + self.params().b() + } + /// returns a reference to the membrane resistance, `R`, of the neuron + pub const fn resistance(&self) -> &T { + self.params().resistance() + } + /// returns a reference to the membrane time constant, `tau_m`, of the neuron + pub const fn tau_m(&self) -> &T { + self.params().tau_m() + } + /// returns a reference to the synaptic time constant, `tau_s`, of the neuron + pub const fn tau_s(&self) -> &T { + self.params().tau_s() + } + /// returns a reference to the adaptation time constant, `tau_w`, of the neuron + pub const fn tau_w(&self) -> &T { + self.params().tau_w() + } + /// returns a reference to the spike threshold, `v_thresh`, of the neuron + pub const fn v_thresh(&self) -> &T { + self.params().v_thresh() + } + /// returns a reference to the reset potential, `v_reset`, of the neuron + pub const fn v_reset(&self) -> &T { + self.params().v_reset() + } + /// returns a reference to the resting membrane potential, `v_rest`, of the neuron + pub const fn v_rest(&self) -> &T { + self.params().v_rest() + } + /// Apply a presynaptic spike event to the neuron; this increments the synaptic variable `s` + /// by `weight` instantaneously (models delta spike arrival). + pub fn apply_spike(&mut self, weight: T) + where + T: core::ops::AddAssign, + { + self.state_mut().apply_spike(weight); + } + /// reset state variables (keeps parameters). + pub fn reset_state(&mut self) + where + T: Default, + { + self.state_mut().reset(); + } + /// Integrate the neuron state forward by `dt` [ms] using forward Euler; the externally + /// applied current, `i_ext`, is added to the synaptic current `s` for the integration + /// step. Therefore it is important to maintain unitary consistency between `i_ext` and `s` + /// and to ensure that the provided `dt` is sufficiently small to avoid missing spikes, yet + /// still greater than 0 + /// + /// **Note**: The step function clamps `dt` to be at least `min_dt` to avoid numerical + /// issues while also ensuring the value is positve by using its absolute value. + /// Additionally, this method checks for threshold crossing explicitly to avoid missing + /// spikes due to large `dt`. + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all, level = "trace"))] + pub fn step(&mut self, dt: T, i_ext: T) -> StepResult + where + T: Float + FromPrimitive + core::ops::AddAssign, + { + if dt.is_sign_negative() { + eprintln!("Warning: dt is negative, taking absolute value."); + } + let dt = dt.abs().max(self.min_dt); + let LeakyParams { + b, + tau_m, + tau_s, + tau_w, + resistance, + v_rest, + v_reset, + v_thresh, + } = self.params; + let LeakyState { v, w, s } = self.state; + + if i_ext < (v_rest - v_thresh) / resistance { + #[cfg(feature = "tracing")] + tracing::warn!("The external current i_ext may be too low to reach threshold."); + } + // The total synaptic current (i_syn) for this step is given by `s` (for explicit Euler consistency). + let i_syn = s; + // evaluate ds/dt + let ds = -s / tau_s; + // evaluate dv/dt + let dv = (-(v - v_rest) + resistance * (i_ext + i_syn) - w) / tau_m; + // adaptation dw/dt = -w / tau_w + let dw = -w / tau_w; + // compute next state values + let next_state = LeakyState { + v: v + dt * dv, + w: w + dt * dw, + s: s + dt * ds, + }; + // replace the current state + let _prev = self.state_mut().replace(next_state); + let LeakyState { v: v_next, .. } = next_state; + // check for crossing + if v < v_thresh && v_next >= v_thresh { + // apply reset and adaptation increment + self.state_mut().set_v(v_reset).apply_adaptation(b); + StepResult::spiked(v_next) + } else { + StepResult::not_spiked(v) + } + } +} + +impl Default for Leaky +where + T: Float + FromPrimitive, +{ + fn default() -> Self { + Self::from_params(LeakyParams::default(), None) + } +} diff --git a/ext/src/snn/leaky/impl_leaky_params.rs b/ext/src/snn/leaky/impl_leaky_params.rs new file mode 100644 index 00000000..ed00429c --- /dev/null +++ b/ext/src/snn/leaky/impl_leaky_params.rs @@ -0,0 +1,164 @@ +/* + Appellation: impl_leaky_params + Created At: 2025.12.09:15:38:54 + Contrib: @FL03 +*/ +use super::LeakyParams; +use num_traits::{FromPrimitive, One}; + +impl LeakyParams { + #[allow(clippy::too_many_arguments)] + /// Create a new `LeakyParams` with the given parameters. + pub const fn new( + b: T, + tau_m: T, + tau_s: T, + tau_w: T, + resistance: T, + v_rest: T, + v_reset: T, + v_thresh: T, + ) -> Self { + Self { + b, + tau_m, + tau_s, + tau_w, + resistance, + v_rest, + v_reset, + v_thresh, + } + } + /// returns a reference to the adaptation increment, `b`, of the neuron + pub const fn b(&self) -> &T { + &self.b + } + /// returns a mutable reference to the adaptation increment, `b`, of the neuron + pub const fn b_mut(&mut self) -> &mut T { + &mut self.b + } + /// returns a reference to the membrane resistance, `R`, of the neuron + pub const fn resistance(&self) -> &T { + &self.resistance + } + /// returns a mutable reference to the membrane resistance, `R`, of the neuron + pub const fn resistance_mut(&mut self) -> &mut T { + &mut self.resistance + } + /// returns a reference to the membrane time constant, `tau_m`, of the neuron + pub const fn tau_m(&self) -> &T { + &self.tau_m + } + /// returns a reference to the mutable membrane time constant, `tau_m`, of the neuron + pub const fn tau_m_mut(&mut self) -> &mut T { + &mut self.tau_m + } + /// returns a reference to the synaptic time constant, `tau_s`, of the neuron + pub const fn tau_s(&self) -> &T { + &self.tau_s + } + /// returns a mutable reference to the synaptic time constant, `tau_s`, of the neuron + pub const fn tau_s_mut(&mut self) -> &mut T { + &mut self.tau_s + } + /// returns a reference to the adaptation time constant, `tau_w`, of the neuron + pub const fn tau_w(&self) -> &T { + &self.tau_w + } + /// returns a mutable reference to the adaptation time constant, `tau_w`, of the neuron + pub const fn tau_w_mut(&mut self) -> &mut T { + &mut self.tau_w + } + /// returns a reference to the reset potential, `v_reset`, of the neuron + pub const fn v_reset(&self) -> &T { + &self.v_reset + } + /// returns a mutable reference to the reset potential, `v_reset`, of the neuron + pub const fn v_reset_mut(&mut self) -> &mut T { + &mut self.v_reset + } + /// returns a reference to the resting membrane potential, `v_rest`, of the neuron + pub const fn v_rest(&self) -> &T { + &self.v_rest + } + /// returns a mutable reference to the resting membrane potential, `v_rest`, of the neuron + pub const fn v_rest_mut(&mut self) -> &mut T { + &mut self.v_rest + } + /// returns a reference to the spike threshold, `v_thresh`, of the neuron + pub const fn v_thresh(&self) -> &T { + &self.v_thresh + } + /// returns a mutable reference to the spike threshold, `v_thresh`, of the neuron + pub const fn v_thresh_mut(&mut self) -> &mut T { + &mut self.v_thresh + } + + set_with! { + b: T, + tau_m: T, + tau_s: T, + tau_w: T, + resistance: T, + v_rest: T, + v_reset: T, + v_thresh: T, + } +} + +impl Default for LeakyParams +where + T: FromPrimitive + One + core::ops::Neg, +{ + fn default() -> Self { + Self { + b: T::from_f32(0.5).unwrap(), // adaptation increment + resistance: T::one(), // arbitrary + tau_m: T::from_usize(20).unwrap(), // ms + tau_s: T::from_usize(5).unwrap(), // ms (fast synapse) + tau_w: T::from_usize(200).unwrap(), // ms (slow adaptation) + v_reset: T::from_usize(65).unwrap().neg(), // mV + v_rest: T::from_usize(65).unwrap().neg(), // mV + v_thresh: T::from_usize(50).unwrap().neg(), // mV + } + } +} + +impl core::fmt::Display for LeakyParams +where + T: core::fmt::Display, +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!( + f, + "{{ b: {}, resistance: {}, tau_m: {}, tau_s: {}, tau_w: {}, v_rest: {}, v_reset: {}, v_thresh: {} }}", + self.b, + self.resistance, + self.tau_m, + self.tau_s, + self.tau_w, + self.v_rest, + self.v_reset, + self.v_thresh + ) + } +} + +impl core::ops::Index<&str> for LeakyParams { + type Output = T; + + fn index(&self, index: &str) -> &Self::Output { + match index { + "b" | "adaptation_increment" => &self.b, + "resistance" | "r" => &self.resistance, + "tau_m" | "membrane_time_constant" => &self.tau_m, + "tau_s" | "synaptic_time_constant" => &self.tau_s, + "tau_w" | "adaptation_time_constant" => &self.tau_w, + "v_reset" | "reset_potential" => &self.v_reset, + "v_rest" | "resting_potential" => &self.v_rest, + "v_thresh" | "threshold_potential" => &self.v_thresh, + _ => panic!("invalid index for LeakyParams: {}", index), + } + } +} diff --git a/ext/src/snn/leaky/impl_leaky_state.rs b/ext/src/snn/leaky/impl_leaky_state.rs new file mode 100644 index 00000000..78551cbc --- /dev/null +++ b/ext/src/snn/leaky/impl_leaky_state.rs @@ -0,0 +1,250 @@ +/* + Appellation: impl_leaky_state + Created At: 2025.12.10:13:08:07 + Contrib: @FL03 +*/ +use super::LeakyState; +use num_traits::{One, Zero}; + +impl LeakyState { + /// Create a new `LeakyState` with all state variables initialized to zero. + pub const fn new(v: T, w: T, s: T) -> Self { + Self { v, w, s } + } + /// returns a new instance of the leak state with the given membrane potential + pub fn from_v(v: T) -> Self + where + T: Zero, + { + Self { + v, + w: T::zero(), + s: T::zero(), + } + } + /// create a new state initialize to 1 + pub fn one() -> Self + where + T: One, + { + Self { + v: T::one(), + w: T::one(), + s: T::one(), + } + } + /// create a new state initialize to zero + pub fn zero() -> Self + where + T: Zero, + { + Self { + v: T::zero(), + w: T::zero(), + s: T::zero(), + } + } + /// returns a reference to the neuron's adaptation variable (`w`) + pub const fn w(&self) -> &T { + &self.w + } + /// returns a mutable reference to the neuron's adaptation variable (`w`) + pub const fn w_mut(&mut self) -> &mut T { + &mut self.w + } + /// returns a reference to the membrane potential, `v`, of the neuron + pub const fn v(&self) -> &T { + &self.v + } + /// returns a mutable reference to the membrane potential, `v`, of the neuron + pub const fn v_mut(&mut self) -> &mut T { + &mut self.v + } + /// returns a reference to the current value, or synaptic state, of the neuron + pub const fn s(&self) -> &T { + &self.s + } + /// returns a mutable reference to the current value, or synaptic state, of the neuron + pub const fn s_mut(&mut self) -> &mut T { + &mut self.s + } + /// [`replace`](core::mem::replace) the values of one state with another returning the previous state + pub fn replace(&mut self, other: LeakyState) -> LeakyState { + LeakyState { + v: core::mem::replace(&mut self.v, other.v), + w: core::mem::replace(&mut self.w, other.w), + s: core::mem::replace(&mut self.s, other.s), + } + } + /// [`swap`](core::mem::swap) the values of one state with another + pub const fn swap(&mut self, other: &mut LeakyState) { + core::mem::swap(&mut self.v, &mut other.v); + core::mem::swap(&mut self.w, &mut other.w); + core::mem::swap(&mut self.s, &mut other.s); + } + #[inline] + /// [`take`](core::mem::take) the values of one state, replacing them with their logical defaults + pub fn take(&mut self) -> Self + where + T: Default, + { + Self { + v: core::mem::take(&mut self.v), + w: core::mem::take(&mut self.w), + s: core::mem::take(&mut self.s), + } + } + #[inline] + /// set the adaptation variable (`w`) to the given value + pub fn set_w(&mut self, w: T) -> &mut Self { + self.w = w; + self + } + #[inline] + /// set the membrane potential (`v`) to the given value + pub fn set_v(&mut self, v: T) -> &mut Self { + self.v = v; + self + } + #[inline] + /// update the synaptic state (`s`) to the given value + pub fn set_s(&mut self, s: T) -> &mut Self { + self.s = s; + self + } + #[inline] + /// consumes the current instance to create another with the given adaptation (`w`) + pub fn with_w(self, w: T) -> Self { + Self { w, ..self } + } + #[inline] + /// consumes the current instance to create another with the given membrane potential (`v`) + pub fn with_v(self, v: T) -> Self { + Self { v, ..self } + } + #[inline] + /// consumes the current instance to create another with the given synaptic state (`s`) + pub fn with_s(self, s: T) -> Self { + Self { s, ..self } + } + /// reset all state variables to their logical defaults + #[inline] + #[cfg_attr( + feature = "tracing", + tracing::instrument(skip_all, target = "leaky", name = "leaky_state::reset") + )] + pub fn reset(&mut self) + where + T: Default, + { + #[cfg(feature = "tracing")] + tracing::trace!("Resetting leaky neuron state to default values."); + self.v = T::default(); + self.w = T::default(); + self.s = T::default(); + } + /// update all state variables to the given values + #[inline] + pub fn update(&mut self, v: T, w: T, s: T) { + self.set_w(w).set_v(v).set_s(s); + } + /// Apply an incrementation to the adaptation variable `w` of the neuron. + pub fn apply_adaptation(&mut self, dw: T) -> &mut Self + where + T: core::ops::AddAssign, + { + self.w += dw; + self + } + /// Apply the given increment to the synaptic variable `s` of the neuron. + pub fn apply_spike(&mut self, ds: T) -> &mut Self + where + T: core::ops::AddAssign, + { + self.s += ds; + self + } + /// returns a reference to the value associated with the given key + /// + /// # Panics + /// + /// Panics if the key is not one of "v", "w", "s", or one of their respective aliases. + pub fn get(&self, key: &str) -> &T { + match key { + "v" | "membrane_potential" => &self.v, + "w" | "adaptation_variable" => &self.w, + "s" | "synaptic_current" => &self.s, + _ => panic!("invalid key for LeakyState: {}", key), + } + } + /// returns a reference to the value associated with the given key + /// + /// # Panics + /// + /// Panics if the key is not one of "v", "w", "s", or one of their respective aliases. + pub fn get_mut(&mut self, key: &str) -> &mut T { + match key { + "v" | "membrane_potential" => &mut self.v, + "w" | "adaptation_variable" => &mut self.w, + "s" | "synaptic_current" => &mut self.s, + _ => panic!("invalid key for LeakyState: {}", key), + } + } +} + +impl Default for LeakyState +where + T: Zero, +{ + fn default() -> Self { + Self::zero() + } +} + +impl core::fmt::Display for LeakyState +where + T: core::fmt::Display, +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{{ v: {}, w: {}, s: {} }}", self.v, self.w, self.s) + } +} + +impl core::ops::Index for LeakyState { + type Output = T; + + fn index(&self, index: usize) -> &Self::Output { + match index % 3 { + 0 => &self.v, + 1 => &self.w, + 2 => &self.s, + _ => unreachable!("modulo 3 should only yield 0, 1, or 2"), + } + } +} + +impl core::ops::Index for LeakyState { + type Output = T; + + fn index(&self, index: char) -> &Self::Output { + match index { + 'v' => &self.v, + 'w' => &self.w, + 's' => &self.s, + _ => panic!("invalid index for LeakyState: {}", index), + } + } +} + +impl core::ops::Index<&str> for LeakyState { + type Output = T; + + fn index(&self, index: &str) -> &Self::Output { + match index { + "v" | "membrane_potential" => &self.v, + "w" | "adaptation_variable" => &self.w, + "s" | "synaptic_current" => &self.s, + _ => panic!("invalid index for LeakyState: {}", index), + } + } +} diff --git a/ext/src/snn/mod.rs b/ext/src/snn/mod.rs index 017c38f4..3b940dfd 100644 --- a/ext/src/snn/mod.rs +++ b/ext/src/snn/mod.rs @@ -1,8 +1,4 @@ -//! Spiking neural networks (SNNs) for the [`concision`](https://crates.io/crates/concision) machine learning framework. -//! -//! ## References -//! -//! - [Deep Learning in Spiking Neural Networks](https://arxiv.org/abs/1804.08150) +//! An implementation of Spiking Neural Networks (SNNs) in Rust. //! //! ## Background //! @@ -13,45 +9,20 @@ //! event-driven processing, making them suitable for tasks that involve time-series data //! or require low-power computation. //! -//! ### Model (forward-Euler integration; units are arbitrary but consistent): -//! -//! ```math -//! \tau_m * \frac{dv}{dt} = -(v - v_{rest}) + R*(I_{ext} + I_{syn}) - \omega -//! ``` -//! -//! ```math -//! \tau_w * \frac{d\omega}{dt} = -\omega -//! ``` -//! -//! ```math -//! \tau_s * \frac{ds}{dt} = -s -//! ``` -//! -//! where: -//! - $`v`$: membrane potential -//! - $`\omega`$: adaptation variable -//! - $`s`$: synaptic variable representing total synaptic current -//! -//! If we allow the spike to be represented as $`\delta`$, then: -//! -//! ```math -//! v\geq{v_{thresh}}\rightarrow{\delta},v\leftarrow{v_{reset}},\omega\mathrel{+}=b -//! ``` +//! ## References //! -//! where $`b` is the adaptation increment added on spike. The synaptic current is given by: +//! - [Deep Learning in Spiking Neural Networks](https://arxiv.org/abs/1804.08150) //! -//! ```math -//! I_{syn} = s -//! ``` #[doc(inline)] -pub use self::{model::*, neurons::*, types::*, utils::*}; +pub use self::{leaky::*, types::*, utils::*}; + +#[cfg(feature = "complex")] +pub mod complex_neuron; +pub mod leaky; -mod model; -mod neurons; mod utils; -pub mod types { - //! Types for spiking neural networks +mod types { #[doc(inline)] pub use self::{event::*, result::*}; @@ -60,7 +31,55 @@ pub mod types { } pub(crate) mod prelude { - pub use super::model::*; - pub use super::neurons::*; - pub use super::types::*; + pub use super::leaky::Leaky; +} + +#[cfg(test)] +mod tests { + use super::Leaky; + + #[test] + fn test_leaky_neuron_resting() { + let mut n = Leaky::::default(); + let dt: f64 = 1.0; + // simulate 100 ms with no input -> should not spike and v near v_rest + for _ in 0..100 { + let res = n.step(dt, 0.0); + assert!(!res.is_spiked()); + } + let v = n.membrane_potential(); + assert!( + (v - n.v_rest()).abs() < 1e-5, + "v = {v}, v_rest = {}", + n.v_rest() + ); + } + + #[test] + fn test_leaky_neuron_spiking() { + // params + let dt = 1f64; + let i_ext = 50f64; // large i_ext to force spiking + // neuron + let mut n = Leaky::default(); + let mut spiked = false; + let mut steps = 0usize; + // run until spiked or max steps reached + while !spiked && steps < 1000 { + spiked = n.step(dt, i_ext).is_spiked(); + steps += 1; + } + assert!( + spiked, + "Neuron did not spike under a strong current (i_ext = {i_ext})" + ); + } + + #[test] + fn test_leaky_neuron_state_change() { + let mut n = Leaky::default(); + let before = *n.synaptic_state(); + n.apply_spike(2.5); + assert!(*n.synaptic_state() > before); + } } diff --git a/ext/src/snn/model.rs b/ext/src/snn/model.rs deleted file mode 100644 index 0f33f231..00000000 --- a/ext/src/snn/model.rs +++ /dev/null @@ -1,118 +0,0 @@ -/* - appellation: model - authors: @FL03 -*/ -use cnc::config::StandardModelConfig; -use cnc::prelude::{DeepModelParams, Model, ModelFeatures}; - -#[cfg(feature = "rand")] -use cnc::init::rand_distr::{Distribution, StandardNormal}; -use num_traits::{Float, FromPrimitive}; - -#[derive(Clone, Debug)] -pub struct SpikingNeuralNetwork { - pub config: StandardModelConfig, - pub features: ModelFeatures, - pub params: DeepModelParams, -} - -impl SpikingNeuralNetwork { - pub fn new(config: StandardModelConfig, features: ModelFeatures) -> Self - where - T: Clone + Default, - { - let params = DeepModelParams::default(features); - SpikingNeuralNetwork { - config, - features, - params, - } - } - /// returns a reference to the model configuration - pub const fn config(&self) -> &StandardModelConfig { - &self.config - } - /// returns a mutable reference to the model configuration - pub const fn config_mut(&mut self) -> &mut StandardModelConfig { - &mut self.config - } - /// returns the model features - pub const fn features(&self) -> ModelFeatures { - self.features - } - /// returns a mutable reference to the model features - pub const fn features_mut(&mut self) -> &mut ModelFeatures { - &mut self.features - } - /// returns a reference to the model parameters - pub const fn params(&self) -> &DeepModelParams { - &self.params - } - /// returns a mutable reference to the model parameters - pub const fn params_mut(&mut self) -> &mut DeepModelParams { - &mut self.params - } - /// set the current configuration and return a mutable reference to the model - pub fn set_config(&mut self, config: StandardModelConfig) { - self.config = config; - } - /// set the current features and return a mutable reference to the model - pub const fn set_features(&mut self, features: ModelFeatures) { - self.features = features; - } - /// set the current parameters and return a mutable reference to the model - pub fn set_params(&mut self, params: DeepModelParams) { - self.params = params; - } - #[inline] - /// consumes the current instance to create another with the given configuration - pub fn with_config(self, config: StandardModelConfig) -> Self { - Self { config, ..self } - } - #[inline] - /// consumes the current instance to create another with the given features - pub fn with_features(self, features: ModelFeatures) -> Self { - Self { features, ..self } - } - #[inline] - /// consumes the current instance to create another with the given parameters - pub fn with_params(self, params: DeepModelParams) -> Self { - Self { params, ..self } - } - - #[cfg(feature = "rand")] - pub fn init(self) -> Self - where - T: 'static + Float + FromPrimitive, - StandardNormal: Distribution, - { - let params = DeepModelParams::glorot_normal(self.features()); - SpikingNeuralNetwork { params, ..self } - } -} - -impl Model for SpikingNeuralNetwork { - type Config = StandardModelConfig; - - type Layout = ModelFeatures; - - fn config(&self) -> &StandardModelConfig { - &self.config - } - - fn config_mut(&mut self) -> &mut StandardModelConfig { - &mut self.config - } - - fn layout(&self) -> &ModelFeatures { - &self.features - } - - fn params(&self) -> &DeepModelParams { - &self.params - } - - fn params_mut(&mut self) -> &mut DeepModelParams { - &mut self.params - } -} diff --git a/ext/src/snn/neurons/lif.rs b/ext/src/snn/neurons/lif.rs deleted file mode 100644 index eacbe16c..00000000 --- a/ext/src/snn/neurons/lif.rs +++ /dev/null @@ -1,214 +0,0 @@ -/* - Appellation: neuron - Created At: 2025.11.25:09:33:30 - Contrib: @FL03 -*/ - -use crate::snn::StepResult; -use num_traits::{Float, FromPrimitive, NumAssign, Zero}; - -/// Leaky Integrate-and-Fire (LIF) neuron with an adaptation term and exponential synaptic -/// current. -/// -/// The neuron dynamics are governed by the following equations: -/// -/// ```math -/// \frac{dv}{dt} = \frac{-(v - v_{rest}) + R \cdot (i_{ext} + s) - w}{\tau_{m}} -/// ``` -#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] -#[cfg_attr( - feature = "serde", - derive(serde::Deserialize, serde::Serialize), - serde(rename_all = "snake_case") -)] -pub struct LIFNeuron { - // ---- Parameters ---- - /// Membrane time constant $`\tau_{m}`$ (ms) - pub tau_m: T, - /// Membrane resistance `R` (MΩ or arbitrary) - pub resistance: T, - /// Resting potential $``v_{rest}`$ (mV) - pub v_rest: T, - /// Threshold potential $`v_{thresh}`$ (mV) - pub v_thresh: T, - /// Reset potential after spike $`v_{reset}`$ (mV) - pub v_reset: T, - - /// Adaptation time constant $`\tau_{w}`$ (ms) - pub tau_w: T, - /// Adaptation increment added on spike `b` (same units as w/current) - pub b: T, - - /// Synaptic time constant $`\tau_{s}`$ (ms) - pub tau_s: T, - - // ---- State variables ---- - /// Membrane potential `v` - pub v: T, - /// Adaptation variable `w` - pub w: T, - /// Synaptic variable `s` representing total synaptic current - pub s: T, - - /// Minimum allowed dt for integration (ms) - pub min_dt: T, -} - -impl LIFNeuron { - /// Create a neuron with explicit parameters and initial state. - pub fn new( - tau_m: T, - resistance: T, - v_rest: T, - v_thresh: T, - v_reset: T, - tau_w: T, - b: T, - tau_s: T, - initial_v: Option, - ) -> Self - where - T: Float + FromPrimitive, - { - let v0 = if let Some(v_init) = initial_v { - v_init - } else { - v_rest - }; - Self { - tau_m, - resistance, - v_rest, - v_thresh, - v_reset, - tau_w, - b, - tau_s, - v: v0, - w: T::zero(), - s: T::zero(), - min_dt: T::from_f32(1e-6).unwrap(), - } - } - /// returns a reference to the neuron's adaptation variable (`w`) - pub const fn adaptation(&self) -> &T { - &self.w - } - /// returns a reference to the membrane potential, `v`, of the neuron - pub const fn membrane_potential(&self) -> &T { - &self.v - } - /// returns a reference to the current value, or synaptic state, of the neuron (`s`) - pub const fn synaptic_state(&self) -> &T { - &self.s - } - /// returns a reference to the membrane time constant, `tau_m`, of the neuron - pub const fn tau_m(&self) -> &T { - &self.tau_m - } - /// returns a reference to the membrane resistance, `R`, of the neuron - pub const fn resistance(&self) -> &T { - &self.resistance - } - #[cfg_attr(feature = "tracing", tracing::instrument(skip_all, level = "trace"))] - /// Apply a presynaptic spike event to the neuron; this increments the synaptic variable `s` - /// by `weight` instantaneously (models delta spike arrival). - pub fn apply_spike(&mut self, weight: T) - where - T: NumAssign + Zero, - { - self.s += weight; - } - /// reset state variables (keeps parameters). - pub fn reset_state(&mut self) - where - T: Clone + Default, - { - self.v = self.v_rest.clone(); - self.w = T::default(); - self.s = T::default(); - } - #[cfg_attr(feature = "tracing", tracing::instrument(skip_all, level = "trace"))] - /// Integrate the neuron state forward by `dt` [ms] using forward Euler; the externally - /// applied current, `i_ext`, is added to the synaptic current `s` for the integration - /// step. Therefore it is important to maintain unitary consistency between `i_ext` and `s` - /// and to ensure that the provided `dt` is sufficiently small to avoid missing spikes, yet - /// still greater than 0 - /// - /// **Note**: This method checks for threshold crossing explicitly to avoid missing spikes - /// due to large `dt`. Additionally, if `dt` is less than `min_dt`, it is clamped to - /// `min_dt`. - pub fn step(&mut self, dt: T, i_ext: T) -> StepResult - where - T: Float + FromPrimitive + NumAssign, - { - let dt = if dt.is_sign_negative() { - panic!("dt must be > 0") - } else { - dt.max(self.min_dt) - }; - - // remember previous membrane potential for crossing detection - let v_prev = self.v; - - // synaptic current is represented by `s` - // ds/dt = -s / tau_s - let ds = -self.s / self.tau_s; - let s_next = self.s + dt * ds; - - // total synaptic current for this step (use current s, or average between s and s_next) - // we use s for explicit Euler consistency. - let i_syn = self.s; - - // membrane dv/dt = (-(v - v_rest) + R*(i_ext + i_syn) - w) / tau_m - let dv = - (-(self.v - self.v_rest) + self.resistance * (i_ext + i_syn) - self.w) / self.tau_m; - let v_next = self.v + dt * dv; - - // adaptation dw/dt = -w / tau_w - let dw = -self.w / self.tau_w; - let w_next = self.w + dt * dw; - - // Commit state tentatively - self.v = v_next; - self.w = w_next; - self.s = s_next; - - // Check for threshold crossing (explicit crossing test to avoid misses) - if v_prev < self.v_thresh && self.v >= self.v_thresh { - // spike: capture pre-reset potential if that is expected by StepResult consumers - let pre_spike_v = self.v; - // apply reset and adaptation increment - self.v = self.v_reset; - self.w += self.b; - StepResult { - spiked: true, - v: pre_spike_v, - } - } else { - StepResult { - spiked: false, - v: self.v, - } - } - } -} - -impl Default for LIFNeuron -where - T: Float + FromPrimitive, -{ - fn default() -> Self { - let tau_m = T::from_usize(20).unwrap(); // ms - let resistance = T::one(); // arbitrary - let v_rest = T::from_usize(65).unwrap().neg(); // mV - let v_thresh = T::from_usize(50).unwrap().neg(); // mV - let v_reset = T::from_usize(65).unwrap().neg(); // mV - let tau_w = T::from_usize(200).unwrap(); // ms (slow adaptation) - let b = T::from_f32(0.5).unwrap(); // adaptation increment - let tau_s = T::from_usize(5).unwrap(); // ms (fast synapse) - Self::new( - tau_m, resistance, v_rest, v_thresh, v_reset, tau_w, b, tau_s, None, - ) - } -} diff --git a/ext/src/snn/neurons/mod.rs b/ext/src/snn/neurons/mod.rs deleted file mode 100644 index 2ceb1744..00000000 --- a/ext/src/snn/neurons/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -/* - Appellation: neurons - Created At: 2025.12.08:17:26:07 - Contrib: @FL03 -*/ -#[doc(inline)] -pub use self::lif::LIFNeuron; - -pub mod lif; diff --git a/ext/src/snn/types/event.rs b/ext/src/snn/types/event.rs index 7012f48b..accad006 100644 --- a/ext/src/snn/types/event.rs +++ b/ext/src/snn/types/event.rs @@ -7,6 +7,7 @@ /// A synaptic event that modifies the synaptic variable `s` by an instantaneous weight. #[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[repr(transparent)] pub struct SynapticEvent { /// instantaneous weight added to synaptic variable `s`. pub weight: T, diff --git a/ext/src/snn/types/result.rs b/ext/src/snn/types/result.rs index 8e4b87c5..5ac498a6 100644 --- a/ext/src/snn/types/result.rs +++ b/ext/src/snn/types/result.rs @@ -5,54 +5,111 @@ */ /// Result of a single integration step. -#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct StepResult { - /// Whether the neuron emitted a spike on this step. - pub(crate) spiked: bool, - /// The membrane potential after the step (mV or arbitrary units). - pub(crate) v: T, +#[repr(C)] +pub enum StepResult { + Spiked { v: T }, + NotSpiked { v: T }, } impl StepResult { - /// returns a new instance of the `StepResult`; - /// - /// **Note**:: defaults to a state of being _not spiked_. - pub const fn new(v: T) -> Self { - Self { spiked: false, v } + /// create a new instance of the `StepResult` using a boolean to indicate if the neuron + /// spiked during the step + pub const fn new(v: T, spiked: bool) -> Self { + if spiked { + Self::Spiked { v } + } else { + Self::NotSpiked { v } + } + } + /// returns an [`NotSpiked`](StepResult::NotSpiked) variant of the result with the given membrane potential + pub const fn not_spiked(v: T) -> Self { + Self::NotSpiked { v } } - /// returns a new, _spiked_ instance of the `StepResult` + /// returns a [`Spiked`](StepResult::Spiked) variant of the result with the given membrane potential pub const fn spiked(v: T) -> Self { - Self { spiked: true, v } + Self::Spiked { v } } #[inline] - /// consumes the current instance to create another that is said to have _spiked_. + /// returns a new instance of the result configured as a _spiked_ variant. pub fn spike(self) -> Self { - Self { - spiked: true, - ..self + match self { + Self::NotSpiked { v } => Self::Spiked { v }, + _ => self, } } #[inline] /// consumes the current instance to create another that is said to have _not spiked_. pub fn unspike(self) -> Self { - Self { - spiked: true, - ..self + match self { + Self::Spiked { v } => Self::NotSpiked { v }, + _ => self, } } - /// returns true if the neuron spiked during this step + /// returns true if the result is of a [`Spiked`](StepResult::Spiked) variant pub const fn is_spiked(&self) -> bool { - self.spiked + matches!(self, Self::Spiked { .. }) + } + /// returns true if the result is of a [`NotSpiked`](StepResult::NotSpiked) variant + pub const fn is_not_spiked(&self) -> bool { + matches!(self, Self::NotSpiked { .. }) } /// returns a reference to the membrane potential (`v`) - pub const fn membrane_potential(&self) -> &T { - &self.v + pub const fn get(&self) -> &T { + match self { + Self::Spiked { v } => v, + Self::NotSpiked { v } => v, + } + } + /// returns a mutable reference to the membrane potential (`v`) + pub const fn get_mut(&mut self) -> &mut T { + match self { + Self::Spiked { v } => v, + Self::NotSpiked { v } => v, + } + } +} + +impl AsRef for StepResult { + fn as_ref(&self) -> &T { + self.get() + } +} + +impl AsMut for StepResult { + fn as_mut(&mut self) -> &mut T { + self.get_mut() + } +} + +impl core::borrow::Borrow for StepResult { + fn borrow(&self) -> &T { + self.get() + } +} + +impl core::borrow::BorrowMut for StepResult { + fn borrow_mut(&mut self) -> &mut T { + self.get_mut() + } +} + +impl core::ops::Deref for StepResult { + type Target = T; + fn deref(&self) -> &Self::Target { + self.get() + } +} + +impl core::ops::DerefMut for StepResult { + fn deref_mut(&mut self) -> &mut Self::Target { + self.get_mut() } } impl PartialEq for StepResult { fn eq(&self, other: &bool) -> bool { - &self.spiked == other + &self.is_spiked() == other } } diff --git a/ext/src/snn/utils.rs b/ext/src/snn/utils.rs index aaade919..f867ad5a 100644 --- a/ext/src/snn/utils.rs +++ b/ext/src/snn/utils.rs @@ -3,14 +3,31 @@ Created At: 2025.12.08:15:53:49 Contrib: @FL03 */ - -use super::{LIFNeuron, SynapticEvent}; +use super::{Leaky, SynapticEvent}; #[cfg(feature = "alloc")] use alloc::vec::Vec; use num_traits::{Float, FromPrimitive, NumAssign}; +/// Compute the minimum external current required to make a leaky integrate-and-fire neuron +/// spike instantly +pub fn find_min_external_current(v_rest: T, v_thresh: T, resistance: T) -> T +where + T: core::ops::Div + core::ops::Sub, +{ + (v_rest - v_thresh) / resistance +} +/// Compute the minimum external drive required to make a leaky integrate-and-fire neuron spike +/// over a single time step. This is based on the analytical solution of the leaky +/// integrate-and-fire model. +#[inline] +pub fn compute_min_drive(v_rest: T, v_th: T, tau_m: T, dt: T) -> T { + let exp_term = (-dt / tau_m).exp(); + (v_th - v_rest * exp_term) / (T::one() - exp_term) +} + /// A basic method for _discovering_ the minimum external drive required to make a spiking /// neuron spike +#[inline] pub fn sweep_for_min_drive(step_size: T) -> T where T: Float + FromPrimitive + NumAssign, @@ -22,7 +39,7 @@ where let mut i_ext = T::zero(); loop { - let mut neuron = LIFNeuron::::default(); + let mut neuron = Leaky::::default(); let mut events: Vec>> = vec![Vec::new(); steps + 1]; for (t_spike, weight) in &presyn_spikes { let idx = (*t_spike / dt).round().to_isize().unwrap(); @@ -31,10 +48,8 @@ where } } let mut spiked = false; - for step in 0..steps { - for ev in &events[step] { - neuron.apply_spike(ev.weight); - } + for ev in events.iter().take(steps) { + ev.iter().for_each(|event| neuron.apply_spike(event.weight)); let res = neuron.step(dt, i_ext); if res.is_spiked() { spiked = true; diff --git a/ext/src/transformer/mod.rs b/ext/src/transformer/mod.rs deleted file mode 100644 index a2920c26..00000000 --- a/ext/src/transformer/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -/* - Appellation: transformers - Created At: 2025.11.26:13:59:39 - Contrib: @FL03 -*/ -//! A custom transformer model implementation for the Concision framework. -#[doc(inline)] -pub use self::model::*; - -mod model; - -pub(crate) mod prelude { - #[doc(inline)] - pub use super::model::*; -} diff --git a/ext/src/transformer/model.rs b/ext/src/transformer/model.rs deleted file mode 100644 index 2cf4531b..00000000 --- a/ext/src/transformer/model.rs +++ /dev/null @@ -1,322 +0,0 @@ -/* - Appellation: transformer - Contrib: @FL03 -*/ -#[cfg(feature = "rand")] -use cnc::init::rand_distr::{Distribution, StandardNormal}; -use cnc::{ - DeepModelParams, Forward, Model, ModelFeatures, Norm, Params, ReLUActivation, - SigmoidActivation, StandardModelConfig, Train, -}; - -use ndarray::linalg::Dot; -use ndarray::{Array1, Array2, ArrayBase, ArrayView1, Data, Ix1, Ix2, ScalarOperand}; -use num_traits::{Float, FromPrimitive, NumAssign}; - -#[derive(Clone, Debug)] -pub struct TransformerModel { - pub config: StandardModelConfig, - pub features: ModelFeatures, - pub params: DeepModelParams, -} - -impl TransformerModel { - pub fn new(config: StandardModelConfig, features: ModelFeatures) -> Self - where - T: Clone + Default, - { - let params = DeepModelParams::default(features); - TransformerModel { - config, - features, - params, - } - } - /// returns a reference to the model configuration - pub const fn config(&self) -> &StandardModelConfig { - &self.config - } - /// returns a mutable reference to the model configuration - pub const fn config_mut(&mut self) -> &mut StandardModelConfig { - &mut self.config - } - /// returns the model features - pub const fn features(&self) -> ModelFeatures { - self.features - } - /// returns a mutable reference to the model features - pub const fn features_mut(&mut self) -> &mut ModelFeatures { - &mut self.features - } - /// returns a reference to the model parameters - pub const fn params(&self) -> &DeepModelParams { - &self.params - } - /// returns a mutable reference to the model parameters - pub const fn params_mut(&mut self) -> &mut DeepModelParams { - &mut self.params - } - /// set the current configuration and return a mutable reference to the model - pub fn set_config(&mut self, config: StandardModelConfig) -> &mut Self { - self.config = config; - self - } - /// set the current features and return a mutable reference to the model - pub fn set_features(&mut self, features: ModelFeatures) -> &mut Self { - self.features = features; - self - } - /// set the current parameters and return a mutable reference to the model - pub fn set_params(&mut self, params: DeepModelParams) -> &mut Self { - self.params = params; - self - } - /// consumes the current instance to create another with the given configuration - pub fn with_config(self, config: StandardModelConfig) -> Self { - Self { config, ..self } - } - /// consumes the current instance to create another with the given features - pub fn with_features(self, features: ModelFeatures) -> Self { - Self { features, ..self } - } - /// consumes the current instance to create another with the given parameters - pub fn with_params(self, params: DeepModelParams) -> Self { - Self { params, ..self } - } -} - -impl TransformerModel -where - T: 'static + Float + FromPrimitive, -{ - #[cfg(feature = "rand")] - pub fn init(self) -> Self - where - T: 'static + Float + FromPrimitive, - StandardNormal: Distribution, - { - let params = DeepModelParams::glorot_normal(self.features()); - TransformerModel { params, ..self } - } -} - -impl Model for TransformerModel { - type Config = StandardModelConfig; - type Layout = ModelFeatures; - - fn config(&self) -> &StandardModelConfig { - &self.config - } - - fn config_mut(&mut self) -> &mut StandardModelConfig { - &mut self.config - } - - fn layout(&self) -> &ModelFeatures { - &self.features - } - - fn params(&self) -> &DeepModelParams { - &self.params - } - - fn params_mut(&mut self) -> &mut DeepModelParams { - &mut self.params - } -} - -impl Forward for TransformerModel -where - A: Float + FromPrimitive + ScalarOperand, - V: ReLUActivation + SigmoidActivation, - Params: Forward + Forward, - for<'a> &'a U: Dot, Output = V> + core::ops::Add<&'a Array1>, - V: for<'a> core::ops::Add<&'a Array1, Output = V>, -{ - type Output = V; - - fn forward(&self, input: &U) -> Self::Output { - let mut output = self.params().input().forward(input).relu(); - - for layer in self.params().hidden() { - output = layer.forward(&output).relu(); - } - - output = self.params().output().forward(&output).sigmoid(); - output - } -} - -impl Train, ArrayBase> for TransformerModel -where - A: Float + FromPrimitive + NumAssign + ScalarOperand + core::fmt::Debug, - S: Data, - T: Data, -{ - type Error = anyhow::Error; - type Output = A; - - #[cfg_attr( - feature = "tracing", - tracing::instrument( - skip(self, input, target), - level = "trace", - name = "backward", - target = "model", - ) - )] - fn train( - &mut self, - input: &ArrayBase, - target: &ArrayBase, - ) -> Result { - if input.len() != self.features().input() { - anyhow::bail!( - "Invalid input shape: expected {}, got {}", - self.features().input(), - input.len() - ); - } - if target.len() != self.features().output() { - anyhow::bail!( - "Invalid target shape: expected {}, got {}", - self.features().output(), - target.len() - ); - } - // get the learning rate from the model's configuration - let lr = self - .config() - .learning_rate() - .copied() - .unwrap_or(A::from_f32(0.01).unwrap()); - // Normalize the input and target - let input = input / input.l2_norm(); - let target_norm = target.l2_norm(); - let target = target / target_norm; - // self.prev_target_norm = Some(target_norm); - // Forward pass to collect activations - let mut activations = Vec::new(); - activations.push(input.to_owned()); - - let mut output = self.params().input().forward(&input).relu(); - activations.push(output.to_owned()); - // collect the activations of the hidden - for layer in self.params().hidden() { - output = layer.forward(&output).relu(); - activations.push(output.to_owned()); - } - - output = self.params().output().forward(&output).sigmoid(); - activations.push(output.to_owned()); - - // Calculate output layer error - let error = &target - &output; - let loss = error.pow2().mean().unwrap_or(A::zero()); - #[cfg(feature = "tracing")] - tracing::trace!("Training loss: {loss:?}"); - let mut delta = error * output.sigmoid_derivative(); - delta /= delta.l2_norm(); // Normalize the delta to prevent exploding gradients - - // Update output weights - self.params_mut() - .output_mut() - .backward(activations.last().unwrap(), &delta, lr); - - let num_hidden = self.features().layers(); - // Iterate through hidden layers in reverse order - for i in (0..num_hidden).rev() { - // Calculate error for this layer - delta = if i == num_hidden - 1 { - // use the output activations for the final hidden layer - self.params().output().weights().dot(&delta) * activations[i + 1].relu_derivative() - } else { - // else; backpropagate using the previous hidden layer - self.params().hidden()[i + 1].weights().t().dot(&delta) - * activations[i + 1].relu_derivative() - }; - // Normalize delta to prevent exploding gradients - delta /= delta.l2_norm(); - self.params_mut().hidden_mut()[i].backward(&activations[i + 1], &delta, lr); - } - /* - Backpropagate to the input layer - The delta for the input layer is computed using the weights of the first hidden layer - and the derivative of the activation function of the first hidden layer. - - (h, h).dot(h) * derivative(h) = dim(h) where h is the number of features within a hidden layer - */ - delta = self.params().hidden()[0].weights().dot(&delta) * activations[1].relu_derivative(); - delta /= delta.l2_norm(); // Normalize the delta to prevent exploding gradients - self.params_mut() - .input_mut() - .backward(&activations[1], &delta, lr); - - Ok(loss) - } -} - -impl Train, ArrayBase> for TransformerModel -where - A: Float + FromPrimitive + NumAssign + ScalarOperand + core::fmt::Debug, - S: Data, - T: Data, -{ - type Error = anyhow::Error; - type Output = A; - - #[cfg_attr( - feature = "tracing", - tracing::instrument( - skip(self, input, target), - level = "trace", - name = "train", - target = "model", - fields(input_shape = ?input.shape(), target_shape = ?target.shape()) - ) - )] - fn train( - &mut self, - input: &ArrayBase, - target: &ArrayBase, - ) -> Result { - if input.nrows() == 0 || target.nrows() == 0 { - anyhow::bail!("Input and target batches must have at least one sample each"); - } - if input.ncols() != self.features().input() { - anyhow::bail!( - "Invalid input shape: expected {}, got {}", - self.features().input(), - input.ncols() - ); - } - if target.ncols() != self.features().output() || target.nrows() != input.nrows() { - anyhow::bail!( - "Invalid target shape: expected ({}, {}), got ({}, {})", - input.nrows(), - self.features().output(), - target.nrows(), - target.ncols() - ); - } - let mut loss = A::zero(); - - for (_i, (x, e)) in input.rows().into_iter().zip(target.rows()).enumerate() { - loss += match Train::, ArrayView1>::train(self, &x, &e) { - Ok(l) => l, - Err(err) => { - #[cfg(feature = "tracing")] - tracing::error!( - "Training failed for batch {}/{}: {:?}", - _i + 1, - input.nrows(), - err - ); - return Err(err); - } - }; - } - - Ok(loss) - } -} diff --git a/ext/tests/attention.rs b/ext/tests/attention.rs deleted file mode 100644 index 7b819932..00000000 --- a/ext/tests/attention.rs +++ /dev/null @@ -1,17 +0,0 @@ -/* - appellation: attention - authors: @FL03 -*/ -use concision_ext::attention::{Qkv, ScaledDotProductAttention}; - -#[test] -fn test_scaled_dot_product_attention() { - let (m, n) = (7, 10); - let qkv = Qkv::::ones((m, n)); - // initialize the scaled dot-product attention layer - let layer = ScaledDotProductAttention::::new(0.1, 1.0); - // compute the attention scores - let z_score = layer.attention(&qkv); - // verify the output dimensions - assert_eq!(z_score.shape(), &[m, n]); -} diff --git a/ext/tests/snn.rs b/ext/tests/snn.rs deleted file mode 100644 index ef208164..00000000 --- a/ext/tests/snn.rs +++ /dev/null @@ -1,48 +0,0 @@ -/* - Appellation: snn - Created At: 2025.11.26:15:42:45 - Contrib: @FL03 -*/ -use approx::assert_abs_diff_eq; -use concision_ext::snn::LIFNeuron; - -#[test] -fn test_snn_neuron_resting_no_input() { - let mut n = LIFNeuron::default(); - let dt = 1.0; - // simulate 100 ms with no input -> should not spike and v near v_rest - for _ in 0..100 { - let res = n.step(dt, 0.0); - assert!(!res.is_spiked()); - } - let v = n.membrane_potential(); - assert_abs_diff_eq!(v, &n.v_rest); -} - -#[test] -fn test_snn_neuron_spikes() { - // params - let dt = 1f64; - let i_ext = 50f64; // large i_ext to force spiking - // neuron - let mut n = LIFNeuron::default(); - let mut spiked = false; - let mut steps = 0usize; - // run until spiked or max steps reached - while !spiked && steps < 1000 { - spiked = n.step(dt, i_ext).is_spiked(); - steps += 1; - } - assert!( - spiked, - "Neuron did not spike under a strong current (i_ext = {i_ext})" - ); -} - -#[test] -fn test_snn_neuron_synaptic_state_change() { - let mut n = LIFNeuron::default(); - let before = *n.synaptic_state(); - n.apply_spike(2.5); - assert!(*n.synaptic_state() > before); -} diff --git a/flake.lock b/flake.lock index e42d3e3e..f56e07c4 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1764389371, - "narHash": "sha256-Bq3kTfPl2q5pbJypVESXB1iYlcg35M7DWrmUS/aV27I=", + "lastModified": 1766807290, + "narHash": "sha256-7sgUQMKMDvpeu9cwE7mWJfP9N3wJduunmJ/s8LOd9nk=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "7d9be7b1c63840e80f7518979518e89a596a0060", + "rev": "4eec65df50d796a8fd9c146a09b5007916ce376b", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 7830c5b6..94c1ca8e 100644 --- a/flake.nix +++ b/flake.nix @@ -15,8 +15,8 @@ { packages.default = rustPlatform.buildRustPackage { pname = "concision"; - version = "0.3.0"; - src = self; # "./."; + version = "0.3.1"; + src = self; # If Cargo.lock doesn't exist yet, remove or comment out this block: cargoLock = { lockFile = ./Cargo.lock; diff --git a/init/Cargo.toml b/init/Cargo.toml index 2afe0370..0b526dc8 100644 --- a/init/Cargo.toml +++ b/init/Cargo.toml @@ -1,6 +1,6 @@ [package] build = "build.rs" -description = "tthis crate provides various random distribution and initialization routines for the concision framework" +description = "this crate provides various random distribution and initialization routines for the concision framework" name = "concision-init" authors.workspace = true @@ -16,7 +16,7 @@ version.workspace = true [package.metadata.docs.rs] all-features = false -features = ["full"] +features = ["default"] rustc-args = ["--cfg", "docsrs"] [package.metadata.release] @@ -25,92 +25,113 @@ tag-name = "v{{version}}" [lib] bench = false -crate-type = ["cdylib", "rlib"] -doc = true -doctest = true -test = true [dependencies] -# data & serialization +# data-structures +hashbrown = { optional = true, workspace = true } +ndarray = { workspace = true } +# serialization serde = { features = ["derive"], optional = true, workspace = true } serde_derive = { optional = true, workspace = true } -serde_json = { optional = true, workspace = true } # error-handling +anyhow = { workspace = true } thiserror = { workspace = true } -# logging -tracing = { optional = true, workspace = true } # macros & utilities paste = { workspace = true } smart-default = { workspace = true } strum = { workspace = true } # mathematics approx = { optional = true, workspace = true } -ndarray = { workspace = true } -num = { workspace = true } num-complex = { optional = true, workspace = true } num-traits = { workspace = true } # random -getrandom = { default-features = false, optional = true, workspace = true } -rand = { optional = true, workspace = true } -rand_distr = { optional = true, workspace = true } +getrandom = { workspace = true } +rand = { features = ["os_rng", "small_rng"], workspace = true } +rand_distr = { workspace = true } +# WebAssembly (wasm) +wasm-bindgen = { optional = true, workspace = true } [dev-dependencies] lazy_static = { workspace = true } [features] -default = ["rand", "std"] - -full = ["approx", "complex", "default", "serde", "tracing"] +default = ["std"] + +full = [ + "default", + "approx", + "complex", + "hashbrown", + "serde", +] -nightly = [] +nightly = [ + "hashbrown?/nightly", + "rand/nightly", +] # ************* [FF:Dependencies] ************* std = [ "alloc", + "anyhow/std", + "getrandom/std", + "hashbrown?/default", "ndarray/std", "num-complex?/std", "num-traits/std", - "num/std", "rand/std", "rand/std_rng", - "serde/std", + "rand/thread_rng", + "rand_distr/std", + "serde?/std", "strum/std", "thiserror/std", - "tracing?/std", ] wasi = [] -wasm = ["getrandom?/wasm_js"] +wasm = [ + "wasm_bindgen", + "getrandom/wasm_js" +] + # ************* [FF:Dependencies] ************* -alloc = ["num/alloc", "serde?/alloc"] -approx = ["dep:approx", "ndarray/approx"] +alloc = [ + "hashbrown?/alloc", + "rand/alloc", + "rand_distr/alloc", + "serde?/alloc", +] + +approx = [ + "dep:approx", + "ndarray/approx", +] blas = ["ndarray/blas"] -complex = ["dep:num-complex"] +complex = [ + "dep:num-complex", + "num-complex?/rand", +] -rand = ["dep:rand", "dep:rand_distr", "num-complex?/rand", "num/rand", "rng"] +hashbrown = ["dep:hashbrown"] -rng = ["dep:getrandom", "rand?/small_rng", "rand?/thread_rng"] +rayon = [ + "hashbrown?/rayon", + "ndarray/rayon", +] serde = [ "dep:serde", "dep:serde_derive", + "serde?/derive", + "hashbrown?/serde", "ndarray/serde", "num-complex?/serde", - "num/serde", - "rand?/serde", - "rand_distr?/serde", + "rand/serde", + "rand_distr/serde", ] -tracing = ["dep:tracing"] - -# ************* [Unit Tests] ************* -[[test]] -name = "default" - -[[test]] -name = "init" -required-features = ["rand", "std"] +wasm_bindgen = ["dep:wasm-bindgen"] diff --git a/init/src/distr/lecun.rs b/init/src/distr/lecun.rs index 4667a2c2..db7af7e0 100644 --- a/init/src/distr/lecun.rs +++ b/init/src/distr/lecun.rs @@ -10,9 +10,9 @@ use rand_distr::{Distribution, StandardNormal}; /// [LecunNormal] is a truncated [normal](rand_distr::Normal) distribution centered at 0 /// with a standard deviation that is calculated as: /// -/// $$ -/// \sigma = {n_{in}}^{-\frac{1}{2}} -/// $$ +/// ```math +/// \sigma=\sqrt\frac{1}{n_{in}} +/// ``` /// /// where $`n_{in}`$ is the number of input units. #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] @@ -26,19 +26,18 @@ impl LecunNormal { } /// Create a [truncated normal](TruncatedNormal) [distribution](Distribution) centered at 0; /// See [Self::std_dev] for the standard deviation calculations. - pub fn distr(&self) -> crate::InitResult> + pub fn distr(&self) -> crate::Result> where F: Float, StandardNormal: Distribution, { TruncatedNormal::new(F::zero(), self.std_dev()) } - /// Calculate the standard deviation ($`\sigma`$) of the distribution. - /// This is done by computing the root of the reciprocal of the number of inputs - /// ($`n_{in}`$) as follows: + /// compute the standard deviation ($`\sigma`$) of the distribution by calculating the + /// root of the reciprocal of the number of inputs. /// /// ```math - /// \sigma = {n_{in}}^{-\frac{1}{2}} + /// \sigma=\sqrt\frac{1}{n_{in}} /// ``` pub fn std_dev(&self) -> F where diff --git a/init/src/distr/trunc.rs b/init/src/distr/trunc.rs index b48bcff7..c6a3627e 100644 --- a/init/src/distr/trunc.rs +++ b/init/src/distr/trunc.rs @@ -2,13 +2,13 @@ Appellation: trunc Contrib: FL03 */ -use num::traits::Float; +use num_traits::Float; use rand::{Rng, RngCore}; use rand_distr::{Distribution, Normal, StandardNormal}; /// The [`TruncatedNormal`] distribution is similar to the [`StandardNormal`] distribution, -/// differing in that is computes a boundary equal to two standard deviations from the mean. -/// More formally, the boundary is defined as: +/// differing in that is computes a boundary equal to two standard deviations from the mean +/// ($`\mu`$). More formally, the boundary is defined as: /// /// ```math /// \text{boundary} = \mu + 2\sigma @@ -29,7 +29,7 @@ where { /// create a new [`TruncatedNormal`] distribution with the given mean and standard /// deviation; both of which are type `T`. - pub const fn new(mean: T, std: T) -> crate::InitResult { + pub const fn new(mean: T, std: T) -> crate::Result { Ok(Self { mean, std }) } /// returns a copy of the mean for the distribution @@ -42,12 +42,12 @@ where } /// compute the boundary of the truncated normal distribution /// which is two standard deviations from the mean: - /// $$ - /// \text{boundary} = \mu + 2\sigma - /// $$ + /// ```math + /// \text{boundary}=\mu + 2\sigma + /// ``` pub fn boundary(&self) -> T where - T: Float, + T: Float + core::ops::Mul + core::ops::Add, { self.mean() + self.std_dev() * T::from(2).unwrap() } @@ -61,14 +61,14 @@ where } /// compute the score of the distribution at point `x`. The score is calculated by /// subtracing a scaled standard deviation from the mean: - /// $$ - /// \text{score}(x) = \mu - \sigma \cdot x - /// $$ + /// ```math + /// \text{score}(x)=\mu-\sigma\cdot{x} + /// ``` /// /// where $\mu$ is the mean and $\sigma$ is the standard deviation. pub fn score(&self, x: T) -> T where - T: Float, + T: core::ops::Mul + core::ops::Sub, { self.mean() - self.std_dev() * x } @@ -92,6 +92,30 @@ where x } } +// impl Distribution for TruncatedNormal +// where +// T: Copy +// + PartialOrd +// + FromPrimitive +// + core::ops::Add +// + core::ops::Mul +// + core::ops::Sub +// + core::ops::Neg, +// StandardNormal: Distribution, +// { +// fn sample(&self, rng: &mut R) -> T +// where +// R: RngCore + ?Sized, +// { +// let bnd = self.boundary(); +// let mut x = self.score(rng.sample(StandardNormal)); +// // if x is outside of the boundary, re-sample +// while x < -bnd || x > bnd { +// x = self.score(rng.sample(StandardNormal)); +// } +// x +// } +// } impl From> for TruncatedNormal where diff --git a/init/src/distr/xavier.rs b/init/src/distr/xavier.rs index 6f22a4c0..d1c57e45 100644 --- a/init/src/distr/xavier.rs +++ b/init/src/distr/xavier.rs @@ -68,7 +68,7 @@ mod impl_normal { } /// tries creating a new [`Normal`] distribution with a mean of 0 and the computed /// standard deviation ($\sigma$) based on the number of inputs and outputs. - pub fn distr(&self) -> crate::InitResult> { + pub fn distr(&self) -> crate::Result> { Ok(Normal::new(T::zero(), self.std_dev())?) } /// returns a reference to the standard deviation of the distribution @@ -111,7 +111,7 @@ mod impl_uniform { where T: SampleUniform, { - pub fn new(inputs: usize, outputs: usize) -> crate::InitResult + pub fn new(inputs: usize, outputs: usize) -> crate::Result where T: Float + FromPrimitive, { diff --git a/init/src/error.rs b/init/src/error.rs index 2349efda..6f1eab70 100644 --- a/init/src/error.rs +++ b/init/src/error.rs @@ -8,7 +8,7 @@ use alloc::string::String; /// a type alias for a [`Result`](core::result::Result) type that is used throughout /// the library using an [`InitError`] as the error type. -pub type InitResult = core::result::Result; +pub type Result = core::result::Result; /// The [`InitError`] type enumerates various initialization errors while integrating with the /// external errors largely focused on randomization. @@ -18,34 +18,26 @@ pub enum InitError { #[cfg(feature = "alloc")] #[error("Failed to initialize with the given distribution: {0}")] DistributionError(String), - #[cfg(feature = "rng")] #[error(transparent)] RngError(#[from] getrandom::Error), - #[cfg(feature = "rand")] #[error("[NormalError]: {0}")] NormalError(rand_distr::NormalError), #[error(transparent)] - #[cfg(feature = "rand")] UniformError(#[from] rand_distr::uniform::Error), #[error("[WeibullError]: {0}")] - #[cfg(feature = "rand")] WeibullError(rand_distr::WeibullError), } -#[cfg(feature = "rand")] -mod rand_err { - use super::InitError; - use rand_distr::{NormalError, WeibullError}; +use rand_distr::{NormalError, WeibullError}; - impl From for InitError { - fn from(err: rand_distr::NormalError) -> Self { - InitError::NormalError(err) - } +impl From for InitError { + fn from(err: rand_distr::NormalError) -> Self { + InitError::NormalError(err) } +} - impl From for InitError { - fn from(err: rand_distr::WeibullError) -> Self { - InitError::WeibullError(err) - } +impl From for InitError { + fn from(err: rand_distr::WeibullError) -> Self { + InitError::WeibullError(err) } } diff --git a/init/src/lib.rs b/init/src/lib.rs index ab54d424..ea4145c0 100644 --- a/init/src/lib.rs +++ b/init/src/lib.rs @@ -1,19 +1,7 @@ -/* - Appellation: concision-init - Contrib: FL03 -*/ -//! Initialization related tools and utilities for neural networks and machine learning models. -//! This crate provides various initialization distributions and traits to facilitate -//! the effective initialization of model parameters. -//! -//! ## Features -//! -//! - `rand`: Enables random number generation functionalities using the `rand` crate. -//! -//! Implementors of the [`Initialize`] trait can leverage the various initialization -//! distributions provided within this crate to initialize their model parameters in a -//! manner conducive to effective training and convergence. +//! Custom random number distributions and initializers focused on neural networks. //! +#![crate_name = "concision_init"] +#![crate_type = "lib"] #![allow( clippy::missing_safety_doc, clippy::module_inception, @@ -23,82 +11,51 @@ rustdoc::redundant_explicit_links )] #![cfg_attr(not(feature = "std"), no_std)] - -#[doc(inline)] -#[cfg(feature = "rand")] -pub use self::{distr::prelude::*, utils::*}; -#[doc(inline)] -pub use self::{error::*, traits::*}; - +#![cfg_attr(all(feature = "alloc", feature = "nightly"), feature(allocator_api))] +// compile-time checks +#[cfg(not(any(feature = "std", feature = "alloc")))] +compiler_error! { "At least one of the \"std\" or \"alloc\" features must be enabled for the crate to compile." } +// external crate #[cfg(feature = "alloc")] extern crate alloc; - -#[cfg(feature = "rand")] +// re-declarations #[doc(no_inline)] pub use rand; -#[cfg(feature = "rand")] #[doc(no_inline)] pub use rand_distr; - +// modules pub mod error; -pub(crate) mod utils { - //! this module provides various utility functions for random initialization. +pub mod distr { + //! random distributions optimized for neural network initialization. #[doc(inline)] - #[cfg(feature = "rand")] - pub use self::prelude::*; - - #[cfg(feature = "rand")] - mod rand_utils; + pub use self::{lecun::*, trunc::*, xavier::*}; - mod prelude { - #[cfg(feature = "rand")] - pub use super::rand_utils::*; - } + mod lecun; + mod trunc; + mod xavier; } mod traits { #[doc(inline)] - pub use self::prelude::*; - - mod init; - #[cfg(feature = "rand")] - mod initialize; + pub use self::ndrand::*; - mod prelude { - #[doc(inline)] - pub use super::init::*; - #[doc(inline)] - #[cfg(feature = "rand")] - pub use super::initialize::*; - } + mod ndrand; } -#[cfg(feature = "rand")] -pub mod distr { - //! this module implements various random distributions optimized for neural network - //! initialization. +mod utils { #[doc(inline)] - pub use self::{lecun::*, trunc::*, xavier::*}; + pub use self::rand_utils::*; - pub mod lecun; - pub mod trunc; - pub mod xavier; - - pub(crate) mod prelude { - pub use super::lecun::*; - pub use super::trunc::*; - pub use super::xavier::*; - } + mod rand_utils; } - +// re-exports +#[doc(inline)] +pub use self::{distr::*, error::*, traits::*, utils::*}; +// prelude #[doc(hidden)] pub mod prelude { - pub use crate::error::InitError; + pub use crate::distr::*; pub use crate::traits::*; - - #[cfg(feature = "rand")] - pub use crate::distr::prelude::*; - #[cfg(feature = "rand")] pub use crate::utils::*; } diff --git a/init/src/traits/init.rs b/init/src/traits/init.rs index ef58d36f..b19ad288 100644 --- a/init/src/traits/init.rs +++ b/init/src/traits/init.rs @@ -3,26 +3,31 @@ Contrib: @FL03 */ -#[cfg(feature = "rand")] -use rand::RngCore; -/// Initializes parameters and state from RNG and/or config. -/// Macro will implement this to produce shaped params/state. -pub trait Initialize { - type Output; - - fn init_random(rng: &mut R) -> Self::Output; -} - /// A trait for creating custom initialization routines for models or other entities. -pub trait Init { +pub trait InitWith { + type Cont; /// consumes the current instance to initialize a new one - fn init(self) -> Self + fn init_with(f: F) -> Self::Cont where + F: FnOnce() -> U, Self: Sized; } -/// This trait enables models to implement custom, in-place initialization methods. -pub trait InitInplace { - /// initialize the object in-place and return a mutable reference to it. - fn init_inplace(&mut self) -> &mut Self; +#[cfg(feature = "rand")] +/// The [`InitRand`] trait provides a generic interface for initializing objects using +/// random number generators. This trait is particularly useful for types that require +/// random initialization, such as neural network weights, biases, or other parameters. +pub trait InitRand { + type Output; + /// use the provided random number generator `rng` to initialize the object + fn init_random(rng: &mut R) -> Self::Output; +} + +/// [`Initialize`] provides a mechanism for _initializing_ some object using a value of type +/// `T` to produce another object. +pub trait Initialize { + type Output; + /// initializes the object using the given value, consuming the caller to produce another + /// object + fn init(self, with: T) -> Self::Output; } diff --git a/init/src/traits/initialize.rs b/init/src/traits/ndrand.rs similarity index 75% rename from init/src/traits/initialize.rs rename to init/src/traits/ndrand.rs index 11d9be47..0b60f32e 100644 --- a/init/src/traits/initialize.rs +++ b/init/src/traits/ndrand.rs @@ -7,7 +7,6 @@ use crate::distr::*; use core::ops::Neg; use ndarray::{ArrayBase, DataOwned, Dimension, RawData, Shape, ShapeBuilder}; use num_traits::{Float, FromPrimitive}; -use rand::rngs::{SmallRng, StdRng}; use rand::{Rng, RngCore, SeedableRng}; use rand_distr::uniform::{SampleUniform, Uniform}; use rand_distr::{Bernoulli, BernoulliError, Distribution, Normal, NormalError, StandardNormal}; @@ -22,39 +21,36 @@ where (a, b) } -#[deprecated( - since = "0.2.9", - note = "Please use the `NdInit` trait instead which provides more comprehensive functionality." -)] -pub trait InitRand: NdInit -where - D: Dimension, - S: RawData, -{ -} -/// This trait provides the base methods required for initializing tensors with random values. +/// The [`NdRandom`] trait focuses on providing an interface for initializing n-dimensional +/// tensors. Similar to the `RandomExt` trait from the `ndarray_rand` crate, it offers methods to +/// create tensors filled with random values drawn from various probability distributions. /// The trait is similar to the `RandomExt` trait provided by the `ndarray_rand` crate, /// however, it is designed to be more generic, extensible, and optimized for neural network /// initialization routines. -pub trait NdInit::Elem>: Sized +pub trait NdRandom::Elem>: Sized where D: Dimension, S: RawData, { - fn rand(shape: Sh, distr: Ds) -> Self + type Cont<_S, _D> + where + _D: Dimension, + _S: RawData; + + fn rand(shape: Sh, distr: Ds) -> Self::Cont where Ds: Distribution, Sh: ShapeBuilder, S: DataOwned; - fn rand_with(shape: Sh, distr: Ds, rng: &mut R) -> Self + fn rand_with(shape: Sh, distr: Ds, rng: &mut R) -> Self::Cont where R: RngCore + ?Sized, Ds: Distribution, Sh: ShapeBuilder, S: DataOwned; - fn bernoulli(shape: Sh, p: f64) -> Result + fn bernoulli(shape: Sh, p: f64) -> Result, BernoulliError> where Bernoulli: Distribution, S: DataOwned, @@ -64,7 +60,7 @@ where Ok(Self::rand(shape, dist)) } /// Initialize the object according to the Glorot Initialization scheme. - fn glorot_normal>(shape: Sh) -> Self + fn glorot_normal>(shape: Sh) -> Self::Cont where StandardNormal: Distribution, S: DataOwned, @@ -76,7 +72,7 @@ where Self::rand(shape, distr) } /// Initialize the object according to the Glorot Initialization scheme. - fn glorot_uniform(shape: Sh) -> crate::InitResult + fn glorot_uniform(shape: Sh) -> crate::Result> where S: DataOwned, Sh: ShapeBuilder, @@ -92,7 +88,7 @@ where /// LecunNormal distributions are truncated [Normal](rand_distr::Normal) /// distributions centered at 0 with a standard deviation equal to the /// square root of the reciprocal of the number of inputs. - fn lecun_normal(shape: Sh) -> Self + fn lecun_normal(shape: Sh) -> Self::Cont where StandardNormal: Distribution, S: DataOwned, @@ -104,7 +100,7 @@ where Self::rand(shape, distr) } /// Given a shape, mean, and standard deviation generate a new object using the [Normal](rand_distr::Normal) distribution - fn normal(shape: Sh, mean: A, std: A) -> Result + fn normal(shape: Sh, mean: A, std: A) -> Result, NormalError> where StandardNormal: Distribution, S: DataOwned, @@ -115,7 +111,7 @@ where Ok(Self::rand(shape, distr)) } #[cfg(feature = "complex")] - fn randc(shape: Sh, re: A, im: A) -> Self + fn randc(shape: Sh, re: A, im: A) -> Self::Cont where S: DataOwned, Sh: ShapeBuilder, @@ -125,7 +121,7 @@ where Self::rand(shape, &distr) } /// Generate a random array using the [StandardNormal](rand_distr::StandardNormal) distribution - fn stdnorm(shape: Sh) -> Self + fn stdnorm(shape: Sh) -> Self::Cont where StandardNormal: Distribution, S: DataOwned, @@ -133,17 +129,22 @@ where { Self::rand(shape, StandardNormal) } + #[cfg(feature = "std")] /// Generate a random array using the [`StandardNormal`] distribution with a given seed - fn stdnorm_from_seed(shape: Sh, seed: u64) -> Self + fn stdnorm_from_seed(shape: Sh, seed: u64) -> Self::Cont where StandardNormal: Distribution, S: DataOwned, Sh: ShapeBuilder, { - Self::rand_with(shape, StandardNormal, &mut StdRng::seed_from_u64(seed)) + Self::rand_with( + shape, + StandardNormal, + &mut rand::rngs::StdRng::seed_from_u64(seed), + ) } /// Initialize the object using the [`TruncatedNormal`] distribution - fn truncnorm(shape: Sh, mean: A, std: A) -> crate::InitResult + fn truncnorm(shape: Sh, mean: A, std: A) -> crate::Result> where StandardNormal: Distribution, S: DataOwned, @@ -154,7 +155,7 @@ where Ok(Self::rand(shape, distr)) } /// initialize the object using the [`Uniform`] distribution with values bounded by `+/- dk` - fn uniform(shape: Sh, dk: A) -> crate::InitResult + fn uniform(shape: Sh, dk: A) -> crate::Result> where S: DataOwned, Sh: ShapeBuilder, @@ -163,9 +164,15 @@ where { Self::uniform_between(shape, dk.clone().neg(), dk) } + #[cfg(feature = "std")] /// randomly initialize the object using the [`Uniform`] distribution with values between /// the `start` and `stop` params using some random seed. - fn uniform_from_seed(shape: Sh, start: A, stop: A, key: u64) -> crate::InitResult + fn uniform_from_seed( + shape: Sh, + start: A, + stop: A, + key: u64, + ) -> crate::Result> where S: DataOwned, Sh: ShapeBuilder, @@ -176,13 +183,13 @@ where Ok(Self::rand_with( shape, distr, - &mut StdRng::seed_from_u64(key), + &mut rand::rngs::StdRng::seed_from_u64(key), )) } /// initialize the object using the [`Uniform`] distribution with values bounded by the /// size of the specified axis. /// The values are bounded by `+/- dk` where `dk = 1 / size(axis)`. - fn uniform_along(shape: Sh, axis: usize) -> crate::InitResult + fn uniform_along(shape: Sh, axis: usize) -> crate::Result> where Sh: ShapeBuilder, S: DataOwned, @@ -197,7 +204,7 @@ where } /// initialize the object using the [`Uniform`] distribution with values between then given /// bounds, `a` and `b`. - fn uniform_between(shape: Sh, a: A, b: A) -> crate::InitResult + fn uniform_between(shape: Sh, a: A, b: A) -> crate::Result> where Sh: ShapeBuilder, S: DataOwned, @@ -212,21 +219,31 @@ where ************ Implementations ************ */ -impl NdInit for ArrayBase +impl NdRandom for ArrayBase where D: Dimension, S: RawData, { - fn rand(shape: Sh, distr: Ds) -> Self + type Cont<_S, _D> + = ArrayBase<_S, _D, A> + where + _D: Dimension, + _S: RawData; + + fn rand(shape: Sh, distr: Ds) -> Self::Cont where Ds: Distribution, Sh: ShapeBuilder, S: DataOwned, { - Self::rand_with(shape, distr, &mut SmallRng::from_rng(&mut rand::rng())) + Self::rand_with( + shape, + distr, + &mut rand::rngs::SmallRng::from_rng(&mut rand::rng()), + ) } - fn rand_with(shape: Sh, distr: Ds, rng: &mut R) -> Self + fn rand_with(shape: Sh, distr: Ds, rng: &mut R) -> Self::Cont where R: Rng + ?Sized, Ds: Distribution, diff --git a/init/src/utils/rand_utils.rs b/init/src/utils/rand_utils.rs index fbb7e15c..08fafa46 100644 --- a/init/src/utils/rand_utils.rs +++ b/init/src/utils/rand_utils.rs @@ -2,25 +2,24 @@ Appellation: utils Contrib: FL03 */ -use crate::NdInit; -use ndarray::{Array, ArrayBase, DataOwned, Dimension, IntoDimension, RawData, ShapeBuilder}; -use num::Num; -use num::complex::{Complex, ComplexDistribution}; +use crate::NdRandom; +use ndarray::{Array, ArrayBase, DataOwned, Dimension, IntoDimension, ShapeBuilder}; use rand::{SeedableRng, rngs}; use rand_distr::{ Distribution, StandardNormal, uniform::{SampleUniform, Uniform}, }; +#[cfg(feature = "complex")] /// Generate a random array of complex numbers with real and imaginary parts in the range [0, 1) pub fn randc(shape: impl IntoDimension) -> ArrayBase where - A: Clone + Num, + A: Clone + num_traits::Num, D: Dimension, - S: RawData + DataOwned>, - ComplexDistribution: Distribution, + S: DataOwned>, + num_complex::ComplexDistribution: Distribution, { - let distr = ComplexDistribution::::new(A::one(), A::one()); + let distr = num_complex::ComplexDistribution::::new(A::one(), A::one()); ArrayBase::rand(shape, distr) } @@ -54,7 +53,7 @@ pub fn uniform_from_seed( start: T, stop: T, shape: impl IntoDimension, -) -> crate::InitResult> +) -> crate::Result> where D: Dimension, T: SampleUniform, diff --git a/init/tests/init.rs b/init/tests/distr.rs similarity index 91% rename from init/tests/init.rs rename to init/tests/distr.rs index 0ceee1fb..e3eab444 100644 --- a/init/tests/init.rs +++ b/init/tests/distr.rs @@ -2,10 +2,8 @@ Appellation: random Contrib: FL03 */ -extern crate concision_init as cnc; - -use cnc::NdInit; -use cnc::distr::LecunNormal; +use concision_init::NdRandom; +use concision_init::distr::LecunNormal; use ndarray::prelude::*; #[test] diff --git a/macros/src/impls/model.rs b/macros/src/impls/model.rs index 4909b81a..b41c35ba 100644 --- a/macros/src/impls/model.rs +++ b/macros/src/impls/model.rs @@ -22,8 +22,7 @@ pub fn impl_model( // if edges are defined, but no nodes, return an error if layers.is_some() && params.is_none() { return syn::Error::new_spanned(model, "edges cannot be defined without nodes") - .to_compile_error() - .into(); + .to_compile_error(); } // initialize a vector to hold the statements let mut stmts = Vec::new(); diff --git a/macros/src/lib.rs b/macros/src/lib.rs index edcbaf37..7afedc26 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -15,13 +15,14 @@ pub(crate) mod kw; use self::ast::ModelAst; use proc_macro::TokenStream; +use syn::parse_macro_input; #[proc_macro] /// [`model_config!`] is a procedural macro used to define the configuration for a model in the /// `concision` framework. It allows users to specify various parameters and settings for the model -/// in a concise and structured manner, declaring a name for their instanc +/// in a concise and structured manner, declaring a name for their instance pub fn model_config(input: TokenStream) -> TokenStream { - let data = syn::parse_macro_input!(input as ast::ConfigAst); + let data = parse_macro_input! { input as ast::ConfigAst }; // use the handler to process the input data let res = impls::impl_model_config(data); // convert the tokens into a TokenStream @@ -32,7 +33,32 @@ pub fn model_config(input: TokenStream) -> TokenStream { /// the [`model!`]procedural macro is used to streamline the creation of custom models using the /// `concision` framework pub fn model(input: TokenStream) -> TokenStream { - let data = syn::parse_macro_input!(input as ModelAst); + let data = parse_macro_input! { input as ModelAst }; + // use the handler to process the input data + let res = impls::impl_model(data); + // convert the tokens into a TokenStream + res.into() +} + +#[proc_macro] +/// [`nn!`] is a procedural macro designed to streamline the process of creating new neural +/// networks; +/// +/// ```ignore +/// nn! { +/// MyNeuralNetwork { +/// layout: { input: 128, output: 10 }, +/// params: { +/// learning_rate: 0.01, +/// epochs: 100, +/// ... +/// }, +/// layers: [Linear, ReLU, ..., Linear], +/// } +/// } +/// ``` +pub fn nn(input: TokenStream) -> TokenStream { + let data = parse_macro_input!(input as ModelAst); // use the handler to process the input data let res = impls::impl_model(data); // convert the tokens into a TokenStream diff --git a/params/Cargo.toml b/params/Cargo.toml index 431c1f14..4d8ca863 100644 --- a/params/Cargo.toml +++ b/params/Cargo.toml @@ -16,7 +16,7 @@ version.workspace = true [package.metadata.docs.rs] all-features = false -features = ["full"] +features = ["default"] rustc-args = ["--cfg", "docsrs"] [package.metadata.release] @@ -25,17 +25,15 @@ tag-name = "v{{version}}" [lib] bench = false -crate-type = ["cdylib", "rlib"] -doctest = false -test = true [dependencies] -concision-init = { workspace = true } +concision-init = { optional = true, workspace = true } concision-traits = { workspace = true } # custom +rspace-traits = { workspace = true } variants = { workspace = true } # concurrency & parallelism -rayon-core = { optional = true, workspace = true } +rayon = { optional = true, workspace = true } # data & serialization serde = { features = ["derive"], optional = true, workspace = true } serde_derive = { optional = true, workspace = true } @@ -48,114 +46,126 @@ approx = { optional = true, workspace = true } ndarray = { workspace = true } num-complex = { optional = true, workspace = true } num-traits = { workspace = true } -# random -getrandom = { default-features = false, optional = true, workspace = true } -rand = { optional = true, workspace = true } -rand_distr = { optional = true, workspace = true } +# WebAssembly (wasm) +wasm-bindgen = { optional = true, workspace = true } [features] default = ["std"] full = [ + "default", "approx", "complex", - "default", - "json", "rand", + "json", "serde", ] +# ********* [FF] Features ********* +autodiff = [ + "nightly", + "concision-traits/autodiff" +] + +json = [ + "alloc", + "serde", + "serde_json", +] + nightly = [ - "concision-init/nightly", + "concision-init?/nightly", "concision-traits/nightly", ] -# ************* [FF:Dependencies] ************* +# ********* [FF] Environments ********* + std = [ "alloc", "anyhow/std", - "concision-init/std", + "concision-init?/std", "concision-traits/std", "ndarray/std", "num-complex?/std", "num-traits/std", - "rand?/std", - "rand?/std_rng", - "serde/std", + "rspace-traits/std", + "serde?/std", "thiserror/std", "variants/std", ] wasi = [ - "concision-init/wasi", + "concision-init?/wasi", "concision-traits/wasi", + "rspace-traits/wasi", ] wasm = [ - "concision-init/wasm", + "wasm_bindgen", + "concision-init?/wasm", "concision-traits/wasm", - "getrandom?/wasm_js", - "rayon-core?/web_spin_lock", + "rayon?/web_spin_lock", + "rspace-traits/wasm", ] -# ************* [FF:Dependencies] ************* +# ********* [FF] Dependencies ********* alloc = [ - "concision-init/alloc", + "concision-init?/alloc", "concision-traits/alloc", + "rspace-traits/alloc", "serde?/alloc", "serde_json?/alloc", "variants/alloc", ] approx = [ - "concision-init/approx", "dep:approx", + "concision-init?/approx", "ndarray/approx", ] blas = [ - "concision-init/blas", + "concision-init?/blas", "ndarray/blas", ] complex = [ - "concision-init/complex", "dep:num-complex", + "concision-init?/complex", + "concision-traits/complex", + "rspace-traits/complex", ] -json = ["alloc", "serde", "serde_json"] - -rand = [ - "concision-init/rand", +rand = [ + "dep:concision-init", "concision-traits/rand", - "dep:rand", - "dep:rand_distr", "num-complex?/rand", - "rng", + "rspace-traits/rand", ] rayon = [ - "dep:rayon-core", + "dep:rayon", "ndarray/rayon", -] - -rng = [ - "concision-init/rng", - "concision-traits/rng", - "dep:getrandom", - "rand?/small_rng", - "rand?/thread_rng", + "concision-init?/rayon", + "rspace-traits/rayon", ] serde = [ - "concision-init/serde", "dep:serde", "dep:serde_derive", + "concision-init?/serde", + "concision-traits/serde", "ndarray/serde", "num-complex?/serde", - "rand?/serde", - "rand_distr?/serde", + "rspace-traits/serde", ] serde_json = ["dep:serde_json"] + +wasm_bindgen = [ + "dep:wasm-bindgen", + "concision-init?/wasm_bindgen", + "concision-traits/wasm_bindgen", + "rspace-traits/wasm_bindgen", +] \ No newline at end of file diff --git a/params/src/impls/impl_params.rs b/params/src/impls/impl_params.rs index 7186aaba..c524cb19 100644 --- a/params/src/impls/impl_params.rs +++ b/params/src/impls/impl_params.rs @@ -1,176 +1,340 @@ /* - appellation: impl_params - authors: @FL03 + Appellation: impl_params + Created At: 2026.01.13:18:36:16 + Contrib: @FL03 */ +use crate::Params; use crate::params_base::ParamsBase; -use crate::traits::{Biased, Weighted}; -use core::iter::Once; -use ndarray::{ArrayBase, Data, DataOwned, Dimension, Ix1, Ix2, RawData}; +use crate::utils::extract_bias_dim; +use ndarray::{ + ArrayBase, Axis, Data, DataMut, DataOwned, Dimension, LayoutRef, RawData, RawRef, RemoveAxis, + ShapeArg, ShapeBuilder, +}; -impl ParamsBase +impl ParamsBase where + D: Dimension, S: RawData, { - /// returns a new instance of the [`ParamsBase`] initialized using a _scalar_ bias along - /// with the given, one-dimensional weight tensor. - pub fn from_scalar_bias(bias: A, weights: ArrayBase) -> Self + /// create a new instance of the [`ParamsBase`] with the given bias and weights + pub const fn new(bias: ArrayBase, weights: ArrayBase) -> Self { + Self { bias, weights } + } + /// returns a new instance of the [`ParamsBase`] using the initialization routine + pub fn init_from_fn(shape: Sh, init: F) -> Self where A: Clone, + D: RemoveAxis, + S: DataOwned, + Sh: ShapeBuilder, + F: Fn() -> A, + { + let weights = ArrayBase::from_shape_fn(shape, |_| init()); + // initialize the bias using a shape that is 1 rank lower then the weights + let bias = ArrayBase::from_shape_fn(extract_bias_dim(&weights), |_| init()); + // create a new instance from the generated bias and weights + Self::new(bias, weights) + } + /// returns a new instance of the [`ParamsBase`] initialized use the given shape_function + pub fn from_shape_fn(shape: Sh, w: F1, b: F2) -> Self + where + A: Clone, + D: RemoveAxis, + S: DataOwned, + Sh: ShapeBuilder, + D::Smaller: Dimension + ShapeArg, + F1: Fn(::Pattern) -> A, + F2: Fn(::Pattern) -> A, + { + // initialize the weights with some shape using the given function + let weights = ArrayBase::from_shape_fn(shape, w); + // initialize the bias tensor w.r.t. the weights + let bias = ArrayBase::from_shape_fn(extract_bias_dim(&weights), b); + // return a new instance + Self::new(bias, weights) + } + /// create a new instance of the [`ParamsBase`] with the given bias used the default weights + pub fn from_bias(shape: Sh, bias: ArrayBase) -> Self + where + A: Clone + Default, + D: RemoveAxis, S: DataOwned, + Sh: ShapeBuilder, { - Self { - bias: ArrayBase::from_elem((), bias), - weights, + let weights = ArrayBase::from_elem(shape, A::default()); + let bdim = extract_bias_dim(&weights); + if bias.raw_dim() != bdim { + panic!("the given bias shape is invalid"); } + Self::new(bias, weights) } - /// returns the number of rows in the weights matrix - pub fn nrows(&self) -> usize { - self.weights().len() + /// create a new instance of the [`ParamsBase`] with the given weights used the default + /// bias + pub fn from_weights(weights: ArrayBase) -> Self + where + A: Clone + Default, + D: RemoveAxis, + S: DataOwned, + { + let bias = ArrayBase::from_elem(extract_bias_dim(&weights), A::default()); + Self::new(bias, weights) } -} - -impl ParamsBase -where - S: RawData, -{ - /// returns the number of columns in the weights matrix - pub fn ncols(&self) -> usize { - self.weights().ncols() + /// create a new instance of the [`ParamsBase`] from the given shape and element; + pub fn from_elem>(shape: Sh, elem: A) -> Self + where + A: Clone, + D: RemoveAxis, + S: DataOwned, + { + let weights = ArrayBase::from_elem(shape, elem.clone()); + let bias = ArrayBase::from_elem(extract_bias_dim(&weights), elem); + Self::new(bias, weights) } - /// returns the number of rows in the weights matrix - pub fn nrows(&self) -> usize { - self.weights().nrows() + #[allow(clippy::should_implement_trait)] + /// create an instance of the parameters with all values set to the default value + pub fn default(shape: Sh) -> Self + where + A: Clone + Default, + D: RemoveAxis, + S: DataOwned, + Sh: ShapeBuilder, + { + Self::from_elem(shape, A::default()) } -} - -impl Weighted for ParamsBase -where - S: RawData, - D: Dimension, -{ - fn weights(&self) -> &ArrayBase { - self.weights() + /// initialize the parameters with all values set to zero + pub fn ones(shape: Sh) -> Self + where + A: Clone + num_traits::One, + D: RemoveAxis, + S: DataOwned, + Sh: ShapeBuilder, + { + Self::from_elem(shape, A::one()) } - - fn weights_mut(&mut self) -> &mut ArrayBase { - self.weights_mut() + /// create an instance of the parameters with all values set to zero + pub fn zeros(shape: Sh) -> Self + where + A: Clone + num_traits::Zero, + D: RemoveAxis, + S: DataOwned, + Sh: ShapeBuilder, + { + Self::from_elem(shape, A::zero()) } -} - -impl Biased for ParamsBase -where - S: RawData, - D: Dimension, -{ - fn bias(&self) -> &ArrayBase { - self.bias() + /// returns an immutable reference to the bias + pub const fn bias(&self) -> &ArrayBase { + &self.bias } - - fn bias_mut(&mut self) -> &mut ArrayBase { - self.bias_mut() + /// returns a mutable reference to the bias + pub const fn bias_mut(&mut self) -> &mut ArrayBase { + &mut self.bias } -} - -impl core::fmt::Debug for ParamsBase -where - D: Dimension, - S: Data, - A: core::fmt::Debug, -{ - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - f.debug_struct("ModelParams") - .field("bias", self.bias()) - .field("weights", self.weights()) - .finish() + /// returns an immutable reference to the weights + pub const fn weights(&self) -> &ArrayBase { + &self.weights } -} - -impl core::fmt::Display for ParamsBase -where - D: Dimension, - S: Data, - A: core::fmt::Display, -{ - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - write!( - f, - "{{ bias: {}, weights: {} }}", - self.bias(), - self.weights() - ) + /// returns a mutable reference to the weights + pub const fn weights_mut(&mut self) -> &mut ArrayBase { + &mut self.weights } -} - -impl Clone for ParamsBase -where - D: Dimension, - S: ndarray::RawDataClone, - A: Clone, -{ - fn clone(&self) -> Self { - Self::new(self.bias().clone(), self.weights().clone()) + /// returns a raw reference to the bias tensor + pub fn bias_as_raw_ref(&self) -> &RawRef + where + S: Data, + { + self.bias().as_raw_ref() } -} - -impl Copy for ParamsBase -where - D: Dimension + Copy, - ::Smaller: Copy, - S: ndarray::RawDataClone + Copy, - A: Copy, -{ -} - -impl PartialEq for ParamsBase -where - D: Dimension, - S: Data, - A: PartialEq, -{ - fn eq(&self, other: &Self) -> bool { - self.bias() == other.bias() && self.weights() == other.weights() + /// returns a raw reference of the weights + pub fn weights_as_raw_ref(&self) -> &RawRef + where + S: Data, + { + self.weights().as_raw_ref() } -} - -impl PartialEq<&ParamsBase> for ParamsBase -where - D: Dimension, - S: Data, - A: PartialEq, -{ - fn eq(&self, other: &&ParamsBase) -> bool { - self.bias() == other.bias() && self.weights() == other.weights() + /// returns an immutable rererence to the bias as a layout reference + pub fn bias_layout_ref(&self) -> &LayoutRef + where + S: Data, + { + self.bias().as_layout_ref() } -} - -impl PartialEq<&mut ParamsBase> for ParamsBase -where - D: Dimension, - S: Data, - A: PartialEq, -{ - fn eq(&self, other: &&mut ParamsBase) -> bool { - self.bias() == other.bias() && self.weights() == other.weights() + /// returns a mutable rererence to the weights as a layout reference + pub fn bias_layout_ref_mut(&mut self) -> &mut LayoutRef + where + S: DataMut, + { + self.bias_mut().as_layout_ref_mut() } -} - -impl Eq for ParamsBase -where - D: Dimension, - S: Data, - A: Eq, -{ -} - -impl IntoIterator for ParamsBase -where - D: Dimension, - S: RawData, -{ - type Item = ParamsBase; - type IntoIter = Once>; - - fn into_iter(self) -> Self::IntoIter { - core::iter::once(self) + /// returns an immutable rererence to the weights as a layout reference + pub fn weights_layout_ref(&self) -> &LayoutRef + where + S: Data, + { + self.weights().as_layout_ref() + } + /// returns a mutable rererence to the weights as a layout reference + pub fn weights_layout_ref_mut(&mut self) -> &mut LayoutRef + where + S: DataMut, + { + self.weights_mut().as_layout_ref_mut() + } + /// assign the bias + pub fn assign_bias(&mut self, bias: &ArrayBase) -> &mut Self + where + A: Clone, + S: DataMut, + { + self.bias_mut().assign(bias); + self + } + /// assign the weights + pub fn assign_weights(&mut self, weights: &ArrayBase) -> &mut Self + where + A: Clone, + S: DataMut, + { + self.weights_mut().assign(weights); + self + } + /// replace the bias and return the previous state; uses [replace](core::mem::replace) + pub fn replace_bias( + &mut self, + bias: ArrayBase, + ) -> ArrayBase { + core::mem::replace(&mut self.bias, bias) + } + /// replace the weights and return the previous state; uses [replace](core::mem::replace) + pub fn replace_weights(&mut self, weights: ArrayBase) -> ArrayBase { + core::mem::replace(&mut self.weights, weights) + } + /// set the bias + pub fn set_bias(&mut self, bias: ArrayBase) -> &mut Self { + *self.bias_mut() = bias; + self + } + /// set the weights + pub fn set_weights(&mut self, weights: ArrayBase) -> &mut Self { + *self.weights_mut() = weights; + self + } + /// returns the dimensions of the weights + pub fn dim(&self) -> D::Pattern { + self.weights().dim() + } + /// returns true if both the weights and bias are empty; uses [`is_empty`](ArrayBase::is_empty) + pub fn is_empty(&self) -> bool { + self.is_weights_empty() && self.is_bias_empty() + } + /// returns true if the weights are empty + pub fn is_weights_empty(&self) -> bool { + self.weights().is_empty() + } + /// returns true if the bias is empty + pub fn is_bias_empty(&self) -> bool { + self.bias().is_empty() + } + /// the total number of elements within the weight tensor + pub fn count_weights(&self) -> usize { + self.weights().len() + } + /// the total number of elements within the bias tensor + pub fn count_bias(&self) -> usize { + self.bias().len() + } + /// returns the raw dimensions of the weights; + pub fn raw_dim(&self) -> D { + self.weights().raw_dim() + } + /// returns the shape of the parameters; uses the shape of the weight tensor + pub fn shape<'a>(&'a self) -> &'a [usize] + where + A: 'a, + { + self.weights.shape() + } + /// returns the shape of the bias tensor; the shape should be equivalent to that of the + /// weight tensor minus the "zero-th" axis + pub fn shape_bias(&self) -> &[usize] + where + A: 'static, + { + self.bias.shape() + } + /// returns the total number of parameters within the layer + pub fn size(&self) -> usize { + self.weights().len() + self.bias().len() + } + /// returns an owned instance of the parameters + pub fn to_owned(&self) -> Params + where + A: Clone, + S: DataOwned, + { + ParamsBase::new(self.bias().to_owned(), self.weights().to_owned()) + } + /// change the shape of the parameters; the shape of the bias parameters is determined by + /// removing the "zero-th" axis of the given shape + pub fn to_shape( + &self, + shape: Sh, + ) -> crate::Result, Sh::Dim>> + where + A: Clone, + S: DataOwned, + Sh: ShapeBuilder, + Sh::Dim: Dimension + RemoveAxis, + { + let shape = shape.into_shape_with_order(); + let dim = shape.raw_dim().clone(); + let bias = self.bias().to_shape(dim.remove_axis(Axis(0)))?; + let weights = self.weights().to_shape(dim)?; + Ok(ParamsBase::new(bias, weights)) + } + /// returns a new [`ParamsBase`] instance with the same paramaters, but using a shared + /// representation of the data; + pub fn to_shared(&self) -> ParamsBase, D> + where + A: Clone, + S: Data, + { + ParamsBase::new(self.bias().to_shared(), self.weights().to_shared()) + } + /// returns a "view" of the parameters; see [`view`](ndarray::ViewRepr) for more information + pub fn view(&self) -> ParamsBase, D> + where + S: Data, + { + ParamsBase::new(self.bias().view(), self.weights().view()) + } + /// returns mutable view of the parameters + pub fn view_mut(&mut self) -> ParamsBase, D> + where + S: DataMut, + { + ParamsBase::new(self.bias.view_mut(), self.weights.view_mut()) + } + /// clamps all values within the parameters between the given min and max values + pub fn clamp(&mut self, min: A, max: A) -> Params + where + A: 'static + Clone + PartialOrd, + S: Data, + { + ParamsBase { + bias: self.bias().clamp(min.clone(), max.clone()), + weights: self.weights().clamp(min, max), + } + } + /// applies the given function onto each element, capturing the results in a new instance + pub fn mapv(&self, f: F) -> Params + where + A: Clone, + S: Data, + F: Fn(A) -> U, + { + ParamsBase { + bias: self.bias().mapv(&f), + weights: self.weights().mapv(&f), + } } } diff --git a/params/src/impls/impl_params_deprecated.rs b/params/src/impls/impl_params_deprecated.rs deleted file mode 100644 index fc30e57c..00000000 --- a/params/src/impls/impl_params_deprecated.rs +++ /dev/null @@ -1,15 +0,0 @@ -/* - appellation: impl_params_iter - authors: @FL03 -*/ -use crate::params_base::ParamsBase; - -use ndarray::{Dimension, RawData}; - -#[doc(hidden)] -impl ParamsBase -where - S: RawData, - D: Dimension, -{ -} diff --git a/params/src/impls/impl_params_ext.rs b/params/src/impls/impl_params_ext.rs new file mode 100644 index 00000000..a6e2e993 --- /dev/null +++ b/params/src/impls/impl_params_ext.rs @@ -0,0 +1,291 @@ +/* + Appellation: impl_params_ext + Created At: 2026.01.13:18:32:54 + Contrib: @FL03 +*/ +use crate::params_base::ParamsBase; + +use crate::Params; +use crate::traits::{Biased, Weighted}; +use concision_traits::{Apply, FillLike, MapInto, MapTo, OnesLike, ZerosLike}; +use core::iter::Once; +use ndarray::{ArrayBase, Data, DataOwned, Dimension, OwnedRepr, RawData}; +use num_traits::{One, Zero}; +use rspace_traits::RawSpace; + +impl RawSpace for ParamsBase +where + D: Dimension, + S: RawData, +{ + type Elem = A; +} + +impl Weighted for ParamsBase +where + S: RawData, + D: Dimension, +{ + type Tensor<_S, _D, _A> + = ArrayBase<_S, _D, _A> + where + _D: Dimension, + _S: RawData; + + fn weights(&self) -> &ArrayBase { + self.weights() + } + + fn weights_mut(&mut self) -> &mut ArrayBase { + self.weights_mut() + } +} + +impl Biased for ParamsBase +where + S: RawData, + D: Dimension, +{ + fn bias(&self) -> &ArrayBase { + self.bias() + } + + fn bias_mut(&mut self) -> &mut ArrayBase { + self.bias_mut() + } +} + +impl core::ops::Deref for ParamsBase +where + D: Dimension, + S: RawData, +{ + type Target = ndarray::LayoutRef; + + fn deref(&self) -> &Self::Target { + self.weights().as_layout_ref() + } +} + +impl core::fmt::Debug for ParamsBase +where + D: Dimension, + S: Data, + A: core::fmt::Debug, +{ + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + f.debug_struct("ModelParams") + .field("bias", self.bias()) + .field("weights", self.weights()) + .finish() + } +} + +impl core::fmt::Display for ParamsBase +where + D: Dimension, + S: Data, + A: core::fmt::Display, +{ + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!( + f, + "{{ bias: {}, weights: {} }}", + self.bias(), + self.weights() + ) + } +} + +impl Clone for ParamsBase +where + D: Dimension, + S: ndarray::RawDataClone, + A: Clone, +{ + fn clone(&self) -> Self { + Self::new(self.bias().clone(), self.weights().clone()) + } +} + +impl Copy for ParamsBase +where + D: Dimension + Copy, + ::Smaller: Copy, + S: ndarray::RawDataClone + Copy, + A: Copy, +{ +} + +impl PartialEq for ParamsBase +where + D: Dimension, + S: Data, + A: PartialEq, +{ + fn eq(&self, other: &Self) -> bool { + self.bias() == other.bias() && self.weights() == other.weights() + } +} + +impl PartialEq<&ParamsBase> for ParamsBase +where + D: Dimension, + S: Data, + A: PartialEq, +{ + fn eq(&self, other: &&ParamsBase) -> bool { + self.bias() == other.bias() && self.weights() == other.weights() + } +} + +impl PartialEq<&mut ParamsBase> for ParamsBase +where + D: Dimension, + S: Data, + A: PartialEq, +{ + fn eq(&self, other: &&mut ParamsBase) -> bool { + self.bias() == other.bias() && self.weights() == other.weights() + } +} + +impl Eq for ParamsBase +where + D: Dimension, + S: Data, + A: Eq, +{ +} + +impl IntoIterator for ParamsBase +where + D: Dimension, + S: RawData, +{ + type Item = ParamsBase; + type IntoIter = Once>; + + fn into_iter(self) -> Self::IntoIter { + core::iter::once(self) + } +} + +impl Apply for ParamsBase +where + A: Clone, + D: Dimension, + S: Data, + F: Fn(A) -> B, +{ + type Output = ParamsBase, D>; + + fn apply(&self, func: F) -> Self::Output { + ParamsBase { + bias: self.bias().mapv(&func), + weights: self.weights().mapv(&func), + } + } +} + +impl MapInto for ParamsBase +where + A: Clone, + D: Dimension, + S: Data, + F: Fn(A) -> B, +{ + type Elem = A; + type Cont = Params; + + fn mapi(self, func: F) -> Self::Cont { + ParamsBase { + bias: self.bias().mapv(&func), + weights: self.weights().mapv(&func), + } + } +} + +impl MapInto for &ParamsBase +where + A: Clone, + D: Dimension, + S: Data, + F: Fn(A) -> B, +{ + type Elem = A; + type Cont = Params; + + fn mapi(self, func: F) -> Self::Cont { + ParamsBase { + bias: self.bias().mapv(&func), + weights: self.weights().mapv(&func), + } + } +} + +impl MapTo for ParamsBase +where + A: Clone, + D: Dimension, + S: Data, + F: Fn(A) -> B, +{ + type Cont = Params; + type Elem = A; + + fn mapt(&self, func: F) -> Self::Cont { + ParamsBase { + bias: self.bias().mapv(&func), + weights: self.weights().mapv(&func), + } + } +} + +impl OnesLike for ParamsBase +where + D: Dimension, + S: DataOwned, + A: Clone + One, +{ + type Output = ParamsBase; + + fn ones_like(&self) -> Self::Output { + ParamsBase { + bias: self.bias().ones_like(), + weights: self.weights().ones_like(), + } + } +} + +impl ZerosLike for ParamsBase +where + D: Dimension, + S: DataOwned, + A: Clone + Zero, +{ + type Output = ParamsBase; + + fn zeros_like(&self) -> Self::Output { + ParamsBase { + bias: self.bias().zeros_like(), + weights: self.weights().zeros_like(), + } + } +} + +impl FillLike for ParamsBase +where + D: Dimension, + S: DataOwned, + A: Clone, +{ + type Output = ParamsBase; + + fn fill_like(&self, elem: A) -> Self::Output { + ParamsBase { + bias: self.bias().fill_like(elem.clone()), + weights: self.weights().fill_like(elem), + } + } +} diff --git a/params/src/impls/impl_params_iter.rs b/params/src/impls/impl_params_iter.rs index 58e07f91..309083d6 100644 --- a/params/src/impls/impl_params_iter.rs +++ b/params/src/impls/impl_params_iter.rs @@ -4,7 +4,7 @@ */ use crate::params_base::ParamsBase; -use crate::iter::{Iter, IterMut}; +use crate::iter::{ParamsIter, ParamsIterMut}; use ndarray::iter as nditer; use ndarray::{Axis, Data, DataMut, Dimension, RawData, RemoveAxis}; @@ -14,19 +14,19 @@ use ndarray::{Axis, Data, DataMut, Dimension, RawData, RemoveAxis}; /// - immutable and mutable iterators over each parameter (weights and bias) respectively; /// - an iterator over the parameters, which zips together an axis iterator over the columns of /// the weights and an iterator over the bias; -impl ParamsBase +impl ParamsBase where S: RawData, D: Dimension, { /// an iterator of the parameters; the created iterator zips together an axis iterator over /// the columns of the weights and an iterator over the bias - pub fn iter(&self) -> Iter<'_, A, D> + pub fn iter(&self) -> ParamsIter<'_, A, D> where D: RemoveAxis, S: Data, { - Iter { + ParamsIter { bias: self.bias().iter(), weights: self.weights().axis_iter(Axis(1)), } @@ -34,14 +34,14 @@ where /// returns a mutable iterator of the parameters, [`IterMut`], which essentially zips /// together a mutable axis iterator over the columns of the weights against a mutable /// iterator over the elements of the bias - pub fn iter_mut(&mut self) -> IterMut<'_, A, D> + pub fn iter_mut(&mut self) -> ParamsIterMut<'_, A, D> where D: RemoveAxis, S: DataMut, { - IterMut { + ParamsIterMut { bias: self.bias.iter_mut(), - weights: self.weights.axis_iter_mut(Axis(1)), + weights: self.weights.axis_iter_mut(Axis(0)), } } /// returns an iterator over the bias @@ -72,4 +72,20 @@ where { self.weights_mut().iter_mut() } + /// returns an iterator over the weights along the specified axis + pub fn axis_iter_weights(&self, axis: Axis) -> nditer::AxisIter<'_, A, D::Smaller> + where + D: RemoveAxis, + S: Data, + { + self.weights().axis_iter(axis) + } + /// returns a mutable iterator over the weights along the specified axis + pub fn axis_iter_weights_mut(&mut self, axis: Axis) -> nditer::AxisIterMut<'_, A, D::Smaller> + where + D: RemoveAxis, + S: DataMut, + { + self.weights_mut().axis_iter_mut(axis) + } } diff --git a/params/src/impls/impl_params_ops.rs b/params/src/impls/impl_params_ops.rs index 13283804..ce471071 100644 --- a/params/src/impls/impl_params_ops.rs +++ b/params/src/impls/impl_params_ops.rs @@ -8,7 +8,52 @@ use ndarray::linalg::Dot; use ndarray::{ Array, ArrayBase, ArrayView, Data, Dimension, Ix0, Ix1, Ix2, RemoveAxis, ScalarOperand, }; -use num_traits::{Float, FromPrimitive, Num}; +use num_traits::{Float, FromPrimitive, Num, Signed}; + +macro_rules! impl_tensor_unary { + (@impl $method:ident $(where $($where:tt)*)?) => { + pub fn $method(&self) -> Params $(where $($where)*)? { + self.mapv(|x| x.$method()) + } + }; + ($($method:ident),* $(,)?) => { + $(impl_tensor_unary! { @impl $method where A: Float })* + } +} + +impl ParamsBase +where + A: 'static + Clone, + D: Dimension, + S: Data, +{ + impl_tensor_unary! { + cos, + cosh, + exp, + ln, + sin, + sinh, + sqrt, + tan, + tanh, + } + /// take the absolute value of each element within the parameters + pub fn abs(&self) -> Params + where + A: Signed, + { + self.mapv(|x| x.abs()) + } + #[cfg(feature = "complex")] + /// compute the conjugate of each element within the parameters + pub fn conj(&self) -> Params + where + A: num_complex::ComplexFloat, + { + self.mapv(|x| x.conj()) + } +} impl ParamsBase where diff --git a/params/src/impls/impl_params_rand.rs b/params/src/impls/impl_params_rand.rs index 31094ce2..98213649 100644 --- a/params/src/impls/impl_params_rand.rs +++ b/params/src/impls/impl_params_rand.rs @@ -3,14 +3,13 @@ Created At: 2025.11.26:15:28:12 Contrib: @FL03 */ +#![cfg(feature = "rand")] use crate::params_base::ParamsBase; - -use concision_init::NdInit; +use concision_init::{NdRandom, rand, rand_distr}; use ndarray::{ ArrayBase, Axis, DataOwned, Dimension, RawData, RemoveAxis, ScalarOperand, ShapeBuilder, }; use num_traits::{Float, FromPrimitive}; -use rand::rngs::SmallRng; use rand_distr::Distribution; impl ParamsBase @@ -53,11 +52,17 @@ where } } -impl NdInit for ParamsBase +impl NdRandom for ParamsBase where D: RemoveAxis, S: RawData, { + type Cont<_S, _D> + = ParamsBase<_S, _D, A> + where + _D: Dimension, + _S: RawData; + fn rand(shape: Sh, distr: Ds) -> Self where Ds: Distribution, @@ -65,7 +70,11 @@ where S: DataOwned, { use rand::SeedableRng; - Self::rand_with(shape, distr, &mut SmallRng::from_rng(&mut rand::rng())) + Self::rand_with( + shape, + distr, + &mut rand::rngs::SmallRng::from_rng(&mut rand::rng()), + ) } fn rand_with(shape: Sh, distr: Ds, rng: &mut R) -> Self diff --git a/params/src/impls/impl_params_ref.rs b/params/src/impls/impl_params_ref.rs new file mode 100644 index 00000000..eed0569f --- /dev/null +++ b/params/src/impls/impl_params_ref.rs @@ -0,0 +1,9 @@ +/* + Appellation: impl_params_ref + Created At: 2025.12.10:23:37:09 + Contrib: @FL03 +*/ +use crate::ParamsRef; +use ndarray::Dimension; + +impl ParamsRef where D: Dimension {} diff --git a/params/src/impls/impl_params_repr.rs b/params/src/impls/impl_params_repr.rs new file mode 100644 index 00000000..ff838021 --- /dev/null +++ b/params/src/impls/impl_params_repr.rs @@ -0,0 +1,50 @@ +/* + appellation: impl_params_repr + authors: @FL03 +*/ +use crate::params_base::ParamsBase; + +use ndarray::{ArrayBase, DataOwned, Dimension, Ix1, Ix2, RawData}; + +impl ParamsBase +where + D: Dimension, + S: RawData, +{ +} + +impl ParamsBase +where + S: RawData, +{ + /// returns a new instance of the [`ParamsBase`] initialized using a _scalar_ bias along + /// with the given, one-dimensional weight tensor. + pub fn from_scalar_bias(bias: A, weights: ArrayBase) -> Self + where + A: Clone, + S: DataOwned, + { + Self { + bias: ArrayBase::from_elem((), bias), + weights, + } + } + /// returns the number of rows in the weights tensor + pub fn nrows(&self) -> usize { + self.weights().len() + } +} + +impl ParamsBase +where + S: RawData, +{ + /// returns the number of columns in the weights tensor + pub fn ncols(&self) -> usize { + self.weights().ncols() + } + /// returns the number of rows in the weights tensor + pub fn nrows(&self) -> usize { + self.weights().nrows() + } +} diff --git a/params/src/impls/impl_params_serde.rs b/params/src/impls/impl_params_serde.rs index dd410bcc..4022dc7f 100644 --- a/params/src/impls/impl_params_serde.rs +++ b/params/src/impls/impl_params_serde.rs @@ -1,7 +1,9 @@ /* - appellation: impl_params_serde - authors: @FL03 + Appellation: impl_params_serde + Created At: 2026.01.13:18:35:20 + Contrib: @FL03 */ +#![cfg(feature = "serde")] use crate::params_base::ParamsBase; use ndarray::{Data, DataOwned, Dimension, RawData}; use serde::de::{Deserialize, Deserializer, Error, Visitor}; diff --git a/params/src/iter.rs b/params/src/iter.rs index 1bee6aa1..7090e859 100644 --- a/params/src/iter.rs +++ b/params/src/iter.rs @@ -1,78 +1,16 @@ /* Appellation: iter + Created At: 2026.01.12:11:08:01 Contrib: @FL03 */ -//! Iterators for parameters within a neural network - -use ndarray::Dimension; -use ndarray::iter::{AxisIter, AxisIterMut}; -use ndarray::iter::{Iter as NdIter, IterMut as NdIterMut}; - -pub(crate) type ItemRef<'a, A, D> = ( - ::Smaller> as Iterator>::Item, - &'a A, -); -pub(crate) type ItemMut<'a, A, D> = ( - ::Smaller> as Iterator>::Item, - &'a mut A, -); -/// The [`Iter`] type provides an iterator over the parameters of a neural network layer by -/// zipping together an axis iterator over the columns of the weights and an iterator over the -/// bias. -pub struct Iter<'a, A, D> -where - D: Dimension, -{ - pub(crate) weights: AxisIter<'a, A, D::Smaller>, - pub(crate) bias: NdIter<'a, A, D::Smaller>, -} -/// The [`IterMut`] type provides a mutable iterator over the parameters of a neural network -/// layer by zipping together a mutable axis iterator over the columns of the weights and -/// a mutable iterator over the bias. -pub struct IterMut<'a, A, D> -where - D: Dimension, -{ - pub(crate) weights: AxisIterMut<'a, A, D::Smaller>, - pub(crate) bias: NdIterMut<'a, A, D::Smaller>, -} - -/* - ************* Implementations ************* -*/ -impl<'a, A, D> Iterator for Iter<'a, A, D> -where - D: Dimension, -{ - type Item = ItemRef<'a, A, D>; - - fn next(&mut self) -> Option { - match (self.weights.next(), self.bias.next()) { - (Some(w), Some(b)) => Some((w, b)), - _ => None, - } - } -} - -impl<'a, A, D> Iterator for IterMut<'a, A, D> -where - D: Dimension, -{ - type Item = ItemMut<'a, A, D>; - - fn next(&mut self) -> Option { - match (self.weights.next(), self.bias.next()) { - (Some(w), Some(b)) => Some((w, b)), - _ => None, - } - } -} - -impl<'a, A, D> ExactSizeIterator for Iter<'a, A, D> -where - D: Dimension, -{ - fn len(&self) -> usize { - self.weights.len() - } +//! iterators for parameters within a neural network +#[doc(inline)] +pub use self::iter_params::*; +// modules +pub mod iter_params; +// prelude (local) +#[doc(hidden)] +#[allow(unused_imports)] +pub(crate) mod prelude { + pub use super::iter_params::*; } diff --git a/params/src/iter/iter_params.rs b/params/src/iter/iter_params.rs new file mode 100644 index 00000000..8d97b12e --- /dev/null +++ b/params/src/iter/iter_params.rs @@ -0,0 +1,77 @@ +/* + Appellation: iter + Contrib: @FL03 +*/ +//! Iterators for parameters within a neural network +use ndarray::Dimension; +use ndarray::iter::{AxisIter, AxisIterMut}; +use ndarray::iter::{Iter as NdIter, IterMut as NdIterMut}; + +pub(crate) type ItemRef<'a, A, D> = ( + ::Smaller> as Iterator>::Item, + &'a A, +); +pub(crate) type ItemMut<'a, A, D> = ( + ::Smaller> as Iterator>::Item, + &'a mut A, +); +/// The [`ParamsIter`] defines a iterator over owned parameters within a neural network by +/// zipping together an axis iterator over the columns of the weights and an iterator over the +/// bias. +pub struct ParamsIter<'a, A, D> +where + D: Dimension, +{ + pub(crate) weights: AxisIter<'a, A, D::Smaller>, + pub(crate) bias: NdIter<'a, A, D::Smaller>, +} +/// The [`ParamsIterMut`] type provides a mutable iterator over the parameters of a neural +/// network layer by zipping together a mutable axis iterator over the columns of the weights +/// and a mutable iterator over the bias. +pub struct ParamsIterMut<'a, A, D> +where + D: Dimension, +{ + pub(crate) weights: AxisIterMut<'a, A, D::Smaller>, + pub(crate) bias: NdIterMut<'a, A, D::Smaller>, +} + +/* + ************* Implementations ************* +*/ +impl<'a, A, D> Iterator for ParamsIter<'a, A, D> +where + D: Dimension, +{ + type Item = ItemRef<'a, A, D>; + + fn next(&mut self) -> Option { + match (self.weights.next(), self.bias.next()) { + (Some(w), Some(b)) => Some((w, b)), + _ => None, + } + } +} + +impl<'a, A, D> ExactSizeIterator for ParamsIter<'a, A, D> +where + D: Dimension, +{ + fn len(&self) -> usize { + self.weights.len() + } +} + +impl<'a, A, D> Iterator for ParamsIterMut<'a, A, D> +where + D: Dimension, +{ + type Item = ItemMut<'a, A, D>; + + fn next(&mut self) -> Option { + match (self.weights.next(), self.bias.next()) { + (Some(w), Some(b)) => Some((w, b)), + _ => None, + } + } +} diff --git a/params/src/lib.rs b/params/src/lib.rs index e6f7bac5..59105401 100644 --- a/params/src/lib.rs +++ b/params/src/lib.rs @@ -13,8 +13,8 @@ //! values. At its core, the [`ParamsBase`] object is defined as an object composed of two //! independent tensors: //! -//! - An $`n`$ dimensional weight tensor -//! - An $`n-1`$ dimensional bias tensor +//! - An $n$ dimensional weight tensor +//! - An $n-1$ dimensional bias tensor //! //! These tensors can be of any shape or size, allowing for a wide range of neural network //! architectures to be represented. The crate also provides various utilities and traits for @@ -30,64 +30,60 @@ clippy::upper_case_acronyms, rustdoc::redundant_explicit_links )] - +#![cfg_attr(all(feature = "nightly", feature = "alloc"), feature(allocator_api))] +#![cfg_attr(all(feature = "nightly", feature = "autodiff"), feature(autodiff))] +// compiler checks +#[cfg(not(any(feature = "alloc", feature = "std")))] +compiler_error! { "Either the \"alloc\" or \"std\" feature must be enabled for this crate." } +// external crates #[cfg(feature = "alloc")] extern crate alloc; -extern crate ndarray as nd; - -#[cfg(all(not(feature = "alloc"), not(feature = "std")))] -compiler_error! { - "Either the \"alloc\" or \"std\" feature must be enabled for this crate." -} - -pub mod error; -pub mod iter; - -mod params_base; - +// macros #[macro_use] pub(crate) mod macros { #[macro_use] pub mod seal; } +// public modules +pub mod error; +pub mod iter; +// internal modules +mod params_base; mod impls { mod impl_params; + mod impl_params_ext; mod impl_params_iter; mod impl_params_ops; - - #[allow(deprecated)] - mod impl_params_deprecated; - #[cfg(feature = "rand")] mod impl_params_rand; - #[cfg(feature = "serde")] + mod impl_params_ref; + mod impl_params_repr; mod impl_params_serde; } -pub mod traits { - //! Traits for working with model parameters - pub use self::{param::*, wnb::*}; +mod traits { + #[doc(inline)] + pub use self::{iterators::*, raw_params::*, shape::*, wnb::*}; - mod param; + mod iterators; + mod raw_params; + mod shape; mod wnb; } -mod types { - //! Supporting types and aliases for working with model parameters +mod utils { #[doc(inline)] - pub use self::aliases::*; + pub use self::shape::*; - mod aliases; + mod shape; } - // re-exports #[doc(inline)] -pub use self::{error::*, params_base::ParamsBase, traits::*, types::*}; +pub use self::{error::*, params_base::*, traits::*, utils::*}; // prelude #[doc(hidden)] pub mod prelude { - pub use crate::error::ParamsError; pub use crate::params_base::*; pub use crate::traits::*; - pub use crate::types::*; + pub use crate::utils::*; } diff --git a/params/src/params_base.rs b/params/src/params_base.rs index 0ef143f4..b807fa94 100644 --- a/params/src/params_base.rs +++ b/params/src/params_base.rs @@ -2,19 +2,44 @@ Appellation: params Contrib: @FL03 */ +#[cfg(feature = "alloc")] +use alloc::boxed::Box; use ndarray::{ - ArrayBase, Axis, Data, DataMut, DataOwned, Dimension, RawData, RemoveAxis, ShapeArg, - ShapeBuilder, + ArrayBase, CowRepr, Dimension, Ix2, OwnedArcRepr, OwnedRepr, RawData, RawRef, RawViewRepr, + ViewRepr, }; +/// A type alias for a [`ParamsBase`] with an owned internal layout +pub type Params = ParamsBase, D, A>; +/// A type alias for shared parameters +pub type ArcParams = ParamsBase, D, A>; +/// A type alias for an immutable view of the parameters +pub type ParamsView<'a, A = f32, D = Ix2> = ParamsBase, D, A>; +/// A type alias for a mutable view of the parameters +pub type ParamsViewMut<'a, A = f32, D = Ix2> = ParamsBase, D, A>; +/// A type alias for a [`ParamsBase`] with a _borrowed_ internal layout +pub type CowParams<'a, A = f32, D = Ix2> = ParamsBase, D, A>; +/// A type alias for the [`ParamsBase`] whose elements are of type `*const A` using a +/// [`RawViewRepr`] layout +pub type RawViewParams = ParamsBase, D, A>; +/// A type alias for the [`ParamsBase`] whose elements are of type `*mut A` using a +/// [`RawViewRepr`] layout +pub type RawMutParams = ParamsBase, D, A>; + +#[cfg(feature = "alloc")] +pub struct ParamsRef { + pub bias: Box>, + pub weights: RawRef, +} + /// The [`ParamsBase`] implementation aims to provide a generic, n-dimensional weight and bias /// pair for a model (or layer). The object requires the bias tensor to be a single dimension /// smaller than the weights tensor. /// /// Therefore, we allow the weight tensor to be the _shape_ of the parameters, using the shape -/// as the basis for the bias tensor by removing one axi (typically the first axis). +/// as the basis for the bias tensor by removing the first axis. /// Consequently, this constrains the [`ParamsBase`] implementation to only support dimensions -/// that can be reduced by one axis, typically the "zero-th" axis: $`\mbox{rank}(D)>0`$. +/// that can be reduced by one axis, typically the "zero-th" axis: $\text{rank}(D)$. pub struct ParamsBase::Elem> where D: Dimension, @@ -23,263 +48,3 @@ where pub bias: ArrayBase, pub weights: ArrayBase, } - -impl ParamsBase -where - D: Dimension, - S: RawData, -{ - /// create a new instance of the [`ParamsBase`] with the given bias and weights - pub const fn new(bias: ArrayBase, weights: ArrayBase) -> Self { - Self { bias, weights } - } - /// returns a new instance of the [`ParamsBase`] using the initialization routine - pub fn init_from_fn(shape: Sh, init: F) -> Self - where - A: Clone, - D: RemoveAxis, - S: DataOwned, - Sh: ShapeBuilder, - F: Fn() -> A, - { - let shape = shape.into_shape_with_order(); - // initialize the bias using a shape that is 1 rank lower then the weights - let bias = ArrayBase::from_shape_fn(shape.raw_dim().remove_axis(Axis(0)), |_| init()); - let weights = ArrayBase::from_shape_fn(shape, |_| init()); - // create a new instance from the generated bias and weights - Self::new(bias, weights) - } - /// returns a new instance of the [`ParamsBase`] initialized use the given shape_function - pub fn from_shape_fn(shape: Sh, f: F) -> Self - where - A: Clone, - D: RemoveAxis, - S: DataOwned, - Sh: ShapeBuilder, - D::Smaller: Dimension + ShapeArg, - F: Fn(::Pattern) -> A + Fn(::Pattern) -> A, - { - let shape = shape.into_shape_with_order(); - let bdim = shape.raw_dim().remove_axis(Axis(0)); - let bias = ArrayBase::from_shape_fn(bdim, |s| f(s)); - let weights = ArrayBase::from_shape_fn(shape, |s| f(s)); - Self::new(bias, weights) - } - /// create a new instance of the [`ParamsBase`] with the given bias used the default weights - pub fn from_bias(shape: Sh, bias: ArrayBase) -> Self - where - A: Clone + Default, - D: RemoveAxis, - S: DataOwned, - Sh: ShapeBuilder, - { - let weights = ArrayBase::from_elem(shape, A::default()); - Self::new(bias, weights) - } - /// create a new instance of the [`ParamsBase`] with the given weights used the default - /// bias - pub fn from_weights(shape: Sh, weights: ArrayBase) -> Self - where - A: Clone + Default, - D: RemoveAxis, - S: DataOwned, - Sh: ShapeBuilder, - { - let shape = shape.into_shape_with_order(); - let dim_bias = shape.raw_dim().remove_axis(Axis(0)); - let bias = ArrayBase::from_elem(dim_bias, A::default()); - Self::new(bias, weights) - } - /// create a new instance of the [`ParamsBase`] from the given shape and element; - pub fn from_elem(shape: Sh, elem: A) -> Self - where - A: Clone, - D: RemoveAxis, - S: DataOwned, - Sh: ShapeBuilder, - { - let weights = ArrayBase::from_elem(shape, elem.clone()); - let dim = weights.raw_dim(); - let bias = ArrayBase::from_elem(dim.remove_axis(Axis(0)), elem); - Self::new(bias, weights) - } - #[allow(clippy::should_implement_trait)] - /// create an instance of the parameters with all values set to the default value - pub fn default(shape: Sh) -> Self - where - A: Clone + Default, - D: RemoveAxis, - S: DataOwned, - Sh: ShapeBuilder, - { - Self::from_elem(shape, A::default()) - } - /// initialize the parameters with all values set to zero - pub fn ones(shape: Sh) -> Self - where - A: Clone + num_traits::One, - D: RemoveAxis, - S: DataOwned, - Sh: ShapeBuilder, - { - Self::from_elem(shape, A::one()) - } - /// create an instance of the parameters with all values set to zero - pub fn zeros(shape: Sh) -> Self - where - A: Clone + num_traits::Zero, - D: RemoveAxis, - S: DataOwned, - Sh: ShapeBuilder, - { - Self::from_elem(shape, A::zero()) - } - /// returns an immutable reference to the bias - pub const fn bias(&self) -> &ArrayBase { - &self.bias - } - /// returns a mutable reference to the bias - pub const fn bias_mut(&mut self) -> &mut ArrayBase { - &mut self.bias - } - /// returns an immutable reference to the weights - pub const fn weights(&self) -> &ArrayBase { - &self.weights - } - /// returns a mutable reference to the weights - pub const fn weights_mut(&mut self) -> &mut ArrayBase { - &mut self.weights - } - /// assign the bias - pub fn assign_bias(&mut self, bias: &ArrayBase) -> &mut Self - where - A: Clone, - S: DataMut, - { - self.bias_mut().assign(bias); - self - } - /// assign the weights - pub fn assign_weights(&mut self, weights: &ArrayBase) -> &mut Self - where - A: Clone, - S: DataMut, - { - self.weights_mut().assign(weights); - self - } - /// replace the bias and return the previous state; uses [replace](core::mem::replace) - pub fn replace_bias( - &mut self, - bias: ArrayBase, - ) -> ArrayBase { - core::mem::replace(&mut self.bias, bias) - } - /// replace the weights and return the previous state; uses [replace](core::mem::replace) - pub fn replace_weights(&mut self, weights: ArrayBase) -> ArrayBase { - core::mem::replace(&mut self.weights, weights) - } - /// set the bias - pub fn set_bias(&mut self, bias: ArrayBase) -> &mut Self { - *self.bias_mut() = bias; - self - } - /// set the weights - pub fn set_weights(&mut self, weights: ArrayBase) -> &mut Self { - *self.weights_mut() = weights; - self - } - /// returns the dimensions of the weights - pub fn dim(&self) -> D::Pattern { - self.weights().dim() - } - /// returns true if both the weights and bias are empty; uses [`is_empty`](ArrayBase::is_empty) - pub fn is_empty(&self) -> bool { - self.is_weights_empty() && self.is_bias_empty() - } - /// returns true if the weights are empty - pub fn is_weights_empty(&self) -> bool { - self.weights().is_empty() - } - /// returns true if the bias is empty - pub fn is_bias_empty(&self) -> bool { - self.bias().is_empty() - } - /// the total number of elements within the weight tensor - pub fn count_weights(&self) -> usize { - self.weights().len() - } - /// the total number of elements within the bias tensor - pub fn count_bias(&self) -> usize { - self.bias().len() - } - /// returns the raw dimensions of the weights; - pub fn raw_dim(&self) -> D { - self.weights().raw_dim() - } - /// returns the shape of the parameters; uses the shape of the weight tensor - pub fn shape<'a>(&'a self) -> &'a [usize] - where - A: 'a, - { - self.weights.shape() - } - /// returns the shape of the bias tensor; the shape should be equivalent to that of the - /// weight tensor minus the "zero-th" axis - pub fn shape_bias(&self) -> &[usize] - where - A: 'static, - { - self.bias.shape() - } - /// returns the total number of parameters within the layer - pub fn size(&self) -> usize { - self.weights().len() + self.bias().len() - } - /// returns an owned instance of the parameters - pub fn to_owned(&self) -> ParamsBase, D> - where - A: Clone, - S: DataOwned, - { - ParamsBase::new(self.bias().to_owned(), self.weights().to_owned()) - } - /// change the shape of the parameters; the shape of the bias parameters is determined by - /// removing the "zero-th" axis of the given shape - pub fn to_shape(&self, shape: Sh) -> crate::Result, Sh::Dim>> - where - A: Clone, - S: DataOwned, - Sh: ShapeBuilder, - Sh::Dim: Dimension + RemoveAxis, - { - let shape = shape.into_shape_with_order(); - let dim = shape.raw_dim().clone(); - let bias = self.bias().to_shape(dim.remove_axis(Axis(0)))?; - let weights = self.weights().to_shape(dim)?; - Ok(ParamsBase::new(bias, weights)) - } - /// returns a new [`ParamsBase`] instance with the same paramaters, but using a shared - /// representation of the data; - pub fn to_shared(&self) -> ParamsBase, D> - where - A: Clone, - S: Data, - { - ParamsBase::new(self.bias().to_shared(), self.weights().to_shared()) - } - /// returns a "view" of the parameters; see [`view`](ndarray::ViewRepr) for more information - pub fn view(&self) -> ParamsBase, D> - where - S: Data, - { - ParamsBase::new(self.bias().view(), self.weights().view()) - } - /// returns mutable view of the parameters - pub fn view_mut(&mut self) -> ParamsBase, D> - where - S: DataMut, - { - ParamsBase::new(self.bias.view_mut(), self.weights.view_mut()) - } -} diff --git a/params/src/traits/iterators.rs b/params/src/traits/iterators.rs new file mode 100644 index 00000000..fdeeafd2 --- /dev/null +++ b/params/src/traits/iterators.rs @@ -0,0 +1,67 @@ +/* + Appellation: iterators + Created At: 2025.12.14:11:02:25 + Contrib: @FL03 +*/ +use ndarray::iter as nditer; +use ndarray::{ArrayBase, Data, DataMut, Dimension}; + +pub trait NdIter +where + D: Dimension, +{ + type Iter<'b, T> + where + T: 'b, + Self: 'b; + /// returns an iterator over the weights; + fn nditer(&self) -> Self::Iter<'_, A>; +} + +pub trait NdIterMut +where + D: Dimension, +{ + type IterMut<'b, T> + where + T: 'b, + Self: 'b; + /// returns a mutable iterator over the weights + fn nditer_mut(&mut self) -> Self::IterMut<'_, A>; +} + +/* + ************* Implementations ************* +*/ + +impl NdIter for ArrayBase +where + S: Data, + D: Dimension, +{ + type Iter<'b, T> + = nditer::Iter<'b, T, D> + where + T: 'b, + Self: 'b; + + fn nditer(&self) -> Self::Iter<'_, A> { + self.iter() + } +} + +impl NdIterMut for ArrayBase +where + S: DataMut, + D: Dimension, +{ + type IterMut<'b, T> + = nditer::IterMut<'b, T, D> + where + T: 'b, + Self: 'b; + + fn nditer_mut(&mut self) -> Self::IterMut<'_, A> { + self.iter_mut() + } +} diff --git a/params/src/traits/param.rs b/params/src/traits/raw_params.rs similarity index 63% rename from params/src/traits/param.rs rename to params/src/traits/raw_params.rs index 8d3fbfed..8e112574 100644 --- a/params/src/traits/param.rs +++ b/params/src/traits/raw_params.rs @@ -1,9 +1,10 @@ /* - Appellation: param + Appellation: tensor Created At: 2025.12.08:16:03:55 Contrib: @FL03 */ -/// The [`RawParam`] trait is used to denote objects capable of being used as a paramater + +/// The [`RawParams`] trait is used to denote objects capable of being used as a paramater /// within a neural network or machine learning context. More over, it provides us with an /// ability to associate some generic element type with the parameter and thus allows us to /// consider so-called _parameter spaces_. If we allow a parameter space to simply be a @@ -13,90 +14,93 @@ /// and training in a more formal manner. /// /// **Note**: This trait is sealed and cannot be implemented outside of this crate. -pub trait RawParam { +pub trait RawParams { type Elem: ?Sized; - private!(); + private! {} } -/// The [`ScalarParam`] trait naturally extends the [`RawParameter`] trait to define a -/// scaler as a parameter whose element type is itself. This is useful for defining -/// parameters which are simple scalars such as `f32` or `i64`. -pub trait ScalarParam: RawParam + Sized { +/// The [`ScalarParams`] is a marker trait automatically implemented for +pub trait ScalarParams: RawParams + Sized { private!(); } -pub trait TensorParams: RawParam { - type Shape: ?Sized; +pub trait TensorParams: RawParams { /// returns the number of dimensions of the parameter fn rank(&self) -> usize; - /// returns the shape of the parameter as a slice - fn shape(&self) -> &Self::Shape; /// returns the size of the parameter fn size(&self) -> usize; } +pub trait ExactDimParams: TensorParams { + type Shape: ?Sized; + /// returns a reference to the shape of the parameter + fn shape(&self) -> &Self::Shape; +} + /* ************* Implementations ************* */ use crate::ParamsBase; use ndarray::{ArrayBase, Dimension, RawData}; -impl RawParam for &T +impl RawParams for &T where - T: RawParam, + T: RawParams, { - type Elem = T::Elem; + type Elem = A; seal! {} } -impl RawParam for &mut T +impl RawParams for &mut T where - T: RawParam, + T: RawParams, { - type Elem = T::Elem; + type Elem = A; seal! {} } -impl ScalarParam for T +impl ScalarParams for T where - T: RawParam, + T: RawParams, { - seal!(); + seal! {} } -macro_rules! impl_param { +macro_rules! impl_scalar_param { ($($T:ty),* $(,)?) => { - $(impl_param!(@impl $T);)* + $(impl_scalar_param!(@impl $T);)* }; (@impl $T:ty) => { - impl RawParam for $T { + impl RawParams for $T { type Elem = $T; seal! {} } impl TensorParams for $T { - type Shape = [usize; 0]; - fn rank(&self) -> usize { 0 } - fn shape(&self) -> &Self::Shape { - &[] - } - fn size(&self) -> usize { 1 } } + + impl ExactDimParams for $T { + type Shape = [usize; 0]; + + fn shape(&self) -> &Self::Shape { + &[] + } + } }; } -impl_param! { +impl_scalar_param! { u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, f32, f64, @@ -104,13 +108,13 @@ impl_param! { } #[cfg(feature = "alloc")] -impl RawParam for alloc::string::String { - type Elem = u8; +impl RawParams for alloc::string::String { + type Elem = alloc::string::String; seal! {} } -impl RawParam for ArrayBase +impl RawParams for ArrayBase where D: Dimension, S: RawData, @@ -125,22 +129,28 @@ where D: Dimension, S: RawData, { - type Shape = [usize]; - fn rank(&self) -> usize { self.ndim() } - fn shape(&self) -> &Self::Shape { - self.shape() - } - fn size(&self) -> usize { self.len() } } -impl RawParam for ParamsBase +impl ExactDimParams for ArrayBase +where + D: Dimension, + S: RawData, +{ + type Shape = [usize]; + + fn shape(&self) -> &[usize] { + self.shape() + } +} + +impl RawParams for ParamsBase where D: Dimension, S: RawData, @@ -155,57 +165,77 @@ where D: Dimension, S: RawData, { - type Shape = [usize]; - fn rank(&self) -> usize { self.weights().ndim() } - fn shape(&self) -> &[usize] { - self.weights().shape() - } - fn size(&self) -> usize { self.weights().len() } } -impl RawParam for [T; N] +impl ExactDimParams for ParamsBase where - T: RawParam, + D: Dimension, + S: RawData, { - type Elem = T::Elem; + type Shape = [usize]; + + fn shape(&self) -> &[usize] { + self.weights().shape() + } +} + +impl RawParams for [T] { + type Elem = T; seal! {} } -impl TensorParams for [T; N] -where - T: RawParam, -{ - type Shape = [usize; 1]; +impl RawParams for &[T] { + type Elem = T; + + seal! {} +} + +impl RawParams for &mut [T] { + type Elem = T; + + seal! {} +} +impl RawParams for [T; N] { + type Elem = T; + + seal! {} +} + +impl TensorParams for [T; N] { fn rank(&self) -> usize { 1 } - fn shape(&self) -> &Self::Shape { - &[N] - } - fn size(&self) -> usize { N } } +impl ExactDimParams for [T; N] { + type Shape = [usize; 1]; + + fn shape(&self) -> &Self::Shape { + &[N] + } +} + #[cfg(feature = "alloc")] mod impl_alloc { use super::*; use alloc::vec::Vec; - impl RawParam for Vec + impl RawParams for Vec where - T: RawParam, + T: RawParams, { type Elem = T::Elem; diff --git a/params/src/traits/shape.rs b/params/src/traits/shape.rs new file mode 100644 index 00000000..93624aaf --- /dev/null +++ b/params/src/traits/shape.rs @@ -0,0 +1,31 @@ +/* + Appellation: shape + Created At: 2025.12.14:11:03:05 + Contrib: @FL03 +*/ +use ndarray::RemoveAxis; + +pub trait GetBiasDim +where + D: RemoveAxis, +{ + type Output; + + fn get_bias_dim(&self) -> Self::Output; +} + +/* + ************* Implementations ************* +*/ + +impl GetBiasDim for U +where + D: RemoveAxis, + U: AsRef>, +{ + type Output = D::Smaller; + + fn get_bias_dim(&self) -> Self::Output { + crate::extract_bias_dim(self) + } +} diff --git a/params/src/traits/wnb.rs b/params/src/traits/wnb.rs index 23ca94df..5b44acea 100644 --- a/params/src/traits/wnb.rs +++ b/params/src/traits/wnb.rs @@ -3,51 +3,32 @@ Created At: 2025.11.28:21:21:42 Contrib: @FL03 */ +use ndarray::iter as nditer; use ndarray::{ArrayBase, Data, DataMut, Dimension, RawData}; +/// A trait denoting an implementor with weights and associated methods pub trait Weighted::Elem>: Sized where D: Dimension, S: RawData, { + type Tensor<_S, _D, _A> + where + _D: Dimension, + _S: RawData; /// returns the weights of the model - fn weights(&self) -> &ArrayBase; + fn weights(&self) -> &Self::Tensor; /// returns a mutable reference to the weights of the model - fn weights_mut(&mut self) -> &mut ArrayBase; - /// assigns the given bias to the current weight - fn assign_weights(&mut self, weights: &ArrayBase) -> &mut Self - where - S: DataMut, - S::Elem: Clone, - { - self.weights_mut().assign(weights); - self - } + fn weights_mut(&mut self) -> &mut Self::Tensor; /// replaces the current weights with the given weights - fn replace_weights(&mut self, weights: ArrayBase) -> ArrayBase { + fn replace_weights(&mut self, weights: Self::Tensor) -> Self::Tensor { core::mem::replace(self.weights_mut(), weights) } /// sets the weights of the model - fn set_weights(&mut self, weights: ArrayBase) -> &mut Self { + fn set_weights(&mut self, weights: Self::Tensor) -> &mut Self { *self.weights_mut() = weights; self } - /// returns an iterator over the weights; see [`iter`](ndarray::iter::Iter) for more information - fn iter_weights<'a>(&'a self) -> ndarray::iter::Iter<'a, S::Elem, D> - where - S: Data + 'a, - D: 'a, - { - self.weights().iter() - } - /// returns a mutable iterator over the weights; see [`iter_mut`](ndarray::iter::IterMut) for more information - fn iter_weights_mut<'a>(&'a mut self) -> ndarray::iter::IterMut<'a, S::Elem, D> - where - S: DataMut + 'a, - D: 'a, - { - self.weights_mut().iter_mut() - } } pub trait Biased::Elem>: Weighted @@ -78,7 +59,7 @@ where self } /// returns an iterator over the bias - fn iter_bias<'a>(&'a self) -> ndarray::iter::Iter<'a, S::Elem, D::Smaller> + fn iter_bias<'a>(&'a self) -> nditer::Iter<'a, S::Elem, D::Smaller> where S: Data + 'a, D: 'a, @@ -86,7 +67,7 @@ where self.bias().iter() } /// returns a mutable iterator over the bias - fn iter_bias_mut<'a>(&'a mut self) -> ndarray::iter::IterMut<'a, S::Elem, D::Smaller> + fn iter_bias_mut<'a>(&'a mut self) -> nditer::IterMut<'a, S::Elem, D::Smaller> where S: DataMut + 'a, D: 'a, @@ -94,7 +75,3 @@ where self.bias_mut().iter_mut() } } - -/* - ************* Implementations ************* -*/ diff --git a/params/src/types/aliases.rs b/params/src/types/aliases.rs deleted file mode 100644 index f787c182..00000000 --- a/params/src/types/aliases.rs +++ /dev/null @@ -1,24 +0,0 @@ -/* - appellation: aliases - authors: @FL03 -*/ -use crate::params_base::ParamsBase; - -use ndarray::{CowRepr, Ix2, OwnedArcRepr, OwnedRepr, RawViewRepr, ViewRepr}; - -/// A type alias for a [`ParamsBase`] with an owned internal layout -pub type Params = ParamsBase, D, A>; -/// A type alias for shared parameters -pub type ArcParams = ParamsBase, D, A>; -/// A type alias for an immutable view of the parameters -pub type ParamsView<'a, A, D = Ix2> = ParamsBase, D, A>; -/// A type alias for a mutable view of the parameters -pub type ParamsViewMut<'a, A, D = Ix2> = ParamsBase, D, A>; -/// A type alias for a [`ParamsBase`] with a _borrowed_ internal layout -pub type CowParams<'a, A, D = Ix2> = ParamsBase, D, A>; -/// A type alias for the [`ParamsBase`] whose elements are of type `*const A` using a -/// [`RawViewRepr`] layout -pub type RawViewParams = ParamsBase, D, A>; -/// A type alias for the [`ParamsBase`] whose elements are of type `*mut A` using a -/// [`RawViewRepr`] layout -pub type RawMutParams = ParamsBase, D, A>; diff --git a/params/src/utils/shape.rs b/params/src/utils/shape.rs new file mode 100644 index 00000000..e8e82bff --- /dev/null +++ b/params/src/utils/shape.rs @@ -0,0 +1,34 @@ +/* + Appellation: shape + Created At: 2025.12.14:07:37:48 + Contrib: @FL03 +*/ +use ndarray::{Axis, LayoutRef, RemoveAxis}; + +/// Extract a suitable dimension for a bias tensor from the given reference to the layout of +/// the weight tensor. +pub fn extract_bias_dim(layout: impl AsRef>) -> D::Smaller +where + D: RemoveAxis, +{ + let layout = layout.as_ref(); + let dim = layout.raw_dim(); + dim.remove_axis(Axis(0)) +} + +#[cfg(test)] +mod tests { + use super::extract_bias_dim; + use ndarray::{Array, array}; + + #[test] + fn test_extract_bias_dim() { + let layout = Array::linspace(0f32, 1f32, 100); + let bias_dim = extract_bias_dim(&layout); + assert_eq!(bias_dim, ndarray::Ix0()); + + let layout = array![[1., 2., 3.], [4., 5., 6.]]; + let bias_dim = extract_bias_dim(&layout); + assert_eq!(bias_dim, ndarray::Ix1(3)); + } +} diff --git a/params/tests/create.rs b/params/tests/create.rs index aaa517d4..984678bf 100644 --- a/params/tests/create.rs +++ b/params/tests/create.rs @@ -34,7 +34,7 @@ fn test_params_zeros() { #[test] #[cfg(feature = "rand")] fn test_params_init_rand() -> anyhow::Result<()> { - use concision_init::NdInit; + use concision_init::NdRandom; let lecun = Params::::lecun_normal((3, 4)); assert_eq!(lecun.dim(), (3, 4)); diff --git a/params/tests/propagate.rs b/params/tests/propagate.rs index 7041232a..06339934 100644 --- a/params/tests/propagate.rs +++ b/params/tests/propagate.rs @@ -14,10 +14,13 @@ use ndarray::{Ix2, array}; */ #[test] fn test_params_fwd_dimensionality() { - let params = Params::::ones((3, 4)); + // define the input let input = array![1.0, 2.0, 3.0]; - // should be of shape 4: - let output = params.forward(&input); - assert_eq!(output.dim(), 4); - assert_eq!(output, array![7.0, 7.0, 7.0, 7.0]); + // initialize the params + let params = Params::::ones((3, 4)); + // complete a forward pass + let y = params.forward(&input); + // verify the results + assert_eq!(y.dim(), 4); + assert_eq!(y, array![7.0, 7.0, 7.0, 7.0]); } diff --git a/scripts/nix.sh b/scripts/nix.sh new file mode 100755 index 00000000..0933d257 --- /dev/null +++ b/scripts/nix.sh @@ -0,0 +1,3 @@ +#! /usr/bin/bash + +nix --extra-experimental-features nix-command --extra-experimental-features flakes "$@" \ No newline at end of file diff --git a/shell.nix b/shell.nix new file mode 100644 index 00000000..7507c2ef --- /dev/null +++ b/shell.nix @@ -0,0 +1,10 @@ +{ channel ? "stable", profile ? "default" }: +with import { overlays = [ (import rust-overlay) ]; }; +mkShell { + nativeBuildInputs = [ + (if channel == "nightly" then + rust-bin.selectLatestNightlyWith (toolchain: toolchain.${profile}) + else + rust-bin.${channel}.latest.${profile}) + ]; +} \ No newline at end of file diff --git a/traits/Cargo.toml b/traits/Cargo.toml index 251af85c..160d7fca 100644 --- a/traits/Cargo.toml +++ b/traits/Cargo.toml @@ -16,7 +16,7 @@ version.workspace = true [package.metadata.docs.rs] all-features = false -features = ["full"] +features = ["default"] rustc-args = ["--cfg", "docsrs"] [package.metadata.release] @@ -25,9 +25,6 @@ tag-name = "v{{version}}" [lib] bench = false -crate-type = ["cdylib", "rlib"] -doctest = false -test = true [[test]] name = "complex" @@ -35,12 +32,10 @@ required-features = ["complex"] [dependencies] # custom -variants = { workspace = true } +rspace-traits = { workspace = true } # data structures +hashbrown = { optional = true, workspace = true } ndarray = { workspace = true } -# error-handling -anyhow = { workspace = true } -thiserror = { workspace = true } # macros & utilities paste = { workspace = true } # mathematics @@ -49,58 +44,92 @@ num-complex = { optional = true, workspace = true } num-integer = { workspace = true } num-traits = { workspace = true } # random -getrandom = { optional = true, workspace = true } -rand = { optional = true, workspace = true } -rand_distr = { optional = true, workspace = true } +rand_core = { optional = true, workspace = true } +# serialization +serde = { optional = true, workspace = true } +serde_derive = { optional = true, workspace = true } +# wasmbindgen +wasm-bindgen = { optional = true, workspace = true } [features] default = ["std"] full = [ + "default", "approx", "complex", - "default", + "hashbrown", "rand", + "serde", ] -nightly = [] +autodiff = [ + "nightly", +] -# ************* [FF:Dependencies] ************* +nightly = [ + "hashbrown?/nightly", +] + +# ********* [FF] Environments ********* std = [ "alloc", - "anyhow/std", + "hashbrown?/default", "ndarray/std", "num-complex?/std", "num-integer/std", "num-traits/std", - "rand?/std", - "rand?/std_rng", - "thiserror/std", + "rand_core?/std", + "rspace-traits/std", + "serde?/std", ] wasi = [] -wasm = ["getrandom?/wasm_js"] -# ************* [FF:Dependencies] ************* -alloc = [] +wasm = [] + +# ********* [FF] Dependencies ********* +alloc = [ + "hashbrown?/alloc", + "rspace-traits/alloc", + "serde?/alloc", +] -approx = ["dep:approx", "ndarray/approx"] +approx = [ + "dep:approx", + "ndarray/approx", +] blas = ["ndarray/blas"] -complex = ["dep:num-complex"] +complex = [ + "dep:num-complex", + "rspace-traits/complex", +] -rayon = ["ndarray/rayon"] +hashbrown = [ + "dep:hashbrown", + "alloc", +] + +rayon = [ + "hashbrown?/rayon", + "ndarray/rayon", + "rspace-traits/rayon", +] rand = [ - "dep:rand", - "dep:rand_distr", + "dep:rand_core", "num-complex?/rand", - "rng", + "rspace-traits/rand", ] -rng = [ - "dep:getrandom", - "rand?/small_rng", - "rand?/thread_rng", +serde = [ + "dep:serde", + "dep:serde_derive", + "serde?/derive", + "hashbrown?/serde", + "rspace-traits/serde", ] + +wasm_bindgen = ["dep:wasm-bindgen"] \ No newline at end of file diff --git a/traits/src/apply.rs b/traits/src/apply.rs deleted file mode 100644 index 75da1b81..00000000 --- a/traits/src/apply.rs +++ /dev/null @@ -1,196 +0,0 @@ -/* - appellation: apply - authors: @FL03 -*/ -/// The [`CallInto`] trait is a consuming interface for passing an object into a single-valued -/// function. While the intended affect is the same as [`CallOn`], the difference is that -/// [`CallInto`] enables a transfer of ownership instead of relyin upon a reference. -pub trait CallInto { - type Output; - - /// The `call_into` method allows an object to be passed into a function that takes ownership - /// of the object. This is useful for cases where you want to perform an operation on an - /// object and consume it in the process. - fn call_into(self, f: F) -> Self::Output - where - F: FnOnce(T) -> Self::Output; -} -/// The [`CallOn`] trait enables an object to be passed onto a unary, or single value, function -/// that is applied to the object. -pub trait CallOn: CallInto { - /// The `call_on` method allows an object to be passed onto a function that takes a reference - /// to the object. This is useful for cases where you want to perform an operation on - /// an object without needing to extract it from a container or context. - fn call_on(&self, f: F) -> Self::Output - where - F: FnMut(&T) -> Self::Output; -} -/// The [`CallOnMut`] is a supertrait of the [`CallInto`] trait that enables an object to be -/// passed onto a unary, or single value, function that is applied to the object, but with the -/// ability to mutate the object in-place. -pub trait CallInPlace: CallInto { - /// The `call_on_mut` method allows an object to be passed onto a function that takes a mutable reference - /// to the object. This is useful for cases where you want to perform an operation on - /// an object and mutate it in the process. - fn call_inplace(&mut self, f: F) -> Self::Output - where - F: FnMut(&mut T) -> Self::Output; -} - -/// The [`Apply`] establishes an interface for _owned_ containers that are capable of applying -/// some function onto their elements. -pub trait Apply { - type Cont<_T>; - - fn apply(&self, f: F) -> Self::Cont - where - F: Fn(T) -> U; -} -/// The [`ApplyMut`] trait mutates the each element of the container, in-place, using the given -/// function. -pub trait ApplyMut { - type Cont<_T>; - - fn apply_mut<'a, F>(&'a mut self, f: F) - where - T: 'a, - F: FnMut(T) -> T; -} - -/* - ************* Implementations ************* -*/ -use ndarray::{Array, ArrayBase, Data, DataMut, Dimension, ScalarOperand}; - -impl CallInto for T { - type Output = T; - - fn call_into(self, f: F) -> Self::Output - where - F: FnOnce(T) -> Self::Output, - { - f(self) - } -} - -impl CallOn for T -where - T: CallInto, -{ - fn call_on(&self, mut f: F) -> Self::Output - where - F: FnMut(&T) -> Self::Output, - { - f(self) - } -} - -impl CallInPlace for T -where - T: CallInto, -{ - fn call_inplace(&mut self, mut f: F) -> Self::Output - where - F: FnMut(&mut T) -> Self::Output, - { - f(self) - } -} - -impl Apply for ArrayBase -where - A: ScalarOperand, - D: Dimension, - S: Data, -{ - type Cont = Array; - - fn apply(&self, f: F) -> Self::Cont - where - F: Fn(A) -> V, - { - self.mapv(f) - } -} - -// impl Apply for TensorBase -// where -// A: ScalarOperand, -// D: Dimension, -// S: Data, -// { -// type Cont = Tensor; - -// fn apply(&self, f: F) -> Self::Cont -// where -// F: Fn(A) -> V, -// { -// self.map(f) -// } -// } - -impl Apply for &ArrayBase -where - A: ScalarOperand, - D: Dimension, - S: Data, -{ - type Cont = Array; - - fn apply(&self, f: F) -> Array - where - F: Fn(A) -> B, - { - self.mapv(f) - } -} - -impl Apply for &mut ArrayBase -where - A: ScalarOperand, - D: Dimension, - S: Data, -{ - type Cont = Array; - - fn apply(&self, f: F) -> Array - where - F: Fn(A) -> B, - { - self.mapv(f) - } -} - -impl ApplyMut for ArrayBase -where - A: ScalarOperand, - D: Dimension, - S: DataMut, -{ - type Cont = Array; - - fn apply_mut<'a, F>(&'a mut self, f: F) - where - A: 'a, - F: FnMut(A) -> A, - { - self.mapv_inplace(f) - } -} - -impl ApplyMut for &mut ArrayBase -where - A: ScalarOperand, - D: Dimension, - S: DataMut, -{ - type Cont = Array; - - fn apply_mut<'b, F>(&'b mut self, f: F) - where - A: 'b, - F: FnMut(A) -> A, - { - self.mapv_inplace(f) - } -} diff --git a/traits/src/clip.rs b/traits/src/clip.rs index 95f8b0da..a4c7aeef 100644 --- a/traits/src/clip.rs +++ b/traits/src/clip.rs @@ -39,7 +39,7 @@ pub trait ClipMut { ************* Implementations ************* */ use crate::norm::{L1Norm, L2Norm}; -use ndarray::{ArrayBase, Data, DataMut, Dimension}; +use ndarray::{Array, ArrayBase, Data, DataMut, Dimension}; use num_traits::Float; impl Clip for ArrayBase @@ -48,7 +48,7 @@ where S: Data, D: Dimension, { - type Output = ndarray::Array; + type Output = Array; fn clip(&self, min: A, max: A) -> Self::Output { self.clamp(min, max) diff --git a/traits/src/complex.rs b/traits/src/complex.rs index 04917d68..ff6233ac 100644 --- a/traits/src/complex.rs +++ b/traits/src/complex.rs @@ -2,20 +2,19 @@ Appellation: num Contrib: FL03 */ -#![cfg(feature = "complex")] - -use num_complex::Complex; -use num_traits::Num; +/// [`AsComplex`] defines an interface for converting a reference of some numerical type into a +/// complex number. pub trait AsComplex { type Complex; - + /// converts the current state into a complex number as either the real or imaginary part, + /// depending on the `real` flag fn as_complex(&self, real: bool) -> Self::Complex; - + /// convert a reference of the current state into the real part of a complex number fn as_re(&self) -> Self::Complex { self.as_complex(true) } - + /// convert a reference of the current state into the imaginary part of a complex number fn as_im(&self) -> Self::Complex { self.as_complex(false) } @@ -23,18 +22,19 @@ pub trait AsComplex { /// Trait for converting a type into a complex number. pub trait IntoComplex { type Complex; - + /// converts the current state into a complex number used either as the real or imaginary + /// part, depending on the `real` flag fn into_complex(self, real: bool) -> Self::Complex where Self: Sized; - + /// uses the current state as the real value of a complex number fn into_re(self) -> Self::Complex where Self: Sized, { self.into_complex(true) } - + /// uses the current state as the imaginary value of a complex number fn into_im(self) -> Self::Complex where Self: Sized, @@ -46,34 +46,39 @@ pub trait IntoComplex { /* ********* Implementations ********* */ +#[cfg(feature = "complex")] +mod impl_complex { + use super::{AsComplex, IntoComplex}; -impl AsComplex for T -where - T: Clone + Num, -{ - type Complex = Complex; + use num_complex::Complex; + use num_traits::Zero; - fn as_complex(&self, real: bool) -> Complex { - match real { - true => Complex::new(self.clone(), Self::zero()), - false => Complex::new(Self::zero(), self.clone()), + impl AsComplex for T + where + T: Clone + IntoComplex, + { + type Complex = >::Complex; + + fn as_complex(&self, real: bool) -> Self::Complex { + self.clone().into_complex(real) } } -} - -impl IntoComplex for T -where - T: Num, -{ - type Complex = Complex; - fn into_complex(self, real: bool) -> Self::Complex + impl IntoComplex for T where - Self: Sized, + T: Zero, { - match real { - true => Complex::new(self, T::zero()), - false => Complex::new(T::zero(), self), + type Complex = Complex; + + fn into_complex(self, real: bool) -> Self::Complex + where + Self: Sized, + { + if real { + Complex::new(self, T::zero()) + } else { + Complex::new(T::zero(), self) + } } } } diff --git a/traits/src/container.rs b/traits/src/container.rs deleted file mode 100644 index 826e8a75..00000000 --- a/traits/src/container.rs +++ /dev/null @@ -1,84 +0,0 @@ -/* - Appellation: container - Created At: 2025.11.28:15:30:46 - Contrib: @FL03 -*/ -/// The [`Container`] trait defines a generic interface for container types. -pub trait Container { - type Cont: ?Sized; - type Item; -} - -pub trait KeyValue { - type Cont<_K, _V>; - type Key; - type Value; - - fn key(&self) -> &Self::Key; - fn value(&self) -> &Self::Value; -} - -impl KeyValue for (K, V) { - type Cont<_K, _V> = (_K, _V); - type Key = K; - type Value = V; - - fn key(&self) -> &Self::Key { - &self.0 - } - - fn value(&self) -> &Self::Value { - &self.1 - } -} - -macro_rules! container { - ($( - $($container:ident)::*<$A:ident $(, $B:ident)?> - ),* $(,)?) => { - $(container!(@impl $($container)::*<$A $(, $B)?>);)* - }; - - (@impl $($container:ident)::*<$T:ident>) => { - impl<$T> $crate::container::Container for $($container)::*<$T> { - type Cont = $($container)::*; - type Item = $T; - } - }; - (@impl $($container:ident)::*<$K:ident, $V:ident>) => { - impl<$K, $V> $crate::container::Container for $($container)::*<$K, $V> { - type Cont = $($container)::*<$K, U>; - type Item = $V; - } - }; -} - -impl Container for [T] { - type Cont = [U]; - type Item = T; -} - -container! { - core::option::Option, - core::result::Result, -} - -#[cfg(feature = "alloc")] -container! { - alloc::boxed::Box, - alloc::vec::Vec, - alloc::collections::BTreeMap, - alloc::collections::BTreeSet, - alloc::collections::VecDeque, - alloc::rc::Rc, - alloc::sync::Arc, -} - -#[cfg(feature = "std")] -container! { - std::collections::HashMap, - std::collections::HashSet, - std::cell::Cell, - std::sync::Mutex, - std::sync::RwLock, -} diff --git a/traits/src/convert.rs b/traits/src/convert.rs deleted file mode 100644 index 61ec5aa8..00000000 --- a/traits/src/convert.rs +++ /dev/null @@ -1,43 +0,0 @@ -/* - appellation: convert - authors: @FL03 -*/ -use ndarray::{Axis, Dimension, RemoveAxis}; - -/// The [`IntoAxis`] trait is used to define a conversion routine that takes a type and wraps -/// it in an [`Axis`] type. -pub trait IntoAxis { - fn into_axis(self) -> Axis; -} - -/// The [`AsBiasDim`] trait is used to define a type that can be used to get the bias dimension -/// of the parameters. -pub trait AsBiasDim { - /// returns the bias dimension of the parameters - fn as_bias_dim(&self) -> D; -} - -/* - ************* Implementations ************* -*/ - -impl IntoAxis for S -where - S: AsRef, -{ - fn into_axis(self) -> Axis { - Axis(*self.as_ref()) - } -} - -impl AsBiasDim for A -where - A: RemoveAxis, - B: Dimension, -{ - /// returns the bias dimension of the parameters by removing the "zero-th" axis from the - /// given dimension - fn as_bias_dim(&self) -> B { - self.remove_axis(Axis(0)) - } -} diff --git a/traits/src/error.rs b/traits/src/error.rs deleted file mode 100644 index 568cc1a7..00000000 --- a/traits/src/error.rs +++ /dev/null @@ -1,49 +0,0 @@ -/* - Appellation: error - Contrib: @FL03 -*/ -//! This module implements the core [`Error`] type for the framework and provides a [`Result`] -//! type alias for convenience. -#[cfg(feature = "alloc")] -use alloc::{boxed::Box, string::String}; - -#[allow(dead_code)] -/// a type alias for a [Result](core::result::Result) configured with an [`Error`] as its error -/// type. -pub type Result = core::result::Result; - -/// The [`Error`] type enumerates various errors that can occur within the framework. -#[derive(Debug, thiserror::Error)] -#[non_exhaustive] -pub enum Error { - #[error(transparent)] - FmtError(#[from] core::fmt::Error), - #[cfg(feature = "std")] - #[error(transparent)] - IoError(#[from] std::io::Error), - #[error(transparent)] - ShapeError(#[from] ndarray::ShapeError), - #[error(transparent)] - #[cfg(feature = "rand")] - UniformError(#[from] rand_distr::uniform::Error), - #[cfg(feature = "alloc")] - #[error(transparent)] - BoxError(#[from] Box), - #[cfg(feature = "alloc")] - #[error("Unknown Error: {0}")] - Unknown(String), -} - -#[cfg(feature = "alloc")] -impl From for Error { - fn from(value: String) -> Self { - Self::Unknown(value) - } -} - -#[cfg(feature = "alloc")] -impl From<&str> for Error { - fn from(value: &str) -> Self { - String::from(value).into() - } -} diff --git a/traits/src/math/gradient.rs b/traits/src/gradient.rs similarity index 100% rename from traits/src/math/gradient.rs rename to traits/src/gradient.rs diff --git a/traits/src/impls/impl_backward.rs b/traits/src/impls/impl_backward.rs new file mode 100644 index 00000000..715560e8 --- /dev/null +++ b/traits/src/impls/impl_backward.rs @@ -0,0 +1,33 @@ +/* + Appellation: impl_backward + Created At: 2025.12.14:09:36:08 + Contrib: @FL03 +*/ +use crate::Backward; +use ndarray::linalg::Dot; +use ndarray::{Array, ArrayBase, ArrayView, Data, DataMut, Dimension}; +use num_traits::Num; + +impl Backward, ArrayBase> + for ArrayBase +where + A: 'static + Copy + Num, + D: Dimension, + S: DataMut, + D1: Dimension, + D2: Dimension, + S1: Data, + S2: Data, + for<'b> &'b ArrayBase: Dot, Output = Array>, +{ + type Elem = A; + + fn backward( + &mut self, + input: &ArrayBase, + delta: &ArrayBase, + gamma: Self::Elem, + ) { + self.scaled_add(gamma, &input.dot(&delta.t())) + } +} diff --git a/traits/src/impls/impl_forward.rs b/traits/src/impls/impl_forward.rs new file mode 100644 index 00000000..3a2181bd --- /dev/null +++ b/traits/src/impls/impl_forward.rs @@ -0,0 +1,56 @@ +/* + Appellation: impl_forward + Created At: 2025.12.14:09:36:14 + Contrib: @FL03 +*/ +use crate::{Forward, ForwardMut, ForwardOnce}; + +use ndarray::linalg::Dot; +use ndarray::{ArrayBase, Data, Dimension}; + +impl ForwardOnce for F +where + F: FnOnce(X) -> Y, +{ + type Output = Y; + + fn forward_once(self, input: X) -> Self::Output { + self(input) + } +} + +impl Forward for F +where + F: Fn(&X) -> Y, +{ + type Output = Y; + + fn forward(&self, input: &X) -> Self::Output { + self(input) + } +} + +impl ForwardMut for F +where + F: FnMut(&X) -> Y, +{ + type Output = Y; + + fn forward_mut(&mut self, input: &X) -> Self::Output { + self(input) + } +} + +impl Forward for ArrayBase +where + A: Clone, + D: Dimension, + S: Data, + for<'a> X: Dot, Output = Y>, +{ + type Output = Y; + + fn forward(&self, input: &X) -> Self::Output { + input.dot(self) + } +} diff --git a/traits/src/init.rs b/traits/src/init.rs new file mode 100644 index 00000000..6489b9e4 --- /dev/null +++ b/traits/src/init.rs @@ -0,0 +1,34 @@ +/* + Appellation: init + Created At: 2026.01.13:18:38:50 + Contrib: @FL03 +*/ + +/// [`InitWith`] enables a container to +pub trait InitWith { + type Cont; + /// consumes the current instance to initialize a new one + fn init_with(f: F) -> Self::Cont + where + F: FnOnce() -> U, + Self: Sized; +} + +#[cfg(feature = "rand")] +/// The [`InitRand`] trait provides a generic interface for initializing objects using +/// random number generators. This trait is particularly useful for types that require +/// random initialization, such as neural network weights, biases, or other parameters. +pub trait InitRand { + type Output; + /// use the provided random number generator `rng` to initialize the object + fn init_random(rng: &mut R) -> Self::Output; +} + +/// [`Initialize`] provides a mechanism for _initializing_ some object using a value of type +/// `T` to produce another object. +pub trait Initialize { + type Output; + /// initializes the object using the given value, consuming the caller to produce another + /// object + fn init(self, with: T) -> Self::Output; +} diff --git a/traits/src/lib.rs b/traits/src/lib.rs index 257f629d..cfe252ec 100644 --- a/traits/src/lib.rs +++ b/traits/src/lib.rs @@ -1,8 +1,6 @@ -/* - Appellation: concision-traits - Contrib: @FL03 -*/ -//! Traits for the concicion machine learning framework +//! Core traits defining fundamental abstractions and operations useful for neural networks. +//! +#![crate_type = "lib"] #![allow( clippy::missing_safety_doc, clippy::module_inception, @@ -12,17 +10,16 @@ rustdoc::redundant_explicit_links )] #![cfg_attr(not(feature = "std"), no_std)] -#![cfg_attr(feature = "nightly", feature(allocator_api))] -#![crate_type = "lib"] - +#![cfg_attr(all(feature = "nightly", feature = "alloc"), feature(allocator_api))] +#![cfg_attr(all(feature = "nightly", feature = "autodiff"), feature(autodiff))] +// compile-time checks #[cfg(not(any(feature = "std", feature = "alloc")))] compiler_error! { "At least one of the \"std\" or \"alloc\" features must be enabled for the crate to compile." } - +// external crates #[cfg(feature = "alloc")] extern crate alloc; -extern crate ndarray as nd; #[macro_use] pub(crate) mod macros { @@ -30,19 +27,21 @@ pub(crate) mod macros { pub mod seal; } -pub mod error; +mod impls { + mod impl_backward; + mod impl_forward; +} -mod apply; mod clip; mod codex; mod complex; -mod container; -mod convert; mod entropy; +mod gradient; +mod init; mod loss; mod norm; mod predict; -mod propagation; +mod propagate; mod rounding; mod store; mod training; @@ -50,10 +49,10 @@ mod training; pub mod math { //! Mathematically oriented operators and functions useful in machine learning contexts. #[doc(inline)] - pub use self::{difference::*, gradient::*, roots::*, stats::*, unary::*}; + pub use self::{linalg::*, percentages::*, roots::*, stats::*, unary::*}; - mod difference; - mod gradient; + mod linalg; + mod percentages; mod roots; mod stats; mod unary; @@ -62,51 +61,49 @@ pub mod math { pub mod ops { //! composable operators for tensor manipulations and transformations, neural networks, and //! more - #[allow(unused_imports)] #[doc(inline)] - pub use self::{binary::*, unary::*}; + pub use self::{apply::*, fill::*, like::*, map::*, reshape::*, unary::*}; - mod binary; + mod apply; + mod fill; + mod like; + mod map; + mod reshape; mod unary; } pub mod tensor { #[doc(inline)] - pub use self::{dimensionality::*, fill::*, like::*, linalg::*, ndtensor::*, reshape::*}; + pub use self::{dimensionality::*, ndtensor::*, tensor_data::*}; mod dimensionality; - mod fill; - mod like; - mod linalg; mod ndtensor; - mod reshape; + mod tensor_data; } // re-exports #[doc(inline)] -pub use self::error::*; -#[doc(inline)] -pub use self::prelude::*; - +pub use self::{ + clip::*, codex::*, complex::*, entropy::*, gradient::*, init::*, loss::*, math::*, norm::*, + ops::*, predict::*, propagate::*, rounding::*, store::*, tensor::*, training::*, +}; +// prelude #[doc(hidden)] pub mod prelude { - pub use crate::math::*; - pub use crate::tensor::*; - - pub use crate::apply::*; pub use crate::clip::*; pub use crate::codex::*; - pub use crate::container::*; - pub use crate::convert::*; + pub use crate::complex::*; pub use crate::entropy::*; + pub use crate::gradient::*; + pub use crate::init::*; pub use crate::loss::*; + pub use crate::math::*; pub use crate::norm::*; + pub use crate::ops::*; pub use crate::predict::*; - pub use crate::propagation::*; + pub use crate::propagate::*; pub use crate::rounding::*; pub use crate::store::*; + pub use crate::tensor::*; pub use crate::training::*; - - #[cfg(feature = "complex")] - pub use crate::complex::*; } diff --git a/traits/src/math/difference.rs b/traits/src/math/difference.rs deleted file mode 100644 index f8f1fdc6..00000000 --- a/traits/src/math/difference.rs +++ /dev/null @@ -1,17 +0,0 @@ -/* - Appellation: difference - Created At: 2025.11.26:12:09:51 - Contrib: @FL03 -*/ - -/// Compute the percentage difference between two values. -/// The percentage difference is defined as: -/// -/// ```text -/// percent_diff = |x, y| 100 * |x - y| / ((|x| + |y|) / 2) -/// ``` -pub trait PercentDiff { - type Output; - - fn percent_diff(self, rhs: Rhs) -> Self::Output; -} diff --git a/traits/src/tensor/linalg.rs b/traits/src/math/linalg.rs similarity index 100% rename from traits/src/tensor/linalg.rs rename to traits/src/math/linalg.rs diff --git a/traits/src/math/percentages.rs b/traits/src/math/percentages.rs new file mode 100644 index 00000000..fb9e34e9 --- /dev/null +++ b/traits/src/math/percentages.rs @@ -0,0 +1,66 @@ +/* + Appellation: difference + Created At: 2025.11.26:12:09:51 + Contrib: @FL03 +*/ +/// The [`PercentChange`] trait establishes a binary operator for computing the percent change +/// between two values where the caller is considered the original value. +pub trait PercentChange { + type Output; + + fn percent_change(self, rhs: Rhs) -> Self::Output; +} +/// Compute the percentage difference between two values. +/// The percentage difference is defined as: +/// +/// ```math +/// \text{PercentDifference}(x, y) = 2\cdot\frac{|x - y|}{|x| + |y|} +/// ``` +pub trait PercentDiff { + type Output; + + fn percent_diff(self, rhs: Rhs) -> Self::Output; +} + +/* + ************* Implementations ************* +*/ +use num_traits::{FromPrimitive, NumOps, Signed, Zero}; + +impl PercentDiff for T +where + T: Copy + Signed + Zero + FromPrimitive + NumOps, +{ + type Output = T; + + fn percent_diff(self, rhs: T) -> Self::Output { + T::from_u8(2).unwrap() * (self - rhs).abs() / (self.abs() + rhs.abs()) + } +} + +impl PercentChange for A +where + C: core::ops::Div, + for<'b> A: core::ops::Sub<&'b B, Output = C>, +{ + type Output = C; + + fn percent_change(self, rhs: B) -> Self::Output { + (self - &rhs) / rhs + } +} + +// macro_rules! impl_percent_change { +// ($($T:ty),* $(,)?) => { +// $(impl_percent_change! { @impl $T })* +// }; +// (@impl $T:ty) => { +// impl PercentChange<$T> for $T { +// type Output = $T; + +// fn percent_change(self, rhs: $T) -> Self::Output { +// (self - rhs) / rhs +// } +// } +// }; +// } diff --git a/traits/src/math/unary.rs b/traits/src/math/unary.rs index ce5bc8b5..596b9b4c 100644 --- a/traits/src/math/unary.rs +++ b/traits/src/math/unary.rs @@ -6,34 +6,46 @@ use ndarray::{Array, ArrayBase, Data, Dimension}; use num_traits::Signed; macro_rules! unary { - (@branch $name:ident::$call:ident($($rest:tt)*)) => { - unary!(@impl $name::$call($($rest)*)); - }; - (@impl $name:ident::$call:ident(self)) => { + (@impl $(#[$meta:meta])* $name:ident::$call:ident($($rest:tt)*)) => { + $(#[$meta])* pub trait $name { type Output; - fn $call(self) -> Self::Output; + fn $call($($rest)*) -> Self::Output; } }; - (@impl $name:ident::$call:ident(&self)) => { - pub trait $name { - type Output; + ($($(#[$meta:meta])*$name:ident::$call:ident($($rest:tt)*)),* $(,)?) => { + $(unary! { @impl $(#[$meta])* $name::$call($($rest)*) })* + }; +} + +macro_rules! impl_unary_op { + (@impl $name:ident::<$T:ty>::$method:ident) => { + impl $name for $T { + type Output = $T; - fn $call(&self) -> Self::Output; + fn $method(self) -> Self::Output { + <$T>::$method(self) + } } }; - (@impl $name:ident::$call:ident(&mut self)) => { - pub trait $name { - type Output; + ($($name:ident::<[$($T:ty),*]>::$method:ident),* $(,)?) => { + $($(impl_unary_op! { @impl $name::<$T>::$method })*)* + }; +} + +macro_rules! impl_something { + (@impl $trait:ident::<$T:ty>::$method:ident($self:ident $(, $($input:ident: $I:ty),*)?) -> $out:ty {$func:expr}) => { + impl $trait for $T { + type Output = $out; - fn $call(&self) -> Self::Output; + fn $method($self $(, $($input: $I),*)?) -> Self::Output { + $func + } } }; - ($($name:ident::$call:ident($($rest:tt)*)),* $(,)?) => { - $( - unary!(@impl $name::$call($($rest)*)); - )* + ($($trait:ident::<[$($T:ty),* $(,)?]>::$method:ident($self:ident) -> $out:ty {$func:expr});* $(;)?) => { + $($(impl_something! { @impl $trait::<$T>::$method($self) -> $out {$func} } )*)* }; } @@ -48,52 +60,12 @@ unary! { Tanh::tanh(self), Squared::pow2(self), Cubed::pow3(self), - SquareRoot::sqrt(self) -} - -unary! { + SquareRoot::sqrt(self), Conjugate::conj(&self), } -/* - ********* Implementations ********* -*/ - -macro_rules! unary_impl { - ($($name:ident<$T:ty$(, Output = $O:ty)?>::$method:ident),* $(,)?) => { - $(unary_impl!(@impl $name::$method<$T$(, Output = $O>)?);)* - }; - ($($name:ident::<$T:ty, Output = $O:ty>::$method:ident),* $(,)?) => { - $(unary_impl!(@impl $name::$method<$T, Output = $O>);)* - }; - ($($name:ident::<[$($T:ty),*]>::$method:ident),* $(,)?) => { - $(unary_impl!(@loop $name::$method<[$($T),*]>);)* - }; - (@loop $name:ident::<[$($T:ty),* $(,)?]>::$method:ident) => { - $(unary_impl!(@impl $name::<$T>::$method);)* - }; - (@impl $name:ident::<$T:ty>::$method:ident) => { - unary_impl!(@impl $name::<$T, Output = $T>::$method); - }; - (@impl $name:ident::<$T:ty, Output = $O:ty>::$method:ident) => { - impl $name for $T { - type Output = $O; - - fn $method(self) -> Self::Output { - <$T>::$method(self) - } - } - }; -} - -macro_rules! unary_impls { - ($($name:ident::<[$($T:ty),* $(,)?]>::$method:ident),* $(,)?) => { - $(unary_impl!(@loop $name::<[$($T),*]>::$method);)* - }; -} - -unary_impls! { - Abs::<[f32, f64]>::abs, +impl_unary_op! { + Abs::<[i8, i16, i32, i64, i128, isize, f32, f64]>::abs, Cos::<[f32, f64]>::cos, Cosh::<[f32, f64]>::cosh, Exp::<[f32, f64]>::exp, @@ -104,9 +76,14 @@ unary_impls! { SquareRoot::<[f32, f64]>::sqrt } -/* - ************* implementations ************* -*/ +impl_something! { + Squared::<[u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, f32, f64]>::pow2(self) -> Self { + self * self + }; + Cubed::<[u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, f32, f64]>::pow3(self) -> Self { + self * self * self + }; +} impl Abs for ArrayBase where @@ -134,17 +111,6 @@ where } } -impl Squared for A -where - A: Clone + core::ops::Mul, -{ - type Output = A; - - fn pow2(self) -> Self::Output { - self.clone() * self - } -} - impl SquareRoot for ArrayBase where A: Clone + SquareRoot, @@ -158,27 +124,25 @@ where } } -#[cfg(not(feature = "complex"))] -impl Exp for &ArrayBase +impl Exp for ArrayBase where - A: Clone + Exp, + A: Clone + Exp, D: Dimension, S: Data, { - type Output = Array; + type Output = Array; fn exp(self) -> Self::Output { self.mapv(|x| x.exp()) } } - -impl Exp for ArrayBase +impl Exp for &ArrayBase where - A: Clone + Exp, + A: Clone + Exp, D: Dimension, S: Data, { - type Output = Array; + type Output = Array; fn exp(self) -> Self::Output { self.mapv(|x| x.exp()) @@ -188,8 +152,40 @@ where #[cfg(feature = "complex")] mod impl_complex { use super::*; + use ndarray::{Array, Dimension}; use num_complex::{Complex, ComplexFloat}; + use num_traits::Signed; + + macro_rules! impl_complex_for { + (@impl $name:ident::<$T:ident>::$method:ident) => { + #[cfg(feature = "complex")] + impl<$T> $name for num_complex::Complex<$T> + where + num_complex::Complex<$T>: num_complex::ComplexFloat, + { + type Output = num_complex::Complex<$T>; + + fn $method(self) -> Self::Output { + num_complex::ComplexFloat::$method(self) + } + } + }; + ($($name:ident::<$T:ident>::$method:ident),* $(,)?) => { + $(impl_complex_for!(@impl $name::<$T>::$method);)* + }; + } + + impl_complex_for! { + Cos::::cos, + Cosh::::cosh, + Exp::::exp, + Sine::::sin, + Sinh::::sinh, + Tan::::tan, + Tanh::::tanh, + SquareRoot::::sqrt, + } macro_rules! impl_conj { ($($t:ident<$res:ident>),*) => { @@ -231,39 +227,4 @@ mod impl_complex { self.mapv(|x| x.conj()) } } - - impl Cos for Complex - where - Complex: ComplexFloat, - { - type Output = Self; - - fn cos(self) -> Self::Output { - ComplexFloat::cos(self) - } - } - - impl Exp for &ArrayBase - where - A: Clone + ComplexFloat, - D: Dimension, - S: Data, - { - type Output = Array; - - fn exp(self) -> Self::Output { - self.mapv(|x| x.exp()) - } - } - - impl SquareRoot for Complex - where - Complex: ComplexFloat, - { - type Output = Self; - - fn sqrt(self) -> Self::Output { - ComplexFloat::sqrt(self) - } - } } diff --git a/traits/src/ops/apply.rs b/traits/src/ops/apply.rs new file mode 100644 index 00000000..78e023f6 --- /dev/null +++ b/traits/src/ops/apply.rs @@ -0,0 +1,66 @@ +/* + appellation: apply + authors: @FL03 +*/ + +/// [`Apply`] is a composable binary operator generally used to apply some object or function +/// onto the caller to produce some output. +pub trait Apply { + type Output; + + fn apply(&self, rhs: Rhs) -> Self::Output; +} +/// The [`ApplyOnce`] trait consumes the container and applies the given function to every +/// element before returning a new container with the results. +pub trait ApplyOnce { + type Output; + + fn apply_once(self, rhs: Rhs) -> Self::Output; +} +/// [`ApplyMut`] provides an interface for mutable containers that can apply a function onto +/// their elements, modifying them in place. +pub trait ApplyMut { + fn apply_mut(&mut self, rhs: Rhs); +} + +/* + ************* Implementations ************* +*/ +use ndarray::{Array, ArrayBase, Data, DataMut, Dimension}; + +impl ApplyOnce for Option +where + F: FnOnce(U) -> V, +{ + type Output = Option; + + fn apply_once(self, f: F) -> Self::Output { + self.map(f) + } +} + +impl ApplyMut for ArrayBase +where + A: Clone, + D: Dimension, + S: DataMut, + F: FnMut(A) -> A, +{ + fn apply_mut(&mut self, f: F) { + self.mapv_inplace(f) + } +} + +impl Apply for ArrayBase +where + A: Clone, + D: Dimension, + S: Data, + F: Fn(A) -> B, +{ + type Output = Array; + + fn apply(&self, f: F) -> Self::Output { + self.mapv(f) + } +} diff --git a/traits/src/ops/binary.rs b/traits/src/ops/binary.rs deleted file mode 100644 index 35f152e6..00000000 --- a/traits/src/ops/binary.rs +++ /dev/null @@ -1,5 +0,0 @@ -/* - Appellation: binary - Created At: 2025.12.09:07:27:17 - Contrib: @FL03 -*/ diff --git a/traits/src/tensor/fill.rs b/traits/src/ops/fill.rs similarity index 100% rename from traits/src/tensor/fill.rs rename to traits/src/ops/fill.rs diff --git a/traits/src/tensor/like.rs b/traits/src/ops/like.rs similarity index 100% rename from traits/src/tensor/like.rs rename to traits/src/ops/like.rs diff --git a/traits/src/ops/map.rs b/traits/src/ops/map.rs new file mode 100644 index 00000000..e6bbd86b --- /dev/null +++ b/traits/src/ops/map.rs @@ -0,0 +1,102 @@ +/* + Appellation: map + Created At: 2026.01.06:13:50:37 + Contrib: @FL03 +*/ +/// [`MapInto`] defines an interface for containers that can consume themselves to apply a given +/// function onto each of their elements. +pub trait MapInto +where + F: FnOnce(Self::Elem) -> U, +{ + type Cont<_T>; + type Elem; + + fn mapi(self, f: F) -> Self::Cont; +} + +/// [`MapTo`] establishes an interface for containers capable of applying a given function onto +/// each of their elements, by reference. +pub trait MapTo +where + F: FnOnce(Self::Elem) -> U, +{ + type Cont<_T>; + type Elem; + + fn mapt(&self, f: F) -> Self::Cont; +} + +/* + ************* Implementations ************* +*/ +use ndarray::{Array, ArrayBase, Data, Dimension}; + +impl MapInto for Option +where + F: FnOnce(U) -> V, +{ + type Cont = Option; + type Elem = U; + + fn mapi(self, f: F) -> Self::Cont { + self.map(f) + } +} + +impl<'a, U, V, F> MapTo for Option<&'a U> +where + for<'b> F: FnOnce(&'b U) -> V, +{ + type Cont = Option; + type Elem = &'a U; + + fn mapt(&self, f: F) -> Self::Cont { + self.as_ref().map(|a| f(a)) + } +} + +impl MapInto for ArrayBase +where + A: Clone, + D: Dimension, + S: Data, + F: Fn(A) -> B, +{ + type Cont = Array; + type Elem = A; + + fn mapi(self, f: F) -> Self::Cont { + self.mapv(f) + } +} + +impl MapInto for &ArrayBase +where + A: Clone, + D: Dimension, + S: Data, + F: Fn(A) -> B, +{ + type Cont = Array; + type Elem = A; + + fn mapi(self, f: F) -> Self::Cont { + self.mapv(f) + } +} + +impl MapTo for ArrayBase +where + A: Clone, + D: Dimension, + S: Data, + F: Fn(A) -> B, +{ + type Cont = Array; + type Elem = A; + + fn mapt(&self, f: F) -> Self::Cont { + self.mapv(f) + } +} diff --git a/traits/src/tensor/reshape.rs b/traits/src/ops/reshape.rs similarity index 100% rename from traits/src/tensor/reshape.rs rename to traits/src/ops/reshape.rs diff --git a/traits/src/propagate.rs b/traits/src/propagate.rs new file mode 100644 index 00000000..64664bec --- /dev/null +++ b/traits/src/propagate.rs @@ -0,0 +1,50 @@ +/* + Appellation: propagate + Created At: 2026.01.06:14:13:38 + Contrib: @FL03 +*/ + +/// The [`Backward`] trait establishes a common interface for completing a single backward +/// step in a neural network or machine learning model. +pub trait Backward { + type Elem; + + fn backward(&mut self, input: &X, delta: &Delta, gamma: Self::Elem); +} + +pub trait BackwardStep { + type Data<_X>; + type Grad<_X>; + type Output; + + fn backward(&mut self, input: &Self::Data, delta: &Self::Grad, gamma: T) -> Self::Output; +} + +/// A consuming implementation of forward propagation +pub trait ForwardOnce { + type Output; + /// a single forward step consuming the implementor + fn forward_once(self, input: Rhs) -> Self::Output; +} +/// The [`Forward`] trait describes a common interface for objects designated to perform a +/// single forward step in a neural network or machine learning model. +pub trait Forward { + type Output; + /// a single forward step + fn forward(&self, input: &Rhs) -> Self::Output; + /// this method enables the forward pass to be generically _activated_ using some closure. + /// This is useful for isolating the logic of the forward pass from that of the activation + /// function and is often used by layers and models. + fn forward_then(&self, input: &Rhs, then: F) -> Self::Output + where + F: FnOnce(Self::Output) -> Self::Output, + { + then(self.forward(input)) + } +} + +pub trait ForwardMut { + type Output; + /// a single forward step with mutable access + fn forward_mut(&mut self, input: &Rhs) -> Self::Output; +} diff --git a/traits/src/propagation.rs b/traits/src/propagation.rs deleted file mode 100644 index d1840e2e..00000000 --- a/traits/src/propagation.rs +++ /dev/null @@ -1,98 +0,0 @@ -/* - Appellation: predict - Contrib: @FL03 -*/ - -/// The [`PropagationError`] type defines custom errors that can occur during forward and -/// backward propagation. -#[derive(Debug, thiserror::Error)] -#[non_exhaustive] -pub enum PropagationError { - #[error("Forward Propagation Error: {0}")] - ForwardError(&'static str), - #[error("Backward Propagation Error: {0}")] - BackwardError(&'static str), - #[error("Mismatched Dimensions")] - MismatchedDimensions, - #[error("Invalid Input")] - InvalidInput, -} - -/// The [`Backward`] trait establishes a common interface for completing a single backward -/// step in a neural network or machine learning model. -pub trait Backward { - type Elem; - - fn backward(&mut self, input: &X, delta: &Delta, gamma: Self::Elem); -} - -pub trait BackwardStep { - type Data<_X>; - type Grad<_X>; - type Output; - - fn backward(&mut self, input: &Self::Data, delta: &Self::Grad, gamma: T) -> Self::Output; -} - -/// The [`Forward`] trait describes a common interface for objects designated to perform a -/// single forward step in a neural network or machine learning model. -pub trait Forward { - type Output; - /// a single forward step - fn forward(&self, input: &Rhs) -> Self::Output; - /// this method enables the forward pass to be generically _activated_ using some closure. - /// This is useful for isolating the logic of the forward pass from that of the activation - /// function and is often used by layers and models. - fn forward_then(&self, input: &Rhs, then: F) -> Self::Output - where - F: FnOnce(Self::Output) -> Self::Output, - { - then(self.forward(input)) - } -} - -/* - ************* Implementations ************* -*/ - -use ndarray::linalg::Dot; -use ndarray::{Array, ArrayBase, ArrayView, Data, DataMut, Dimension}; -use num_traits::Num; - -impl Backward, ArrayBase> - for ArrayBase -where - A: 'static + Copy + Num, - D: Dimension, - S: DataMut, - D1: Dimension, - D2: Dimension, - S1: Data, - S2: Data, - for<'b> &'b ArrayBase: Dot, Output = Array>, -{ - type Elem = A; - - fn backward( - &mut self, - input: &ArrayBase, - delta: &ArrayBase, - gamma: Self::Elem, - ) { - self.scaled_add(gamma, &input.dot(&delta.t())) - } -} - -impl Forward for ArrayBase -where - A: Clone, - D: Dimension, - S: Data, - for<'a> X: Dot, Output = Y>, -{ - type Output = Y; - - fn forward(&self, input: &X) -> Self::Output { - input.dot(self) - } -} diff --git a/traits/src/store.rs b/traits/src/store.rs index ccf11fc4..7482cccc 100644 --- a/traits/src/store.rs +++ b/traits/src/store.rs @@ -1,193 +1,288 @@ /* - appellation: store - authors: @FL03 + Appellation: key_value + Created At: 2026.01.13:20:49:22 + Contrib: @FL03 */ -/// The [`RawStore`] trait provides a generalized interface for all _containers_. The trait is -/// sealed, preventing any external implementations and is primarily used as the basis for -/// other traits, such as [`Sequential`]. -pub trait RawStore { - type Elem; - - private!(); -} -/// The [`Sequential`] trait is a marker trait defining a sequential collection of elements. -/// It is sealed, preventing external implementations, and is used to indicate that a type can -/// be treated as a sequence of elements, such as arrays or vectors. -pub trait Sequential { - private!(); +/// The [`StoreEntry`] trait establishes a common interface for all _entries_ within a +/// key-value store. These types enable in-place manipulation of key-value pairs by allowing +/// for keys to point to empty or _vacant_ slots within the store. +pub trait StoreEntry<'a, K, V> { + /// checks if the entry is occupied + fn is_occupied(&self) -> bool; + /// checks if the entry is vacant + fn is_vacant(&self) -> bool; } -macro_rules! impl_raw_store { - (@impl $($name:ident)::*<$T:ident>) => { - impl<$T> $crate::store::RawStore for $($name)::*<$T> { - type Elem = $T; - - seal!(); - } - }; - { - $( - $($name:ident)::*<$T:ident> - ),* $(,)? - } => { - $( - impl_raw_store!(@impl $($name)::*<$T>); - )* - }; +/// The [`RawStore`] trait is used to define an interface for key-value stores like hash-maps, +/// dictionaries, and similar data structures. +pub trait RawStore { + /// retrieves a reference to a value by key + fn get(&self, key: &K) -> Option<&V>; + /// returns true if the key is associated with a value in the store + fn contains_key(&self, key: &K) -> bool { + self.get(key).is_some() + } } - -macro_rules! impl_sequential { - (@impl $($name:ident)::*<$T:ident>) => { - impl<$T> $crate::store::Sequential for $($name)::*<$T> { - seal!(); - } - }; - { - $( - $($name:ident)::*<$T:ident> - ),* $(,)? - } => { - $( - impl_sequential!(@impl $($name)::*<$T>); - )* - }; +/// [`RawStoreMut`] extends the [`RawStore`] trait by introducing various mutable operations +/// and accessors for elements within the store. +pub trait RawStoreMut: RawStore { + /// retrieves a mutable reference to a value by key + fn get_mut(&mut self, key: &K) -> Option<&mut V>; + /// inserts a key-value pair into the store + fn insert(&mut self, key: K, value: V) -> Option; + /// removes a key-value pair from the store by key + fn remove(&mut self, key: &K) -> Option; } - -impl RawStore for &S -where - S: RawStore, -{ - type Elem = T; - - seal!(); +/// The [`Store`] trait is a more robust interface for key-value stores, building upon both +/// [`RawStore`] and [`RawStoreMut`] traits by introducing an `entry` method for in-place +/// manipulation of key-value pairs. +pub trait Store: RawStoreMut { + type Entry<'a>: StoreEntry<'a, K, V> + where + Self: 'a; + /// returns the entry corresponding to the given key for in-place manipulation + fn entry<'a>(&'a mut self, key: K) -> Self::Entry<'a>; } +/* + ************* Implementations ************* +*/ -impl RawStore for &mut S -where - S: RawStore, -{ - type Elem = T; +// macro_rules! impl_store { +// (<$($src:ident)::*>::$store:ident<$K:ident, $V:ident $(, $($T:ident),*)?> $(where $($where:tt)*)?) => { + +// impl<$K, $V $(, $($T),*)?> RawStore<$K, $V> for $($src)::*::$store<$K, $V $(, $($T),*)?> { +// fn contains_key(&self, key: &K) -> bool { +// $($src)::*::$store::contains_key(self, key) +// } + +// fn get(&self, key: &K) -> Option<&V> { +// $($src)::*::$store::get(self, key) +// } +// } + +// impl<$K, $V $(, $($T),*)?> RawStoreMut for $($src)::*::$store<$K, $V $(, $($T),*)?> $(where $($where)*)? { +// fn insert(&mut self, key: K, value: V) -> Option { +// $($src)::*::$store::insert(self, key, value) +// } + +// fn get_mut(&mut self, key: &K) -> Option<&mut V> { +// $($src)::*::$store::get_mut(self, key) +// } + +// fn remove(&mut self, key: &K) -> Option { +// $($src)::*::$store::remove(self, key) +// } +// } + +// impl<$K, $V $(, $($T),*)?> Store for $($src)::*::$store<$K, $V $(, $($T),*)?> $(where $($where)*)? { +// type Entry<'a> +// = $($src)::*::Entry<'a, K, V> +// where +// Self: 'a; + +// fn entry<'a>(&'a mut self, key: K) -> Self::Entry<'a> { +// $($src)::*::$store::entry(self, key) +// } +// } +// }; +// } + +// #[cfg(feature = "alloc")] +// impl_store! { +// ::BTreeMap where K: Ord +// } + +// #[cfg(feature = "hashbrown")] +// impl_store! { +// ::HashMap where K: Eq + core::hash::Hash, S: core::hash::BuildHasher, +// } + +// #[cfg(feature = "std")] +// impl_store! { +// ::HashMap where K: Eq + core::hash::Hash, +// } + +#[cfg(feature = "alloc")] +mod impl_alloc { + use super::*; - seal!(); -} + use alloc::collections::btree_map::{self, BTreeMap}; -impl Sequential for &T -where - T: Sequential, -{ - seal!(); -} + impl<'a, K, V> StoreEntry<'a, K, V> for btree_map::Entry<'a, K, V> { + fn is_occupied(&self) -> bool { + matches!(self, btree_map::Entry::Occupied(_)) + } -impl Sequential for &mut T -where - T: Sequential, -{ - seal!(); -} + fn is_vacant(&self) -> bool { + matches!(self, btree_map::Entry::Vacant(_)) + } + } -impl RawStore for [T] { - type Elem = T; + impl RawStore for BTreeMap + where + K: Ord, + { + fn contains_key(&self, key: &K) -> bool { + BTreeMap::contains_key(self, key) + } - seal!(); -} + fn get(&self, key: &K) -> Option<&V> { + BTreeMap::get(self, key) + } + } -impl RawStore for [T; N] { - type Elem = T; + impl Store for BTreeMap + where + K: Ord, + { + type Entry<'a> + = btree_map::Entry<'a, K, V> + where + Self: 'a; - seal!(); -} + fn entry<'a>(&'a mut self, key: K) -> Self::Entry<'a> { + BTreeMap::entry(self, key) + } + } -impl Sequential for [T] { - seal!(); -} + impl RawStoreMut for BTreeMap + where + K: Ord, + { + fn insert(&mut self, key: K, value: V) -> Option { + BTreeMap::insert(self, key, value) + } -impl Sequential for [T; N] { - seal!(); -} + fn get_mut(&mut self, key: &K) -> Option<&mut V> { + BTreeMap::get_mut(self, key) + } -impl_raw_store! { - Option + fn remove(&mut self, key: &K) -> Option { + BTreeMap::remove(self, key) + } + } } -#[cfg(all(feature = "alloc", not(feature = "nightly")))] -mod impl_alloc { - use super::RawStore; +#[cfg(feature = "hashbrown")] +mod impl_hashbrown { + use super::*; + use core::hash::{BuildHasher, Hash}; + use hashbrown::hash_map::{self, HashMap}; - impl RawStore for alloc::collections::BTreeMap { - type Elem = V; + impl StoreEntry<'_, K, V> for hash_map::Entry<'_, K, V, S> { + fn is_occupied(&self) -> bool { + matches!(self, hash_map::Entry::Occupied(_)) + } - seal!(); + fn is_vacant(&self) -> bool { + matches!(self, hash_map::Entry::Vacant(_)) + } } - impl_raw_store! { - alloc::boxed::Box, - alloc::sync::Arc, - alloc::collections::BTreeSet, - alloc::vec::Vec - } + impl RawStore for HashMap + where + K: Eq + Hash, + S: BuildHasher, + { + fn contains_key(&self, key: &K) -> bool { + HashMap::contains_key(self, key) + } - impl_sequential! { - alloc::vec::Vec, + fn get(&self, key: &K) -> Option<&V> { + HashMap::get(self, key) + } } -} -#[cfg(all(feature = "alloc", feature = "nightly"))] -mod impl_alloc { - use super::RawStore; - use alloc::alloc::Allocator; - use alloc::collections::{BTreeMap, BTreeSet}; - use alloc::vec::Vec; - - impl RawStore for alloc::boxed::Box + impl RawStoreMut for HashMap where - A: Allocator + Clone, + K: Eq + Hash, + S: BuildHasher, { - type Elem = T; + fn insert(&mut self, key: K, value: V) -> Option { + HashMap::insert(self, key, value) + } + + fn get_mut(&mut self, key: &K) -> Option<&mut V> { + HashMap::get_mut(self, key) + } - seal!(); + fn remove(&mut self, key: &K) -> Option { + HashMap::remove(self, key) + } } - impl RawStore for BTreeMap + impl Store for HashMap where - A: Allocator + Clone, + K: Eq + Hash, + S: BuildHasher, { - type Elem = V; + type Entry<'a> + = hash_map::Entry<'a, K, V, S> + where + Self: 'a; - seal!(); + fn entry<'a>(&'a mut self, key: K) -> Self::Entry<'a> { + HashMap::entry(self, key) + } } +} - impl RawStore for BTreeSet - where - A: Allocator + Clone, - { - type Elem = K; +#[cfg(feature = "std")] +mod impl_std { + use super::*; + use core::hash::Hash; + use std::collections::hash_map::{self, HashMap}; - seal!(); + impl StoreEntry<'_, K, V> for hash_map::Entry<'_, K, V> { + fn is_occupied(&self) -> bool { + matches!(self, hash_map::Entry::Occupied(_)) + } + + fn is_vacant(&self) -> bool { + matches!(self, hash_map::Entry::Vacant(_)) + } } - impl RawStore for Vec + impl RawStore for HashMap where - A: Allocator + Clone, + K: Eq + Hash, { - type Elem = T; + fn contains_key(&self, key: &K) -> bool { + HashMap::contains_key(self, key) + } - seal!(); + fn get(&self, key: &K) -> Option<&V> { + HashMap::get(self, key) + } } -} -#[cfg(feature = "std")] -mod impl_std { - use super::RawStore; - - impl RawStore for std::collections::HashMap { - type Elem = V; + impl RawStoreMut for HashMap + where + K: Eq + Hash, + { + fn insert(&mut self, key: K, value: V) -> Option { + HashMap::insert(self, key, value) + } + fn get_mut(&mut self, key: &K) -> Option<&mut V> { + HashMap::get_mut(self, key) + } - seal!(); + fn remove(&mut self, key: &K) -> Option { + HashMap::remove(self, key) + } } - impl RawStore for std::collections::HashSet { - type Elem = K; + impl Store for HashMap + where + K: Eq + Hash, + { + type Entry<'a> + = hash_map::Entry<'a, K, V> + where + Self: 'a; - seal!(); + fn entry<'a>(&'a mut self, key: K) -> Self::Entry<'a> { + HashMap::entry(self, key) + } } } diff --git a/traits/src/tensor/dimensionality.rs b/traits/src/tensor/dimensionality.rs index 2217e87f..816f3181 100644 --- a/traits/src/tensor/dimensionality.rs +++ b/traits/src/tensor/dimensionality.rs @@ -1,32 +1,83 @@ /* - Appellation: shape - Created At: 2025.11.26:13:10:09 + Appellation: dimensionality + Created At: 2025.12.09:10:03:43 Contrib: @FL03 */ +pub trait DimConst {} + /// the [`Dim`] trait is used to define a type that can be used as a raw dimension. /// This trait is primarily used to provide abstracted, generic interpretations of the /// dimensions of the [`ndarray`] crate to ensure long-term compatibility. -pub trait RawDimension { +pub trait Dim { type Shape; private! {} -} -pub trait Dim: RawDimension { + /// returns the rank of the dimension; the rank essentially speaks to the total number of + /// axes defined by the dimension. + fn rank(&self) -> usize; /// returns the total number of elements considered by the dimension fn size(&self) -> usize; } +use ndarray::{Axis, Dimension, RemoveAxis}; + +/// The [`IntoAxis`] trait is used to define a conversion routine that takes a type and wraps +/// it in an [`Axis`] type. +pub trait IntoAxis { + fn into_axis(self) -> Axis; +} + +/// The [`AsBiasDim`] trait is used to define a type that can be used to get the bias dimension +/// of the parameters. +pub trait AsBiasDim { + /// returns the bias dimension of the parameters + fn as_bias_dim(&self) -> D; +} + /* ************* Implementations ************* */ -impl RawDimension for D +impl IntoAxis for S +where + S: AsRef, +{ + fn into_axis(self) -> Axis { + Axis(*self.as_ref()) + } +} + +impl AsBiasDim for A where - D: nd::Dimension, + A: RemoveAxis, + B: Dimension, +{ + /// returns the bias dimension of the parameters by removing the "zero-th" axis from the + /// given dimension + fn as_bias_dim(&self) -> B { + self.remove_axis(Axis(0)) + } +} + +/* + ************* Implementations ************* +*/ + +impl Dim for D +where + D: ndarray::Dimension, { type Shape = D::Pattern; seal! {} + + fn rank(&self) -> usize { + self.ndim() + } + + fn size(&self) -> usize { + self.size() + } } diff --git a/traits/src/tensor/ndtensor.rs b/traits/src/tensor/ndtensor.rs index f89cd09f..aac4214e 100644 --- a/traits/src/tensor/ndtensor.rs +++ b/traits/src/tensor/ndtensor.rs @@ -3,43 +3,184 @@ Created At: 2025.11.26:14:27:51 Contrib: @FL03 */ +use ndarray::{ArrayBase, Data, DataMut, Dimension, OwnedRepr, RawData, RawDataMut}; +use num_traits::Float; -pub trait RawTensorData { - type Elem; +pub trait TensorBase { + type Cont<_S, _D, _A> + where + _D: Dimension, + _S: RawData; + + fn rank(&self) -> usize; + + fn shape(&self) -> &[usize]; + + fn size(&self) -> usize; } -pub trait RawTensor +pub trait NdTensor::Elem>: + TensorBase = ArrayBase> where - S: RawTensorData, + D: Dimension, + S: RawData, { - type Elem; - type Cont<_R: RawTensorData, _D>: RawTensor<_R, _D, Elem = Self::Elem>; - /// returns the rank, or _dimensionality_, of the tensor - fn rank(&self) -> usize; - /// returns the shape of the tensor - fn shape(&self) -> &[usize]; - /// returns the total number of elements in the tensor - fn len(&self) -> usize; + fn as_ptr(&self) -> *const A; + + fn as_mut_ptr(&mut self) -> *mut A + where + S: RawDataMut; + + fn apply(&self, f: F) -> Self::Cont, D, B> + where + F: FnMut(A) -> B, + A: Clone, + S: Data; - fn as_ptr(&self) -> *const Self::Elem; + fn powi(&self, n: i32) -> Self::Cont, D, A> + where + A: Float, + S: DataMut, + { + self.apply(|x| x.powi(n)) + } + + fn exp(&self) -> Self::Cont, D, A> + where + A: Float, + S: DataMut, + { + self.apply(|x| x.exp()) + } + + fn log(&self) -> Self::Cont, D, A> + where + A: Float, + S: DataMut, + { + self.apply(|x| x.ln()) + } + + fn ln(&self) -> Self::Cont, D, A> + where + A: Float, + S: DataMut, + { + self.apply(|x| x.ln()) + } + + fn cos(&self) -> Self::Cont, D, A> + where + A: Float, + S: DataMut, + { + self.apply(|x| x.cos()) + } + + fn cosh(&self) -> Self::Cont, D, A> + where + A: Float, + S: DataMut, + { + self.apply(|x| x.cosh()) + } - fn as_mut_ptr(&mut self) -> *mut Self::Elem; + fn sin(&self) -> Self::Cont, D, A> + where + A: Float, + S: DataMut, + { + self.apply(|x| x.sin()) + } + + fn sinh(&self) -> Self::Cont, D, A> + where + A: Float, + S: DataMut, + { + self.apply(|x| x.sinh()) + } + + fn tan(&self) -> Self::Cont, D, A> + where + A: Float, + S: DataMut, + { + self.apply(|x| x.tan()) + } + + fn tanh(&self) -> Self::Cont, D, A> + where + A: Float, + S: DataMut, + { + self.apply(|x| x.tanh()) + } +} + +pub trait NdGradient::Elem>: NdTensor +where + D: Dimension, + S: RawData, +{ + type Delta<_S, _D, _A>: NdTensor<_S, _D, _A> + where + _D: Dimension, + _S: RawData; + + fn grad(&self, rhs: &Self::Delta) -> Self::Delta; } -pub trait Tensor: RawTensor +/* + ************* Implementations ************* +*/ + +impl TensorBase for ArrayBase where - S: RawTensorData, + D: Dimension, + S: RawData, { - fn apply(&self, f: F) -> Self::Cont + type Cont<_S, _D, _A> + = ArrayBase<_S, _D, _A> where - F: Fn(&Self::Elem) -> U; + _D: Dimension, + _S: RawData; + + fn rank(&self) -> usize { + self.ndim() + } + + fn shape(&self) -> &[usize] { + self.shape() + } + + fn size(&self) -> usize { + self.len() + } } -pub trait TensorGrad: Tensor +impl NdTensor for ArrayBase where - S: RawTensorData, + D: Dimension, + S: RawData, { - type Delta<_S: RawTensorData, _D>: RawTensor<_S, _D, Elem = Self::Elem>; + fn as_ptr(&self) -> *const A { + self.as_ptr() + } - fn grad(&self, rhs: &Self::Delta) -> Self::Delta; + fn as_mut_ptr(&mut self) -> *mut A + where + S: RawDataMut, + { + self.as_mut_ptr() + } + + fn apply(&self, f: F) -> Self::Cont, D, B> + where + A: Clone, + F: FnMut(A) -> B, + S: Data, + { + self.mapv(f) + } } diff --git a/traits/src/tensor/tensor_data.rs b/traits/src/tensor/tensor_data.rs new file mode 100644 index 00000000..d11ff415 --- /dev/null +++ b/traits/src/tensor/tensor_data.rs @@ -0,0 +1,59 @@ +/* + Appellation: repr + Created At: 2025.12.09:10:04:11 + Contrib: @FL03 +*/ +use rspace_traits::RawSpace; + +pub trait RawTensorData: RawSpace { + private! {} +} + +pub trait RawTensor +where + S: RawTensorData, +{ + type Cont<_S, _D, _A> + where + _D: ?Sized, + _S: RawTensorData; +} +/// A marker trait used to denote tensors that represent scalar values; more specifically, we +/// consider _**any**_ type implementing the [`RawTensorData`] type where the `Elem` associated +/// type is the implementor itself a scalar value. +pub trait ScalarTensorData: RawTensorData { + private! {} +} + +/* + ************* Implementations ************* +*/ + +impl ScalarTensorData for T +where + T: RawTensorData, +{ + seal! {} +} + +macro_rules! impl_scalar_tensor { + {$($T:ty),* $(,)?} => { + $( + impl RawTensorData for $T { + seal! {} + } + )* + }; +} + +impl_scalar_tensor! { + u8, u16, u32, u64, u128, usize, + i8, i16, i32, i64, i128, isize, + f32, f64, + bool, char +} + +#[cfg(feature = "alloc")] +impl RawTensorData for alloc::string::String { + seal! {} +} diff --git a/traits/tests/complex.rs b/traits/tests/complex.rs index 566a3bdc..a7854148 100644 --- a/traits/tests/complex.rs +++ b/traits/tests/complex.rs @@ -3,6 +3,8 @@ Created At: 2025.11.26:12:28:18 Contrib: @FL03 */ +#![cfg(feature = "complex")] + use concision_traits::{AsComplex, Conjugate}; use ndarray::prelude::*; use num_complex::Complex; diff --git a/traits/tests/tensor.rs b/traits/tests/ops.rs similarity index 100% rename from traits/tests/tensor.rs rename to traits/tests/ops.rs