Skip to content

Commit 31da646

Browse files
committed
2 parents cd8d77e + 9917e26 commit 31da646

File tree

7 files changed

+339
-2
lines changed

7 files changed

+339
-2
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@ nimcache/
22
*.exe
33
bin/
44
.vscode/
5+
*.code-workspace
6+
*.html
7+
!*.*

README.md

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,12 +264,58 @@ proc adaptiveGauss*[T](f: proc(x: float, optional: seq[T]): T, xStart, xEnd: flo
264264
```
265265
If you don't understand what the "T" stands for, you can replace it with "float" in your head and read up on "Generics" in Nim.
266266

267+
# Optimization
268+
## Optimization methods
269+
## 1 dimensional function optimization
270+
So far only a few methods have been implemented:
271+
272+
### One Dimensional optimization methods
273+
- `steepest_descent` - Standard method for local minimum finding over a 2D plane
274+
- `conjugate_gradient` - iterative implementation of solving Ax = b
275+
- `newtons` - Newton-Raphson implementation for 1-dimensional functions
276+
277+
## Usage
278+
Using default parameters the methods need 3 things: the function f(t, y) = y'(t, y), the initial values and the timepoints that you want the solution y(t) at.
279+
280+
## Quick Tutorial
281+
282+
Say we have some differentiable function and we would like to find one of its roots
283+
284+
f = $\frac{1}{3}$x$^{3}$ - 2x$^{2}$ + 3x
285+
286+
$\frac{df}{dx}$ = x$^{2}$ - 4x + 3
287+
288+
289+
290+
If we translate this to code we get:
291+
292+
```nim
293+
import math
294+
import numericalnim
295+
296+
proc f(x:float64): float64 = (1.0 / 3.0) * x ^ 3 - 2 * x ^ 2 + 3 * x
297+
proc df(x:float64): float64 = x ^ 2 - 4 * x + 3
298+
```
299+
300+
now given a starting point (and optional precision) we can estimate a nearby root
301+
We know for this function our actual root is 0
302+
303+
```nim
304+
import numericalnim
305+
var start = 0.5
306+
result = newtons(f, df, start)
307+
echo result
308+
309+
-1.210640218782444e-23
310+
```
311+
Pretty close!
267312

268313
# Utils
269314
I have included a few handy tools in `numericalnim/utils`.
270315
## Vector
271316
Hurray! Yet another vector library! This was mostly done for my own use but I figured it could come in handy if one wanted to just throw something together. It's main purpose is to enable you to solve systems of ODEs using your own types (for example arbitrary precision numbers). The `Vector` type is just a glorified seq with operator overload. No vectorization (unless the compiler does it automatically) sadly. Maybe can get OpenMP to work (or you maybe you, dear reader, can fix it :wink).
272317
The following operators and procs are supported:
318+
273319
- `+` : Addition between `Vector`s and floats.
274320
- `-` : Addition between `Vector`s and floats.
275321
- `+=`, `-=` : inplace addition and subtraction.
@@ -281,10 +327,12 @@ The following operators and procs are supported:
281327
- `.*=`, `./=` : inplace elementwise multiplication and division between `Vector`s. (not nested `Vector`s)
282328
- `-` : negation (-Vector).
283329
- `dot` : Same as `*` between `Vector`s. It is recursive so it will not be a matrix dot product if nested `Vector`s are used.
284-
- `abs` : The magnitude of the `Vector`. It is recursive so it may not be one of the usual norms.
330+
- `abs` : The magnitude of the `Vector`. It is recursive so it may not be one of the usual norms. Equivalent to norm(`Vector`, 2)
285331
- `[]` : Use `v[i]` to get the i:th element of the `Vector`.
286332
- `==` : Compares two `Vector`s to see if they are equal.
287333
- `@` : Unpacks the Vector to (nested) seqs. Works with 1, 2 and 3 dimensional Vectors.
334+
- `^` : Element-wise exponentiation, works with natural and floating point powers, returns a new Vector object
335+
- `norm` : General vector norm function
288336

289337
A `Vector` is created using the `newVector` proc and is passed an `openArray` of the elements:
290338
```nim
@@ -340,3 +388,4 @@ If you want to use Arraymancer with NumericalNim, the basics should work but I h
340388
- Add more integration methods (Gaussian Quadrature on it's way. Done).
341389
- Make the existing code more efficient and robust.
342390
- Add parallelization of some kind to speed it up. `Vector` would probably benefit from it.
391+
- More optimization methods!

src/numericalnim.nim

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@ import numericalnim/integrate
44
export integrate
55
import numericalnim/ode
66
export ode
7+
import numericalnim/optimize
8+
export optimize

src/numericalnim/optimize.nim

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import strformat
2+
import arraymancer
3+
import sequtils
4+
import math
5+
6+
proc steepest_descent*(deriv: proc(x: float64): float64, start: float64, gamma: float64 = 0.01, precision: float64 = 1e-5, max_iters: Natural = 1000):float64 {.inline.} =
7+
## Gradient descent optimization algorithm for finding local minimums of a function with derivative 'deriv'
8+
##
9+
## Assuming that a multivariable function F is defined and differentiable near a minimum, F(x) decreases fastest
10+
## when going in the direction negative to the gradient of F(a), similar to how water might traverse down a hill
11+
## following the path of least resistance.
12+
## can benefit from preconditioning if the condition number of the coefficient matrix is ill-conditioned
13+
## Input:
14+
## - deriv: derivative of a multivariable function F
15+
## - start: starting point near F's minimum
16+
## - gamma: step size multiplier, used to control the step size between iterations
17+
## - precision: numerical precision
18+
## - max_iters: maximum iterations
19+
##
20+
## Returns:
21+
## - float64.
22+
var
23+
current = 0.0
24+
x = start
25+
26+
for i in 0 .. max_iters:
27+
# calculate the next direction to propogate
28+
current = x
29+
x = current - gamma * deriv(current)
30+
31+
# If we haven't moved much since the last iteration, break
32+
if abs(x - current) <= precision:
33+
break
34+
35+
if i == max_iters:
36+
raise newException(ArithmeticError, "Maximum iterations for Steepest descent method exceeded")
37+
38+
return x
39+
40+
proc conjugate_gradient*[T](A, b, x_0: Tensor[T], tolerance: float64): Tensor[T] =
41+
## Conjugate Gradient method.
42+
## Given a Symmetric and Positive-Definite matrix A, solve the linear system Ax = b
43+
## Symmetric Matrix: Square matrix that is equal to its transpose, transpose(A) == A
44+
## Positive Definite: Square matrix such that transpose(x)Ax > 0 for all x in R^n
45+
##
46+
## Input:
47+
## - A: NxN square matrix
48+
## - b: vector on the right side of Ax=b
49+
## - x_0: Initial guess vector
50+
##
51+
## Returns:
52+
## - Tensor.
53+
54+
var r = b - (A * x_0)
55+
var p = r
56+
var rsold = (r.transpose() * r)[0,0] # multiplication returns a Tensor, so need the first element
57+
58+
result = x_0
59+
60+
var
61+
Ap = A
62+
alpha = 0.0
63+
rsnew = 0.0
64+
Ap_p = 0.0
65+
66+
for i in 1 .. b.shape[0]:
67+
Ap = A * p
68+
Ap_p = (p.transpose() * Ap)[0,0]
69+
alpha = rsold / Ap_p
70+
result = result + alpha * p
71+
r = r - alpha * Ap
72+
rsnew = (r.transpose() * r)[0,0]
73+
if sqrt(rsnew) < tolerance:
74+
break
75+
p = r + (rsnew / rsold) * p
76+
rsold = rsnew
77+
78+
79+
proc newtons*(f: proc(x: float64): float64, deriv: proc(x: float64): float64, start: float64, precision: float64 = 1e-5, max_iters: Natural = 1000): float64 {.raises: [ArithmeticError].} =
80+
## Newton-Raphson implementation for 1-dimensional functions
81+
82+
## Given a single variable function f and it's derivative, calcuate an approximation to f(x) = 0
83+
## Input:
84+
## - f: "Well behaved" function of a single variable with a known root
85+
## - deriv: derivative of f with respect to x
86+
## - start: starting x
87+
## - precision: numerical precision
88+
## - max_iters: maxmimum number of iterations
89+
##
90+
## Returns:
91+
## - float64.
92+
var
93+
x_iter = start
94+
i = 0
95+
current_f = f(start)
96+
97+
while abs(current_f) >= precision and i <= max_iters:
98+
current_f = f(x_iter)
99+
x_iter = x_iter - (current_f / deriv(x_iter))
100+
i += 1
101+
if i == max_iters:
102+
raise newException(ArithmeticError, "Maximum iterations for Newtons method exceeded")
103+
104+
return x_iter - (current_f / deriv(x_iter))
105+
106+
107+
108+

src/numericalnim/utils.nim

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,20 @@ proc abs*[T](v1: Vector[T]): float {.inline.} =
192192
result = sqrt(result)
193193
proc mean_squared_error*[T](v1: Vector[T]): float {.inline.} = abs(v1) / v1.len.toFloat
194194
195+
proc `^`*[float](v: Vector[float], power: Natural): Vector[float] {.inline.} =
196+
## Returns a Vector object after raising each element to a power (Natural number powers)
197+
var newComponents = newSeq[float](v.len)
198+
for i in 0 .. v.components.high:
199+
newComponents[i] = v[i] ^ power
200+
result = newVector(newComponents)
201+
202+
proc `^`*[float](v: Vector[float], power: float): Vector[float] {.inline.} =
203+
## Returns a Vector object after raising each element to a power (float powers)
204+
var newComponents = newSeq[float](v.len)
205+
for i in 0 .. v.components.high:
206+
newComponents[i] = pow(v[i], power)
207+
result = newVector(newComponents)
208+
195209
196210
proc clone*[T](x: T): T {.inline.} = x
197211
proc mean_squared_error*[T](y_true, y: T): float {.inline.} = abs(y_true - y)
@@ -206,6 +220,7 @@ proc hermiteSpline*[T](x, x1, x2: float, y1, y2, dy1, dy2: T): T {.inline.}=
206220
let h11 = t ^ 3 - t ^ 2
207221
result = h00 * y1 + h10 * (x2 - x1) * dy1 + h01 * y2 + h11 * (x2 - x1) * dy2
208222
223+
209224
proc hermiteInterpolate*[T](x: openArray[float], t: openArray[float],
210225
y, dy: openArray[T]): seq[T] {.inline.} =
211226
# loop over each interval and check if x is in there, if x is sorted
@@ -237,6 +252,8 @@ proc hermiteInterpolate*[T](x: openArray[float], t: openArray[float],
237252

238253

239254

255+
256+
240257
proc sortDataset*[T](X: openArray[float], Y: openArray[T]): seq[(float, T)] {.inline.} =
241258
if X.len != Y.len:
242259
raise newException(ValueError, "X and Y must have the same length")
@@ -252,6 +269,7 @@ proc isClose*[T](y1, y2: T, tol: float = 1e-3): bool {.inline.} =
252269
else:
253270
return false
254271
272+
255273
proc arange*(x1, x2, dx: float, includeStart = true, includeEnd = false): seq[float] {.inline.} =
256274
let dx = abs(dx) * sgn(x2 - x1).toFloat
257275
if dx == 0.0:
@@ -275,6 +293,26 @@ proc linspace*(x1, x2: float, N: int): seq[float] {.inline.} =
275293
result.add(x1 + dx * i.toFloat)
276294
result.add(x2)
277295
296+
297+
proc norm*(v1: Vector, p: int): float64 =
298+
## Calculate various norms of our Vector class
299+
300+
# we have to make a case for p = 0 to avoid division by zero, may as well flesh them all out
301+
case p:
302+
of 0:
303+
# max(v1) Infinity norm
304+
result = float64(max(@v1))
305+
of 1:
306+
# sum(v1) Taxicab norm
307+
result = float64(sum(@v1))
308+
of 2:
309+
# sqrt(sum([v ^ 2 for v in v1])) Euclidean norm
310+
result = sqrt(sum(@(v1 ^ 2)))
311+
else:
312+
# pow(sum([v ^ p for v in v1]), 1.0/p) P norm
313+
result = pow(sum(@(v1 ^ p)), 1.0 / float64(p))
314+
315+
278316
template timeit*(s: untyped, n = 100, msg = ""): untyped =
279317
var tTotal = 0.0
280318
for i in 1 .. n:

tests/test_optimize.nim

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import unittest, math, sequtils, arraymancer
2+
import numericalnim
3+
4+
test "steepest_descent func":
5+
proc df(x: float): float = 4 * x^3 - 9.0 * x^2
6+
let start = 6.0
7+
let gamma = 0.01
8+
let precision = 0.00001
9+
let max_iters = 10000
10+
let correct = 2.24996
11+
let value = steepest_descent(df, start, gamma, precision, max_iters)
12+
check isClose(value, correct, tol = 1e-5)
13+
14+
test "steepest_descent func starting at zero":
15+
proc df(x: float): float = 4 * x^3 - 9.0 * x^2 + 4
16+
let start = 0.0
17+
let correct = -0.59301
18+
let value = steepest_descent(df, start)
19+
check isClose(value, correct, tol = 1e-5)
20+
21+
test "conjugate_gradient func":
22+
var A = toSeq([4.0, 1.0, 1.0, 3.0]).toTensor.reshape(2,2).astype(float64)
23+
var x = toSeq([2.0, 1.0]).toTensor.reshape(2,1)
24+
var b = toSeq([1.0,2.0]).toTensor.reshape(2,1)
25+
let tol = 0.001
26+
let correct = toSeq([0.090909, 0.636363]).toTensor.reshape(2,1).astype(float64)
27+
28+
let value = conjugate_gradient(A, b, x, tol)
29+
check isClose(value, correct, tol = 1e-5)
30+
31+
test "Newtons 1 dimension func":
32+
proc f(x:float64): float64 = (1.0 / 3.0) * x ^ 3 - 2 * x ^ 2 + 3 * x
33+
proc df(x:float64): float64 = x ^ 2 - 4 * x + 3
34+
let x = 0.5
35+
let correct = 0.0
36+
let value = newtons(f, df, x, 0.000001, 1000)
37+
check isClose(value, correct, tol=1e-5)
38+
39+
test "Newtons 1 dimension func default args":
40+
proc f(x:float64): float64 = (1.0 / 3.0) * x ^ 3 - 2 * x ^ 2 + 3 * x
41+
proc df(x:float64): float64 = x ^ 2 - 4 * x + 3
42+
let x = 0.5
43+
let correct = 0.0
44+
let value = newtons(f, df, x)
45+
check isClose(value, correct, tol=1e-5)
46+
47+
test "Newtons unable to find a root":
48+
proc bad_f(x:float64): float64 = pow(E, x) + 1
49+
proc bad_df(x:float64): float64 = pow(E, x)
50+
expect(ArithmeticError):
51+
discard newtons(bad_f, bad_df, 0, 0.000001, 1000)
52+
53+

0 commit comments

Comments
 (0)