Skip to content

Commit

Permalink
Add code coverage testing suite
Browse files Browse the repository at this point in the history
- Add tests for core features (more will follow)
- Add PyPi cross-compilation Github Action
- Define variables and functions in testing mode
  that are guaranteed to be stable over concurrent runs
- Add function that checks if a variable is independent
  in a polynomial
- Fix normalization problem for nested additions
- Properly set the normalized flag upon merging terms
- Propetly set the has_coefficient flag
- Normalize output of single replacement
  • Loading branch information
benruijl committed Apr 9, 2024
1 parent c9da5ae commit d3561c5
Show file tree
Hide file tree
Showing 23 changed files with 1,053 additions and 39 deletions.
25 changes: 25 additions & 0 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Coverage

on: [pull_request, push]

jobs:
coverage:
runs-on: ubuntu-latest
env:
CARGO_TERM_COLOR: always
steps:
- uses: actions/checkout@v4
- name: Install Rust
run: rustup update stable
- name: Install cargo-llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov
- name: Install nextest
uses: taiki-e/install-action@nextest
- name: Generate code coverage
run: cargo llvm-cov --workspace --codecov --output-path codecov.json
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
files: codecov.json
fail_ci_if_error: true
45 changes: 45 additions & 0 deletions .github/workflows/publish_pypi.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: PyPi MacOS Release

on:
workflow_dispatch:

permissions:
contents: read

jobs:
macos:
runs-on: ${{ matrix.runner[0] }}
strategy:
matrix:
runner: [[macos-13, x86_64], [macos-latest, x86_64], [macos-14, aarch64]]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.runner[1] }}
args: --release --out dist
sccache: 'true'
- name: Upload wheels
uses: actions/upload-artifact@v4
with:
name: wheels-macos-${{ matrix.runner[1] }}
path: dist

