diff --git a/src/gleam_community/maths.gleam b/src/gleam_community/maths.gleam index a350d3e..1e95fbd 100644 --- a/src/gleam_community/maths.gleam +++ b/src/gleam_community/maths.gleam @@ -5419,6 +5419,11 @@ pub fn linear_space( /// interval. The endpoint of the interval can optionally be included/excluded. The number of /// points, base, and whether the endpoint is included determine the spacing between values. /// +/// The values in the sequence are computed as powers of the given base, where the exponents are +/// evenly spaced between `start` and `stop`. The `base` parameter must be positive, as negative +/// bases lead to undefined behavior when computing fractional exponents. Similarly, the number of +/// points (`steps`) must be positive; specifying zero or a negative value will result in an error. +/// ///
/// Example: /// @@ -5454,7 +5459,7 @@ pub fn logarithmic_space( endpoint: Bool, base: Float, ) -> Result(Yielder(Float), Nil) { - case steps > 0 { + case steps > 0 && base >=. 0.0 { True -> { let assert Ok(linspace) = linear_space(start, stop, steps, endpoint) @@ -5475,11 +5480,20 @@ pub fn logarithmic_space( /// /// /// -/// The function returns an iterator of numbers spaced evenly on a log scale (a geometric -/// progression). Each point in the list is a constant multiple of the previous. The function is -/// similar to the [`logarithmic_space`](#logarithmic_space) function, but with endpoints -/// specified directly. +/// The function returns an iterator for generating a geometric progression between two specified +/// values, where each value is a constant multiple of the previous one. Unlike +/// [`logarithmic_space`](#logarithmic_space), this function allows specifying the starting +/// and ending values (`start` and `stop`) directly, without requiring them to be transformed +/// into exponents. /// +/// Internally, the function computes the logarithms of `start` and `stop` and generates evenly +/// spaced points in the logarithmic domain (using base 10). These points are then transformed back +/// into their original scale to create a sequence of values that grow multiplicatively. +/// +/// The `start` and `stop` values must be positive, as logarithms are undefined for non-positive +/// values. The number of points (`steps`) must also be positive; specifying zero or a negative +/// value will result in an error. +/// ///
/// Example: /// @@ -5496,7 +5510,7 @@ pub fn logarithmic_space( /// |> list.all(fn(x) { x == True }) /// |> should.be_true() /// -/// // Input (start and stop can't be equal to 0.0) +/// // Input (start and stop can't be less than or equal to 0.0) /// maths.geometric_space(0.0, 1000.0, 3, False) /// |> should.be_error() /// @@ -5521,75 +5535,13 @@ pub fn geometric_space( steps: Int, endpoint: Bool, ) -> Result(Yielder(Float), Nil) { - case start == 0.0 || stop == 0.0 { + case start <=. 0.0 || stop <=. 0.0 || steps < 0 { True -> Error(Nil) - False -> - case steps > 0 { - True -> { - let assert Ok(log_start) = logarithm_10(start) - let assert Ok(log_stop) = logarithm_10(stop) - logarithmic_space(log_start, log_stop, steps, endpoint, 10.0) - } - False -> Error(Nil) - } - } -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The function returns an iterator of exponentially spaced points over a specified interval. The -/// endpoint of the interval can optionally be included/excluded. The number of points and whether -/// the endpoint is included determine the spacing between values. The sequence is generated by -/// computing intermediate values in a logarithmic domain and transforming them into the exponential -/// domain. -/// -///
-/// Example: -/// -/// import gleam/yielder -/// import gleeunit/should -/// import gleam_community/maths -/// -/// pub fn example() { -/// let assert Ok(tolerance) = float.power(10.0, -6.0) -/// let assert Ok(exp_space) = maths.exponential_space(1.0, 1000.0, 4, True) -/// let expected = [1.0, 10.0, 100.0, 1000.0] -/// let pairs = exp_space |> yielder.to_list() |> list.zip(expected) -/// let assert Ok(result) = maths.all_close(pairs, 0.0, tolerance) -/// result |> list.all(fn(x) { x == True }) |> should.be_true() -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn exponential_space( - start: Float, - stop: Float, - steps: Int, - endpoint: Bool, -) -> Result(Yielder(Float), Nil) { - case steps > 0 { - True -> { + False -> { let assert Ok(log_start) = logarithm_10(start) let assert Ok(log_stop) = logarithm_10(stop) - let assert Ok(log_space) = - linear_space(log_start, log_stop, steps, endpoint) - Ok( - yielder.map(log_space, fn(value) { - let assert Ok(exp_value) = float.power(10.0, value) - exp_value - }), - ) + logarithmic_space(log_start, log_stop, steps, endpoint, 10.0) } - False -> Error(Nil) } } @@ -5599,8 +5551,8 @@ pub fn exponential_space( /// /// /// -/// Generates evenly spaced points around a center value. The total span is determined by -/// the radius argument of the function. +/// Generates evenly spaced points around a center value. The total span (around the center value) +/// is determined by the `radius` argument of the function. /// ///
/// Example: @@ -5614,6 +5566,12 @@ pub fn exponential_space( /// sym_space /// |> yielder.to_list() /// |> should.equal([-5.0, -2.5, 0.0, 2.5, 5.0]) +/// +/// // A negative radius reverses the order of the values +/// let assert Ok(sym_space) = maths.symmetric_space(0.0, -5.0, 5) +/// sym_space +/// |> yielder.to_list() +/// |> should.equal([5.0, 2.5, 0.0, -2.5, -5.0]) /// } ///
/// diff --git a/test/gleam_community/sequences_test.gleam b/test/gleam_community/sequences_test.gleam index 3fd83e1..27bb0a6 100644 --- a/test/gleam_community/sequences_test.gleam +++ b/test/gleam_community/sequences_test.gleam @@ -28,7 +28,6 @@ pub fn list_linear_space_test() { 0.0, tol, ) - result |> list.all(fn(x) { x == True }) |> should.be_true() @@ -42,7 +41,6 @@ pub fn list_linear_space_test() { 0.0, tol, ) - result |> list.all(fn(x) { x == True }) |> should.be_true() @@ -54,7 +52,6 @@ pub fn list_linear_space_test() { 0.0, tol, ) - result |> list.all(fn(x) { x == True }) |> should.be_true() @@ -69,7 +66,6 @@ pub fn list_linear_space_test() { 0.0, tol, ) - result |> list.all(fn(x) { x == True }) |> should.be_true() @@ -83,7 +79,6 @@ pub fn list_linear_space_test() { 0.0, tol, ) - result |> list.all(fn(x) { x == True }) |> should.be_true() @@ -96,7 +91,6 @@ pub fn list_linear_space_test() { 0.0, tol, ) - result |> list.all(fn(x) { x == True }) |> should.be_true() @@ -108,7 +102,31 @@ pub fn list_linear_space_test() { 0.0, tol, ) + result + |> list.all(fn(x) { x == True }) + |> should.be_true() + // Check that when start == stop and steps > 0, then + // the value start/stop value is just repeated, since the + // step increment will be 0 + let assert Ok(linspace) = maths.linear_space(10.0, 10.0, 5, True) + let assert Ok(result) = + maths.all_close( + linspace |> yielder.to_list() |> list.zip([10.0, 10.0, 10.0, 10.0, 10.0]), + 0.0, + tol, + ) + result + |> list.all(fn(x) { x == True }) + |> should.be_true() + + let assert Ok(linspace) = maths.linear_space(10.0, 10.0, 5, False) + let assert Ok(result) = + maths.all_close( + linspace |> yielder.to_list() |> list.zip([10.0, 10.0, 10.0, 10.0, 10.0]), + 0.0, + tol, + ) result |> list.all(fn(x) { x == True }) |> should.be_true() @@ -123,7 +141,7 @@ pub fn list_logarithmic_space_test() { // Check that the function agrees, at some arbitrary input // points, with known function values // ---> With endpoint included - // - Positive start, stop, base + // - Positive start, stop let assert Ok(logspace) = maths.logarithmic_space(1.0, 3.0, 3, True, 10.0) let assert Ok(result) = maths.all_close( @@ -135,31 +153,7 @@ pub fn list_logarithmic_space_test() { |> list.all(fn(x) { x == True }) |> should.be_true() - // - Positive start, stop, negative base - let assert Ok(logspace) = maths.logarithmic_space(1.0, 3.0, 3, True, -10.0) - let assert Ok(result) = - maths.all_close( - logspace |> yielder.to_list() |> list.zip([-10.0, 100.0, -1000.0]), - 0.0, - tol, - ) - result - |> list.all(fn(x) { x == True }) - |> should.be_true() - - // - Positive start, negative stop, base - let assert Ok(logspace) = maths.logarithmic_space(1.0, -3.0, 3, True, -10.0) - let assert Ok(result) = - maths.all_close( - logspace |> yielder.to_list() |> list.zip([-10.0, -0.1, -0.001]), - 0.0, - tol, - ) - result - |> list.all(fn(x) { x == True }) - |> should.be_true() - - // - Positive start, base, negative stop + // - Positive start, negative stop let assert Ok(logspace) = maths.logarithmic_space(1.0, -3.0, 3, True, 10.0) let assert Ok(result) = maths.all_close( @@ -171,7 +165,7 @@ pub fn list_logarithmic_space_test() { |> list.all(fn(x) { x == True }) |> should.be_true() - // - Positive stop, base, negative start + // - Positive stop, negative start let assert Ok(logspace) = maths.logarithmic_space(-1.0, 3.0, 3, True, 10.0) let assert Ok(result) = maths.all_close( @@ -184,7 +178,7 @@ pub fn list_logarithmic_space_test() { |> should.be_true() // ----> Without endpoint included - // - Positive start, stop, base + // - Positive start, stop let assert Ok(logspace) = maths.logarithmic_space(1.0, 3.0, 3, False, 10.0) let assert Ok(result) = maths.all_close( @@ -198,9 +192,41 @@ pub fn list_logarithmic_space_test() { |> list.all(fn(x) { x == True }) |> should.be_true() + // Check that when start == stop and steps > 0, then + // the value start/stop value is just repeated, since the + // step increment will be 0 + let assert Ok(logspace) = maths.logarithmic_space(5.0, 5.0, 5, True, 5.0) + let assert Ok(result) = + maths.all_close( + logspace + |> yielder.to_list() + |> list.zip([3125.0, 3125.0, 3125.0, 3125.0, 3125.0]), + 0.0, + tol, + ) + result + |> list.all(fn(x) { x == True }) + |> should.be_true() + let assert Ok(logspace) = maths.logarithmic_space(5.0, 5.0, 5, False, 5.0) + let assert Ok(result) = + maths.all_close( + logspace + |> yielder.to_list() + |> list.zip([3125.0, 3125.0, 3125.0, 3125.0, 3125.0]), + 0.0, + tol, + ) + result + |> list.all(fn(x) { x == True }) + |> should.be_true() + // A negative number of points does not work (-3) maths.logarithmic_space(1.0, 3.0, -3, True, 10.0) |> should.be_error() + + // A negative base does not work (-10) + maths.logarithmic_space(1.0, 3.0, 3, True, -10.0) + |> should.be_error() } pub fn list_geometric_space_test() { @@ -259,7 +285,36 @@ pub fn list_geometric_space_test() { |> list.all(fn(x) { x == True }) |> should.be_true() - // Test invalid input (start and stop can't be equal to 0.0) + // Check that when start == stop and steps > 0, then + // the value start/stop value is just repeated, since the + // step increment will be 0 + let assert Ok(logspace) = maths.geometric_space(5.0, 5.0, 5, True) + let assert Ok(result) = + maths.all_close( + logspace + |> yielder.to_list() + |> list.zip([5.0, 5.0, 5.0, 5.0, 5.0]), + 0.0, + tol, + ) + result + |> list.all(fn(x) { x == True }) + |> should.be_true() + + let assert Ok(logspace) = maths.geometric_space(5.0, 5.0, 5, False) + let assert Ok(result) = + maths.all_close( + logspace + |> yielder.to_list() + |> list.zip([5.0, 5.0, 5.0, 5.0, 5.0]), + 0.0, + tol, + ) + result + |> list.all(fn(x) { x == True }) + |> should.be_true() + + // Test invalid input (start and stop can't be less than or equal to 0.0) maths.geometric_space(0.0, 1000.0, 3, False) |> should.be_error() @@ -311,44 +366,6 @@ pub fn list_arange_test() { |> should.equal([-5.0, -4.0, -3.0, -2.0]) } -pub fn list_exponential_space_test() { - let assert Ok(tolerance) = float.power(10.0, -6.0) - - // Check that the function agrees, at some arbitrary input - // points, with known function values - // ---> With endpoint included - let assert Ok(exp_space) = maths.exponential_space(1.0, 1000.0, 4, True) - let assert Ok(result) = - maths.all_close( - exp_space |> yielder.to_list() |> list.zip([1.0, 10.0, 100.0, 1000.0]), - 0.0, - tolerance, - ) - result - |> list.all(fn(x) { x == True }) - |> should.be_true() - - // ---> Without endpoint included - let assert Ok(exp_space) = maths.exponential_space(1.0, 1000.0, 4, False) - let assert Ok(result) = - maths.all_close( - exp_space - |> yielder.to_list() - |> list.zip([ - 1.0, 5.623413251903491, 31.622776601683793, 177.82794100389228, - ]), - 0.0, - tolerance, - ) - result - |> list.all(fn(x) { x == True }) - |> should.be_true() - - // A negative number of points does not work (-3) - maths.exponential_space(1.0, 1000.0, -3, True) - |> should.be_error() -} - pub fn list_symmetric_space_test() { let assert Ok(tolerance) = float.power(10.0, -6.0) @@ -370,6 +387,12 @@ pub fn list_symmetric_space_test() { |> yielder.to_list() |> should.equal([-15.0, -12.5, -10.0, -7.5, -5.0]) + // Negative Radius (simply reverses the order of the values) + let assert Ok(sym_space) = maths.symmetric_space(0.0, -5.0, 5) + sym_space + |> yielder.to_list() + |> should.equal([5.0, 2.5, 0.0, -2.5, -5.0]) + // Uneven number of points let assert Ok(sym_space) = maths.symmetric_space(0.0, 2.0, 4) let assert Ok(result) = @@ -384,6 +407,14 @@ pub fn list_symmetric_space_test() { |> list.all(fn(x) { x == True }) |> should.be_true() + // Check that when radius == 0 and steps > 0, then + // the value center value is just repeated, since the + // step increment will be 0 + let assert Ok(sym_space) = maths.symmetric_space(10.0, 0.0, 4) + sym_space + |> yielder.to_list() + |> should.equal([10.0, 10.0, 10.0, 10.0]) + // A negative number of points does not work (-5) maths.symmetric_space(0.0, 5.0, -5) |> should.be_error()