Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.88
- uses: dtolnay/rust-toolchain@1.95
- uses: Swatinem/rust-cache@v2
- run: cargo check --features std

Expand Down
108 changes: 76 additions & 32 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,17 +1,60 @@
name: Publish

# Releases on merge to main: when Cargo.toml's version is not yet on
# crates.io, this workflow tests, tags, publishes, and creates the
# GitHub release in a single run.
#
# Everything happens in one workflow because tag pushes made with the
# default GITHUB_TOKEN deliberately do not trigger other workflows —
# that is what broke the old auto-tag.yml → publish.yml chain. Here the
# tag is an output of the run, not a trigger, so no PAT is needed.
#
# Every step is idempotent: an already-published version short-circuits
# the run, and an existing tag is left in place. A failed run can be
# retried from the Actions tab (or `gh workflow run publish.yml`).

on:
push:
tags:
- 'v*.*.*'
branches: [main]
workflow_dispatch:

env:
CARGO_TERM_COLOR: always

concurrency:
group: publish
cancel-in-progress: false

jobs:
# Run full test suite before publishing
check:
name: Check release needed
runs-on: ubuntu-latest
outputs:
version: ${{ steps.check.outputs.version }}
release: ${{ steps.check.outputs.release }}
steps:
- uses: actions/checkout@v4
- name: Compare Cargo.toml version against crates.io
id: check
run: |
VERSION=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version')
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
# The sparse index needs no auth and serves one JSON line per
# published version. A 404 means the crate has never been
# published, which also means "release".
if curl -sf "https://index.crates.io/fi/xe/fixed_analytics" \
| jq -e --arg v "$VERSION" 'select(.vers == $v)' > /dev/null; then
echo "release=false" >> "$GITHUB_OUTPUT"
echo "v$VERSION is already on crates.io — nothing to release"
else
echo "release=true" >> "$GITHUB_OUTPUT"
echo "v$VERSION is not on crates.io — releasing"
fi

test:
name: Pre-publish Tests
needs: check
if: needs.check.outputs.release == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -23,53 +66,54 @@ jobs:
env:
RUSTDOCFLAGS: -Dwarnings

