Added Vectors and Matrics operations, slicing, docs.
This commit is contained in:
parent
a72991d1b7
commit
83d8c8b71f
@ -1,7 +0,0 @@
|
|||||||
# Bytecode Migration Plan (Archived)
|
|
||||||
|
|
||||||
Status: completed.
|
|
||||||
|
|
||||||
Historical reference:
|
|
||||||
- `notes/archive/bytecode_migration_plan.md` (full plan)
|
|
||||||
- `notes/archive/bytecode_migration_plan_completed.md` (summary)
|
|
||||||
@ -4,6 +4,13 @@ It's an interface if the [Collection] that provides indexing access, like `array
|
|||||||
Array therefore implements [Iterable] too. Well known implementations of `Array` are
|
Array therefore implements [Iterable] too. Well known implementations of `Array` are
|
||||||
[List] and [ImmutableList].
|
[List] and [ImmutableList].
|
||||||
|
|
||||||
|
The language-level bracket syntax supports one or more selectors:
|
||||||
|
|
||||||
|
- `value[i]`
|
||||||
|
- `value[i, j]`
|
||||||
|
|
||||||
|
Concrete array-like types decide what selectors they accept. Built-in list-like arrays use one selector at a time; custom types such as matrices may interpret multiple selectors.
|
||||||
|
|
||||||
Array adds the following methods:
|
Array adds the following methods:
|
||||||
|
|
||||||
## Binary search
|
## Binary search
|
||||||
|
|||||||
@ -30,6 +30,13 @@ There is a shortcut for the last:
|
|||||||
|
|
||||||
__Important__ negative indexes works wherever indexes are used, e.g. in insertion and removal methods too.
|
__Important__ negative indexes works wherever indexes are used, e.g. in insertion and removal methods too.
|
||||||
|
|
||||||
|
The language also allows multi-selector indexing syntax such as `value[i, j]`, but `List` itself uses a single selector only:
|
||||||
|
|
||||||
|
- `list[index]` for one element
|
||||||
|
- `list[range]` for a slice copy
|
||||||
|
|
||||||
|
Multi-selector indexing is intended for custom indexers such as `Matrix`.
|
||||||
|
|
||||||
## Concatenation
|
## Concatenation
|
||||||
|
|
||||||
You can concatenate lists or iterable objects:
|
You can concatenate lists or iterable objects:
|
||||||
|
|||||||
192
docs/Matrix.md
Normal file
192
docs/Matrix.md
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
# Matrix (`lyng.matrix`)
|
||||||
|
|
||||||
|
`lyng.matrix` adds dense immutable `Matrix` and `Vector` types for linear algebra.
|
||||||
|
|
||||||
|
Import it when you need matrix or vector arithmetic:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.matrix
|
||||||
|
```
|
||||||
|
|
||||||
|
## Construction
|
||||||
|
|
||||||
|
Create vectors from a flat list and matrices from nested row lists:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.matrix
|
||||||
|
|
||||||
|
val v: Vector = vector([1, 2, 3])
|
||||||
|
val m: Matrix = matrix([[1, 2, 3], [4, 5, 6]])
|
||||||
|
|
||||||
|
assertEquals([1.0, 2.0, 3.0], v.toList())
|
||||||
|
assertEquals([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], m.toList())
|
||||||
|
```
|
||||||
|
|
||||||
|
Factory methods are also available:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.matrix
|
||||||
|
|
||||||
|
val z: Vector = Vector.zeros(3)
|
||||||
|
val i: Matrix = Matrix.identity(3)
|
||||||
|
val m: Matrix = Matrix.zeros(2, 4)
|
||||||
|
```
|
||||||
|
|
||||||
|
All elements are standard double-precision numeric values internally.
|
||||||
|
|
||||||
|
## Shapes
|
||||||
|
|
||||||
|
Matrices may have any rectangular geometry:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.matrix
|
||||||
|
|
||||||
|
val a: Matrix = matrix([[1, 2, 3], [4, 5, 6]])
|
||||||
|
|
||||||
|
assertEquals(2, a.rows)
|
||||||
|
assertEquals(3, a.cols)
|
||||||
|
assertEquals([2, 3], a.shape)
|
||||||
|
assertEquals(false, a.isSquare)
|
||||||
|
```
|
||||||
|
|
||||||
|
Vectors expose:
|
||||||
|
|
||||||
|
- `size`
|
||||||
|
- `length` as an alias of `size`
|
||||||
|
|
||||||
|
## Matrix Operations
|
||||||
|
|
||||||
|
Supported matrix operations:
|
||||||
|
|
||||||
|
- `+` and `-` for element-wise matrix arithmetic
|
||||||
|
- `*` for matrix-matrix product
|
||||||
|
- `*` and `/` by a scalar
|
||||||
|
- `transpose()`
|
||||||
|
- `trace()`
|
||||||
|
- `rank()`
|
||||||
|
- `determinant()`
|
||||||
|
- `inverse()`
|
||||||
|
- `solve(rhs)` for `Vector` or `Matrix` right-hand sides
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.matrix
|
||||||
|
|
||||||
|
val a: Matrix = matrix([[1, 2, 3], [4, 5, 6]])
|
||||||
|
val b: Matrix = matrix([[7, 8], [9, 10], [11, 12]])
|
||||||
|
val product: Matrix = a * b
|
||||||
|
assertEquals([[58.0, 64.0], [139.0, 154.0]], product.toList())
|
||||||
|
assertEquals([[1.0, 4.0], [2.0, 5.0], [3.0, 6.0]], a.transpose().toList())
|
||||||
|
```
|
||||||
|
|
||||||
|
Inverse and solve:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.matrix
|
||||||
|
|
||||||
|
val a: Matrix = matrix([[4, 7], [2, 6]])
|
||||||
|
val rhs: Vector = vector([1, 0])
|
||||||
|
|
||||||
|
val inv: Matrix = a.inverse()
|
||||||
|
val x: Vector = a.solve(rhs)
|
||||||
|
|
||||||
|
assert(abs(a.determinant() - 10.0) < 1e-9)
|
||||||
|
assert(abs(inv.get(0, 0) - 0.6) < 1e-9)
|
||||||
|
assert(abs(x.get(0) - 0.6) < 1e-9)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Vector Operations
|
||||||
|
|
||||||
|
Supported vector operations:
|
||||||
|
|
||||||
|
- `+` and `-`
|
||||||
|
- scalar `*` and `/`
|
||||||
|
- `dot(other)`
|
||||||
|
- `norm()`
|
||||||
|
- `normalize()`
|
||||||
|
- `cross(other)` for 3D vectors
|
||||||
|
- `outer(other)` producing a matrix
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.matrix
|
||||||
|
|
||||||
|
val a: Vector = vector([1, 2, 3])
|
||||||
|
val b: Vector = vector([2, 0, 0])
|
||||||
|
|
||||||
|
assertEquals(2.0, a.dot(b))
|
||||||
|
assertEquals([0.2672612419124244, 0.5345224838248488, 0.8017837257372732], a.normalize().toList())
|
||||||
|
```
|
||||||
|
|
||||||
|
## Indexing and Slicing
|
||||||
|
|
||||||
|
`Matrix` supports both method-style indexing and bracket syntax.
|
||||||
|
|
||||||
|
Scalar access:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.matrix
|
||||||
|
|
||||||
|
val m: Matrix = matrix([[1, 2, 3], [4, 5, 6]])
|
||||||
|
|
||||||
|
assertEquals(6.0, m.get(1, 2))
|
||||||
|
assertEquals(6.0, m[1, 2])
|
||||||
|
```
|
||||||
|
|
||||||
|
Bracket indexing accepts two selectors: `[row, col]`.
|
||||||
|
Each selector may be either:
|
||||||
|
|
||||||
|
- an `Int`
|
||||||
|
- a `Range`
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.matrix
|
||||||
|
|
||||||
|
val m: Matrix = matrix([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]])
|
||||||
|
|
||||||
|
assertEquals(7.0, m[1, 2])
|
||||||
|
val columnSlice: Matrix = m[0..2, 2]
|
||||||
|
val topLeft: Matrix = m[0..1, 0..1]
|
||||||
|
val tail: Matrix = m[1.., 1..]
|
||||||
|
assertEquals([[3.0], [7.0], [11.0]], columnSlice.toList())
|
||||||
|
assertEquals([[1.0, 2.0], [5.0, 6.0]], topLeft.toList())
|
||||||
|
assertEquals([[6.0, 7.0, 8.0], [10.0, 11.0, 12.0]], tail.toList())
|
||||||
|
```
|
||||||
|
|
||||||
|
Shape rules:
|
||||||
|
|
||||||
|
- `m[Int, Int]` returns a `Real`
|
||||||
|
- `m[Range, Int]` returns an `Nx1` `Matrix`
|
||||||
|
- `m[Int, Range]` returns a `1xM` `Matrix`
|
||||||
|
- `m[Range, Range]` returns a submatrix
|
||||||
|
|
||||||
|
Open-ended ranges are supported:
|
||||||
|
|
||||||
|
- `m[..1, ..1]`
|
||||||
|
- `m[1.., 1..]`
|
||||||
|
- `m[.., 2]`
|
||||||
|
|
||||||
|
Stepped ranges are not supported in matrix slicing.
|
||||||
|
|
||||||
|
Slices currently return new matrices, not views.
|
||||||
|
|
||||||
|
## Rows and Columns
|
||||||
|
|
||||||
|
If you want plain lists instead of a sliced matrix:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.matrix
|
||||||
|
|
||||||
|
val a: Matrix = matrix([[1, 2, 3], [4, 5, 6]])
|
||||||
|
|
||||||
|
assertEquals([4.0, 5.0, 6.0], a.row(1))
|
||||||
|
assertEquals([2.0, 5.0], a.column(1))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Backend Notes
|
||||||
|
|
||||||
|
The matrix module uses a platform-specific backend where available and falls back to pure Kotlin where needed.
|
||||||
|
|
||||||
|
The public Lyng API stays the same across platforms.
|
||||||
20
docs/OOP.md
20
docs/OOP.md
@ -1183,8 +1183,24 @@ collection's sugar won't work with it:
|
|||||||
assertEquals("buzz", x[0])
|
assertEquals("buzz", x[0])
|
||||||
>>> void
|
>>> void
|
||||||
|
|
||||||
If you want dynamic to function like an array, create a [feature
|
Multiple selectors are packed into one list index object:
|
||||||
request](https://gitea.sergeych.net/SergeychWorks/lyng/issues).
|
|
||||||
|
val x = dynamic {
|
||||||
|
get {
|
||||||
|
if( it == [1, 2] ) "hit"
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertEquals("hit", x[1, 2])
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
So:
|
||||||
|
|
||||||
|
- `x[i]` passes `i`
|
||||||
|
- `x[i, j]` passes `[i, j]`
|
||||||
|
- `x[i, j, k]` passes `[i, j, k]`
|
||||||
|
|
||||||
|
This is the same rule used by Kotlin-backed `getAt` / `putAt` indexers in embedding.
|
||||||
|
|
||||||
# Theory
|
# Theory
|
||||||
|
|
||||||
|
|||||||
@ -82,11 +82,16 @@ Primary sources used: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/{Parser,T
|
|||||||
- Type/containment: `is`, `!is`, `in`, `!in`, `as`, `as?`.
|
- Type/containment: `is`, `!is`, `in`, `!in`, `as`, `as?`.
|
||||||
- Null-safe family:
|
- Null-safe family:
|
||||||
- member access: `?.`
|
- member access: `?.`
|
||||||
- safe index: `?[i]`
|
- safe index: `?[i]`, `?[i, j]`
|
||||||
- safe invoke: `?(...)`
|
- safe invoke: `?(...)`
|
||||||
- safe block invoke: `?{ ... }`
|
- safe block invoke: `?{ ... }`
|
||||||
- elvis: `?:` and `??`.
|
- elvis: `?:` and `??`.
|
||||||
- Increment/decrement: prefix and postfix `++`, `--`.
|
- Increment/decrement: prefix and postfix `++`, `--`.
|
||||||
|
- Indexing syntax:
|
||||||
|
- single selector: `a[i]`
|
||||||
|
- multiple selectors: `a[i, j, k]`
|
||||||
|
- language-level indexing with multiple selectors is passed to `getAt`/`putAt` as one list-like index object, not as multiple method arguments.
|
||||||
|
- example: `m[0..2, 2]`.
|
||||||
|
|
||||||
## 5. Declarations
|
## 5. Declarations
|
||||||
- Variables:
|
- Variables:
|
||||||
|
|||||||
@ -58,6 +58,8 @@ Sources: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt`, `lynglib/s
|
|||||||
- `Observable`, `Subscription`, `ObservableList`, `ListChange` and change subtypes, `ChangeRejectionException`.
|
- `Observable`, `Subscription`, `ObservableList`, `ListChange` and change subtypes, `ChangeRejectionException`.
|
||||||
- `import lyng.complex`
|
- `import lyng.complex`
|
||||||
- `Complex`, `complex(re, im)`, `cis(angle)`, and numeric embedding extensions such as `2.i` / `3.re`.
|
- `Complex`, `complex(re, im)`, `cis(angle)`, and numeric embedding extensions such as `2.i` / `3.re`.
|
||||||
|
- `import lyng.matrix`
|
||||||
|
- `Matrix`, `Vector`, `matrix(rows)`, `vector(values)`, dense linear algebra, inversion, solving, and matrix slicing with `m[row, col]`.
|
||||||
- `import lyng.buffer`
|
- `import lyng.buffer`
|
||||||
- `Buffer`, `MutableBuffer`.
|
- `Buffer`, `MutableBuffer`.
|
||||||
- `import lyng.serialization`
|
- `import lyng.serialization`
|
||||||
|
|||||||
@ -183,6 +183,72 @@ Scope-backed Kotlin lambdas receive a `ScopeFacade` (not a full `Scope`). For mi
|
|||||||
|
|
||||||
If you truly need the full `Scope` (e.g., for low-level interop), use `requireScope()` explicitly.
|
If you truly need the full `Scope` (e.g., for low-level interop), use `requireScope()` explicitly.
|
||||||
|
|
||||||
|
### 4.5) Indexers from Kotlin: `getAt` and `putAt`
|
||||||
|
|
||||||
|
Lyng bracket syntax is dispatched through `getAt` and `putAt`.
|
||||||
|
|
||||||
|
That means:
|
||||||
|
|
||||||
|
- `x[i]` calls `getAt(index)`
|
||||||
|
- `x[i] = value` calls `putAt(index, value)` or `setAt(index, value)`
|
||||||
|
- field-like `x["name"]` also uses the same index path unless you expose a real field/property
|
||||||
|
|
||||||
|
For Kotlin-backed classes, bind indexers as ordinary methods named `getAt` and `putAt`:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
moduleScope.eval("""
|
||||||
|
extern class Grid {
|
||||||
|
override fun getAt(index: List<Int>): Int
|
||||||
|
override fun putAt(index: List<Int>, value: Int): void
|
||||||
|
}
|
||||||
|
""".trimIndent())
|
||||||
|
|
||||||
|
moduleScope.bind("Grid") {
|
||||||
|
init { _ -> data = IntArray(4) }
|
||||||
|
|
||||||
|
addFun("getAt") {
|
||||||
|
val index = args.requiredArg<ObjList>(0)
|
||||||
|
val row = (index.list[0] as ObjInt).value.toInt()
|
||||||
|
val col = (index.list[1] as ObjInt).value.toInt()
|
||||||
|
val data = (thisObj as ObjInstance).data as IntArray
|
||||||
|
ObjInt.of(data[row * 2 + col].toLong())
|
||||||
|
}
|
||||||
|
|
||||||
|
addFun("putAt") {
|
||||||
|
val index = args.requiredArg<ObjList>(0)
|
||||||
|
val value = args.requiredArg<ObjInt>(1).value.toInt()
|
||||||
|
val row = (index.list[0] as ObjInt).value.toInt()
|
||||||
|
val col = (index.list[1] as ObjInt).value.toInt()
|
||||||
|
val data = (thisObj as ObjInstance).data as IntArray
|
||||||
|
data[row * 2 + col] = value
|
||||||
|
ObjVoid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Usage from Lyng:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
val g = Grid()
|
||||||
|
g[0, 1] = 42
|
||||||
|
assertEquals(42, g[0, 1])
|
||||||
|
```
|
||||||
|
|
||||||
|
Important rule: multiple selectors inside brackets are packed into one index object.
|
||||||
|
So:
|
||||||
|
|
||||||
|
- `x[i]` passes `i`
|
||||||
|
- `x[i, j]` passes a `List` containing `[i, j]`
|
||||||
|
- `x[i, j, k]` passes `[i, j, k]`
|
||||||
|
|
||||||
|
This applies equally to:
|
||||||
|
|
||||||
|
- Kotlin-backed classes
|
||||||
|
- Lyng classes overriding `getAt`
|
||||||
|
- `dynamic { get { ... } set { ... } }`
|
||||||
|
|
||||||
|
If you want multi-axis slicing semantics, decode that list yourself in `getAt`.
|
||||||
|
|
||||||
### 5) Add Kotlin‑backed fields
|
### 5) Add Kotlin‑backed fields
|
||||||
|
|
||||||
If you need a simple field (with a value) instead of a computed property, use `createField`. This adds a field to the class that will be present in all its instances.
|
If you need a simple field (with a value) instead of a computed property, use `createField`. This adds a field to the class that will be present in all its instances.
|
||||||
|
|||||||
50
docs/math.md
50
docs/math.md
@ -110,6 +110,56 @@ For example:
|
|||||||
assert( 5.clamp(0..10) == 5 )
|
assert( 5.clamp(0..10) == 5 )
|
||||||
>>> void
|
>>> void
|
||||||
|
|
||||||
|
## Linear algebra: `lyng.matrix`
|
||||||
|
|
||||||
|
For vectors and dense matrices, import `lyng.matrix`:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.matrix
|
||||||
|
```
|
||||||
|
|
||||||
|
It provides:
|
||||||
|
|
||||||
|
- `Vector`
|
||||||
|
- `Matrix`
|
||||||
|
- `vector(values)`
|
||||||
|
- `matrix(rows)`
|
||||||
|
|
||||||
|
Core operations include:
|
||||||
|
|
||||||
|
- matrix addition and subtraction
|
||||||
|
- matrix-matrix multiplication
|
||||||
|
- matrix-vector multiplication
|
||||||
|
- transpose
|
||||||
|
- determinant
|
||||||
|
- inverse
|
||||||
|
- linear solve
|
||||||
|
- vector dot, norm, normalize, cross, outer product
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.matrix
|
||||||
|
|
||||||
|
val a: Matrix = matrix([[1, 2, 3], [4, 5, 6]])
|
||||||
|
val b: Matrix = matrix([[7, 8], [9, 10], [11, 12]])
|
||||||
|
val product: Matrix = a * b
|
||||||
|
assertEquals([[58.0, 64.0], [139.0, 154.0]], product.toList())
|
||||||
|
```
|
||||||
|
|
||||||
|
Matrices also support two-axis bracket indexing and slicing:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.matrix
|
||||||
|
|
||||||
|
val m: Matrix = matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
|
||||||
|
assertEquals(6.0, m[1, 2])
|
||||||
|
val sub: Matrix = m[0..1, 1..2]
|
||||||
|
assertEquals([[2.0, 3.0], [5.0, 6.0]], sub.toList())
|
||||||
|
```
|
||||||
|
|
||||||
|
See [Matrix](Matrix.md) for the full API.
|
||||||
|
|
||||||
## Random values
|
## Random values
|
||||||
|
|
||||||
Lyng stdlib provides a global random singleton and deterministic seeded generators:
|
Lyng stdlib provides a global random singleton and deterministic seeded generators:
|
||||||
|
|||||||
@ -375,6 +375,18 @@ It is rather simple, like everywhere else:
|
|||||||
|
|
||||||
See [math](math.md) for more on it. Notice using Greek as identifier, all languages are allowed.
|
See [math](math.md) for more on it. Notice using Greek as identifier, all languages are allowed.
|
||||||
|
|
||||||
|
For linear algebra, import `lyng.matrix`:
|
||||||
|
|
||||||
|
import lyng.matrix
|
||||||
|
|
||||||
|
val a: Matrix = matrix([[1, 2], [3, 4]])
|
||||||
|
val i: Matrix = Matrix.identity(2)
|
||||||
|
val sum: Matrix = a + i
|
||||||
|
assertEquals([[2.0, 2.0], [3.0, 5.0]], sum.toList())
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
See [Matrix](Matrix.md) for vectors, matrix multiplication, inversion, and slicing such as `m[0..2, 1]`.
|
||||||
|
|
||||||
Logical operation could be used the same
|
Logical operation could be used the same
|
||||||
|
|
||||||
var x = 10
|
var x = 10
|
||||||
@ -811,6 +823,14 @@ Lists can contain any type of objects, lists too:
|
|||||||
|
|
||||||
Notice usage of indexing. You can use negative indexes to offset from the end of the list; see more in [Lists](List.md).
|
Notice usage of indexing. You can use negative indexes to offset from the end of the list; see more in [Lists](List.md).
|
||||||
|
|
||||||
|
In general, bracket indexing may contain more than one selector:
|
||||||
|
|
||||||
|
value[i]
|
||||||
|
value[i, j]
|
||||||
|
|
||||||
|
For built-in lists, strings, maps, and buffers, the selector is usually a single value such as an `Int`, `Range`, or `Regex`.
|
||||||
|
For types with custom indexers, multiple selectors are packed into one list-like index object and passed to `getAt` / `putAt`.
|
||||||
|
|
||||||
When you want to "flatten" it to single array, you can use splat syntax:
|
When you want to "flatten" it to single array, you can use splat syntax:
|
||||||
|
|
||||||
[1, ...[2,3], 4]
|
[1, ...[2,3], 4]
|
||||||
@ -1699,6 +1719,14 @@ Open-ended ranges could be used to get start and end too:
|
|||||||
assertEquals( "pult", "catapult"[ 4.. ])
|
assertEquals( "pult", "catapult"[ 4.. ])
|
||||||
>>> void
|
>>> void
|
||||||
|
|
||||||
|
The same bracket syntax is also used by imported numeric modules such as `lyng.matrix`, where indexing can be multi-axis:
|
||||||
|
|
||||||
|
import lyng.matrix
|
||||||
|
|
||||||
|
val m: Matrix = matrix([[1, 2, 3], [4, 5, 6]])
|
||||||
|
assertEquals(6.0, m[1, 2])
|
||||||
|
>>> void
|
||||||
|
|
||||||
### String operations
|
### String operations
|
||||||
|
|
||||||
Concatenation is a `+`: `"hello " + name` works as expected. No confusion. There is also
|
Concatenation is a `+`: `"hello " + name` works as expected. No confusion. There is also
|
||||||
|
|||||||
@ -5,6 +5,53 @@ For a programmer-focused migration summary, see `docs/whats_new_1_5.md`.
|
|||||||
|
|
||||||
## Language Features
|
## Language Features
|
||||||
|
|
||||||
|
### Matrix and Vector Module (`lyng.matrix`)
|
||||||
|
Lyng now ships a dense linear algebra module with immutable double-precision `Matrix` and `Vector` types.
|
||||||
|
|
||||||
|
It provides:
|
||||||
|
|
||||||
|
- `matrix([[...]])` and `vector([...])`
|
||||||
|
- matrix multiplication
|
||||||
|
- matrix inversion
|
||||||
|
- determinant, trace, rank
|
||||||
|
- solving `A * x = b`
|
||||||
|
- vector operations such as `dot`, `normalize`, `cross`, and `outer`
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.matrix
|
||||||
|
|
||||||
|
val a: Matrix = matrix([[4, 7], [2, 6]])
|
||||||
|
val inv: Matrix = a.inverse()
|
||||||
|
assert(abs(inv.get(0, 0) - 0.6) < 1e-9)
|
||||||
|
```
|
||||||
|
|
||||||
|
Matrices also support Lyng-style slicing:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.matrix
|
||||||
|
|
||||||
|
val m: Matrix = matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
|
||||||
|
assertEquals(6.0, m[1, 2])
|
||||||
|
val column: Matrix = m[0..2, 2]
|
||||||
|
val tail: Matrix = m[1.., 1..]
|
||||||
|
assertEquals([[3.0], [6.0], [9.0]], column.toList())
|
||||||
|
assertEquals([[5.0, 6.0], [8.0, 9.0]], tail.toList())
|
||||||
|
```
|
||||||
|
|
||||||
|
See [Matrix](Matrix.md).
|
||||||
|
|
||||||
|
### Multiple Selectors in Bracket Indexing
|
||||||
|
Bracket indexing now accepts more than one selector:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
value[i]
|
||||||
|
value[i, j]
|
||||||
|
value[i, j, k]
|
||||||
|
```
|
||||||
|
|
||||||
|
For custom indexers, multiple selectors are packed into one list-like index object and dispatched through `getAt` / `putAt`.
|
||||||
|
This is the rule used by `lyng.matrix` and by embedding APIs for Kotlin-backed indexers.
|
||||||
|
|
||||||
### Decimal Arithmetic Module (`lyng.decimal`)
|
### Decimal Arithmetic Module (`lyng.decimal`)
|
||||||
Lyng now ships a first-class decimal module built as a regular extension library rather than a deep core special case.
|
Lyng now ships a first-class decimal module built as a regular extension library rather than a deep core special case.
|
||||||
|
|
||||||
|
|||||||
@ -2,13 +2,14 @@
|
|||||||
agp = "8.5.2"
|
agp = "8.5.2"
|
||||||
clikt = "5.0.3"
|
clikt = "5.0.3"
|
||||||
mordant = "3.0.2"
|
mordant = "3.0.2"
|
||||||
kotlin = "2.3.0"
|
kotlin = "2.3.20"
|
||||||
android-minSdk = "24"
|
android-minSdk = "24"
|
||||||
android-compileSdk = "34"
|
android-compileSdk = "34"
|
||||||
kotlinx-coroutines = "1.10.2"
|
kotlinx-coroutines = "1.10.2"
|
||||||
kotlinx-datetime = "0.6.1"
|
kotlinx-datetime = "0.6.1"
|
||||||
mp_bintools = "0.3.2"
|
mp_bintools = "0.3.2"
|
||||||
ionspin-bignum = "0.3.10"
|
ionspin-bignum = "0.3.10"
|
||||||
|
multik = "0.3.0"
|
||||||
firebaseCrashlyticsBuildtools = "3.0.3"
|
firebaseCrashlyticsBuildtools = "3.0.3"
|
||||||
okioVersion = "3.10.2"
|
okioVersion = "3.10.2"
|
||||||
compiler = "3.2.0-alpha11"
|
compiler = "3.2.0-alpha11"
|
||||||
@ -24,6 +25,7 @@ kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-c
|
|||||||
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
|
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
|
||||||
mp_bintools = { module = "net.sergeych:mp_bintools", version.ref = "mp_bintools" }
|
mp_bintools = { module = "net.sergeych:mp_bintools", version.ref = "mp_bintools" }
|
||||||
ionspin-bignum = { module = "com.ionspin.kotlin:bignum", version.ref = "ionspin-bignum" }
|
ionspin-bignum = { module = "com.ionspin.kotlin:bignum", version.ref = "ionspin-bignum" }
|
||||||
|
multik-default = { module = "org.jetbrains.kotlinx:multik-default", version.ref = "multik" }
|
||||||
firebase-crashlytics-buildtools = { group = "com.google.firebase", name = "firebase-crashlytics-buildtools", version.ref = "firebaseCrashlyticsBuildtools" }
|
firebase-crashlytics-buildtools = { group = "com.google.firebase", name = "firebase-crashlytics-buildtools", version.ref = "firebaseCrashlyticsBuildtools" }
|
||||||
okio = { module = "com.squareup.okio:okio", version.ref = "okioVersion" }
|
okio = { module = "com.squareup.okio:okio", version.ref = "okioVersion" }
|
||||||
okio-fakefilesystem = { module = "com.squareup.okio:okio-fakefilesystem", version.ref = "okioVersion" }
|
okio-fakefilesystem = { module = "com.squareup.okio:okio-fakefilesystem", version.ref = "okioVersion" }
|
||||||
|
|||||||
@ -23,6 +23,8 @@ import com.intellij.execution.ui.ConsoleViewContentType
|
|||||||
import com.intellij.openapi.actionSystem.AnAction
|
import com.intellij.openapi.actionSystem.AnAction
|
||||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||||
import com.intellij.openapi.actionSystem.CommonDataKeys
|
import com.intellij.openapi.actionSystem.CommonDataKeys
|
||||||
|
import com.intellij.openapi.application.ApplicationManager
|
||||||
|
import com.intellij.openapi.fileEditor.FileDocumentManager
|
||||||
import com.intellij.openapi.project.Project
|
import com.intellij.openapi.project.Project
|
||||||
import com.intellij.openapi.wm.ToolWindow
|
import com.intellij.openapi.wm.ToolWindow
|
||||||
import com.intellij.openapi.wm.ToolWindowAnchor
|
import com.intellij.openapi.wm.ToolWindowAnchor
|
||||||
@ -36,9 +38,10 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import net.sergeych.lyng.idea.LyngIcons
|
import net.sergeych.lyng.idea.LyngIcons
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
class RunLyngScriptAction : AnAction(LyngIcons.FILE) {
|
class RunLyngScriptAction : AnAction(LyngIcons.FILE) {
|
||||||
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
|
|
||||||
private fun getPsiFile(e: AnActionEvent): PsiFile? {
|
private fun getPsiFile(e: AnActionEvent): PsiFile? {
|
||||||
val project = e.project ?: return null
|
val project = e.project ?: return null
|
||||||
@ -48,36 +51,99 @@ class RunLyngScriptAction : AnAction(LyngIcons.FILE) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getRunnableFile(e: AnActionEvent): PsiFile? {
|
||||||
|
val psiFile = getPsiFile(e) ?: return null
|
||||||
|
val virtualFile = psiFile.virtualFile ?: return null
|
||||||
|
if (!virtualFile.isInLocalFileSystem) return null
|
||||||
|
if (!psiFile.name.endsWith(".lyng")) return null
|
||||||
|
return psiFile
|
||||||
|
}
|
||||||
|
|
||||||
override fun update(e: AnActionEvent) {
|
override fun update(e: AnActionEvent) {
|
||||||
val psiFile = getPsiFile(e)
|
val psiFile = getRunnableFile(e)
|
||||||
val isLyng = psiFile?.name?.endsWith(".lyng") == true
|
val isRunnable = psiFile != null
|
||||||
e.presentation.isEnabledAndVisible = isLyng
|
e.presentation.isEnabledAndVisible = isRunnable
|
||||||
if (isLyng) {
|
if (isRunnable) {
|
||||||
e.presentation.isEnabled = false
|
e.presentation.text = "Run '${psiFile.name}'"
|
||||||
e.presentation.text = "Run '${psiFile.name}' (disabled)"
|
e.presentation.description = "Run the current Lyng script using the Lyng CLI"
|
||||||
e.presentation.description = "Running scripts from the IDE is disabled; use the CLI."
|
|
||||||
} else {
|
} else {
|
||||||
e.presentation.text = "Run Lyng Script"
|
e.presentation.text = "Run Lyng Script"
|
||||||
|
e.presentation.description = "Run the current Lyng script"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun actionPerformed(e: AnActionEvent) {
|
override fun actionPerformed(e: AnActionEvent) {
|
||||||
val project = e.project ?: return
|
val project = e.project ?: return
|
||||||
val psiFile = getPsiFile(e) ?: return
|
val psiFile = getRunnableFile(e) ?: return
|
||||||
val fileName = psiFile.name
|
val virtualFile = psiFile.virtualFile ?: return
|
||||||
|
FileDocumentManager.getInstance().getDocument(virtualFile)?.let { document ->
|
||||||
|
FileDocumentManager.getInstance().saveDocument(document)
|
||||||
|
}
|
||||||
|
val filePath = virtualFile.path
|
||||||
|
val workingDir = virtualFile.parent?.path ?: project.basePath ?: File(filePath).parent
|
||||||
|
|
||||||
val (console, toolWindow) = getConsoleAndToolWindow(project)
|
val (console, toolWindow) = getConsoleAndToolWindow(project)
|
||||||
console.clear()
|
console.clear()
|
||||||
|
|
||||||
toolWindow.show {
|
toolWindow.show {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
console.print("--- Run is disabled ---\n", ConsoleViewContentType.SYSTEM_OUTPUT)
|
val command = startLyngProcess(filePath, workingDir)
|
||||||
console.print("Lyng now runs in bytecode-only mode; the IDE no longer evaluates scripts.\n", ConsoleViewContentType.NORMAL_OUTPUT)
|
if (command == null) {
|
||||||
console.print("Use the CLI to run scripts, e.g. `lyng run $fileName`.\n", ConsoleViewContentType.NORMAL_OUTPUT)
|
printToConsole(console, "Unable to start Lyng CLI.\n", ConsoleViewContentType.ERROR_OUTPUT)
|
||||||
|
printToConsole(console, "Tried commands: lyng, jlyng.\n", ConsoleViewContentType.ERROR_OUTPUT)
|
||||||
|
printToConsole(console, "Install `lyng` or `jlyng` and make sure it is available on PATH.\n", ConsoleViewContentType.NORMAL_OUTPUT)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
printToConsole(
|
||||||
|
console,
|
||||||
|
"Running ${command.commandLine} in ${command.workingDir}\n",
|
||||||
|
ConsoleViewContentType.SYSTEM_OUTPUT
|
||||||
|
)
|
||||||
|
streamProcess(command.process, console)
|
||||||
|
val exitCode = command.process.waitFor()
|
||||||
|
val outputType = if (exitCode == 0) ConsoleViewContentType.SYSTEM_OUTPUT else ConsoleViewContentType.ERROR_OUTPUT
|
||||||
|
printToConsole(console, "\nProcess finished with exit code $exitCode\n", outputType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun streamProcess(process: Process, console: ConsoleView) {
|
||||||
|
val stdout = scope.launch {
|
||||||
|
process.inputStream.bufferedReader().useLines { lines ->
|
||||||
|
lines.forEach { printToConsole(console, "$it\n", ConsoleViewContentType.NORMAL_OUTPUT) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val stderr = scope.launch {
|
||||||
|
process.errorStream.bufferedReader().useLines { lines ->
|
||||||
|
lines.forEach { printToConsole(console, "$it\n", ConsoleViewContentType.ERROR_OUTPUT) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stdout.join()
|
||||||
|
stderr.join()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun printToConsole(console: ConsoleView, text: String, type: ConsoleViewContentType) {
|
||||||
|
ApplicationManager.getApplication().invokeLater {
|
||||||
|
console.print(text, type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startLyngProcess(filePath: String, workingDir: String?): StartedProcess? {
|
||||||
|
val candidates = listOf("lyng", "jlyng")
|
||||||
|
for (candidate in candidates) {
|
||||||
|
try {
|
||||||
|
val process = ProcessBuilder(candidate, filePath)
|
||||||
|
.directory(workingDir?.let(::File))
|
||||||
|
.start()
|
||||||
|
return StartedProcess(process, "$candidate $filePath", workingDir ?: File(filePath).parent.orEmpty())
|
||||||
|
} catch (_: java.io.IOException) {
|
||||||
|
// Try the next candidate when the command is not available.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
private fun getConsoleAndToolWindow(project: Project): Pair<ConsoleView, ToolWindow> {
|
private fun getConsoleAndToolWindow(project: Project): Pair<ConsoleView, ToolWindow> {
|
||||||
val toolWindowManager = ToolWindowManager.getInstance(project)
|
val toolWindowManager = ToolWindowManager.getInstance(project)
|
||||||
var toolWindow = toolWindowManager.getToolWindow(ToolWindowId.RUN)
|
var toolWindow = toolWindowManager.getToolWindow(ToolWindowId.RUN)
|
||||||
@ -106,4 +172,10 @@ class RunLyngScriptAction : AnAction(LyngIcons.FILE) {
|
|||||||
contentManager.setSelectedContent(content)
|
contentManager.setSelectedContent(content)
|
||||||
return console to actualToolWindow
|
return console to actualToolWindow
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private data class StartedProcess(
|
||||||
|
val process: Process,
|
||||||
|
val commandLine: String,
|
||||||
|
val workingDir: String
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,7 +29,7 @@ plugins {
|
|||||||
alias(libs.plugins.kotlinMultiplatform)
|
alias(libs.plugins.kotlinMultiplatform)
|
||||||
alias(libs.plugins.androidLibrary)
|
alias(libs.plugins.androidLibrary)
|
||||||
// alias(libs.plugins.vanniktech.mavenPublish)
|
// alias(libs.plugins.vanniktech.mavenPublish)
|
||||||
kotlin("plugin.serialization") version "2.2.21"
|
kotlin("plugin.serialization") version "2.3.20"
|
||||||
id("com.codingfeline.buildkonfig") version "0.17.1"
|
id("com.codingfeline.buildkonfig") version "0.17.1"
|
||||||
`maven-publish`
|
`maven-publish`
|
||||||
}
|
}
|
||||||
@ -110,6 +110,27 @@ kotlin {
|
|||||||
implementation(libs.kotlinx.coroutines.test)
|
implementation(libs.kotlinx.coroutines.test)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val matrixMultikMain by creating {
|
||||||
|
dependsOn(commonMain)
|
||||||
|
dependencies {
|
||||||
|
implementation(libs.multik.default)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val matrixPureMain by creating {
|
||||||
|
dependsOn(commonMain)
|
||||||
|
}
|
||||||
|
val jvmMain by getting { dependsOn(matrixMultikMain) }
|
||||||
|
val androidMain by getting { dependsOn(matrixPureMain) }
|
||||||
|
val jsMain by getting { dependsOn(matrixMultikMain) }
|
||||||
|
val wasmJsMain by getting { dependsOn(matrixMultikMain) }
|
||||||
|
val iosX64Main by getting { dependsOn(matrixMultikMain) }
|
||||||
|
val iosArm64Main by getting { dependsOn(matrixMultikMain) }
|
||||||
|
val iosSimulatorArm64Main by getting { dependsOn(matrixMultikMain) }
|
||||||
|
val macosX64Main by getting { dependsOn(matrixMultikMain) }
|
||||||
|
val macosArm64Main by getting { dependsOn(matrixMultikMain) }
|
||||||
|
val mingwX64Main by getting { dependsOn(matrixMultikMain) }
|
||||||
|
val linuxX64Main by getting { dependsOn(matrixMultikMain) }
|
||||||
|
val linuxArm64Main by getting { dependsOn(matrixPureMain) }
|
||||||
val jvmTest by getting {
|
val jvmTest by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
// Allow tests to load external docs like lyng.io.fs via registrar
|
// Allow tests to load external docs like lyng.io.fs via registrar
|
||||||
|
|||||||
@ -2915,9 +2915,9 @@ class Compiler(
|
|||||||
operand?.let { left ->
|
operand?.let { left ->
|
||||||
// array access via ObjRef
|
// array access via ObjRef
|
||||||
val isOptional = t.type == Token.Type.NULL_COALESCE_INDEX
|
val isOptional = t.type == Token.Type.NULL_COALESCE_INDEX
|
||||||
val index = parseStatement() ?: throw ScriptError(t.pos, "Expecting index expression")
|
val index = parseIndexExpression() ?: throw ScriptError(t.pos, "Expecting index expression")
|
||||||
cc.skipTokenOfType(Token.Type.RBRACKET, "missing ']' at the end of the list literal")
|
cc.skipTokenOfType(Token.Type.RBRACKET, "missing ']' at the end of the list literal")
|
||||||
operand = IndexRef(left, StatementRef(index), isOptional)
|
operand = IndexRef(left, index, isOptional)
|
||||||
} ?: run {
|
} ?: run {
|
||||||
// array literal
|
// array literal
|
||||||
val entries = parseArrayLiteral()
|
val entries = parseArrayLiteral()
|
||||||
@ -3412,6 +3412,20 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun parseIndexExpression(): ObjRef? {
|
||||||
|
val first = parseExpressionLevel() ?: return null
|
||||||
|
if (!cc.skipTokenOfType(Token.Type.COMMA, isOptional = true)) {
|
||||||
|
return first
|
||||||
|
}
|
||||||
|
|
||||||
|
val entries = mutableListOf<ListEntry>(ListEntry.Element(first))
|
||||||
|
do {
|
||||||
|
val next = parseExpressionLevel() ?: throw ScriptError(cc.currentPos(), "Expecting index expression")
|
||||||
|
entries += ListEntry.Element(next)
|
||||||
|
} while (cc.skipTokenOfType(Token.Type.COMMA, isOptional = true))
|
||||||
|
return ListLiteralRef(entries)
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun parseDestructuringPattern(): List<ListEntry> {
|
private suspend fun parseDestructuringPattern(): List<ListEntry> {
|
||||||
// it should be called after Token.Type.LBRACKET is consumed
|
// it should be called after Token.Type.LBRACKET is consumed
|
||||||
val entries = mutableListOf<ListEntry>()
|
val entries = mutableListOf<ListEntry>()
|
||||||
|
|||||||
@ -29,6 +29,7 @@ import net.sergeych.lyng.obj.*
|
|||||||
import net.sergeych.lyng.pacman.ImportManager
|
import net.sergeych.lyng.pacman.ImportManager
|
||||||
import net.sergeych.lyng.stdlib_included.complexLyng
|
import net.sergeych.lyng.stdlib_included.complexLyng
|
||||||
import net.sergeych.lyng.stdlib_included.decimalLyng
|
import net.sergeych.lyng.stdlib_included.decimalLyng
|
||||||
|
import net.sergeych.lyng.stdlib_included.matrixLyng
|
||||||
import net.sergeych.lyng.stdlib_included.observableLyng
|
import net.sergeych.lyng.stdlib_included.observableLyng
|
||||||
import net.sergeych.lyng.stdlib_included.operatorsLyng
|
import net.sergeych.lyng.stdlib_included.operatorsLyng
|
||||||
import net.sergeych.lyng.stdlib_included.rootLyng
|
import net.sergeych.lyng.stdlib_included.rootLyng
|
||||||
@ -839,6 +840,10 @@ class Script(
|
|||||||
module.eval(Source("lyng.decimal", decimalLyng))
|
module.eval(Source("lyng.decimal", decimalLyng))
|
||||||
ObjBigDecimalSupport.bindTo(module)
|
ObjBigDecimalSupport.bindTo(module)
|
||||||
}
|
}
|
||||||
|
addPackage("lyng.matrix") { module ->
|
||||||
|
module.eval(Source("lyng.matrix", matrixLyng))
|
||||||
|
ObjMatrixSupport.bindTo(module)
|
||||||
|
}
|
||||||
addPackage("lyng.complex") { module ->
|
addPackage("lyng.complex") { module ->
|
||||||
module.eval(Source("lyng.complex", complexLyng))
|
module.eval(Source("lyng.complex", complexLyng))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,152 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package net.sergeych.lyng.matrix
|
||||||
|
|
||||||
|
internal data class MatrixData(
|
||||||
|
val rows: Int,
|
||||||
|
val cols: Int,
|
||||||
|
val values: DoubleArray
|
||||||
|
) {
|
||||||
|
init {
|
||||||
|
require(rows > 0) { "matrix must have at least one row" }
|
||||||
|
require(cols > 0) { "matrix must have at least one column" }
|
||||||
|
require(values.size == rows * cols) {
|
||||||
|
"matrix data size ${values.size} does not match shape ${rows}x${cols}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val isSquare: Boolean get() = rows == cols
|
||||||
|
|
||||||
|
fun at(row: Int, col: Int): Double {
|
||||||
|
require(row in 0 until rows) { "row index $row out of bounds for $rows rows" }
|
||||||
|
require(col in 0 until cols) { "column index $col out of bounds for $cols columns" }
|
||||||
|
return values[row * cols + col]
|
||||||
|
}
|
||||||
|
|
||||||
|
fun rowValues(row: Int): DoubleArray {
|
||||||
|
require(row in 0 until rows) { "row index $row out of bounds for $rows rows" }
|
||||||
|
val start = row * cols
|
||||||
|
return values.copyOfRange(start, start + cols)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun columnValues(col: Int): DoubleArray {
|
||||||
|
require(col in 0 until cols) { "column index $col out of bounds for $cols columns" }
|
||||||
|
return DoubleArray(rows) { row -> values[row * cols + col] }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun slice(rowIndices: IntArray, colIndices: IntArray): MatrixData {
|
||||||
|
require(rowIndices.isNotEmpty()) { "matrix slice must include at least one row" }
|
||||||
|
require(colIndices.isNotEmpty()) { "matrix slice must include at least one column" }
|
||||||
|
val out = DoubleArray(rowIndices.size * colIndices.size)
|
||||||
|
var offset = 0
|
||||||
|
for (row in rowIndices) {
|
||||||
|
require(row in 0 until rows) { "row index $row out of bounds for $rows rows" }
|
||||||
|
val rowOffset = row * cols
|
||||||
|
for (col in colIndices) {
|
||||||
|
require(col in 0 until cols) { "column index $col out of bounds for $cols columns" }
|
||||||
|
out[offset++] = values[rowOffset + col]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return MatrixData(rowIndices.size, colIndices.size, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun plus(other: MatrixData): MatrixData {
|
||||||
|
requireSameShape(other)
|
||||||
|
return MatrixData(rows, cols, DoubleArray(values.size) { index -> values[index] + other.values[index] })
|
||||||
|
}
|
||||||
|
|
||||||
|
fun minus(other: MatrixData): MatrixData {
|
||||||
|
requireSameShape(other)
|
||||||
|
return MatrixData(rows, cols, DoubleArray(values.size) { index -> values[index] - other.values[index] })
|
||||||
|
}
|
||||||
|
|
||||||
|
fun scale(factor: Double): MatrixData =
|
||||||
|
MatrixData(rows, cols, DoubleArray(values.size) { index -> values[index] * factor })
|
||||||
|
|
||||||
|
fun divide(divisor: Double): MatrixData {
|
||||||
|
require(divisor != 0.0) { "matrix division by zero" }
|
||||||
|
return MatrixData(rows, cols, DoubleArray(values.size) { index -> values[index] / divisor })
|
||||||
|
}
|
||||||
|
|
||||||
|
fun transpose(): MatrixData {
|
||||||
|
val out = DoubleArray(values.size)
|
||||||
|
for (row in 0 until rows) {
|
||||||
|
for (col in 0 until cols) {
|
||||||
|
out[col * rows + row] = values[row * cols + col]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return MatrixData(cols, rows, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun multiply(other: MatrixData): MatrixData = PlatformMatrixBackend.multiply(this, other)
|
||||||
|
|
||||||
|
fun multiply(other: VectorData): VectorData = PureMatrixAlgorithms.multiply(this, other)
|
||||||
|
|
||||||
|
fun solve(other: VectorData): VectorData = PureMatrixAlgorithms.solve(this, other)
|
||||||
|
|
||||||
|
fun solve(other: MatrixData): MatrixData = PureMatrixAlgorithms.solve(this, other)
|
||||||
|
|
||||||
|
fun determinant(): Double = PureMatrixAlgorithms.determinant(this)
|
||||||
|
|
||||||
|
fun inverse(): MatrixData = PlatformMatrixBackend.inverse(this)
|
||||||
|
|
||||||
|
fun trace(): Double = PureMatrixAlgorithms.trace(this)
|
||||||
|
|
||||||
|
fun rank(): Int = PureMatrixAlgorithms.rank(this)
|
||||||
|
|
||||||
|
fun toNestedLists(): List<List<Double>> =
|
||||||
|
List(rows) { row ->
|
||||||
|
List(cols) { col -> values[row * cols + col] }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun render(): String = buildString {
|
||||||
|
append("Matrix(")
|
||||||
|
append(rows)
|
||||||
|
append("x")
|
||||||
|
append(cols)
|
||||||
|
append(", [")
|
||||||
|
for (row in 0 until rows) {
|
||||||
|
if (row > 0) append(", ")
|
||||||
|
append("[")
|
||||||
|
for (col in 0 until cols) {
|
||||||
|
if (col > 0) append(", ")
|
||||||
|
append(formatMatrixValue(values[row * cols + col]))
|
||||||
|
}
|
||||||
|
append("]")
|
||||||
|
}
|
||||||
|
append("])")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun compareTo(other: MatrixData): Int {
|
||||||
|
val rowCmp = rows.compareTo(other.rows)
|
||||||
|
if (rowCmp != 0) return rowCmp
|
||||||
|
val colCmp = cols.compareTo(other.cols)
|
||||||
|
if (colCmp != 0) return colCmp
|
||||||
|
for (index in values.indices) {
|
||||||
|
val cmp = values[index].compareTo(other.values[index])
|
||||||
|
if (cmp != 0) return cmp
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun requireSameShape(other: MatrixData) {
|
||||||
|
require(rows == other.rows && cols == other.cols) {
|
||||||
|
"matrix shape mismatch: ${rows}x${cols} vs ${other.rows}x${other.cols}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package net.sergeych.lyng.matrix
|
||||||
|
|
||||||
|
import net.sergeych.lyng.obj.ObjReal
|
||||||
|
|
||||||
|
internal fun formatMatrixValue(value: Double): String {
|
||||||
|
if (value.isFinite()) {
|
||||||
|
val asLong = value.toLong()
|
||||||
|
if (asLong.toDouble() == value) {
|
||||||
|
return asLong.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ObjReal.of(value).toString()
|
||||||
|
}
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package net.sergeych.lyng.matrix
|
||||||
|
|
||||||
|
internal expect object PlatformMatrixBackend {
|
||||||
|
fun multiply(left: MatrixData, right: MatrixData): MatrixData
|
||||||
|
fun inverse(matrix: MatrixData): MatrixData
|
||||||
|
}
|
||||||
@ -0,0 +1,286 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package net.sergeych.lyng.matrix
|
||||||
|
|
||||||
|
internal object PureMatrixAlgorithms {
|
||||||
|
private const val singularEpsilon = 1e-12
|
||||||
|
|
||||||
|
fun multiply(left: MatrixData, right: MatrixData): MatrixData {
|
||||||
|
require(left.cols == right.rows) {
|
||||||
|
"matrix multiplication shape mismatch: ${left.rows}x${left.cols} cannot multiply ${right.rows}x${right.cols}"
|
||||||
|
}
|
||||||
|
val out = DoubleArray(left.rows * right.cols)
|
||||||
|
for (row in 0 until left.rows) {
|
||||||
|
val leftRowOffset = row * left.cols
|
||||||
|
val outRowOffset = row * right.cols
|
||||||
|
for (pivot in 0 until left.cols) {
|
||||||
|
val leftValue = left.values[leftRowOffset + pivot]
|
||||||
|
val rightRowOffset = pivot * right.cols
|
||||||
|
for (col in 0 until right.cols) {
|
||||||
|
out[outRowOffset + col] += leftValue * right.values[rightRowOffset + col]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return MatrixData(left.rows, right.cols, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun multiply(left: MatrixData, right: VectorData): VectorData {
|
||||||
|
require(left.cols == right.size) {
|
||||||
|
"matrix-vector multiplication shape mismatch: ${left.rows}x${left.cols} cannot multiply length ${right.size}"
|
||||||
|
}
|
||||||
|
val out = DoubleArray(left.rows)
|
||||||
|
for (row in 0 until left.rows) {
|
||||||
|
var sum = 0.0
|
||||||
|
val rowOffset = row * left.cols
|
||||||
|
for (col in 0 until left.cols) {
|
||||||
|
sum += left.values[rowOffset + col] * right.values[col]
|
||||||
|
}
|
||||||
|
out[row] = sum
|
||||||
|
}
|
||||||
|
return VectorData(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun inverse(matrix: MatrixData): MatrixData {
|
||||||
|
require(matrix.isSquare) { "matrix inverse requires a square matrix, got ${matrix.rows}x${matrix.cols}" }
|
||||||
|
val n = matrix.rows
|
||||||
|
val width = n * 2
|
||||||
|
val augmented = DoubleArray(n * width)
|
||||||
|
|
||||||
|
for (row in 0 until n) {
|
||||||
|
for (col in 0 until n) {
|
||||||
|
augmented[row * width + col] = matrix.values[row * n + col]
|
||||||
|
}
|
||||||
|
augmented[row * width + (n + row)] = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
for (pivotCol in 0 until n) {
|
||||||
|
var pivotRow = pivotCol
|
||||||
|
var pivotAbs = kotlin.math.abs(augmented[pivotRow * width + pivotCol])
|
||||||
|
for (candidate in pivotCol + 1 until n) {
|
||||||
|
val candidateAbs = kotlin.math.abs(augmented[candidate * width + pivotCol])
|
||||||
|
if (candidateAbs > pivotAbs) {
|
||||||
|
pivotAbs = candidateAbs
|
||||||
|
pivotRow = candidate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require(pivotAbs > singularEpsilon) { "matrix is singular and cannot be inverted" }
|
||||||
|
if (pivotRow != pivotCol) {
|
||||||
|
swapRows(augmented, width, pivotRow, pivotCol)
|
||||||
|
}
|
||||||
|
|
||||||
|
val pivotValue = augmented[pivotCol * width + pivotCol]
|
||||||
|
val pivotOffset = pivotCol * width
|
||||||
|
for (col in 0 until width) {
|
||||||
|
augmented[pivotOffset + col] /= pivotValue
|
||||||
|
}
|
||||||
|
|
||||||
|
for (row in 0 until n) {
|
||||||
|
if (row == pivotCol) continue
|
||||||
|
val factor = augmented[row * width + pivotCol]
|
||||||
|
if (factor == 0.0) continue
|
||||||
|
val rowOffset = row * width
|
||||||
|
for (col in 0 until width) {
|
||||||
|
augmented[rowOffset + col] -= factor * augmented[pivotOffset + col]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val inverse = DoubleArray(n * n)
|
||||||
|
for (row in 0 until n) {
|
||||||
|
for (col in 0 until n) {
|
||||||
|
inverse[row * n + col] = augmented[row * width + n + col]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return MatrixData(n, n, inverse)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun determinant(matrix: MatrixData): Double {
|
||||||
|
require(matrix.isSquare) { "matrix determinant requires a square matrix, got ${matrix.rows}x${matrix.cols}" }
|
||||||
|
val n = matrix.rows
|
||||||
|
val work = matrix.values.copyOf()
|
||||||
|
var sign = 1.0
|
||||||
|
var determinant = 1.0
|
||||||
|
|
||||||
|
for (pivotCol in 0 until n) {
|
||||||
|
var pivotRow = pivotCol
|
||||||
|
var pivotAbs = kotlin.math.abs(work[pivotRow * n + pivotCol])
|
||||||
|
for (candidate in pivotCol + 1 until n) {
|
||||||
|
val candidateAbs = kotlin.math.abs(work[candidate * n + pivotCol])
|
||||||
|
if (candidateAbs > pivotAbs) {
|
||||||
|
pivotAbs = candidateAbs
|
||||||
|
pivotRow = candidate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pivotAbs <= singularEpsilon) {
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
if (pivotRow != pivotCol) {
|
||||||
|
swapRows(work, n, pivotRow, pivotCol)
|
||||||
|
sign = -sign
|
||||||
|
}
|
||||||
|
|
||||||
|
val pivotValue = work[pivotCol * n + pivotCol]
|
||||||
|
determinant *= pivotValue
|
||||||
|
|
||||||
|
for (row in pivotCol + 1 until n) {
|
||||||
|
val rowOffset = row * n
|
||||||
|
val factor = work[rowOffset + pivotCol] / pivotValue
|
||||||
|
if (factor == 0.0) continue
|
||||||
|
for (col in pivotCol + 1 until n) {
|
||||||
|
work[rowOffset + col] -= factor * work[pivotCol * n + col]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return determinant * sign
|
||||||
|
}
|
||||||
|
|
||||||
|
fun trace(matrix: MatrixData): Double {
|
||||||
|
require(matrix.isSquare) { "matrix trace requires a square matrix, got ${matrix.rows}x${matrix.cols}" }
|
||||||
|
var sum = 0.0
|
||||||
|
for (index in 0 until matrix.rows) {
|
||||||
|
sum += matrix.values[index * matrix.cols + index]
|
||||||
|
}
|
||||||
|
return sum
|
||||||
|
}
|
||||||
|
|
||||||
|
fun rank(matrix: MatrixData): Int {
|
||||||
|
val work = matrix.values.copyOf()
|
||||||
|
var rank = 0
|
||||||
|
var pivotRow = 0
|
||||||
|
val rows = matrix.rows
|
||||||
|
val cols = matrix.cols
|
||||||
|
|
||||||
|
for (pivotCol in 0 until cols) {
|
||||||
|
var bestRow = -1
|
||||||
|
var bestAbs = singularEpsilon
|
||||||
|
for (candidate in pivotRow until rows) {
|
||||||
|
val absValue = kotlin.math.abs(work[candidate * cols + pivotCol])
|
||||||
|
if (absValue > bestAbs) {
|
||||||
|
bestAbs = absValue
|
||||||
|
bestRow = candidate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bestRow == -1) continue
|
||||||
|
if (bestRow != pivotRow) {
|
||||||
|
swapRows(work, cols, bestRow, pivotRow)
|
||||||
|
}
|
||||||
|
val pivotValue = work[pivotRow * cols + pivotCol]
|
||||||
|
for (row in pivotRow + 1 until rows) {
|
||||||
|
val factor = work[row * cols + pivotCol] / pivotValue
|
||||||
|
if (kotlin.math.abs(factor) <= singularEpsilon) continue
|
||||||
|
val rowOffset = row * cols
|
||||||
|
val pivotOffset = pivotRow * cols
|
||||||
|
for (col in pivotCol until cols) {
|
||||||
|
work[rowOffset + col] -= factor * work[pivotOffset + col]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rank += 1
|
||||||
|
pivotRow += 1
|
||||||
|
if (pivotRow == rows) break
|
||||||
|
}
|
||||||
|
return rank
|
||||||
|
}
|
||||||
|
|
||||||
|
fun solve(matrix: MatrixData, rhs: VectorData): VectorData {
|
||||||
|
require(matrix.isSquare) { "matrix solve requires a square matrix, got ${matrix.rows}x${matrix.cols}" }
|
||||||
|
require(matrix.rows == rhs.size) {
|
||||||
|
"matrix solve shape mismatch: ${matrix.rows}x${matrix.cols} cannot solve length ${rhs.size}"
|
||||||
|
}
|
||||||
|
val solution = solveAugmented(matrix.rows, matrix.cols, 1, matrix.values, rhs.values)
|
||||||
|
return VectorData(solution)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun solve(matrix: MatrixData, rhs: MatrixData): MatrixData {
|
||||||
|
require(matrix.isSquare) { "matrix solve requires a square matrix, got ${matrix.rows}x${matrix.cols}" }
|
||||||
|
require(matrix.rows == rhs.rows) {
|
||||||
|
"matrix solve shape mismatch: ${matrix.rows}x${matrix.cols} cannot solve ${rhs.rows}x${rhs.cols}"
|
||||||
|
}
|
||||||
|
val solution = solveAugmented(matrix.rows, matrix.cols, rhs.cols, matrix.values, rhs.values)
|
||||||
|
return MatrixData(matrix.rows, rhs.cols, solution)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun solveAugmented(rows: Int, cols: Int, rhsCols: Int, matrixValues: DoubleArray, rhsValues: DoubleArray): DoubleArray {
|
||||||
|
val width = cols + rhsCols
|
||||||
|
val augmented = DoubleArray(rows * width)
|
||||||
|
|
||||||
|
for (row in 0 until rows) {
|
||||||
|
val matrixOffset = row * cols
|
||||||
|
val augmentedOffset = row * width
|
||||||
|
for (col in 0 until cols) {
|
||||||
|
augmented[augmentedOffset + col] = matrixValues[matrixOffset + col]
|
||||||
|
}
|
||||||
|
val rhsOffset = row * rhsCols
|
||||||
|
for (col in 0 until rhsCols) {
|
||||||
|
augmented[augmentedOffset + cols + col] = rhsValues[rhsOffset + col]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (pivotCol in 0 until cols) {
|
||||||
|
var pivotRow = pivotCol
|
||||||
|
var pivotAbs = kotlin.math.abs(augmented[pivotRow * width + pivotCol])
|
||||||
|
for (candidate in pivotCol + 1 until rows) {
|
||||||
|
val candidateAbs = kotlin.math.abs(augmented[candidate * width + pivotCol])
|
||||||
|
if (candidateAbs > pivotAbs) {
|
||||||
|
pivotAbs = candidateAbs
|
||||||
|
pivotRow = candidate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require(pivotAbs > singularEpsilon) { "matrix is singular and cannot be solved" }
|
||||||
|
if (pivotRow != pivotCol) {
|
||||||
|
swapRows(augmented, width, pivotRow, pivotCol)
|
||||||
|
}
|
||||||
|
|
||||||
|
val pivotOffset = pivotCol * width
|
||||||
|
val pivotValue = augmented[pivotOffset + pivotCol]
|
||||||
|
for (col in pivotCol until width) {
|
||||||
|
augmented[pivotOffset + col] /= pivotValue
|
||||||
|
}
|
||||||
|
|
||||||
|
for (row in 0 until rows) {
|
||||||
|
if (row == pivotCol) continue
|
||||||
|
val factor = augmented[row * width + pivotCol]
|
||||||
|
if (kotlin.math.abs(factor) <= singularEpsilon) continue
|
||||||
|
val rowOffset = row * width
|
||||||
|
for (col in pivotCol until width) {
|
||||||
|
augmented[rowOffset + col] -= factor * augmented[pivotOffset + col]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val solution = DoubleArray(rows * rhsCols)
|
||||||
|
for (row in 0 until rows) {
|
||||||
|
val augmentedOffset = row * width + cols
|
||||||
|
val solutionOffset = row * rhsCols
|
||||||
|
for (col in 0 until rhsCols) {
|
||||||
|
solution[solutionOffset + col] = augmented[augmentedOffset + col]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return solution
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun swapRows(data: DoubleArray, stride: Int, firstRow: Int, secondRow: Int) {
|
||||||
|
val firstOffset = firstRow * stride
|
||||||
|
val secondOffset = secondRow * stride
|
||||||
|
for (col in 0 until stride) {
|
||||||
|
val tmp = data[firstOffset + col]
|
||||||
|
data[firstOffset + col] = data[secondOffset + col]
|
||||||
|
data[secondOffset + col] = tmp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,113 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package net.sergeych.lyng.matrix
|
||||||
|
|
||||||
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
|
internal data class VectorData(
|
||||||
|
val values: DoubleArray
|
||||||
|
) {
|
||||||
|
init {
|
||||||
|
require(values.isNotEmpty()) { "vector must have at least one element" }
|
||||||
|
}
|
||||||
|
|
||||||
|
val size: Int get() = values.size
|
||||||
|
|
||||||
|
fun at(index: Int): Double {
|
||||||
|
require(index in values.indices) { "vector index $index out of bounds for length $size" }
|
||||||
|
return values[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
fun plus(other: VectorData): VectorData {
|
||||||
|
require(size == other.size) { "vector size mismatch: $size vs ${other.size}" }
|
||||||
|
return VectorData(DoubleArray(size) { index -> values[index] + other.values[index] })
|
||||||
|
}
|
||||||
|
|
||||||
|
fun minus(other: VectorData): VectorData {
|
||||||
|
require(size == other.size) { "vector size mismatch: $size vs ${other.size}" }
|
||||||
|
return VectorData(DoubleArray(size) { index -> values[index] - other.values[index] })
|
||||||
|
}
|
||||||
|
|
||||||
|
fun scale(factor: Double): VectorData =
|
||||||
|
VectorData(DoubleArray(size) { index -> values[index] * factor })
|
||||||
|
|
||||||
|
fun divide(divisor: Double): VectorData {
|
||||||
|
require(divisor != 0.0) { "vector division by zero" }
|
||||||
|
return VectorData(DoubleArray(size) { index -> values[index] / divisor })
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dot(other: VectorData): Double {
|
||||||
|
require(size == other.size) { "vector size mismatch: $size vs ${other.size}" }
|
||||||
|
var sum = 0.0
|
||||||
|
for (index in values.indices) {
|
||||||
|
sum += values[index] * other.values[index]
|
||||||
|
}
|
||||||
|
return sum
|
||||||
|
}
|
||||||
|
|
||||||
|
fun norm(): Double = sqrt(dot(this))
|
||||||
|
|
||||||
|
fun normalize(): VectorData {
|
||||||
|
val length = norm()
|
||||||
|
require(length != 0.0) { "cannot normalize a zero vector" }
|
||||||
|
return divide(length)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cross(other: VectorData): VectorData {
|
||||||
|
require(size == 3 && other.size == 3) { "cross product requires two 3D vectors" }
|
||||||
|
return VectorData(
|
||||||
|
doubleArrayOf(
|
||||||
|
values[1] * other.values[2] - values[2] * other.values[1],
|
||||||
|
values[2] * other.values[0] - values[0] * other.values[2],
|
||||||
|
values[0] * other.values[1] - values[1] * other.values[0]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun outer(other: VectorData): MatrixData {
|
||||||
|
val out = DoubleArray(size * other.size)
|
||||||
|
for (row in values.indices) {
|
||||||
|
val rowOffset = row * other.size
|
||||||
|
for (col in other.values.indices) {
|
||||||
|
out[rowOffset + col] = values[row] * other.values[col]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return MatrixData(size, other.size, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toList(): List<Double> = values.asList()
|
||||||
|
|
||||||
|
fun render(): String = buildString {
|
||||||
|
append("Vector([")
|
||||||
|
for (index in values.indices) {
|
||||||
|
if (index > 0) append(", ")
|
||||||
|
append(formatMatrixValue(values[index]))
|
||||||
|
}
|
||||||
|
append("])")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun compareTo(other: VectorData): Int {
|
||||||
|
val sizeCmp = size.compareTo(other.size)
|
||||||
|
if (sizeCmp != 0) return sizeCmp
|
||||||
|
for (index in values.indices) {
|
||||||
|
val cmp = values[index].compareTo(other.values[index])
|
||||||
|
if (cmp != 0) return cmp
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -17,22 +17,14 @@
|
|||||||
|
|
||||||
package net.sergeych.lyng.obj
|
package net.sergeych.lyng.obj
|
||||||
|
|
||||||
import com.ionspin.kotlin.bignum.decimal.BigDecimal as IonBigDecimal
|
|
||||||
import com.ionspin.kotlin.bignum.decimal.DecimalMode
|
import com.ionspin.kotlin.bignum.decimal.DecimalMode
|
||||||
import com.ionspin.kotlin.bignum.decimal.RoundingMode
|
import com.ionspin.kotlin.bignum.decimal.RoundingMode
|
||||||
import com.ionspin.kotlin.bignum.integer.BigInteger
|
import com.ionspin.kotlin.bignum.integer.BigInteger
|
||||||
import net.sergeych.lyng.Arguments
|
import net.sergeych.lyng.*
|
||||||
import net.sergeych.lyng.FrameSlotRef
|
|
||||||
import net.sergeych.lyng.InteropOperator
|
|
||||||
import net.sergeych.lyng.ModuleScope
|
|
||||||
import net.sergeych.lyng.OperatorInteropRegistry
|
|
||||||
import net.sergeych.lyng.RecordSlotRef
|
|
||||||
import net.sergeych.lyng.Scope
|
|
||||||
import net.sergeych.lyng.ScopeFacade
|
|
||||||
import net.sergeych.lyng.TypeDecl
|
|
||||||
import net.sergeych.lyng.miniast.addPropertyDoc
|
import net.sergeych.lyng.miniast.addPropertyDoc
|
||||||
import net.sergeych.lyng.miniast.type
|
import net.sergeych.lyng.miniast.type
|
||||||
import net.sergeych.lyng.requiredArg
|
import net.sergeych.lyng.requiredArg
|
||||||
|
import com.ionspin.kotlin.bignum.decimal.BigDecimal as IonBigDecimal
|
||||||
|
|
||||||
object ObjBigDecimalSupport {
|
object ObjBigDecimalSupport {
|
||||||
private const val decimalContextVar = "__lyng_decimal_context__"
|
private const val decimalContextVar = "__lyng_decimal_context__"
|
||||||
@ -88,6 +80,9 @@ object ObjBigDecimalSupport {
|
|||||||
decimalClass.addFn("toReal") {
|
decimalClass.addFn("toReal") {
|
||||||
ObjReal.of(valueOf(thisObj).doubleValue(false))
|
ObjReal.of(valueOf(thisObj).doubleValue(false))
|
||||||
}
|
}
|
||||||
|
decimalClass.addFn("toString") {
|
||||||
|
ObjString(valueOf(thisObj).toStringExpanded())
|
||||||
|
}
|
||||||
decimalClass.addFn("toStringExpanded") {
|
decimalClass.addFn("toStringExpanded") {
|
||||||
ObjString(valueOf(thisObj).toStringExpanded())
|
ObjString(valueOf(thisObj).toStringExpanded())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,385 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package net.sergeych.lyng.obj
|
||||||
|
|
||||||
|
import net.sergeych.lyng.ModuleScope
|
||||||
|
import net.sergeych.lyng.Scope
|
||||||
|
import net.sergeych.lyng.ScopeFacade
|
||||||
|
import net.sergeych.lyng.matrix.MatrixData
|
||||||
|
import net.sergeych.lyng.matrix.VectorData
|
||||||
|
import net.sergeych.lyng.requiredArg
|
||||||
|
|
||||||
|
object ObjMatrixSupport {
|
||||||
|
private sealed interface MatrixAxisIndex {
|
||||||
|
data class Single(val value: Int) : MatrixAxisIndex
|
||||||
|
data class Slice(val values: IntArray) : MatrixAxisIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
private object BoundMarker
|
||||||
|
private val defaultMatrix = MatrixData(1, 1, doubleArrayOf(0.0))
|
||||||
|
private val defaultVector = VectorData(doubleArrayOf(0.0))
|
||||||
|
|
||||||
|
suspend fun bindTo(module: ModuleScope) {
|
||||||
|
val matrixClass = module.requireClass("Matrix")
|
||||||
|
val vectorClass = module.requireClass("Vector")
|
||||||
|
if (matrixClass.kotlinClassData === BoundMarker && vectorClass.kotlinClassData === BoundMarker) return
|
||||||
|
|
||||||
|
bindVectorClass(vectorClass, matrixClass)
|
||||||
|
bindMatrixClass(matrixClass, vectorClass)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindVectorClass(vectorClass: ObjClass, matrixClass: ObjClass) {
|
||||||
|
if (vectorClass.kotlinClassData === BoundMarker) return
|
||||||
|
vectorClass.kotlinClassData = BoundMarker
|
||||||
|
vectorClass.isAbstract = false
|
||||||
|
|
||||||
|
val hooks = vectorClass.bridgeInitHooks ?: mutableListOf<suspend (ScopeFacade, ObjInstance) -> Unit>().also {
|
||||||
|
vectorClass.bridgeInitHooks = it
|
||||||
|
}
|
||||||
|
hooks += { _, instance -> instance.kotlinInstanceData = defaultVector }
|
||||||
|
|
||||||
|
vectorClass.addProperty("size", getter = {
|
||||||
|
ObjInt.of(vectorOf(thisObj).size.toLong())
|
||||||
|
})
|
||||||
|
vectorClass.addProperty("length", getter = {
|
||||||
|
ObjInt.of(vectorOf(thisObj).size.toLong())
|
||||||
|
})
|
||||||
|
vectorClass.addFn("toList") {
|
||||||
|
Obj.from(vectorOf(thisObj).toList())
|
||||||
|
}
|
||||||
|
vectorClass.addFn("get") {
|
||||||
|
ObjReal.of(vectorOf(thisObj).at(requiredArg<ObjInt>(0).value.toInt()))
|
||||||
|
}
|
||||||
|
vectorClass.addFn("plus") {
|
||||||
|
newVector(vectorClass, vectorOf(thisObj).plus(coerceVectorArg(requireScope(), args.firstAndOnly())))
|
||||||
|
}
|
||||||
|
vectorClass.addFn("minus") {
|
||||||
|
newVector(vectorClass, vectorOf(thisObj).minus(coerceVectorArg(requireScope(), args.firstAndOnly())))
|
||||||
|
}
|
||||||
|
vectorClass.addFn("mul") {
|
||||||
|
newVector(vectorClass, vectorOf(thisObj).scale(coerceScalarArg(requireScope(), args.firstAndOnly())))
|
||||||
|
}
|
||||||
|
vectorClass.addFn("div") {
|
||||||
|
newVector(vectorClass, vectorOf(thisObj).divide(coerceScalarArg(requireScope(), args.firstAndOnly())))
|
||||||
|
}
|
||||||
|
vectorClass.addFn("dot") {
|
||||||
|
ObjReal.of(vectorOf(thisObj).dot(coerceVectorArg(requireScope(), args.firstAndOnly())))
|
||||||
|
}
|
||||||
|
vectorClass.addFn("norm") {
|
||||||
|
ObjReal.of(vectorOf(thisObj).norm())
|
||||||
|
}
|
||||||
|
vectorClass.addFn("normalize") {
|
||||||
|
newVector(vectorClass, vectorOf(thisObj).normalize())
|
||||||
|
}
|
||||||
|
vectorClass.addFn("cross") {
|
||||||
|
newVector(vectorClass, vectorOf(thisObj).cross(coerceVectorArg(requireScope(), args.firstAndOnly())))
|
||||||
|
}
|
||||||
|
vectorClass.addFn("outer") {
|
||||||
|
newMatrix(matrixClass, vectorOf(thisObj).outer(coerceVectorArg(requireScope(), args.firstAndOnly())))
|
||||||
|
}
|
||||||
|
vectorClass.addFn("toString") {
|
||||||
|
ObjString(vectorOf(thisObj).render())
|
||||||
|
}
|
||||||
|
vectorClass.addFn("compareTo") {
|
||||||
|
ObjInt.of(vectorOf(thisObj).compareTo(coerceVectorArg(requireScope(), args.firstAndOnly())).toLong())
|
||||||
|
}
|
||||||
|
|
||||||
|
vectorClass.addClassFn("fromList") {
|
||||||
|
newVector(vectorClass, parseVector(requireScope(), requiredArg(0)))
|
||||||
|
}
|
||||||
|
vectorClass.addClassFn("zeros") {
|
||||||
|
val size = requiredArg<ObjInt>(0).value.toInt()
|
||||||
|
if (size <= 0) requireScope().raiseIllegalArgument("vector size must be positive")
|
||||||
|
newVector(vectorClass, VectorData(DoubleArray(size)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindMatrixClass(matrixClass: ObjClass, vectorClass: ObjClass) {
|
||||||
|
if (matrixClass.kotlinClassData === BoundMarker) return
|
||||||
|
matrixClass.kotlinClassData = BoundMarker
|
||||||
|
matrixClass.isAbstract = false
|
||||||
|
|
||||||
|
val hooks = matrixClass.bridgeInitHooks ?: mutableListOf<suspend (ScopeFacade, ObjInstance) -> Unit>().also {
|
||||||
|
matrixClass.bridgeInitHooks = it
|
||||||
|
}
|
||||||
|
hooks += { _, instance -> instance.kotlinInstanceData = defaultMatrix }
|
||||||
|
|
||||||
|
matrixClass.addProperty("rows", getter = {
|
||||||
|
ObjInt.of(matrixOf(thisObj).rows.toLong())
|
||||||
|
})
|
||||||
|
matrixClass.addProperty("cols", getter = {
|
||||||
|
ObjInt.of(matrixOf(thisObj).cols.toLong())
|
||||||
|
})
|
||||||
|
matrixClass.addProperty("shape", getter = {
|
||||||
|
ObjList(
|
||||||
|
mutableListOf(
|
||||||
|
ObjInt.of(matrixOf(thisObj).rows.toLong()),
|
||||||
|
ObjInt.of(matrixOf(thisObj).cols.toLong())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
matrixClass.addProperty("isSquare", getter = {
|
||||||
|
matrixOf(thisObj).isSquare.toObj()
|
||||||
|
})
|
||||||
|
|
||||||
|
matrixClass.addFn("plus") {
|
||||||
|
newMatrix(matrixClass, matrixOf(thisObj).plus(coerceMatrixArg(requireScope(), args.firstAndOnly())))
|
||||||
|
}
|
||||||
|
matrixClass.addFn("minus") {
|
||||||
|
newMatrix(matrixClass, matrixOf(thisObj).minus(coerceMatrixArg(requireScope(), args.firstAndOnly())))
|
||||||
|
}
|
||||||
|
matrixClass.addFn("mul") {
|
||||||
|
when (val other = args.firstAndOnly()) {
|
||||||
|
is ObjInstance -> when (other.objClass.className) {
|
||||||
|
"Matrix" -> newMatrix(matrixClass, matrixOf(thisObj).multiply(matrixOf(other)))
|
||||||
|
"Vector" -> newVector(vectorClass, matrixOf(thisObj).multiply(vectorOf(other)))
|
||||||
|
else -> newMatrix(matrixClass, matrixOf(thisObj).scale(coerceScalarArg(requireScope(), other)))
|
||||||
|
}
|
||||||
|
else -> newMatrix(matrixClass, matrixOf(thisObj).scale(coerceScalarArg(requireScope(), other)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
matrixClass.addFn("div") {
|
||||||
|
newMatrix(matrixClass, matrixOf(thisObj).divide(coerceScalarArg(requireScope(), args.firstAndOnly())))
|
||||||
|
}
|
||||||
|
matrixClass.addFn("transpose") {
|
||||||
|
newMatrix(matrixClass, matrixOf(thisObj).transpose())
|
||||||
|
}
|
||||||
|
matrixClass.addFn("trace") {
|
||||||
|
ObjReal.of(matrixOf(thisObj).trace())
|
||||||
|
}
|
||||||
|
matrixClass.addFn("rank") {
|
||||||
|
ObjInt.of(matrixOf(thisObj).rank().toLong())
|
||||||
|
}
|
||||||
|
matrixClass.addFn("determinant") {
|
||||||
|
ObjReal.of(matrixOf(thisObj).determinant())
|
||||||
|
}
|
||||||
|
matrixClass.addFn("inverse") {
|
||||||
|
newMatrix(matrixClass, matrixOf(thisObj).inverse())
|
||||||
|
}
|
||||||
|
matrixClass.addFn("solve") {
|
||||||
|
when (val rhs = args.firstAndOnly()) {
|
||||||
|
is ObjInstance -> when (rhs.objClass.className) {
|
||||||
|
"Vector" -> newVector(vectorClass, matrixOf(thisObj).solve(vectorOf(rhs)))
|
||||||
|
"Matrix" -> newMatrix(matrixClass, matrixOf(thisObj).solve(matrixOf(rhs)))
|
||||||
|
else -> requireScope().raiseClassCastError("Matrix.solve expects Vector or Matrix")
|
||||||
|
}
|
||||||
|
else -> requireScope().raiseClassCastError("Matrix.solve expects Vector or Matrix")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
matrixClass.addFn("get") {
|
||||||
|
ObjReal.of(
|
||||||
|
matrixOf(thisObj).at(
|
||||||
|
requiredArg<ObjInt>(0).value.toInt(),
|
||||||
|
requiredArg<ObjInt>(1).value.toInt()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
matrixClass.addFn("getAt") {
|
||||||
|
resolveMatrixIndex(matrixClass, matrixOf(thisObj), args.firstAndOnly(), thisObj)
|
||||||
|
}
|
||||||
|
matrixClass.addFn("row") {
|
||||||
|
doubleArrayToObjList(matrixOf(thisObj).rowValues(requiredArg<ObjInt>(0).value.toInt()))
|
||||||
|
}
|
||||||
|
matrixClass.addFn("column") {
|
||||||
|
doubleArrayToObjList(matrixOf(thisObj).columnValues(requiredArg<ObjInt>(0).value.toInt()))
|
||||||
|
}
|
||||||
|
matrixClass.addFn("toList") {
|
||||||
|
Obj.from(matrixOf(thisObj).toNestedLists())
|
||||||
|
}
|
||||||
|
matrixClass.addFn("toString") {
|
||||||
|
ObjString(matrixOf(thisObj).render())
|
||||||
|
}
|
||||||
|
matrixClass.addFn("compareTo") {
|
||||||
|
ObjInt.of(matrixOf(thisObj).compareTo(coerceMatrixArg(requireScope(), args.firstAndOnly())).toLong())
|
||||||
|
}
|
||||||
|
|
||||||
|
matrixClass.addClassFn("fromRows") {
|
||||||
|
newMatrix(matrixClass, parseRows(requireScope(), requiredArg(0)))
|
||||||
|
}
|
||||||
|
matrixClass.addClassFn("zeros") {
|
||||||
|
val rows = requiredArg<ObjInt>(0).value.toInt()
|
||||||
|
val cols = requiredArg<ObjInt>(1).value.toInt()
|
||||||
|
if (rows <= 0) requireScope().raiseIllegalArgument("matrix must have at least one row")
|
||||||
|
if (cols <= 0) requireScope().raiseIllegalArgument("matrix must have at least one column")
|
||||||
|
newMatrix(matrixClass, MatrixData(rows, cols, DoubleArray(rows * cols)))
|
||||||
|
}
|
||||||
|
matrixClass.addClassFn("identity") {
|
||||||
|
val size = requiredArg<ObjInt>(0).value.toInt()
|
||||||
|
if (size <= 0) requireScope().raiseIllegalArgument("identity matrix size must be positive")
|
||||||
|
val values = DoubleArray(size * size)
|
||||||
|
for (index in 0 until size) {
|
||||||
|
values[index * size + index] = 1.0
|
||||||
|
}
|
||||||
|
newMatrix(matrixClass, MatrixData(size, size, values))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun matrixOf(obj: Obj): MatrixData {
|
||||||
|
val instance = obj as? ObjInstance ?: error("Matrix receiver must be an object instance")
|
||||||
|
return instance.kotlinInstanceData as? MatrixData ?: defaultMatrix
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun vectorOf(obj: Obj): VectorData {
|
||||||
|
val instance = obj as? ObjInstance ?: error("Vector receiver must be an object instance")
|
||||||
|
return instance.kotlinInstanceData as? VectorData ?: defaultVector
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun ScopeFacade.newMatrix(matrixClass: ObjClass, value: MatrixData): ObjInstance {
|
||||||
|
val instance = call(matrixClass) as? ObjInstance
|
||||||
|
?: raiseIllegalState("Matrix() did not return an object instance")
|
||||||
|
instance.kotlinInstanceData = value
|
||||||
|
return instance
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun ScopeFacade.newVector(vectorClass: ObjClass, value: VectorData): ObjInstance {
|
||||||
|
val instance = call(vectorClass) as? ObjInstance
|
||||||
|
?: raiseIllegalState("Vector() did not return an object instance")
|
||||||
|
instance.kotlinInstanceData = value
|
||||||
|
return instance
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun coerceMatrixArg(scope: Scope, value: Obj): MatrixData {
|
||||||
|
val instance = value as? ObjInstance
|
||||||
|
?: scope.raiseClassCastError("expected Matrix, got ${value.objClass.className}")
|
||||||
|
if (instance.objClass.className != "Matrix") {
|
||||||
|
scope.raiseClassCastError("expected Matrix, got ${instance.objClass.className}")
|
||||||
|
}
|
||||||
|
return instance.kotlinInstanceData as? MatrixData ?: defaultMatrix
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun coerceVectorArg(scope: Scope, value: Obj): VectorData {
|
||||||
|
val instance = value as? ObjInstance
|
||||||
|
?: scope.raiseClassCastError("expected Vector, got ${value.objClass.className}")
|
||||||
|
if (instance.objClass.className != "Vector") {
|
||||||
|
scope.raiseClassCastError("expected Vector, got ${instance.objClass.className}")
|
||||||
|
}
|
||||||
|
return instance.kotlinInstanceData as? VectorData ?: defaultVector
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun coerceScalarArg(scope: Scope, value: Obj): Double = try {
|
||||||
|
value.toDouble()
|
||||||
|
} catch (_: IllegalArgumentException) {
|
||||||
|
scope.raiseClassCastError("expected matrix scalar (Int or Real), got ${value.objClass.className}")
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun parseRows(scope: Scope, rowsObj: Obj): MatrixData {
|
||||||
|
val outer = asObjList(scope, rowsObj, "Matrix.fromRows expects a list of rows")
|
||||||
|
if (outer.list.isEmpty()) scope.raiseIllegalArgument("matrix must have at least one row")
|
||||||
|
|
||||||
|
val rows = outer.list.size
|
||||||
|
var cols = -1
|
||||||
|
val values = mutableListOf<Double>()
|
||||||
|
|
||||||
|
for (rowObj in outer.list) {
|
||||||
|
val row = asObjList(scope, rowObj, "Matrix rows must be lists")
|
||||||
|
if (cols == -1) {
|
||||||
|
cols = row.list.size
|
||||||
|
if (cols <= 0) scope.raiseIllegalArgument("matrix must have at least one column")
|
||||||
|
} else if (row.list.size != cols) {
|
||||||
|
scope.raiseIllegalArgument("matrix rows must all have the same length")
|
||||||
|
}
|
||||||
|
for (cell in row.list) {
|
||||||
|
values += coerceScalarArg(scope, cell)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return MatrixData(rows, cols, values.toDoubleArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun parseVector(scope: Scope, valuesObj: Obj): VectorData {
|
||||||
|
val list = asObjList(scope, valuesObj, "Vector.fromList expects a list")
|
||||||
|
if (list.list.isEmpty()) scope.raiseIllegalArgument("vector must have at least one element")
|
||||||
|
return VectorData(list.list.map { coerceScalarArg(scope, it) }.toDoubleArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun ScopeFacade.resolveMatrixIndex(
|
||||||
|
matrixClass: ObjClass,
|
||||||
|
matrix: MatrixData,
|
||||||
|
index: Obj,
|
||||||
|
receiver: Obj
|
||||||
|
): Obj {
|
||||||
|
val tuple = asObjList(requireScope(), index, "Matrix index must be [row, col]")
|
||||||
|
if (tuple.list.size != 2) {
|
||||||
|
raiseIllegalArgument("Matrix index must contain exactly two selectors: [row, col]")
|
||||||
|
}
|
||||||
|
|
||||||
|
val rowIndex = decodeAxisIndex(requireScope(), tuple.list[0], matrix.rows, "row")
|
||||||
|
val colIndex = decodeAxisIndex(requireScope(), tuple.list[1], matrix.cols, "column")
|
||||||
|
|
||||||
|
return when {
|
||||||
|
rowIndex is MatrixAxisIndex.Single && colIndex is MatrixAxisIndex.Single ->
|
||||||
|
ObjReal.of(matrix.at(rowIndex.value, colIndex.value))
|
||||||
|
|
||||||
|
rowIndex is MatrixAxisIndex.Single && colIndex is MatrixAxisIndex.Slice ->
|
||||||
|
newMatrix(matrixClass, matrix.slice(intArrayOf(rowIndex.value), colIndex.values))
|
||||||
|
|
||||||
|
rowIndex is MatrixAxisIndex.Slice && colIndex is MatrixAxisIndex.Single ->
|
||||||
|
newMatrix(matrixClass, matrix.slice(rowIndex.values, intArrayOf(colIndex.value)))
|
||||||
|
|
||||||
|
rowIndex is MatrixAxisIndex.Slice && colIndex is MatrixAxisIndex.Slice ->
|
||||||
|
newMatrix(matrixClass, matrix.slice(rowIndex.values, colIndex.values))
|
||||||
|
|
||||||
|
else -> requireScope().raiseIllegalState("unreachable matrix index state for ${receiver.objClass.className}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun decodeAxisIndex(scope: Scope, index: Obj, size: Int, axisName: String): MatrixAxisIndex =
|
||||||
|
when (index) {
|
||||||
|
is ObjInt -> {
|
||||||
|
val value = index.value.toInt()
|
||||||
|
if (value !in 0 until size) {
|
||||||
|
scope.raiseIllegalArgument("$axisName index $value out of bounds for length $size")
|
||||||
|
}
|
||||||
|
MatrixAxisIndex.Single(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
is ObjRange -> {
|
||||||
|
if (index.hasExplicitStep) {
|
||||||
|
scope.raiseIllegalArgument("Matrix slicing does not support stepped $axisName ranges")
|
||||||
|
}
|
||||||
|
val start = index.startInt(scope)
|
||||||
|
val endExclusive = index.exclusiveIntEnd(scope) ?: size
|
||||||
|
if (start !in 0..size) {
|
||||||
|
scope.raiseIllegalArgument("$axisName slice start $start out of bounds for length $size")
|
||||||
|
}
|
||||||
|
if (endExclusive !in 0..size) {
|
||||||
|
scope.raiseIllegalArgument("$axisName slice end $endExclusive out of bounds for length $size")
|
||||||
|
}
|
||||||
|
if (start > endExclusive) {
|
||||||
|
scope.raiseIllegalArgument("$axisName slice start $start is after end $endExclusive")
|
||||||
|
}
|
||||||
|
if (start == endExclusive) {
|
||||||
|
scope.raiseIllegalArgument("Matrix slice must include at least one $axisName")
|
||||||
|
}
|
||||||
|
MatrixAxisIndex.Slice(IntArray(endExclusive - start) { start + it })
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> scope.raiseClassCastError("Matrix $axisName selector must be Int or Range, got ${index.objClass.className}")
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun asObjList(scope: Scope, value: Obj, message: String): ObjList = when (value) {
|
||||||
|
is ObjList -> value
|
||||||
|
else -> if (value.isInstanceOf(ObjIterable)) {
|
||||||
|
value.callMethod<ObjList>(scope, "toList")
|
||||||
|
} else {
|
||||||
|
scope.raiseClassCastError(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun doubleArrayToObjList(values: DoubleArray): ObjList =
|
||||||
|
ObjList(values.map { ObjReal.of(it) }.toMutableList())
|
||||||
|
}
|
||||||
@ -135,4 +135,15 @@ class BigDecimalModuleTest {
|
|||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDefaultToString() = runTest {
|
||||||
|
eval("""
|
||||||
|
import lyng.decimal
|
||||||
|
|
||||||
|
var s0 = "0.1".d + "0.1".d
|
||||||
|
assertEquals("0.2", s0.toStringExpanded())
|
||||||
|
assertEquals("0.2", s0.toString())
|
||||||
|
""".trimIndent())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,155 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package net.sergeych.lyng
|
||||||
|
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import kotlin.test.Test
|
||||||
|
|
||||||
|
class MatrixModuleTest {
|
||||||
|
@Test
|
||||||
|
fun testMatrixConstructionAndShape() = runTest {
|
||||||
|
val scope = Script.newScope()
|
||||||
|
scope.eval(
|
||||||
|
"""
|
||||||
|
import lyng.matrix
|
||||||
|
|
||||||
|
val a: Matrix = matrix([[1, 2, 3], [4, 5, 6]])
|
||||||
|
val v: Vector = vector([1, 2, 3])
|
||||||
|
assertEquals(2, a.rows)
|
||||||
|
assertEquals(3, a.cols)
|
||||||
|
assertEquals([2, 3], a.shape)
|
||||||
|
assertEquals(false, a.isSquare)
|
||||||
|
assertEquals([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], a.toList())
|
||||||
|
assertEquals("Matrix(2x3, [[1, 2, 3], [4, 5, 6]])", a.toString())
|
||||||
|
assertEquals(3, v.size)
|
||||||
|
assertEquals([1.0, 2.0, 3.0], v.toList())
|
||||||
|
assertEquals("Vector([1, 2, 3])", v.toString())
|
||||||
|
assertEquals([0.2672612419124244, 0.5345224838248488, 0.8017837257372732], v.normalize().toList())
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testMatrixArithmeticAndTranspose() = runTest {
|
||||||
|
val scope = Script.newScope()
|
||||||
|
scope.eval(
|
||||||
|
"""
|
||||||
|
import lyng.matrix
|
||||||
|
|
||||||
|
val a: Matrix = matrix([[1, 2, 3], [4, 5, 6]])
|
||||||
|
val b: Matrix = matrix([[7, 8], [9, 10], [11, 12]])
|
||||||
|
val product: Matrix = a * b
|
||||||
|
assertEquals([[58.0, 64.0], [139.0, 154.0]], product.toList())
|
||||||
|
|
||||||
|
val scaled: Matrix = product * 0.5
|
||||||
|
assertEquals([[29.0, 32.0], [69.5, 77.0]], scaled.toList())
|
||||||
|
|
||||||
|
val sum: Matrix = matrix([[1, 2], [3, 4]]) + Matrix.identity(2)
|
||||||
|
assertEquals([[2.0, 2.0], [3.0, 5.0]], sum.toList())
|
||||||
|
|
||||||
|
val transposed: Matrix = a.transpose()
|
||||||
|
assertEquals([[1.0, 4.0], [2.0, 5.0], [3.0, 6.0]], transposed.toList())
|
||||||
|
assertEquals([4.0, 5.0, 6.0], a.row(1))
|
||||||
|
assertEquals([2.0, 5.0], a.column(1))
|
||||||
|
|
||||||
|
val x: Vector = vector([1, 0.5, -1])
|
||||||
|
val y: Vector = a * x
|
||||||
|
assertEquals([-1.0, 0.5], y.toList())
|
||||||
|
|
||||||
|
val shifted: Vector = x + vector([2, 2, 2])
|
||||||
|
assertEquals([3.0, 2.5, 1.0], shifted.toList())
|
||||||
|
val d0: Vector = vector([1, 2, 3])
|
||||||
|
val d1: Vector = vector([2, 0, 0])
|
||||||
|
assertEquals(2.0, d0.dot(d1))
|
||||||
|
val cx0: Vector = vector([1, 0, 0])
|
||||||
|
val cx1: Vector = vector([0, 1, 0])
|
||||||
|
assertEquals([0.0, 0.0, 1.0], cx0.cross(cx1).toList())
|
||||||
|
val o0: Vector = vector([1.5, 3.0])
|
||||||
|
val o1: Vector = vector([2, 2.6666666666666665])
|
||||||
|
assertEquals([[3.0, 4.0], [6.0, 8.0]], o0.outer(o1).toList())
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testMatrixDeterminantAndInverse() = runTest {
|
||||||
|
val scope = Script.newScope()
|
||||||
|
scope.eval(
|
||||||
|
"""
|
||||||
|
import lyng.matrix
|
||||||
|
|
||||||
|
val eps = 1e-9
|
||||||
|
val a: Matrix = matrix([[4, 7], [2, 6]])
|
||||||
|
assert(abs(a.determinant() - 10.0) < eps)
|
||||||
|
assertEquals(10.0, a.trace())
|
||||||
|
assertEquals(2, a.rank())
|
||||||
|
|
||||||
|
val inv: Matrix = a.inverse()
|
||||||
|
assert(abs(inv.get(0, 0) - 0.6) < eps)
|
||||||
|
assert(abs(inv.get(0, 1) + 0.7) < eps)
|
||||||
|
assert(abs(inv.get(1, 0) + 0.2) < eps)
|
||||||
|
assert(abs(inv.get(1, 1) - 0.4) < eps)
|
||||||
|
|
||||||
|
val identity: Matrix = a * inv
|
||||||
|
assert(abs(identity.get(0, 0) - 1.0) < eps)
|
||||||
|
assert(abs(identity.get(0, 1)) < eps)
|
||||||
|
assert(abs(identity.get(1, 0)) < eps)
|
||||||
|
assert(abs(identity.get(1, 1) - 1.0) < eps)
|
||||||
|
|
||||||
|
val rhs: Vector = vector([1, 0])
|
||||||
|
val solution: Vector = a.solve(rhs)
|
||||||
|
assert(abs(solution.get(0) - 0.6) < eps)
|
||||||
|
assert(abs(solution.get(1) + 0.2) < eps)
|
||||||
|
|
||||||
|
val rhsMatrix: Matrix = Matrix.identity(2)
|
||||||
|
val solvedMatrix: Matrix = a.solve(rhsMatrix)
|
||||||
|
assert(abs(solvedMatrix.get(0, 0) - 0.6) < eps)
|
||||||
|
assert(abs(solvedMatrix.get(1, 1) - 0.4) < eps)
|
||||||
|
|
||||||
|
val lowRank: Matrix = matrix([[1, 2], [2, 4]])
|
||||||
|
assertEquals(1, lowRank.rank())
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testMatrixBracketIndexingAndSlices() = runTest {
|
||||||
|
val scope = Script.newScope()
|
||||||
|
scope.eval(
|
||||||
|
"""
|
||||||
|
import lyng.matrix
|
||||||
|
|
||||||
|
val m: Matrix = matrix([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]])
|
||||||
|
|
||||||
|
assertEquals(7.0, m[1, 2])
|
||||||
|
|
||||||
|
val columnSlice: Matrix = m[0..2, 2]
|
||||||
|
assertEquals([[3.0], [7.0], [11.0]], columnSlice.toList())
|
||||||
|
|
||||||
|
val topLeft: Matrix = m[0..1, 0..1]
|
||||||
|
assertEquals([[1.0, 2.0], [5.0, 6.0]], topLeft.toList())
|
||||||
|
|
||||||
|
val tail: Matrix = m[1.., 1..]
|
||||||
|
assertEquals([[6.0, 7.0, 8.0], [10.0, 11.0, 12.0]], tail.toList())
|
||||||
|
|
||||||
|
val rowSlice: Matrix = m[1, 1..2]
|
||||||
|
assertEquals([[6.0, 7.0]], rowSlice.toList())
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package net.sergeych.lyng.matrix
|
||||||
|
|
||||||
|
import org.jetbrains.kotlinx.multik.api.mk
|
||||||
|
import org.jetbrains.kotlinx.multik.api.ndarray
|
||||||
|
import org.jetbrains.kotlinx.multik.api.linalg.dot
|
||||||
|
import org.jetbrains.kotlinx.multik.api.linalg.inv
|
||||||
|
import org.jetbrains.kotlinx.multik.ndarray.data.get
|
||||||
|
|
||||||
|
internal actual object PlatformMatrixBackend {
|
||||||
|
actual fun multiply(left: MatrixData, right: MatrixData): MatrixData {
|
||||||
|
require(left.cols == right.rows) {
|
||||||
|
"matrix multiplication shape mismatch: ${left.rows}x${left.cols} cannot multiply ${right.rows}x${right.cols}"
|
||||||
|
}
|
||||||
|
val leftArray = mk.ndarray(left.values, left.rows, left.cols)
|
||||||
|
val rightArray = mk.ndarray(right.values, right.rows, right.cols)
|
||||||
|
val product = leftArray dot rightArray
|
||||||
|
return MatrixData(left.rows, right.cols, extract(product, left.rows, right.cols))
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun inverse(matrix: MatrixData): MatrixData {
|
||||||
|
require(matrix.isSquare) { "matrix inverse requires a square matrix, got ${matrix.rows}x${matrix.cols}" }
|
||||||
|
val source = mk.ndarray(matrix.values, matrix.rows, matrix.cols)
|
||||||
|
val inverse = mk.linalg.inv(source)
|
||||||
|
return MatrixData(matrix.rows, matrix.cols, extract(inverse, matrix.rows, matrix.cols))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extract(array: org.jetbrains.kotlinx.multik.ndarray.data.D2Array<Double>, rows: Int, cols: Int): DoubleArray {
|
||||||
|
val out = DoubleArray(rows * cols)
|
||||||
|
for (row in 0 until rows) {
|
||||||
|
for (col in 0 until cols) {
|
||||||
|
out[row * cols + col] = array[row, col]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package net.sergeych.lyng.matrix
|
||||||
|
|
||||||
|
internal actual object PlatformMatrixBackend {
|
||||||
|
actual fun multiply(left: MatrixData, right: MatrixData): MatrixData =
|
||||||
|
PureMatrixAlgorithms.multiply(left, right)
|
||||||
|
|
||||||
|
actual fun inverse(matrix: MatrixData): MatrixData =
|
||||||
|
PureMatrixAlgorithms.inverse(matrix)
|
||||||
|
}
|
||||||
157
lynglib/stdlib/lyng/matrix.lyng
Normal file
157
lynglib/stdlib/lyng/matrix.lyng
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
package lyng.matrix
|
||||||
|
|
||||||
|
type MatrixScalar = Real | Int
|
||||||
|
|
||||||
|
/** Dense immutable vector backed by double-precision elements. */
|
||||||
|
extern class Vector() {
|
||||||
|
/** Number of elements. */
|
||||||
|
val size: Int
|
||||||
|
|
||||||
|
/** Alias to `size`. */
|
||||||
|
val length: Int
|
||||||
|
|
||||||
|
/** Convert to a plain list. */
|
||||||
|
extern fun toList(): List<Real>
|
||||||
|
|
||||||
|
/** Get one element by zero-based index. */
|
||||||
|
extern fun get(index: Int): Real
|
||||||
|
|
||||||
|
/** Element-wise addition. */
|
||||||
|
extern fun plus(other: Vector): Vector
|
||||||
|
|
||||||
|
/** Element-wise subtraction. */
|
||||||
|
extern fun minus(other: Vector): Vector
|
||||||
|
|
||||||
|
/** Scale every element. */
|
||||||
|
extern fun mul(other: MatrixScalar): Vector
|
||||||
|
|
||||||
|
/** Divide every element by a scalar. */
|
||||||
|
extern fun div(other: MatrixScalar): Vector
|
||||||
|
|
||||||
|
/** Dot product. */
|
||||||
|
extern fun dot(other: Vector): Real
|
||||||
|
|
||||||
|
/** Euclidean norm. */
|
||||||
|
extern fun norm(): Real
|
||||||
|
|
||||||
|
/** Return a unit-length vector in the same direction. */
|
||||||
|
extern fun normalize(): Vector
|
||||||
|
|
||||||
|
/** 3D cross product. Both vectors must have length 3. */
|
||||||
|
extern fun cross(other: Vector): Vector
|
||||||
|
|
||||||
|
/** Outer product, returning a matrix of shape `(this.size, other.size)`. */
|
||||||
|
extern fun outer(other: Vector): Matrix
|
||||||
|
|
||||||
|
/** Build a vector from numeric values. */
|
||||||
|
static extern fun fromList(values: List<MatrixScalar>): Vector
|
||||||
|
|
||||||
|
/** Zero-filled vector. */
|
||||||
|
static extern fun zeros(size: Int): Vector
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dense immutable matrix backed by double-precision elements.
|
||||||
|
*
|
||||||
|
* `Matrix` supports arbitrary rectangular geometry (`rows x cols`), matrix arithmetic,
|
||||||
|
* transpose, determinant, and inversion.
|
||||||
|
*
|
||||||
|
* Use this type for numeric linear algebra where contiguous dense storage is appropriate.
|
||||||
|
*/
|
||||||
|
extern class Matrix() {
|
||||||
|
/** Number of rows. */
|
||||||
|
val rows: Int
|
||||||
|
|
||||||
|
/** Number of columns. */
|
||||||
|
val cols: Int
|
||||||
|
|
||||||
|
/** Two-element shape `[rows, cols]`. */
|
||||||
|
val shape: List<Int>
|
||||||
|
|
||||||
|
/** Whether `rows == cols`. */
|
||||||
|
val isSquare: Bool
|
||||||
|
|
||||||
|
/** Element-wise addition. Shapes must match. */
|
||||||
|
extern fun plus(other: Matrix): Matrix
|
||||||
|
|
||||||
|
/** Element-wise subtraction. Shapes must match. */
|
||||||
|
extern fun minus(other: Matrix): Matrix
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Multiply by another matrix, by a vector, or by a scalar.
|
||||||
|
*
|
||||||
|
* - `Matrix * Matrix`: matrix product (`A.cols == B.rows`)
|
||||||
|
* - `Matrix * Vector`: matrix-vector product (`A.cols == x.size`)
|
||||||
|
* - `Matrix * MatrixScalar`: scale every element
|
||||||
|
*/
|
||||||
|
extern fun mul(other: Matrix | Vector | MatrixScalar): Matrix | Vector
|
||||||
|
|
||||||
|
/** Divide every element by a scalar. */
|
||||||
|
extern fun div(other: MatrixScalar): Matrix
|
||||||
|
|
||||||
|
/** Matrix transpose. */
|
||||||
|
extern fun transpose(): Matrix
|
||||||
|
|
||||||
|
/** Sum of diagonal elements of a square matrix. */
|
||||||
|
extern fun trace(): Real
|
||||||
|
|
||||||
|
/** Row rank computed numerically via row-echelon reduction. */
|
||||||
|
extern fun rank(): Int
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determinant of a square matrix.
|
||||||
|
*
|
||||||
|
* Raises an error for non-square matrices.
|
||||||
|
*/
|
||||||
|
extern fun determinant(): Real
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inverse of a square matrix.
|
||||||
|
*
|
||||||
|
* Raises an error for non-square or singular matrices.
|
||||||
|
*/
|
||||||
|
extern fun inverse(): Matrix
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Solve `A * x = rhs`.
|
||||||
|
*
|
||||||
|
* - with a `Vector` right-hand side, returns a `Vector`
|
||||||
|
* - with a `Matrix` right-hand side, returns a `Matrix`
|
||||||
|
*/
|
||||||
|
extern fun solve(rhs: Vector | Matrix): Vector | Matrix
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bracket indexing by `[row, col]`.
|
||||||
|
*
|
||||||
|
* Each selector may be an `Int` or a `Range`:
|
||||||
|
* - `m[1, 2]` returns one scalar
|
||||||
|
* - `m[0..2, 2]` returns a `3x1` matrix
|
||||||
|
* - `m[0..2, 0..2]` returns a sub-matrix
|
||||||
|
* - `m[1.., 1..]` returns the bottom-right tail
|
||||||
|
*/
|
||||||
|
override extern fun getAt(index: List<Int | Range>): Real | Matrix
|
||||||
|
|
||||||
|
/** Get one element by zero-based row and column indices. */
|
||||||
|
extern fun get(row: Int, col: Int): Real
|
||||||
|
|
||||||
|
/** Return one row as a `List<Real>`. */
|
||||||
|
extern fun row(index: Int): List<Real>
|
||||||
|
|
||||||
|
/** Return one column as a `List<Real>`. */
|
||||||
|
extern fun column(index: Int): List<Real>
|
||||||
|
|
||||||
|
/** Convert to nested row lists. */
|
||||||
|
extern fun toList(): List<List<Real>>
|
||||||
|
|
||||||
|
/** Build a matrix from nested row lists. All rows must have the same length. */
|
||||||
|
static extern fun fromRows(rows: List<List<MatrixScalar>>): Matrix
|
||||||
|
|
||||||
|
/** Create a zero-filled matrix. */
|
||||||
|
static extern fun zeros(rows: Int, cols: Int): Matrix
|
||||||
|
|
||||||
|
/** Create an identity matrix of shape `(size, size)`. */
|
||||||
|
static extern fun identity(size: Int): Matrix
|
||||||
|
}
|
||||||
|
|
||||||
|
fun vector(values: List<MatrixScalar>): Vector = Vector.fromList(values)
|
||||||
|
fun matrix(rows: List<List<MatrixScalar>>): Matrix = Matrix.fromRows(rows)
|
||||||
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.kotlinMultiplatform)
|
alias(libs.plugins.kotlinMultiplatform)
|
||||||
id("org.jetbrains.kotlin.plugin.compose") version "2.2.21"
|
id("org.jetbrains.kotlin.plugin.compose") version "2.3.20"
|
||||||
id("org.jetbrains.compose") version "1.9.3"
|
id("org.jetbrains.compose") version "1.9.3"
|
||||||
`maven-publish`
|
`maven-publish`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,8 +21,8 @@
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.kotlinMultiplatform)
|
alias(libs.plugins.kotlinMultiplatform)
|
||||||
// Compose compiler plugin for Kotlin 2.2.21 (matches version catalog)
|
// Compose compiler plugin aligned with the project Kotlin version.
|
||||||
id("org.jetbrains.kotlin.plugin.compose") version "2.2.21"
|
id("org.jetbrains.kotlin.plugin.compose") version "2.3.20"
|
||||||
// Compose Multiplatform plugin for convenient dependencies (compose.html.core, etc.)
|
// Compose Multiplatform plugin for convenient dependencies (compose.html.core, etc.)
|
||||||
id("org.jetbrains.compose") version "1.9.3"
|
id("org.jetbrains.compose") version "1.9.3"
|
||||||
}
|
}
|
||||||
|
|||||||
18
tmp/test.lyng
Normal file
18
tmp/test.lyng
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
#!/binenv lyng
|
||||||
|
|
||||||
|
import lyng.decimal
|
||||||
|
|
||||||
|
val x = "0.1".d
|
||||||
|
val y = 0.1
|
||||||
|
var s0 = 0.d
|
||||||
|
var s1 = 0.0
|
||||||
|
|
||||||
|
for( i in 1..100 ) {
|
||||||
|
s0 += x
|
||||||
|
s1 += y
|
||||||
|
}
|
||||||
|
|
||||||
|
println("$s0")
|
||||||
|
println("$s1")
|
||||||
|
|
||||||
|
println(":: ${sin(3.14)}")
|
||||||
Loading…
x
Reference in New Issue
Block a user