Add fixed age plots for properties and states.

This commit is contained in:
Alexander Nozik 2023-11-06 11:39:56 +03:00
parent 78b18ebda6
commit 0443fdc3c0
6 changed files with 222 additions and 43 deletions

View File

@ -0,0 +1,69 @@
package space.kscience.controls.misc
import io.ktor.utils.io.core.Input
import io.ktor.utils.io.core.Output
import kotlinx.datetime.Instant
import space.kscience.dataforge.io.IOFormat
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.transformations.MetaConverter
import kotlin.reflect.KType
import kotlin.reflect.typeOf
/**
* A value coupled to a time it was obtained at
*/
public data class ValueWithTime<T>(val value: T, val time: Instant) {
public companion object {
/**
* Create a [ValueWithTime] format for given value value [IOFormat]
*/
public fun <T> ioFormat(
valueFormat: IOFormat<T>,
): IOFormat<ValueWithTime<T>> = ValueWithTimeIOFormat(valueFormat)
/**
* Create a [MetaConverter] with time for given value [MetaConverter]
*/
public fun <T> metaConverter(
valueConverter: MetaConverter<T>,
): MetaConverter<ValueWithTime<T>> = ValueWithTimeMetaConverter(valueConverter)
public const val META_TIME_KEY: String = "time"
public const val META_VALUE_KEY: String = "value"
}
}
private class ValueWithTimeIOFormat<T>(val valueFormat: IOFormat<T>) : IOFormat<ValueWithTime<T>> {
override val type: KType get() = typeOf<ValueWithTime<T>>()
override fun readObject(input: Input): ValueWithTime<T> {
val timestamp = InstantIOFormat.readObject(input)
val value = valueFormat.readObject(input)
return ValueWithTime(value, timestamp)
}
override fun writeObject(output: Output, obj: ValueWithTime<T>) {
InstantIOFormat.writeObject(output, obj.time)
valueFormat.writeObject(output, obj.value)
}
}
private class ValueWithTimeMetaConverter<T>(
val valueConverter: MetaConverter<T>,
) : MetaConverter<ValueWithTime<T>> {
override fun metaToObject(
meta: Meta,
): ValueWithTime<T>? = valueConverter.metaToObject(meta[ValueWithTime.META_VALUE_KEY] ?: Meta.EMPTY)?.let {
ValueWithTime(it, meta[ValueWithTime.META_TIME_KEY]?.instant ?: Instant.DISTANT_PAST)
}
override fun objectToMeta(obj: ValueWithTime<T>): Meta = Meta {
ValueWithTime.META_TIME_KEY put obj.time.toMeta()
ValueWithTime.META_VALUE_KEY put valueConverter.objectToMeta(obj.value)
}
}
public fun <T : Any> MetaConverter<T>.withTime(): MetaConverter<ValueWithTime<T>> = ValueWithTimeMetaConverter(this)

View File

@ -0,0 +1,40 @@
package space.kscience.controls.misc
import io.ktor.utils.io.core.*
import kotlinx.datetime.Instant
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.io.IOFormat
import space.kscience.dataforge.io.IOFormatFactory
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.string
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.asName
import kotlin.reflect.KType
import kotlin.reflect.typeOf
/**
* An [IOFormat] for [Instant]
*/
public object InstantIOFormat : IOFormat<Instant>, IOFormatFactory<Instant> {
override fun build(context: Context, meta: Meta): IOFormat<Instant> = this
override val name: Name = "instant".asName()
override val type: KType get() = typeOf<Instant>()
override fun writeObject(output: Output, obj: Instant) {
output.writeLong(obj.epochSeconds)
output.writeInt(obj.nanosecondsOfSecond)
}
override fun readObject(input: Input): Instant {
val seconds = input.readLong()
val nanoseconds = input.readInt()
return Instant.fromEpochSeconds(seconds, nanoseconds)
}
}
public fun Instant.toMeta(): Meta = Meta(toString())
public val Meta.instant: Instant? get() = value?.string?.let { Instant.parse(it) }

View File

@ -1,18 +0,0 @@
package space.kscience.controls.misc
import kotlinx.datetime.Instant
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.long
// TODO move to core
public fun Instant.toMeta(): Meta = Meta {
"seconds" put epochSeconds
"nanos" put nanosecondsOfSecond
}
public fun Meta.instant(): Instant = value?.long?.let { Instant.fromEpochMilliseconds(it) } ?: Instant.fromEpochSeconds(
get("seconds")?.long ?: 0L,
get("nanos")?.long ?: 0L,
)

View File

