Skip to content

Commit

Permalink
Add complex.from_real_image()
Browse files Browse the repository at this point in the history
This staticmethod constructor allows us to create a complex instance
from two Reals.

Also add a test suite for complex and fix division to raise exception on
zero-divison.

The acton.guide has been updated with a page on complex numbers.
  • Loading branch information
plajjan committed Jan 9, 2025
1 parent 0cf4d78 commit 645d787
Show file tree
Hide file tree
Showing 5 changed files with 299 additions and 1 deletion.
11 changes: 11 additions & 0 deletions base/builtin/complex.c
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ B_complex B_complexG_new(B_Number wit, $WORD c) {
return $NEW(B_complex,wit,c);
}

B_complex B_complexD_from_real_imag (B_Real wit1, B_Real wit2, $WORD real, $WORD imag) {
double re = wit1->$class->__float__(wit1, real)->val;
double im = wit2->$class->__float__(wit2, imag)->val;
return toB_complex(re + im * _Complex_I);
}

B_NoneType B_complexD___init__(B_complex self, B_Number wit, $WORD c){
self->val = wit->$class->__complx__(wit,c)->val;
return B_None;
Expand Down Expand Up @@ -112,6 +118,11 @@ B_complex B_NumberD_complexD_conjugate (B_NumberD_complex wit, B_complex c) {
// B_DivD_complex /////////////////////////////////////////////////////////////////////////////////////////

B_complex B_DivD_complexD___truediv__ (B_DivD_complex wit, B_complex a, B_complex b) {
if (b->val == 0.0) {
char errmsg[1024];
snprintf(errmsg, sizeof(errmsg), "complex truediv: divisor is zero");
$RAISE((B_BaseException)$NEW(B_ZeroDivisionError, to$str(errmsg)));
}
return toB_complex(a->val/b->val);
}

Expand Down
5 changes: 4 additions & 1 deletion base/src/__builtin__.act
Original file line number Diff line number Diff line change
Expand Up @@ -516,11 +516,14 @@ protocol Div[A]:
protocol Hashable (Eq):
__hash__ : () -> int


class complex (value):
def __init__(self, val: Number) -> None:
NotImplemented

@staticmethod
def from_real_imag(real: Real, imag: Real) -> complex:
NotImplemented

class dict[A(Hashable),B] (object):
def __init__(self, iterable: ?Iterable[(A,B)]) -> None:
NotImplemented
Expand Down
1 change: 1 addition & 0 deletions docs/acton-by-example/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- [Variable data types](primitives.md)
- [Scalars](primitives/scalars.md)
- [float](primitives/float.md)
- [complex](primitives/complex.md)
- [Lists](primitives/lists.md)
- [Dictionaries](primitives/dicts.md)
- [Tuples](primitives/tuples.md)
Expand Down
155 changes: 155 additions & 0 deletions docs/acton-by-example/src/primitives/complex.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# Complex Numbers

Complex numbers in Acton provide support for mathematical operations involving both real and imaginary components. Complex numbers implement the `Number` protocol and include operations for arithmetic, comparison, and hashing. Complex also implement the `Div[Eq]`, `Eq` and `Hashable` protocols.

## Construction

Complex numbers can be created using the `from_real_imag` constructor method:

```python
# Create a complex number with real part 3.0 and imaginary part 4.0
c = complex.from_real_imag(3.0, 4.0)
```

## Properties

Complex numbers have two main properties accessible through methods:

- `real()`: Returns the real part as a float
- `imag()`: Returns the imaginary part as a float

```python
c = complex.from_real_imag(3.0, 4.0)
r = c.real() # 3.0
i = c.imag() # 4.0
```

## Arithmetic Operations

Complex numbers support standard arithmetic operations:

### Addition and Subtraction
```python
a = complex.from_real_imag(1.0, 2.0)
b = complex.from_real_imag(3.0, 4.0)
sum = a + b # 4.0 + 6.0i
diff = b - a # 2.0 + 2.0i
```

### Multiplication
Complex multiplication follows the rule (a + bi)(c + di) = (ac - bd) + (ad + bc)i
```python
# (1 + 2i)(3 + 4i) = (1×3 - 2×4) + (1×4 + 2×3)i = -5 + 10i
prod = a * b # -5.0 + 10.0i
```

### Division
Complex division is performed by multiplying both numerator and denominator by the complex conjugate of the denominator:
```python
# (1 + 2i)/(1 + i) = (1 + 2i)(1 - i)/(1 + i)(1 - i) = (3 + i)/2
quotient = a / b
```

### Power Operation
```python
c = complex.from_real_imag(1.0, 1.0)
squared = c ** 2 # 0.0 + 2.0i
```

## Special Operations

### Complex Conjugate
The complex conjugate of a + bi is a - bi:
```python
c = complex.from_real_imag(1.0, 2.0)
conj = c.conjugate() # 1.0 - 2.0i
```

### Absolute Value (Magnitude)
The absolute value or magnitude of a complex number is the square root of (a² + b²):
```python
c = complex.from_real_imag(3.0, 4.0)
magnitude = abs(c) # 5.0
```

## Comparison and Hashing

Complex numbers can be compared for equality and can be used as dictionary keys:

```python
a = complex.from_real_imag(1.0, 2.0)
b = complex.from_real_imag(1.0, 2.0)
c = complex.from_real_imag(2.0, 1.0)

a == b # True
a != c # True

# Can be used as dictionary keys
d = {a: "value"}
```

## Edge Cases and Limitations

### Division by Zero
Attempting to divide by zero raises a `ZeroDivisionError`:
```python
zero = complex.from_real_imag(0.0, 0.0)
# a / zero # Raises ZeroDivisionError
```

### Numerical Limits
Complex numbers use floating-point arithmetic and are subject to the same limitations:
- Very large numbers may result in infinity
- Very small numbers may underflow to zero
- Floating-point arithmetic may introduce small rounding errors

## Protocols

Complex numbers implement multiple protocols that define their behavior:

### Number Protocol
The `Number` protocol provides:
- Basic arithmetic operations (+, -, *)
- Power operation (**)
- Negation (-x)
- Properties for real and imaginary parts
- Absolute value (magnitude)
- Complex conjugate

### Div[complex] Protocol
The `Div[complex]` protocol adds:
- Division operation (/)
- In-place division operation (/=)

### Eq Protocol
The `Eq` protocol provides:
- Equality comparison (==)
- Inequality comparison (!=)

### Hashable Protocol
The `Hashable` protocol (which extends `Eq`) enables:
- Hash computation via `__hash__`
- Use as dictionary keys or set elements

## Best Practices

1. Use appropriate tolerance when comparing results of complex arithmetic due to floating-point rounding:
```python
if abs(result.real() - expected_real) < 1e-10 and abs(result.imag() - expected_imag) < 1e-10:
# Numbers are equal within tolerance
```

2. Handle potential exceptions when performing division:
```python
try:
result = a / b
except ZeroDivisionError:
# Handle division by zero
```

3. Consider numerical stability when working with very large or very small numbers:
```python
# Check for overflow/underflow
if result.real() == float('inf') or result.imag() == float('inf'):
# Handle overflow
```
128 changes: 128 additions & 0 deletions test/builtins_auto/test_complex.act
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
def test_complex() -> bool:
# Test basic construction
c1 = complex.from_real_imag(3.0, 4.0)
if c1.real() != 3.0 or c1.imag() != 4.0:
print("Basic construction failed - expected 3.0+4.0i, got:", c1.real(), "+", c1.imag(), "i")
return False

# Test zero components
c2 = complex.from_real_imag(0.0, 0.0)
if c2.real() != 0.0 or c2.imag() != 0.0:
print("Zero construction failed - expected 0.0+0.0i, got:", c2.real(), "+", c2.imag(), "i")
return False

# Test negative components
c3 = complex.from_real_imag(-1.0, -1.0)
if c3.real() != -1.0 or c3.imag() != -1.0:
print("Negative construction failed - expected -1.0-1.0i, got:", c3.real(), "+", c3.imag(), "i")
return False

# Test addition
a = complex.from_real_imag(1.0, 2.0)
b = complex.from_real_imag(3.0, 4.0)
sum = a + b
if sum.real() != 4.0 or sum.imag() != 6.0:
print("Addition failed - expected 4.0+6.0i, got:", sum.real(), "+", sum.imag(), "i")
return False

# Test subtraction
diff = b - a
if diff.real() != 2.0 or diff.imag() != 2.0:
print("Subtraction failed - expected 2.0+2.0i, got:", diff.real(), "+", diff.imag(), "i")
return False

# Test multiplication
# (1 + 2i)(3 + 4i) = (1×3 - 2×4) + (1×4 + 2×3)i = -5 + 10i
prod = a * b
if prod.real() != -5.0 or prod.imag() != 10.0:
print("Multiplication failed - expected -5.0+10.0i, got:", prod.real(), "+", prod.imag(), "i")
return False

# Test division
# (1 + 2i)/(1 + i) = (1 + 2i)(1 - i)/(1 + i)(1 - i) = (1 - i + 2i - 2i²)/(1 - i²) = (3 + i)/2
num = complex.from_real_imag(1.0, 2.0)
den = complex.from_real_imag(1.0, 1.0)
quot = num / den
if abs(quot.real() - 1.5) > 1e-10 or abs(quot.imag() - 0.5) > 1e-10:
print("Division failed - expected 1.5+0.5i, got:", quot.real(), "+", quot.imag(), "i")
return False

# Test in-place division
dividend = complex.from_real_imag(1.0, 2.0)
divisor = complex.from_real_imag(1.0, 1.0)
dividend /= divisor
if abs(dividend.real() - 1.5) > 1e-10 or abs(dividend.imag() - 0.5) > 1e-10:
print("In-place division failed - expected 1.5+0.5i, got:", dividend.real(), "+", dividend.imag(), "i")
return False

# Test conjugate
conj = a.conjugate()
if conj.real() != 1.0 or conj.imag() != -2.0:
print("Conjugate failed - expected 1.0-2.0i, got:", conj.real(), "+", conj.imag(), "i")
return False

# Test absolute value
# |1 + 2i| = √(1² + 2²) = √5
abs_val = a.__abs__()
if abs(abs_val - 2.236067977499790) > 1e-10:
print("Absolute value failed - expected ≈2.236067977499790, got:", abs_val)
return False

# Test power operation
# (1 + i)² = 1 + 2i - 1 = 2i
c = complex.from_real_imag(1.0, 1.0)
# TODO: this depends on __fromatom__ being implemented
#d = c ** 2
#if abs(d.real()) > 1e-10 or abs(d.imag() - 2.0) > 1e-10:
# print("Power operation failed - expected 0.0+2.0i, got:", d.real(), "+", d.imag(), "i")
# return False

# Test equality and hash consistency
c1 = complex.from_real_imag(1.0, 2.0)
c2 = complex.from_real_imag(1.0, 2.0)
c3 = complex.from_real_imag(2.0, 1.0)

if c1 != c2:
print("Equality failed - identical complex numbers not equal")
return False

if c1 == c3:
print("Equality failed - different complex numbers compared equal")
return False

if hash(c1) != hash(c2):
print("Hash consistency failed - equal numbers have different hashes")
return False

# Test division by zero
try:
zero = complex.from_real_imag(0.0, 0.0)
bad = a / zero
print("Division by zero didn't raise expected exception")
return False
except ZeroDivisionError:
pass # Expected behavior

# Test very large numbers
large = 1e308
big = complex.from_real_imag(large, large)
overflow = big * big
if not (overflow.real() == float('inf') or overflow.imag() == float('inf')):
print("Large number multiplication failed to handle overflow correctly")
return False

# Test very small numbers
small = 1e-308
tiny = complex.from_real_imag(small, small)
underflow = tiny * tiny
if not (underflow.real() == 0.0 or abs(underflow.real()) < 1e-307):
print("Small number multiplication failed to handle underflow correctly")
return False

return True

actor main(env):
if test_complex():
print("All complex number tests passed!")
env.exit(0)
env.exit(1)

0 comments on commit 645d787

Please sign in to comment.