Compare commits
No commits in common. "dev-maxim" and "master" have entirely different histories.
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,7 +1,6 @@
|
|||||||
# Created by .ignore support plugin (hsz.mobi)
|
# Created by .ignore support plugin (hsz.mobi)
|
||||||
.idea/
|
.idea/
|
||||||
.gradle
|
.gradle
|
||||||
.kotlin
|
|
||||||
|
|
||||||
*.iws
|
*.iws
|
||||||
*.iml
|
*.iml
|
||||||
@ -9,7 +8,4 @@
|
|||||||
|
|
||||||
out/
|
out/
|
||||||
build/
|
build/
|
||||||
|
|
||||||
!gradle-wrapper.jar
|
!gradle-wrapper.jar
|
||||||
|
|
||||||
/demo/device-collective/mapCache/
|
|
||||||
|
45
.space.kts
Normal file
45
.space.kts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
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
.space/CODEOWNERS
Normal file
1
.space/CODEOWNERS
Normal file
@ -0,0 +1 @@
|
|||||||
|
./space/* "Project Admin"
|
68
CHANGELOG.md
68
CHANGELOG.md
@ -3,64 +3,6 @@
|
|||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Value averaging plot extension
|
|
||||||
- PLC4X bindings
|
|
||||||
- Shortcuts to access all Controls devices in a magix network.
|
|
||||||
- `DeviceClient` properly evaluates lifecycle and logs
|
|
||||||
- `PeerConnection` API for direct device-device binary sharing
|
|
||||||
- DeviceDrawable2D intermediate visualization implementation
|
|
||||||
- New interface `WithLifeCycle`. Change Port API to adhere to it.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Constructor properties return `DeviceState` in order to be able to subscribe to them
|
|
||||||
- Refactored ports. Now we have `AsynchronousPort` as well as `SynchronousPort`
|
|
||||||
- `DeviceClient` now initializes property and action descriptors eagerly.
|
|
||||||
- `DeviceHub` now works with `Name` instead of `NameToken`. Tree-like structure is made using `Path`. Device messages no longer have access to sub-devices.
|
|
||||||
- Add some utility methods to ports. Synchronous port response could be now consumed as `Source`.
|
|
||||||
- `DeviceLifecycleState` is replaced by `LifecycleState`.
|
|
||||||
|
|
||||||
|
|
||||||
### Deprecated
|
|
||||||
|
|
||||||
### Removed
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Fix a problem with rsocket endpoint with no filter.
|
|
||||||
|
|
||||||
### Security
|
|
||||||
|
|
||||||
## 0.3.0 - 2024-03-04
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Device lifecycle message
|
|
||||||
- Low-code constructor
|
|
||||||
- Automatic description generation for spec properties (JVM only)
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Property caching moved from core `Device` to the `CachingDevice`
|
|
||||||
- `DeviceSpec` properties no explicitly pass property name to getters and setters.
|
|
||||||
- `DeviceHub.respondHubMessage` now returns a list of messages to allow querying multiple devices. Device server also returns an array.
|
|
||||||
- DataForge 0.8.0
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Property writing does not trigger change if logical state already is the same as value to be set.
|
|
||||||
- Modbus-slave triggers only once for multi-register write.
|
|
||||||
- Removed unnecessary scope in hub messageFlow
|
|
||||||
|
|
||||||
## 0.2.2-dev-1 - 2023-09-24
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- updating logical state in `DeviceBase` is now protected and called `propertyChanged()`
|
|
||||||
- `DeviceBase` tries to read property after write if the writer does not set the value.
|
|
||||||
|
|
||||||
## 0.2.1 - 2023-09-24
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Core interfaces for building a device server
|
- Core interfaces for building a device server
|
||||||
- Magix service for binding controls devices (both as RPC client and server)
|
- Magix service for binding controls devices (both as RPC client and server)
|
||||||
- A plugin for Controls-kt device server on top of modbus-rtu/modbus-tcp protocols
|
- A plugin for Controls-kt device server on top of modbus-rtu/modbus-tcp protocols
|
||||||
@ -78,3 +20,13 @@
|
|||||||
- A magix event loop implementation in Kotlin. Includes HTTP/SSE and RSocket routes.
|
- A magix event loop implementation in Kotlin. Includes HTTP/SSE and RSocket routes.
|
||||||
- Magix history database API
|
- Magix history database API
|
||||||
- ZMQ client endpoint for Magix
|
- ZMQ client endpoint for Magix
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
### Security
|
||||||
|
39
README.md
39
README.md
@ -1,7 +1,5 @@
|
|||||||
[![JetBrains Research](https://jb.gg/badges/research.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub)
|
[![JetBrains Research](https://jb.gg/badges/research.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub)
|
||||||
|
|
||||||
[![](https://maven.sciprog.center/api/badge/latest/kscience/space/kscience/controls-core-jvm?color=40c14a&name=repo.kotlin.link&prefix=v)](https://maven.sciprog.center/)
|
|
||||||
|
|
||||||
# Controls.kt
|
# Controls.kt
|
||||||
|
|
||||||
Controls.kt (former DataForge-control) is a data acquisition framework (work in progress). It is based on DataForge, a software framework for automated data processing.
|
Controls.kt (former DataForge-control) is a data acquisition framework (work in progress). It is based on DataForge, a software framework for automated data processing.
|
||||||
@ -44,11 +42,6 @@ Example view of a demo:
|
|||||||
## Modules
|
## Modules
|
||||||
|
|
||||||
|
|
||||||
### [controls-constructor](controls-constructor)
|
|
||||||
> A low-code constructor for composite devices simulation
|
|
||||||
>
|
|
||||||
> **Maturity**: PROTOTYPE
|
|
||||||
|
|
||||||
### [controls-core](controls-core)
|
### [controls-core](controls-core)
|
||||||
> Core interfaces for building a device server
|
> Core interfaces for building a device server
|
||||||
>
|
>
|
||||||
@ -63,10 +56,6 @@ Example view of a demo:
|
|||||||
> - [ports](controls-core/src/commonMain/kotlin/space/kscience/controls/ports) : Working with asynchronous data sending and receiving raw byte arrays
|
> - [ports](controls-core/src/commonMain/kotlin/space/kscience/controls/ports) : Working with asynchronous data sending and receiving raw byte arrays
|
||||||
|
|
||||||
|
|
||||||
### [controls-jupyter](controls-jupyter)
|
|
||||||
>
|
|
||||||
> **Maturity**: EXPERIMENTAL
|
|
||||||
|
|
||||||
### [controls-magix](controls-magix)
|
### [controls-magix](controls-magix)
|
||||||
> Magix service for binding controls devices (both as RPC client and server)
|
> Magix service for binding controls devices (both as RPC client and server)
|
||||||
>
|
>
|
||||||
@ -104,11 +93,6 @@ Automatically checks consistency.
|
|||||||
>
|
>
|
||||||
> **Maturity**: EXPERIMENTAL
|
> **Maturity**: EXPERIMENTAL
|
||||||
|
|
||||||
### [controls-plc4x](controls-plc4x)
|
|
||||||
> A plugin for Controls-kt device server on top of plc4x library
|
|
||||||
>
|
|
||||||
> **Maturity**: EXPERIMENTAL
|
|
||||||
|
|
||||||
### [controls-ports-ktor](controls-ports-ktor)
|
### [controls-ports-ktor](controls-ports-ktor)
|
||||||
> Implementation of byte ports on top os ktor-io asynchronous API
|
> Implementation of byte ports on top os ktor-io asynchronous API
|
||||||
>
|
>
|
||||||
@ -129,16 +113,6 @@ Automatically checks consistency.
|
|||||||
>
|
>
|
||||||
> **Maturity**: PROTOTYPE
|
> **Maturity**: PROTOTYPE
|
||||||
|
|
||||||
### [controls-vision](controls-vision)
|
|
||||||
> Dashboard and visualization extensions for devices
|
|
||||||
>
|
|
||||||
> **Maturity**: PROTOTYPE
|
|
||||||
|
|
||||||
### [controls-visualisation-compose](controls-visualisation-compose)
|
|
||||||
> Visualisation extension using compose-multiplatform
|
|
||||||
>
|
|
||||||
> **Maturity**: PROTOTYPE
|
|
||||||
|
|
||||||
### [demo](demo)
|
### [demo](demo)
|
||||||
>
|
>
|
||||||
> **Maturity**: EXPERIMENTAL
|
> **Maturity**: EXPERIMENTAL
|
||||||
@ -160,14 +134,6 @@ Automatically checks consistency.
|
|||||||
>
|
>
|
||||||
> **Maturity**: EXPERIMENTAL
|
> **Maturity**: EXPERIMENTAL
|
||||||
|
|
||||||
### [demo/constructor](demo/constructor)
|
|
||||||
>
|
|
||||||
> **Maturity**: EXPERIMENTAL
|
|
||||||
|
|
||||||
### [demo/device-collective](demo/device-collective)
|
|
||||||
>
|
|
||||||
> **Maturity**: EXPERIMENTAL
|
|
||||||
|
|
||||||
### [demo/echo](demo/echo)
|
### [demo/echo](demo/echo)
|
||||||
>
|
>
|
||||||
> **Maturity**: EXPERIMENTAL
|
> **Maturity**: EXPERIMENTAL
|
||||||
@ -223,11 +189,6 @@ Automatically checks consistency.
|
|||||||
>
|
>
|
||||||
> **Maturity**: PROTOTYPE
|
> **Maturity**: PROTOTYPE
|
||||||
|
|
||||||
### [magix/magix-utils](magix/magix-utils)
|
|
||||||
> Common utilities and services for Magix endpoints.
|
|
||||||
>
|
|
||||||
> **Maturity**: EXPERIMENTAL
|
|
||||||
|
|
||||||
### [magix/magix-zmq](magix/magix-zmq)
|
### [magix/magix-zmq](magix/magix-zmq)
|
||||||
> ZMQ client endpoint for Magix
|
> ZMQ client endpoint for Magix
|
||||||
>
|
>
|
||||||
|
@ -1,26 +1,37 @@
|
|||||||
|
import space.kscience.gradle.isInDevelopment
|
||||||
import space.kscience.gradle.useApache2Licence
|
import space.kscience.gradle.useApache2Licence
|
||||||
import space.kscience.gradle.useSPCTeam
|
import space.kscience.gradle.useSPCTeam
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("space.kscience.gradle.project")
|
id("space.kscience.gradle.project")
|
||||||
alias(libs.plugins.versions)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val dataforgeVersion: String by extra("0.6.2-dev-3")
|
||||||
|
val ktorVersion: String by extra(space.kscience.gradle.KScienceVersions.ktorVersion)
|
||||||
|
val rsocketVersion by extra("0.15.4")
|
||||||
|
val xodusVersion by extra("2.0.1")
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
group = "space.kscience"
|
group = "space.kscience"
|
||||||
version = "0.4.0-dev-6"
|
version = "0.2.0"
|
||||||
repositories{
|
repositories{
|
||||||
google()
|
maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ksciencePublish {
|
ksciencePublish {
|
||||||
pom("https://github.com/SciProgCentre/controls-kt") {
|
pom("https://github.com/SciProgCentre/controls.kt") {
|
||||||
useApache2Licence()
|
useApache2Licence()
|
||||||
useSPCTeam()
|
useSPCTeam()
|
||||||
}
|
}
|
||||||
repository("spc","https://maven.sciprog.center/kscience")
|
github("controls.kt", "SciProgCentre")
|
||||||
sonatype("https://oss.sonatype.org")
|
space(
|
||||||
|
if (isInDevelopment) {
|
||||||
|
"https://maven.pkg.jetbrains.space/spc/p/sci/dev"
|
||||||
|
} else {
|
||||||
|
"https://maven.pkg.jetbrains.space/spc/p/sci/maven"
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
readme.readmeTemplate = file("docs/templates/README-TEMPLATE.md")
|
readme.readmeTemplate = file("docs/templates/README-TEMPLATE.md")
|
@ -1,21 +0,0 @@
|
|||||||
# 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")
|
|
||||||
}
|
|
||||||
```
|
|
@ -1,31 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,241 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,150 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,318 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,103 +0,0 @@
|
|||||||
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)"
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,39 +0,0 @@
|
|||||||
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)"
|
|
||||||
}
|
|
@ -1,103 +0,0 @@
|
|||||||
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)
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
@ -1,38 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
@ -1,65 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
|||||||
package space.kscience.controls.constructor.dsl.core
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Класс для представления аннотации.
|
|
||||||
*/
|
|
||||||
public data class Annotation(
|
|
||||||
val name: String,
|
|
||||||
val properties: Map<String, Any>
|
|
||||||
)
|
|
@ -1,196 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
@ -1,19 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
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())
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,80 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,81 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
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
|
|
@ -1,163 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
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()
|
|
@ -1,24 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
package space.kscience.controls.constructor.dsl.core.equations
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Абстрактный базовый класс для уравнений.
|
|
||||||
*/
|
|
||||||
public sealed class EquationBase
|
|
@ -1,11 +0,0 @@
|
|||||||
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()
|
|
@ -1,18 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,91 +0,0 @@
|
|||||||
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())
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
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.")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
package space.kscience.controls.constructor.dsl.core.expressions
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Перечисление для бинарных операций над массивами.
|
|
||||||
*/
|
|
||||||
public enum class ArrayBinaryOperator {
|
|
||||||
ADD,
|
|
||||||
SUBTRACT,
|
|
||||||
MULTIPLY,
|
|
||||||
DIVIDE
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
package space.kscience.controls.constructor.dsl.core.expressions
|
|
||||||
|
|
||||||
public sealed class ArrayExpression : Expression()
|
|
@ -1,34 +0,0 @@
|
|||||||
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.")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
package space.kscience.controls.constructor.dsl.core.expressions
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Перечисление операторов между массивом и скаляром.
|
|
||||||
*/
|
|
||||||
public enum class ArrayScalarOperator {
|
|
||||||
MULTIPLY,
|
|
||||||
DIVIDE
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
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.")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,135 +0,0 @@
|
|||||||
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.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
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.")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,91 +0,0 @@
|
|||||||
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.")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
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.")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,32 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
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
|
|
||||||
)
|
|
@ -1,50 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
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>>
|
|
||||||
)
|
|
@ -1,93 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,78 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,51 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,89 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
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
|
|
||||||
)
|
|
@ -1,28 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
package space.kscience.controls.constructor.dsl.core.variables
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Перечисление каузальности переменной.
|
|
||||||
*/
|
|
||||||
public enum class Causality {
|
|
||||||
INPUT,
|
|
||||||
OUTPUT,
|
|
||||||
INTERNAL
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
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)
|
|
@ -1,22 +0,0 @@
|
|||||||
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.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
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)
|
|
@ -1,25 +0,0 @@
|
|||||||
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.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
package space.kscience.controls.constructor.dsl.core.variables
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Перечисление типов изменчивости переменной.
|
|
||||||
*/
|
|
||||||
public enum class Variability {
|
|
||||||
CONSTANT,
|
|
||||||
PARAMETER,
|
|
||||||
DISCRETE,
|
|
||||||
CONTINUOUS
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
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() {}
|
|
||||||
}
|
|
@ -1,65 +0,0 @@
|
|||||||
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)
|
|
@ -1,20 +0,0 @@
|
|||||||
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)
|
|
@ -1,53 +0,0 @@
|
|||||||
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)"
|
|
||||||
|
|
||||||
}
|
|
@ -1,70 +0,0 @@
|
|||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
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 )
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,48 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,73 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,70 +0,0 @@
|
|||||||
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)
|
|
@ -1,25 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
package space.kscience.controls.constructor.units
|
|
||||||
|
|
||||||
public enum class Direction(public val coef: Int) {
|
|
||||||
UP(1),
|
|
||||||
DOWN(-1)
|
|
||||||
}
|
|
@ -1,60 +0,0 @@
|
|||||||
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>>
|
|
@ -1,60 +0,0 @@
|
|||||||
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
|
|
@ -1,44 +0,0 @@
|
|||||||
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)
|
|
@ -1,43 +0,0 @@
|
|||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,89 +0,0 @@
|
|||||||
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"))
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,73 +0,0 @@
|
|||||||
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"))
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,101 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
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()
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,85 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,41 +0,0 @@
|
|||||||
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,16 +16,18 @@ Core interfaces for building a device server
|
|||||||
|
|
||||||
## Artifact:
|
## Artifact:
|
||||||
|
|
||||||
The Maven coordinates of this project are `space.kscience:controls-core:0.4.0-dev-4`.
|
The Maven coordinates of this project are `space.kscience:controls-core:0.2.0`.
|
||||||
|
|
||||||
**Gradle Kotlin DSL:**
|
**Gradle Kotlin DSL:**
|
||||||
```kotlin
|
```kotlin
|
||||||
repositories {
|
repositories {
|
||||||
maven("https://repo.kotlin.link")
|
maven("https://repo.kotlin.link")
|
||||||
|
//uncomment to access development builds
|
||||||
|
//maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("space.kscience:controls-core:0.4.0-dev-4")
|
implementation("space.kscience:controls-core:0.2.0")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -9,24 +9,21 @@ description = """
|
|||||||
Core interfaces for building a device server
|
Core interfaces for building a device server
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
|
||||||
|
val dataforgeVersion: String by rootProject.extra
|
||||||
|
|
||||||
kscience {
|
kscience {
|
||||||
jvm()
|
jvm()
|
||||||
js()
|
js()
|
||||||
native()
|
native()
|
||||||
wasm()
|
|
||||||
useCoroutines()
|
useCoroutines()
|
||||||
useSerialization{
|
useSerialization{
|
||||||
json()
|
json()
|
||||||
}
|
}
|
||||||
useContextReceivers()
|
useContextReceivers()
|
||||||
commonMain {
|
dependencies {
|
||||||
api(libs.dataforge.io)
|
api("space.kscience:dataforge-io:$dataforgeVersion")
|
||||||
api(spclibs.kotlinx.datetime)
|
api(spclibs.kotlinx.datetime)
|
||||||
}
|
}
|
||||||
|
|
||||||
jvmTest{
|
|
||||||
implementation(spclibs.logback.classic)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
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,31 +3,41 @@ package space.kscience.controls.api
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.filterIsInstance
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import space.kscience.controls.api.Device.Companion.DEVICE_TARGET
|
import space.kscience.controls.api.Device.Companion.DEVICE_TARGET
|
||||||
import space.kscience.dataforge.context.ContextAware
|
import space.kscience.dataforge.context.ContextAware
|
||||||
import space.kscience.dataforge.context.info
|
import space.kscience.dataforge.context.info
|
||||||
import space.kscience.dataforge.context.logger
|
import space.kscience.dataforge.context.logger
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
import space.kscience.dataforge.meta.get
|
import space.kscience.dataforge.misc.DFExperimental
|
||||||
import space.kscience.dataforge.meta.string
|
import space.kscience.dataforge.misc.Type
|
||||||
import space.kscience.dataforge.misc.DfType
|
import space.kscience.dataforge.names.Name
|
||||||
import space.kscience.dataforge.names.parseAsName
|
|
||||||
|
/**
|
||||||
|
* A lifecycle state of a device
|
||||||
|
*/
|
||||||
|
public enum class DeviceLifecycleState{
|
||||||
|
INIT,
|
||||||
|
OPEN,
|
||||||
|
CLOSED
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* General interface describing a managed Device.
|
* General interface describing a managed Device.
|
||||||
* [Device] is a supervisor scope encompassing all operations on a device.
|
* [Device] is a supervisor scope encompassing all operations on a device.
|
||||||
* When canceled, cancels all running processes.
|
* When canceled, cancels all running processes.
|
||||||
*/
|
*/
|
||||||
@DfType(DEVICE_TARGET)
|
@Type(DEVICE_TARGET)
|
||||||
public interface Device : ContextAware, WithLifeCycle, CoroutineScope {
|
public interface Device : AutoCloseable, ContextAware, CoroutineScope {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initial configuration meta for the device
|
* Initial configuration meta for the device
|
||||||
*/
|
*/
|
||||||
public val meta: Meta get() = Meta.EMPTY
|
public val meta: Meta get() = Meta.EMPTY
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of supported property descriptors
|
* List of supported property descriptors
|
||||||
*/
|
*/
|
||||||
@ -44,6 +54,18 @@ public interface Device : ContextAware, WithLifeCycle, CoroutineScope {
|
|||||||
*/
|
*/
|
||||||
public suspend fun readProperty(propertyName: String): Meta
|
public suspend fun readProperty(propertyName: String): Meta
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the logical state of property or return null if it is invalid
|
||||||
|
*/
|
||||||
|
public fun getProperty(propertyName: String): Meta?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate property (set logical state to invalid)
|
||||||
|
*
|
||||||
|
* This message is suspended to provide lock-free local property changes (they require coroutine context).
|
||||||
|
*/
|
||||||
|
public suspend fun invalidate(propertyName: String)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set property [value] for a property with name [propertyName].
|
* Set property [value] for a property with name [propertyName].
|
||||||
* In rare cases could suspend if the [Device] supports command queue, and it is full at the moment.
|
* In rare cases could suspend if the [Device] supports command queue, and it is full at the moment.
|
||||||
@ -63,86 +85,44 @@ public interface Device : ContextAware, WithLifeCycle, CoroutineScope {
|
|||||||
public suspend fun execute(actionName: String, argument: Meta? = null): Meta?
|
public suspend fun execute(actionName: String, argument: Meta? = null): Meta?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the device. This function suspends until the device is finished initialization.
|
* Initialize the device. This function suspends until the device is finished initialization
|
||||||
* Does nothing if the device is started or is starting
|
|
||||||
*/
|
*/
|
||||||
override suspend fun start(): Unit = Unit
|
public suspend fun open(): Unit = Unit
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Close and terminate the device. This function does not wait for the device to be closed.
|
* Close and terminate the device. This function does not wait for the device to be closed.
|
||||||
*/
|
*/
|
||||||
override suspend fun stop() {
|
override fun close() {
|
||||||
coroutineContext[Job]?.cancel("The device is closed")
|
|
||||||
logger.info { "Device $this is closed" }
|
logger.info { "Device $this is closed" }
|
||||||
|
cancel("The device is closed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@DFExperimental
|
||||||
|
public val lifecycleState: DeviceLifecycleState
|
||||||
|
|
||||||
public companion object {
|
public companion object {
|
||||||
public const val DEVICE_TARGET: String = "device"
|
public const val DEVICE_TARGET: String = "device"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Inner id of a device. Not necessary corresponds to the name in the parent container
|
|
||||||
*/
|
|
||||||
public val Device.id: String get() = meta["id"].string ?: "device[${hashCode().toString(16)}]"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Device that caches properties values
|
|
||||||
*/
|
|
||||||
public interface CachingDevice : Device {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Immediately (without waiting) get the cached (logical) state of property or return null if it is invalid
|
|
||||||
*/
|
|
||||||
public fun getProperty(propertyName: String): Meta?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Invalidate property (set logical state to invalid).
|
|
||||||
*
|
|
||||||
* This message is suspended to provide lock-free local property changes (they require coroutine context).
|
|
||||||
*/
|
|
||||||
public suspend fun invalidate(propertyName: String)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the logical state of property or suspend to read the physical value.
|
* Get the logical state of property or suspend to read the physical value.
|
||||||
*/
|
*/
|
||||||
public suspend fun Device.getOrReadProperty(propertyName: String): Meta = if (this is CachingDevice) {
|
public suspend fun Device.getOrReadProperty(propertyName: String): Meta =
|
||||||
getProperty(propertyName) ?: readProperty(propertyName)
|
getProperty(propertyName) ?: readProperty(propertyName)
|
||||||
} else {
|
|
||||||
readProperty(propertyName)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a snapshot of the device logical state
|
* Get a snapshot of the device logical state
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public fun CachingDevice.getAllProperties(): Meta = Meta {
|
public fun Device.getAllProperties(): Meta = Meta {
|
||||||
for (descriptor in propertyDescriptors) {
|
for (descriptor in propertyDescriptors) {
|
||||||
set(descriptor.name.parseAsName(), getProperty(descriptor.name))
|
setMeta(Name.parse(descriptor.name), getProperty(descriptor.name))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscribe on property changes for the whole device
|
* Subscribe on property changes for the whole device
|
||||||
*/
|
*/
|
||||||
public fun Device.onPropertyChange(
|
public fun Device.onPropertyChange(callback: suspend PropertyChangedMessage.() -> Unit): Job =
|
||||||
scope: CoroutineScope = this,
|
messageFlow.filterIsInstance<PropertyChangedMessage>().onEach(callback).launchIn(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,62 +1,73 @@
|
|||||||
package space.kscience.controls.api
|
package space.kscience.controls.api
|
||||||
|
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
import space.kscience.dataforge.names.Name
|
import space.kscience.dataforge.names.*
|
||||||
import space.kscience.dataforge.provider.Path
|
|
||||||
import space.kscience.dataforge.provider.Provider
|
import space.kscience.dataforge.provider.Provider
|
||||||
import space.kscience.dataforge.provider.asPath
|
|
||||||
import space.kscience.dataforge.provider.plus
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A hub that could locate multiple devices and redirect actions to them
|
* A hub that could locate multiple devices and redirect actions to them
|
||||||
*/
|
*/
|
||||||
public interface DeviceHub : Provider {
|
public interface DeviceHub : Provider {
|
||||||
public val devices: Map<Name, Device>
|
public val devices: Map<NameToken, Device>
|
||||||
|
|
||||||
override val defaultTarget: String get() = Device.DEVICE_TARGET
|
override val defaultTarget: String get() = Device.DEVICE_TARGET
|
||||||
|
|
||||||
override val defaultChainTarget: String get() = Device.DEVICE_TARGET
|
override val defaultChainTarget: String get() = Device.DEVICE_TARGET
|
||||||
|
|
||||||
override fun content(target: String): Map<Name, Any> = if (target == Device.DEVICE_TARGET) {
|
override fun content(target: String): Map<Name, Any> = if (target == Device.DEVICE_TARGET) {
|
||||||
devices
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
emptyMap()
|
emptyMap()
|
||||||
}
|
}
|
||||||
//TODO send message on device change
|
|
||||||
|
|
||||||
public companion object
|
public companion object
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun DeviceHub(deviceMap: Map<Name, Device>): DeviceHub = object : DeviceHub {
|
public operator fun DeviceHub.get(nameToken: NameToken): Device =
|
||||||
override val devices: Map<Name, Device>
|
devices[nameToken] ?: error("Device with name $nameToken not found in $this")
|
||||||
get() = deviceMap
|
|
||||||
|
public fun DeviceHub.getOrNull(name: Name): Device? = when {
|
||||||
|
name.isEmpty() -> this as? Device
|
||||||
|
name.length == 1 -> get(name.firstOrNull()!!)
|
||||||
|
else -> (get(name.firstOrNull()!!) as? DeviceHub)?.getOrNull(name.cutFirst())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public operator fun DeviceHub.get(name: Name): Device =
|
||||||
* List all devices, including sub-devices
|
getOrNull(name) ?: error("Device with name $name not found in $this")
|
||||||
*/
|
|
||||||
public fun DeviceHub.provideAllDevices(): Map<Path, Device> = buildMap {
|
|
||||||
fun putAll(prefix: Path, hub: DeviceHub) {
|
|
||||||
hub.devices.forEach {
|
|
||||||
put(prefix + it.key.asPath(), it.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
devices.forEach {
|
public fun DeviceHub.getOrNull(nameString: String): Device? = getOrNull(Name.parse(nameString))
|
||||||
val name: Name = it.key
|
|
||||||
put(name.asPath(), it.value)
|
public operator fun DeviceHub.get(nameString: String): Device =
|
||||||
(it.value as? DeviceHub)?.let { hub ->
|
getOrNull(nameString) ?: error("Device with name $nameString not found in $this")
|
||||||
putAll(name.asPath(), hub)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public suspend fun DeviceHub.readProperty(deviceName: Name, propertyName: String): Meta =
|
public suspend fun DeviceHub.readProperty(deviceName: Name, propertyName: String): Meta =
|
||||||
(devices[deviceName] ?: error("Device with name $deviceName not found in $this")).readProperty(propertyName)
|
this[deviceName].readProperty(propertyName)
|
||||||
|
|
||||||
public suspend fun DeviceHub.writeProperty(deviceName: Name, propertyName: String, value: Meta) {
|
public suspend fun DeviceHub.writeProperty(deviceName: Name, propertyName: String, value: Meta) {
|
||||||
(devices[deviceName] ?: error("Device with name $deviceName not found in $this")).writeProperty(propertyName, value)
|
this[deviceName].writeProperty(propertyName, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
public suspend fun DeviceHub.execute(deviceName: Name, command: String, argument: Meta?): Meta? =
|
public suspend fun DeviceHub.execute(deviceName: Name, command: String, argument: Meta?): Meta? =
|
||||||
(devices[deviceName] ?: error("Device with name $deviceName not found in $this")).execute(command, argument)
|
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)
|
||||||
|
//}
|
@ -22,10 +22,10 @@ public sealed class DeviceMessage {
|
|||||||
public abstract val sourceDevice: Name?
|
public abstract val sourceDevice: Name?
|
||||||
public abstract val targetDevice: Name?
|
public abstract val targetDevice: Name?
|
||||||
public abstract val comment: String?
|
public abstract val comment: String?
|
||||||
public abstract val time: Instant
|
public abstract val time: Instant?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the source device name for composition. If the original name is null, the resulting name is also null.
|
* Update the source device name for composition. If the original name is null, resulting name is also null.
|
||||||
*/
|
*/
|
||||||
public abstract fun changeSource(block: (Name) -> Name): DeviceMessage
|
public abstract fun changeSource(block: (Name) -> Name): DeviceMessage
|
||||||
|
|
||||||
@ -59,7 +59,7 @@ public data class PropertyChangedMessage(
|
|||||||
override val sourceDevice: Name = Name.EMPTY,
|
override val sourceDevice: Name = Name.EMPTY,
|
||||||
override val targetDevice: Name? = null,
|
override val targetDevice: Name? = null,
|
||||||
override val comment: String? = null,
|
override val comment: String? = null,
|
||||||
@EncodeDefault override val time: Instant = Clock.System.now(),
|
@EncodeDefault override val time: Instant? = Clock.System.now(),
|
||||||
) : DeviceMessage() {
|
) : DeviceMessage() {
|
||||||
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
|
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
|
||||||
}
|
}
|
||||||
@ -71,11 +71,11 @@ public data class PropertyChangedMessage(
|
|||||||
@SerialName("property.set")
|
@SerialName("property.set")
|
||||||
public data class PropertySetMessage(
|
public data class PropertySetMessage(
|
||||||
public val property: String,
|
public val property: String,
|
||||||
public val value: Meta,
|
public val value: Meta?,
|
||||||
override val sourceDevice: Name? = null,
|
override val sourceDevice: Name? = null,
|
||||||
override val targetDevice: Name?,
|
override val targetDevice: Name,
|
||||||
override val comment: String? = null,
|
override val comment: String? = null,
|
||||||
@EncodeDefault override val time: Instant = Clock.System.now(),
|
@EncodeDefault override val time: Instant? = Clock.System.now(),
|
||||||
) : DeviceMessage() {
|
) : DeviceMessage() {
|
||||||
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
|
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
|
||||||
}
|
}
|
||||||
@ -91,7 +91,7 @@ public data class PropertyGetMessage(
|
|||||||
override val sourceDevice: Name? = null,
|
override val sourceDevice: Name? = null,
|
||||||
override val targetDevice: Name,
|
override val targetDevice: Name,
|
||||||
override val comment: String? = null,
|
override val comment: String? = null,
|
||||||
@EncodeDefault override val time: Instant = Clock.System.now(),
|
@EncodeDefault override val time: Instant? = Clock.System.now(),
|
||||||
) : DeviceMessage() {
|
) : DeviceMessage() {
|
||||||
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
|
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
|
||||||
}
|
}
|
||||||
@ -103,9 +103,9 @@ public data class PropertyGetMessage(
|
|||||||
@SerialName("description.get")
|
@SerialName("description.get")
|
||||||
public data class GetDescriptionMessage(
|
public data class GetDescriptionMessage(
|
||||||
override val sourceDevice: Name? = null,
|
override val sourceDevice: Name? = null,
|
||||||
override val targetDevice: Name? = null,
|
override val targetDevice: Name,
|
||||||
override val comment: String? = null,
|
override val comment: String? = null,
|
||||||
@EncodeDefault override val time: Instant = Clock.System.now(),
|
@EncodeDefault override val time: Instant? = Clock.System.now(),
|
||||||
) : DeviceMessage() {
|
) : DeviceMessage() {
|
||||||
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
|
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
|
||||||
}
|
}
|
||||||
@ -122,7 +122,7 @@ public data class DescriptionMessage(
|
|||||||
override val sourceDevice: Name,
|
override val sourceDevice: Name,
|
||||||
override val targetDevice: Name? = null,
|
override val targetDevice: Name? = null,
|
||||||
override val comment: String? = null,
|
override val comment: String? = null,
|
||||||
@EncodeDefault override val time: Instant = Clock.System.now(),
|
@EncodeDefault override val time: Instant? = Clock.System.now(),
|
||||||
) : DeviceMessage() {
|
) : DeviceMessage() {
|
||||||
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
|
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
|
||||||
}
|
}
|
||||||
@ -141,7 +141,7 @@ public data class ActionExecuteMessage(
|
|||||||
override val sourceDevice: Name? = null,
|
override val sourceDevice: Name? = null,
|
||||||
override val targetDevice: Name,
|
override val targetDevice: Name,
|
||||||
override val comment: String? = null,
|
override val comment: String? = null,
|
||||||
@EncodeDefault override val time: Instant = Clock.System.now(),
|
@EncodeDefault override val time: Instant? = Clock.System.now(),
|
||||||
) : DeviceMessage() {
|
) : DeviceMessage() {
|
||||||
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
|
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
|
||||||
}
|
}
|
||||||
@ -160,28 +160,22 @@ public data class ActionResultMessage(
|
|||||||
override val sourceDevice: Name,
|
override val sourceDevice: Name,
|
||||||
override val targetDevice: Name? = null,
|
override val targetDevice: Name? = null,
|
||||||
override val comment: String? = null,
|
override val comment: String? = null,
|
||||||
@EncodeDefault override val time: Instant = Clock.System.now(),
|
@EncodeDefault override val time: Instant? = Clock.System.now(),
|
||||||
) : DeviceMessage() {
|
) : DeviceMessage() {
|
||||||
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
|
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notifies listeners that a new binary with given [contentId] and [contentMeta] is available.
|
* Notifies listeners that a new binary with given [binaryID] is available. The binary itself could not be provided via [DeviceMessage] API.
|
||||||
*
|
|
||||||
* [contentMeta] includes public information that could be shared with loop subscribers. It should not contain sensitive data.
|
|
||||||
*
|
|
||||||
* The binary itself could not be provided via [DeviceMessage] API.
|
|
||||||
* [space.kscience.controls.peer.PeerConnection] must be used instead
|
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
@SerialName("binary.notification")
|
@SerialName("binary.notification")
|
||||||
public data class BinaryNotificationMessage(
|
public data class BinaryNotificationMessage(
|
||||||
val contentId: String,
|
val binaryID: String,
|
||||||
val contentMeta: Meta,
|
|
||||||
override val sourceDevice: Name,
|
override val sourceDevice: Name,
|
||||||
override val targetDevice: Name? = null,
|
override val targetDevice: Name? = null,
|
||||||
override val comment: String? = null,
|
override val comment: String? = null,
|
||||||
@EncodeDefault override val time: Instant = Clock.System.now(),
|
@EncodeDefault override val time: Instant? = Clock.System.now(),
|
||||||
) : DeviceMessage() {
|
) : DeviceMessage() {
|
||||||
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
|
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
|
||||||
}
|
}
|
||||||
@ -196,7 +190,7 @@ public data class EmptyDeviceMessage(
|
|||||||
override val sourceDevice: Name? = null,
|
override val sourceDevice: Name? = null,
|
||||||
override val targetDevice: Name? = null,
|
override val targetDevice: Name? = null,
|
||||||
override val comment: String? = null,
|
override val comment: String? = null,
|
||||||
@EncodeDefault override val time: Instant = Clock.System.now(),
|
@EncodeDefault override val time: Instant? = Clock.System.now(),
|
||||||
) : DeviceMessage() {
|
) : DeviceMessage() {
|
||||||
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
|
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
|
||||||
}
|
}
|
||||||
@ -209,12 +203,12 @@ public data class EmptyDeviceMessage(
|
|||||||
public data class DeviceLogMessage(
|
public data class DeviceLogMessage(
|
||||||
val message: String,
|
val message: String,
|
||||||
val data: Meta? = null,
|
val data: Meta? = null,
|
||||||
override val sourceDevice: Name = Name.EMPTY,
|
override val sourceDevice: Name? = null,
|
||||||
override val targetDevice: Name? = null,
|
override val targetDevice: Name? = null,
|
||||||
override val comment: String? = null,
|
override val comment: String? = null,
|
||||||
@EncodeDefault override val time: Instant = Clock.System.now(),
|
@EncodeDefault override val time: Instant? = Clock.System.now(),
|
||||||
) : DeviceMessage() {
|
) : DeviceMessage() {
|
||||||
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
|
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -226,25 +220,10 @@ public data class DeviceErrorMessage(
|
|||||||
public val errorMessage: String?,
|
public val errorMessage: String?,
|
||||||
public val errorType: String? = null,
|
public val errorType: String? = null,
|
||||||
public val errorStackTrace: String? = null,
|
public val errorStackTrace: String? = null,
|
||||||
override val sourceDevice: Name = Name.EMPTY,
|
override val sourceDevice: Name,
|
||||||
override val targetDevice: Name? = null,
|
override val targetDevice: Name? = null,
|
||||||
override val comment: String? = null,
|
override val comment: String? = null,
|
||||||
@EncodeDefault override val time: Instant = Clock.System.now(),
|
@EncodeDefault override val time: Instant? = Clock.System.now(),
|
||||||
) : DeviceMessage() {
|
|
||||||
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Device [Device.lifecycleState] is changed
|
|
||||||
*/
|
|
||||||
@Serializable
|
|
||||||
@SerialName("lifecycle")
|
|
||||||
public data class DeviceLifeCycleMessage(
|
|
||||||
val state: LifecycleState,
|
|
||||||
override val sourceDevice: Name = Name.EMPTY,
|
|
||||||
override val targetDevice: Name? = null,
|
|
||||||
override val comment: String? = null,
|
|
||||||
@EncodeDefault override val time: Instant = Clock.System.now(),
|
|
||||||
) : DeviceMessage() {
|
) : DeviceMessage() {
|
||||||
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
|
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
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) }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
|||||||
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,26 +12,21 @@ import space.kscience.dataforge.meta.descriptors.MetaDescriptorBuilder
|
|||||||
@Serializable
|
@Serializable
|
||||||
public class PropertyDescriptor(
|
public class PropertyDescriptor(
|
||||||
public val name: String,
|
public val name: String,
|
||||||
public var description: String? = null,
|
public var info: String? = null,
|
||||||
public var metaDescriptor: MetaDescriptor = MetaDescriptor(),
|
public var metaDescriptor: MetaDescriptor = MetaDescriptor(),
|
||||||
public var readable: Boolean = true,
|
public var readable: Boolean = true,
|
||||||
public var mutable: Boolean = false,
|
public var writable: Boolean = false
|
||||||
)
|
)
|
||||||
|
|
||||||
public fun PropertyDescriptor.metaDescriptor(block: MetaDescriptorBuilder.() -> Unit) {
|
public fun PropertyDescriptor.metaDescriptor(block: MetaDescriptorBuilder.()->Unit){
|
||||||
metaDescriptor = MetaDescriptor {
|
metaDescriptor = MetaDescriptor(block)
|
||||||
from(metaDescriptor)
|
|
||||||
block()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A descriptor for property
|
* A descriptor for property
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
public class ActionDescriptor(
|
public class ActionDescriptor(public val name: String) {
|
||||||
public val name: String,
|
public var info: String? = null
|
||||||
public var description: String? = null,
|
}
|
||||||
public var inputMetaDescriptor: MetaDescriptor = MetaDescriptor(),
|
|
||||||
public var outputMetaDescriptor: MetaDescriptor = MetaDescriptor()
|
|
||||||
)
|
|
||||||
|
@ -1,109 +0,0 @@
|
|||||||
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,13 +3,12 @@ package space.kscience.controls.manager
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import space.kscience.controls.api.Device
|
import space.kscience.controls.api.Device
|
||||||
import space.kscience.controls.api.DeviceHub
|
import space.kscience.controls.api.DeviceHub
|
||||||
import space.kscience.controls.api.id
|
import space.kscience.controls.api.getOrNull
|
||||||
import space.kscience.dataforge.context.*
|
import space.kscience.dataforge.context.*
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
import space.kscience.dataforge.meta.MutableMeta
|
import space.kscience.dataforge.meta.MutableMeta
|
||||||
import space.kscience.dataforge.names.Name
|
import space.kscience.dataforge.names.Name
|
||||||
import space.kscience.dataforge.names.get
|
import space.kscience.dataforge.names.NameToken
|
||||||
import space.kscience.dataforge.names.parseAsName
|
|
||||||
import kotlin.collections.set
|
import kotlin.collections.set
|
||||||
import kotlin.properties.ReadOnlyProperty
|
import kotlin.properties.ReadOnlyProperty
|
||||||
|
|
||||||
@ -22,11 +21,11 @@ public class DeviceManager : AbstractPlugin(), DeviceHub {
|
|||||||
/**
|
/**
|
||||||
* Actual list of connected devices
|
* Actual list of connected devices
|
||||||
*/
|
*/
|
||||||
private val _devices = HashMap<Name, Device>()
|
private val top = HashMap<NameToken, Device>()
|
||||||
override val devices: Map<Name, Device> get() = _devices
|
override val devices: Map<NameToken, Device> get() = top
|
||||||
|
|
||||||
public fun registerDevice(name: Name, device: Device) {
|
public fun registerDevice(name: NameToken, device: Device) {
|
||||||
_devices[name] = device
|
top[name] = device
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun content(target: String): Map<Name, Any> = super<DeviceHub>.content(target)
|
override fun content(target: String): Map<Name, Any> = super<DeviceHub>.content(target)
|
||||||
@ -39,19 +38,13 @@ public class DeviceManager : AbstractPlugin(), DeviceHub {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public fun <D : Device> DeviceManager.install(name: String, device: D): D {
|
public fun <D : Device> DeviceManager.install(name: String, device: D): D {
|
||||||
registerDevice(name.parseAsName(), device)
|
registerDevice(NameToken(name), device)
|
||||||
device.launch {
|
device.launch {
|
||||||
device.start()
|
device.open()
|
||||||
}
|
}
|
||||||
return device
|
return device
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun <D : Device> DeviceManager.install(device: D): D = install(device.id, device)
|
|
||||||
|
|
||||||
|
|
||||||
public fun <D : Device> Context.install(name: String, device: D): D = request(DeviceManager).install(name, device)
|
|
||||||
|
|
||||||
public fun <D : Device> Context.install(device: D): D = request(DeviceManager).install(device.id, device)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register and start a device built by [factory] with current [Context] and [meta].
|
* Register and start a device built by [factory] with current [Context] and [meta].
|
||||||
@ -69,7 +62,7 @@ public inline fun <D : Device> DeviceManager.installing(
|
|||||||
val meta = Meta(builder)
|
val meta = Meta(builder)
|
||||||
return ReadOnlyProperty { _, property ->
|
return ReadOnlyProperty { _, property ->
|
||||||
val name = property.name
|
val name = property.name
|
||||||
val current = devices[name]
|
val current = getOrNull(name)
|
||||||
if (current == null) {
|
if (current == null) {
|
||||||
install(name, factory, meta)
|
install(name, factory, meta)
|
||||||
} else if (current.meta != meta) {
|
} else if (current.meta != meta) {
|
||||||
@ -80,3 +73,4 @@ 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