Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Various documentation improvements. #265

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 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
16 changes: 10 additions & 6 deletions Sources/ComplexModule/Complex+Numeric.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,18 @@ extension Complex: Numeric {
self.init(real, 0)
}

/// The -norm of the value (`max(abs(real), abs(imaginary))`).
/// The infinity-norm of the value (a.k.a. "maximum norm" or "Чебышёв norm").
///
/// If you need the Euclidean norm (a.k.a. 2-norm) use the `length` or
/// `lengthSquared` properties instead.
/// Equal to `max(abs(real), abs(imaginary))`.
///
/// Edge cases:
/// - If `z` is not finite, `z.magnitude` is `.infinity`.
/// - If `z` is zero, `z.magnitude` is `0`.
/// If you need to work with the Euclidean norm (a.k.a. 2-norm) instead,
/// use the `length` or `lengthSquared` properties. If you just need to
/// know "how big" a number is, use this property.
///
/// **Edge cases:**
///
/// - If `z` is not finite, `z.magnitude` is infinity.
/// - If `z` is zero, `z.magnitude` is zero.
/// - Otherwise, `z.magnitude` is finite and non-zero.
///
/// See also `.length` and `.lengthSquared`.
Expand Down
53 changes: 19 additions & 34 deletions Sources/ComplexModule/Complex.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,39 +11,11 @@

import RealModule

