Update constructor

This commit is contained in:
Alexander Nozik 2023-10-28 14:18:00 +03:00
parent 4f028ccee8
commit 1fcdbdc9f4
14 changed files with 196 additions and 176 deletions

View File

@ -3,6 +3,8 @@
## Unreleased
### Added
- Device lifecycle message
- Low-code constructor
### Changed

View File

@ -4,7 +4,7 @@ plugins {
}
description = """
A low-code constructor foe composite devices simulation
A low-code constructor for composite devices simulation
""".trimIndent()
kscience{

View File

@ -0,0 +1,25 @@
package space.kscience.controls.constructor
import kotlinx.coroutines.flow.Flow
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.transformations.MetaConverter
/**
* An observable state of a device
*/
public interface DeviceState<T> {
public val converter: MetaConverter<T>
public val value: T
public val valueFlow: Flow<T>
public val metaFlow: Flow<Meta>
}
/**
* A mutable state of a device
*/
public interface MutableDeviceState<T> : DeviceState<T>{
override var value: T
}

View File

@ -1,4 +1,4 @@
package space.kscience.controls.spec
package space.kscience.controls.constructor
import space.kscience.controls.api.Device
import space.kscience.controls.api.DeviceHub
@ -7,11 +7,8 @@ import space.kscience.dataforge.context.Factory
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.names.NameToken
import kotlin.collections.Map
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.collections.mapValues
import kotlin.collections.mutableMapOf
import kotlin.collections.set

View File

@ -0,0 +1,91 @@
package space.kscience.controls.constructor
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.datetime.Clock
import space.kscience.controls.api.Device
import space.kscience.controls.spec.DeviceBySpec
import space.kscience.controls.spec.DevicePropertySpec
import space.kscience.controls.spec.DeviceSpec
import space.kscience.controls.spec.doubleProperty
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.meta.double
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.transformations.MetaConverter
import kotlin.math.pow
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.DurationUnit
/**
* A classic drive regulated by force with encoder
*/
public interface Drive : Device {
/**
* Get or set drive force or momentum
*/
public var force: Double
/**
* Current position value
*/
public val position: Double
public companion object : DeviceSpec<Drive>() {
public val force: DevicePropertySpec<Drive, Double> by Drive.property(
MetaConverter.double,
Drive::force
)
public val position: DevicePropertySpec<Drive, Double> by doubleProperty { position }
}
}
/**
* A virtual drive
*/
public class VirtualDrive(
context: Context,
private val mass: Double,
position: Double,
) : Drive, DeviceBySpec<Drive>(Drive, context) {
private val dt = meta["time.step"].double?.milliseconds ?: 5.milliseconds
private val clock = Clock.System
override var force: Double = 0.0
override var position: Double = position
private set
public var velocity: Double = 0.0
private set
private var updateJob: Job? = null
override suspend fun onStart() {
updateJob = launch {
var lastTime = clock.now()
while (isActive) {
delay(dt)
val realTime = clock.now()
val dtSeconds = (realTime - lastTime).toDouble(DurationUnit.SECONDS)
//set last time and value to new values
lastTime = realTime
// compute new value based on velocity and acceleration from the previous step
position += velocity * dtSeconds + force/mass * dtSeconds.pow(2) / 2
// compute new velocity based on acceleration on the previous step
velocity += force/mass * dtSeconds
}
}
}
override fun onStop() {
updateJob?.cancel()
}
}

View File

@ -1,4 +1,4 @@
package center.sciprog.controls.devices.misc
package space.kscience.controls.constructor
import space.kscience.controls.api.Device
import space.kscience.controls.spec.DeviceBySpec

View File

@ -1,4 +1,4 @@
package center.sciprog.controls.devices.misc
package space.kscience.controls.constructor
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
@ -8,28 +8,27 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import space.kscience.controls.api.DeviceLifecycleState
import space.kscience.controls.spec.DeviceBySpec
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.DurationUnit
/**
* PID controller on top of a [Regulator]
* A drive with PID regulator
*/
public class PidRegulator(
public val regulator: Regulator,
public val drive: Drive,
public val kp: Double,
public val ki: Double,
public val kd: Double,
private val dt: Duration = 0.5.milliseconds,
private val dt: Duration = 1.milliseconds,
private val clock: Clock = Clock.System,
) : DeviceBySpec<Regulator>(Regulator, regulator.context), Regulator {
) : DeviceBySpec<Regulator>(Regulator, drive.context), Regulator {
override var target: Double = regulator.target
override var target: Double = drive.position
private var lastTime: Instant = clock.now()
private var lastRegulatorTarget: Double = target
private var lastPosition: Double = target
private var integral: Double = 0.0
@ -39,10 +38,7 @@ public class PidRegulator(
override suspend fun onStart() {
if(regulator.lifecycleState == DeviceLifecycleState.STOPPED){
regulator.start()
}
regulator.start()
drive.start()
updateJob = launch {
while (isActive) {
delay(dt)
@ -51,13 +47,13 @@ public class PidRegulator(
val delta = target - position
val dtSeconds = (realTime - lastTime).toDouble(DurationUnit.SECONDS)
integral += delta * dtSeconds
val derivative = (regulator.target - lastRegulatorTarget) / dtSeconds
val derivative = (drive.position - lastPosition) / dtSeconds
//set last time and value to new values
lastTime = realTime
lastRegulatorTarget = regulator.target
lastPosition = drive.position
regulator.target = regulator.position + kp * delta + ki * integral + kd * derivative
drive.force = kp * delta + ki * integral + kd * derivative
}
}
}
@ -67,129 +63,6 @@ public class PidRegulator(
updateJob?.cancel()
}
override val position: Double get() = regulator.position
override val position: Double get() = drive.position
}
//
//interface PidRegulator : Device {
// /**
// * Proportional coefficient
// */
// val kp: Double
//
// /**
// * Integral coefficient
// */
// val ki: Double
//
// /**
// * Differential coefficient
// */
// val kd: Double
//
// /**
// * The target value for PID
// */
// var target: Double
//
// /**
// * Read current value
// */
// suspend fun read(): Double
//
// companion object : DeviceSpec<PidRegulator>() {
// val target by property(MetaConverter.double, PidRegulator::target)
// val value by doubleProperty { read() }
// }
//}
//
///**
// *
// */
//class VirtualPid(
// context: Context,
// override val kp: Double,
// override val ki: Double,
// override val kd: Double,
// val mass: Double,
// override var target: Double = 0.0,
// private val dt: Duration = 0.5.milliseconds,
// private val clock: Clock = Clock.System,
//) : DeviceBySpec<PidRegulator>(PidRegulator, context), PidRegulator {
//
// private val mutex = Mutex()
//
//
// private var lastTime: Instant = clock.now()
// private var lastValue: Double = target
//
// private var value: Double = target
// private var velocity: Double = 0.0
// private var acceleration: Double = 0.0
// private var integral: Double = 0.0
//
//
// private var updateJob: Job? = null
//
// override suspend fun onStart() {
// updateJob = launch {
// while (isActive) {
// delay(dt)
// mutex.withLock {
// val realTime = clock.now()
// val delta = target - value
// val dtSeconds = (realTime - lastTime).toDouble(DurationUnit.SECONDS)
// integral += delta * dtSeconds
// val derivative = (value - lastValue) / dtSeconds
//
// //set last time and value to new values
// lastTime = realTime
// lastValue = value
//
// // compute new value based on velocity and acceleration from the previous step
// value += velocity * dtSeconds + acceleration * dtSeconds.pow(2) / 2
//
// // compute new velocity based on acceleration on the previous step
// velocity += acceleration * dtSeconds
//
// //compute force for the next step based on current values
// acceleration = (kp * delta + ki * integral + kd * derivative) / mass
//
//
// check(value.isFinite() && velocity.isFinite()) {
// "Value $value is not finite"
// }
// }
// }
// }
// }
//
// override fun onStop() {
// updateJob?.cancel()
// super<PidRegulator>.stop()
// }
//
// override suspend fun read(): Double = value
//
// suspend fun readVelocity(): Double = velocity
//
// suspend fun readAcceleration(): Double = acceleration
//
// suspend fun write(newTarget: Double) = mutex.withLock {
// require(newTarget.isFinite()) { "Value $newTarget is not valid" }
// target = newTarget
// }
//
// companion object : Factory<Device> {
// override fun build(context: Context, meta: Meta) = VirtualPid(
// context,
// meta["kp"].double ?: error("Kp is not defined"),
// meta["ki"].double ?: error("Ki is not defined"),
// meta["kd"].double ?: error("Kd is not defined"),
// meta["m"].double ?: error("Mass is not defined"),
// )
//
// }
//}
}

View File

@ -1,17 +1,14 @@
package center.sciprog.controls.devices.misc
package space.kscience.controls.constructor
import kotlinx.coroutines.Job
import space.kscience.controls.api.Device
import space.kscience.controls.spec.DeviceBySpec
import space.kscience.controls.spec.DevicePropertySpec
import space.kscience.controls.spec.DeviceSpec
import space.kscience.controls.spec.doubleProperty
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.meta.transformations.MetaConverter
/**
* A single axis drive
* A regulator with target value and current position
*/
public interface Regulator : Device {
/**
@ -29,23 +26,4 @@ public interface Regulator : Device {
public val position: DevicePropertySpec<Regulator, Double> by doubleProperty { position }
}
}
/**
* Virtual [Regulator] with speed limit
*/
public class VirtualRegulator(
context: Context,
value: Double,
private val speed: Double,
) : DeviceBySpec<Regulator>(Regulator, context), Regulator {
private var moveJob: Job? = null
override var position: Double = value
private set
override var target: Double = value
}

View File

@ -7,6 +7,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.serialization.Serializable
import space.kscience.controls.api.Device.Companion.DEVICE_TARGET
import space.kscience.dataforge.context.ContextAware
import space.kscience.dataforge.context.info
@ -19,6 +20,7 @@ import space.kscience.dataforge.names.Name
/**
* A lifecycle state of a device
*/
@Serializable
public enum class DeviceLifecycleState {
/**
@ -34,7 +36,12 @@ public enum class DeviceLifecycleState {
/**
* The Device is closed
*/
STOPPED
STOPPED,
/**
* The device encountered irrecoverable error
*/
ERROR
}
/**
@ -98,7 +105,8 @@ public interface Device : ContextAware, CoroutineScope {
public suspend fun execute(actionName: String, argument: Meta? = null): Meta?
/**
* Initialize the device. This function suspends until the device is finished initialization
* Initialize the device. This function suspends until the device is finished initialization.
* Does nothing if the device is started or is starting
*/
public suspend fun start(): Unit = Unit

View File

@ -1,4 +1,4 @@
@file:OptIn(ExperimentalSerializationApi::class)
@file:OptIn(ExperimentalSerializationApi::class, ExperimentalSerializationApi::class)
package space.kscience.controls.api
@ -228,6 +228,21 @@ public data class DeviceErrorMessage(
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: DeviceLifecycleState,
override val sourceDevice: Name = Name.EMPTY,
override val targetDevice: Name? = null,
override val comment: String? = null,
@EncodeDefault override val time: Instant? = Clock.System.now(),
) : DeviceMessage() {
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
}
public fun DeviceMessage.toMeta(): Meta = Json.encodeToJsonElement(this).toMeta()

View File

@ -64,6 +64,7 @@ public suspend fun Device.respondMessage(deviceTarget: Name, request: DeviceMess
is DeviceErrorMessage,
is EmptyDeviceMessage,
is DeviceLogMessage,
is DeviceLifeCycleMessage,
-> null
}
} catch (ex: Exception) {
@ -87,7 +88,7 @@ public suspend fun DeviceHub.respondHubMessage(request: DeviceMessage): DeviceMe
* Collect all messages from given [DeviceHub], applying proper relative names.
*/
public fun DeviceHub.hubMessageFlow(scope: CoroutineScope): Flow<DeviceMessage> {
//TODO could we avoid using downstream scope?
val outbox = MutableSharedFlow<DeviceMessage>()
if (this is Device) {

View File

@ -190,7 +190,16 @@ public abstract class DeviceBase<D : Device>(
@DFExperimental
override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED
protected set
protected set(value) {
if (field != value) {
launch {
sharedMessageFlow.emit(
DeviceLifeCycleMessage(value)
)
}
}
field = value
}
protected open suspend fun onStart() {
@ -198,7 +207,7 @@ public abstract class DeviceBase<D : Device>(
@OptIn(DFExperimental::class)
final override suspend fun start() {
if(lifecycleState == DeviceLifecycleState.STOPPED) {
if (lifecycleState == DeviceLifecycleState.STOPPED) {
super.start()
lifecycleState = DeviceLifecycleState.STARTING
onStart()

View File

@ -0,0 +1,20 @@
plugins {
id("space.kscience.gradle.mpp")
`maven-publish`
}
description = """
Dashboard and visualization extensions for devices
""".trimIndent()
kscience{
jvm()
js()
dependencies {
api(projects.controlsCore)
}
}
readme{
maturity = space.kscience.gradle.Maturity.PROTOTYPE
}

View File

@ -51,6 +51,7 @@ include(
":controls-storage",
":controls-storage:controls-xodus",
":controls-constructor",
":controls-vision",
":magix",
":magix:magix-api",
":magix:magix-server",