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 ## Unreleased
### Added ### Added
- Device lifecycle message
- Low-code constructor
### Changed ### Changed

View File

@ -4,7 +4,7 @@ plugins {
} }
description = """ description = """
A low-code constructor foe composite devices simulation A low-code constructor for composite devices simulation
""".trimIndent() """.trimIndent()
kscience{ 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.Device
import space.kscience.controls.api.DeviceHub 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.Meta
import space.kscience.dataforge.meta.get import space.kscience.dataforge.meta.get
import space.kscience.dataforge.names.NameToken import space.kscience.dataforge.names.NameToken
import kotlin.collections.Map
import kotlin.collections.component1 import kotlin.collections.component1
import kotlin.collections.component2 import kotlin.collections.component2
import kotlin.collections.mapValues
import kotlin.collections.mutableMapOf
import kotlin.collections.set 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.api.Device
import space.kscience.controls.spec.DeviceBySpec 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.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -8,28 +8,27 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import space.kscience.controls.api.DeviceLifecycleState
import space.kscience.controls.spec.DeviceBySpec import space.kscience.controls.spec.DeviceBySpec
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.DurationUnit import kotlin.time.DurationUnit
/** /**
* PID controller on top of a [Regulator] * A drive with PID regulator
*/ */
public class PidRegulator( public class PidRegulator(
public val regulator: Regulator, public val drive: Drive,
public val kp: Double, public val kp: Double,
public val ki: Double, public val ki: Double,
public val kd: Double, public val kd: Double,
private val dt: Duration = 0.5.milliseconds, private val dt: Duration = 1.milliseconds,
private val clock: Clock = Clock.System, 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 lastTime: Instant = clock.now()
private var lastRegulatorTarget: Double = target private var lastPosition: Double = target
private var integral: Double = 0.0 private var integral: Double = 0.0
@ -39,10 +38,7 @@ public class PidRegulator(
override suspend fun onStart() { override suspend fun onStart() {
if(regulator.lifecycleState == DeviceLifecycleState.STOPPED){ drive.start()
regulator.start()
}
regulator.start()
updateJob = launch { updateJob = launch {
while (isActive) { while (isActive) {
delay(dt) delay(dt)
@ -51,13 +47,13 @@ public class PidRegulator(
val delta = target - position val delta = target - position
val dtSeconds = (realTime - lastTime).toDouble(DurationUnit.SECONDS) val dtSeconds = (realTime - lastTime).toDouble(DurationUnit.SECONDS)
integral += delta * dtSeconds integral += delta * dtSeconds
val derivative = (regulator.target - lastRegulatorTarget) / dtSeconds val derivative = (drive.position - lastPosition) / dtSeconds
//set last time and value to new values //set last time and value to new values
lastTime = realTime 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() 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.api.Device
import space.kscience.controls.spec.DeviceBySpec
import space.kscience.controls.spec.DevicePropertySpec import space.kscience.controls.spec.DevicePropertySpec
import space.kscience.controls.spec.DeviceSpec import space.kscience.controls.spec.DeviceSpec
import space.kscience.controls.spec.doubleProperty import space.kscience.controls.spec.doubleProperty
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.meta.transformations.MetaConverter import space.kscience.dataforge.meta.transformations.MetaConverter
/** /**
* A single axis drive * A regulator with target value and current position
*/ */
public interface Regulator : Device { public interface Regulator : Device {
/** /**
@ -30,22 +27,3 @@ public interface Regulator : Device {
public val position: DevicePropertySpec<Regulator, Double> by doubleProperty { position } 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.filterIsInstance
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.serialization.Serializable
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
@ -19,6 +20,7 @@ import space.kscience.dataforge.names.Name
/** /**
* A lifecycle state of a device * A lifecycle state of a device
*/ */
@Serializable
public enum class DeviceLifecycleState { public enum class DeviceLifecycleState {
/** /**
@ -34,7 +36,12 @@ public enum class DeviceLifecycleState {
/** /**
* The Device is closed * 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? 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 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 package space.kscience.controls.api
@ -228,6 +228,21 @@ public data class DeviceErrorMessage(
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice)) 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() 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 DeviceErrorMessage,
is EmptyDeviceMessage, is EmptyDeviceMessage,
is DeviceLogMessage, is DeviceLogMessage,
is DeviceLifeCycleMessage,
-> null -> null
} }
} catch (ex: Exception) { } catch (ex: Exception) {

View File

@ -190,7 +190,16 @@ public abstract class DeviceBase<D : Device>(
@DFExperimental @DFExperimental
override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED 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() { protected open suspend fun onStart() {
@ -198,7 +207,7 @@ public abstract class DeviceBase<D : Device>(
@OptIn(DFExperimental::class) @OptIn(DFExperimental::class)
final override suspend fun start() { final override suspend fun start() {
if(lifecycleState == DeviceLifecycleState.STOPPED) { if (lifecycleState == DeviceLifecycleState.STOPPED) {
super.start() super.start()
lifecycleState = DeviceLifecycleState.STARTING lifecycleState = DeviceLifecycleState.STARTING
onStart() 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-storage:controls-xodus", ":controls-storage:controls-xodus",
":controls-constructor", ":controls-constructor",
":controls-vision",
":magix", ":magix",
":magix:magix-api", ":magix:magix-api",
":magix:magix-server", ":magix:magix-server",