Compare commits

..

No commits in common. "dev-maxim" and "master" have entirely different histories.

304 changed files with 2385 additions and 14240 deletions

4
.gitignore vendored
View File

@ -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
View 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
View File

@ -0,0 +1 @@
./space/* "Project Admin"

View File

@ -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

View File

@ -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
> >

View File

@ -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")

View File

@ -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")
}
```

View File

@ -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
}

View File

@ -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)
}
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)"
}

View File

@ -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)
}
}

View File

@ -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)"
}

View File

@ -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)

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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
}
}
)

View File

@ -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)
}

View File

@ -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
}

View File

@ -1,9 +0,0 @@
package space.kscience.controls.constructor.dsl.core
/**
* Класс для представления аннотации.
*/
public data class Annotation(
val name: String,
val properties: Map<String, Any>
)

View File

@ -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>
)

View File

@ -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)
}
}

View File

@ -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())
}
}

View File

@ -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)
}
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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

View File

@ -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
}
}

View File

@ -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()
}
}

View File

@ -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()

View File

@ -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
}
}

View File

@ -1,6 +0,0 @@
package space.kscience.controls.constructor.dsl.core.equations
/**
* Абстрактный базовый класс для уравнений.
*/
public sealed class EquationBase

View File

@ -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()

View File

@ -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)
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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())
}

View File

@ -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.")
}
}

View File

@ -1,11 +0,0 @@
package space.kscience.controls.constructor.dsl.core.expressions
/**
* Перечисление для бинарных операций над массивами.
*/
public enum class ArrayBinaryOperator {
ADD,
SUBTRACT,
MULTIPLY,
DIVIDE
}

View File

@ -1,3 +0,0 @@
package space.kscience.controls.constructor.dsl.core.expressions
public sealed class ArrayExpression : Expression()

View File

@ -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.")
}
}

View File

@ -1,9 +0,0 @@
package space.kscience.controls.constructor.dsl.core.expressions
/**
* Перечисление операторов между массивом и скаляром.
*/
public enum class ArrayScalarOperator {
MULTIPLY,
DIVIDE
}

View File

@ -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.")
}
}

View File

@ -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.")
}
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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.")
}
}

View File

@ -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.")
}
}

View File

@ -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.")
}
}

View File

@ -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)
}
}

View File

@ -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
)

View File

@ -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)
}
}

View File

@ -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>>
)

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -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
)

View File

@ -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)
}
}

View File

@ -1,10 +0,0 @@
package space.kscience.controls.constructor.dsl.core.variables
/**
* Перечисление каузальности переменной.
*/
public enum class Causality {
INPUT,
OUTPUT,
INTERNAL
}

View File

@ -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)

View File

@ -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.")
}
}
}

View File

@ -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)

View File

@ -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.")
}
}
}

View File

@ -1,11 +0,0 @@
package space.kscience.controls.constructor.dsl.core.variables
/**
* Перечисление типов изменчивости переменной.
*/
public enum class Variability {
CONSTANT,
PARAMETER,
DISCRETE,
CONTINUOUS
}

View File

@ -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() {}
}

View File

@ -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)

View File

@ -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)

View File

@ -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)"
}

View File

@ -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
)
}
}

View File

@ -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 )
}
}

View File

@ -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
}
}

View File

@ -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)
}
}
}
}

View File

@ -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)

View File

@ -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
}
}
}

View File

@ -1,6 +0,0 @@
package space.kscience.controls.constructor.units
public enum class Direction(public val coef: Int) {
UP(1),
DOWN(-1)
}

View File

@ -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>>

View File

@ -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

View File

@ -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)

View File

@ -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")
}
}

View File

@ -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"))
}
}

View File

@ -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"))
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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()
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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")
} }
``` ```

View File

@ -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)
}
} }

View File

@ -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) }
}

View File

@ -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)

View File

@ -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)
//}

View File

@ -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))
} }

View File

@ -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) }
}

View File

@ -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()
}
}
}

View File

@ -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()
)

View File

@ -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
}
}

View File

@ -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