@ -147,7 +147,7 @@ internal class MetaStructureCodec(
"Float" -> member.value?.numberOrNull?.toFloat() "Float" -> member.value?.numberOrNull?.toFloat()
"Double" -> member.value?.numberOrNull?.toDouble() "Double" -> member.value?.numberOrNull?.toDouble()
"String" -> member.string "String" -> member.string
"DateTime" -> DateTime(member.instant().toJavaInstant()) "DateTime" -> DateTime(member.instant.toJavaInstant())
"Guid" -> member.string?.let { UUID.fromString(it) } "Guid" -> member.string?.let { UUID.fromString(it) }
"ByteString" -> member.value?.list?.let { list -> "ByteString" -> member.value?.list?.let { list ->
ByteString(list.map { it.number.toByte() }.toByteArray()) ByteString(list.map { it.number.toByte() }.toByteArray())

View File

@ -4,18 +4,27 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.transform
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import space.kscience.controls.api.Device import space.kscience.controls.api.Device
import space.kscience.controls.api.propertyMessageFlow import space.kscience.controls.api.propertyMessageFlow
import space.kscience.controls.constructor.DeviceState import space.kscience.controls.constructor.DeviceState
import space.kscience.controls.manager.clock import space.kscience.controls.manager.clock
import space.kscience.controls.misc.ValueWithTime
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
import space.kscience.dataforge.meta.* import space.kscience.dataforge.meta.*
import space.kscience.plotly.Plot import space.kscience.plotly.Plot
import space.kscience.plotly.bar import space.kscience.plotly.bar
import space.kscience.plotly.models.Bar import space.kscience.plotly.models.Bar
import space.kscience.plotly.models.Scatter import space.kscience.plotly.models.Scatter
import space.kscience.plotly.models.Trace
import space.kscience.plotly.models.TraceValues import space.kscience.plotly.models.TraceValues
import space.kscience.plotly.scatter import space.kscience.plotly.scatter
import kotlin.time.Duration
import kotlin.time.Duration.Companion.hours
private var TraceValues.values: List<Value> private var TraceValues.values: List<Value>
get() = value?.list ?: emptyList() get() = value?.list ?: emptyList()
@ -23,48 +32,126 @@ private var TraceValues.values: List<Value>
value = ListValue(newValues) value = ListValue(newValues)
} }
private var TraceValues.times: List<Instant>
get() = value?.list?.map { Instant.parse(it.string) } ?: emptyList()
set(newValues) {
value = ListValue(newValues.map { it.toString().asValue() })
}
private class TimeData(private var points: MutableList<ValueWithTime<Value>> = mutableListOf()) {
private val mutex = Mutex()
suspend fun append(time: Instant, value: Value) = mutex.withLock {
points.add(ValueWithTime(value, time))
}
suspend fun trim(maxAge: Duration, maxPoints: Int = 800, minPoints: Int = 400) {
require(maxPoints > 2)
require(minPoints > 0)
require(maxPoints > minPoints)
val now = Clock.System.now()
// filter old points
points.removeAll { now - it.time > maxAge }
if (points.size > maxPoints) {
val durationBetweenPoints = maxAge / minPoints
val markedForRemoval = buildList<ValueWithTime<Value>> {
var lastTime: Instant? = null
points.forEach { point ->
if (lastTime?.let { point.time - it < durationBetweenPoints } == true) {
add(point)
} else {
lastTime = point.time
}
}
}
points.removeAll(markedForRemoval)
}
}
suspend fun fillPlot(x: TraceValues, y: TraceValues) = mutex.withLock {
x.strings = points.map { it.time.toString() }
y.values = points.map { it.value }
}
}
/** /**
* Add a trace that shows a [Device] property change over time. Show only latest [pointsNumber] . * Add a trace that shows a [Device] property change over time. Show only latest [maxPoints] .
* @return a [Job] that handles the listener * @return a [Job] that handles the listener
*/ */
public fun Plot.plotDeviceProperty( public fun Plot.plotDeviceProperty(
device: Device, device: Device,
propertyName: String, propertyName: String,
extractValue: Meta.() -> Value = { value ?: Null }, extractValue: Meta.() -> Value = { value ?: Null },
pointsNumber: Int = 400, maxAge: Duration = 1.hours,
maxPoints: Int = 800,
minPoints: Int = 400,
coroutineScope: CoroutineScope = device.context, coroutineScope: CoroutineScope = device.context,
configuration: Scatter.() -> Unit = {}, configuration: Scatter.() -> Unit = {},
): Job = scatter(configuration).run { ): Job = scatter(configuration).run {
val clock = device.context.clock val clock = device.context.clock
device.propertyMessageFlow(propertyName).onEach { message -> val data = TimeData()
x.strings = (x.strings + (message.time ?: clock.now()).toString()).takeLast(pointsNumber) device.propertyMessageFlow(propertyName).transform {
y.values = (y.values + message.value.extractValue()).takeLast(pointsNumber) data.append(it.time ?: clock.now(), it.value.extractValue())
data.trim(maxAge, maxPoints, minPoints)
emit(data)
}.onEach {
it.fillPlot(x, y)
}.launchIn(coroutineScope) }.launchIn(coroutineScope)
} }
private fun <T> Trace.updateFromState(
context: Context,
state: DeviceState<T>,
extractValue: T.() -> Value = { state.converter.objectToMeta(this).value ?: space.kscience.dataforge.meta.Null },
maxAge: Duration = 1.hours,
maxPoints: Int = 800,
minPoints: Int = 400,
): Job{
val clock = context.clock
val data = TimeData()
return state.valueFlow.transform<T, TimeData> {
data.append(clock.now(), it.extractValue())
data.trim(maxAge, maxPoints, minPoints)
}.onEach {
it.fillPlot(x, y)
}.launchIn(context)
}
public fun <T> Plot.plotDeviceState(
context: Context,
state: DeviceState<T>,
extractValue: T.() -> Value = { state.converter.objectToMeta(this).value ?: Null },
maxAge: Duration = 1.hours,
maxPoints: Int = 800,
minPoints: Int = 400,
configuration: Scatter.() -> Unit = {},
): Job = scatter(configuration).run {
updateFromState(context, state, extractValue, maxAge, maxPoints, minPoints)
}
public fun Plot.plotNumberState( public fun Plot.plotNumberState(
context: Context, context: Context,
state: DeviceState<out Number>, state: DeviceState<out Number>,
pointsNumber: Int = 400, maxAge: Duration = 1.hours,
maxPoints: Int = 800,
minPoints: Int = 400,
configuration: Scatter.() -> Unit = {}, configuration: Scatter.() -> Unit = {},
): Job = scatter(configuration).run { ): Job = scatter(configuration).run {
val clock = context.clock updateFromState(context, state, { asValue() }, maxAge, maxPoints, minPoints)
state.valueFlow.onEach {
x.strings = (x.strings + clock.now().toString()).takeLast(pointsNumber)
y.numbers = (y.numbers + it).takeLast(pointsNumber)
}.launchIn(context)
} }
public fun Plot.plotBooleanState( public fun Plot.plotBooleanState(
context: Context, context: Context,
state: DeviceState<Boolean>, state: DeviceState<Boolean>,
pointsNumber: Int = 400, maxAge: Duration = 1.hours,
maxPoints: Int = 800,
minPoints: Int = 400,
configuration: Bar.() -> Unit = {}, configuration: Bar.() -> Unit = {},
): Job = bar(configuration).run { ): Job = bar(configuration).run {
val clock = context.clock updateFromState(context, state, { asValue() }, maxAge, maxPoints, minPoints)
state.valueFlow.onEach {
x.strings = (x.strings + clock.now().toString()).takeLast(pointsNumber)
y.values = (y.values + it.asValue()).takeLast(pointsNumber)
}.launchIn(context)
} }

View File

@ -51,32 +51,33 @@ public fun main() {
val t = timeFromStart.toDouble(DurationUnit.SECONDS) val t = timeFromStart.toDouble(DurationUnit.SECONDS)
val freq = 0.1 val freq = 0.1
val target = 5 * sin(2.0 * PI * freq * t) + val target = 5 * sin(2.0 * PI * freq * t) +
sin(2 * PI * 21 * freq * t + 0.1 * (timeFromStart / pidParameters.timeStep)) sin(2 * PI * 21 * freq * t + 0.02 * (timeFromStart / pidParameters.timeStep))
pid.write(Regulator.target, target) pid.write(Regulator.target, target)
} }
} }
val maxAge = 10.seconds
context.showDashboard { context.showDashboard {
plot { plot {
plotNumberState(context, state) { plotNumberState(context, state, maxAge = maxAge) {
name = "real position" name = "real position"
} }
plotDeviceProperty(device["pid"], Regulator.position.name) { plotDeviceProperty(device["pid"], Regulator.position.name, maxAge = maxAge) {
name = "read position" name = "read position"
} }
plotDeviceProperty(device["pid"], Regulator.target.name) { plotDeviceProperty(device["pid"], Regulator.target.name, maxAge = maxAge) {
name = "target" name = "target"
} }
} }
plot { plot {
plotDeviceProperty(device["start"], LimitSwitch.locked.name) { plotDeviceProperty(device["start"], LimitSwitch.locked.name, maxAge = maxAge) {
name = "start measured" name = "start measured"
mode = ScatterMode.markers mode = ScatterMode.markers
} }
plotDeviceProperty(device["end"], LimitSwitch.locked.name) { plotDeviceProperty(device["end"], LimitSwitch.locked.name, maxAge = maxAge) {
name = "end measured" name = "end measured"
mode = ScatterMode.markers mode = ScatterMode.markers
} }