WIP Constructor update

This commit is contained in:
Alexander Nozik 2024-06-01 09:35:03 +03:00
parent 9edde7bdbd
commit 54e915ef10
22 changed files with 320 additions and 212 deletions

View File

@ -36,7 +36,7 @@ public class ConnectionConstrucorElement(
) : ConstructorElement ) : ConstructorElement
public class ModelConstructorElement( public class ModelConstructorElement(
public val model: ConstructorModel public val model: ModelConstructor
) : ConstructorElement ) : ConstructorElement
@ -89,7 +89,7 @@ public fun <T> StateContainer.stateOf(initialValue: T): MutableDeviceState<T> =
MutableDeviceState(initialValue) MutableDeviceState(initialValue)
) )
public fun <T : ConstructorModel> StateContainer.model(model: T): T { public fun <T : ModelConstructor> StateContainer.model(model: T): T {
registerElement(ModelConstructorElement(model)) registerElement(ModelConstructorElement(model))
return model return model
} }
@ -125,14 +125,13 @@ public fun <T1, T2, R> StateContainer.combineState(
transformation: (T1, T2) -> R, transformation: (T1, T2) -> R,
): DeviceState<R> = state(DeviceState.combine(first, second, transformation)) ): DeviceState<R> = state(DeviceState.combine(first, second, transformation))
/** /**
* Create and start binding between [sourceState] and [targetState]. Changes made to [sourceState] are automatically * Create and start binding between [sourceState] and [targetState]. Changes made to [sourceState] are automatically
* transferred onto [targetState], but not vise versa. * transferred onto [targetState], but not vise versa.
* *
* On resulting [Job] cancel the binding is unregistered * On resulting [Job] cancel the binding is unregistered
*/ */
public fun <T> StateContainer.bindTo(sourceState: DeviceState<T>, targetState: MutableDeviceState<T>): Job { public fun <T> StateContainer.bind(sourceState: DeviceState<T>, targetState: MutableDeviceState<T>): Job {
val descriptor = ConnectionConstrucorElement(setOf(sourceState), setOf(targetState)) val descriptor = ConnectionConstrucorElement(setOf(sourceState), setOf(targetState))
registerElement(descriptor) registerElement(descriptor)
return sourceState.valueFlow.onEach { return sourceState.valueFlow.onEach {

View File

@ -32,13 +32,14 @@ public abstract class DeviceConstructor(
_constructorElements.remove(constructorElement) _constructorElements.remove(constructorElement)
} }
override fun <T> registerProperty( override fun <T, S: DeviceState<T>> registerAsProperty(
converter: MetaConverter<T>, converter: MetaConverter<T>,
descriptor: PropertyDescriptor, descriptor: PropertyDescriptor,
state: DeviceState<T>, state: S,
) { ): S {
super.registerProperty(converter, descriptor, state) val res = super.registerAsProperty(converter, descriptor, state)
registerElement(PropertyConstructorElement(this, descriptor.name, state)) registerElement(PropertyConstructorElement(this, descriptor.name, state))
return res
} }
} }
@ -83,7 +84,7 @@ public fun <T, S : DeviceState<T>> DeviceConstructor.property(
PropertyDelegateProvider { _: DeviceConstructor, property -> PropertyDelegateProvider { _: DeviceConstructor, property ->
val name = nameOverride ?: property.name val name = nameOverride ?: property.name
val descriptor = PropertyDescriptor(name).apply(descriptorBuilder) val descriptor = PropertyDescriptor(name).apply(descriptorBuilder)
registerProperty(converter, descriptor, state) registerAsProperty(converter, descriptor, state)
ReadOnlyProperty { _: DeviceConstructor, _ -> ReadOnlyProperty { _: DeviceConstructor, _ ->
state state
} }
@ -140,7 +141,10 @@ public fun <T> DeviceConstructor.virtualProperty(
nameOverride, nameOverride,
) )
public fun <T, S : DeviceState<T>> DeviceConstructor.property( public fun <T, S : DeviceState<T>> DeviceConstructor.registerAsProperty(
spec: DevicePropertySpec<*, T>, spec: DevicePropertySpec<*, T>,
state: S, state: S,
): Unit = registerProperty(spec.converter, spec.descriptor, state) ): S {
registerAsProperty(spec.converter, spec.descriptor, state)
return state
}

View File

@ -93,11 +93,11 @@ public open class DeviceGroup(
/** /**
* Register a new property based on [DeviceState]. Properties could be modified dynamically * Register a new property based on [DeviceState]. Properties could be modified dynamically
*/ */
public open fun <T> registerProperty( public open fun <T, S : DeviceState<T>> registerAsProperty(
converter: MetaConverter<T>, converter: MetaConverter<T>,
descriptor: PropertyDescriptor, descriptor: PropertyDescriptor,
state: DeviceState<T>, state: S,
) { ): S {
val name = descriptor.name.parseAsName() val name = descriptor.name.parseAsName()
require(properties[name] == null) { "Can't add property with name $name. It already exists." } require(properties[name] == null) { "Can't add property with name $name. It already exists." }
properties[name] = Property(state, converter, descriptor) properties[name] = Property(state, converter, descriptor)
@ -109,6 +109,7 @@ public open class DeviceGroup(
) )
) )
}.launchIn(this) }.launchIn(this)
return state
} }
private val actions: MutableMap<Name, Action> = hashMapOf() private val actions: MutableMap<Name, Action> = hashMapOf()
@ -174,8 +175,8 @@ public open class DeviceGroup(
} }
} }
public fun <T> DeviceGroup.registerProperty(propertySpec: DevicePropertySpec<*, T>, state: DeviceState<T>) { public fun <T> DeviceGroup.registerAsProperty(propertySpec: DevicePropertySpec<*, T>, state: DeviceState<T>) {
registerProperty(propertySpec.converter, propertySpec.descriptor, state) registerAsProperty(propertySpec.converter, propertySpec.descriptor, state)
} }
public fun DeviceManager.registerDeviceGroup( public fun DeviceManager.registerDeviceGroup(
@ -246,13 +247,13 @@ public fun DeviceGroup.registerDeviceGroup(name: String, block: DeviceGroup.() -
/** /**
* Register read-only property based on [state] * Register read-only property based on [state]
*/ */
public fun <T : Any> DeviceGroup.registerProperty( public fun <T : Any> DeviceGroup.registerAsProperty(
name: String, name: String,
converter: MetaConverter<T>, converter: MetaConverter<T>,
state: DeviceState<T>, state: DeviceState<T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
) { ) {
registerProperty( registerAsProperty(
converter, converter,
PropertyDescriptor(name).apply(descriptorBuilder), PropertyDescriptor(name).apply(descriptorBuilder),
state state
@ -268,7 +269,7 @@ public fun <T : Any> DeviceGroup.registerMutableProperty(
state: MutableDeviceState<T>, state: MutableDeviceState<T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
) { ) {
registerProperty( registerAsProperty(
converter, converter,
PropertyDescriptor(name).apply(descriptorBuilder), PropertyDescriptor(name).apply(descriptorBuilder),
state state

View File

@ -67,9 +67,11 @@ public fun <T, R> DeviceState.Companion.map(
override val valueFlow: Flow<R> = state.valueFlow.map(mapper) override val valueFlow: Flow<R> = state.valueFlow.map(mapper)
override fun toString(): String = "DeviceState.map(arg=${state})" override fun toString(): String = "DeviceState.map(state=${state})"
} }
public fun <T, R> DeviceState<T>.map(mapper: (T) -> R): DeviceStateWithDependencies<R> = DeviceState.map(this, mapper)
/** /**
* Combine two device states into one read-only [DeviceState]. Only the latest value of each state is used. * Combine two device states into one read-only [DeviceState]. Only the latest value of each state is used.
*/ */

View File

@ -6,7 +6,7 @@ import kotlinx.coroutines.newCoroutineContext
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
public abstract class ConstructorModel( public abstract class ModelConstructor(
final override val context: Context, final override val context: Context,
vararg dependencies: DeviceState<*>, vararg dependencies: DeviceState<*>,
) : StateContainer, CoroutineScope { ) : StateContainer, CoroutineScope {

View File

@ -1,57 +0,0 @@
package space.kscience.controls.constructor
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
/**
* A state describing a [Double] value in the [range]
*/
public class DoubleInRangeState(
initialValue: Double,
public val range: ClosedFloatingPointRange<Double>,
) : MutableDeviceState<Double> {
init {
require(initialValue in range) { "Initial value should be in range" }
}
private val _valueFlow = MutableStateFlow(initialValue)
override var value: Double
get() = _valueFlow.value
set(newValue) {
_valueFlow.value = newValue.coerceIn(range)
}
override val valueFlow: StateFlow<Double> get() = _valueFlow
/**
* A state showing that the range is on its lower boundary
*/
public val atStart: DeviceState<Boolean> = DeviceState.map(this) {
it <= range.start
}
/**
* A state showing that the range is on its higher boundary
*/
public val atEnd: DeviceState<Boolean> = DeviceState.map(this) {
it >= range.endInclusive
}
override fun toString(): String = "DoubleRangeState(range=$range)"
}
/**
* Create and register a [DoubleInRangeState]
*/
public fun StateContainer.doubleInRangeState(
initialValue: Double,
range: ClosedFloatingPointRange<Double>,
): DoubleInRangeState = DoubleInRangeState(initialValue, range).also {
registerElement(StateConstructorElement(it))
}

View File

@ -2,6 +2,7 @@ package space.kscience.controls.constructor
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.emptyFlow
/** /**
* A [MutableDeviceState] that does not correspond to a physical state * A [MutableDeviceState] that does not correspond to a physical state
@ -35,3 +36,18 @@ public fun <T> MutableDeviceState(
initialValue: T, initialValue: T,
callback: (T) -> Unit = {}, callback: (T) -> Unit = {},
): MutableDeviceState<T> = VirtualDeviceState(initialValue, callback) ): 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,20 +0,0 @@
package space.kscience.controls.constructor.library
import kotlinx.coroutines.flow.FlowCollector
import space.kscience.controls.constructor.DeviceConstructor
import space.kscience.controls.constructor.DeviceState
import space.kscience.controls.constructor.DeviceStateWithDependencies
import space.kscience.controls.constructor.flowState
import space.kscience.dataforge.context.Context
/**
* A device that converts one type of physical quantity to another type
*/
public class Converter<T, R>(
context: Context,
input: DeviceState<T>,
initialValue: R,
transform: suspend FlowCollector<R>.(T) -> Unit,
) : DeviceConstructor(context) {
public val output: DeviceStateWithDependencies<R> = flowState(input, initialValue, transform)
}

View File

@ -0,0 +1,57 @@
package space.kscience.controls.constructor.library
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
/**
* A state describing a [Double] value in the [range]
*/
public open class DoubleInRangeState(
private val input: DeviceState<Double>,
public val range: ClosedFloatingPointRange<Double>,
) : DeviceState<Double> {
override val valueFlow: Flow<Double> get() = input.valueFlow.map { it.coerceIn(range) }
override val value: Double 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 MutableDoubleInRangeState(
private val mutableInput: MutableDeviceState<Double>,
range: ClosedFloatingPointRange<Double>
) : DoubleInRangeState(mutableInput, range), MutableDeviceState<Double> {
override var value: Double
get() = super.value
set(value) {
mutableInput.value = value.coerceIn(range)
}
}
public fun MutableDoubleInRangeState(
initialValue: Double,
range: ClosedFloatingPointRange<Double>
): MutableDoubleInRangeState = MutableDoubleInRangeState(MutableDeviceState(initialValue),range)
public fun DeviceState<Double>.coerceIn(
range: ClosedFloatingPointRange<Double>
): DoubleInRangeState = DoubleInRangeState(this, range)
public fun MutableDeviceState<Double>.coerceIn(
range: ClosedFloatingPointRange<Double>
): MutableDoubleInRangeState = MutableDoubleInRangeState(this, range)

View File

@ -98,4 +98,5 @@ public class VirtualDrive(
} }
} }
public suspend fun Drive.stateOfForce(): MutableDeviceState<Double> = propertyAsState(Drive.force) public fun Drive.stateOfForce(initialForce: Double = 0.0): MutableDeviceState<Double> =
propertyAsState(Drive.force, initialForce)

View File

@ -1,37 +1,45 @@
package space.kscience.controls.constructor.library package space.kscience.controls.constructor.library
import space.kscience.controls.api.Device
import space.kscience.controls.constructor.DeviceConstructor import space.kscience.controls.constructor.DeviceConstructor
import space.kscience.controls.constructor.DeviceState import space.kscience.controls.constructor.DeviceState
import space.kscience.controls.constructor.property 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.constructor.units.compareTo
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.booleanProperty import space.kscience.controls.spec.booleanProperty
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
import space.kscience.dataforge.meta.MetaConverter
/**
* A limit switch device
*/
public interface LimitSwitch : Device {
public fun isLocked(): Boolean
public companion object : DeviceSpec<LimitSwitch>() {
public val locked: DevicePropertySpec<LimitSwitch, Boolean> by booleanProperty { isLocked() }
}
}
/** /**
* Virtual [LimitSwitch] * Virtual [LimitSwitch]
*/ */
public class VirtualLimitSwitch( public class LimitSwitch(
context: Context, context: Context,
locked: DeviceState<Boolean>, locked: DeviceState<Boolean>,
) : DeviceConstructor(context), LimitSwitch { ) : DeviceConstructor(context) {
public val locked: DeviceState<Boolean> by property(MetaConverter.boolean, locked) public val locked: DeviceState<Boolean> = registerAsProperty(LimitSwitch.locked, locked)
override fun isLocked(): Boolean = locked.value 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,19 +1,17 @@
package space.kscience.controls.constructor.library package space.kscience.controls.constructor.library
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.datetime.Instant import space.kscience.controls.constructor.DeviceState
import space.kscience.controls.constructor.DeviceGroup import space.kscience.controls.constructor.MutableDeviceState
import space.kscience.controls.constructor.state
import space.kscience.controls.constructor.stateOf
import space.kscience.controls.manager.clock import space.kscience.controls.manager.clock
import space.kscience.controls.spec.DeviceBySpec import space.kscience.dataforge.context.Context
import space.kscience.controls.spec.write
import space.kscience.dataforge.names.parseAsName
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.DurationUnit import kotlin.time.DurationUnit
@ -24,64 +22,53 @@ public data class PidParameters(
val kp: Double, val kp: Double,
val ki: Double, val ki: Double,
val kd: Double, val kd: Double,
val timeStep: Duration = 1.milliseconds, val timeStep: Duration,
) )
/** /**
* A drive with PID regulator * A PID regulator
*/ */
public class PidRegulator( public class PidRegulator(
public val drive: Drive, context: Context,
private val position: DeviceState<Double>,
public var pidParameters: PidParameters, // TODO expose as property public var pidParameters: PidParameters, // TODO expose as property
) : DeviceBySpec<Regulator>(Regulator, drive.context), Regulator { output: MutableDeviceState<Double> = MutableDeviceState(0.0),
) : Regulator<Double>(context) {
private val clock = drive.context.clock override val target: MutableDeviceState<Double> = stateOf(0.0)
override val output: MutableDeviceState<Double> = state(output)
override var target: Double = drive.position private var lastPosition: Double = target.value
private var lastTime: Instant = clock.now()
private var lastPosition: Double = target
private var integral: Double = 0.0 private var integral: Double = 0.0
private var updateJob: Job? = null
private val mutex = Mutex() private val mutex = Mutex()
private var lastTime = clock.now()
override suspend fun onStart() { private val updateJob = launch {
drive.start()
updateJob = launch {
while (isActive) { while (isActive) {
delay(pidParameters.timeStep) delay(pidParameters.timeStep)
mutex.withLock { mutex.withLock {
val realTime = clock.now() val realTime = clock.now()
val delta = target - getPosition() val delta = target.value - position.value
val dtSeconds = (realTime - lastTime).toDouble(DurationUnit.SECONDS) val dtSeconds = (realTime - lastTime).toDouble(DurationUnit.SECONDS)
integral += delta * dtSeconds integral += delta * dtSeconds
val derivative = (drive.position - lastPosition) / dtSeconds val derivative = (position.value - lastPosition) / dtSeconds
//set last time and value to new values //set last time and value to new values
lastTime = realTime lastTime = realTime
lastPosition = drive.position lastPosition = position.value
drive.write(Drive.force,pidParameters.kp * delta + pidParameters.ki * integral + pidParameters.kd * derivative) output.value = pidParameters.kp * delta + pidParameters.ki * integral + pidParameters.kd * derivative
//drive.force = pidParameters.kp * delta + pidParameters.ki * integral + pidParameters.kd * derivative
propertyChanged(Regulator.position, drive.position)
} }
} }
} }
} }
override suspend fun onStop() { //
updateJob?.cancel() //public fun DeviceGroup.pid(
drive.stop() // name: String,
} // drive: Drive,
// pidParameters: PidParameters,
override suspend fun getPosition(): Double = drive.position //): PidRegulator = install(name.parseAsName(), PidRegulator(drive, pidParameters))
}
public fun DeviceGroup.pid(
name: String,
drive: Drive,
pidParameters: PidParameters,
): PidRegulator = install(name.parseAsName(), PidRegulator(drive, pidParameters))

View File

@ -1,27 +1,17 @@
package space.kscience.controls.constructor.library package space.kscience.controls.constructor.library
import space.kscience.controls.api.Device import space.kscience.controls.constructor.DeviceConstructor
import space.kscience.controls.spec.* import space.kscience.controls.constructor.DeviceState
import space.kscience.dataforge.meta.MetaConverter import space.kscience.controls.constructor.MutableDeviceState
import space.kscience.dataforge.context.Context
/** /**
* A regulator with target value and current position * A regulator with target value and current position
*/ */
public interface Regulator : Device { public abstract class Regulator<T>(context: Context) : DeviceConstructor(context) {
/**
* Get or set target value
*/
public var target: Double
/** public abstract val target: MutableDeviceState<T>
* Current position value
*/
public suspend fun getPosition(): Double
public companion object : DeviceSpec<Regulator>() { public abstract val output: DeviceState<T>
public val target: MutableDevicePropertySpec<Regulator, Double> by mutableProperty(MetaConverter.double, Regulator::target)
public val position: DevicePropertySpec<Regulator, Double> by doubleProperty { getPosition() }
}
} }

View File

@ -0,0 +1,33 @@
package space.kscience.controls.constructor.library
import space.kscience.controls.constructor.DeviceState
import space.kscience.controls.constructor.MutableDeviceState
import space.kscience.controls.constructor.combineState
import space.kscience.controls.constructor.property
import space.kscience.controls.constructor.units.*
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.meta.MetaConverter
/**
* A step drive regulated by [input]
*/
public class StepDrive(
context: Context,
public val step: NumericalValue<Degrees>,
public val zero: NumericalValue<Degrees> = NumericalValue(0.0),
direction: Direction = Direction.UP,
input: MutableDeviceState<Int> = MutableDeviceState(0),
hold: MutableDeviceState<Boolean> = MutableDeviceState(false)
) : Transmission<Int, NumericalValue<Degrees>>(context) {
override val input: MutableDeviceState<Int> by property(MetaConverter.int, input)
public val hold: MutableDeviceState<Boolean> by property(MetaConverter.boolean, hold)
override val output: DeviceState<NumericalValue<Degrees>> = combineState(
input, hold
) { input, hold ->
//TODO use hold parameter
zero + input * direction.coef * step
}
}

View File

@ -0,0 +1,33 @@
package space.kscience.controls.constructor.library
import space.kscience.controls.constructor.DeviceConstructor
import space.kscience.controls.constructor.DeviceState
import space.kscience.controls.constructor.MutableDeviceState
import space.kscience.controls.constructor.flowState
import space.kscience.dataforge.context.Context
/**
* A model for a device that converts one type of physical quantity to another type
*/
public abstract class Transmission<T, R>(context: Context) : DeviceConstructor(context) {
public abstract val input: MutableDeviceState<T>
public abstract val output: DeviceState<R>
public companion object {
/**
* Create a device that is a hard connection between two physical quantities
*/
public suspend fun <T, R> direct(
context: Context,
input: MutableDeviceState<T>,
transform: suspend (T) -> R
): Transmission<T, R> {
val initialValue = transform(input.value)
return object : Transmission<T, R>(context) {
override val input: MutableDeviceState<T> = input
override val output: DeviceState<R> = flowState(input, initialValue) { emit(transform(it)) }
}
}
}
}

View File

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

View File

@ -7,4 +7,31 @@ import kotlin.jvm.JvmInline
* A value without identity coupled to units of measurements. * A value without identity coupled to units of measurements.
*/ */
@JvmInline @JvmInline
public value class NumericalValue<T: UnitsOfMeasurement>(public val value: Double) public value class NumericalValue<U : UnitsOfMeasurement>(public val value: Double)
public operator fun <U: UnitsOfMeasurement> NumericalValue<U>.compareTo(other: NumericalValue<U>): Int =
value.compareTo(other.value)
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())

View File

@ -23,6 +23,13 @@ public data object MetersPerSecond: UnitsOfVelocity
/**/ /**/
public sealed interface UnitsOfAngles : UnitsOfMeasurement
public data object Radians : UnitsOfAngles
public data object Degrees : UnitsOfAngles
/**/
public interface UnitsAngularOfVelocity : UnitsOfMeasurement public interface UnitsAngularOfVelocity : UnitsOfMeasurement
public data object RadiansPerSecond : UnitsAngularOfVelocity public data object RadiansPerSecond : UnitsAngularOfVelocity

View File

@ -15,8 +15,9 @@ class TimerTest {
@Test @Test
fun timer() = runTest { fun timer() = runTest {
val timer = TimerState(Global.request(ClockManager), 10.milliseconds) val timer = TimerState(Global.request(ClockManager), 10.milliseconds)
timer.valueFlow.take(10).onEach { timer.valueFlow.take(100).onEach {
println(it) println(it)
}.collect() }.collect()
} }
} }

View File

@ -9,6 +9,7 @@ import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.double import space.kscience.dataforge.meta.double
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.math.roundToLong import kotlin.math.roundToLong
import kotlin.time.Duration
@OptIn(InternalCoroutinesApi::class) @OptIn(InternalCoroutinesApi::class)
private class CompressedTimeDispatcher( private class CompressedTimeDispatcher(
@ -78,6 +79,14 @@ public class ClockManager : AbstractPlugin() {
CompressedTimeDispatcher(this, dispatcher, timeCompression) 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> { public companion object : PluginFactory<ClockManager> {
override val tag: PluginTag = PluginTag("clock", group = PluginTag.DATAFORGE_GROUP) override val tag: PluginTag = PluginTag("clock", group = PluginTag.DATAFORGE_GROUP)

View File

@ -41,7 +41,7 @@ class Spring(
val l0: Double, val l0: Double,
val begin: DeviceState<XY>, val begin: DeviceState<XY>,
val end: DeviceState<XY>, val end: DeviceState<XY>,
) : ConstructorModel(context) { ) : ModelConstructor(context) {
/** /**
* vector from start to end * vector from start to end
@ -76,7 +76,7 @@ class MaterialPoint(
val force: DeviceState<XY>, val force: DeviceState<XY>,
val position: MutableDeviceState<XY>, val position: MutableDeviceState<XY>,
val velocity: MutableDeviceState<XY> = MutableDeviceState(XY.ZERO), val velocity: MutableDeviceState<XY> = MutableDeviceState(XY.ZERO),
) : ConstructorModel(context, force, position, velocity) { ) : ModelConstructor(context, force, position, velocity) {
private val timer: TimerState = timer(2.milliseconds) private val timer: TimerState = timer(2.milliseconds)

View File

@ -52,14 +52,18 @@ class LinearDrive(
) : DeviceConstructor(drive.context, meta) { ) : DeviceConstructor(drive.context, meta) {
val drive by device(drive) val drive by device(drive)
val pid by device(PidRegulator(drive, pidParameters)) val pid by device(
PidRegulator(
context = context,
position = drive.propertyAsState(Drive.position, 0.0),
pidParameters = pidParameters
)
)
private val binding = bind(pid.output, drive.stateOfForce())
val start by device(start) val start by device(start)
val end by device(end) val end by device(end)
val position = drive.propertyAsState(Drive.position, Double.NaN)
val target = pid.propertyAsState(Regulator.target, 0.0)
} }
/** /**
@ -67,14 +71,14 @@ class LinearDrive(
*/ */
fun LinearDrive( fun LinearDrive(
context: Context, context: Context,
positionState: DoubleInRangeState, positionState: MutableDoubleInRangeState,
mass: Double, mass: Double,
pidParameters: PidParameters, pidParameters: PidParameters,
meta: Meta = Meta.EMPTY, meta: Meta = Meta.EMPTY,
): LinearDrive = LinearDrive( ): LinearDrive = LinearDrive(
drive = VirtualDrive(context, mass, positionState), drive = VirtualDrive(context, mass, positionState),
start = VirtualLimitSwitch(context, positionState.atStart), start = LimitSwitch(context, positionState.atStart),
end = VirtualLimitSwitch(context, positionState.atEnd), end = LimitSwitch(context, positionState.atEnd),
pidParameters = pidParameters, pidParameters = pidParameters,
meta = meta meta = meta
) )
@ -116,7 +120,7 @@ fun main() = application {
mutableStateOf(PidParameters(kp = 2.5, ki = 0.0, kd = -0.1, timeStep = 0.005.seconds)) mutableStateOf(PidParameters(kp = 2.5, ki = 0.0, kd = -0.1, timeStep = 0.005.seconds))
} }
val state = remember { DoubleInRangeState(0.0, -6.0..6.0) } val state = remember { MutableDoubleInRangeState(0.0, -6.0..6.0) }
val linearDrive = remember { val linearDrive = remember {
context.install( context.install(
@ -128,7 +132,7 @@ fun main() = application {
val modulator = remember { val modulator = remember {
context.install( context.install(
"modulator", "modulator",
Modulator(context, linearDrive.target) Modulator(context, linearDrive.pid.target)
) )
} }
@ -207,7 +211,7 @@ fun main() = application {
Slider( Slider(
pidParameters.timeStep.toDouble(DurationUnit.MILLISECONDS).toFloat(), pidParameters.timeStep.toDouble(DurationUnit.MILLISECONDS).toFloat(),
{ pidParameters = pidParameters.copy(timeStep = it.toDouble().milliseconds) }, { pidParameters = pidParameters.copy(timeStep = it.toDouble().milliseconds) },
valueRange = 0f..100f, valueRange = 1f..100f,
steps = 100 steps = 100
) )
} }
@ -248,14 +252,14 @@ fun main() = application {
lineStyle = LineStyle(SolidColor(Color.Blue)) lineStyle = LineStyle(SolidColor(Color.Blue))
) )
PlotDeviceProperty( PlotDeviceProperty(
linearDrive.pid, linearDrive.drive,
Regulator.position, Drive.position,
maxAge = maxAge, maxAge = maxAge,
sampling = 50.milliseconds, sampling = 50.milliseconds,
) )
PlotDeviceProperty( PlotNumberState(
linearDrive.pid, context = context,
Regulator.target, state = linearDrive.pid.target,
maxAge = maxAge, maxAge = maxAge,
sampling = 50.milliseconds, sampling = 50.milliseconds,
lineStyle = LineStyle(SolidColor(Color.Red)) lineStyle = LineStyle(SolidColor(Color.Red))