release:
name: Release
runs-on: ubuntu-latest
if: ${{ startsWith(github.ref, 'refs/tags/') }}
needs: [macos]
steps:
- uses: actions/download-artifact@v4
- name: Publish to PyPI
uses: PyO3/maturin-action@v1
env:
MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
with:
command: upload
args: --non-interactive --skip-existing wheels-*/*
2 changes: 1 addition & 1 deletion examples/collect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ fn main() {
let input = Atom::parse("x*(1+a)+x*5*y+f(5,x)+2+y^2+x^2 + x^3").unwrap();
let x = State::get_symbol("x");
let key = State::get_symbol("key");
let coeff = State::get_symbol("coeff");
let coeff = State::get_symbol("val");

let (r, rest) = input.coefficient_list(x);

Expand Down
15 changes: 6 additions & 9 deletions src/api/mathematica.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ use std::sync::{Arc, RwLock};

use smartstring::{LazyCompact, SmartString};

use crate::domains::finite_field::{FiniteField, FiniteFieldCore};
use crate::domains::integer::IntegerRing;
use crate::domains::finite_field::{FiniteFieldCore, Zp, Zp64};
use crate::domains::integer::{IntegerRing, Z};
use crate::domains::rational::RationalField;
use crate::parser::Token;
use crate::poly::Variable;
Expand Down Expand Up @@ -48,7 +48,7 @@ fn set_vars(vars: String) {

let mut var_map = vec![];
for var in vars.split(',') {
let v = State::get_or_insert_var(var);
let v = State::get_symbol(var);
var_map.push(v.into());
symbolica.var_name_map.push(var.into());
}
Expand All @@ -70,7 +70,6 @@ fn simplify(input: String, prime: i64, explicit_rational_polynomial: bool) -> St
let r: RationalPolynomial<IntegerRing, $exp_size> = Workspace::get_local()
.with(|workspace| {
token.to_rational_polynomial(
&workspace,
&<$in_field>::new(),
&Z,
&symbolica.var_map,
Expand All @@ -86,7 +85,7 @@ fn simplify(input: String, prime: i64, explicit_rational_polynomial: bool) -> St
opts: PrintOptions {
terms_on_new_line: false,
color_top_level_sum: false,
color_builtin_functions: false,
color_builtin_symbols: false,
print_finite_field: false,
explicit_rational_polynomial,
symmetric_representation_for_finite_field: false,
Expand All @@ -105,7 +104,6 @@ fn simplify(input: String, prime: i64, explicit_rational_polynomial: bool) -> St
let rf: RationalPolynomial<Zp, $exp_size> = Workspace::get_local()
.with(|workspace| {
token.to_rational_polynomial(
&workspace,
&field,
&field,
&symbolica.var_map,
Expand All @@ -122,7 +120,7 @@ fn simplify(input: String, prime: i64, explicit_rational_polynomial: bool) -> St
opts: PrintOptions {
terms_on_new_line: false,
color_top_level_sum: false,
color_builtin_functions: false,
color_builtin_symbols: false,
print_finite_field: false,
explicit_rational_polynomial,
symmetric_representation_for_finite_field: false,
Expand All @@ -140,7 +138,6 @@ fn simplify(input: String, prime: i64, explicit_rational_polynomial: bool) -> St
let rf: RationalPolynomial<Zp64, $exp_size> = Workspace::get_local()
.with(|workspace| {
token.to_rational_polynomial(
&workspace,
&field,
&field,
&symbolica.var_map,
Expand All @@ -157,7 +154,7 @@ fn simplify(input: String, prime: i64, explicit_rational_polynomial: bool) -> St
opts: PrintOptions {
terms_on_new_line: false,
color_top_level_sum: false,
color_builtin_functions: false,
color_builtin_symbols: false,
print_finite_field: false,
explicit_rational_polynomial,
symmetric_representation_for_finite_field: false,
Expand Down
2 changes: 1 addition & 1 deletion src/api/python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2941,7 +2941,7 @@ impl PythonFunction {
opts.push(FunctionAttribute::Linear);
}

let id = State::get_symbol_with_attributes(name, opts)
let id = State::get_symbol_with_attributes(name, &opts)
.map_err(|e| exceptions::PyTypeError::new_err(e.to_string()))?;

Ok(PythonFunction { id })
Expand Down
96 changes: 85 additions & 11 deletions src/collect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,18 +191,19 @@ impl<'a> AtomView<'a> {
let mut rest_norm = Atom::new();
rest.as_view().normalize(workspace, &mut rest_norm);

(
h.into_iter()
.map(|(k, v)| {
(k, {
let mut a = Atom::new();
v.as_view().normalize(workspace, &mut a);
a
})
let mut r: Vec<_> = h
.into_iter()
.map(|(k, v)| {
(k, {
let mut a = Atom::new();
v.as_view().normalize(workspace, &mut a);
a
})
.collect(),
rest_norm,
)
})
.collect();
r.sort_unstable_by_key(|(a, _)| *a);

(r, rest_norm)
}

/// Check if a factor contains `x` at the ground level.
Expand Down Expand Up @@ -341,3 +342,76 @@ impl<'a> AtomView<'a> {
}
}
}

#[cfg(test)]
mod test {
use crate::{
fun,
representations::{Atom, FunctionBuilder},
state::State,
};

#[test]
fn coefficient_list() {
let input = Atom::parse("v1*(1+v3)+v1*5*v2+f1(5,v1)+2+v2^2+v1^2+v1^3").unwrap();
let x = State::get_symbol("v1");

let (r, rest) = input.coefficient_list(x);

let res = vec![
(
Atom::parse("v1").unwrap(),
Atom::parse("v3+5*v2+1").unwrap(),
),
(Atom::parse("v1^2").unwrap(), Atom::parse("1").unwrap()),
(Atom::parse("v1^3").unwrap(), Atom::parse("1").unwrap()),
];
let res_rest = Atom::parse("v2^2+f1(5,v1)+2").unwrap();

let res_ref = res
.iter()
.map(|(a, b)| (a.as_view(), b.clone()))
.collect::<Vec<_>>();

assert_eq!(r, res_ref);
assert_eq!(rest, res_rest);
}

#[test]
fn collect() {
let input = Atom::parse("v1*(1+v3)+v1*5*v2+f1(5,v1)+2+v2^2+v1^2+v1^3").unwrap();
let x = State::get_symbol("v1");

let out = input.collect(x, None, None);

let ref_out = Atom::parse("v1^2+v1^3+v2^2+f1(5,v1)+v1*(5*v2+v3+1)+2").unwrap();
assert_eq!(out, ref_out)
}

#[test]
fn collect_wrap() {
let input = Atom::parse("v1*(1+v3)+v1*5*v2+f1(5,v1)+2+v2^2+v1^2+v1^3").unwrap();
let x = State::get_symbol("v1");
let key = State::get_symbol("f3");
let coeff = State::get_symbol("f4");
println!("> Collect in x with wrapping:");
let out = input.collect(
x,
Some(Box::new(move |a, out| {
out.set_from_view(&a);
*out = fun!(key, out);
})),
Some(Box::new(move |a, out| {
out.set_from_view(&a);
*out = fun!(coeff, out);
})),
);

let ref_out = Atom::parse(
"f3(1)*f4(v2^2+f1(5,v1)+2)+f3(v1)*f4(5*v2+v3+1)+f3(v1^2)*f4(1)+f3(v1^3)*f4(1)",
)
.unwrap();

assert_eq!(out, ref_out);
}
}
61 changes: 53 additions & 8 deletions src/combinatorics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,6 @@ impl CombinationIterator {
}
}

#[test]
fn test() {
let mut c = CombinationIterator::new(10, 5);
while let Some(a) = c.next() {
println!("{:?}", a);
}
}

/// An iterator for combinations with replacement.
pub struct CombinationWithReplacementIterator {
indices: SmallVec<[u32; 10]>,
Expand Down Expand Up @@ -362,3 +354,56 @@ pub fn partitions<T: Ord + Hash + Copy, B: Ord + Hash + Copy>(

res
}

#[cfg(test)]
mod test {
use super::{partitions, CombinationIterator};

#[test]
fn combinations() {
let mut c = CombinationIterator::new(4, 3);
let mut combinations = vec![];
while let Some(a) = c.next() {
combinations.push(a.to_vec());
}

let ans = vec![[0, 1, 2], [0, 1, 3], [0, 2, 3], [1, 2, 3]];

assert_eq!(combinations, ans);
}

#[test]
fn partitions_no_fill() {
let p = partitions(
&[1, 1, 1, 2, 2],
&[('f', 2), ('g', 2), ('f', 1)],
false,
false,
);

let res = vec![
(
3.into(),
vec![('f', vec![1]), ('f', vec![1, 1]), ('g', vec![2, 2])],
),
(
12.into(),
vec![('f', vec![1]), ('f', vec![1, 2]), ('g', vec![1, 2])],
),
(
3.into(),
vec![('f', vec![1]), ('f', vec![2, 2]), ('g', vec![1, 1])],
),
(
6.into(),
vec![('f', vec![2]), ('f', vec![1, 1]), ('g', vec![1, 2])],
),
(
6.into(),
vec![('f', vec![2]), ('f', vec![1, 2]), ('g', vec![1, 1])],
),
];

assert_eq!(p, res);
}
}
37 changes: 37 additions & 0 deletions src/derivative.rs
Original file line number Diff line number Diff line change
Expand Up @@ -450,3 +450,40 @@ impl<'a> AtomView<'a> {
true
}
}

#[cfg(test)]
mod test {
use crate::{representations::Atom, state::State};

#[test]
fn derivative() {
let x = State::get_symbol("x");
let inputs = [
"(1+2*x)^(5+x)",
"log(2*x) + exp(3*x) + sin(4*x) + cos(y*x)",
"f(x^2,x)",
"der(0,1,f(x,x^3))",
];
let r = inputs.map(|input| Atom::parse(input).unwrap().derivative(x));

let res = [
"(2*x+1)^(x+5)*log(2*x+1)+2*(x+5)*(2*x+1)^(x+4)",
"2*(2*x)^-1+3*exp(3*x)+4*cos(4*x)-y*sin(x*y)",
"der(0,1,f(x^2,x))+2*x*der(1,0,f(x^2,x))",
"der(1,1,f(x,x^3))+3*x^2*der(0,2,f(x,x^3))",
];
let res = res.map(|input| Atom::parse(input).unwrap());

assert_eq!(r, res);
}

#[test]
fn taylor_series() {
let x = State::get_symbol("x");
let input = Atom::parse("cos(x^2+1)*(x+3)").unwrap();
let t = input.taylor_series(x, Atom::new_num(0).as_view(), 2);

let res = Atom::parse("3*cos(1)+x*cos(1)-3*x^2*sin(1)").unwrap();
assert_eq!(t, res);
}
}
Loading

0 comments on commit d3561c5

Please sign in to comment.