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)
|
||||
.idea/
|
||||
.gradle
|
||||
.kotlin
|
||||
|
||||
*.iws
|
||||
*.iml
|
||||
@ -8,4 +9,7 @@
|
||||
|
||||
out/
|
||||
build/
|
||||
|
||||
!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
|
||||
|
||||
### 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
|
||||
- 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
|
||||
@ -20,13 +78,3 @@
|
||||
- A magix event loop implementation in Kotlin. Includes HTTP/SSE and RSocket routes.
|
||||
- Magix history database API
|
||||
- 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)
|
||||
|
||||
[![](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 (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
|
||||
|
||||
|
||||
### [controls-constructor](controls-constructor)
|
||||
> A low-code constructor for composite devices simulation
|
||||
>
|
||||
> **Maturity**: PROTOTYPE
|
||||
|
||||
### [controls-core](controls-core)
|
||||
> 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
|
||||
|
||||
|
||||
### [controls-jupyter](controls-jupyter)
|
||||
>
|
||||
> **Maturity**: EXPERIMENTAL
|
||||
|
||||
### [controls-magix](controls-magix)
|
||||
> Magix service for binding controls devices (both as RPC client and server)
|
||||
>
|
||||
@ -93,6 +104,11 @@ Automatically checks consistency.
|
||||
>
|
||||
> **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)
|
||||
> Implementation of byte ports on top os ktor-io asynchronous API
|
||||
>
|
||||
@ -113,6 +129,16 @@ Automatically checks consistency.
|
||||
>
|
||||
> **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)
|
||||
>
|
||||
> **Maturity**: EXPERIMENTAL
|
||||
@ -134,6 +160,14 @@ Automatically checks consistency.
|
||||
>
|
||||
> **Maturity**: EXPERIMENTAL
|
||||
|
||||
### [demo/constructor](demo/constructor)
|
||||
>
|
||||
> **Maturity**: EXPERIMENTAL
|
||||
|
||||
### [demo/device-collective](demo/device-collective)
|
||||
>
|
||||
> **Maturity**: EXPERIMENTAL
|
||||
|
||||
### [demo/echo](demo/echo)
|
||||
>
|
||||
> **Maturity**: EXPERIMENTAL
|
||||
@ -189,6 +223,11 @@ Automatically checks consistency.
|
||||
>
|
||||
> **Maturity**: PROTOTYPE
|
||||
|
||||
### [magix/magix-utils](magix/magix-utils)
|
||||
> Common utilities and services for Magix endpoints.
|
||||
>
|
||||
> **Maturity**: EXPERIMENTAL
|
||||
|
||||
### [magix/magix-zmq](magix/magix-zmq)
|
||||
> ZMQ client endpoint for Magix
|
||||
>
|
||||
|
@ -1,37 +1,26 @@
|
||||
import space.kscience.gradle.isInDevelopment
|
||||
import space.kscience.gradle.useApache2Licence
|
||||
import space.kscience.gradle.useSPCTeam
|
||||
|
||||
plugins {
|
||||
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 {
|
||||
group = "space.kscience"
|
||||
version = "0.2.0"
|
||||
version = "0.4.0-dev-6"
|
||||
repositories{
|
||||
maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
|
||||
google()
|
||||
}
|
||||
}
|
||||
|
||||
ksciencePublish {
|
||||
pom("https://github.com/SciProgCentre/controls.kt") {
|
||||
pom("https://github.com/SciProgCentre/controls-kt") {
|
||||
useApache2Licence()
|
||||
useSPCTeam()
|
||||
}
|
||||
github("controls.kt", "SciProgCentre")
|
||||
space(
|
||||
if (isInDevelopment) {
|
||||
"https://maven.pkg.jetbrains.space/spc/p/sci/dev"
|
||||
} else {
|
||||
"https://maven.pkg.jetbrains.space/spc/p/sci/maven"
|
||||
}
|
||||
)
|
||||
repository("spc","https://maven.sciprog.center/kscience")
|
||||
sonatype("https://oss.sonatype.org")
|
||||
}
|
||||
|
||||
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:
|
||||
|
||||
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:**
|
||||
```kotlin
|
||||
repositories {
|
||||
maven("https://repo.kotlin.link")
|
||||
//uncomment to access development builds
|
||||
//maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
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
|
||||
""".trimIndent()
|
||||
|
||||
val dataforgeVersion: String by rootProject.extra
|
||||
|
||||
kscience {
|
||||
jvm()
|
||||
js()
|
||||
native()
|
||||
wasm()
|
||||
useCoroutines()
|
||||
useSerialization{
|
||||
json()
|
||||
}
|
||||
useContextReceivers()
|
||||
dependencies {
|
||||
api("space.kscience:dataforge-io:$dataforgeVersion")
|
||||
commonMain {
|
||||
api(libs.dataforge.io)
|
||||
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.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.*
|
||||
import space.kscience.controls.api.Device.Companion.DEVICE_TARGET
|
||||
import space.kscience.dataforge.context.ContextAware
|
||||
import space.kscience.dataforge.context.info
|
||||
import space.kscience.dataforge.context.logger
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import space.kscience.dataforge.misc.DFExperimental
|
||||
import space.kscience.dataforge.misc.Type
|
||||
import space.kscience.dataforge.names.Name
|
||||
|
||||
/**
|
||||
* A lifecycle state of a device
|
||||
*/
|
||||
public enum class DeviceLifecycleState{
|
||||
INIT,
|
||||
OPEN,
|
||||
CLOSED
|
||||
}
|
||||
import space.kscience.dataforge.meta.get
|
||||
import space.kscience.dataforge.meta.string
|
||||
import space.kscience.dataforge.misc.DfType
|
||||
import space.kscience.dataforge.names.parseAsName
|
||||
|
||||
/**
|
||||
* General interface describing a managed Device.
|
||||
* [Device] is a supervisor scope encompassing all operations on a device.
|
||||
* When canceled, cancels all running processes.
|
||||
*/
|
||||
@Type(DEVICE_TARGET)
|
||||
public interface Device : AutoCloseable, ContextAware, CoroutineScope {
|
||||
@DfType(DEVICE_TARGET)
|
||||
public interface Device : ContextAware, WithLifeCycle, CoroutineScope {
|
||||
|
||||
/**
|
||||
* Initial configuration meta for the device
|
||||
*/
|
||||
public val meta: Meta get() = Meta.EMPTY
|
||||
|
||||
|
||||
/**
|
||||
* List of supported property descriptors
|
||||
*/
|
||||
@ -54,18 +44,6 @@ public interface Device : AutoCloseable, ContextAware, CoroutineScope {
|
||||
*/
|
||||
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].
|
||||
* 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?
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
override fun close() {
|
||||
override suspend fun stop() {
|
||||
coroutineContext[Job]?.cancel("The device is closed")
|
||||
logger.info { "Device $this is closed" }
|
||||
cancel("The device is closed")
|
||||
}
|
||||
|
||||
@DFExperimental
|
||||
public val lifecycleState: DeviceLifecycleState
|
||||
|
||||
public companion object {
|
||||
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.
|
||||
*/
|
||||
public suspend fun Device.getOrReadProperty(propertyName: String): Meta =
|
||||
public suspend fun Device.getOrReadProperty(propertyName: String): Meta = if (this is CachingDevice) {
|
||||
getProperty(propertyName) ?: readProperty(propertyName)
|
||||
} else {
|
||||
readProperty(propertyName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a snapshot of the device logical state
|
||||
*
|
||||
*/
|
||||
public fun Device.getAllProperties(): Meta = Meta {
|
||||
public fun CachingDevice.getAllProperties(): Meta = Meta {
|
||||
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
|
||||
*/
|
||||
public fun Device.onPropertyChange(callback: suspend PropertyChangedMessage.() -> Unit): Job =
|
||||
messageFlow.filterIsInstance<PropertyChangedMessage>().onEach(callback).launchIn(this)
|
||||
public fun Device.onPropertyChange(
|
||||
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
|
||||
|
||||
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.asPath
|
||||
import space.kscience.dataforge.provider.plus
|
||||
|
||||
/**
|
||||
* A hub that could locate multiple devices and redirect actions to them
|
||||
*/
|
||||
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 defaultChainTarget: String get() = Device.DEVICE_TARGET
|
||||
|
||||
override fun content(target: String): Map<Name, Any> = if (target == Device.DEVICE_TARGET) {
|
||||
buildMap {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
devices
|
||||
} else {
|
||||
emptyMap()
|
||||
}
|
||||
//TODO send message on device change
|
||||
|
||||
public companion object
|
||||
}
|
||||
|
||||
public operator fun DeviceHub.get(nameToken: NameToken): Device =
|
||||
devices[nameToken] ?: error("Device with name $nameToken not found in $this")
|
||||
|
||||
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 fun DeviceHub(deviceMap: Map<Name, Device>): DeviceHub = object : DeviceHub {
|
||||
override val devices: Map<Name, Device>
|
||||
get() = deviceMap
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
public operator fun DeviceHub.get(nameString: String): Device =
|
||||
getOrNull(nameString) ?: error("Device with name $nameString not found in $this")
|
||||
devices.forEach {
|
||||
val name: Name = it.key
|
||||
put(name.asPath(), it.value)
|
||||
(it.value as? DeviceHub)?.let { hub ->
|
||||
putAll(name.asPath(), hub)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
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? =
|
||||
this[deviceName].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)
|
||||
//}
|
||||
(devices[deviceName] ?: error("Device with name $deviceName not found in $this")).execute(command, argument)
|
@ -22,10 +22,10 @@ public sealed class DeviceMessage {
|
||||
public abstract val sourceDevice: Name?
|
||||
public abstract val targetDevice: Name?
|
||||
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
|
||||
|
||||
@ -59,7 +59,7 @@ public data class PropertyChangedMessage(
|
||||
override val sourceDevice: Name = Name.EMPTY,
|
||||
override val targetDevice: Name? = 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))
|
||||
}
|
||||
@ -71,11 +71,11 @@ public data class PropertyChangedMessage(
|
||||
@SerialName("property.set")
|
||||
public data class PropertySetMessage(
|
||||
public val property: String,
|
||||
public val value: Meta?,
|
||||
public val value: Meta,
|
||||
override val sourceDevice: Name? = null,
|
||||
override val targetDevice: Name,
|
||||
override val targetDevice: Name?,
|
||||
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 = sourceDevice?.let(block))
|
||||
}
|
||||
@ -91,7 +91,7 @@ public data class PropertyGetMessage(
|
||||
override val sourceDevice: Name? = null,
|
||||
override val targetDevice: Name,
|
||||
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 = sourceDevice?.let(block))
|
||||
}
|
||||
@ -103,9 +103,9 @@ public data class PropertyGetMessage(
|
||||
@SerialName("description.get")
|
||||
public data class GetDescriptionMessage(
|
||||
override val sourceDevice: Name? = null,
|
||||
override val targetDevice: Name,
|
||||
override val targetDevice: Name? = 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 = sourceDevice?.let(block))
|
||||
}
|
||||
@ -122,7 +122,7 @@ public data class DescriptionMessage(
|
||||
override val sourceDevice: Name,
|
||||
override val targetDevice: Name? = 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))
|
||||
}
|
||||
@ -141,7 +141,7 @@ public data class ActionExecuteMessage(
|
||||
override val sourceDevice: Name? = null,
|
||||
override val targetDevice: Name,
|
||||
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 = sourceDevice?.let(block))
|
||||
}
|
||||
@ -160,22 +160,28 @@ public data class ActionResultMessage(
|
||||
override val sourceDevice: Name,
|
||||
override val targetDevice: Name? = 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))
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
@SerialName("binary.notification")
|
||||
public data class BinaryNotificationMessage(
|
||||
val binaryID: String,
|
||||
val contentId: String,
|
||||
val contentMeta: Meta,
|
||||
override val sourceDevice: Name,
|
||||
override val targetDevice: Name? = 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))
|
||||
}
|
||||
@ -190,7 +196,7 @@ public data class EmptyDeviceMessage(
|
||||
override val sourceDevice: Name? = null,
|
||||
override val targetDevice: Name? = 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 = sourceDevice?.let(block))
|
||||
}
|
||||
@ -203,12 +209,12 @@ public data class EmptyDeviceMessage(
|
||||
public data class DeviceLogMessage(
|
||||
val message: String,
|
||||
val data: Meta? = null,
|
||||
override val sourceDevice: Name? = null,
|
||||
override val sourceDevice: Name = Name.EMPTY,
|
||||
override val targetDevice: Name? = 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 = 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 errorType: String? = null,
|
||||
public val errorStackTrace: String? = null,
|
||||
override val sourceDevice: Name,
|
||||
override val sourceDevice: Name = Name.EMPTY,
|
||||
override val targetDevice: Name? = 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() {
|
||||
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
|
||||
public class PropertyDescriptor(
|
||||
public val name: String,
|
||||
public var info: String? = null,
|
||||
public var description: String? = null,
|
||||
public var metaDescriptor: MetaDescriptor = MetaDescriptor(),
|
||||
public var readable: Boolean = true,
|
||||
public var writable: Boolean = false
|
||||
public var mutable: Boolean = false,
|
||||
)
|
||||
|
||||
public fun PropertyDescriptor.metaDescriptor(block: MetaDescriptorBuilder.()->Unit){
|
||||
metaDescriptor = MetaDescriptor(block)
|
||||
public fun PropertyDescriptor.metaDescriptor(block: MetaDescriptorBuilder.() -> Unit) {
|
||||
metaDescriptor = MetaDescriptor {
|
||||
from(metaDescriptor)
|
||||
block()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A descriptor for property
|
||||
*/
|
||||
@Serializable
|
||||
public class ActionDescriptor(public val name: String) {
|
||||
public var info: String? = null
|
||||
}
|
||||
|
||||
public class ActionDescriptor(
|
||||
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 space.kscience.controls.api.Device
|
||||
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.meta.Meta
|
||||
import space.kscience.dataforge.meta.MutableMeta
|
||||
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.properties.ReadOnlyProperty
|
||||
|
||||
@ -21,11 +22,11 @@ public class DeviceManager : AbstractPlugin(), DeviceHub {
|
||||
/**
|
||||
* Actual list of connected devices
|
||||
*/
|
||||
private val top = HashMap<NameToken, Device>()
|
||||
override val devices: Map<NameToken, Device> get() = top
|
||||
private val _devices = HashMap<Name, Device>()
|
||||
override val devices: Map<Name, Device> get() = _devices
|
||||
|
||||
public fun registerDevice(name: NameToken, device: Device) {
|
||||
top[name] = device
|
||||
public fun registerDevice(name: Name, device: Device) {
|
||||
_devices[name] = device
|
||||
}
|
||||
|
||||
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 {
|
||||
registerDevice(NameToken(name), device)
|
||||
registerDevice(name.parseAsName(), device)
|
||||
device.launch {
|
||||
device.open()
|
||||
device.start()
|
||||
}
|
||||
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].
|
||||
@ -62,7 +69,7 @@ public inline fun <D : Device> DeviceManager.installing(
|
||||
val meta = Meta(builder)
|
||||
return ReadOnlyProperty { _, property ->
|
||||
val name = property.name
|
||||
val current = getOrNull(name)
|
||||
val current = devices[name]
|
||||
if (current == null) {
|
||||
install(name, factory, 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