/// A complex number represented by real and imaginary parts.
/// A [complex number](https://en.wikipedia.org/wiki/Complex_number).
///
/// TODO: introductory text on complex numbers
///
/// Implementation notes:
///
/// This type does not provide heterogeneous real/complex arithmetic,
/// not even the natural vector-space operations like real * complex.
/// There are two reasons for this choice: first, Swift broadly avoids
/// mixed-type arithmetic when the operation can be adequately expressed
/// by a conversion and homogeneous arithmetic. Second, with the current
/// typechecker rules, it would lead to undesirable ambiguity in common
/// expressions (see README.md for more details).
///
/// Unlike C's `_Complex` and C++'s `std::complex<>` types, we do not
/// attempt to make meaningful semantic distinctions between different
/// representations of infinity or NaN. Any Complex value with at least
/// one non-finite component is simply "non-finite". In as much as
/// possible, we use the semantics of the point at infinity on the
/// Riemann sphere for such values. This approach simplifies the number of
/// edge cases that need to be considered for multiplication, division, and
/// the elementary functions considerably.
///
/// `.magnitude` does not return the Euclidean norm; it uses the "infinity
/// norm" (`max(|real|,|imaginary|)`) instead. There are two reasons for this
/// choice: first, it's simply faster to compute on most hardware. Second,
/// there exist values for which the Euclidean norm cannot be represented
/// (consider a number with `.real` and `.imaginary` both equal to
/// `RealType.greatestFiniteMagnitude`; the Euclidean norm would be
/// `.sqrt(2) * .greatestFiniteMagnitude`, which overflows). Using
/// the infinity norm avoids this problem entirely without significant
/// downsides. You can access the Euclidean norm using the `length`
/// property.
/// `Complex` is an `AlgebraicField`, so it has all the normal arithmetic
/// operators. It conforms to `ElementaryFunctions`, so it has all the usual
/// math functions.
@frozen
public struct Complex<RealType> where RealType: Real {
// A note on the `x` and `y` properties
Expand All @@ -53,11 +25,11 @@ public struct Complex<RealType> where RealType: Real {
// `.real` and `.imaginary` properties, which wrap this storage and
// fixup the semantics for non-finite values.

/// The real component of the value.
/// The storage for the real component of the value.
@usableFromInline @inline(__always)
internal var x: RealType

/// The imaginary part of the value.
/// The storage for the imaginary part of the value.
@usableFromInline @inline(__always)
internal var y: RealType

Expand Down Expand Up @@ -95,11 +67,24 @@ extension Complex {
set { y = newValue }
}

/// The raw representation of the value.
///
/// Use this when you need the underlying RealType values,
/// without fixup for NaN or infinity.
public var rawStorage: (x: RealType, y: RealType) {
@_transparent
get { (x, y) }
@_transparent
set { (x, y) = newValue }
}

/// The raw representation of the real part of this value.
@available(*, deprecated, message: "Use rawStorage")
@_transparent
public var _rawX: RealType { x }

/// The raw representation of the imaginary part of this value.
@available(*, deprecated, message: "Use rawStorage")
@_transparent
public var _rawY: RealType { y }
}
Expand Down
51 changes: 51 additions & 0 deletions Sources/ComplexModule/Documentation.docc/Complex.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# ``Complex``

## Topics

### Real and imaginary parts

A `Complex` value is represented with two `RealType` values, corresponding to
the real and imaginary parts of the number:
```swift
let z = Complex(1,-1) // 1 - i
let re = z.real // 1
let im = z.imaginary // -1
```
All `Complex` numbers with a non-finite component is treated as a single
"point at infinity," with infinite magnitude and indeterminant phase. Thus,
the real and imaginary parts of an infinity are nan.
```swift
let w = Complex<Double>.infinity
w == -w // true
let re = w.real // .nan
let im = w.imag // .nan
```
See <doc:Infinity> for more details.

- ``init(_:_:)``
- ``init(_:)-5aesj``
- ``init(imaginary:)``
- ``real``
- ``imaginary``

### Magnitude and norms

See the article <doc:Magnitude> for more details.

- ``magnitude``
- ``length``
- ``lengthSquared``
- ``normalized``

### Polar representations

- ``init(length:phase:)``
- ``phase``
- ``length``
- ``polar``

### Conversions from other types

- ``init(_:)-4csd3``
- ``init(_:)-80jml``
- ``init(exactly:)-767k9``
63 changes: 63 additions & 0 deletions Sources/ComplexModule/Documentation.docc/ComplexModule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# ``ComplexModule``

Types and operations for working with complex numbers.

## Representation

The `Complex` type is generic over an associated `RealType`; complex numbers
are represented as two `RealType` values, the real and imaginary parts of the
number.
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For broader compatibility with various Markdown viewer implementations, would it be possible to add an extra line before and after such code blocks?

let z = Complex<Double>(1, 2)
let re = z.real
let im = z.imaginary
```

### Memory layout

A `Complex` value is stored as two `RealType` values arranged consecutively
in memory. Thus it has the same memory layout as:
- A Fortran complex value built on the corresponding real type (as used
by BLAS and LAPACK).
- A C struct with real and imaginary parts and nothing else (as used by
computational libraries predating C99).
- A C99 `_Complex` value built on the corresponding real type.
- A C++ `std::complex` value built on the corresponding real type.
Functions taking complex arguments in these other languages are not
automatically converted on import, but you can safely write shims that
map them into Swift types by converting pointers.

## Real-Complex arithmetic

Because the real numbers are a subset of the complex numbers, many
languages support arithmetic with mixed real and complex operands.
For example, C allows the following:
```c
#include <complex.h>
double r = 1;
double complex z = CMPLX(0, 2); // 2i
double complex w = r + z; // 1 + 2i
```
The `Complex` type does not provide such mixed operators. There are two
reasons for this choice. First, Swift generally avoids mixed-type
arithmetic, period. Second, mixed-type arithmetic operators lead to
undesirable behavior in common expressions when combined with literal
type inference. Consider the following example:
```swift
let a: Double = 1
let b = 2*a
Comment on lines +67 to +68
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is easier to follow along the arithmetic examples (especially the next one), if this unsupported heterogeneous operator example would re-use the example from C above (and below) – I may be wrong.

Suggested change
let a: Double = 1
let b = 2*a
import ComplexModule
let r: Double = 1.0
let z = Complex<Double>(0, 2)
let w = r + z // ❌

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The point is to provide an example that would break if we did have a heterogeneous operator.

Copy link
Contributor

@markuswntr markuswntr Jul 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right - my example is misleading. Does it make sense to adjust the other two examples then, and make them use multiplication over addition?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to rearrange stuff to clarify it a little bit, might revisit later.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

```
If we had a heterogeneous `*` operator defined, then if there's no prevailing
type context (i.e. we aren't in an extension on some type), the expression
`2*a` is ambiguous; `2` could be either a `Double` or `Complex<Double>`. In
a `Complex` context, the situation is even worse: `2*a` is inferred to have
type `Complex`.

Therefore, the `Complex` type does not have these operators. In order to write
the example from C above, you would use an explicit conversion:
```swift
import ComplexModule
let r = 1.0
let z = Complex<Double>(0, 2)
let w = Complex(r) + z
```
49 changes: 49 additions & 0 deletions Sources/ComplexModule/Documentation.docc/Infinity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Zero and infinity

Semantics of `Complex` zero and infinity values, and important considerations
when porting code from other languages.

Unlike C and C++'s complex types, `Complex` does not attempt to make a
semantic distinction between different infinity and NaN values. Any `Complex`
datum with a non-finite component is treated as the "point at infinity" on
the Riemann sphere--a value with infinite magnitude and unspecified phase.

As a consequence, all values with either component infinite or NaN compare
equal, and hash the same. Similarly, all zero values compare equal and hash
the same.

## Rationale

This choice has some drawbacks,¹ but also some significant advantages.
In particular, complex multiplication is the most common operation performed
with a complex type, and one would like to be able to use the usual naive
arithmetic implementation, consisting of four real multiplications and two
real additions:
```
(a + bi) * (c + di) = (ac - bd) + (ad + bc)i
```
`Complex` can use this implementation, because we do not differentiate between
infinities and NaN values. C and C++, by contrast, cannot use this
implementation by default, because, for example:
```
(1 + ∞i) * (0 - 2i) = (1*0 - ∞*(-2)) + (1*(-2) + ∞*0)i
= (0 - ∞) + (-2 + nan)i
= -∞ + nan i
```
`Complex` treats this as "infinity", which is the correct result. C and C++
treat it as a nan value, however, which is incorrect; infinity multiplied
by a non-zero number should be infinity. Thus, C and C++ (by default) must
detect these special cases and fix them up, which makes multiplication a
more computationally expensive operation.²

### Footnotes:
¹ W. Kahan, Branch Cuts for Complex Elementary Functions, or Much Ado
About Nothing's Sign Bit. In A. Iserles and M.J.D. Powell, editors,
_Proceedings The State of Art in Numerical Analysis_, pages 165–211, 1987.

² This can be addressed in C programs by use of the `STDC CX_LIMITED_RANGE`
pragma, which instructs the compiler to simply not care about these cases.
Unfortunately, this pragma is not often used in real C or C++ programs
(though it does see some use in _libraries_). Programmers tend to specify
`-ffast-math` or maybe `-ffinite-math-only` instead, which has other
undesirable consequences.
Loading