Compare commits
124 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
3f09d20465 | ||
89d78c43bb | |||
c12f1ce1cd | |||
47327aef19 | |||
f0f9d0e174 | |||
fbf79f0a37 | |||
dc4f2c6126 | |||
a9d58bfac2 | |||
8c7c017ab4 | |||
f13b7268d6 | |||
92c4355f48 | |||
eb126a6090 | |||
a5bb42706b | |||
60a693b1b3 | |||
c55ce2cf9a | |||
e9bde68674 | |||
13b80be884 | |||
5c7d3d8a7a | |||
a2b5880da9 | |||
c63c2db651 | |||
91f860adf6 | |||
1799a9a909 | |||
9f21a14f96 | |||
4a5f5fab8c | |||
a2b7d1ecb0 | |||
d0e3faea88 | |||
54e915ef10 | |||
9edde7bdbd | |||
f72d7aa3fa | |||
05757aefdc | |||
55bcb08668 | |||
673a7c89a6 | |||
a66e411848 | |||
4a10c3c443 | |||
207064cd45 | |||
e5088ac8e4 | |||
ee83f81a04 | |||
5921978122 | |||
a9592d0372 | |||
44514cd477 | |||
24b6856f15 | |||
381da970bf | |||
4b05f46fa7 | |||
f974483a41 | |||
e729cb1a79 | |||
8e7277df69 | |||
23bceed89d | |||
9eb583dfc6 | |||
977500223d | |||
58675f72f5 | |||
85c2910ee9 | |||
d91296c47d | |||
8965629151 | |||
9a40d4f340 | |||
78dade4b49 | |||
70ab60f98c | |||
53cc4dc0df | |||
f28e9dc226 | |||
29af4dfb2c | |||
4835376c0d | |||
4639fdb558 | |||
2946f23a4b | |||
e8c6e90a0f | |||
2a700a5a2a | |||
dbacdbc7cf | |||
28ec2bc8b8 | |||
cfd9eb053c | |||
9edf3b13ef | |||
57e9df140b | |||
231f1bc858 | |||
8bd9bcc6a6 | |||
b1121d61cb | |||
fa2414ef47 | |||
7579ddfad4 | |||
aa52b4b927 | |||
34f9108ef7 | |||
bec075328b | |||
701ea8cf57 | |||
5e64b79b77 | |||
a12cf440e8 | |||
606c2cf5b1 | |||
fb03fcc982 | |||
cf129b6242 | |||
827eb6e4c1 | |||
81d6b672cf | |||
07cc41c645 | |||
0c647cff30 | |||
b539c2046a | |||
afee2f0a02 | |||
fb8ee59f14 | |||
74301afb42 | |||
fe98a836f8 | |||
0c128bce36 | |||
4e17c9051c | |||
0f687c3c51 | |||
53fc240c75 | |||
825f1a4d04 | |||
0443fdc3c0 | |||
78b18ebda6 | |||
0e963a7b13 | |||
2698cee80b | |||
811477a636 | |||
984e7f12ef | |||
1414cf5a2f | |||
1fcdbdc9f4 | |||
4f028ccee8 | |||
1619fdadf2 | |||
7f71d0c9e9 | |||
290010fc8c | |||
80cc62e25b | |||
f1b63c3951 | |||
01606af307 | |||
2cc0a5bcbc | |||
efe9a2e842 | |||
34e7dd2c6d | |||
a337daee93 | |||
a51510606f | |||
aef94767c5 | |||
8b6a6abd92 | |||
bc5037b256 | |||
036bef1adb | |||
cc36ef805b | |||
0f610a5e19 | |||
4c93b5c9b3 |
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,6 +1,7 @@
|
|||||||
# Created by .ignore support plugin (hsz.mobi)
|
# Created by .ignore support plugin (hsz.mobi)
|
||||||
.idea/
|
.idea/
|
||||||
.gradle
|
.gradle
|
||||||
|
.kotlin
|
||||||
|
|
||||||
*.iws
|
*.iws
|
||||||
*.iml
|
*.iml
|
||||||
@ -8,4 +9,7 @@
|
|||||||
|
|
||||||
out/
|
out/
|
||||||
build/
|
build/
|
||||||
|
|
||||||
!gradle-wrapper.jar
|
!gradle-wrapper.jar
|
||||||
|
|
||||||
|
/demo/device-collective/mapCache/
|
||||||
|
45
.space.kts
45
.space.kts
@ -1,45 +0,0 @@
|
|||||||
import kotlin.io.path.readText
|
|
||||||
|
|
||||||
job("Build") {
|
|
||||||
gradlew("spc.registry.jetbrains.space/p/sci/containers/kotlin-ci:1.0.3", "build")
|
|
||||||
}
|
|
||||||
|
|
||||||
job("Publish") {
|
|
||||||
startOn {
|
|
||||||
gitPush { enabled = false }
|
|
||||||
}
|
|
||||||
container("spc.registry.jetbrains.space/p/sci/containers/kotlin-ci:1.0.3") {
|
|
||||||
env["SPACE_USER"] = "{{ project:space_user }}"
|
|
||||||
env["SPACE_TOKEN"] = "{{ project:space_token }}"
|
|
||||||
kotlinScript { api ->
|
|
||||||
|
|
||||||
val spaceUser = System.getenv("SPACE_USER")
|
|
||||||
val spaceToken = System.getenv("SPACE_TOKEN")
|
|
||||||
|
|
||||||
// write the version to the build directory
|
|
||||||
api.gradlew("version")
|
|
||||||
|
|
||||||
//read the version from build file
|
|
||||||
val version = java.nio.file.Path.of("build/project-version.txt").readText()
|
|
||||||
|
|
||||||
val revisionSuffix = if (version.endsWith("SNAPSHOT")) {
|
|
||||||
"-" + api.gitRevision().take(7)
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
}
|
|
||||||
|
|
||||||
api.space().projects.automation.deployments.start(
|
|
||||||
project = api.projectIdentifier(),
|
|
||||||
targetIdentifier = TargetIdentifier.Key("maps-kt"),
|
|
||||||
version = version+revisionSuffix,
|
|
||||||
// automatically update deployment status based on the status of a job
|
|
||||||
syncWithAutomationJob = true
|
|
||||||
)
|
|
||||||
api.gradlew(
|
|
||||||
"publishAllPublicationsToSpaceRepository",
|
|
||||||
"-Ppublishing.space.user=\"$spaceUser\"",
|
|
||||||
"-Ppublishing.space.token=\"$spaceToken\"",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
./space/* "Project Admin"
|
|
68
CHANGELOG.md
68
CHANGELOG.md
@ -3,6 +3,64 @@
|
|||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
- Value averaging plot extension
|
||||||
|
- PLC4X bindings
|
||||||
|
- Shortcuts to access all Controls devices in a magix network.
|
||||||
|
- `DeviceClient` properly evaluates lifecycle and logs
|
||||||
|
- `PeerConnection` API for direct device-device binary sharing
|
||||||
|
- DeviceDrawable2D intermediate visualization implementation
|
||||||
|
- New interface `WithLifeCycle`. Change Port API to adhere to it.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Constructor properties return `DeviceState` in order to be able to subscribe to them
|
||||||
|
- Refactored ports. Now we have `AsynchronousPort` as well as `SynchronousPort`
|
||||||
|
- `DeviceClient` now initializes property and action descriptors eagerly.
|
||||||
|
- `DeviceHub` now works with `Name` instead of `NameToken`. Tree-like structure is made using `Path`. Device messages no longer have access to sub-devices.
|
||||||
|
- Add some utility methods to ports. Synchronous port response could be now consumed as `Source`.
|
||||||
|
- `DeviceLifecycleState` is replaced by `LifecycleState`.
|
||||||
|
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fix a problem with rsocket endpoint with no filter.
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
## 0.3.0 - 2024-03-04
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Device lifecycle message
|
||||||
|
- Low-code constructor
|
||||||
|
- Automatic description generation for spec properties (JVM only)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Property caching moved from core `Device` to the `CachingDevice`
|
||||||
|
- `DeviceSpec` properties no explicitly pass property name to getters and setters.
|
||||||
|
- `DeviceHub.respondHubMessage` now returns a list of messages to allow querying multiple devices. Device server also returns an array.
|
||||||
|
- DataForge 0.8.0
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Property writing does not trigger change if logical state already is the same as value to be set.
|
||||||
|
- Modbus-slave triggers only once for multi-register write.
|
||||||
|
- Removed unnecessary scope in hub messageFlow
|
||||||
|
|
||||||
|
## 0.2.2-dev-1 - 2023-09-24
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- updating logical state in `DeviceBase` is now protected and called `propertyChanged()`
|
||||||
|
- `DeviceBase` tries to read property after write if the writer does not set the value.
|
||||||
|
|
||||||
|
## 0.2.1 - 2023-09-24
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
- Core interfaces for building a device server
|
- Core interfaces for building a device server
|
||||||
- Magix service for binding controls devices (both as RPC client and server)
|
- Magix service for binding controls devices (both as RPC client and server)
|
||||||
- A plugin for Controls-kt device server on top of modbus-rtu/modbus-tcp protocols
|
- A plugin for Controls-kt device server on top of modbus-rtu/modbus-tcp protocols
|
||||||
@ -20,13 +78,3 @@
|
|||||||
- A magix event loop implementation in Kotlin. Includes HTTP/SSE and RSocket routes.
|
- A magix event loop implementation in Kotlin. Includes HTTP/SSE and RSocket routes.
|
||||||
- Magix history database API
|
- Magix history database API
|
||||||
- ZMQ client endpoint for Magix
|
- ZMQ client endpoint for Magix
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
### Deprecated
|
|
||||||
|
|
||||||
### Removed
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
### Security
|
|
||||||
|
39
README.md
39
README.md
@ -1,5 +1,7 @@
|
|||||||
[![JetBrains Research](https://jb.gg/badges/research.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub)
|
[![JetBrains Research](https://jb.gg/badges/research.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub)
|
||||||
|
|
||||||
|
[![](https://maven.sciprog.center/api/badge/latest/kscience/space/kscience/controls-core-jvm?color=40c14a&name=repo.kotlin.link&prefix=v)](https://maven.sciprog.center/)
|
||||||
|
|
||||||
# Controls.kt
|
# Controls.kt
|
||||||
|
|
||||||
Controls.kt (former DataForge-control) is a data acquisition framework (work in progress). It is based on DataForge, a software framework for automated data processing.
|
Controls.kt (former DataForge-control) is a data acquisition framework (work in progress). It is based on DataForge, a software framework for automated data processing.
|
||||||
@ -42,6 +44,11 @@ Example view of a demo:
|
|||||||
## Modules
|
## Modules
|
||||||
|
|
||||||
|
|
||||||
|
### [controls-constructor](controls-constructor)
|
||||||
|
> A low-code constructor for composite devices simulation
|
||||||
|
>
|
||||||
|
> **Maturity**: PROTOTYPE
|
||||||
|
|
||||||
### [controls-core](controls-core)
|
### [controls-core](controls-core)
|
||||||
> Core interfaces for building a device server
|
> Core interfaces for building a device server
|
||||||
>
|
>
|
||||||
@ -56,6 +63,10 @@ Example view of a demo:
|
|||||||
> - [ports](controls-core/src/commonMain/kotlin/space/kscience/controls/ports) : Working with asynchronous data sending and receiving raw byte arrays
|
> - [ports](controls-core/src/commonMain/kotlin/space/kscience/controls/ports) : Working with asynchronous data sending and receiving raw byte arrays
|
||||||
|
|
||||||
|
|
||||||
|
### [controls-jupyter](controls-jupyter)
|
||||||
|
>
|
||||||
|
> **Maturity**: EXPERIMENTAL
|
||||||
|
|
||||||
### [controls-magix](controls-magix)
|
### [controls-magix](controls-magix)
|
||||||
> Magix service for binding controls devices (both as RPC client and server)
|
> Magix service for binding controls devices (both as RPC client and server)
|
||||||
>
|
>
|
||||||
@ -93,6 +104,11 @@ Automatically checks consistency.
|
|||||||
>
|
>
|
||||||
> **Maturity**: EXPERIMENTAL
|
> **Maturity**: EXPERIMENTAL
|
||||||
|
|
||||||
|
### [controls-plc4x](controls-plc4x)
|
||||||
|
> A plugin for Controls-kt device server on top of plc4x library
|
||||||
|
>
|
||||||
|
> **Maturity**: EXPERIMENTAL
|
||||||
|
|
||||||
### [controls-ports-ktor](controls-ports-ktor)
|
### [controls-ports-ktor](controls-ports-ktor)
|
||||||
> Implementation of byte ports on top os ktor-io asynchronous API
|
> Implementation of byte ports on top os ktor-io asynchronous API
|
||||||
>
|
>
|
||||||
@ -113,6 +129,16 @@ Automatically checks consistency.
|
|||||||
>
|
>
|
||||||
> **Maturity**: PROTOTYPE
|
> **Maturity**: PROTOTYPE
|
||||||
|
|
||||||
|
### [controls-vision](controls-vision)
|
||||||
|
> Dashboard and visualization extensions for devices
|
||||||
|
>
|
||||||
|
> **Maturity**: PROTOTYPE
|
||||||
|
|
||||||
|
### [controls-visualisation-compose](controls-visualisation-compose)
|
||||||
|
> Visualisation extension using compose-multiplatform
|
||||||
|
>
|
||||||
|
> **Maturity**: PROTOTYPE
|
||||||
|
|
||||||
### [demo](demo)
|
### [demo](demo)
|
||||||
>
|
>
|
||||||
> **Maturity**: EXPERIMENTAL
|
> **Maturity**: EXPERIMENTAL
|
||||||
@ -134,6 +160,14 @@ Automatically checks consistency.
|
|||||||
>
|
>
|
||||||
> **Maturity**: EXPERIMENTAL
|
> **Maturity**: EXPERIMENTAL
|
||||||
|
|
||||||
|
### [demo/constructor](demo/constructor)
|
||||||
|
>
|
||||||
|
> **Maturity**: EXPERIMENTAL
|
||||||
|
|
||||||
|
### [demo/device-collective](demo/device-collective)
|
||||||
|
>
|
||||||
|
> **Maturity**: EXPERIMENTAL
|
||||||
|
|
||||||
### [demo/echo](demo/echo)
|
### [demo/echo](demo/echo)
|
||||||
>
|
>
|
||||||
> **Maturity**: EXPERIMENTAL
|
> **Maturity**: EXPERIMENTAL
|
||||||
@ -189,6 +223,11 @@ Automatically checks consistency.
|
|||||||
>
|
>
|
||||||
> **Maturity**: PROTOTYPE
|
> **Maturity**: PROTOTYPE
|
||||||
|
|
||||||
|
### [magix/magix-utils](magix/magix-utils)
|
||||||
|
> Common utilities and services for Magix endpoints.
|
||||||
|
>
|
||||||
|
> **Maturity**: EXPERIMENTAL
|
||||||
|
|
||||||
### [magix/magix-zmq](magix/magix-zmq)
|
### [magix/magix-zmq](magix/magix-zmq)
|
||||||
> ZMQ client endpoint for Magix
|
> ZMQ client endpoint for Magix
|
||||||
>
|
>
|
||||||
|
@ -1,37 +1,26 @@
|
|||||||
import space.kscience.gradle.isInDevelopment
|
|
||||||
import space.kscience.gradle.useApache2Licence
|
import space.kscience.gradle.useApache2Licence
|
||||||
import space.kscience.gradle.useSPCTeam
|
import space.kscience.gradle.useSPCTeam
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("space.kscience.gradle.project")
|
id("space.kscience.gradle.project")
|
||||||
|
alias(libs.plugins.versions)
|
||||||
}
|
}
|
||||||
|
|
||||||
val dataforgeVersion: String by extra("0.6.2-dev-3")
|
|
||||||
val ktorVersion: String by extra(space.kscience.gradle.KScienceVersions.ktorVersion)
|
|
||||||
val rsocketVersion by extra("0.15.4")
|
|
||||||
val xodusVersion by extra("2.0.1")
|
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
group = "space.kscience"
|
group = "space.kscience"
|
||||||
version = "0.2.0"
|
version = "0.4.0-dev-6"
|
||||||
repositories{
|
repositories{
|
||||||
maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
|
google()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ksciencePublish {
|
ksciencePublish {
|
||||||
pom("https://github.com/SciProgCentre/controls.kt") {
|
pom("https://github.com/SciProgCentre/controls-kt") {
|
||||||
useApache2Licence()
|
useApache2Licence()
|
||||||
useSPCTeam()
|
useSPCTeam()
|
||||||
}
|
}
|
||||||
github("controls.kt", "SciProgCentre")
|
repository("spc","https://maven.sciprog.center/kscience")
|
||||||
space(
|
sonatype("https://oss.sonatype.org")
|
||||||
if (isInDevelopment) {
|
|
||||||
"https://maven.pkg.jetbrains.space/spc/p/sci/dev"
|
|
||||||
} else {
|
|
||||||
"https://maven.pkg.jetbrains.space/spc/p/sci/maven"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
readme.readmeTemplate = file("docs/templates/README-TEMPLATE.md")
|
readme.readmeTemplate = file("docs/templates/README-TEMPLATE.md")
|
21
controls-constructor/README.md
Normal file
21
controls-constructor/README.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Module controls-constructor
|
||||||
|
|
||||||
|
A low-code constructor for composite devices simulation
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
## Artifact:
|
||||||
|
|
||||||
|
The Maven coordinates of this project are `space.kscience:controls-constructor:0.4.0-dev-4`.
|
||||||
|
|
||||||
|
**Gradle Kotlin DSL:**
|
||||||
|
```kotlin
|
||||||
|
repositories {
|
||||||
|
maven("https://repo.kotlin.link")
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("space.kscience:controls-constructor:0.4.0-dev-4")
|
||||||
|
}
|
||||||
|
```
|
31
controls-constructor/build.gradle.kts
Normal file
31
controls-constructor/build.gradle.kts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
plugins {
|
||||||
|
id("space.kscience.gradle.mpp")
|
||||||
|
`maven-publish`
|
||||||
|
}
|
||||||
|
|
||||||
|
description = """
|
||||||
|
A low-code constructor for composite devices simulation
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
kscience{
|
||||||
|
jvm()
|
||||||
|
js()
|
||||||
|
native()
|
||||||
|
wasm()
|
||||||
|
useCoroutines()
|
||||||
|
useSerialization()
|
||||||
|
commonMain {
|
||||||
|
api(projects.controlsCore)
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
|
||||||
|
implementation("org.jetbrains.kotlinx:multik-core:0.2.3")
|
||||||
|
}
|
||||||
|
|
||||||
|
commonTest{
|
||||||
|
implementation(spclibs.logback.classic)
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readme{
|
||||||
|
maturity = space.kscience.gradle.Maturity.PROTOTYPE
|
||||||
|
}
|
@ -0,0 +1,241 @@
|
|||||||
|
package space.kscience.controls.constructor
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
import space.kscience.controls.api.Device
|
||||||
|
import space.kscience.controls.manager.ClockManager
|
||||||
|
import space.kscience.dataforge.context.ContextAware
|
||||||
|
import space.kscience.dataforge.context.request
|
||||||
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A binding that is used to describe device functionality
|
||||||
|
*/
|
||||||
|
public sealed interface ConstructorElement
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A binding that exposes device property as read-only state
|
||||||
|
*/
|
||||||
|
public class PropertyConstructorElement<T>(
|
||||||
|
public val device: Device,
|
||||||
|
public val propertyName: String,
|
||||||
|
public val state: DeviceState<T>,
|
||||||
|
) : ConstructorElement
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A binding for independent state like a timer
|
||||||
|
*/
|
||||||
|
public class StateConstructorElement<T>(
|
||||||
|
public val state: DeviceState<T>,
|
||||||
|
) : ConstructorElement
|
||||||
|
|
||||||
|
public class ConnectionConstrucorElement(
|
||||||
|
public val reads: Collection<DeviceState<*>>,
|
||||||
|
public val writes: Collection<DeviceState<*>>,
|
||||||
|
) : ConstructorElement
|
||||||
|
|
||||||
|
public class ModelConstructorElement(
|
||||||
|
public val model: ModelConstructor,
|
||||||
|
) : ConstructorElement
|
||||||
|
|
||||||
|
|
||||||
|
public interface StateContainer : ContextAware, CoroutineScope {
|
||||||
|
public val constructorElements: Set<ConstructorElement>
|
||||||
|
public fun registerElement(constructorElement: ConstructorElement)
|
||||||
|
public fun unregisterElement(constructorElement: ConstructorElement)
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind an action to a [DeviceState]. [onChange] block is performed on each state change
|
||||||
|
*
|
||||||
|
* Optionally provide [writes] - a set of states that this change affects.
|
||||||
|
*/
|
||||||
|
public fun <T> DeviceState<T>.onNext(
|
||||||
|
writes: Collection<DeviceState<*>> = emptySet(),
|
||||||
|
reads: Collection<DeviceState<*>> = emptySet(),
|
||||||
|
onChange: suspend (T) -> Unit,
|
||||||
|
): Job = valueFlow.onEach(onChange).launchIn(this@StateContainer).also {
|
||||||
|
registerElement(ConnectionConstrucorElement(reads + this, writes))
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun <T> DeviceState<T>.onChange(
|
||||||
|
writes: Collection<DeviceState<*>> = emptySet(),
|
||||||
|
reads: Collection<DeviceState<*>> = emptySet(),
|
||||||
|
onChange: suspend (prev: T, next: T) -> Unit,
|
||||||
|
): Job = valueFlow.runningFold(Pair(value, value)) { pair, next ->
|
||||||
|
Pair(pair.second, next)
|
||||||
|
}.onEach { pair ->
|
||||||
|
if (pair.first != pair.second) {
|
||||||
|
onChange(pair.first, pair.second)
|
||||||
|
}
|
||||||
|
}.launchIn(this@StateContainer).also {
|
||||||
|
registerElement(ConnectionConstrucorElement(reads + this, writes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a [state] in this container. The state is not registered as a device property if [this] is a [DeviceConstructor]
|
||||||
|
*/
|
||||||
|
public fun <T, D : DeviceState<T>> StateContainer.registerState(state: D): D {
|
||||||
|
registerElement(StateConstructorElement(state))
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a register a [MutableDeviceState]
|
||||||
|
*/
|
||||||
|
public fun <T> StateContainer.stateOf(initialValue: T): MutableDeviceState<T> = registerState(
|
||||||
|
MutableDeviceState(initialValue)
|
||||||
|
)
|
||||||
|
|
||||||
|
public fun <T : ModelConstructor> StateContainer.model(model: T): T {
|
||||||
|
registerElement(ModelConstructorElement(model))
|
||||||
|
return model
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and register a timer state.
|
||||||
|
*/
|
||||||
|
public fun StateContainer.timer(tick: Duration): TimerState =
|
||||||
|
registerState(TimerState(context.request(ClockManager), tick))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a new timer and perform [block] on its change
|
||||||
|
*/
|
||||||
|
public fun StateContainer.onTimer(
|
||||||
|
tick: Duration,
|
||||||
|
writes: Collection<DeviceState<*>> = emptySet(),
|
||||||
|
reads: Collection<DeviceState<*>> = emptySet(),
|
||||||
|
block: suspend (prev: Instant, next: Instant) -> Unit,
|
||||||
|
): Job = timer(tick).onChange(writes = writes, reads = reads, onChange = block)
|
||||||
|
|
||||||
|
public enum class DefaultTimer(public val duration: Duration){
|
||||||
|
REALTIME(5.milliseconds),
|
||||||
|
VERY_FAST(10.milliseconds),
|
||||||
|
FAST(20.milliseconds),
|
||||||
|
MEDIUM(50.milliseconds),
|
||||||
|
SLOW(100.milliseconds),
|
||||||
|
VERY_SLOW(500.milliseconds),
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform an action on default timer
|
||||||
|
*/
|
||||||
|
public fun StateContainer.onTimer(
|
||||||
|
defaultTimer: DefaultTimer = DefaultTimer.FAST,
|
||||||
|
writes: Collection<DeviceState<*>> = emptySet(),
|
||||||
|
reads: Collection<DeviceState<*>> = emptySet(),
|
||||||
|
block: suspend (prev: Instant, next: Instant) -> Unit,
|
||||||
|
): Job = timer(defaultTimer.duration).onChange(writes = writes, reads = reads, onChange = block)
|
||||||
|
//TODO implement timer pooling
|
||||||
|
|
||||||
|
public fun <T, R> StateContainer.mapState(
|
||||||
|
origin: DeviceState<T>,
|
||||||
|
transformation: (T) -> R,
|
||||||
|
): DeviceStateWithDependencies<R> = registerState(DeviceState.map(origin, transformation))
|
||||||
|
|
||||||
|
|
||||||
|
public fun <T, R> StateContainer.flowState(
|
||||||
|
origin: DeviceState<T>,
|
||||||
|
initialValue: R,
|
||||||
|
transformation: suspend FlowCollector<R>.(T) -> Unit,
|
||||||
|
): DeviceStateWithDependencies<R> {
|
||||||
|
val state = MutableDeviceState(initialValue)
|
||||||
|
origin.valueFlow.transform(transformation).onEach { state.value = it }.launchIn(this)
|
||||||
|
return registerState(state.withDependencies(setOf(origin)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new state by combining two existing ones
|
||||||
|
*/
|
||||||
|
public fun <T1, T2, R> StateContainer.combineState(
|
||||||
|
first: DeviceState<T1>,
|
||||||
|
second: DeviceState<T2>,
|
||||||
|
transformation: (T1, T2) -> R,
|
||||||
|
): DeviceState<R> = registerState(DeviceState.combine(first, second, transformation))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and start binding between [sourceState] and [targetState]. Changes made to [sourceState] are automatically
|
||||||
|
* transferred onto [targetState], but not vise versa.
|
||||||
|
*
|
||||||
|
* On resulting [Job] cancel the binding is unregistered
|
||||||
|
*/
|
||||||
|
public fun <T> StateContainer.bind(sourceState: DeviceState<T>, targetState: MutableDeviceState<T>): Job {
|
||||||
|
val descriptor = ConnectionConstrucorElement(setOf(sourceState), setOf(targetState))
|
||||||
|
registerElement(descriptor)
|
||||||
|
return sourceState.valueFlow.onEach {
|
||||||
|
targetState.value = it
|
||||||
|
}.launchIn(this).apply {
|
||||||
|
invokeOnCompletion {
|
||||||
|
unregisterElement(descriptor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and start binding between [sourceState] and [targetState]. Changes made to [sourceState] are automatically
|
||||||
|
* transferred onto [targetState] via [transformation], but not vise versa.
|
||||||
|
*
|
||||||
|
* On resulting [Job] cancel the binding is unregistered
|
||||||
|
*/
|
||||||
|
public fun <T, R> StateContainer.transformTo(
|
||||||
|
sourceState: DeviceState<T>,
|
||||||
|
targetState: MutableDeviceState<R>,
|
||||||
|
transformation: suspend (T) -> R,
|
||||||
|
): Job {
|
||||||
|
val descriptor = ConnectionConstrucorElement(setOf(sourceState), setOf(targetState))
|
||||||
|
registerElement(descriptor)
|
||||||
|
return sourceState.valueFlow.onEach {
|
||||||
|
targetState.value = transformation(it)
|
||||||
|
}.launchIn(this).apply {
|
||||||
|
invokeOnCompletion {
|
||||||
|
unregisterElement(descriptor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register [ConstructorElement] that combines values from [sourceState1] and [sourceState2] using [transformation].
|
||||||
|
*
|
||||||
|
* On resulting [Job] cancel the binding is unregistered
|
||||||
|
*/
|
||||||
|
public fun <T1, T2, R> StateContainer.combineTo(
|
||||||
|
sourceState1: DeviceState<T1>,
|
||||||
|
sourceState2: DeviceState<T2>,
|
||||||
|
targetState: MutableDeviceState<R>,
|
||||||
|
transformation: suspend (T1, T2) -> R,
|
||||||
|
): Job {
|
||||||
|
val descriptor = ConnectionConstrucorElement(setOf(sourceState1, sourceState2), setOf(targetState))
|
||||||
|
registerElement(descriptor)
|
||||||
|
return kotlinx.coroutines.flow.combine(sourceState1.valueFlow, sourceState2.valueFlow, transformation).onEach {
|
||||||
|
targetState.value = it
|
||||||
|
}.launchIn(this).apply {
|
||||||
|
invokeOnCompletion {
|
||||||
|
unregisterElement(descriptor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register [ConstructorElement] that combines values from [sourceStates] using [transformation].
|
||||||
|
*
|
||||||
|
* On resulting [Job] cancel the binding is unregistered
|
||||||
|
*/
|
||||||
|
public inline fun <reified T, R> StateContainer.combineTo(
|
||||||
|
sourceStates: Collection<DeviceState<T>>,
|
||||||
|
targetState: MutableDeviceState<R>,
|
||||||
|
noinline transformation: suspend (Array<T>) -> R,
|
||||||
|
): Job {
|
||||||
|
val descriptor = ConnectionConstrucorElement(sourceStates, setOf(targetState))
|
||||||
|
registerElement(descriptor)
|
||||||
|
return kotlinx.coroutines.flow.combine(sourceStates.map { it.valueFlow }, transformation).onEach {
|
||||||
|
targetState.value = it
|
||||||
|
}.launchIn(this).apply {
|
||||||
|
invokeOnCompletion {
|
||||||
|
unregisterElement(descriptor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,150 @@
|
|||||||
|
package space.kscience.controls.constructor
|
||||||
|
|
||||||
|
import space.kscience.controls.api.Device
|
||||||
|
import space.kscience.controls.api.PropertyDescriptor
|
||||||
|
import space.kscience.controls.spec.DevicePropertySpec
|
||||||
|
import space.kscience.dataforge.context.Context
|
||||||
|
import space.kscience.dataforge.context.Factory
|
||||||
|
import space.kscience.dataforge.meta.Meta
|
||||||
|
import space.kscience.dataforge.meta.MetaConverter
|
||||||
|
import space.kscience.dataforge.names.Name
|
||||||
|
import space.kscience.dataforge.names.asName
|
||||||
|
import kotlin.properties.PropertyDelegateProvider
|
||||||
|
import kotlin.properties.ReadOnlyProperty
|
||||||
|
import kotlin.reflect.KProperty
|
||||||
|
import kotlin.time.Duration
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A base for strongly typed device constructor block. Has additional delegates for type-safe devices
|
||||||
|
*/
|
||||||
|
public abstract class DeviceConstructor(
|
||||||
|
context: Context,
|
||||||
|
meta: Meta = Meta.EMPTY,
|
||||||
|
) : DeviceGroup(context, meta), StateContainer {
|
||||||
|
private val _constructorElements: MutableSet<ConstructorElement> = mutableSetOf()
|
||||||
|
override val constructorElements: Set<ConstructorElement> get() = _constructorElements
|
||||||
|
|
||||||
|
override fun registerElement(constructorElement: ConstructorElement) {
|
||||||
|
_constructorElements.add(constructorElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun unregisterElement(constructorElement: ConstructorElement) {
|
||||||
|
_constructorElements.remove(constructorElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun <T, S: DeviceState<T>> registerProperty(
|
||||||
|
converter: MetaConverter<T>,
|
||||||
|
descriptor: PropertyDescriptor,
|
||||||
|
state: S,
|
||||||
|
): S {
|
||||||
|
val res = super.registerProperty(converter, descriptor, state)
|
||||||
|
registerElement(PropertyConstructorElement(this, descriptor.name, state))
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a device, provided by a given [factory] and
|
||||||
|
*/
|
||||||
|
public fun <D : Device> DeviceConstructor.device(
|
||||||
|
factory: Factory<D>,
|
||||||
|
meta: Meta? = null,
|
||||||
|
nameOverride: Name? = null,
|
||||||
|
metaLocation: Name? = null,
|
||||||
|
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, D>> =
|
||||||
|
PropertyDelegateProvider { _: DeviceConstructor, property: KProperty<*> ->
|
||||||
|
val name = nameOverride ?: property.name.asName()
|
||||||
|
val device = install(name, factory, meta, metaLocation ?: name)
|
||||||
|
ReadOnlyProperty { _: DeviceConstructor, _ ->
|
||||||
|
device
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun <D : Device> DeviceConstructor.device(
|
||||||
|
device: D,
|
||||||
|
nameOverride: Name? = null,
|
||||||
|
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, D>> =
|
||||||
|
PropertyDelegateProvider { _: DeviceConstructor, property: KProperty<*> ->
|
||||||
|
val name = nameOverride ?: property.name.asName()
|
||||||
|
install(name, device)
|
||||||
|
ReadOnlyProperty { _: DeviceConstructor, _ ->
|
||||||
|
device
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a property and provide a direct reader for it
|
||||||
|
*/
|
||||||
|
public fun <T, S : DeviceState<T>> DeviceConstructor.property(
|
||||||
|
converter: MetaConverter<T>,
|
||||||
|
state: S,
|
||||||
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
|
nameOverride: String? = null,
|
||||||
|
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, S>> =
|
||||||
|
PropertyDelegateProvider { _: DeviceConstructor, property ->
|
||||||
|
val name = nameOverride ?: property.name
|
||||||
|
val descriptor = PropertyDescriptor(name).apply(descriptorBuilder)
|
||||||
|
registerProperty(converter, descriptor, state)
|
||||||
|
ReadOnlyProperty { _: DeviceConstructor, _ ->
|
||||||
|
state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register external state as a property
|
||||||
|
*/
|
||||||
|
public fun <T : Any> DeviceConstructor.property(
|
||||||
|
metaConverter: MetaConverter<T>,
|
||||||
|
reader: suspend () -> T,
|
||||||
|
readInterval: Duration,
|
||||||
|
initialState: T,
|
||||||
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
|
nameOverride: String? = null,
|
||||||
|
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, DeviceState<T>>> = property(
|
||||||
|
metaConverter,
|
||||||
|
DeviceState.external(this, readInterval, initialState, reader),
|
||||||
|
descriptorBuilder,
|
||||||
|
nameOverride,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and register a mutable external state as a property
|
||||||
|
*/
|
||||||
|
public fun <T : Any> DeviceConstructor.mutableProperty(
|
||||||
|
metaConverter: MetaConverter<T>,
|
||||||
|
reader: suspend () -> T,
|
||||||
|
writer: suspend (T) -> Unit,
|
||||||
|
readInterval: Duration,
|
||||||
|
initialState: T,
|
||||||
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
|
nameOverride: String? = null,
|
||||||
|
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = property(
|
||||||
|
metaConverter,
|
||||||
|
DeviceState.external(this, readInterval, initialState, reader, writer),
|
||||||
|
descriptorBuilder,
|
||||||
|
nameOverride,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and register a virtual mutable property with optional [callback]
|
||||||
|
*/
|
||||||
|
public fun <T> DeviceConstructor.virtualProperty(
|
||||||
|
metaConverter: MetaConverter<T>,
|
||||||
|
initialState: T,
|
||||||
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
|
nameOverride: String? = null,
|
||||||
|
callback: (T) -> Unit = {},
|
||||||
|
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = property(
|
||||||
|
metaConverter,
|
||||||
|
MutableDeviceState(initialState, callback),
|
||||||
|
descriptorBuilder,
|
||||||
|
nameOverride,
|
||||||
|
)
|
||||||
|
|
||||||
|
public fun <T, S : DeviceState<T>> DeviceConstructor.registerAsProperty(
|
||||||
|
spec: DevicePropertySpec<*, T>,
|
||||||
|
state: S,
|
||||||
|
): S {
|
||||||
|
registerProperty(spec.converter, spec.descriptor, state)
|
||||||
|
return state
|
||||||
|
}
|
@ -0,0 +1,318 @@
|
|||||||
|
package space.kscience.controls.constructor
|
||||||
|
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
import space.kscience.controls.api.*
|
||||||
|
import space.kscience.controls.api.LifecycleState.*
|
||||||
|
import space.kscience.controls.manager.DeviceManager
|
||||||
|
import space.kscience.controls.manager.install
|
||||||
|
import space.kscience.controls.spec.DevicePropertySpec
|
||||||
|
import space.kscience.dataforge.context.*
|
||||||
|
import space.kscience.dataforge.meta.Laminate
|
||||||
|
import space.kscience.dataforge.meta.Meta
|
||||||
|
import space.kscience.dataforge.meta.MetaConverter
|
||||||
|
import space.kscience.dataforge.meta.MutableMeta
|
||||||
|
import space.kscience.dataforge.misc.DFExperimental
|
||||||
|
import space.kscience.dataforge.names.Name
|
||||||
|
import space.kscience.dataforge.names.get
|
||||||
|
import space.kscience.dataforge.names.parseAsName
|
||||||
|
import kotlin.collections.set
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A mutable group of devices and properties to be used for lightweight design and simulations.
|
||||||
|
*/
|
||||||
|
public open class DeviceGroup(
|
||||||
|
final override val context: Context,
|
||||||
|
override val meta: Meta,
|
||||||
|
) : DeviceHub, CachingDevice {
|
||||||
|
|
||||||
|
private class Property<T>(
|
||||||
|
val state: DeviceState<T>,
|
||||||
|
val converter: MetaConverter<T>,
|
||||||
|
val descriptor: PropertyDescriptor,
|
||||||
|
) {
|
||||||
|
val valueAsMeta get() = converter.convert(state.value)
|
||||||
|
|
||||||
|
fun setMeta(meta: Meta) {
|
||||||
|
check(state is MutableDeviceState) { "Can't write to read-only property" }
|
||||||
|
|
||||||
|
state.value = converter.read(meta)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Action<T, R>(
|
||||||
|
val inputConverter: MetaConverter<T>,
|
||||||
|
val outputConverter: MetaConverter<R>,
|
||||||
|
val descriptor: ActionDescriptor,
|
||||||
|
val action: suspend (T) -> R,
|
||||||
|
) {
|
||||||
|
suspend operator fun invoke(argument: Meta?): Meta? = argument?.let { inputConverter.readOrNull(it) }
|
||||||
|
?.let { action(it)?.let { outputConverter.convert(it) } }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private val sharedMessageFlow = MutableSharedFlow<DeviceMessage>()
|
||||||
|
|
||||||
|
override val messageFlow: Flow<DeviceMessage>
|
||||||
|
get() = sharedMessageFlow
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
override val coroutineContext: CoroutineContext = context.newCoroutineContext(
|
||||||
|
SupervisorJob(context.coroutineContext[Job]) +
|
||||||
|
CoroutineName("Device $id") +
|
||||||
|
CoroutineExceptionHandler { _, throwable ->
|
||||||
|
context.launch {
|
||||||
|
sharedMessageFlow.emit(
|
||||||
|
DeviceErrorMessage(
|
||||||
|
errorMessage = throwable.message,
|
||||||
|
errorType = throwable::class.simpleName,
|
||||||
|
errorStackTrace = throwable.stackTraceToString()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
logger.error(throwable) { "Exception in device $id" }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
private val _devices = hashMapOf<Name, Device>()
|
||||||
|
|
||||||
|
override val devices: Map<Name, Device> = _devices
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register and initialize (synchronize child's lifecycle state with group state) a new device in this group
|
||||||
|
*/
|
||||||
|
@OptIn(DFExperimental::class)
|
||||||
|
public open fun <D : Device> install(token: Name, device: D): D {
|
||||||
|
require(_devices[token] == null) { "A child device with name $token already exists" }
|
||||||
|
//start the child device if needed
|
||||||
|
if (lifecycleState == STARTED || lifecycleState == STARTING) launch { device.start() }
|
||||||
|
_devices[token] = device
|
||||||
|
return device
|
||||||
|
}
|
||||||
|
|
||||||
|
private val properties: MutableMap<Name, Property<*>> = hashMapOf()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a new property based on [DeviceState]. Properties could be modified dynamically
|
||||||
|
*/
|
||||||
|
public open fun <T, S : DeviceState<T>> registerProperty(
|
||||||
|
converter: MetaConverter<T>,
|
||||||
|
descriptor: PropertyDescriptor,
|
||||||
|
state: S,
|
||||||
|
): S {
|
||||||
|
val name = descriptor.name.parseAsName()
|
||||||
|
require(properties[name] == null) { "Can't add property with name $name. It already exists." }
|
||||||
|
properties[name] = Property(state, converter, descriptor)
|
||||||
|
state.valueFlow.map(converter::convert).onEach {
|
||||||
|
sharedMessageFlow.emit(
|
||||||
|
PropertyChangedMessage(
|
||||||
|
descriptor.name,
|
||||||
|
it
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}.launchIn(this)
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
private val actions: MutableMap<Name, Action<*, *>> = hashMapOf()
|
||||||
|
|
||||||
|
public fun <T, R> registerAction(
|
||||||
|
inputConverter: MetaConverter<T>,
|
||||||
|
outputConverter: MetaConverter<R>,
|
||||||
|
descriptor: ActionDescriptor,
|
||||||
|
action: suspend (T) -> R,
|
||||||
|
): suspend (T) -> R {
|
||||||
|
val name = descriptor.name.parseAsName()
|
||||||
|
require(actions[name] == null) { "Can't add action with name $name. It already exists." }
|
||||||
|
actions[name] = Action(
|
||||||
|
inputConverter = inputConverter,
|
||||||
|
outputConverter = outputConverter,
|
||||||
|
descriptor = descriptor,
|
||||||
|
action = action
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
action(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val propertyDescriptors: Collection<PropertyDescriptor>
|
||||||
|
get() = properties.values.map { it.descriptor }
|
||||||
|
|
||||||
|
override val actionDescriptors: Collection<ActionDescriptor>
|
||||||
|
get() = actions.values.map { it.descriptor }
|
||||||
|
|
||||||
|
override suspend fun readProperty(propertyName: String): Meta =
|
||||||
|
properties[propertyName.parseAsName()]?.valueAsMeta
|
||||||
|
?: error("Property with name $propertyName not found")
|
||||||
|
|
||||||
|
override fun getProperty(propertyName: String): Meta? = properties[propertyName.parseAsName()]?.valueAsMeta
|
||||||
|
|
||||||
|
override suspend fun invalidate(propertyName: String) {
|
||||||
|
//does nothing for this implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun writeProperty(propertyName: String, value: Meta) {
|
||||||
|
val property = properties[propertyName.parseAsName()] ?: error("Property with name $propertyName not found")
|
||||||
|
property.setMeta(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override suspend fun execute(actionName: String, argument: Meta?): Meta? {
|
||||||
|
val action: Action<*, *> = actions[actionName] ?: error("Action with name $actionName not found")
|
||||||
|
return action(argument)
|
||||||
|
}
|
||||||
|
|
||||||
|
final override var lifecycleState: LifecycleState = LifecycleState.STOPPED
|
||||||
|
private set
|
||||||
|
|
||||||
|
|
||||||
|
private suspend fun setLifecycleState(lifecycleState: LifecycleState) {
|
||||||
|
this.lifecycleState = lifecycleState
|
||||||
|
sharedMessageFlow.emit(
|
||||||
|
DeviceLifeCycleMessage(lifecycleState)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override suspend fun start() {
|
||||||
|
setLifecycleState(STARTING)
|
||||||
|
super.start()
|
||||||
|
devices.values.forEach {
|
||||||
|
it.start()
|
||||||
|
}
|
||||||
|
setLifecycleState(STARTED)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun stop() {
|
||||||
|
devices.values.forEach {
|
||||||
|
it.stop()
|
||||||
|
}
|
||||||
|
setLifecycleState(STOPPED)
|
||||||
|
super.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
public companion object {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun <T> DeviceGroup.registerAsProperty(propertySpec: DevicePropertySpec<*, T>, state: DeviceState<T>) {
|
||||||
|
registerProperty(propertySpec.converter, propertySpec.descriptor, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun DeviceManager.registerDeviceGroup(
|
||||||
|
name: String = "@group",
|
||||||
|
meta: Meta = Meta.EMPTY,
|
||||||
|
block: DeviceGroup.() -> Unit,
|
||||||
|
): DeviceGroup {
|
||||||
|
val group = DeviceGroup(context, meta).apply(block)
|
||||||
|
install(name, group)
|
||||||
|
return group
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun Context.registerDeviceGroup(
|
||||||
|
name: String = "@group",
|
||||||
|
meta: Meta = Meta.EMPTY,
|
||||||
|
block: DeviceGroup.() -> Unit,
|
||||||
|
): DeviceGroup = request(DeviceManager).registerDeviceGroup(name, meta, block)
|
||||||
|
|
||||||
|
///**
|
||||||
|
// * Register a device at given [path] path
|
||||||
|
// */
|
||||||
|
//public fun <D : Device> DeviceGroup.install(path: Path, device: D): D {
|
||||||
|
// return when (path.length) {
|
||||||
|
// 0 -> error("Can't use empty path for a child device")
|
||||||
|
// 1 -> install(path.first().name, device)
|
||||||
|
// else -> getOrCreateGroup(path.cutLast()).install(path.tokens.last(), device)
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
public fun <D : Device> DeviceGroup.install(name: String, device: D): D = install(name.parseAsName(), device)
|
||||||
|
|
||||||
|
public fun <D : Device> DeviceGroup.install(device: D): D = install(device.id, device)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a device creating intermediate groups if necessary. If device with given [name] already exists, throws an error.
|
||||||
|
* @param name the name of the device in the group
|
||||||
|
* @param factory a factory used to create a device
|
||||||
|
* @param deviceMeta meta override for this specific device
|
||||||
|
* @param metaLocation location of the template meta in parent group meta
|
||||||
|
*/
|
||||||
|
public fun <D : Device> DeviceGroup.install(
|
||||||
|
name: Name,
|
||||||
|
factory: Factory<D>,
|
||||||
|
deviceMeta: Meta? = null,
|
||||||
|
metaLocation: Name = name,
|
||||||
|
): D {
|
||||||
|
val newDevice = factory.build(context, Laminate(deviceMeta, meta[metaLocation]))
|
||||||
|
install(name, newDevice)
|
||||||
|
return newDevice
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun <D : Device> DeviceGroup.install(
|
||||||
|
name: String,
|
||||||
|
factory: Factory<D>,
|
||||||
|
metaLocation: Name = name.parseAsName(),
|
||||||
|
metaBuilder: (MutableMeta.() -> Unit)? = null,
|
||||||
|
): D = install(name.parseAsName(), factory, metaBuilder?.let { Meta(it) }, metaLocation)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create or edit a group with a given [name].
|
||||||
|
*/
|
||||||
|
public fun DeviceGroup.registerDeviceGroup(name: Name, block: DeviceGroup.() -> Unit): DeviceGroup =
|
||||||
|
install(name, DeviceGroup(context, meta).apply(block))
|
||||||
|
|
||||||
|
public fun DeviceGroup.registerDeviceGroup(name: String, block: DeviceGroup.() -> Unit): DeviceGroup =
|
||||||
|
registerDeviceGroup(name.parseAsName(), block)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register read-only property based on [state]
|
||||||
|
*/
|
||||||
|
public fun <T : Any> DeviceGroup.registerAsProperty(
|
||||||
|
name: String,
|
||||||
|
converter: MetaConverter<T>,
|
||||||
|
state: DeviceState<T>,
|
||||||
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
|
) {
|
||||||
|
registerProperty(
|
||||||
|
converter,
|
||||||
|
PropertyDescriptor(name).apply(descriptorBuilder),
|
||||||
|
state
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a mutable property based on mutable [state]
|
||||||
|
*/
|
||||||
|
public fun <T : Any> DeviceGroup.registerMutableProperty(
|
||||||
|
name: String,
|
||||||
|
converter: MetaConverter<T>,
|
||||||
|
state: MutableDeviceState<T>,
|
||||||
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
|
) {
|
||||||
|
registerProperty(
|
||||||
|
converter,
|
||||||
|
PropertyDescriptor(name).apply(descriptorBuilder),
|
||||||
|
state
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new virtual mutable state and a property based on it.
|
||||||
|
* @return the mutable state used in property
|
||||||
|
*/
|
||||||
|
public fun <T : Any> DeviceGroup.registerVirtualProperty(
|
||||||
|
name: String,
|
||||||
|
initialValue: T,
|
||||||
|
converter: MetaConverter<T>,
|
||||||
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
|
callback: (T) -> Unit = {},
|
||||||
|
): MutableDeviceState<T> {
|
||||||
|
val state = MutableDeviceState<T>(initialValue, callback)
|
||||||
|
registerMutableProperty(name, converter, state, descriptorBuilder)
|
||||||
|
return state
|
||||||
|
}
|
@ -0,0 +1,103 @@
|
|||||||
|
package space.kscience.controls.constructor
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import space.kscience.controls.constructor.units.NumericalValue
|
||||||
|
import space.kscience.controls.constructor.units.UnitsOfMeasurement
|
||||||
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An observable state of a device
|
||||||
|
*/
|
||||||
|
public interface DeviceState<out T> {
|
||||||
|
public val value: T
|
||||||
|
|
||||||
|
public val valueFlow: Flow<T>
|
||||||
|
|
||||||
|
override fun toString(): String
|
||||||
|
|
||||||
|
public companion object
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public operator fun <T> DeviceState<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect values in a given [scope]
|
||||||
|
*/
|
||||||
|
public fun <T> DeviceState<T>.collectValuesIn(scope: CoroutineScope, block: suspend (T) -> Unit): Job =
|
||||||
|
valueFlow.onEach(block).launchIn(scope)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A mutable state of a device
|
||||||
|
*/
|
||||||
|
public interface MutableDeviceState<T> : DeviceState<T> {
|
||||||
|
override var value: T
|
||||||
|
}
|
||||||
|
|
||||||
|
public operator fun <T> MutableDeviceState<T>.setValue(thisRef: Any?, property: KProperty<*>, value: T) {
|
||||||
|
this.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Device state with a value that depends on other device states
|
||||||
|
*/
|
||||||
|
public interface DeviceStateWithDependencies<T> : DeviceState<T> {
|
||||||
|
public val dependencies: Collection<DeviceState<*>>
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun <T> DeviceState<T>.withDependencies(
|
||||||
|
dependencies: Collection<DeviceState<*>>,
|
||||||
|
): DeviceStateWithDependencies<T> = object : DeviceStateWithDependencies<T>, DeviceState<T> by this {
|
||||||
|
override val dependencies: Collection<DeviceState<*>> = dependencies
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new read-only [DeviceState] that mirrors receiver state by mapping the value with [mapper].
|
||||||
|
*/
|
||||||
|
public fun <T, R> DeviceState.Companion.map(
|
||||||
|
state: DeviceState<T>,
|
||||||
|
mapper: (T) -> R,
|
||||||
|
): DeviceStateWithDependencies<R> = object : DeviceStateWithDependencies<R> {
|
||||||
|
override val dependencies = listOf(state)
|
||||||
|
|
||||||
|
override val value: R get() = mapper(state.value)
|
||||||
|
|
||||||
|
override val valueFlow: Flow<R> = state.valueFlow.map(mapper)
|
||||||
|
|
||||||
|
override fun toString(): String = "DeviceState.map(state=${state})"
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun <T, R> DeviceState<T>.map(mapper: (T) -> R): DeviceStateWithDependencies<R> = DeviceState.map(this, mapper)
|
||||||
|
|
||||||
|
public fun DeviceState<NumericalValue<out UnitsOfMeasurement>>.values(): DeviceState<Double> =
|
||||||
|
object : DeviceState<Double> {
|
||||||
|
override val value: Double
|
||||||
|
get() = this@values.value.value
|
||||||
|
|
||||||
|
override val valueFlow: Flow<Double>
|
||||||
|
get() = this@values.valueFlow.map { it.value }
|
||||||
|
|
||||||
|
override fun toString(): String = this@values.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combine two device states into one read-only [DeviceState]. Only the latest value of each state is used.
|
||||||
|
*/
|
||||||
|
public fun <T1, T2, R> DeviceState.Companion.combine(
|
||||||
|
state1: DeviceState<T1>,
|
||||||
|
state2: DeviceState<T2>,
|
||||||
|
mapper: (T1, T2) -> R,
|
||||||
|
): DeviceStateWithDependencies<R> = object : DeviceStateWithDependencies<R> {
|
||||||
|
override val dependencies = listOf(state1, state2)
|
||||||
|
|
||||||
|
override val value: R get() = mapper(state1.value, state2.value)
|
||||||
|
|
||||||
|
override val valueFlow: Flow<R> = kotlinx.coroutines.flow.combine(state1.valueFlow, state2.valueFlow, mapper)
|
||||||
|
|
||||||
|
override fun toString(): String = "DeviceState.combine(state1=$state1, state2=$state2)"
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
package space.kscience.controls.constructor
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.newCoroutineContext
|
||||||
|
import space.kscience.dataforge.context.Context
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
public abstract class ModelConstructor(
|
||||||
|
final override val context: Context,
|
||||||
|
vararg dependencies: DeviceState<*>,
|
||||||
|
) : StateContainer, CoroutineScope {
|
||||||
|
|
||||||
|
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
|
||||||
|
override val coroutineContext: CoroutineContext = context.newCoroutineContext(SupervisorJob())
|
||||||
|
|
||||||
|
|
||||||
|
private val _constructorElements: MutableSet<ConstructorElement> = mutableSetOf<ConstructorElement>().apply {
|
||||||
|
dependencies.forEach {
|
||||||
|
add(StateConstructorElement(it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val constructorElements: Set<ConstructorElement> get() = _constructorElements
|
||||||
|
|
||||||
|
override fun registerElement(constructorElement: ConstructorElement) {
|
||||||
|
_constructorElements.add(constructorElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun unregisterElement(constructorElement: ConstructorElement) {
|
||||||
|
_constructorElements.remove(constructorElement)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
package space.kscience.controls.constructor
|
||||||
|
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
import space.kscience.controls.manager.ClockManager
|
||||||
|
import kotlin.time.Duration
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A dedicated [DeviceState] that operates with time.
|
||||||
|
* The state changes with [tick] interval and always shows the time of the last update.
|
||||||
|
*
|
||||||
|
* Both [tick] and current time are computed by [clockManager] enabling time manipulation.
|
||||||
|
*
|
||||||
|
* The timer runs indefinitely until the parent context is closed
|
||||||
|
*/
|
||||||
|
public class TimerState(
|
||||||
|
public val clockManager: ClockManager,
|
||||||
|
public val tick: Duration,
|
||||||
|
) : DeviceState<Instant> {
|
||||||
|
|
||||||
|
private val clock = MutableStateFlow(clockManager.clock.now())
|
||||||
|
|
||||||
|
private val updateJob = clockManager.context.launch(clockManager.asDispatcher()) {
|
||||||
|
while (isActive) {
|
||||||
|
delay(tick)
|
||||||
|
clock.value = clockManager.clock.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val valueFlow: Flow<Instant> get() = clock
|
||||||
|
|
||||||
|
override val value: Instant get() = clock.value
|
||||||
|
|
||||||
|
override fun toString(): String = "TimerState(tick=$tick)"
|
||||||
|
}
|
@ -0,0 +1,103 @@
|
|||||||
|
package space.kscience.controls.constructor
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import space.kscience.controls.api.Device
|
||||||
|
import space.kscience.controls.api.PropertyChangedMessage
|
||||||
|
import space.kscience.controls.api.id
|
||||||
|
import space.kscience.controls.spec.DevicePropertySpec
|
||||||
|
import space.kscience.controls.spec.MutableDevicePropertySpec
|
||||||
|
import space.kscience.controls.spec.name
|
||||||
|
import space.kscience.dataforge.meta.MetaConverter
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A copy-free [DeviceState] bound to a device property
|
||||||
|
*/
|
||||||
|
private open class BoundDeviceState<T>(
|
||||||
|
val converter: MetaConverter<T>,
|
||||||
|
val device: Device,
|
||||||
|
val propertyName: String,
|
||||||
|
initialValue: T,
|
||||||
|
) : DeviceState<T> {
|
||||||
|
|
||||||
|
override val valueFlow: StateFlow<T> = device.messageFlow.filterIsInstance<PropertyChangedMessage>().filter {
|
||||||
|
it.property == propertyName
|
||||||
|
}.mapNotNull {
|
||||||
|
converter.read(it.value)
|
||||||
|
}.stateIn(device.context, SharingStarted.Eagerly, initialValue)
|
||||||
|
|
||||||
|
override val value: T get() = valueFlow.value
|
||||||
|
override fun toString(): String =
|
||||||
|
"BoundDeviceState(converter=$converter, device=${device.id}, propertyName='$propertyName')"
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun <T> Device.propertyAsState(
|
||||||
|
propertyName: String,
|
||||||
|
metaConverter: MetaConverter<T>,
|
||||||
|
initialValue: T,
|
||||||
|
): DeviceState<T> = BoundDeviceState(metaConverter, this, propertyName, initialValue)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind a read-only [DeviceState] to a [Device] property
|
||||||
|
*/
|
||||||
|
public suspend fun <T> Device.propertyAsState(
|
||||||
|
propertyName: String,
|
||||||
|
metaConverter: MetaConverter<T>,
|
||||||
|
): DeviceState<T> = propertyAsState(
|
||||||
|
propertyName,
|
||||||
|
metaConverter,
|
||||||
|
metaConverter.readOrNull(readProperty(propertyName)) ?: error("Conversion of property failed")
|
||||||
|
)
|
||||||
|
|
||||||
|
public suspend fun <D : Device, T> D.propertyAsState(
|
||||||
|
propertySpec: DevicePropertySpec<D, T>,
|
||||||
|
): DeviceState<T> = propertyAsState(propertySpec.name, propertySpec.converter)
|
||||||
|
|
||||||
|
public fun <D : Device, T> D.propertyAsState(
|
||||||
|
propertySpec: DevicePropertySpec<D, T>,
|
||||||
|
initialValue: T,
|
||||||
|
): DeviceState<T> = propertyAsState(propertySpec.name, propertySpec.converter, initialValue)
|
||||||
|
|
||||||
|
|
||||||
|
private class MutableBoundDeviceState<T>(
|
||||||
|
converter: MetaConverter<T>,
|
||||||
|
device: Device,
|
||||||
|
propertyName: String,
|
||||||
|
initialValue: T,
|
||||||
|
) : BoundDeviceState<T>(converter, device, propertyName, initialValue), MutableDeviceState<T> {
|
||||||
|
|
||||||
|
override var value: T
|
||||||
|
get() = valueFlow.value
|
||||||
|
set(newValue) {
|
||||||
|
device.launch {
|
||||||
|
device.writeProperty(propertyName, converter.convert(newValue))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun <T> Device.mutablePropertyAsState(
|
||||||
|
propertyName: String,
|
||||||
|
metaConverter: MetaConverter<T>,
|
||||||
|
initialValue: T,
|
||||||
|
): MutableDeviceState<T> = MutableBoundDeviceState(metaConverter, this, propertyName, initialValue)
|
||||||
|
|
||||||
|
public suspend fun <T> Device.mutablePropertyAsState(
|
||||||
|
propertyName: String,
|
||||||
|
metaConverter: MetaConverter<T>,
|
||||||
|
): MutableDeviceState<T> {
|
||||||
|
val initialValue = metaConverter.readOrNull(readProperty(propertyName)) ?: error("Conversion of property failed")
|
||||||
|
return mutablePropertyAsState(propertyName, metaConverter, initialValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
public suspend fun <D : Device, T> D.propertyAsState(
|
||||||
|
propertySpec: MutableDevicePropertySpec<D, T>,
|
||||||
|
): MutableDeviceState<T> = mutablePropertyAsState(propertySpec.name, propertySpec.converter)
|
||||||
|
|
||||||
|
public fun <D : Device, T> D.propertyAsState(
|
||||||
|
propertySpec: MutableDevicePropertySpec<D, T>,
|
||||||
|
initialValue: T,
|
||||||
|
): MutableDeviceState<T> = mutablePropertyAsState(propertySpec.name, propertySpec.converter, initialValue)
|
||||||
|
|
@ -0,0 +1,19 @@
|
|||||||
|
package space.kscience.controls.constructor.devices
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.DeviceConstructor
|
||||||
|
import space.kscience.controls.constructor.MutableDeviceState
|
||||||
|
import space.kscience.controls.constructor.property
|
||||||
|
import space.kscience.controls.constructor.units.NewtonsMeters
|
||||||
|
import space.kscience.controls.constructor.units.NumericalValue
|
||||||
|
import space.kscience.controls.constructor.units.numerical
|
||||||
|
import space.kscience.dataforge.context.Context
|
||||||
|
import space.kscience.dataforge.meta.MetaConverter
|
||||||
|
|
||||||
|
//TODO use current as input
|
||||||
|
|
||||||
|
public class Drive(
|
||||||
|
context: Context,
|
||||||
|
force: MutableDeviceState<NumericalValue<NewtonsMeters>> = MutableDeviceState(NumericalValue(0)),
|
||||||
|
) : DeviceConstructor(context) {
|
||||||
|
public val force: MutableDeviceState<NumericalValue<NewtonsMeters>> by property(MetaConverter.numerical(), force)
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
package space.kscience.controls.constructor.devices
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.DeviceConstructor
|
||||||
|
import space.kscience.controls.constructor.DeviceState
|
||||||
|
import space.kscience.controls.constructor.property
|
||||||
|
import space.kscience.controls.constructor.units.Degrees
|
||||||
|
import space.kscience.controls.constructor.units.NumericalValue
|
||||||
|
import space.kscience.controls.constructor.units.numerical
|
||||||
|
import space.kscience.dataforge.context.Context
|
||||||
|
import space.kscience.dataforge.meta.MetaConverter
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An encoder that can read an angle
|
||||||
|
*/
|
||||||
|
public class EncoderDevice(
|
||||||
|
context: Context,
|
||||||
|
position: DeviceState<NumericalValue<Degrees>>
|
||||||
|
) : DeviceConstructor(context) {
|
||||||
|
public val position: DeviceState<NumericalValue<Degrees>> by property(MetaConverter.numerical<Degrees>(), position)
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
package space.kscience.controls.constructor.devices
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.DeviceConstructor
|
||||||
|
import space.kscience.controls.constructor.DeviceState
|
||||||
|
import space.kscience.controls.constructor.map
|
||||||
|
import space.kscience.controls.constructor.registerAsProperty
|
||||||
|
import space.kscience.controls.constructor.units.Direction
|
||||||
|
import space.kscience.controls.constructor.units.NumericalValue
|
||||||
|
import space.kscience.controls.constructor.units.UnitsOfMeasurement
|
||||||
|
import space.kscience.controls.spec.DevicePropertySpec
|
||||||
|
import space.kscience.controls.spec.DeviceSpec
|
||||||
|
import space.kscience.controls.spec.booleanProperty
|
||||||
|
import space.kscience.dataforge.context.Context
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A device that detects if a motor hits the end of its range
|
||||||
|
*/
|
||||||
|
public class LimitSwitch(
|
||||||
|
context: Context,
|
||||||
|
locked: DeviceState<Boolean>,
|
||||||
|
) : DeviceConstructor(context) {
|
||||||
|
|
||||||
|
public val locked: DeviceState<Boolean> = registerAsProperty(LimitSwitch.locked, locked)
|
||||||
|
|
||||||
|
public companion object : DeviceSpec<LimitSwitch>() {
|
||||||
|
public val locked: DevicePropertySpec<LimitSwitch, Boolean> by booleanProperty { locked.value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun <U : UnitsOfMeasurement, T : NumericalValue<U>> LimitSwitch(
|
||||||
|
context: Context,
|
||||||
|
limit: T,
|
||||||
|
boundary: Direction,
|
||||||
|
position: DeviceState<T>,
|
||||||
|
): LimitSwitch = LimitSwitch(
|
||||||
|
context,
|
||||||
|
DeviceState.map(position) {
|
||||||
|
when (boundary) {
|
||||||
|
Direction.UP -> it >= limit
|
||||||
|
Direction.DOWN -> it <= limit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
@ -0,0 +1,38 @@
|
|||||||
|
package space.kscience.controls.constructor.devices
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.*
|
||||||
|
import space.kscience.controls.constructor.models.PidParameters
|
||||||
|
import space.kscience.controls.constructor.models.PidRegulator
|
||||||
|
import space.kscience.controls.constructor.units.Meters
|
||||||
|
import space.kscience.controls.constructor.units.NewtonsMeters
|
||||||
|
import space.kscience.controls.constructor.units.NumericalValue
|
||||||
|
import space.kscience.controls.constructor.units.numerical
|
||||||
|
import space.kscience.dataforge.context.Context
|
||||||
|
import space.kscience.dataforge.meta.Meta
|
||||||
|
import space.kscience.dataforge.meta.MetaConverter
|
||||||
|
|
||||||
|
public class LinearDrive(
|
||||||
|
drive: Drive,
|
||||||
|
start: LimitSwitch,
|
||||||
|
end: LimitSwitch,
|
||||||
|
position: DeviceState<NumericalValue<Meters>>,
|
||||||
|
pidParameters: PidParameters,
|
||||||
|
context: Context = drive.context,
|
||||||
|
meta: Meta = Meta.EMPTY,
|
||||||
|
) : DeviceConstructor(context, meta) {
|
||||||
|
|
||||||
|
public val position: DeviceState<NumericalValue<Meters>> by property(MetaConverter.numerical(), position)
|
||||||
|
|
||||||
|
public val drive: Drive by device(drive)
|
||||||
|
public val pid: PidRegulator<Meters, NewtonsMeters> = model(
|
||||||
|
PidRegulator(
|
||||||
|
context = context,
|
||||||
|
position = position,
|
||||||
|
output = drive.force,
|
||||||
|
pidParameters = pidParameters
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
public val startLimit: LimitSwitch by device(start)
|
||||||
|
public val endLimit: LimitSwitch by device(end)
|
||||||
|
}
|
@ -0,0 +1,65 @@
|
|||||||
|
package space.kscience.controls.constructor.devices
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.*
|
||||||
|
import space.kscience.controls.constructor.units.Degrees
|
||||||
|
import space.kscience.controls.constructor.units.NumericalValue
|
||||||
|
import space.kscience.controls.constructor.units.plus
|
||||||
|
import space.kscience.controls.constructor.units.times
|
||||||
|
import space.kscience.dataforge.context.Context
|
||||||
|
import space.kscience.dataforge.meta.MetaConverter
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
import kotlin.math.roundToLong
|
||||||
|
import kotlin.time.DurationUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A step drive
|
||||||
|
*
|
||||||
|
* @param ticksPerSecond ticks per second
|
||||||
|
* @param target target ticks state
|
||||||
|
* @param writeTicks a hardware callback
|
||||||
|
*/
|
||||||
|
public class StepDrive(
|
||||||
|
context: Context,
|
||||||
|
ticksPerSecond: Double,
|
||||||
|
position: MutableDeviceState<Long> = MutableDeviceState(0),
|
||||||
|
private val writeTicks: suspend (ticks: Long, speed: Double) -> Unit = { _, _ -> },
|
||||||
|
) : DeviceConstructor(context) {
|
||||||
|
|
||||||
|
public val target: MutableDeviceState<Long> by property(
|
||||||
|
MetaConverter.long,
|
||||||
|
MutableDeviceState<Long>(position.value)
|
||||||
|
)
|
||||||
|
|
||||||
|
public val speed: MutableDeviceState<Double> by property(
|
||||||
|
MetaConverter.double,
|
||||||
|
MutableDeviceState<Double>(ticksPerSecond)
|
||||||
|
)
|
||||||
|
|
||||||
|
public val position: DeviceState<Long> by property(MetaConverter.long, position)
|
||||||
|
|
||||||
|
//FIXME round to zero problem
|
||||||
|
private val ticker = onTimer(reads = setOf(target, position), writes = setOf(position)) { prev, next ->
|
||||||
|
val tickSpeed = speed.value
|
||||||
|
val timeDelta = (next - prev).toDouble(DurationUnit.SECONDS)
|
||||||
|
val ticksDelta: Long = target.value - position.value
|
||||||
|
val steps: Long = when {
|
||||||
|
ticksDelta > 0 -> min(ticksDelta, (timeDelta * tickSpeed).roundToLong())
|
||||||
|
ticksDelta < 0 -> max(ticksDelta, -(timeDelta * tickSpeed).roundToLong())
|
||||||
|
else -> return@onTimer
|
||||||
|
}
|
||||||
|
writeTicks(steps, tickSpeed)
|
||||||
|
position.value += steps
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute a state using given tick-to-angle transformation
|
||||||
|
*/
|
||||||
|
public fun StepDrive.angle(
|
||||||
|
step: NumericalValue<Degrees>,
|
||||||
|
zero: NumericalValue<Degrees> = NumericalValue(0),
|
||||||
|
): DeviceState<NumericalValue<Degrees>> = position.map {
|
||||||
|
zero + it * step
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Класс для представления аннотации.
|
||||||
|
*/
|
||||||
|
public data class Annotation(
|
||||||
|
val name: String,
|
||||||
|
val properties: Map<String, Any>
|
||||||
|
)
|
@ -0,0 +1,196 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.dsl.core.equations.*
|
||||||
|
import space.kscience.controls.constructor.dsl.core.expressions.*
|
||||||
|
import space.kscience.controls.constructor.dsl.core.variables.ArrayVariable
|
||||||
|
import space.kscience.controls.constructor.dsl.core.variables.Variable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Класс для хранения и обработки системы уравнений.
|
||||||
|
*/
|
||||||
|
public class EquationSystem {
|
||||||
|
public val equations: MutableList<EquationBase> = mutableListOf()
|
||||||
|
public val variables: MutableSet<String> = mutableSetOf()
|
||||||
|
public val derivativeVariables: MutableSet<String> = mutableSetOf()
|
||||||
|
public val algebraicEquations: MutableList<Equation> = mutableListOf()
|
||||||
|
public val initialEquations: MutableList<EquationBase> = mutableListOf()
|
||||||
|
public val conditionalEquations: MutableList<ConditionalEquation> = mutableListOf()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Метод для добавления списка уравнений.
|
||||||
|
*/
|
||||||
|
public fun addEquations(eqs: List<EquationBase>) {
|
||||||
|
for (eq in eqs) {
|
||||||
|
addEquation(eq)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Метод для добавления одного уравнения.
|
||||||
|
*/
|
||||||
|
public fun addEquation(eq: EquationBase) {
|
||||||
|
equations.add(eq)
|
||||||
|
collectVariables(eq)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun collectVariables(eq: EquationBase) {
|
||||||
|
when (eq) {
|
||||||
|
is Equation -> {
|
||||||
|
collectVariablesFromExpression(eq.leftExpression)
|
||||||
|
collectVariablesFromExpression(eq.rightExpression)
|
||||||
|
|
||||||
|
// Classify the equation
|
||||||
|
if (isDifferentialEquation(eq)) {
|
||||||
|
// Handled in toODESystem()
|
||||||
|
} else {
|
||||||
|
algebraicEquations.add(eq)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is ConditionalEquation -> {
|
||||||
|
collectVariablesFromExpression(eq.condition)
|
||||||
|
collectVariables(eq.trueEquation)
|
||||||
|
eq.falseEquation?.let { collectVariables(it) }
|
||||||
|
conditionalEquations.add(eq)
|
||||||
|
}
|
||||||
|
is InitialEquation -> {
|
||||||
|
collectVariablesFromExpression(eq.leftExpression)
|
||||||
|
collectVariablesFromExpression(eq.rightExpression)
|
||||||
|
initialEquations.add(eq)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
throw UnsupportedOperationException("Unknown equation type: ${eq::class}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun collectVariablesFromExpression(expr: Expression) {
|
||||||
|
when (expr) {
|
||||||
|
is VariableExpression -> variables.add(expr.variable.name)
|
||||||
|
is ConstantExpression -> {
|
||||||
|
// Constants do not contain variables
|
||||||
|
}
|
||||||
|
is DerivativeExpression -> {
|
||||||
|
collectVariablesFromExpression(expr.variable)
|
||||||
|
derivativeVariables.add(expr.variable.variable.name)
|
||||||
|
}
|
||||||
|
is PartialDerivativeExpression -> {
|
||||||
|
collectVariablesFromExpression(expr.variable)
|
||||||
|
collectVariablesFromExpression(expr.respectTo)
|
||||||
|
// For partial derivatives, variables may need special handling
|
||||||
|
}
|
||||||
|
is BinaryExpression -> {
|
||||||
|
collectVariablesFromExpression(expr.left)
|
||||||
|
collectVariablesFromExpression(expr.right)
|
||||||
|
}
|
||||||
|
is FunctionCallExpression -> {
|
||||||
|
expr.arguments.forEach { collectVariablesFromExpression(it) }
|
||||||
|
}
|
||||||
|
is ArrayVariableExpression -> {
|
||||||
|
collectVariablesFromArrayVariable(expr.left)
|
||||||
|
collectVariablesFromArrayVariable(expr.right)
|
||||||
|
}
|
||||||
|
is ArrayScalarExpression -> {
|
||||||
|
collectVariablesFromArrayVariable(expr.array)
|
||||||
|
collectVariablesFromExpression(expr.scalar)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
throw UnsupportedOperationException("Unknown expression type: ${expr::class}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun collectVariablesFromArrayVariable(arrayVar: ArrayVariable) {
|
||||||
|
variables.add(arrayVar.name)
|
||||||
|
// Additional processing if necessary
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isDifferentialEquation(eq: Equation): Boolean {
|
||||||
|
val leftIsDerivative = containsDerivative(eq.leftExpression)
|
||||||
|
val rightIsDerivative = containsDerivative(eq.rightExpression)
|
||||||
|
return leftIsDerivative || rightIsDerivative
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun containsDerivative(expr: Expression): Boolean {
|
||||||
|
return when (expr) {
|
||||||
|
is DerivativeExpression -> true
|
||||||
|
is BinaryExpression -> containsDerivative(expr.left) || containsDerivative(expr.right)
|
||||||
|
is FunctionCallExpression -> expr.arguments.any { containsDerivative(it) }
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Подготовка системы ОДУ для численного решения.
|
||||||
|
*/
|
||||||
|
public fun toODESystem(): ODESystem {
|
||||||
|
val odeEquations = mutableListOf<ODEEquation>()
|
||||||
|
|
||||||
|
// Process equations to separate ODEs and algebraic equations
|
||||||
|
for (eqBase in equations) {
|
||||||
|
when (eqBase) {
|
||||||
|
is Equation -> {
|
||||||
|
val eq = eqBase
|
||||||
|
if (isDifferentialEquation(eq)) {
|
||||||
|
// Handle differential equations
|
||||||
|
processDifferentialEquation(eq, odeEquations)
|
||||||
|
} else {
|
||||||
|
// Handle algebraic equations
|
||||||
|
algebraicEquations.add(eq)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is ConditionalEquation -> {
|
||||||
|
// Handle conditional equations
|
||||||
|
// For ODE system, we might need to process them differently
|
||||||
|
conditionalEquations.add(eqBase)
|
||||||
|
}
|
||||||
|
is InitialEquation -> {
|
||||||
|
// Initial equations are used for setting initial conditions
|
||||||
|
initialEquations.add(eqBase)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
throw UnsupportedOperationException("Unknown equation type: ${eqBase::class}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ODESystem(odeEquations, algebraicEquations, initialEquations, conditionalEquations)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processDifferentialEquation(eq: Equation, odeEquations: MutableList<ODEEquation>) {
|
||||||
|
// Identify which side has the derivative
|
||||||
|
val derivativeExpr = findDerivativeExpression(eq.leftExpression) ?: findDerivativeExpression(eq.rightExpression)
|
||||||
|
if (derivativeExpr != null) {
|
||||||
|
val variableName = derivativeExpr.variable.variable.name
|
||||||
|
if (derivativeExpr.order != 1) {
|
||||||
|
throw UnsupportedOperationException("Only first-order derivatives are supported in this solver.")
|
||||||
|
}
|
||||||
|
|
||||||
|
val rhs = if (eq.leftExpression is DerivativeExpression) eq.rightExpression else eq.leftExpression
|
||||||
|
odeEquations.add(ODEEquation(variableName, rhs))
|
||||||
|
derivativeVariables.add(variableName)
|
||||||
|
} else {
|
||||||
|
throw UnsupportedOperationException("Equation is marked as differential but no derivative found.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findDerivativeExpression(expr: Expression): DerivativeExpression? {
|
||||||
|
return when (expr) {
|
||||||
|
is DerivativeExpression -> expr
|
||||||
|
is BinaryExpression -> findDerivativeExpression(expr.left) ?: findDerivativeExpression(expr.right)
|
||||||
|
is FunctionCallExpression -> expr.arguments.mapNotNull { findDerivativeExpression(it) }.firstOrNull()
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public data class ODEEquation(
|
||||||
|
val variableName: String,
|
||||||
|
val rhs: Expression
|
||||||
|
)
|
||||||
|
|
||||||
|
public class ODESystem(
|
||||||
|
public val odeEquations: List<ODEEquation>,
|
||||||
|
public val algebraicEquations: List<Equation>,
|
||||||
|
public val initialEquations: List<EquationBase>,
|
||||||
|
public val conditionalEquations: List<ConditionalEquation>
|
||||||
|
)
|
@ -0,0 +1,19 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.dsl.core.units.PhysicalUnit
|
||||||
|
import space.kscience.controls.constructor.dsl.core.variables.Variable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Класс для представления записи (record).
|
||||||
|
*/
|
||||||
|
public class Record(public val name: String) {
|
||||||
|
public val fields: MutableMap<String, Variable> = mutableMapOf()
|
||||||
|
|
||||||
|
|
||||||
|
public fun field(name: String, unit: PhysicalUnit) {
|
||||||
|
if (fields.containsKey(name)) {
|
||||||
|
throw IllegalArgumentException("Field with name $name already exists in record $name.")
|
||||||
|
}
|
||||||
|
fields[name] = Variable(name, unit)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.algorithms
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.dsl.core.expressions.Expression
|
||||||
|
import space.kscience.controls.constructor.dsl.core.variables.Variable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Класс для построения алгоритмов.
|
||||||
|
*/
|
||||||
|
public class AlgorithmBuilder {
|
||||||
|
public val statements: MutableList<Statement> = mutableListOf()
|
||||||
|
|
||||||
|
public fun assign(variable: Variable, expression: Expression) {
|
||||||
|
statements.add(AssignmentStatement(variable, expression))
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun ifStatement(condition: Expression, init: AlgorithmBuilder.() -> Unit) {
|
||||||
|
val trueBuilder = AlgorithmBuilder()
|
||||||
|
trueBuilder.init()
|
||||||
|
statements.add(IfStatement(condition, trueBuilder.build(), null))
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun forStatement(variable: Variable, range: IntRange, init: AlgorithmBuilder.() -> Unit) {
|
||||||
|
val bodyBuilder = AlgorithmBuilder()
|
||||||
|
bodyBuilder.init()
|
||||||
|
statements.add(ForStatement(variable, range, bodyBuilder.build()))
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun whileStatement(condition: Expression, init: AlgorithmBuilder.() -> Unit) {
|
||||||
|
val bodyBuilder = AlgorithmBuilder()
|
||||||
|
bodyBuilder.init()
|
||||||
|
statements.add(WhileStatement(condition, bodyBuilder.build()))
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun build(): Algorithm {
|
||||||
|
return Algorithm(statements.toList())
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.algorithms
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.dsl.core.expressions.Expression
|
||||||
|
import space.kscience.controls.constructor.dsl.core.simulation.SimulationContext
|
||||||
|
import space.kscience.controls.constructor.dsl.core.variables.Variable
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Абстрактный класс для операторов в алгоритмической секции.
|
||||||
|
*/
|
||||||
|
public sealed class Statement {
|
||||||
|
public abstract suspend fun execute(context: SimulationContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
public data class AssignmentStatement(
|
||||||
|
val variable: Variable,
|
||||||
|
val expression: Expression
|
||||||
|
) : Statement() {
|
||||||
|
override suspend fun execute(context: SimulationContext) {
|
||||||
|
val value = expression.evaluate(context.getVariableValues())
|
||||||
|
context.updateVariable(variable.name, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public data class IfStatement(
|
||||||
|
val condition: Expression,
|
||||||
|
val trueBranch: Algorithm,
|
||||||
|
val falseBranch: Algorithm? = null
|
||||||
|
) : Statement() {
|
||||||
|
override suspend fun execute(context: SimulationContext) {
|
||||||
|
val conditionValue = condition.evaluate(context.getVariableValues())
|
||||||
|
if (conditionValue != 0.0) {
|
||||||
|
trueBranch.execute(context)
|
||||||
|
} else {
|
||||||
|
falseBranch?.execute(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public data class ForStatement(
|
||||||
|
val variable: Variable,
|
||||||
|
val range: IntRange,
|
||||||
|
val body: Algorithm
|
||||||
|
) : Statement() {
|
||||||
|
override suspend fun execute(context: SimulationContext) {
|
||||||
|
for (i in range) {
|
||||||
|
context.updateVariable(variable.name, i.toDouble())
|
||||||
|
body.execute(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public data class WhileStatement(
|
||||||
|
val condition: Expression,
|
||||||
|
val body: Algorithm
|
||||||
|
) : Statement() {
|
||||||
|
override suspend fun execute(context: SimulationContext) {
|
||||||
|
while (condition.evaluate(context.getVariableValues()) != 0.0) {
|
||||||
|
body.execute(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Algorithm(private val statements: List<Statement>) {
|
||||||
|
/**
|
||||||
|
* Добавляет оператор в алгоритм.
|
||||||
|
*/
|
||||||
|
public fun addStatement(statement: Statement) {
|
||||||
|
(statements as MutableList).add(statement)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Выполняет алгоритм в заданном контексте симуляции.
|
||||||
|
*/
|
||||||
|
public suspend fun execute(context: SimulationContext) {
|
||||||
|
for (statement in statements) {
|
||||||
|
statement.execute(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,81 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.components
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.dsl.core.Annotation
|
||||||
|
import space.kscience.controls.constructor.dsl.core.equations.Equation
|
||||||
|
import space.kscience.controls.constructor.dsl.core.variables.ParameterVariable
|
||||||
|
import space.kscience.controls.constructor.dsl.core.variables.Variable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Базовый класс для всех компонентов модели.
|
||||||
|
*/
|
||||||
|
public abstract class Component<V : Variable, P : ParameterVariable, C : Connector>(public val name: String) {
|
||||||
|
public val variables: MutableMap<String, V> = mutableMapOf()
|
||||||
|
public val parameters: MutableMap<String, P> = mutableMapOf()
|
||||||
|
public val equations: MutableList<Equation> = mutableListOf()
|
||||||
|
public val connectors: MutableMap<String, C> = mutableMapOf()
|
||||||
|
public val annotations: MutableList<Annotation> = mutableListOf()
|
||||||
|
public var isReplaceable: Boolean = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Метод для инициализации переменных.
|
||||||
|
*/
|
||||||
|
protected abstract fun initializeVariables()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Метод для определения уравнений компонента.
|
||||||
|
*/
|
||||||
|
protected abstract fun defineEquations()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Метод для добавления переменной.
|
||||||
|
*/
|
||||||
|
public fun addVariable(variable: V) {
|
||||||
|
if (variables.containsKey(variable.name)) {
|
||||||
|
throw IllegalArgumentException("Variable with name ${variable.name} already exists in component $name.")
|
||||||
|
}
|
||||||
|
variables[variable.name] = variable
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Метод для добавления параметра.
|
||||||
|
*/
|
||||||
|
public fun addParameter(parameter: P) {
|
||||||
|
if (parameters.containsKey(parameter.name)) {
|
||||||
|
throw IllegalArgumentException("Parameter with name ${parameter.name} already exists in component $name.")
|
||||||
|
}
|
||||||
|
parameters[parameter.name] = parameter
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Метод для добавления уравнения.
|
||||||
|
*/
|
||||||
|
public fun addEquation(equation: Equation) {
|
||||||
|
equations.add(equation)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Метод для добавления коннектора.
|
||||||
|
*/
|
||||||
|
public fun addConnector(name: String, connector: C) {
|
||||||
|
if (connectors.containsKey(name)) {
|
||||||
|
throw IllegalArgumentException("Connector with name $name already exists in component $name.")
|
||||||
|
}
|
||||||
|
connectors[name] = connector
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun annotate(annotation: Annotation) {
|
||||||
|
annotations.add(annotation)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Метод для модификации параметров компонента.
|
||||||
|
*/
|
||||||
|
public fun modify(modifier: Component<V, P, C>.() -> Unit): Component<V, P, C> {
|
||||||
|
this.apply(modifier)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun markAsReplaceable(isReplaceable: Boolean) {
|
||||||
|
this.isReplaceable = isReplaceable
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.components
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.dsl.core.equations.Equation
|
||||||
|
import space.kscience.controls.constructor.dsl.core.expressions.VariableExpression
|
||||||
|
import space.kscience.controls.constructor.dsl.core.variables.Variable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Класс для представления соединения между двумя коннекторами.
|
||||||
|
*/
|
||||||
|
public class Connection<C : Connector>(
|
||||||
|
public val connector1: C,
|
||||||
|
public val connector2: C
|
||||||
|
) {
|
||||||
|
init {
|
||||||
|
if (connector1.unit != connector2.unit) {
|
||||||
|
throw IllegalArgumentException("Cannot connect connectors with different units: ${connector1.unit} and ${connector2.unit}.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Генерация уравнений для соединения.
|
||||||
|
*/
|
||||||
|
public fun generateEquations(): List<Equation> {
|
||||||
|
val equations = mutableListOf<Equation>()
|
||||||
|
for ((varName, var1) in connector1.variables) {
|
||||||
|
val var2 = connector2.variables[varName]
|
||||||
|
?: throw IllegalArgumentException("Variable $varName not found in connector ${connector2.name}.")
|
||||||
|
|
||||||
|
equations.add(
|
||||||
|
Equation(
|
||||||
|
VariableExpression(var1),
|
||||||
|
VariableExpression(var2)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return equations
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.components
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.dsl.core.units.PhysicalUnit
|
||||||
|
import space.kscience.controls.constructor.dsl.core.variables.Variable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Интерфейс для коннектора, позволяющего соединять компоненты.
|
||||||
|
*/
|
||||||
|
public interface Connector {
|
||||||
|
public val name: String
|
||||||
|
public val variables: Map<String, Variable>
|
||||||
|
public val unit: PhysicalUnit
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Базовая реализация коннектора.
|
||||||
|
*/
|
||||||
|
public class BasicConnector(
|
||||||
|
override val name: String,
|
||||||
|
override val unit: PhysicalUnit,
|
||||||
|
override val variables: Map<String, Variable>
|
||||||
|
) : Connector
|
@ -0,0 +1,163 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.components
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.dsl.core.*
|
||||||
|
import space.kscience.controls.constructor.dsl.core.algorithms.*
|
||||||
|
import space.kscience.controls.constructor.dsl.core.equations.Equation
|
||||||
|
import space.kscience.controls.constructor.dsl.core.equations.ConditionalEquation
|
||||||
|
import space.kscience.controls.constructor.dsl.core.expressions.Expression
|
||||||
|
import space.kscience.controls.constructor.dsl.core.expressions.VariableExpression
|
||||||
|
import space.kscience.controls.constructor.dsl.core.units.PhysicalUnit
|
||||||
|
import space.kscience.controls.constructor.dsl.core.variables.ParameterVariable
|
||||||
|
import space.kscience.controls.constructor.dsl.core.variables.Variable
|
||||||
|
import space.kscience.controls.constructor.dsl.core.functions.Function
|
||||||
|
import space.kscience.controls.constructor.dsl.core.functions.FunctionBuilder
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Класс для представления модели, содержащей компоненты и соединения.
|
||||||
|
*/
|
||||||
|
public class Model(public val name: String, init: Model.() -> Unit) {
|
||||||
|
public val components: MutableMap<String, Component<*, *, *>> = mutableMapOf<String, Component<*, *, *>>()
|
||||||
|
public val connections: MutableList<Connection<*>> = mutableListOf<Connection<*>>()
|
||||||
|
public val variables: MutableMap<String, Variable> = mutableMapOf<String, Variable>()
|
||||||
|
public val parameters: MutableMap<String, ParameterVariable> = mutableMapOf<String, ParameterVariable>()
|
||||||
|
public val equations: MutableList<Equation> = mutableListOf<Equation>()
|
||||||
|
public val initialValues: MutableMap<String, Double> = mutableMapOf<String, Double>()
|
||||||
|
public val initialEquations: MutableList<Equation> = mutableListOf<Equation>()
|
||||||
|
public val conditionalEquations: MutableList<ConditionalEquation> = mutableListOf<ConditionalEquation>()
|
||||||
|
public val algorithms: MutableList<Algorithm> = mutableListOf<Algorithm>()
|
||||||
|
public val functions: MutableMap<String, Function> = mutableMapOf<String, Function>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Определение переменной.
|
||||||
|
*/
|
||||||
|
public fun variable(name: String, unit: PhysicalUnit): VariableExpression {
|
||||||
|
val variable = Variable(name, unit)
|
||||||
|
variables[name] = variable
|
||||||
|
return VariableExpression(variable)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Определение параметра.
|
||||||
|
*/
|
||||||
|
public fun parameter(name: String, unit: PhysicalUnit, value: Double): ParameterVariable {
|
||||||
|
val parameter = ParameterVariable(name, unit, value)
|
||||||
|
parameters[name] = parameter
|
||||||
|
return parameter
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Добавление уравнения.
|
||||||
|
*/
|
||||||
|
public fun equation(equationBuilder: () -> Equation) {
|
||||||
|
val equation = equationBuilder()
|
||||||
|
equations.add(equation)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Установка начальных значений.
|
||||||
|
*/
|
||||||
|
public fun initialValues(init: MutableMap<String, Double>.() -> Unit) {
|
||||||
|
initialValues.apply(init)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Добавление инициализационного уравнения.
|
||||||
|
*/
|
||||||
|
public fun initialEquation(equationBuilder: () -> Equation) {
|
||||||
|
val equation = equationBuilder()
|
||||||
|
initialEquations.add(equation)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Метод для добавления компонента.
|
||||||
|
*/
|
||||||
|
public fun addComponent(component: Component<*, *, *>) {
|
||||||
|
if (components.containsKey(component.name)) {
|
||||||
|
throw IllegalArgumentException("Component with name ${component.name} already exists in model $name.")
|
||||||
|
}
|
||||||
|
components[component.name] = component
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Метод для добавления соединения.
|
||||||
|
*/
|
||||||
|
public fun addConnection(connection: Connection<*>) {
|
||||||
|
connections.add(connection)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Метод для генерации системы уравнений модели.
|
||||||
|
*/
|
||||||
|
public fun generateEquationSystem(): EquationSystem {
|
||||||
|
val equationSystem = EquationSystem()
|
||||||
|
|
||||||
|
// Добавление уравнений модели
|
||||||
|
equationSystem.addEquations(equations)
|
||||||
|
|
||||||
|
// Добавление уравнений компонентов
|
||||||
|
for (component in components.values) {
|
||||||
|
equationSystem.addEquations(component.equations)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавление уравнений соединений
|
||||||
|
for (connection in connections) {
|
||||||
|
equationSystem.addEquations(connection.generateEquations())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавление условных уравнений
|
||||||
|
for (condEq in conditionalEquations) {
|
||||||
|
equationSystem.addEquation(condEq)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавление инициализационных уравнений
|
||||||
|
equationSystem.addEquations(initialEquations)
|
||||||
|
|
||||||
|
return equationSystem
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Метод для замены компонента.
|
||||||
|
*/
|
||||||
|
public fun replaceComponent(name: String, newComponent: Component<*, *, *>) {
|
||||||
|
val oldComponent = components[name]
|
||||||
|
if (oldComponent != null && oldComponent.isReplaceable) {
|
||||||
|
components[name] = newComponent
|
||||||
|
} else {
|
||||||
|
throw IllegalArgumentException("Component $name is not replaceable or does not exist.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Добавление условного уравнения.
|
||||||
|
*/
|
||||||
|
public fun conditionalEquation(condition: Expression, trueEquation: Equation, falseEquation: Equation? = null) {
|
||||||
|
val condEq = ConditionalEquation(condition, trueEquation, falseEquation)
|
||||||
|
conditionalEquations.add(condEq)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Добавление алгоритма.
|
||||||
|
*/
|
||||||
|
public fun algorithm(init: AlgorithmBuilder.() -> Unit) {
|
||||||
|
val builder = AlgorithmBuilder()
|
||||||
|
builder.init()
|
||||||
|
algorithms.add(builder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Добавление пользовательской функции.
|
||||||
|
*/
|
||||||
|
public fun function(name: String, init: FunctionBuilder.() -> Unit) {
|
||||||
|
if (functions.containsKey(name)) {
|
||||||
|
throw IllegalArgumentException("Function with name $name already exists in model $name.")
|
||||||
|
}
|
||||||
|
val builder = FunctionBuilder(name)
|
||||||
|
builder.init()
|
||||||
|
val function = builder.build()
|
||||||
|
functions[name] = function
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.components
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Класс для представления пакета моделей.
|
||||||
|
*/
|
||||||
|
public class Package(public val name: String) {
|
||||||
|
public val models: MutableMap<String, Model> = mutableMapOf()
|
||||||
|
public val subPackages: MutableMap<String, Package> = mutableMapOf()
|
||||||
|
|
||||||
|
public fun addModel(model: Model) {
|
||||||
|
models[model.name] = model
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun addPackage(pkg: Package) {
|
||||||
|
subPackages[pkg.name] = pkg
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun getModel(name: String): Model? {
|
||||||
|
return models[name] ?: subPackages.values.mapNotNull { it.getModel(name) }.firstOrNull()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.equations
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.dsl.core.expressions.Expression
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Класс для условного уравнения.
|
||||||
|
*/
|
||||||
|
public class ConditionalEquation(
|
||||||
|
public val condition: Expression,
|
||||||
|
public val trueEquation: Equation,
|
||||||
|
public val falseEquation: Equation? = null
|
||||||
|
) : EquationBase()
|
@ -0,0 +1,24 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.equations
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.dsl.core.expressions.Expression
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Класс для представления уравнения.
|
||||||
|
*/
|
||||||
|
public class Equation(
|
||||||
|
public val leftExpression: Expression,
|
||||||
|
public val rightExpression: Expression
|
||||||
|
) : EquationBase() {
|
||||||
|
init {
|
||||||
|
// Проверка совместимости единиц измерения
|
||||||
|
if (!leftExpression.unit.dimension.isCompatibleWith(rightExpression.unit.dimension)) {
|
||||||
|
throw IllegalArgumentException("Units of left and right expressions in the equation are not compatible.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun evaluate(values: Map<String, Double>): Double {
|
||||||
|
val leftValue = leftExpression.evaluate(values)
|
||||||
|
val rightValue = rightExpression.evaluate(values)
|
||||||
|
return leftValue - rightValue
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.equations
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Абстрактный базовый класс для уравнений.
|
||||||
|
*/
|
||||||
|
public sealed class EquationBase
|
@ -0,0 +1,11 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.equations
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.dsl.core.expressions.Expression
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing an initial equation for setting initial conditions.
|
||||||
|
*/
|
||||||
|
public class InitialEquation(
|
||||||
|
public val leftExpression: Expression,
|
||||||
|
public val rightExpression: Expression
|
||||||
|
) : EquationBase()
|
@ -0,0 +1,18 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.events
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.dsl.core.expressions.Expression
|
||||||
|
import space.kscience.controls.constructor.dsl.core.simulation.SimulationContext
|
||||||
|
import space.kscience.controls.constructor.dsl.core.variables.Variable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Действие присваивания нового значения переменной.
|
||||||
|
*/
|
||||||
|
public data class AssignAction(
|
||||||
|
val variable: Variable,
|
||||||
|
val newValue: Expression
|
||||||
|
) : EventAction() {
|
||||||
|
override suspend fun execute(context: SimulationContext) {
|
||||||
|
val value = newValue.evaluate(context.getVariableValues())
|
||||||
|
context.updateVariable(variable.name, value)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.events
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.dsl.core.simulation.SimulationContext
|
||||||
|
import space.kscience.controls.constructor.dsl.core.expressions.Expression
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Класс для представления события.
|
||||||
|
*/
|
||||||
|
public class Event(
|
||||||
|
public val condition: Expression,
|
||||||
|
public val actions: List<EventAction>
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Метод для проверки, произошло ли событие.
|
||||||
|
*/
|
||||||
|
public suspend fun checkEvent(context: SimulationContext): Boolean {
|
||||||
|
val values = context.getVariableValues()
|
||||||
|
val conditionValue = condition.evaluate(values)
|
||||||
|
return conditionValue > 0 // Событие происходит, когда условие положительно
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Метод для выполнения действий при событии.
|
||||||
|
*/
|
||||||
|
public suspend fun executeActions(context: SimulationContext) {
|
||||||
|
for (action in actions) {
|
||||||
|
action.execute(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.events
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.dsl.core.expressions.Expression
|
||||||
|
import space.kscience.controls.constructor.dsl.core.simulation.SimulationContext
|
||||||
|
import space.kscience.controls.constructor.dsl.core.variables.Variable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Абстрактный класс для действий при событии.
|
||||||
|
*/
|
||||||
|
public sealed class EventAction {
|
||||||
|
public abstract suspend fun execute(context: SimulationContext)
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.events
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.dsl.core.expressions.Expression
|
||||||
|
import space.kscience.controls.constructor.dsl.core.simulation.SimulationContext
|
||||||
|
import space.kscience.controls.constructor.dsl.core.variables.Variable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Действие реинициализации переменной.
|
||||||
|
*/
|
||||||
|
public data class ReinitAction(
|
||||||
|
val variable: Variable,
|
||||||
|
val newValue: Expression
|
||||||
|
) : EventAction() {
|
||||||
|
override suspend fun execute(context: SimulationContext) {
|
||||||
|
val value = newValue.evaluate(context.getVariableValues())
|
||||||
|
context.updateVariable(variable.name, value)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,91 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.expression
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.dsl.core.variables.ArrayVariable
|
||||||
|
import space.kscience.controls.constructor.dsl.core.equations.Equation
|
||||||
|
import space.kscience.controls.constructor.dsl.core.expressions.ArrayBinaryExpression
|
||||||
|
import space.kscience.controls.constructor.dsl.core.expressions.ArrayBinaryOperator
|
||||||
|
import space.kscience.controls.constructor.dsl.core.expressions.ArrayScalarExpression
|
||||||
|
import space.kscience.controls.constructor.dsl.core.expressions.ArrayScalarOperator
|
||||||
|
import space.kscience.controls.constructor.dsl.core.expressions.BinaryExpression
|
||||||
|
import space.kscience.controls.constructor.dsl.core.expressions.BinaryOperator
|
||||||
|
import space.kscience.controls.constructor.dsl.core.expressions.ConstantExpression
|
||||||
|
import space.kscience.controls.constructor.dsl.core.expressions.Expression
|
||||||
|
import space.kscience.controls.constructor.dsl.core.expressions.FunctionCallExpression
|
||||||
|
import space.kscience.controls.constructor.dsl.core.units.Units
|
||||||
|
import space.kscience.controls.constructor.dsl.core.functions.Function
|
||||||
|
|
||||||
|
// Операторные функции для скалярных выражений
|
||||||
|
|
||||||
|
public operator fun Expression.plus(other: Expression): Expression {
|
||||||
|
return BinaryExpression(this, BinaryOperator.ADD, other)
|
||||||
|
}
|
||||||
|
|
||||||
|
public operator fun Expression.minus(other: Expression): Expression {
|
||||||
|
return BinaryExpression(this, BinaryOperator.SUBTRACT, other)
|
||||||
|
}
|
||||||
|
|
||||||
|
public operator fun Expression.times(other: Expression): Expression {
|
||||||
|
return BinaryExpression(this, BinaryOperator.MULTIPLY, other)
|
||||||
|
}
|
||||||
|
|
||||||
|
public operator fun Expression.div(other: Expression): Expression {
|
||||||
|
return BinaryExpression(this, BinaryOperator.DIVIDE, other)
|
||||||
|
}
|
||||||
|
|
||||||
|
public infix fun Expression.pow(exponent: Expression): Expression {
|
||||||
|
return FunctionCallExpression("pow", listOf(this, exponent))
|
||||||
|
}
|
||||||
|
|
||||||
|
public infix fun Expression.pow(exponent: Double): Expression {
|
||||||
|
return FunctionCallExpression("pow", listOf(this, ConstantExpression(exponent, Units.dimensionless)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сравнительные операторы
|
||||||
|
|
||||||
|
public infix fun Expression.eq(other: Expression): Equation {
|
||||||
|
return Equation(this, other)
|
||||||
|
}
|
||||||
|
|
||||||
|
public infix fun Expression.gt(other: Expression): Expression {
|
||||||
|
return BinaryExpression(this, BinaryOperator.GREATER_THAN, other)
|
||||||
|
}
|
||||||
|
|
||||||
|
public infix fun Expression.lt(other: Expression): Expression {
|
||||||
|
return BinaryExpression(this, BinaryOperator.LESS_THAN, other)
|
||||||
|
}
|
||||||
|
|
||||||
|
public infix fun Expression.ge(other: Expression): Expression {
|
||||||
|
return BinaryExpression(this, BinaryOperator.GREATER_OR_EQUAL, other)
|
||||||
|
}
|
||||||
|
|
||||||
|
public infix fun Expression.le(other: Expression): Expression {
|
||||||
|
return BinaryExpression(this, BinaryOperator.LESS_OR_EQUAL, other)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Операторные функции для массивов
|
||||||
|
|
||||||
|
public operator fun ArrayVariable.plus(other: ArrayVariable): Expression {
|
||||||
|
return ArrayBinaryExpression(this, ArrayBinaryOperator.ADD, other)
|
||||||
|
}
|
||||||
|
|
||||||
|
public operator fun ArrayVariable.minus(other: ArrayVariable): Expression {
|
||||||
|
return ArrayBinaryExpression(this, ArrayBinaryOperator.SUBTRACT, other)
|
||||||
|
}
|
||||||
|
|
||||||
|
public operator fun ArrayVariable.times(scalar: Expression): Expression {
|
||||||
|
return ArrayScalarExpression(this, ArrayScalarOperator.MULTIPLY, scalar)
|
||||||
|
}
|
||||||
|
|
||||||
|
public operator fun ArrayVariable.div(scalar: Expression): Expression {
|
||||||
|
return ArrayScalarExpression(this, ArrayScalarOperator.DIVIDE, scalar)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Функция для вызова пользовательской функции в выражении.
|
||||||
|
*/
|
||||||
|
public fun callFunction(function: Function, vararg args: Expression): FunctionCallExpression {
|
||||||
|
require(args.size == function.inputs.size) {
|
||||||
|
"Number of arguments does not match number of function inputs."
|
||||||
|
}
|
||||||
|
return FunctionCallExpression(function.name, args.toList())
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.expressions
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.dsl.core.units.PhysicalUnit
|
||||||
|
import space.kscience.controls.constructor.dsl.core.variables.ArrayVariable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Класс для представления бинарной операции между двумя массивами.
|
||||||
|
*/
|
||||||
|
public class ArrayBinaryExpression(
|
||||||
|
public val left: ArrayVariable,
|
||||||
|
public val operator: ArrayBinaryOperator,
|
||||||
|
public val right: ArrayVariable
|
||||||
|
) : Expression() {
|
||||||
|
|
||||||
|
override val unit: PhysicalUnit = when (operator) {
|
||||||
|
ArrayBinaryOperator.ADD, ArrayBinaryOperator.SUBTRACT -> {
|
||||||
|
if (left.unit != right.unit) {
|
||||||
|
throw IllegalArgumentException("Units must be the same for array addition or subtraction.")
|
||||||
|
}
|
||||||
|
left.unit
|
||||||
|
}
|
||||||
|
ArrayBinaryOperator.MULTIPLY -> left.unit * right.unit
|
||||||
|
ArrayBinaryOperator.DIVIDE -> left.unit / right.unit
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun evaluate(values: Map<String, Double>): Double {
|
||||||
|
throw UnsupportedOperationException("Cannot evaluate array expression to a scalar value.")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun checkDimensions() {
|
||||||
|
TODO()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun derivative(variable: VariableExpression): Expression {
|
||||||
|
throw UnsupportedOperationException("Derivative of array expression is not implemented.")
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.expressions
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Перечисление для бинарных операций над массивами.
|
||||||
|
*/
|
||||||
|
public enum class ArrayBinaryOperator {
|
||||||
|
ADD,
|
||||||
|
SUBTRACT,
|
||||||
|
MULTIPLY,
|
||||||
|
DIVIDE
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.expressions
|
||||||
|
|
||||||
|
public sealed class ArrayExpression : Expression()
|
@ -0,0 +1,34 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.expressions
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.dsl.core.units.PhysicalUnit
|
||||||
|
import space.kscience.controls.constructor.dsl.core.variables.ArrayVariable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Представляет операцию между массивом и скаляром.
|
||||||
|
*/
|
||||||
|
public class ArrayScalarExpression(
|
||||||
|
public val array: ArrayVariable,
|
||||||
|
public val operator: ArrayScalarOperator,
|
||||||
|
public val scalar: Expression
|
||||||
|
) : Expression() {
|
||||||
|
|
||||||
|
override val unit: PhysicalUnit = when (operator) {
|
||||||
|
ArrayScalarOperator.MULTIPLY -> array.unit * scalar.unit
|
||||||
|
ArrayScalarOperator.DIVIDE -> array.unit / scalar.unit
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun evaluate(values: Map<String, Double>): Double {
|
||||||
|
// Невозможно вычислить массивное выражение до скалярного значения
|
||||||
|
throw UnsupportedOperationException("Cannot evaluate array expression to a scalar value.")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun checkDimensions() {
|
||||||
|
// array.checkDimensions()
|
||||||
|
scalar.checkDimensions()
|
||||||
|
// Единицы измерения уже проверены в свойстве `unit`
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun derivative(variable: VariableExpression): Expression {
|
||||||
|
throw UnsupportedOperationException("Derivative of array expression is not implemented.")
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.expressions
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Перечисление операторов между массивом и скаляром.
|
||||||
|
*/
|
||||||
|
public enum class ArrayScalarOperator {
|
||||||
|
MULTIPLY,
|
||||||
|
DIVIDE
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.expressions
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.dsl.core.expressions.Expression
|
||||||
|
import space.kscience.controls.constructor.dsl.core.units.PhysicalUnit
|
||||||
|
import space.kscience.controls.constructor.dsl.core.variables.ArrayVariable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Представляет операцию между двумя массивами.
|
||||||
|
*/
|
||||||
|
public class ArrayVariableExpression(
|
||||||
|
public val left: ArrayVariable,
|
||||||
|
public val operator: ArrayBinaryOperator,
|
||||||
|
public val right: ArrayVariable
|
||||||
|
) : Expression() {
|
||||||
|
|
||||||
|
override val unit: PhysicalUnit = when (operator) {
|
||||||
|
ArrayBinaryOperator.ADD, ArrayBinaryOperator.SUBTRACT -> {
|
||||||
|
if (left.unit != right.unit) {
|
||||||
|
throw IllegalArgumentException("Units must be the same for array addition or subtraction.")
|
||||||
|
}
|
||||||
|
left.unit
|
||||||
|
}
|
||||||
|
ArrayBinaryOperator.MULTIPLY -> left.unit * right.unit
|
||||||
|
ArrayBinaryOperator.DIVIDE -> left.unit / right.unit
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun evaluate(values: Map<String, Double>): Double {
|
||||||
|
// Невозможно вычислить массивное выражение до скалярного значения
|
||||||
|
throw UnsupportedOperationException("Cannot evaluate array expression to a scalar value.")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun checkDimensions() {
|
||||||
|
// left.checkDimensions()
|
||||||
|
// right.checkDimensions()
|
||||||
|
// Единицы измерения уже проверены в свойстве `unit`
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun derivative(variable: VariableExpression): Expression {
|
||||||
|
// Производная массива не реализована
|
||||||
|
throw UnsupportedOperationException("Derivative of array expression is not implemented.")
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,135 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.expressions
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.dsl.core.units.PhysicalUnit
|
||||||
|
import space.kscience.controls.constructor.dsl.core.units.Units
|
||||||
|
import kotlin.math.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Перечисление типов бинарных операций.
|
||||||
|
*/
|
||||||
|
public enum class BinaryOperator {
|
||||||
|
ADD,
|
||||||
|
SUBTRACT,
|
||||||
|
MULTIPLY,
|
||||||
|
DIVIDE,
|
||||||
|
POWER,
|
||||||
|
GREATER_THAN,
|
||||||
|
LESS_THAN,
|
||||||
|
GREATER_OR_EQUAL,
|
||||||
|
LESS_OR_EQUAL
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Класс для представления бинарной операции.
|
||||||
|
*/
|
||||||
|
public class BinaryExpression(
|
||||||
|
public val left: Expression,
|
||||||
|
public val operator: BinaryOperator,
|
||||||
|
public val right: Expression
|
||||||
|
) : Expression() {
|
||||||
|
|
||||||
|
override val unit: PhysicalUnit = when (operator) {
|
||||||
|
BinaryOperator.ADD, BinaryOperator.SUBTRACT -> {
|
||||||
|
if (left.unit != right.unit) {
|
||||||
|
throw IllegalArgumentException("Units of left and right expressions must be the same for addition or subtraction.")
|
||||||
|
}
|
||||||
|
left.unit
|
||||||
|
}
|
||||||
|
BinaryOperator.MULTIPLY -> left.unit * right.unit
|
||||||
|
BinaryOperator.DIVIDE -> left.unit / right.unit
|
||||||
|
BinaryOperator.POWER -> {
|
||||||
|
if (right is ConstantExpression) {
|
||||||
|
left.unit.pow(right.value)
|
||||||
|
} else {
|
||||||
|
throw IllegalArgumentException("Exponent must be a constant expression.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BinaryOperator.GREATER_THAN, BinaryOperator.LESS_THAN,
|
||||||
|
BinaryOperator.GREATER_OR_EQUAL, BinaryOperator.LESS_OR_EQUAL -> {
|
||||||
|
Units.dimensionless
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun evaluate(values: Map<String, Double>): Double {
|
||||||
|
val leftValue = left.evaluate(values)
|
||||||
|
val rightValue = right.evaluate(values)
|
||||||
|
return when (operator) {
|
||||||
|
BinaryOperator.ADD -> leftValue + rightValue
|
||||||
|
BinaryOperator.SUBTRACT -> leftValue - rightValue
|
||||||
|
BinaryOperator.MULTIPLY -> leftValue * rightValue
|
||||||
|
BinaryOperator.DIVIDE -> leftValue / rightValue
|
||||||
|
BinaryOperator.POWER -> leftValue.pow(rightValue)
|
||||||
|
BinaryOperator.GREATER_THAN -> if (leftValue > rightValue) 1.0 else 0.0
|
||||||
|
BinaryOperator.LESS_THAN -> if (leftValue < rightValue) 1.0 else 0.0
|
||||||
|
BinaryOperator.GREATER_OR_EQUAL -> if (leftValue >= rightValue) 1.0 else 0.0
|
||||||
|
BinaryOperator.LESS_OR_EQUAL -> if (leftValue <= rightValue) 1.0 else 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun checkDimensions() {
|
||||||
|
left.checkDimensions()
|
||||||
|
right.checkDimensions()
|
||||||
|
when (operator) {
|
||||||
|
BinaryOperator.ADD, BinaryOperator.SUBTRACT -> {
|
||||||
|
if (left.unit != right.unit) {
|
||||||
|
throw IllegalArgumentException("Units must be the same for addition or subtraction.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BinaryOperator.MULTIPLY, BinaryOperator.DIVIDE -> {
|
||||||
|
// Единицы измерения уже вычислены в `unit`
|
||||||
|
}
|
||||||
|
BinaryOperator.POWER -> {
|
||||||
|
if (!right.unit.dimension.isDimensionless()) {
|
||||||
|
throw IllegalArgumentException("Exponent must be dimensionless.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BinaryOperator.GREATER_THAN, BinaryOperator.LESS_THAN,
|
||||||
|
BinaryOperator.GREATER_OR_EQUAL, BinaryOperator.LESS_OR_EQUAL -> {
|
||||||
|
if (left.unit != right.unit) {
|
||||||
|
throw IllegalArgumentException("Units must be the same for comparison operations.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun derivative(variable: VariableExpression): Expression {
|
||||||
|
return when (operator) {
|
||||||
|
BinaryOperator.ADD, BinaryOperator.SUBTRACT -> {
|
||||||
|
BinaryExpression(left.derivative(variable), operator, right.derivative(variable))
|
||||||
|
}
|
||||||
|
BinaryOperator.MULTIPLY -> {
|
||||||
|
// Правило произведения: (u*v)' = u'*v + u*v'
|
||||||
|
val leftDerivative = BinaryExpression(left.derivative(variable), BinaryOperator.MULTIPLY, right)
|
||||||
|
val rightDerivative = BinaryExpression(left, BinaryOperator.MULTIPLY, right.derivative(variable))
|
||||||
|
BinaryExpression(leftDerivative, BinaryOperator.ADD, rightDerivative)
|
||||||
|
}
|
||||||
|
BinaryOperator.DIVIDE -> {
|
||||||
|
// Правило частного: (u/v)' = (u'*v - u*v') / v^2
|
||||||
|
val numerator = BinaryExpression(
|
||||||
|
BinaryExpression(left.derivative(variable), BinaryOperator.MULTIPLY, right),
|
||||||
|
BinaryOperator.SUBTRACT,
|
||||||
|
BinaryExpression(left, BinaryOperator.MULTIPLY, right.derivative(variable))
|
||||||
|
)
|
||||||
|
val denominator = BinaryExpression(right, BinaryOperator.POWER, ConstantExpression(2.0, Units.dimensionless))
|
||||||
|
BinaryExpression(numerator, BinaryOperator.DIVIDE, denominator)
|
||||||
|
}
|
||||||
|
BinaryOperator.POWER -> {
|
||||||
|
if (right is ConstantExpression) {
|
||||||
|
// (u^n)' = n * u^(n-1) * u'
|
||||||
|
val n = right.value
|
||||||
|
val newExponent = ConstantExpression(n - 1.0, Units.dimensionless)
|
||||||
|
val baseDerivative = left.derivative(variable)
|
||||||
|
val powerExpression = BinaryExpression(left, BinaryOperator.POWER, newExponent)
|
||||||
|
BinaryExpression(
|
||||||
|
BinaryExpression(ConstantExpression(n, Units.dimensionless), BinaryOperator.MULTIPLY, powerExpression),
|
||||||
|
BinaryOperator.MULTIPLY,
|
||||||
|
baseDerivative
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
throw UnsupportedOperationException("Derivative of expression with variable exponent is not supported.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> throw UnsupportedOperationException("Derivative not implemented for operator $operator.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.expressions
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.dsl.core.units.PhysicalUnit
|
||||||
|
import space.kscience.controls.constructor.dsl.core.units.Units
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Класс для представления константного значения.
|
||||||
|
*/
|
||||||
|
public class ConstantExpression(public val value: Double, override val unit: PhysicalUnit) : Expression() {
|
||||||
|
override fun evaluate(values: Map<String, Double>): Double {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
override fun checkDimensions() {
|
||||||
|
// Константа имеет единицу измерения, дополнительных проверок не требуется
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun derivative(variable: VariableExpression): Expression {
|
||||||
|
return ConstantExpression(0.0, Units.dimensionless)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.expressions
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.dsl.core.units.PhysicalUnit
|
||||||
|
import space.kscience.controls.constructor.dsl.core.units.Units
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Класс для представления производной переменной по времени.
|
||||||
|
*/
|
||||||
|
public class DerivativeExpression(
|
||||||
|
public val variable: VariableExpression,
|
||||||
|
public val order: Int = 1 // Порядок производной (по умолчанию 1)
|
||||||
|
) : Expression() {
|
||||||
|
|
||||||
|
override val unit: PhysicalUnit = variable.unit / Units.second.pow(order.toDouble())
|
||||||
|
|
||||||
|
override fun evaluate(values: Map<String, Double>): Double {
|
||||||
|
// вычисление производной невозможно в этом контексте
|
||||||
|
throw UnsupportedOperationException("Cannot evaluate derivative directly.")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun checkDimensions() {
|
||||||
|
variable.checkDimensions()
|
||||||
|
// Единица измерения уже рассчитана
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun derivative(variable: VariableExpression): Expression {
|
||||||
|
// Производная от производной - это производная более высокого порядка
|
||||||
|
return DerivativeExpression(this.variable, this.order + 1)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.expressions
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.dsl.core.units.PhysicalUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Абстрактный класс для представления математических выражений.
|
||||||
|
* Каждое выражение связано с единицей измерения.
|
||||||
|
*/
|
||||||
|
public abstract class Expression {
|
||||||
|
public abstract val unit: PhysicalUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Метод для вычисления численного значения выражения.
|
||||||
|
* Параметр values содержит значения переменных.
|
||||||
|
*/
|
||||||
|
public abstract fun evaluate(values: Map<String, Double>): Double
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверка размерности выражения.
|
||||||
|
*/
|
||||||
|
public abstract fun checkDimensions()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Упрощение выражения.
|
||||||
|
*/
|
||||||
|
public open fun simplify(): Expression = this
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Производная выражения по заданной переменной.
|
||||||
|
*/
|
||||||
|
public open fun derivative(variable: VariableExpression): Expression {
|
||||||
|
throw UnsupportedOperationException("Derivative not implemented for this expression type.")
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,91 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.expressions
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.dsl.core.units.Units
|
||||||
|
import space.kscience.controls.constructor.dsl.core.units.PhysicalUnit
|
||||||
|
import kotlin.math.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Класс для представления вызова математической функции.
|
||||||
|
*/
|
||||||
|
public class FunctionCallExpression(
|
||||||
|
public val functionName: String,
|
||||||
|
public val arguments: List<Expression>
|
||||||
|
) : Expression() {
|
||||||
|
|
||||||
|
override val unit: PhysicalUnit = determineUnit()
|
||||||
|
|
||||||
|
private fun determineUnit(): PhysicalUnit {
|
||||||
|
return when (functionName) {
|
||||||
|
"sin", "cos", "tan", "asin", "acos", "atan",
|
||||||
|
"sinh", "cosh", "tanh", "asinh", "acosh", "atanh",
|
||||||
|
"exp", "log", "log10", "signum" -> Units.dimensionless
|
||||||
|
"sqrt", "cbrt", "abs" -> arguments[0].unit
|
||||||
|
"pow", "max", "min" -> arguments[0].unit
|
||||||
|
else -> throw IllegalArgumentException("Unknown function: $functionName")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun evaluate(values: Map<String, Double>): Double {
|
||||||
|
val argValues = arguments.map { it.evaluate(values) }
|
||||||
|
return when (functionName) {
|
||||||
|
"sin" -> sin(argValues[0])
|
||||||
|
"cos" -> cos(argValues[0])
|
||||||
|
"tan" -> tan(argValues[0])
|
||||||
|
"asin" -> asin(argValues[0])
|
||||||
|
"acos" -> acos(argValues[0])
|
||||||
|
"atan" -> atan(argValues[0])
|
||||||
|
"sinh" -> sinh(argValues[0])
|
||||||
|
"cosh" -> cosh(argValues[0])
|
||||||
|
"tanh" -> tanh(argValues[0])
|
||||||
|
"asinh" -> asinh(argValues[0])
|
||||||
|
"acosh" -> acosh(argValues[0])
|
||||||
|
"atanh" -> atanh(argValues[0])
|
||||||
|
"exp" -> exp(argValues[0])
|
||||||
|
"log" -> ln(argValues[0])
|
||||||
|
"log10" -> log10(argValues[0])
|
||||||
|
"sqrt" -> sqrt(argValues[0])
|
||||||
|
"cbrt" -> cbrt(argValues[0])
|
||||||
|
"abs" -> abs(argValues[0])
|
||||||
|
"signum" -> sign(argValues[0])
|
||||||
|
"pow" -> argValues[0].pow(argValues[1])
|
||||||
|
"max" -> max(argValues[0], argValues[1])
|
||||||
|
"min" -> min(argValues[0], argValues[1])
|
||||||
|
else -> throw IllegalArgumentException("Unknown function: $functionName")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun checkDimensions() {
|
||||||
|
when (functionName) {
|
||||||
|
"sin", "cos", "tan", "asin", "acos", "atan",
|
||||||
|
"sinh", "cosh", "tanh", "asinh", "acosh", "atanh",
|
||||||
|
"exp", "log", "log10", "signum" -> {
|
||||||
|
val argUnit = arguments[0].unit
|
||||||
|
if (!argUnit.dimension.isDimensionless()) {
|
||||||
|
throw IllegalArgumentException("Argument of $functionName must be dimensionless.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"sqrt", "cbrt", "abs" -> {
|
||||||
|
// Единица измерения сохраняется, дополнительных проверок не требуется
|
||||||
|
}
|
||||||
|
"pow" -> {
|
||||||
|
val exponentUnit = arguments[1].unit
|
||||||
|
if (!exponentUnit.dimension.isDimensionless()) {
|
||||||
|
throw IllegalArgumentException("Exponent in $functionName must be dimensionless.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"max", "min" -> {
|
||||||
|
if (arguments[0].unit != arguments[1].unit) {
|
||||||
|
throw IllegalArgumentException("Arguments of $functionName must have the same units.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> throw IllegalArgumentException("Unknown function: $functionName")
|
||||||
|
}
|
||||||
|
// Проверяем размерности аргументов
|
||||||
|
arguments.forEach { it.checkDimensions() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun derivative(variable: VariableExpression): Expression {
|
||||||
|
// Реализация производной для функций
|
||||||
|
throw UnsupportedOperationException("Derivative for function $functionName is not implemented.")
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.expressions
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.dsl.core.units.PhysicalUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Представляет частную производную переменной по другой переменной.
|
||||||
|
*/
|
||||||
|
public class PartialDerivativeExpression(
|
||||||
|
public val variable: VariableExpression,
|
||||||
|
public val respectTo: VariableExpression,
|
||||||
|
public val order: Int = 1
|
||||||
|
) : Expression() {
|
||||||
|
|
||||||
|
override val unit: PhysicalUnit = variable.unit / respectTo.unit.pow(order.toDouble())
|
||||||
|
|
||||||
|
override fun evaluate(values: Map<String, Double>): Double {
|
||||||
|
// Невозможно вычислить частную производную без дополнительного контекста
|
||||||
|
throw UnsupportedOperationException("Cannot evaluate partial derivative directly.")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun checkDimensions() {
|
||||||
|
variable.checkDimensions()
|
||||||
|
respectTo.checkDimensions()
|
||||||
|
// Единицы измерения уже рассчитаны
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun derivative(variable: VariableExpression): Expression {
|
||||||
|
// Производная частной производной не реализована
|
||||||
|
throw UnsupportedOperationException("Derivative of partial derivative is not implemented.")
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.expressions
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.dsl.core.units.PhysicalUnit
|
||||||
|
import space.kscience.controls.constructor.dsl.core.units.Units
|
||||||
|
import space.kscience.controls.constructor.dsl.core.variables.Variable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Класс для представления переменной в выражении.
|
||||||
|
*/
|
||||||
|
public class VariableExpression(public val variable: Variable) : Expression() {
|
||||||
|
override val unit: PhysicalUnit = variable.unit
|
||||||
|
|
||||||
|
override fun evaluate(values: Map<String, Double>): Double {
|
||||||
|
return values[variable.name] ?: throw IllegalArgumentException("Variable ${variable.name} is not defined.")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun checkDimensions() {
|
||||||
|
// Переменная уже имеет единицу измерения, дополнительных проверок не требуется
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun derivative(variable: VariableExpression): Expression {
|
||||||
|
return if (this.variable == variable.variable) {
|
||||||
|
ConstantExpression(1.0, Units.dimensionless)
|
||||||
|
} else {
|
||||||
|
ConstantExpression(0.0, Units.dimensionless)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun partialDerivative(respectTo: VariableExpression, order: Int = 1): PartialDerivativeExpression {
|
||||||
|
return PartialDerivativeExpression(this, respectTo, order)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.functions
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.dsl.core.algorithms.Algorithm
|
||||||
|
import space.kscience.controls.constructor.dsl.core.variables.Variable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Класс для представления пользовательской функции.
|
||||||
|
*/
|
||||||
|
public data class Function(
|
||||||
|
val name: String,
|
||||||
|
val inputs: List<Variable>,
|
||||||
|
val outputs: List<Variable>,
|
||||||
|
val algorithm: Algorithm
|
||||||
|
)
|
@ -0,0 +1,50 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.functions
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.dsl.core.algorithms.AlgorithmBuilder
|
||||||
|
import space.kscience.controls.constructor.dsl.core.algorithms.Algorithm
|
||||||
|
import space.kscience.controls.constructor.dsl.core.components.Model
|
||||||
|
import space.kscience.controls.constructor.dsl.core.units.PhysicalUnit
|
||||||
|
import space.kscience.controls.constructor.dsl.core.variables.Causality
|
||||||
|
import space.kscience.controls.constructor.dsl.core.variables.Variable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Класс для построения пользовательской функции.
|
||||||
|
*/
|
||||||
|
public class FunctionBuilder(private val name: String) {
|
||||||
|
public val inputs: MutableList<Variable> = mutableListOf<Variable>()
|
||||||
|
public val outputs: MutableList<Variable> = mutableListOf<Variable>()
|
||||||
|
private val algorithmBuilder = AlgorithmBuilder()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Определение входного параметра функции.
|
||||||
|
*/
|
||||||
|
public fun input(name: String, unit: PhysicalUnit): Variable {
|
||||||
|
val variable = Variable(name, unit, causality = Causality.INPUT)
|
||||||
|
inputs.add(variable)
|
||||||
|
return variable
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Определение выходного параметра функции.
|
||||||
|
*/
|
||||||
|
public fun output(name: String, unit: PhysicalUnit): Variable {
|
||||||
|
val variable = Variable(name, unit, causality = Causality.OUTPUT)
|
||||||
|
outputs.add(variable)
|
||||||
|
return variable
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Определение алгоритмического тела функции.
|
||||||
|
*/
|
||||||
|
public fun algorithm(init: AlgorithmBuilder.() -> Unit) {
|
||||||
|
algorithmBuilder.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Построение функции.
|
||||||
|
*/
|
||||||
|
public fun build(): Function {
|
||||||
|
val algorithm = algorithmBuilder.build()
|
||||||
|
return Function(name, inputs, outputs, algorithm)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.simulation
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import space.kscience.controls.constructor.dsl.core.EquationSystem
|
||||||
|
import space.kscience.controls.constructor.dsl.core.events.Event
|
||||||
|
|
||||||
|
public interface NumericSolver {
|
||||||
|
/**
|
||||||
|
* Инициализирует решатель.
|
||||||
|
*/
|
||||||
|
public suspend fun initialize(
|
||||||
|
equationSystem: EquationSystem,
|
||||||
|
context: SimulationContext
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Выполняет один шаг симуляции.
|
||||||
|
*/
|
||||||
|
public suspend fun step(context: SimulationContext, timeStep: Double): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Запускает симуляцию.
|
||||||
|
*/
|
||||||
|
public suspend fun solve(
|
||||||
|
equationSystem: EquationSystem,
|
||||||
|
context: SimulationContext,
|
||||||
|
timeStart: Double,
|
||||||
|
timeEnd: Double,
|
||||||
|
timeStep: Double
|
||||||
|
): SimulationResult
|
||||||
|
}
|
||||||
|
|
||||||
|
public data class SimulationResult(
|
||||||
|
val timePoints: List<Double>,
|
||||||
|
val variableValues: Map<String, List<Double>>
|
||||||
|
)
|
@ -0,0 +1,93 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.simulation
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import space.kscience.controls.constructor.dsl.core.events.Event
|
||||||
|
import space.kscience.controls.constructor.dsl.core.variables.Variable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Класс для хранения состояния симуляции.
|
||||||
|
*/
|
||||||
|
public class SimulationContext(
|
||||||
|
private val scope: CoroutineScope
|
||||||
|
) {
|
||||||
|
public var currentTime: Double = 0.0
|
||||||
|
private val _variableValues: MutableMap<String, MutableStateFlow<Double>> = mutableMapOf()
|
||||||
|
|
||||||
|
// Канал для событий
|
||||||
|
private val eventChannel = Channel<Event>()
|
||||||
|
|
||||||
|
// Список событий
|
||||||
|
public val events: MutableList<Event> = mutableListOf()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Регистрирует переменную в контексте симуляции.
|
||||||
|
*/
|
||||||
|
public fun registerVariable(variable: Variable) {
|
||||||
|
_variableValues[variable.name] = MutableStateFlow(variable.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обновляет значение переменной.
|
||||||
|
*/
|
||||||
|
public suspend fun updateVariable(name: String, value: Double) {
|
||||||
|
_variableValues[name]?.emit(value)
|
||||||
|
?: throw IllegalArgumentException("Variable $name is not defined in the simulation context.")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает текущее значение переменной.
|
||||||
|
*/
|
||||||
|
public fun getCurrentVariableValue(name: String): Double {
|
||||||
|
return _variableValues[name]?.value
|
||||||
|
?: throw IllegalArgumentException("Variable $name is not defined in the simulation context.")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Возвращает текущие значения всех переменных.
|
||||||
|
*/
|
||||||
|
public fun getVariableValues(): Map<String, Double> {
|
||||||
|
return _variableValues.mapValues { it.value.value }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отправляет событие на обработку.
|
||||||
|
*/
|
||||||
|
public suspend fun sendEvent(event: Event) {
|
||||||
|
eventChannel.send(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Запускает обработку событий.
|
||||||
|
*/
|
||||||
|
public fun startEventHandling() {
|
||||||
|
scope.launch {
|
||||||
|
for (event in eventChannel) {
|
||||||
|
if (event.checkEvent(this@SimulationContext)) {
|
||||||
|
event.executeActions(this@SimulationContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обрабатывает события из списка events.
|
||||||
|
*/
|
||||||
|
public suspend fun handleEvents() {
|
||||||
|
for (event in events) {
|
||||||
|
if (event.checkEvent(this)) {
|
||||||
|
event.executeActions(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Увеличивает время симуляции.
|
||||||
|
*/
|
||||||
|
public suspend fun advanceTime(deltaTime: Double) {
|
||||||
|
currentTime += deltaTime
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,78 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.units
|
||||||
|
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Класс для представления размерности физической величины.
|
||||||
|
*/
|
||||||
|
public data class Dimension(
|
||||||
|
val length: Double = 0.0,
|
||||||
|
val mass: Double = 0.0,
|
||||||
|
val time: Double = 0.0,
|
||||||
|
val electricCurrent: Double = 0.0,
|
||||||
|
val temperature: Double = 0.0,
|
||||||
|
val amountOfSubstance: Double = 0.0,
|
||||||
|
val luminousIntensity: Double = 0.0
|
||||||
|
) {
|
||||||
|
public operator fun plus(other: Dimension): Dimension {
|
||||||
|
return Dimension(
|
||||||
|
length + other.length,
|
||||||
|
mass + other.mass,
|
||||||
|
time + other.time,
|
||||||
|
electricCurrent + other.electricCurrent,
|
||||||
|
temperature + other.temperature,
|
||||||
|
amountOfSubstance + other.amountOfSubstance,
|
||||||
|
luminousIntensity + other.luminousIntensity
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public operator fun minus(other: Dimension): Dimension {
|
||||||
|
return Dimension(
|
||||||
|
length - other.length,
|
||||||
|
mass - other.mass,
|
||||||
|
time - other.time,
|
||||||
|
electricCurrent - other.electricCurrent,
|
||||||
|
temperature - other.temperature,
|
||||||
|
amountOfSubstance - other.amountOfSubstance,
|
||||||
|
luminousIntensity - other.luminousIntensity
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public operator fun times(factor: Double): Dimension {
|
||||||
|
return Dimension(
|
||||||
|
length * factor,
|
||||||
|
mass * factor,
|
||||||
|
time * factor,
|
||||||
|
electricCurrent * factor,
|
||||||
|
temperature * factor,
|
||||||
|
amountOfSubstance * factor,
|
||||||
|
luminousIntensity * factor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет, является ли размерность безразмерной.
|
||||||
|
*/
|
||||||
|
public fun isDimensionless(epsilon: Double = 1e-10): Boolean {
|
||||||
|
return abs(length) < epsilon &&
|
||||||
|
abs(mass) < epsilon &&
|
||||||
|
abs(time) < epsilon &&
|
||||||
|
abs(electricCurrent) < epsilon &&
|
||||||
|
abs(temperature) < epsilon &&
|
||||||
|
abs(amountOfSubstance) < epsilon &&
|
||||||
|
abs(luminousIntensity) < epsilon
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет совместимость размерностей.
|
||||||
|
*/
|
||||||
|
public fun isCompatibleWith(other: Dimension, epsilon: Double = 1e-10): Boolean {
|
||||||
|
return abs(length - other.length) < epsilon &&
|
||||||
|
abs(mass - other.mass) < epsilon &&
|
||||||
|
abs(time - other.time) < epsilon &&
|
||||||
|
abs(electricCurrent - other.electricCurrent) < epsilon &&
|
||||||
|
abs(temperature - other.temperature) < epsilon &&
|
||||||
|
abs(amountOfSubstance - other.amountOfSubstance) < epsilon &&
|
||||||
|
abs(luminousIntensity - other.luminousIntensity) < epsilon
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.units
|
||||||
|
|
||||||
|
import kotlin.math.pow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Класс для представления физической единицы измерения.
|
||||||
|
*/
|
||||||
|
public data class PhysicalUnit(
|
||||||
|
val name: String,
|
||||||
|
val symbol: String,
|
||||||
|
val conversionFactorToSI: Double,
|
||||||
|
val dimension: Dimension
|
||||||
|
) {
|
||||||
|
public operator fun times(other: PhysicalUnit): PhysicalUnit {
|
||||||
|
return PhysicalUnit(
|
||||||
|
name = "$name·${other.name}",
|
||||||
|
symbol = "$symbol·${other.symbol}",
|
||||||
|
conversionFactorToSI = this.conversionFactorToSI * other.conversionFactorToSI,
|
||||||
|
dimension = this.dimension + other.dimension
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public operator fun div(other: PhysicalUnit): PhysicalUnit {
|
||||||
|
return PhysicalUnit(
|
||||||
|
name = "$name/${other.name}",
|
||||||
|
symbol = "$symbol/${other.symbol}",
|
||||||
|
conversionFactorToSI = this.conversionFactorToSI / other.conversionFactorToSI,
|
||||||
|
dimension = this.dimension - other.dimension
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun pow(exponent: Double): PhysicalUnit {
|
||||||
|
return PhysicalUnit(
|
||||||
|
name = "$name^$exponent",
|
||||||
|
symbol = "${symbol}^$exponent",
|
||||||
|
conversionFactorToSI = this.conversionFactorToSI.pow(exponent),
|
||||||
|
dimension = this.dimension * exponent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Преобразует значение из текущей единицы в целевую единицу.
|
||||||
|
*/
|
||||||
|
public fun convertValueTo(valueInThisUnit: Double, targetUnit: PhysicalUnit): Double {
|
||||||
|
require(this.dimension.isCompatibleWith(targetUnit.dimension)) {
|
||||||
|
"Units are not compatible: $this and $targetUnit"
|
||||||
|
}
|
||||||
|
val valueInSI = valueInThisUnit * this.conversionFactorToSI
|
||||||
|
return valueInSI / targetUnit.conversionFactorToSI
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,89 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.units
|
||||||
|
|
||||||
|
import kotlin.math.*
|
||||||
|
|
||||||
|
public object Units {
|
||||||
|
private val unitsRegistry = mutableMapOf<String, PhysicalUnit>()
|
||||||
|
|
||||||
|
// Основные единицы СИ
|
||||||
|
public val meter: PhysicalUnit = PhysicalUnit("meter", "m", 1.0, Dimension(length = 1.0))
|
||||||
|
public val kilogram: PhysicalUnit = PhysicalUnit("kilogram", "kg", 1.0, Dimension(mass = 1.0))
|
||||||
|
public val second: PhysicalUnit = PhysicalUnit("second", "s", 1.0, Dimension(time = 1.0))
|
||||||
|
public val ampere: PhysicalUnit = PhysicalUnit("ampere", "A", 1.0, Dimension(electricCurrent = 1.0))
|
||||||
|
public val kelvin: PhysicalUnit = PhysicalUnit("kelvin", "K", 1.0, Dimension(temperature = 1.0))
|
||||||
|
public val mole: PhysicalUnit = PhysicalUnit("mole", "mol", 1.0, Dimension(amountOfSubstance = 1.0))
|
||||||
|
public val candela: PhysicalUnit = PhysicalUnit("candela", "cd", 1.0, Dimension(luminousIntensity = 1.0))
|
||||||
|
public val radian: PhysicalUnit = PhysicalUnit("radian", "rad", 1.0, Dimension())
|
||||||
|
public val hertz: PhysicalUnit = PhysicalUnit("hertz", "Hz", 1.0, Dimension(time = -1.0))
|
||||||
|
|
||||||
|
public val dimensionless: PhysicalUnit = PhysicalUnit("dimensionless", "", 1.0, Dimension())
|
||||||
|
|
||||||
|
init {
|
||||||
|
// Регистрируем основные единицы
|
||||||
|
registerUnit(meter)
|
||||||
|
registerUnit(kilogram)
|
||||||
|
registerUnit(second)
|
||||||
|
registerUnit(ampere)
|
||||||
|
registerUnit(kelvin)
|
||||||
|
registerUnit(mole)
|
||||||
|
registerUnit(candela)
|
||||||
|
registerUnit(radian)
|
||||||
|
registerUnit(hertz)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Регистрирует единицу измерения в реестре.
|
||||||
|
*/
|
||||||
|
public fun registerUnit(unit: PhysicalUnit) {
|
||||||
|
unitsRegistry[unit.name] = unit
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает единицу измерения по имени.
|
||||||
|
*/
|
||||||
|
public fun getUnit(name: String): PhysicalUnit? {
|
||||||
|
return unitsRegistry[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Список префиксов СИ.
|
||||||
|
*/
|
||||||
|
public enum class Prefix(public val prefixName: String, public val symbol: String, public val factor: Double) {
|
||||||
|
YOTTA("yotta", "Y", 1e24),
|
||||||
|
ZETTA("zetta", "Z", 1e21),
|
||||||
|
EXA("exa", "E", 1e18),
|
||||||
|
PETA("peta", "P", 1e15),
|
||||||
|
TERA("tera", "T", 1e12),
|
||||||
|
GIGA("giga", "G", 1e9),
|
||||||
|
MEGA("mega", "M", 1e6),
|
||||||
|
KILO("kilo", "k", 1e3),
|
||||||
|
HECTO("hecto", "h", 1e2),
|
||||||
|
DECA("deca", "da", 1e1),
|
||||||
|
DECI("deci", "d", 1e-1),
|
||||||
|
CENTI("centi", "c", 1e-2),
|
||||||
|
MILLI("milli", "m", 1e-3),
|
||||||
|
MICRO("micro", "μ", 1e-6),
|
||||||
|
NANO("nano", "n", 1e-9),
|
||||||
|
PICO("pico", "p", 1e-12),
|
||||||
|
FEMTO("femto", "f", 1e-15),
|
||||||
|
ATTO("atto", "a", 1e-18),
|
||||||
|
ZEPTO("zepto", "z", 1e-21),
|
||||||
|
YOCTO("yocto", "y", 1e-24)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создает новую единицу с заданным префиксом.
|
||||||
|
*/
|
||||||
|
public fun withPrefix(prefix: Prefix, unit: PhysicalUnit): PhysicalUnit {
|
||||||
|
val newName = "${prefix.prefixName}${unit.name}"
|
||||||
|
val newSymbol = "${prefix.symbol}${unit.symbol}"
|
||||||
|
val newConversionFactor = unit.conversionFactorToSI * prefix.factor
|
||||||
|
val newUnit = PhysicalUnit(newName, newSymbol, newConversionFactor, unit.dimension)
|
||||||
|
registerUnit(newUnit)
|
||||||
|
return newUnit
|
||||||
|
}
|
||||||
|
|
||||||
|
public val kilometer: PhysicalUnit = withPrefix(Prefix.KILO, meter)
|
||||||
|
public val millimeter: PhysicalUnit = withPrefix(Prefix.MILLI, meter)
|
||||||
|
public val microsecond: PhysicalUnit = withPrefix(Prefix.MICRO, second)
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.variables
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.dsl.core.units.PhysicalUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Абстрактный класс для переменных.
|
||||||
|
*/
|
||||||
|
public abstract class AbstractVariable(
|
||||||
|
public val name: String,
|
||||||
|
public val unit: PhysicalUnit,
|
||||||
|
public val variability: Variability,
|
||||||
|
public val causality: Causality
|
||||||
|
)
|
@ -0,0 +1,28 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.variables
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.dsl.core.units.PhysicalUnit
|
||||||
|
import org.jetbrains.kotlinx.multik.ndarray.data.D2
|
||||||
|
import org.jetbrains.kotlinx.multik.ndarray.data.NDArray
|
||||||
|
import org.jetbrains.kotlinx.multik.ndarray.data.set
|
||||||
|
import org.jetbrains.kotlinx.multik.ndarray.operations.plus
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Класс для представления переменной-массива.
|
||||||
|
* Использует Multik для работы с многомерными массивами.
|
||||||
|
*/
|
||||||
|
public class ArrayVariable(
|
||||||
|
name: String,
|
||||||
|
unit: PhysicalUnit,
|
||||||
|
public val array: NDArray<Double, D2>,
|
||||||
|
causality: Causality = Causality.INTERNAL
|
||||||
|
) : AbstractVariable(name, unit, Variability.CONTINUOUS, causality) {
|
||||||
|
|
||||||
|
public fun add(other: ArrayVariable): ArrayVariable {
|
||||||
|
require(this.array.shape contentEquals other.array.shape) {
|
||||||
|
"Array shapes do not match."
|
||||||
|
}
|
||||||
|
val resultArray = this.array + other.array
|
||||||
|
return ArrayVariable("$name + ${other.name}", unit, resultArray, causality)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.variables
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Перечисление каузальности переменной.
|
||||||
|
*/
|
||||||
|
public enum class Causality {
|
||||||
|
INPUT,
|
||||||
|
OUTPUT,
|
||||||
|
INTERNAL
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.variables
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.dsl.core.units.PhysicalUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Класс для представления константы.
|
||||||
|
*/
|
||||||
|
public class ConstantVariable(
|
||||||
|
name: String,
|
||||||
|
unit: PhysicalUnit,
|
||||||
|
public val value: Double
|
||||||
|
) : AbstractVariable(name, unit, Variability.CONSTANT, Causality.INTERNAL)
|
@ -0,0 +1,22 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.variables
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.dsl.core.units.PhysicalUnit
|
||||||
|
|
||||||
|
public class ContinuousVariable(
|
||||||
|
name: String,
|
||||||
|
unit: PhysicalUnit,
|
||||||
|
public var value: Double = 0.0,
|
||||||
|
public val min: Double? = null,
|
||||||
|
public val max: Double? = null,
|
||||||
|
causality: Causality = Causality.INTERNAL
|
||||||
|
) : AbstractVariable(name, unit, Variability.CONTINUOUS, causality) {
|
||||||
|
init {
|
||||||
|
// Проверка допустимого диапазона
|
||||||
|
if (min != null && value < min) {
|
||||||
|
throw IllegalArgumentException("Value of $name is less than minimum allowed value.")
|
||||||
|
}
|
||||||
|
if (max != null && value > max) {
|
||||||
|
throw IllegalArgumentException("Value of $name is greater than maximum allowed value.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.variables
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.dsl.core.units.PhysicalUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Класс для представления дискретной переменной.
|
||||||
|
*/
|
||||||
|
public class DiscreteVariable(
|
||||||
|
name: String,
|
||||||
|
unit: PhysicalUnit,
|
||||||
|
public var value: Double = 0.0,
|
||||||
|
causality: Causality = Causality.INTERNAL
|
||||||
|
) : AbstractVariable(name, unit, Variability.DISCRETE, causality)
|
@ -0,0 +1,25 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.variables
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.dsl.core.units.PhysicalUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Класс для представления параметра (неизменяемой величины).
|
||||||
|
*/
|
||||||
|
public open class ParameterVariable(
|
||||||
|
name: String,
|
||||||
|
unit: PhysicalUnit,
|
||||||
|
public val value: Double,
|
||||||
|
public val min: Double? = null,
|
||||||
|
public val max: Double? = null,
|
||||||
|
causality: Causality = Causality.INTERNAL
|
||||||
|
) : AbstractVariable(name, unit, Variability.PARAMETER, causality) {
|
||||||
|
init {
|
||||||
|
// Проверка допустимого диапазона
|
||||||
|
if (min != null && value < min) {
|
||||||
|
throw IllegalArgumentException("Value of parameter $name is less than minimum allowed value.")
|
||||||
|
}
|
||||||
|
if (max != null && value > max) {
|
||||||
|
throw IllegalArgumentException("Value of parameter $name is greater than maximum allowed value.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.variables
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Перечисление типов изменчивости переменной.
|
||||||
|
*/
|
||||||
|
public enum class Variability {
|
||||||
|
CONSTANT,
|
||||||
|
PARAMETER,
|
||||||
|
DISCRETE,
|
||||||
|
CONTINUOUS
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
package space.kscience.controls.constructor.dsl.core.variables
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.dsl.core.units.PhysicalUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Класс для представления переменной.
|
||||||
|
*/
|
||||||
|
public open class Variable(
|
||||||
|
public val name: String,
|
||||||
|
public val unit: PhysicalUnit,
|
||||||
|
public open var value: Double = 0.0,
|
||||||
|
public val min: Double? = null,
|
||||||
|
public val max: Double? = null,
|
||||||
|
public var variability: Variability = Variability.CONTINUOUS,
|
||||||
|
public var causality: Causality = Causality.INTERNAL
|
||||||
|
) {
|
||||||
|
init {
|
||||||
|
// Проверка, что значение находится в допустимом диапазоне
|
||||||
|
if (min != null && value < min) {
|
||||||
|
throw IllegalArgumentException("Value of $name is less than minimum allowed value.")
|
||||||
|
}
|
||||||
|
if (max != null && value > max) {
|
||||||
|
throw IllegalArgumentException("Value of $name is greater than maximum allowed value.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public open fun checkDimensions() {}
|
||||||
|
}
|
@ -0,0 +1,65 @@
|
|||||||
|
package space.kscience.controls.constructor
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.time.Duration
|
||||||
|
|
||||||
|
|
||||||
|
private open class ExternalState<T>(
|
||||||
|
val scope: CoroutineScope,
|
||||||
|
val readInterval: Duration,
|
||||||
|
initialValue: T,
|
||||||
|
val reader: suspend () -> T,
|
||||||
|
) : DeviceState<T> {
|
||||||
|
|
||||||
|
protected val flow: StateFlow<T> = flow {
|
||||||
|
while (true) {
|
||||||
|
delay(readInterval)
|
||||||
|
emit(reader())
|
||||||
|
}
|
||||||
|
}.stateIn(scope, SharingStarted.Eagerly, initialValue)
|
||||||
|
|
||||||
|
override val value: T get() = flow.value
|
||||||
|
override val valueFlow: Flow<T> get() = flow
|
||||||
|
|
||||||
|
override fun toString(): String = "ExternalState()"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a [DeviceState] which is constructed by regularly reading external value
|
||||||
|
*/
|
||||||
|
public fun <T> DeviceState.Companion.external(
|
||||||
|
scope: CoroutineScope,
|
||||||
|
readInterval: Duration,
|
||||||
|
initialValue: T,
|
||||||
|
reader: suspend () -> T,
|
||||||
|
): DeviceState<T> = ExternalState(scope, readInterval, initialValue, reader)
|
||||||
|
|
||||||
|
private class MutableExternalState<T>(
|
||||||
|
scope: CoroutineScope,
|
||||||
|
readInterval: Duration,
|
||||||
|
initialValue: T,
|
||||||
|
reader: suspend () -> T,
|
||||||
|
val writer: suspend (T) -> Unit,
|
||||||
|
) : ExternalState<T>(scope, readInterval, initialValue, reader), MutableDeviceState<T> {
|
||||||
|
override var value: T
|
||||||
|
get() = super.value
|
||||||
|
set(value) {
|
||||||
|
scope.launch {
|
||||||
|
writer(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a [MutableDeviceState] which is constructed by regularly reading external value and allows writing
|
||||||
|
*/
|
||||||
|
public fun <T> DeviceState.Companion.external(
|
||||||
|
scope: CoroutineScope,
|
||||||
|
readInterval: Duration,
|
||||||
|
initialValue: T,
|
||||||
|
reader: suspend () -> T,
|
||||||
|
writer: suspend (T) -> Unit,
|
||||||
|
): MutableDeviceState<T> = MutableExternalState(scope, readInterval, initialValue, reader, writer)
|
@ -0,0 +1,20 @@
|
|||||||
|
package space.kscience.controls.constructor
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
||||||
|
|
||||||
|
private class StateFlowAsState<T>(
|
||||||
|
val flow: MutableStateFlow<T>,
|
||||||
|
) : MutableDeviceState<T> {
|
||||||
|
override var value: T by flow::value
|
||||||
|
override val valueFlow: Flow<T> get() = flow
|
||||||
|
|
||||||
|
override fun toString(): String = "FlowAsState($value)"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a read-only [DeviceState] that wraps [MutableStateFlow].
|
||||||
|
* No data copy is performed.
|
||||||
|
*/
|
||||||
|
public fun <T> MutableStateFlow<T>.asDeviceState(): MutableDeviceState<T> = StateFlowAsState(this)
|
@ -0,0 +1,53 @@
|
|||||||
|
package space.kscience.controls.constructor
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [MutableDeviceState] that does not correspond to a physical state
|
||||||
|
*
|
||||||
|
* @param callback a synchronous callback that could be used without a scope
|
||||||
|
*/
|
||||||
|
private class VirtualDeviceState<T>(
|
||||||
|
initialValue: T,
|
||||||
|
private val callback: (T) -> Unit = {}
|
||||||
|
) : MutableDeviceState<T> {
|
||||||
|
private val flow = MutableStateFlow(initialValue)
|
||||||
|
override val valueFlow: Flow<T> get() = flow
|
||||||
|
|
||||||
|
override var value: T
|
||||||
|
get() = flow.value
|
||||||
|
set(value) {
|
||||||
|
flow.value = value
|
||||||
|
callback(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String = "VirtualDeviceState($value)"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [MutableDeviceState] that does not correspond to a physical state
|
||||||
|
*
|
||||||
|
* @param callback a synchronous callback that could be used without a scope
|
||||||
|
*/
|
||||||
|
public fun <T> MutableDeviceState(
|
||||||
|
initialValue: T,
|
||||||
|
callback: (T) -> Unit = {}
|
||||||
|
): MutableDeviceState<T> = VirtualDeviceState(initialValue, callback)
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a [DeviceState] with constant value
|
||||||
|
*/
|
||||||
|
public fun <T> DeviceState(
|
||||||
|
value: T
|
||||||
|
): DeviceState<T> = object : DeviceState<T> {
|
||||||
|
override val value: T get() = value
|
||||||
|
override val valueFlow: Flow<T>
|
||||||
|
get() = emptyFlow()
|
||||||
|
|
||||||
|
override fun toString(): String = "ConstDeviceState($value)"
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,70 @@
|
|||||||
|
package space.kscience.controls.constructor.models
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.*
|
||||||
|
import space.kscience.controls.constructor.units.*
|
||||||
|
import space.kscience.dataforge.context.Context
|
||||||
|
import kotlin.math.pow
|
||||||
|
import kotlin.time.DurationUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A model for inertial movement. Both linear and angular
|
||||||
|
*/
|
||||||
|
public class Inertia<U : UnitsOfMeasurement, V : UnitsOfMeasurement>(
|
||||||
|
context: Context,
|
||||||
|
force: DeviceState<Double>, //TODO add system unit sets
|
||||||
|
inertia: Double,
|
||||||
|
public val position: MutableDeviceState<NumericalValue<U>>,
|
||||||
|
public val velocity: MutableDeviceState<NumericalValue<V>>,
|
||||||
|
) : ModelConstructor(context) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
registerState(position)
|
||||||
|
registerState(velocity)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var currentForce = force.value
|
||||||
|
|
||||||
|
private val movement = onTimer(DefaultTimer.REALTIME) { prev, next ->
|
||||||
|
val dtSeconds = (next - prev).toDouble(DurationUnit.SECONDS)
|
||||||
|
|
||||||
|
// compute new value based on velocity and acceleration from the previous step
|
||||||
|
position.value += NumericalValue(velocity.value.value * dtSeconds + currentForce / inertia * dtSeconds.pow(2) / 2)
|
||||||
|
|
||||||
|
// compute new velocity based on acceleration on the previous step
|
||||||
|
velocity.value += NumericalValue(currentForce / inertia * dtSeconds)
|
||||||
|
currentForce = force.value
|
||||||
|
}
|
||||||
|
|
||||||
|
public companion object {
|
||||||
|
/**
|
||||||
|
* Linear inertial model with [force] in newtons and [mass] in kilograms
|
||||||
|
*/
|
||||||
|
public fun linear(
|
||||||
|
context: Context,
|
||||||
|
force: DeviceState<NumericalValue<Newtons>>,
|
||||||
|
mass: NumericalValue<Kilograms>,
|
||||||
|
position: MutableDeviceState<NumericalValue<Meters>>,
|
||||||
|
velocity: MutableDeviceState<NumericalValue<MetersPerSecond>> = MutableDeviceState(NumericalValue(0.0)),
|
||||||
|
): Inertia<Meters, MetersPerSecond> = Inertia(
|
||||||
|
context = context,
|
||||||
|
force = force.values(),
|
||||||
|
inertia = mass.value,
|
||||||
|
position = position,
|
||||||
|
velocity = velocity
|
||||||
|
)
|
||||||
|
|
||||||
|
public fun circular(
|
||||||
|
context: Context,
|
||||||
|
force: DeviceState<NumericalValue<NewtonsMeters>>,
|
||||||
|
momentOfInertia: NumericalValue<KgM2>,
|
||||||
|
position: MutableDeviceState<NumericalValue<Degrees>>,
|
||||||
|
velocity: MutableDeviceState<NumericalValue<DegreesPerSecond>> = MutableDeviceState(NumericalValue(0.0)),
|
||||||
|
): Inertia<Degrees, DegreesPerSecond> = Inertia(
|
||||||
|
context = context,
|
||||||
|
force = force.values(),
|
||||||
|
inertia = momentOfInertia.value,
|
||||||
|
position = position,
|
||||||
|
velocity = velocity
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
package space.kscience.controls.constructor.models
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.DeviceState
|
||||||
|
import space.kscience.controls.constructor.ModelConstructor
|
||||||
|
import space.kscience.controls.constructor.map
|
||||||
|
import space.kscience.controls.constructor.units.*
|
||||||
|
import space.kscience.dataforge.context.Context
|
||||||
|
import kotlin.math.PI
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://en.wikipedia.org/wiki/Leadscrew
|
||||||
|
*/
|
||||||
|
public class Leadscrew(
|
||||||
|
context: Context,
|
||||||
|
public val leverage: NumericalValue<Meters>,
|
||||||
|
) : ModelConstructor(context) {
|
||||||
|
|
||||||
|
public fun torqueToForce(
|
||||||
|
stateOfTorque: DeviceState<NumericalValue<NewtonsMeters>>,
|
||||||
|
): DeviceState<NumericalValue<Newtons>> = DeviceState.map(stateOfTorque) { torque ->
|
||||||
|
NumericalValue(torque.value / leverage.value )
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun degreesToMeters(
|
||||||
|
stateOfAngle: DeviceState<NumericalValue<Degrees>>,
|
||||||
|
offset: NumericalValue<Meters> = NumericalValue(0),
|
||||||
|
): DeviceState<NumericalValue<Meters>> = DeviceState.map(stateOfAngle) { degrees ->
|
||||||
|
offset + NumericalValue(degrees.value * 2 * PI / 360 * leverage.value )
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
package space.kscience.controls.constructor.models
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.*
|
||||||
|
import space.kscience.controls.constructor.units.*
|
||||||
|
import space.kscience.dataforge.context.Context
|
||||||
|
import kotlin.math.pow
|
||||||
|
import kotlin.time.DurationUnit
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 3D material point
|
||||||
|
*/
|
||||||
|
public class MaterialPoint(
|
||||||
|
context: Context,
|
||||||
|
force: DeviceState<XYZ<Newtons>>,
|
||||||
|
mass: NumericalValue<Kilograms>,
|
||||||
|
public val position: MutableDeviceState<XYZ<Meters>>,
|
||||||
|
public val velocity: MutableDeviceState<XYZ<MetersPerSecond>>,
|
||||||
|
) : ModelConstructor(context) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
registerState(position)
|
||||||
|
registerState(velocity)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var currentForce = force.value
|
||||||
|
|
||||||
|
private val movement = onTimer(
|
||||||
|
DefaultTimer.REALTIME,
|
||||||
|
reads = setOf(velocity, position),
|
||||||
|
writes = setOf(velocity, position)
|
||||||
|
) { prev, next ->
|
||||||
|
val dtSeconds = (next - prev).toDouble(DurationUnit.SECONDS)
|
||||||
|
|
||||||
|
// compute new value based on velocity and acceleration from the previous step
|
||||||
|
val deltaR = (velocity.value * dtSeconds).cast(Meters) +
|
||||||
|
(currentForce / mass.value * dtSeconds.pow(2) / 2).cast(Meters)
|
||||||
|
position.value += deltaR
|
||||||
|
|
||||||
|
// compute new velocity based on acceleration on the previous step
|
||||||
|
val deltaV = (currentForce / mass.value * dtSeconds).cast(MetersPerSecond)
|
||||||
|
//TODO apply energy correction
|
||||||
|
//val work = deltaR.length.value * currentForce.length.value
|
||||||
|
velocity.value += deltaV
|
||||||
|
|
||||||
|
currentForce = force.value
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,73 @@
|
|||||||
|
package space.kscience.controls.constructor.models
|
||||||
|
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import space.kscience.controls.constructor.*
|
||||||
|
import space.kscience.controls.constructor.units.*
|
||||||
|
import space.kscience.controls.manager.clock
|
||||||
|
import space.kscience.dataforge.context.Context
|
||||||
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
import kotlin.time.DurationUnit
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pid regulator parameters
|
||||||
|
*/
|
||||||
|
public data class PidParameters(
|
||||||
|
val kp: Double,
|
||||||
|
val ki: Double,
|
||||||
|
val kd: Double,
|
||||||
|
val timeStep: Duration = 10.milliseconds,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A PID regulator
|
||||||
|
*
|
||||||
|
* @param P units of position values
|
||||||
|
* @param O units of output values
|
||||||
|
*/
|
||||||
|
public class PidRegulator<P : UnitsOfMeasurement, O : UnitsOfMeasurement>(
|
||||||
|
context: Context,
|
||||||
|
private val position: DeviceState<NumericalValue<P>>,
|
||||||
|
public var pidParameters: PidParameters, // TODO expose as property
|
||||||
|
output: MutableDeviceState<NumericalValue<O>> = MutableDeviceState(NumericalValue(0.0)),
|
||||||
|
private val convertOutput: (NumericalValue<P>) -> NumericalValue<O> = { NumericalValue(it.value) },
|
||||||
|
) : ModelConstructor(context) {
|
||||||
|
|
||||||
|
public val target: MutableDeviceState<NumericalValue<P>> = stateOf(NumericalValue(0.0))
|
||||||
|
public val output: MutableDeviceState<NumericalValue<O>> = registerState(output)
|
||||||
|
|
||||||
|
private val updateJob = launch {
|
||||||
|
var lastPosition: NumericalValue<P> = target.value
|
||||||
|
|
||||||
|
var integral: NumericalValue<P> = NumericalValue(0.0)
|
||||||
|
|
||||||
|
val mutex = Mutex()
|
||||||
|
|
||||||
|
val clock = context.clock
|
||||||
|
|
||||||
|
var lastTime = clock.now()
|
||||||
|
|
||||||
|
while (isActive) {
|
||||||
|
delay(pidParameters.timeStep)
|
||||||
|
mutex.withLock {
|
||||||
|
val realTime = clock.now()
|
||||||
|
val delta: NumericalValue<P> = target.value - position.value
|
||||||
|
val dtSeconds = (realTime - lastTime).toDouble(DurationUnit.SECONDS)
|
||||||
|
integral += delta * dtSeconds
|
||||||
|
val derivative = (position.value - lastPosition) / dtSeconds
|
||||||
|
|
||||||
|
//set last time and value to new values
|
||||||
|
lastTime = realTime
|
||||||
|
lastPosition = position.value
|
||||||
|
|
||||||
|
output.value =
|
||||||
|
convertOutput(pidParameters.kp * delta + pidParameters.ki * integral + pidParameters.kd * derivative)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,70 @@
|
|||||||
|
package space.kscience.controls.constructor.models
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import space.kscience.controls.constructor.DeviceState
|
||||||
|
import space.kscience.controls.constructor.MutableDeviceState
|
||||||
|
import space.kscience.controls.constructor.map
|
||||||
|
import space.kscience.controls.constructor.units.NumericalValue
|
||||||
|
import space.kscience.controls.constructor.units.UnitsOfMeasurement
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A state describing a [T] value in the [range]
|
||||||
|
*/
|
||||||
|
public open class RangeState<T : Comparable<T>>(
|
||||||
|
private val input: DeviceState<T>,
|
||||||
|
public val range: ClosedRange<T>,
|
||||||
|
) : DeviceState<T> {
|
||||||
|
|
||||||
|
override val valueFlow: Flow<T> get() = input.valueFlow.map {
|
||||||
|
it.coerceIn(range)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val value: T get() = input.value.coerceIn(range)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A state showing that the range is on its lower boundary
|
||||||
|
*/
|
||||||
|
public val atStart: DeviceState<Boolean> = input.map { it <= range.start }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A state showing that the range is on its higher boundary
|
||||||
|
*/
|
||||||
|
public val atEnd: DeviceState<Boolean> = input.map { it >= range.endInclusive }
|
||||||
|
|
||||||
|
override fun toString(): String = "DoubleRangeState(value=${value},range=$range)"
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MutableRangeState<T : Comparable<T>>(
|
||||||
|
private val mutableInput: MutableDeviceState<T>,
|
||||||
|
range: ClosedRange<T>,
|
||||||
|
) : RangeState<T>(mutableInput, range), MutableDeviceState<T> {
|
||||||
|
override var value: T
|
||||||
|
get() = super.value
|
||||||
|
set(value) {
|
||||||
|
mutableInput.value = value.coerceIn(range)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun <T : Comparable<T>> MutableRangeState(
|
||||||
|
initialValue: T,
|
||||||
|
range: ClosedRange<T>,
|
||||||
|
): MutableRangeState<T> = MutableRangeState<T>(MutableDeviceState(initialValue), range)
|
||||||
|
|
||||||
|
public fun <U : UnitsOfMeasurement> MutableRangeState(
|
||||||
|
initialValue: Double,
|
||||||
|
range: ClosedRange<Double>,
|
||||||
|
): MutableRangeState<NumericalValue<U>> = MutableRangeState(
|
||||||
|
initialValue = NumericalValue(initialValue),
|
||||||
|
range = NumericalValue<U>(range.start)..NumericalValue<U>(range.endInclusive)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
public fun <T : Comparable<T>> DeviceState<T>.coerceIn(
|
||||||
|
range: ClosedRange<T>,
|
||||||
|
): RangeState<T> = RangeState(this, range)
|
||||||
|
|
||||||
|
|
||||||
|
public fun <T : Comparable<T>> MutableDeviceState<T>.coerceIn(
|
||||||
|
range: ClosedRange<T>,
|
||||||
|
): MutableRangeState<T> = MutableRangeState(this, range)
|
@ -0,0 +1,25 @@
|
|||||||
|
package space.kscience.controls.constructor.models
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.*
|
||||||
|
import space.kscience.controls.constructor.units.Degrees
|
||||||
|
import space.kscience.controls.constructor.units.NumericalValue
|
||||||
|
import space.kscience.controls.constructor.units.times
|
||||||
|
import space.kscience.dataforge.context.Context
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A reducer device used for simulations only (no public properties)
|
||||||
|
*/
|
||||||
|
public class Reducer(
|
||||||
|
context: Context,
|
||||||
|
public val ratio: Double,
|
||||||
|
public val input: DeviceState<NumericalValue<Degrees>>,
|
||||||
|
public val output: MutableDeviceState<NumericalValue<Degrees>>,
|
||||||
|
) : ModelConstructor(context) {
|
||||||
|
init {
|
||||||
|
registerState(input)
|
||||||
|
registerState(output)
|
||||||
|
transformTo(input, output) {
|
||||||
|
it * ratio
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
package space.kscience.controls.constructor.units
|
||||||
|
|
||||||
|
public enum class Direction(public val coef: Int) {
|
||||||
|
UP(1),
|
||||||
|
DOWN(-1)
|
||||||
|
}
|
@ -0,0 +1,60 @@
|
|||||||
|
package space.kscience.controls.constructor.units
|
||||||
|
|
||||||
|
import space.kscience.dataforge.meta.Meta
|
||||||
|
import space.kscience.dataforge.meta.MetaConverter
|
||||||
|
import space.kscience.dataforge.meta.double
|
||||||
|
import kotlin.jvm.JvmInline
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A value without identity coupled to units of measurements.
|
||||||
|
*/
|
||||||
|
@JvmInline
|
||||||
|
public value class NumericalValue<U : UnitsOfMeasurement>(public val value: Double) : Comparable<NumericalValue<U>> {
|
||||||
|
override fun compareTo(other: NumericalValue<U>): Int = value.compareTo(other.value)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun <U : UnitsOfMeasurement> NumericalValue(
|
||||||
|
number: Number,
|
||||||
|
): NumericalValue<U> = NumericalValue(number.toDouble())
|
||||||
|
|
||||||
|
public operator fun <U : UnitsOfMeasurement> NumericalValue<U>.plus(
|
||||||
|
other: NumericalValue<U>,
|
||||||
|
): NumericalValue<U> = NumericalValue(this.value + other.value)
|
||||||
|
|
||||||
|
public operator fun <U : UnitsOfMeasurement> NumericalValue<U>.minus(
|
||||||
|
other: NumericalValue<U>,
|
||||||
|
): NumericalValue<U> = NumericalValue(this.value - other.value)
|
||||||
|
|
||||||
|
public operator fun <U : UnitsOfMeasurement> NumericalValue<U>.times(
|
||||||
|
c: Number,
|
||||||
|
): NumericalValue<U> = NumericalValue(this.value * c.toDouble())
|
||||||
|
|
||||||
|
public operator fun <U : UnitsOfMeasurement> Number.times(
|
||||||
|
numericalValue: NumericalValue<U>,
|
||||||
|
): NumericalValue<U> = NumericalValue(numericalValue.value * toDouble())
|
||||||
|
|
||||||
|
public operator fun <U : UnitsOfMeasurement> NumericalValue<U>.times(
|
||||||
|
c: Double,
|
||||||
|
): NumericalValue<U> = NumericalValue(this.value * c)
|
||||||
|
|
||||||
|
public operator fun <U : UnitsOfMeasurement> NumericalValue<U>.div(
|
||||||
|
c: Number,
|
||||||
|
): NumericalValue<U> = NumericalValue(this.value / c.toDouble())
|
||||||
|
|
||||||
|
public operator fun <U : UnitsOfMeasurement> NumericalValue<U>.div(other: NumericalValue<U>): Double =
|
||||||
|
value / other.value
|
||||||
|
|
||||||
|
public operator fun <U: UnitsOfMeasurement> NumericalValue<U>.unaryMinus(): NumericalValue<U> = NumericalValue(-value)
|
||||||
|
|
||||||
|
|
||||||
|
private object NumericalValueMetaConverter : MetaConverter<NumericalValue<*>> {
|
||||||
|
override fun convert(obj: NumericalValue<*>): Meta = Meta(obj.value)
|
||||||
|
|
||||||
|
override fun readOrNull(source: Meta): NumericalValue<*>? = source.double?.let { NumericalValue<Nothing>(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
public fun <U : UnitsOfMeasurement> MetaConverter.Companion.numerical(): MetaConverter<NumericalValue<U>> =
|
||||||
|
NumericalValueMetaConverter as MetaConverter<NumericalValue<U>>
|
@ -0,0 +1,60 @@
|
|||||||
|
package space.kscience.controls.constructor.units
|
||||||
|
|
||||||
|
|
||||||
|
public interface UnitsOfMeasurement
|
||||||
|
|
||||||
|
/**/
|
||||||
|
|
||||||
|
public interface UnitsOfLength : UnitsOfMeasurement
|
||||||
|
|
||||||
|
public data object Meters : UnitsOfLength
|
||||||
|
|
||||||
|
/**/
|
||||||
|
|
||||||
|
public interface UnitsOfTime : UnitsOfMeasurement
|
||||||
|
|
||||||
|
public data object Seconds : UnitsOfTime
|
||||||
|
|
||||||
|
/**/
|
||||||
|
|
||||||
|
public interface UnitsOfVelocity : UnitsOfMeasurement
|
||||||
|
|
||||||
|
public data object MetersPerSecond : UnitsOfVelocity
|
||||||
|
|
||||||
|
/**/
|
||||||
|
|
||||||
|
public sealed interface UnitsOfAngles : UnitsOfMeasurement
|
||||||
|
|
||||||
|
public data object Radians : UnitsOfAngles
|
||||||
|
public data object Degrees : UnitsOfAngles
|
||||||
|
|
||||||
|
/**/
|
||||||
|
|
||||||
|
public sealed interface UnitsAngularOfVelocity : UnitsOfMeasurement
|
||||||
|
|
||||||
|
public data object RadiansPerSecond : UnitsAngularOfVelocity
|
||||||
|
|
||||||
|
public data object DegreesPerSecond : UnitsAngularOfVelocity
|
||||||
|
|
||||||
|
/**/
|
||||||
|
public interface UnitsOfForce: UnitsOfMeasurement
|
||||||
|
|
||||||
|
public data object Newtons: UnitsOfForce
|
||||||
|
|
||||||
|
/**/
|
||||||
|
|
||||||
|
public interface UnitsOfTorque: UnitsOfMeasurement
|
||||||
|
|
||||||
|
public data object NewtonsMeters: UnitsOfTorque
|
||||||
|
|
||||||
|
/**/
|
||||||
|
|
||||||
|
public interface UnitsOfMass: UnitsOfMeasurement
|
||||||
|
|
||||||
|
public data object Kilograms : UnitsOfMass
|
||||||
|
|
||||||
|
/**/
|
||||||
|
|
||||||
|
public interface UnitsOfMomentOfInertia: UnitsOfMeasurement
|
||||||
|
|
||||||
|
public data object KgM2: UnitsOfMomentOfInertia
|
@ -0,0 +1,44 @@
|
|||||||
|
package space.kscience.controls.constructor.units
|
||||||
|
|
||||||
|
import kotlin.math.pow
|
||||||
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
|
public data class XY<U : UnitsOfMeasurement>(val x: NumericalValue<U>, val y: NumericalValue<U>)
|
||||||
|
|
||||||
|
public fun <U : UnitsOfMeasurement> XY(x: Number, y: Number): XY<U> = XY(NumericalValue(x), NumericalValue((y)))
|
||||||
|
|
||||||
|
public operator fun <U : UnitsOfMeasurement> XY<U>.plus(other: XY<U>): XY<U> =
|
||||||
|
XY(x + other.x, y + other.y)
|
||||||
|
|
||||||
|
public operator fun <U : UnitsOfMeasurement> XY<U>.times(c: Number): XY<U> = XY(x * c, y * c)
|
||||||
|
public operator fun <U : UnitsOfMeasurement> XY<U>.div(c: Number): XY<U> = XY(x / c, y / c)
|
||||||
|
|
||||||
|
public operator fun <U : UnitsOfMeasurement> XY<U>.unaryMinus(): XY<U> = XY(-x, -y)
|
||||||
|
|
||||||
|
public data class XYZ<U : UnitsOfMeasurement>(
|
||||||
|
val x: NumericalValue<U>,
|
||||||
|
val y: NumericalValue<U>,
|
||||||
|
val z: NumericalValue<U>,
|
||||||
|
)
|
||||||
|
|
||||||
|
public val <U : UnitsOfMeasurement> XYZ<U>.length: NumericalValue<U>
|
||||||
|
get() = NumericalValue(
|
||||||
|
sqrt(x.value.pow(2) + y.value.pow(2) + z.value.pow(2))
|
||||||
|
)
|
||||||
|
|
||||||
|
public fun <U : UnitsOfMeasurement> XYZ(x: Number, y: Number, z: Number): XYZ<U> =
|
||||||
|
XYZ(NumericalValue(x), NumericalValue((y)), NumericalValue(z))
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST", "UNUSED_PARAMETER")
|
||||||
|
public fun <U : UnitsOfMeasurement, R : UnitsOfMeasurement> XYZ<U>.cast(units: R): XYZ<R> = this as XYZ<R>
|
||||||
|
|
||||||
|
public operator fun <U : UnitsOfMeasurement> XYZ<U>.plus(other: XYZ<U>): XYZ<U> =
|
||||||
|
XYZ(x + other.x, y + other.y, z + other.z)
|
||||||
|
|
||||||
|
public operator fun <U : UnitsOfMeasurement> XYZ<U>.minus(other: XYZ<U>): XYZ<U> =
|
||||||
|
XYZ(x - other.x, y - other.y, z - other.z)
|
||||||
|
|
||||||
|
public operator fun <U : UnitsOfMeasurement> XYZ<U>.times(c: Number): XYZ<U> = XYZ(x * c, y * c, z * c)
|
||||||
|
public operator fun <U : UnitsOfMeasurement> XYZ<U>.div(c: Number): XYZ<U> = XYZ(x / c, y / c, z / c)
|
||||||
|
|
||||||
|
public operator fun <U : UnitsOfMeasurement> XYZ<U>.unaryMinus(): XYZ<U> = XYZ(-x, -y, -z)
|
@ -0,0 +1,43 @@
|
|||||||
|
package space.kscience.controls.constructor
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import space.kscience.controls.api.Device
|
||||||
|
import space.kscience.controls.api.DeviceLifeCycleMessage
|
||||||
|
import space.kscience.controls.api.LifecycleState
|
||||||
|
import space.kscience.controls.manager.DeviceManager
|
||||||
|
import space.kscience.controls.manager.install
|
||||||
|
import space.kscience.controls.spec.doRecurring
|
||||||
|
import space.kscience.dataforge.context.Context
|
||||||
|
import space.kscience.dataforge.context.Factory
|
||||||
|
import space.kscience.dataforge.context.Global
|
||||||
|
import space.kscience.dataforge.context.request
|
||||||
|
import space.kscience.dataforge.meta.Meta
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
class DeviceGroupTest {
|
||||||
|
|
||||||
|
class TestDevice(context: Context) : DeviceConstructor(context) {
|
||||||
|
|
||||||
|
companion object : Factory<Device> {
|
||||||
|
override fun build(context: Context, meta: Meta): Device = TestDevice(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testRecurringRead() = runTest {
|
||||||
|
var counter = 10
|
||||||
|
val testDevice = Global.request(DeviceManager).install("test", TestDevice)
|
||||||
|
testDevice.doRecurring(1.milliseconds) {
|
||||||
|
counter--
|
||||||
|
println(counter)
|
||||||
|
if (counter <= 0) {
|
||||||
|
testDevice.stop()
|
||||||
|
}
|
||||||
|
error("Error!")
|
||||||
|
}
|
||||||
|
testDevice.messageFlow.first { it is DeviceLifeCycleMessage && it.state == LifecycleState.STOPPED }
|
||||||
|
println("stopped")
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,89 @@
|
|||||||
|
package space.kscience.controls.constructor
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.dsl.core.EquationSystem
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFailsWith
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
import space.kscience.controls.constructor.dsl.core.equations.*
|
||||||
|
import space.kscience.controls.constructor.dsl.core.expressions.*
|
||||||
|
import space.kscience.controls.constructor.dsl.core.units.Units
|
||||||
|
import space.kscience.controls.constructor.dsl.core.variables.Variable
|
||||||
|
|
||||||
|
class EquationsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCreatingEquations() {
|
||||||
|
val xVar = Variable("x", Units.meter)
|
||||||
|
val xExpr = VariableExpression(xVar)
|
||||||
|
val yVar = Variable("y", Units.meter)
|
||||||
|
val yExpr = VariableExpression(yVar)
|
||||||
|
val constExpr = ConstantExpression(5.0, Units.meter)
|
||||||
|
|
||||||
|
// Regular equation: x = y + 5
|
||||||
|
val equation = Equation(xExpr, BinaryExpression(yExpr, BinaryOperator.ADD, constExpr))
|
||||||
|
assertEquals(xExpr, equation.leftExpression)
|
||||||
|
assertEquals(BinaryExpression(yExpr, BinaryOperator.ADD, constExpr), equation.rightExpression)
|
||||||
|
|
||||||
|
// Conditional equation: if x > 0 then y = x else y = -x
|
||||||
|
val condition = BinaryExpression(xExpr, BinaryOperator.GREATER_THAN, ConstantExpression(0.0, Units.meter))
|
||||||
|
val trueEquation = Equation(yExpr, xExpr)
|
||||||
|
val falseEquation = Equation(yExpr, BinaryExpression(ConstantExpression(0.0, Units.meter), BinaryOperator.SUBTRACT, xExpr))
|
||||||
|
val conditionalEquation = ConditionalEquation(condition, trueEquation, falseEquation)
|
||||||
|
assertEquals(condition, conditionalEquation.condition)
|
||||||
|
assertEquals(trueEquation, conditionalEquation.trueEquation)
|
||||||
|
assertEquals(falseEquation, conditionalEquation.falseEquation)
|
||||||
|
|
||||||
|
// Initial equation: x(0) = 0
|
||||||
|
val initialEquation = InitialEquation(xExpr, ConstantExpression(0.0, Units.meter))
|
||||||
|
assertEquals(xExpr, initialEquation.leftExpression)
|
||||||
|
assertEquals(ConstantExpression(0.0, Units.meter), initialEquation.rightExpression)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testUnitCompatibilityInEquations() {
|
||||||
|
val lengthVar = Variable("length", Units.meter)
|
||||||
|
val lengthExpr = VariableExpression(lengthVar)
|
||||||
|
val timeVar = Variable("time", Units.second)
|
||||||
|
val timeExpr = VariableExpression(timeVar)
|
||||||
|
|
||||||
|
// Correct equation: length = length + length
|
||||||
|
val validEquation = Equation(lengthExpr, BinaryExpression(lengthExpr, BinaryOperator.ADD, ConstantExpression(5.0, Units.meter)))
|
||||||
|
assertFailsWith<Exception> {
|
||||||
|
validEquation
|
||||||
|
}
|
||||||
|
|
||||||
|
// Incorrect equation: length = time
|
||||||
|
val exception = assertFailsWith<IllegalArgumentException> {
|
||||||
|
Equation(lengthExpr, timeExpr)
|
||||||
|
}
|
||||||
|
assertEquals("Units of left and right expressions in the equation are not compatible.", exception.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testEquationSystem() {
|
||||||
|
val xVar = Variable("x", Units.meter)
|
||||||
|
val xExpr = VariableExpression(xVar)
|
||||||
|
val vVar = Variable("v", Units.meter / Units.second)
|
||||||
|
val vExpr = VariableExpression(vVar)
|
||||||
|
val aVar = Variable("a", Units.meter / Units.second.pow(2.0))
|
||||||
|
val aExpr = VariableExpression(aVar)
|
||||||
|
|
||||||
|
// Differential equation: der(x) = v
|
||||||
|
val derX = DerivativeExpression(xExpr)
|
||||||
|
val eq1 = Equation(derX, vExpr)
|
||||||
|
|
||||||
|
// Algebraic equation: v = a * t
|
||||||
|
val tVar = Variable("t", Units.second)
|
||||||
|
val tExpr = VariableExpression(tVar)
|
||||||
|
val eq2 = Equation(vExpr, BinaryExpression(aExpr, BinaryOperator.MULTIPLY, tExpr))
|
||||||
|
|
||||||
|
val equationSystem = EquationSystem()
|
||||||
|
equationSystem.addEquation(eq1)
|
||||||
|
equationSystem.addEquation(eq2)
|
||||||
|
|
||||||
|
assertEquals(2, equationSystem.equations.size)
|
||||||
|
assertTrue(equationSystem.variables.containsAll(listOf("x", "v", "a", "t")))
|
||||||
|
assertTrue(equationSystem.derivativeVariables.contains("x"))
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,73 @@
|
|||||||
|
package space.kscience.controls.constructor
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import space.kscience.controls.constructor.dsl.core.events.Event
|
||||||
|
import space.kscience.controls.constructor.dsl.core.events.ReinitAction
|
||||||
|
import space.kscience.controls.constructor.dsl.core.events.AssignAction
|
||||||
|
import space.kscience.controls.constructor.dsl.core.expressions.BinaryExpression
|
||||||
|
import space.kscience.controls.constructor.dsl.core.expressions.BinaryOperator
|
||||||
|
import space.kscience.controls.constructor.dsl.core.expressions.ConstantExpression
|
||||||
|
import space.kscience.controls.constructor.dsl.core.expressions.VariableExpression
|
||||||
|
import space.kscience.controls.constructor.dsl.core.simulation.SimulationContext
|
||||||
|
import space.kscience.controls.constructor.dsl.core.units.Units
|
||||||
|
import space.kscience.controls.constructor.dsl.core.variables.Variable
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
|
||||||
|
class EventsAndActionsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCreatingEvents() {
|
||||||
|
val xVar = Variable("x", Units.meter)
|
||||||
|
val xExpr = VariableExpression(xVar)
|
||||||
|
|
||||||
|
val condition = BinaryExpression(xExpr, BinaryOperator.GREATER_THAN, ConstantExpression(10.0, Units.meter))
|
||||||
|
val action = AssignAction(xVar, ConstantExpression(0.0, Units.meter))
|
||||||
|
|
||||||
|
val event = Event(condition, listOf(action))
|
||||||
|
|
||||||
|
assertEquals(condition, event.condition)
|
||||||
|
assertEquals(1, event.actions.size)
|
||||||
|
assertEquals(action, event.actions[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testProcessingEventsInSimulation() = runTest {
|
||||||
|
val xVar = Variable("x", Units.meter)
|
||||||
|
val xExpr = VariableExpression(xVar)
|
||||||
|
val context = SimulationContext(CoroutineScope(Dispatchers.Default)).apply {
|
||||||
|
registerVariable(xVar) // Регистрация переменной
|
||||||
|
launch { updateVariable("x", 5.0) } // Установка начального значения переменной
|
||||||
|
}
|
||||||
|
|
||||||
|
val condition = BinaryExpression(xExpr, BinaryOperator.GREATER_THAN, ConstantExpression(10.0, Units.meter))
|
||||||
|
val action = AssignAction(xVar, ConstantExpression(0.0, Units.meter))
|
||||||
|
val event = Event(condition, listOf(action))
|
||||||
|
|
||||||
|
context.events.add(event)
|
||||||
|
|
||||||
|
// Simulate x crossing the threshold
|
||||||
|
context.updateVariable("x", 15.0)
|
||||||
|
context.handleEvents()
|
||||||
|
|
||||||
|
assertEquals(0.0, context.getCurrentVariableValue("x"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testReinitAction() = runTest {
|
||||||
|
val vVar = Variable("v", Units.meter / Units.second)
|
||||||
|
val vExpr = VariableExpression(vVar)
|
||||||
|
val context = SimulationContext(CoroutineScope(Dispatchers.Default)).apply {
|
||||||
|
registerVariable(vVar) // Регистрация переменной
|
||||||
|
launch { updateVariable("v", 20.0) } // Установка начального значения переменной
|
||||||
|
}
|
||||||
|
|
||||||
|
val action = ReinitAction(vVar, ConstantExpression(0.0, Units.meter / Units.second))
|
||||||
|
action.execute(context)
|
||||||
|
|
||||||
|
assertEquals(0.0, context.getCurrentVariableValue("v"))
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,101 @@
|
|||||||
|
package space.kscience.controls.constructor
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFailsWith
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
import space.kscience.controls.constructor.dsl.core.expressions.*
|
||||||
|
import space.kscience.controls.constructor.dsl.core.units.Units
|
||||||
|
import space.kscience.controls.constructor.dsl.core.variables.Variable
|
||||||
|
|
||||||
|
class ExpressionsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCreationOfDifferentTypesOfExpressions() {
|
||||||
|
// Constant expression
|
||||||
|
val constExpr = ConstantExpression(5.0, Units.meter)
|
||||||
|
assertEquals(5.0, constExpr.value)
|
||||||
|
assertEquals(Units.meter, constExpr.unit)
|
||||||
|
|
||||||
|
// Variable expression
|
||||||
|
val speedVar = Variable("speed", Units.meter / Units.second)
|
||||||
|
val speedExpr = VariableExpression(speedVar)
|
||||||
|
assertEquals(speedVar, speedExpr.variable)
|
||||||
|
assertEquals(Units.meter / Units.second, speedExpr.unit)
|
||||||
|
|
||||||
|
// Binary expression (addition)
|
||||||
|
val distanceVar = Variable("distance", Units.meter)
|
||||||
|
val distanceExpr = VariableExpression(distanceVar)
|
||||||
|
val totalDistanceExpr = BinaryExpression(distanceExpr, BinaryOperator.ADD, constExpr)
|
||||||
|
assertEquals(Units.meter, totalDistanceExpr.unit)
|
||||||
|
|
||||||
|
// Function call expression
|
||||||
|
val sinExpr = FunctionCallExpression("sin", listOf(constExpr))
|
||||||
|
assertEquals(Units.dimensionless, sinExpr.unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testEvaluationOfExpressions() {
|
||||||
|
val xVar = Variable("x", Units.meter)
|
||||||
|
val xExpr = VariableExpression(xVar)
|
||||||
|
val constExpr = ConstantExpression(2.0, Units.meter)
|
||||||
|
|
||||||
|
val values = mapOf("x" to 3.0)
|
||||||
|
|
||||||
|
// Binary expression: x + 2
|
||||||
|
val sumExpr = BinaryExpression(xExpr, BinaryOperator.ADD, constExpr)
|
||||||
|
assertEquals(5.0, sumExpr.evaluate(values))
|
||||||
|
|
||||||
|
// Binary expression: x * 2
|
||||||
|
val mulExpr = BinaryExpression(xExpr, BinaryOperator.MULTIPLY, ConstantExpression(2.0, Units.dimensionless))
|
||||||
|
assertEquals(6.0, mulExpr.evaluate(values))
|
||||||
|
|
||||||
|
// Function call: sin(x)
|
||||||
|
val sinExpr = FunctionCallExpression("sin", listOf(xExpr))
|
||||||
|
assertEquals(kotlin.math.sin(3.0), sinExpr.evaluate(values))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDimensionCheckingInExpressions() {
|
||||||
|
val lengthVar = Variable("length", Units.meter)
|
||||||
|
val timeVar = Variable("time", Units.second)
|
||||||
|
val lengthExpr = VariableExpression(lengthVar)
|
||||||
|
val timeExpr = VariableExpression(timeVar)
|
||||||
|
|
||||||
|
// Correct addition
|
||||||
|
val sumExpr = BinaryExpression(lengthExpr, BinaryOperator.ADD, ConstantExpression(5.0, Units.meter))
|
||||||
|
assertFailsWith<Exception> {
|
||||||
|
sumExpr.checkDimensions()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Incorrect addition (length + time)
|
||||||
|
val invalidSumExpr = BinaryExpression(lengthExpr, BinaryOperator.ADD, timeExpr)
|
||||||
|
val exception = assertFailsWith<IllegalArgumentException> {
|
||||||
|
invalidSumExpr.checkDimensions()
|
||||||
|
}
|
||||||
|
assertEquals("Units must be the same for addition or subtraction.", exception.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDerivativeOfExpressions() {
|
||||||
|
val xVar = Variable("x", Units.meter)
|
||||||
|
val xExpr = VariableExpression(xVar)
|
||||||
|
val constExpr = ConstantExpression(5.0, Units.meter)
|
||||||
|
|
||||||
|
// Derivative of constant: should be zero
|
||||||
|
val derivativeConst = constExpr.derivative(xExpr)
|
||||||
|
assertTrue(derivativeConst is ConstantExpression)
|
||||||
|
assertEquals(0.0, derivativeConst.value)
|
||||||
|
|
||||||
|
// Derivative of variable with respect to itself: should be one
|
||||||
|
val derivativeVar = xExpr.derivative(xExpr)
|
||||||
|
assertTrue(derivativeVar is ConstantExpression)
|
||||||
|
assertEquals(1.0, derivativeVar.value)
|
||||||
|
|
||||||
|
// Derivative of x + 5 with respect to x: should be one
|
||||||
|
val sumExpr = BinaryExpression(xExpr, BinaryOperator.ADD, constExpr)
|
||||||
|
val derivativeSum = sumExpr.derivative(xExpr)
|
||||||
|
assertTrue(derivativeSum is ConstantExpression)
|
||||||
|
assertEquals(1.0, derivativeSum.value)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
package space.kscience.controls.constructor
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import space.kscience.controls.constructor.dsl.core.functions.FunctionBuilder
|
||||||
|
import space.kscience.controls.constructor.dsl.core.expressions.*
|
||||||
|
import space.kscience.controls.constructor.dsl.core.variables.Variable
|
||||||
|
import space.kscience.controls.constructor.dsl.core.units.Units
|
||||||
|
|
||||||
|
class FunctionsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCreatingFunctions() {
|
||||||
|
val functionBuilder = FunctionBuilder("add").apply {
|
||||||
|
val aVar = input("a", Units.dimensionless)
|
||||||
|
val bVar = input("b", Units.dimensionless)
|
||||||
|
val resultVar = output("result", Units.dimensionless)
|
||||||
|
|
||||||
|
algorithm {
|
||||||
|
assign(resultVar, BinaryExpression(VariableExpression(aVar), BinaryOperator.ADD, VariableExpression(bVar)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val addFunction = functionBuilder.build()
|
||||||
|
assertEquals("add", addFunction.name)
|
||||||
|
assertEquals(2, addFunction.inputs.size)
|
||||||
|
assertEquals(1, addFunction.outputs.size)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
package space.kscience.controls.constructor
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.take
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import space.kscience.controls.manager.ClockManager
|
||||||
|
import space.kscience.dataforge.context.Global
|
||||||
|
import space.kscience.dataforge.context.request
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
class TimerTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun timer() = runTest {
|
||||||
|
val timer = TimerState(Global.request(ClockManager), 10.milliseconds)
|
||||||
|
timer.valueFlow.take(100).onEach {
|
||||||
|
println(it)
|
||||||
|
}.collect()
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,85 @@
|
|||||||
|
package space.kscience.controls.constructor
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFailsWith
|
||||||
|
import space.kscience.controls.constructor.dsl.core.units.Dimension
|
||||||
|
import space.kscience.controls.constructor.dsl.core.units.PhysicalUnit
|
||||||
|
import space.kscience.controls.constructor.dsl.core.units.Units
|
||||||
|
|
||||||
|
class UnitsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCreationOfBaseUnits() {
|
||||||
|
val meter = Units.meter
|
||||||
|
assertEquals("meter", meter.name)
|
||||||
|
assertEquals("m", meter.symbol)
|
||||||
|
assertEquals(1.0, meter.conversionFactorToSI)
|
||||||
|
assertEquals(Dimension(length = 1.0), meter.dimension)
|
||||||
|
|
||||||
|
val kilogram = Units.kilogram
|
||||||
|
assertEquals("kilogram", kilogram.name)
|
||||||
|
assertEquals("kg", kilogram.symbol)
|
||||||
|
assertEquals(1.0, kilogram.conversionFactorToSI)
|
||||||
|
assertEquals(Dimension(mass = 1.0), kilogram.dimension)
|
||||||
|
|
||||||
|
val second = Units.second
|
||||||
|
assertEquals("second", second.name)
|
||||||
|
assertEquals("s", second.symbol)
|
||||||
|
assertEquals(1.0, second.conversionFactorToSI)
|
||||||
|
assertEquals(Dimension(time = 1.0), second.dimension)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testArithmeticOperationsOnUnits() {
|
||||||
|
val meter = Units.meter
|
||||||
|
val second = Units.second
|
||||||
|
val kilogram = Units.kilogram
|
||||||
|
|
||||||
|
val velocityUnit = meter / second
|
||||||
|
assertEquals(Dimension(length = 1.0, time = -1.0), velocityUnit.dimension)
|
||||||
|
assertEquals("m/s", velocityUnit.symbol)
|
||||||
|
|
||||||
|
val accelerationUnit = meter / (second * second)
|
||||||
|
assertEquals(Dimension(length = 1.0, time = -2.0), accelerationUnit.dimension)
|
||||||
|
assertEquals("m/s^2", accelerationUnit.symbol)
|
||||||
|
|
||||||
|
val forceUnit = kilogram * accelerationUnit
|
||||||
|
assertEquals(Dimension(mass = 1.0, length = 1.0, time = -2.0), forceUnit.dimension)
|
||||||
|
assertEquals("kg*m/s^2", forceUnit.symbol)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testUnitCompatibilityForAdditionAndSubtraction() {
|
||||||
|
val meter = Units.meter
|
||||||
|
val kilogram = Units.kilogram
|
||||||
|
|
||||||
|
// Correct addition
|
||||||
|
val length1 = meter
|
||||||
|
val length2 = meter
|
||||||
|
assertEquals(length1.dimension, length2.dimension)
|
||||||
|
|
||||||
|
// Incorrect addition should throw exception
|
||||||
|
val mass = kilogram
|
||||||
|
val exception = assertFailsWith<IllegalArgumentException> {
|
||||||
|
if (length1.dimension != mass.dimension) {
|
||||||
|
throw IllegalArgumentException("Units are not compatible for addition.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertEquals("Units are not compatible for addition.", exception.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testUnitConversion() {
|
||||||
|
val kilometer = PhysicalUnit("kilometer", "km", 1000.0, Units.meter.dimension)
|
||||||
|
val centimeter = PhysicalUnit("centimeter", "cm", 0.01, Units.meter.dimension)
|
||||||
|
|
||||||
|
// Convert 1 km to meters
|
||||||
|
val kmInMeters = 1.0 * kilometer.conversionFactorToSI
|
||||||
|
assertEquals(1000.0, kmInMeters)
|
||||||
|
|
||||||
|
// Convert 100 cm to meters
|
||||||
|
val cmInMeters = 100.0 * centimeter.conversionFactorToSI
|
||||||
|
assertEquals(1.0, cmInMeters)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
package space.kscience.controls.constructor
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFailsWith
|
||||||
|
import kotlin.test.assertNull
|
||||||
|
import space.kscience.controls.constructor.dsl.core.units.Units
|
||||||
|
import space.kscience.controls.constructor.dsl.core.variables.ParameterVariable
|
||||||
|
import space.kscience.controls.constructor.dsl.core.variables.Variable
|
||||||
|
|
||||||
|
class VariablesTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCreationOfVariablesAndParameters() {
|
||||||
|
val length = Variable("length", Units.meter)
|
||||||
|
assertEquals("length", length.name)
|
||||||
|
assertEquals(Units.meter, length.unit)
|
||||||
|
assertNull(length.value)
|
||||||
|
|
||||||
|
val mass = ParameterVariable("mass", Units.kilogram, value = 10.0)
|
||||||
|
assertEquals("mass", mass.name)
|
||||||
|
assertEquals(Units.kilogram, mass.unit)
|
||||||
|
assertEquals(10.0, mass.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSettingAndGettingValuesOfVariablesAndParameters() {
|
||||||
|
val speed = Variable("speed", Units.meter / Units.second)
|
||||||
|
speed.value = 15.0
|
||||||
|
assertEquals(15.0, speed.value)
|
||||||
|
|
||||||
|
val density = ParameterVariable("density", Units.kilogram / Units.meter.pow(3.0), value = 1000.0)
|
||||||
|
assertEquals(1000.0, density.value)
|
||||||
|
|
||||||
|
// Attempt to create a parameter with value outside the allowed range
|
||||||
|
val exception = assertFailsWith<IllegalArgumentException> {
|
||||||
|
ParameterVariable("temperature", Units.kelvin, value = -10.0, min = 0.0)
|
||||||
|
}
|
||||||
|
assertEquals("Value of parameter temperature is less than minimum allowed value.", exception.message)
|
||||||
|
}
|
||||||
|
}
|
@ -16,18 +16,16 @@ Core interfaces for building a device server
|
|||||||
|
|
||||||
## Artifact:
|
## Artifact:
|
||||||
|
|
||||||
The Maven coordinates of this project are `space.kscience:controls-core:0.2.0`.
|
The Maven coordinates of this project are `space.kscience:controls-core:0.4.0-dev-4`.
|
||||||
|
|
||||||
**Gradle Kotlin DSL:**
|
**Gradle Kotlin DSL:**
|
||||||
```kotlin
|
```kotlin
|
||||||
repositories {
|
repositories {
|
||||||
maven("https://repo.kotlin.link")
|
maven("https://repo.kotlin.link")
|
||||||
//uncomment to access development builds
|
|
||||||
//maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
|
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("space.kscience:controls-core:0.2.0")
|
implementation("space.kscience:controls-core:0.4.0-dev-4")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -9,21 +9,24 @@ description = """
|
|||||||
Core interfaces for building a device server
|
Core interfaces for building a device server
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
|
||||||
val dataforgeVersion: String by rootProject.extra
|
|
||||||
|
|
||||||
kscience {
|
kscience {
|
||||||
jvm()
|
jvm()
|
||||||
js()
|
js()
|
||||||
native()
|
native()
|
||||||
|
wasm()
|
||||||
useCoroutines()
|
useCoroutines()
|
||||||
useSerialization{
|
useSerialization{
|
||||||
json()
|
json()
|
||||||
}
|
}
|
||||||
useContextReceivers()
|
useContextReceivers()
|
||||||
dependencies {
|
commonMain {
|
||||||
api("space.kscience:dataforge-io:$dataforgeVersion")
|
api(libs.dataforge.io)
|
||||||
api(spclibs.kotlinx.datetime)
|
api(spclibs.kotlinx.datetime)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jvmTest{
|
||||||
|
implementation(spclibs.logback.classic)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
package space.kscience.controls.api
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A generic bidirectional asynchronous sender/receiver object
|
||||||
|
*/
|
||||||
|
public interface AsynchronousSocket<T> : WithLifeCycle {
|
||||||
|
/**
|
||||||
|
* Send an object to the socket
|
||||||
|
*/
|
||||||
|
public suspend fun send(data: T)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flow of objects received from socket
|
||||||
|
*/
|
||||||
|
public fun subscribe(): Flow<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect an input to this socket.
|
||||||
|
* Multiple inputs could be connected to the same [AsynchronousSocket].
|
||||||
|
*
|
||||||
|
* This method suspends indefinitely, so it should be started in a separate coroutine.
|
||||||
|
*/
|
||||||
|
public suspend fun <T> AsynchronousSocket<T>.sendFlow(flow: Flow<T>) {
|
||||||
|
flow.collect { send(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -3,41 +3,31 @@ package space.kscience.controls.api
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.flow.filterIsInstance
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import space.kscience.controls.api.Device.Companion.DEVICE_TARGET
|
import space.kscience.controls.api.Device.Companion.DEVICE_TARGET
|
||||||
import space.kscience.dataforge.context.ContextAware
|
import space.kscience.dataforge.context.ContextAware
|
||||||
import space.kscience.dataforge.context.info
|
import space.kscience.dataforge.context.info
|
||||||
import space.kscience.dataforge.context.logger
|
import space.kscience.dataforge.context.logger
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
import space.kscience.dataforge.misc.DFExperimental
|
import space.kscience.dataforge.meta.get
|
||||||
import space.kscience.dataforge.misc.Type
|
import space.kscience.dataforge.meta.string
|
||||||
import space.kscience.dataforge.names.Name
|
import space.kscience.dataforge.misc.DfType
|
||||||
|
import space.kscience.dataforge.names.parseAsName
|
||||||
/**
|
|
||||||
* A lifecycle state of a device
|
|
||||||
*/
|
|
||||||
public enum class DeviceLifecycleState{
|
|
||||||
INIT,
|
|
||||||
OPEN,
|
|
||||||
CLOSED
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* General interface describing a managed Device.
|
* General interface describing a managed Device.
|
||||||
* [Device] is a supervisor scope encompassing all operations on a device.
|
* [Device] is a supervisor scope encompassing all operations on a device.
|
||||||
* When canceled, cancels all running processes.
|
* When canceled, cancels all running processes.
|
||||||
*/
|
*/
|
||||||
@Type(DEVICE_TARGET)
|
@DfType(DEVICE_TARGET)
|
||||||
public interface Device : AutoCloseable, ContextAware, CoroutineScope {
|
public interface Device : ContextAware, WithLifeCycle, CoroutineScope {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initial configuration meta for the device
|
* Initial configuration meta for the device
|
||||||
*/
|
*/
|
||||||
public val meta: Meta get() = Meta.EMPTY
|
public val meta: Meta get() = Meta.EMPTY
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of supported property descriptors
|
* List of supported property descriptors
|
||||||
*/
|
*/
|
||||||
@ -54,18 +44,6 @@ public interface Device : AutoCloseable, ContextAware, CoroutineScope {
|
|||||||
*/
|
*/
|
||||||
public suspend fun readProperty(propertyName: String): Meta
|
public suspend fun readProperty(propertyName: String): Meta
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the logical state of property or return null if it is invalid
|
|
||||||
*/
|
|
||||||
public fun getProperty(propertyName: String): Meta?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Invalidate property (set logical state to invalid)
|
|
||||||
*
|
|
||||||
* This message is suspended to provide lock-free local property changes (they require coroutine context).
|
|
||||||
*/
|
|
||||||
public suspend fun invalidate(propertyName: String)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set property [value] for a property with name [propertyName].
|
* Set property [value] for a property with name [propertyName].
|
||||||
* In rare cases could suspend if the [Device] supports command queue, and it is full at the moment.
|
* In rare cases could suspend if the [Device] supports command queue, and it is full at the moment.
|
||||||
@ -85,44 +63,86 @@ public interface Device : AutoCloseable, ContextAware, CoroutineScope {
|
|||||||
public suspend fun execute(actionName: String, argument: Meta? = null): Meta?
|
public suspend fun execute(actionName: String, argument: Meta? = null): Meta?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the device. This function suspends until the device is finished initialization
|
* Initialize the device. This function suspends until the device is finished initialization.
|
||||||
|
* Does nothing if the device is started or is starting
|
||||||
*/
|
*/
|
||||||
public suspend fun open(): Unit = Unit
|
override suspend fun start(): Unit = Unit
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Close and terminate the device. This function does not wait for the device to be closed.
|
* Close and terminate the device. This function does not wait for the device to be closed.
|
||||||
*/
|
*/
|
||||||
override fun close() {
|
override suspend fun stop() {
|
||||||
|
coroutineContext[Job]?.cancel("The device is closed")
|
||||||
logger.info { "Device $this is closed" }
|
logger.info { "Device $this is closed" }
|
||||||
cancel("The device is closed")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@DFExperimental
|
|
||||||
public val lifecycleState: DeviceLifecycleState
|
|
||||||
|
|
||||||
public companion object {
|
public companion object {
|
||||||
public const val DEVICE_TARGET: String = "device"
|
public const val DEVICE_TARGET: String = "device"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inner id of a device. Not necessary corresponds to the name in the parent container
|
||||||
|
*/
|
||||||
|
public val Device.id: String get() = meta["id"].string ?: "device[${hashCode().toString(16)}]"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Device that caches properties values
|
||||||
|
*/
|
||||||
|
public interface CachingDevice : Device {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Immediately (without waiting) get the cached (logical) state of property or return null if it is invalid
|
||||||
|
*/
|
||||||
|
public fun getProperty(propertyName: String): Meta?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate property (set logical state to invalid).
|
||||||
|
*
|
||||||
|
* This message is suspended to provide lock-free local property changes (they require coroutine context).
|
||||||
|
*/
|
||||||
|
public suspend fun invalidate(propertyName: String)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the logical state of property or suspend to read the physical value.
|
* Get the logical state of property or suspend to read the physical value.
|
||||||
*/
|
*/
|
||||||
public suspend fun Device.getOrReadProperty(propertyName: String): Meta =
|
public suspend fun Device.getOrReadProperty(propertyName: String): Meta = if (this is CachingDevice) {
|
||||||
getProperty(propertyName) ?: readProperty(propertyName)
|
getProperty(propertyName) ?: readProperty(propertyName)
|
||||||
|
} else {
|
||||||
|
readProperty(propertyName)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a snapshot of the device logical state
|
* Get a snapshot of the device logical state
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public fun Device.getAllProperties(): Meta = Meta {
|
public fun CachingDevice.getAllProperties(): Meta = Meta {
|
||||||
for (descriptor in propertyDescriptors) {
|
for (descriptor in propertyDescriptors) {
|
||||||
setMeta(Name.parse(descriptor.name), getProperty(descriptor.name))
|
set(descriptor.name.parseAsName(), getProperty(descriptor.name))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscribe on property changes for the whole device
|
* Subscribe on property changes for the whole device
|
||||||
*/
|
*/
|
||||||
public fun Device.onPropertyChange(callback: suspend PropertyChangedMessage.() -> Unit): Job =
|
public fun Device.onPropertyChange(
|
||||||
messageFlow.filterIsInstance<PropertyChangedMessage>().onEach(callback).launchIn(this)
|
scope: CoroutineScope = this,
|
||||||
|
callback: suspend PropertyChangedMessage.() -> Unit,
|
||||||
|
): Job = messageFlow.filterIsInstance<PropertyChangedMessage>().onEach(callback).launchIn(scope)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [Flow] of property change messages for specific property.
|
||||||
|
*/
|
||||||
|
public fun Device.propertyMessageFlow(propertyName: String): Flow<PropertyChangedMessage> = messageFlow
|
||||||
|
.filterIsInstance<PropertyChangedMessage>()
|
||||||
|
.filter { it.property == propertyName }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React on device lifecycle events
|
||||||
|
*/
|
||||||
|
public fun Device.onLifecycleEvent(
|
||||||
|
block: suspend (LifecycleState) -> Unit
|
||||||
|
): Job = messageFlow.filterIsInstance<DeviceLifeCycleMessage>().onEach {
|
||||||
|
block(it.state)
|
||||||
|
}.launchIn(this)
|
@ -1,73 +1,62 @@
|
|||||||
package space.kscience.controls.api
|
package space.kscience.controls.api
|
||||||
|
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
import space.kscience.dataforge.names.*
|
import space.kscience.dataforge.names.Name
|
||||||
|
import space.kscience.dataforge.provider.Path
|
||||||
import space.kscience.dataforge.provider.Provider
|
import space.kscience.dataforge.provider.Provider
|
||||||
|
import space.kscience.dataforge.provider.asPath
|
||||||
|
import space.kscience.dataforge.provider.plus
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A hub that could locate multiple devices and redirect actions to them
|
* A hub that could locate multiple devices and redirect actions to them
|
||||||
*/
|
*/
|
||||||
public interface DeviceHub : Provider {
|
public interface DeviceHub : Provider {
|
||||||
public val devices: Map<NameToken, Device>
|
public val devices: Map<Name, Device>
|
||||||
|
|
||||||
override val defaultTarget: String get() = Device.DEVICE_TARGET
|
override val defaultTarget: String get() = Device.DEVICE_TARGET
|
||||||
|
|
||||||
override val defaultChainTarget: String get() = Device.DEVICE_TARGET
|
override val defaultChainTarget: String get() = Device.DEVICE_TARGET
|
||||||
|
|
||||||
override fun content(target: String): Map<Name, Any> = if (target == Device.DEVICE_TARGET) {
|
override fun content(target: String): Map<Name, Any> = if (target == Device.DEVICE_TARGET) {
|
||||||
buildMap {
|
devices
|
||||||
fun putAll(prefix: Name, hub: DeviceHub) {
|
|
||||||
hub.devices.forEach {
|
|
||||||
put(prefix + it.key, it.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
devices.forEach {
|
|
||||||
val name = it.key.asName()
|
|
||||||
put(name, it.value)
|
|
||||||
(it.value as? DeviceHub)?.let { hub ->
|
|
||||||
putAll(name, hub)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
emptyMap()
|
emptyMap()
|
||||||
}
|
}
|
||||||
|
//TODO send message on device change
|
||||||
|
|
||||||
public companion object
|
public companion object
|
||||||
}
|
}
|
||||||
|
|
||||||
public operator fun DeviceHub.get(nameToken: NameToken): Device =
|
public fun DeviceHub(deviceMap: Map<Name, Device>): DeviceHub = object : DeviceHub {
|
||||||
devices[nameToken] ?: error("Device with name $nameToken not found in $this")
|
override val devices: Map<Name, Device>
|
||||||
|
get() = deviceMap
|
||||||
public fun DeviceHub.getOrNull(name: Name): Device? = when {
|
|
||||||
name.isEmpty() -> this as? Device
|
|
||||||
name.length == 1 -> get(name.firstOrNull()!!)
|
|
||||||
else -> (get(name.firstOrNull()!!) as? DeviceHub)?.getOrNull(name.cutFirst())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public operator fun DeviceHub.get(name: Name): Device =
|
/**
|
||||||
getOrNull(name) ?: error("Device with name $name not found in $this")
|
* List all devices, including sub-devices
|
||||||
|
*/
|
||||||
|
public fun DeviceHub.provideAllDevices(): Map<Path, Device> = buildMap {
|
||||||
|
fun putAll(prefix: Path, hub: DeviceHub) {
|
||||||
|
hub.devices.forEach {
|
||||||
|
put(prefix + it.key.asPath(), it.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public fun DeviceHub.getOrNull(nameString: String): Device? = getOrNull(Name.parse(nameString))
|
devices.forEach {
|
||||||
|
val name: Name = it.key
|
||||||
public operator fun DeviceHub.get(nameString: String): Device =
|
put(name.asPath(), it.value)
|
||||||
getOrNull(nameString) ?: error("Device with name $nameString not found in $this")
|
(it.value as? DeviceHub)?.let { hub ->
|
||||||
|
putAll(name.asPath(), hub)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public suspend fun DeviceHub.readProperty(deviceName: Name, propertyName: String): Meta =
|
public suspend fun DeviceHub.readProperty(deviceName: Name, propertyName: String): Meta =
|
||||||
this[deviceName].readProperty(propertyName)
|
(devices[deviceName] ?: error("Device with name $deviceName not found in $this")).readProperty(propertyName)
|
||||||
|
|
||||||
public suspend fun DeviceHub.writeProperty(deviceName: Name, propertyName: String, value: Meta) {
|
public suspend fun DeviceHub.writeProperty(deviceName: Name, propertyName: String, value: Meta) {
|
||||||
this[deviceName].writeProperty(propertyName, value)
|
(devices[deviceName] ?: error("Device with name $deviceName not found in $this")).writeProperty(propertyName, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
public suspend fun DeviceHub.execute(deviceName: Name, command: String, argument: Meta?): Meta? =
|
public suspend fun DeviceHub.execute(deviceName: Name, command: String, argument: Meta?): Meta? =
|
||||||
this[deviceName].execute(command, argument)
|
(devices[deviceName] ?: error("Device with name $deviceName not found in $this")).execute(command, argument)
|
||||||
|
|
||||||
|
|
||||||
//suspend fun DeviceHub.respond(request: Envelope): EnvelopeBuilder {
|
|
||||||
// val target = request.meta[DeviceMessage.TARGET_KEY].string ?: defaultTarget
|
|
||||||
// val device = this[target.toName()]
|
|
||||||
//
|
|
||||||
// return device.respond(device, target, request)
|
|
||||||
//}
|
|
@ -22,10 +22,10 @@ public sealed class DeviceMessage {
|
|||||||
public abstract val sourceDevice: Name?
|
public abstract val sourceDevice: Name?
|
||||||
public abstract val targetDevice: Name?
|
public abstract val targetDevice: Name?
|
||||||
public abstract val comment: String?
|
public abstract val comment: String?
|
||||||
public abstract val time: Instant?
|
public abstract val time: Instant
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the source device name for composition. If the original name is null, resulting name is also null.
|
* Update the source device name for composition. If the original name is null, the resulting name is also null.
|
||||||
*/
|
*/
|
||||||
public abstract fun changeSource(block: (Name) -> Name): DeviceMessage
|
public abstract fun changeSource(block: (Name) -> Name): DeviceMessage
|
||||||
|
|
||||||
@ -59,7 +59,7 @@ public data class PropertyChangedMessage(
|
|||||||
override val sourceDevice: Name = Name.EMPTY,
|
override val sourceDevice: Name = Name.EMPTY,
|
||||||
override val targetDevice: Name? = null,
|
override val targetDevice: Name? = null,
|
||||||
override val comment: String? = null,
|
override val comment: String? = null,
|
||||||
@EncodeDefault override val time: Instant? = Clock.System.now(),
|
@EncodeDefault override val time: Instant = Clock.System.now(),
|
||||||
) : DeviceMessage() {
|
) : DeviceMessage() {
|
||||||
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
|
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
|
||||||
}
|
}
|
||||||
@ -71,11 +71,11 @@ public data class PropertyChangedMessage(
|
|||||||
@SerialName("property.set")
|
@SerialName("property.set")
|
||||||
public data class PropertySetMessage(
|
public data class PropertySetMessage(
|
||||||
public val property: String,
|
public val property: String,
|
||||||
public val value: Meta?,
|
public val value: Meta,
|
||||||
override val sourceDevice: Name? = null,
|
override val sourceDevice: Name? = null,
|
||||||
override val targetDevice: Name,
|
override val targetDevice: Name?,
|
||||||
override val comment: String? = null,
|
override val comment: String? = null,
|
||||||
@EncodeDefault override val time: Instant? = Clock.System.now(),
|
@EncodeDefault override val time: Instant = Clock.System.now(),
|
||||||
) : DeviceMessage() {
|
) : DeviceMessage() {
|
||||||
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
|
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
|
||||||
}
|
}
|
||||||
@ -91,7 +91,7 @@ public data class PropertyGetMessage(
|
|||||||
override val sourceDevice: Name? = null,
|
override val sourceDevice: Name? = null,
|
||||||
override val targetDevice: Name,
|
override val targetDevice: Name,
|
||||||
override val comment: String? = null,
|
override val comment: String? = null,
|
||||||
@EncodeDefault override val time: Instant? = Clock.System.now(),
|
@EncodeDefault override val time: Instant = Clock.System.now(),
|
||||||
) : DeviceMessage() {
|
) : DeviceMessage() {
|
||||||
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
|
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
|
||||||
}
|
}
|
||||||
@ -103,9 +103,9 @@ public data class PropertyGetMessage(
|
|||||||
@SerialName("description.get")
|
@SerialName("description.get")
|
||||||
public data class GetDescriptionMessage(
|
public data class GetDescriptionMessage(
|
||||||
override val sourceDevice: Name? = null,
|
override val sourceDevice: Name? = null,
|
||||||
override val targetDevice: Name,
|
override val targetDevice: Name? = null,
|
||||||
override val comment: String? = null,
|
override val comment: String? = null,
|
||||||
@EncodeDefault override val time: Instant? = Clock.System.now(),
|
@EncodeDefault override val time: Instant = Clock.System.now(),
|
||||||
) : DeviceMessage() {
|
) : DeviceMessage() {
|
||||||
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
|
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
|
||||||
}
|
}
|
||||||
@ -122,7 +122,7 @@ public data class DescriptionMessage(
|
|||||||
override val sourceDevice: Name,
|
override val sourceDevice: Name,
|
||||||
override val targetDevice: Name? = null,
|
override val targetDevice: Name? = null,
|
||||||
override val comment: String? = null,
|
override val comment: String? = null,
|
||||||
@EncodeDefault override val time: Instant? = Clock.System.now(),
|
@EncodeDefault override val time: Instant = Clock.System.now(),
|
||||||
) : DeviceMessage() {
|
) : DeviceMessage() {
|
||||||
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
|
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
|
||||||
}
|
}
|
||||||
@ -141,7 +141,7 @@ public data class ActionExecuteMessage(
|
|||||||
override val sourceDevice: Name? = null,
|
override val sourceDevice: Name? = null,
|
||||||
override val targetDevice: Name,
|
override val targetDevice: Name,
|
||||||
override val comment: String? = null,
|
override val comment: String? = null,
|
||||||
@EncodeDefault override val time: Instant? = Clock.System.now(),
|
@EncodeDefault override val time: Instant = Clock.System.now(),
|
||||||
) : DeviceMessage() {
|
) : DeviceMessage() {
|
||||||
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
|
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
|
||||||
}
|
}
|
||||||
@ -160,22 +160,28 @@ public data class ActionResultMessage(
|
|||||||
override val sourceDevice: Name,
|
override val sourceDevice: Name,
|
||||||
override val targetDevice: Name? = null,
|
override val targetDevice: Name? = null,
|
||||||
override val comment: String? = null,
|
override val comment: String? = null,
|
||||||
@EncodeDefault override val time: Instant? = Clock.System.now(),
|
@EncodeDefault override val time: Instant = Clock.System.now(),
|
||||||
) : DeviceMessage() {
|
) : DeviceMessage() {
|
||||||
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
|
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notifies listeners that a new binary with given [binaryID] is available. The binary itself could not be provided via [DeviceMessage] API.
|
* Notifies listeners that a new binary with given [contentId] and [contentMeta] is available.
|
||||||
|
*
|
||||||
|
* [contentMeta] includes public information that could be shared with loop subscribers. It should not contain sensitive data.
|
||||||
|
*
|
||||||
|
* The binary itself could not be provided via [DeviceMessage] API.
|
||||||
|
* [space.kscience.controls.peer.PeerConnection] must be used instead
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
@SerialName("binary.notification")
|
@SerialName("binary.notification")
|
||||||
public data class BinaryNotificationMessage(
|
public data class BinaryNotificationMessage(
|
||||||
val binaryID: String,
|
val contentId: String,
|
||||||
|
val contentMeta: Meta,
|
||||||
override val sourceDevice: Name,
|
override val sourceDevice: Name,
|
||||||
override val targetDevice: Name? = null,
|
override val targetDevice: Name? = null,
|
||||||
override val comment: String? = null,
|
override val comment: String? = null,
|
||||||
@EncodeDefault override val time: Instant? = Clock.System.now(),
|
@EncodeDefault override val time: Instant = Clock.System.now(),
|
||||||
) : DeviceMessage() {
|
) : DeviceMessage() {
|
||||||
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
|
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
|
||||||
}
|
}
|
||||||
@ -190,7 +196,7 @@ public data class EmptyDeviceMessage(
|
|||||||
override val sourceDevice: Name? = null,
|
override val sourceDevice: Name? = null,
|
||||||
override val targetDevice: Name? = null,
|
override val targetDevice: Name? = null,
|
||||||
override val comment: String? = null,
|
override val comment: String? = null,
|
||||||
@EncodeDefault override val time: Instant? = Clock.System.now(),
|
@EncodeDefault override val time: Instant = Clock.System.now(),
|
||||||
) : DeviceMessage() {
|
) : DeviceMessage() {
|
||||||
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
|
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
|
||||||
}
|
}
|
||||||
@ -203,12 +209,12 @@ public data class EmptyDeviceMessage(
|
|||||||
public data class DeviceLogMessage(
|
public data class DeviceLogMessage(
|
||||||
val message: String,
|
val message: String,
|
||||||
val data: Meta? = null,
|
val data: Meta? = null,
|
||||||
override val sourceDevice: Name? = null,
|
override val sourceDevice: Name = Name.EMPTY,
|
||||||
override val targetDevice: Name? = null,
|
override val targetDevice: Name? = null,
|
||||||
override val comment: String? = null,
|
override val comment: String? = null,
|
||||||
@EncodeDefault override val time: Instant? = Clock.System.now(),
|
@EncodeDefault override val time: Instant = Clock.System.now(),
|
||||||
) : DeviceMessage() {
|
) : DeviceMessage() {
|
||||||
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
|
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -220,10 +226,25 @@ public data class DeviceErrorMessage(
|
|||||||
public val errorMessage: String?,
|
public val errorMessage: String?,
|
||||||
public val errorType: String? = null,
|
public val errorType: String? = null,
|
||||||
public val errorStackTrace: String? = null,
|
public val errorStackTrace: String? = null,
|
||||||
override val sourceDevice: Name,
|
override val sourceDevice: Name = Name.EMPTY,
|
||||||
override val targetDevice: Name? = null,
|
override val targetDevice: Name? = null,
|
||||||
override val comment: String? = null,
|
override val comment: String? = null,
|
||||||
@EncodeDefault override val time: Instant? = Clock.System.now(),
|
@EncodeDefault override val time: Instant = Clock.System.now(),
|
||||||
|
) : DeviceMessage() {
|
||||||
|
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Device [Device.lifecycleState] is changed
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
@SerialName("lifecycle")
|
||||||
|
public data class DeviceLifeCycleMessage(
|
||||||
|
val state: LifecycleState,
|
||||||
|
override val sourceDevice: Name = Name.EMPTY,
|
||||||
|
override val targetDevice: Name? = null,
|
||||||
|
override val comment: String? = null,
|
||||||
|
@EncodeDefault override val time: Instant = Clock.System.now(),
|
||||||
) : DeviceMessage() {
|
) : DeviceMessage() {
|
||||||
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
|
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
|
||||||
}
|
}
|
||||||
|
@ -1,33 +0,0 @@
|
|||||||
package space.kscience.controls.api
|
|
||||||
|
|
||||||
import io.ktor.utils.io.core.Closeable
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A generic bidirectional sender/receiver object
|
|
||||||
*/
|
|
||||||
public interface Socket<T> : Closeable {
|
|
||||||
/**
|
|
||||||
* Send an object to the socket
|
|
||||||
*/
|
|
||||||
public suspend fun send(data: T)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Flow of objects received from socket
|
|
||||||
*/
|
|
||||||
public fun receiving(): Flow<T>
|
|
||||||
public fun isOpen(): Boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Connect an input to this socket using designated [scope] for it and return a handler [Job].
|
|
||||||
* Multiple inputs could be connected to the same [Socket].
|
|
||||||
*/
|
|
||||||
public fun <T> Socket<T>.connectInput(scope: CoroutineScope, flow: Flow<T>): Job = scope.launch {
|
|
||||||
flow.collect { send(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,59 @@
|
|||||||
|
package space.kscience.controls.api
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A lifecycle state of a device
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
public enum class LifecycleState {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Device is initializing
|
||||||
|
*/
|
||||||
|
STARTING,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Device is initialized and running
|
||||||
|
*/
|
||||||
|
STARTED,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Device is closed
|
||||||
|
*/
|
||||||
|
STOPPED,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The device encountered irrecoverable error
|
||||||
|
*/
|
||||||
|
ERROR
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object that could be started or stopped functioning
|
||||||
|
*/
|
||||||
|
public interface WithLifeCycle {
|
||||||
|
|
||||||
|
public suspend fun start()
|
||||||
|
|
||||||
|
public suspend fun stop()
|
||||||
|
|
||||||
|
public val lifecycleState: LifecycleState
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind this object lifecycle to a device lifecycle
|
||||||
|
*
|
||||||
|
* The starting and stopping are done in device scope
|
||||||
|
*/
|
||||||
|
public fun WithLifeCycle.bindToDeviceLifecycle(device: Device){
|
||||||
|
device.onLifecycleEvent {
|
||||||
|
when(it){
|
||||||
|
LifecycleState.STARTING -> start()
|
||||||
|
LifecycleState.STARTED -> {/*ignore*/}
|
||||||
|
LifecycleState.STOPPED -> stop()
|
||||||
|
LifecycleState.ERROR -> stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -12,21 +12,26 @@ import space.kscience.dataforge.meta.descriptors.MetaDescriptorBuilder
|
|||||||
@Serializable
|
@Serializable
|
||||||
public class PropertyDescriptor(
|
public class PropertyDescriptor(
|
||||||
public val name: String,
|
public val name: String,
|
||||||
public var info: String? = null,
|
public var description: String? = null,
|
||||||
public var metaDescriptor: MetaDescriptor = MetaDescriptor(),
|
public var metaDescriptor: MetaDescriptor = MetaDescriptor(),
|
||||||
public var readable: Boolean = true,
|
public var readable: Boolean = true,
|
||||||
public var writable: Boolean = false
|
public var mutable: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
public fun PropertyDescriptor.metaDescriptor(block: MetaDescriptorBuilder.() -> Unit) {
|
public fun PropertyDescriptor.metaDescriptor(block: MetaDescriptorBuilder.() -> Unit) {
|
||||||
metaDescriptor = MetaDescriptor(block)
|
metaDescriptor = MetaDescriptor {
|
||||||
|
from(metaDescriptor)
|
||||||
|
block()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A descriptor for property
|
* A descriptor for property
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
public class ActionDescriptor(public val name: String) {
|
public class ActionDescriptor(
|
||||||
public var info: String? = null
|
public val name: String,
|
||||||
}
|
public var description: String? = null,
|
||||||
|
public var inputMetaDescriptor: MetaDescriptor = MetaDescriptor(),
|
||||||
|
public var outputMetaDescriptor: MetaDescriptor = MetaDescriptor()
|
||||||
|
)
|
||||||
|
@ -0,0 +1,109 @@
|
|||||||
|
package space.kscience.controls.manager
|
||||||
|
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
import space.kscience.controls.api.Device
|
||||||
|
import space.kscience.dataforge.context.*
|
||||||
|
import space.kscience.dataforge.meta.Meta
|
||||||
|
import space.kscience.dataforge.meta.double
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
import kotlin.math.roundToLong
|
||||||
|
import kotlin.time.Duration
|
||||||
|
|
||||||
|
@OptIn(InternalCoroutinesApi::class)
|
||||||
|
private class CompressedTimeDispatcher(
|
||||||
|
val clockManager: ClockManager,
|
||||||
|
val dispatcher: CoroutineDispatcher,
|
||||||
|
val compression: Double,
|
||||||
|
) : CoroutineDispatcher(), Delay {
|
||||||
|
|
||||||
|
@InternalCoroutinesApi
|
||||||
|
override fun dispatchYield(context: CoroutineContext, block: Runnable) {
|
||||||
|
dispatcher.dispatchYield(context, block)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isDispatchNeeded(context: CoroutineContext): Boolean = dispatcher.isDispatchNeeded(context)
|
||||||
|
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
|
override fun limitedParallelism(parallelism: Int): CoroutineDispatcher = dispatcher.limitedParallelism(parallelism)
|
||||||
|
|
||||||
|
override fun dispatch(context: CoroutineContext, block: Runnable) {
|
||||||
|
dispatcher.dispatch(context, block)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val delay = ((dispatcher as? Delay) ?: (Dispatchers.Default as Delay))
|
||||||
|
|
||||||
|
override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
|
||||||
|
delay.scheduleResumeAfterDelay((timeMillis / compression).roundToLong(), continuation)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle {
|
||||||
|
return delay.invokeOnTimeout((timeMillis / compression).roundToLong(), block, context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CompressedClock(
|
||||||
|
val start: Instant,
|
||||||
|
val compression: Double,
|
||||||
|
val baseClock: Clock = Clock.System,
|
||||||
|
) : Clock {
|
||||||
|
override fun now(): Instant {
|
||||||
|
val elapsed = (baseClock.now() - start)
|
||||||
|
return start + elapsed / compression
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ClockManager : AbstractPlugin() {
|
||||||
|
override val tag: PluginTag get() = Companion.tag
|
||||||
|
|
||||||
|
public val timeCompression: Double by meta.double(1.0)
|
||||||
|
|
||||||
|
public val clock: Clock by lazy {
|
||||||
|
if (timeCompression == 1.0) {
|
||||||
|
Clock.System
|
||||||
|
} else {
|
||||||
|
CompressedClock(Clock.System.now(), timeCompression)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide a [CoroutineDispatcher] with compressed time based on given [dispatcher]
|
||||||
|
*/
|
||||||
|
public fun asDispatcher(
|
||||||
|
dispatcher: CoroutineDispatcher = Dispatchers.Default,
|
||||||
|
): CoroutineDispatcher = if (timeCompression == 1.0) {
|
||||||
|
dispatcher
|
||||||
|
} else {
|
||||||
|
CompressedTimeDispatcher(this, dispatcher, timeCompression)
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun scheduleWithFixedDelay(tick: Duration, block: suspend () -> Unit): Job = context.launch(asDispatcher()) {
|
||||||
|
while (isActive) {
|
||||||
|
delay(tick)
|
||||||
|
block()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public companion object : PluginFactory<ClockManager> {
|
||||||
|
override val tag: PluginTag = PluginTag("clock", group = PluginTag.DATAFORGE_GROUP)
|
||||||
|
|
||||||
|
override fun build(context: Context, meta: Meta): ClockManager = ClockManager()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public val Context.clock: Clock get() = plugins[ClockManager]?.clock ?: Clock.System
|
||||||
|
|
||||||
|
public val Device.clock: Clock get() = context.clock
|
||||||
|
|
||||||
|
public fun Device.getCoroutineDispatcher(dispatcher: CoroutineDispatcher = Dispatchers.Default): CoroutineDispatcher =
|
||||||
|
context.plugins[ClockManager]?.asDispatcher(dispatcher) ?: dispatcher
|
||||||
|
|
||||||
|
public fun ContextBuilder.withTimeCompression(compression: Double) {
|
||||||
|
require(compression > 0.0) { "Time compression must be greater than zero." }
|
||||||
|
plugin(ClockManager) {
|
||||||
|
"timeCompression" put compression
|
||||||
|
}
|
||||||
|
}
|
@ -3,12 +3,13 @@ package space.kscience.controls.manager
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import space.kscience.controls.api.Device
|
import space.kscience.controls.api.Device
|
||||||
import space.kscience.controls.api.DeviceHub
|
import space.kscience.controls.api.DeviceHub
|
||||||
import space.kscience.controls.api.getOrNull
|
import space.kscience.controls.api.id
|
||||||
import space.kscience.dataforge.context.*
|
import space.kscience.dataforge.context.*
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
import space.kscience.dataforge.meta.MutableMeta
|
import space.kscience.dataforge.meta.MutableMeta
|
||||||
import space.kscience.dataforge.names.Name
|
import space.kscience.dataforge.names.Name
|
||||||
import space.kscience.dataforge.names.NameToken
|
import space.kscience.dataforge.names.get
|
||||||
|
import space.kscience.dataforge.names.parseAsName
|
||||||
import kotlin.collections.set
|
import kotlin.collections.set
|
||||||
import kotlin.properties.ReadOnlyProperty
|
import kotlin.properties.ReadOnlyProperty
|
||||||
|
|
||||||
@ -21,11 +22,11 @@ public class DeviceManager : AbstractPlugin(), DeviceHub {
|
|||||||
/**
|
/**
|
||||||
* Actual list of connected devices
|
* Actual list of connected devices
|
||||||
*/
|
*/
|
||||||
private val top = HashMap<NameToken, Device>()
|
private val _devices = HashMap<Name, Device>()
|
||||||
override val devices: Map<NameToken, Device> get() = top
|
override val devices: Map<Name, Device> get() = _devices
|
||||||
|
|
||||||
public fun registerDevice(name: NameToken, device: Device) {
|
public fun registerDevice(name: Name, device: Device) {
|
||||||
top[name] = device
|
_devices[name] = device
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun content(target: String): Map<Name, Any> = super<DeviceHub>.content(target)
|
override fun content(target: String): Map<Name, Any> = super<DeviceHub>.content(target)
|
||||||
@ -38,13 +39,19 @@ public class DeviceManager : AbstractPlugin(), DeviceHub {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public fun <D : Device> DeviceManager.install(name: String, device: D): D {
|
public fun <D : Device> DeviceManager.install(name: String, device: D): D {
|
||||||
registerDevice(NameToken(name), device)
|
registerDevice(name.parseAsName(), device)
|
||||||
device.launch {
|
device.launch {
|
||||||
device.open()
|
device.start()
|
||||||
}
|
}
|
||||||
return device
|
return device
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public fun <D : Device> DeviceManager.install(device: D): D = install(device.id, device)
|
||||||
|
|
||||||
|
|
||||||
|
public fun <D : Device> Context.install(name: String, device: D): D = request(DeviceManager).install(name, device)
|
||||||
|
|
||||||
|
public fun <D : Device> Context.install(device: D): D = request(DeviceManager).install(device.id, device)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register and start a device built by [factory] with current [Context] and [meta].
|
* Register and start a device built by [factory] with current [Context] and [meta].
|
||||||
@ -62,7 +69,7 @@ public inline fun <D : Device> DeviceManager.installing(
|
|||||||
val meta = Meta(builder)
|
val meta = Meta(builder)
|
||||||
return ReadOnlyProperty { _, property ->
|
return ReadOnlyProperty { _, property ->
|
||||||
val name = property.name
|
val name = property.name
|
||||||
val current = getOrNull(name)
|
val current = devices[name]
|
||||||
if (current == null) {
|
if (current == null) {
|
||||||
install(name, factory, meta)
|
install(name, factory, meta)
|
||||||
} else if (current.meta != meta) {
|
} else if (current.meta != meta) {
|
||||||
@ -73,4 +80,3 @@ public inline fun <D : Device> DeviceManager.installing(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user