# Publish to crates.io
publish:
name: Publish to crates.io
needs: test
release:
name: Tag, publish, and release
needs: [check, test]
if: needs.check.outputs.release == 'true'
runs-on: ubuntu-latest
permissions:
contents: write
env:
VERSION: ${{ needs.check.outputs.version }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2

- name: Verify version matches tag

# Tag before publishing: a tag without a crates.io release heals
# on retry (the tag step skips, publish runs); the reverse order
# would leave an already-published version that the check job
# filters out, so the tag would never be created.
- name: Create tag if missing
run: |
CARGO_VERSION=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version')
TAG_VERSION=${GITHUB_REF#refs/tags/v}
if [ "$CARGO_VERSION" != "$TAG_VERSION" ]; then
echo "Version mismatch: Cargo.toml has $CARGO_VERSION but tag is $TAG_VERSION"
exit 1
if git ls-remote --tags origin "refs/tags/v$VERSION" | grep -q .; then
echo "Tag v$VERSION already exists, keeping it"
else
git tag "v$VERSION"
git push origin "v$VERSION"
fi
- name: Publish

- name: Publish to crates.io
run: cargo publish
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}

# Create GitHub release with auto-generated notes
release:
name: Create GitHub Release
needs: publish
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: softprops/action-gh-release@v1
- name: Create GitHub release
uses: softprops/action-gh-release@v1
with:
tag_name: v${{ needs.check.outputs.version }}
generate_release_notes: true
body: |
## Installation

```toml
[dependencies]
fixed_analytics = "${{ github.ref_name }}"
fixed_analytics = "${{ needs.check.outputs.version }}"
```

For `no_std` environments:

```toml
[dependencies]
fixed_analytics = { version = "${{ github.ref_name }}", default-features = false }
```
fixed_analytics = { version = "${{ needs.check.outputs.version }}", default-features = false }
```
6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
[package]
name = "fixed_analytics"
version = "2.0.1"
version = "2.1.0"
edition = "2024"
rust-version = "1.88"
rust-version = "1.95"
authors = ["David Gathercole"]
description = "Fixed-point mathematical functions. Accurate, deterministic, and panic free."
repository = "https://github.com/GeEom/fixed_analytics"
Expand All @@ -22,7 +22,7 @@ std = []
verify-no-panic = ["dep:no-panic"]

[dependencies]
fixed = "1.30"
fixed = "1.31"
no-panic = { version = "0.1", optional = true }

[dev-dependencies]
Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ assert!((log.to_num::<f32>() - 1.0).abs() < 0.01);

## Installation

Requires Rust 1.88 or later.
Requires Rust 1.95 or later.

```toml
[dependencies]
fixed_analytics = "2.0.1"
fixed_analytics = "2.1.0"
```

For `no_std` environments:
Expand Down Expand Up @@ -82,9 +82,9 @@ Relative error statistics measured against MPFR reference implementations. Accur
| sin | 6.06e-4 | 8.78e-5 | 1.28e-3 | 1.16e-8 | 1.68e-9 | 2.43e-8 |
| cos | 6.45e-4 | 9.03e-5 | 1.38e-3 | 1.22e-8 | 1.72e-9 | 2.64e-8 |
| tan | 7.20e-5 | 3.57e-5 | 2.20e-4 | 1.28e-9 | 3.98e-10 | 3.03e-9 |
| asin | 2.87e-4 | 5.93e-5 | 6.46e-4 | 5.34e-9 | 8.82e-10 | 1.03e-8 |
| acos | 3.61e-5 | 2.18e-5 | 1.14e-4 | 5.37e-10 | 3.19e-10 | 1.71e-9 |
| atan | 2.71e-5 | 2.21e-5 | 6.29e-5 | 3.69e-10 | 2.92e-10 | 8.74e-10 |
| asin | 1.85e-4 | 3.60e-5 | 4.75e-4 | 4.49e-9 | 7.35e-10 | 8.89e-9 |
| acos | 2.33e-5 | 1.47e-5 | 6.86e-5 | 4.51e-10 | 2.71e-10 | 1.38e-9 |
| atan | 1.50e-5 | 1.23e-5 | 3.45e-5 | 3.01e-10 | 2.44e-10 | 7.05e-10 |
| sinh | 9.80e-5 | 6.23e-5 | 2.79e-4 | 1.52e-9 | 9.64e-10 | 4.29e-9 |
| cosh | 9.40e-5 | 5.75e-5 | 2.77e-4 | 1.44e-9 | 8.90e-10 | 4.25e-9 |
| tanh | 1.60e-5 | 1.32e-5 | 2.56e-5 | 2.25e-10 | 1.22e-10 | 3.90e-10 |
Expand All @@ -93,10 +93,10 @@ Relative error statistics measured against MPFR reference implementations. Accur
| acosh | 6.74e-4 | 5.21e-4 | 1.80e-3 | 1.05e-8 | 7.96e-9 | 2.88e-8 |
| atanh | 3.01e-4 | 5.90e-5 | 6.25e-4 | 6.68e-9 | 1.32e-9 | 1.44e-8 |
| acoth | 2.10e-3 | 1.33e-3 | 6.67e-3 | 4.26e-8 | 2.62e-8 | 1.39e-7 |
| exp | 1.14e-2 | 2.32e-5 | 7.88e-2 | 1.91e-7 | 1.73e-9 | 1.30e-6 |
| exp | 5.98e-3 | 1.47e-5 | 4.13e-2 | 9.49e-8 | 1.50e-9 | 6.50e-7 |
| ln | 1.35e-5 | 8.76e-6 | 2.97e-5 | 4.50e-10 | 3.48e-10 | 9.17e-10 |
| log2 | 1.33e-5 | 8.48e-6 | 2.92e-5 | 3.46e-10 | 2.24e-10 | 7.21e-10 |
| log10 | 1.44e-5 | 9.28e-6 | 3.14e-5 | 4.49e-10 | 3.27e-10 | 9.07e-10 |
| pow2 | 7.21e-4 | 2.58e-5 | 4.72e-3 | 1.13e-8 | 4.93e-10 | 7.43e-8 |
| pow2 | 3.62e-4 | 2.24e-5 | 2.37e-3 | 5.64e-9 | 4.29e-10 | 3.67e-8 |
| sqrt | 1.77e-7 | 1.16e-7 | 4.74e-7 | 2.70e-12 | 1.78e-12 | 7.16e-12 |
<!-- ACCURACY_END -->
6 changes: 3 additions & 3 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ codecov:
coverage:
precision: 2
round: down
range: "70...100"
range: "90...100"

status:
project:
default:
target: 80%
target: 95%
threshold: 1%
patch:
default:
target: 80%
target: 85%

comment:
layout: "reach,diff,flags,files,footer"
Expand Down
32 changes: 31 additions & 1 deletion src/kernel/cordic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,36 @@ const fn table_lookup(table: &[i64; 64], index: u32) -> i64 {
table[index as usize]
}

/// Converts an I1F63 table constant to `T`, rounding to nearest.
///
/// `CordicNumber::from_i1f63` truncates, which biases every converted
/// angle low by up to one ulp. For circular CORDIC the accumulated z is
/// dominated by this table quantization, so rounding to nearest halves
/// the worst case and removes the systematic bias.
///
/// Hyperbolic CORDIC deliberately keeps the truncating conversion: its
/// datapath shifts carry their own bias, which the truncated table
/// angles partially cancel (measured, not designed — see the accuracy
/// baseline history).
fn from_i1f63_rounded<T: CordicNumber>(bits: i64) -> T {
let truncated = T::from_i1f63(bits);
let frac = T::frac_bits();
if frac >= 63 {
// Conversion is exact; nothing to round.
return truncated;
}
// The conversion discards the low (63 - frac) bits. If the highest
// discarded bit is set, the true value is at least half an output
// ulp above the truncated result, so round up by one ulp.
let shift = 63 - frac;
if (bits >> (shift - 1)) & 1 == 1 {
let ulp = T::one() >> frac;
truncated.saturating_add(ulp)
} else {
truncated
}
}

/// Performs circular CORDIC in vectoring mode.
///
/// Given an initial vector (x, y), rotates it until y ≈ 0.
Expand Down Expand Up @@ -68,7 +98,7 @@ pub fn circular_vectoring<T: CordicNumber>(mut x: T, mut y: T, mut z: T) -> (T,
let iterations = T::frac_bits().min(62);

for i in 0..iterations {
let angle = T::from_i1f63(table_lookup(&ATAN_TABLE, i));
let angle = from_i1f63_rounded::<T>(table_lookup(&ATAN_TABLE, i));

if y < zero {
// y is negative, rotate counter-clockwise to bring y toward zero
Expand Down
Loading
Loading