Skip to content

Commit f8c122a

Browse files
committed
Rework continuous simulation to work with custom Amount
1 parent 1f52e5f commit f8c122a

File tree

33 files changed

+493
-329
lines changed

33 files changed

+493
-329
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Unreleased
44

55
### Added
6+
- Flow control simulation
67
- Value averaging plot extension
78
- PLC4X bindings
89
- Shortcuts to access all Controls devices in a magix network.

controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ConstructorElement.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,9 +190,9 @@ public fun <T1, T2, R> StateContainer.combineState(
190190
* @param transformation a function that takes an array of individual state values and maps it to a combined value.
191191
* @return a new [DeviceState] representing the combined state, with the value computed by the transformation function.
192192
*/
193-
public inline fun <reified T, R> StateContainer.combineState(
193+
public fun <T, R> StateContainer.combineState(
194194
states: Collection<DeviceState<T>>,
195-
crossinline transformation: (Array<T>) -> R,
195+
transformation: (List<T>) -> R,
196196
): DeviceState<R> = registerState(DeviceState.combine(states, transformation))
197197

198198
/**

controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt

Lines changed: 67 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ package space.kscience.controls.constructor
33
import kotlinx.coroutines.CoroutineScope
44
import kotlinx.coroutines.Job
55
import kotlinx.coroutines.flow.*
6-
import space.kscience.controls.constructor.units.NumericalValue
6+
import kotlinx.coroutines.launch
7+
import space.kscience.controls.constructor.units.Amount
78
import space.kscience.controls.constructor.units.UnitsOfMeasurement
89
import kotlin.reflect.KProperty
910

@@ -55,6 +56,8 @@ public fun <T> DeviceState<T>.withDependencies(
5556

5657
/**
5758
* Create a new read-only [DeviceState] that mirrors receiver state by mapping the value with [mapper].
59+
*
60+
* This implementation is thread safe and "cold" meaning that it computes values and flows on-demand.
5861
*/
5962
public fun <T, R> DeviceState.Companion.map(
6063
state: DeviceState<T>,
@@ -66,22 +69,47 @@ public fun <T, R> DeviceState.Companion.map(
6669

6770
override val valueFlow: Flow<R> = state.valueFlow.map(mapper)
6871

69-
override fun toString(): String = "DeviceState.map(state=${state})"
72+
override fun toString(): String = "DeviceState.map(state=${state}, mapper=$mapper)"
7073
}
7174

7275
public fun <T, R> DeviceState<T>.map(mapper: (T) -> R): DeviceStateWithDependencies<R> = DeviceState.map(this, mapper)
7376

74-
public fun DeviceState<NumericalValue<out UnitsOfMeasurement>>.values(): DeviceState<Double> =
75-
object : DeviceState<Double> {
76-
override val value: Double
77-
get() = this@values.value.value
7877

79-
override val valueFlow: Flow<Double>
80-
get() = this@values.valueFlow.map { it.value }
78+
/**
79+
* A hot variant of [DeviceState.map]. It allows suspended transformations
80+
*/
81+
public fun <T, R> DeviceState.Companion.transform(
82+
state: DeviceState<T>,
83+
scope: CoroutineScope,
84+
initialValue: R,
85+
transform: suspend (T) -> R
86+
): DeviceStateWithDependencies<R> = object : DeviceStateWithDependencies<R> {
87+
override val dependencies: Collection<DeviceState<*>> = listOf(state)
8188

82-
override fun toString(): String = this@values.toString()
89+
override val valueFlow = MutableStateFlow<R>(initialValue)
90+
91+
val transformJob = scope.launch {
92+
valueFlow.emit(transform(state.value))
93+
state.valueFlow.collect {
94+
valueFlow.emit(transform(it))
95+
}
8396
}
8497

98+
override val value: R get() = valueFlow.value
99+
100+
override fun toString(): String = "DeviceState.transform(state=${state}, transform=$transform)"
101+
}
102+
103+
public suspend fun <T, R> DeviceState<T>.transform(
104+
scope: CoroutineScope,
105+
transform: suspend (T) -> R
106+
): DeviceStateWithDependencies<R> = DeviceState.transform(
107+
state = this,
108+
scope = scope,
109+
initialValue = transform(value),
110+
transform = transform
111+
)
112+
85113
/**
86114
* Combine two device states into one read-only [DeviceState]. Only the latest value of each state is used.
87115
*/
@@ -133,15 +161,18 @@ public fun <T1, T2, T3, R> DeviceState.Companion.combine(
133161
* @param mapper a function that takes a collection of state values and maps it to a combined value.
134162
* @return a [DeviceStateWithDependencies] representing the combined state, which has dependencies on the input [states].
135163
*/
136-
public inline fun <reified T, R> DeviceState.Companion.combine(
164+
public fun <T, R> DeviceState.Companion.combine(
137165
states: Collection<DeviceState<T>>,
138-
crossinline mapper: (Array<T>) -> R,
166+
mapper: (List<T>) -> R,
139167
): DeviceStateWithDependencies<R> = object : DeviceStateWithDependencies<R> {
140168
override val dependencies = states
141169

142-
override val value: R get() = mapper(states.map { it.value }.toTypedArray())
170+
override val value: R get() = mapper(states.map { it.value })
143171

144-
override val valueFlow: Flow<R> = combine<T, R>(states.map { it.valueFlow }, mapper)
172+
@Suppress("UNCHECKED_CAST")
173+
override val valueFlow: Flow<R> = combine(states.map { it.valueFlow } ){ array: Array<Any?>->
174+
mapper(array.asList() as List<T>)
175+
}
145176

146177
override fun toString(): String = "DeviceState.combine(states=${states.joinToString()})"
147178
}
@@ -159,20 +190,38 @@ public inline fun <reified T, R> DeviceState.Companion.combine(
159190
* @return a `DeviceStateWithDependencies` instance representing the combined state, with its value computed
160191
* dynamically based on the input states and the `mapper` function.
161192
*/
162-
public inline fun <reified T, K, R> DeviceState.Companion.combine(
193+
public fun <T, K, R> DeviceState.Companion.combine(
163194
states: Map<K, DeviceState<T>>,
164-
crossinline mapper: (Map<K, T>) -> R,
195+
mapper: (Map<K, T>) -> R,
165196
): DeviceStateWithDependencies<R> = object : DeviceStateWithDependencies<R> {
166197
override val dependencies = states.values
167198

168199
override val value: R get() = mapper(states.mapValues { it.value.value })
169200

170201
private val entries = states.entries.toList()
171202

172-
override val valueFlow: Flow<R> = combine<T, R>(entries.map { it.value.valueFlow }) { array: Array<T> ->
203+
@Suppress("UNCHECKED_CAST")
204+
override val valueFlow: Flow<R> = combine(entries.map { it.value.valueFlow }) { array: Array<Any?> ->
173205
// restore mapping
174-
mapper(entries.indices.associate { entries[it].key to array[it] })
206+
mapper(entries.indices.associate { entries[it].key to (array[it] as T) })
175207
}
176208

177209
override fun toString(): String = "DeviceState.associate(states=${states})"
178-
}
210+
}
211+
212+
/**
213+
* Transforms a [DeviceState] containing a [Amount] with specific [UnitsOfMeasurement]
214+
* into a [DeviceState] containing a [Double] representing the underlying numerical value.
215+
*
216+
* @return A new [DeviceState] object that provides the numerical value as a [Double].
217+
*/
218+
public fun DeviceState<Amount<out UnitsOfMeasurement>>.values(): DeviceState<Double> =
219+
object : DeviceState<Double> {
220+
override val value: Double
221+
get() = this@values.value.value
222+
223+
override val valueFlow: Flow<Double>
224+
get() = this@values.valueFlow.map { it.value }
225+
226+
override fun toString(): String = this@values.toString()
227+
}

controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/Drive.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@ import space.kscience.controls.constructor.DeviceConstructor
44
import space.kscience.controls.constructor.MutableDeviceState
55
import space.kscience.controls.constructor.property
66
import space.kscience.controls.constructor.units.NewtonsMeters
7-
import space.kscience.controls.constructor.units.NumericalValue
8-
import space.kscience.controls.constructor.units.numerical
7+
import space.kscience.controls.constructor.units.Numeric
8+
import space.kscience.controls.constructor.units.numericalValue
99
import space.kscience.dataforge.context.Context
1010
import space.kscience.dataforge.meta.MetaConverter
1111

1212
//TODO use current as input
1313

1414
public class Drive(
1515
context: Context,
16-
force: MutableDeviceState<NumericalValue<NewtonsMeters>> = MutableDeviceState(NumericalValue(0)),
16+
force: MutableDeviceState<Numeric<NewtonsMeters>> = MutableDeviceState(Numeric(0)),
1717
) : DeviceConstructor(context) {
18-
public val force: MutableDeviceState<NumericalValue<NewtonsMeters>> by property(MetaConverter.numerical(), force)
18+
public val force: MutableDeviceState<Numeric<NewtonsMeters>> by property(MetaConverter.numericalValue(), force)
1919
}

controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/EncoderDevice.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import space.kscience.controls.constructor.DeviceConstructor
44
import space.kscience.controls.constructor.DeviceState
55
import space.kscience.controls.constructor.property
66
import space.kscience.controls.constructor.units.Degrees
7-
import space.kscience.controls.constructor.units.NumericalValue
8-
import space.kscience.controls.constructor.units.numerical
7+
import space.kscience.controls.constructor.units.Numeric
8+
import space.kscience.controls.constructor.units.numericalValue
99
import space.kscience.dataforge.context.Context
1010
import space.kscience.dataforge.meta.MetaConverter
1111

@@ -14,7 +14,7 @@ import space.kscience.dataforge.meta.MetaConverter
1414
*/
1515
public class EncoderDevice(
1616
context: Context,
17-
position: DeviceState<NumericalValue<Degrees>>
17+
position: DeviceState<Numeric<Degrees>>
1818
) : DeviceConstructor(context) {
19-
public val position: DeviceState<NumericalValue<Degrees>> by property(MetaConverter.numerical<Degrees>(), position)
19+
public val position: DeviceState<Numeric<Degrees>> by property(MetaConverter.numericalValue<Degrees>(), position)
2020
}

controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/LimitSwitch.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import space.kscience.controls.constructor.DeviceState
55
import space.kscience.controls.constructor.map
66
import space.kscience.controls.constructor.registerAsProperty
77
import space.kscience.controls.constructor.units.Direction
8-
import space.kscience.controls.constructor.units.NumericalValue
8+
import space.kscience.controls.constructor.units.Numeric
99
import space.kscience.controls.constructor.units.UnitsOfMeasurement
1010
import space.kscience.controls.spec.DevicePropertySpec
1111
import space.kscience.controls.spec.DeviceSpec
@@ -28,7 +28,7 @@ public class LimitSwitch(
2828
}
2929
}
3030

31-
public fun <U : UnitsOfMeasurement, T : NumericalValue<U>> LimitSwitch(
31+
public fun <U : UnitsOfMeasurement, T : Numeric<U>> LimitSwitch(
3232
context: Context,
3333
limit: T,
3434
boundary: Direction,

controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/LinearDrive.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import space.kscience.controls.constructor.models.PidParameters
55
import space.kscience.controls.constructor.models.PidRegulator
66
import space.kscience.controls.constructor.units.Meters
77
import space.kscience.controls.constructor.units.NewtonsMeters
8-
import space.kscience.controls.constructor.units.NumericalValue
9-
import space.kscience.controls.constructor.units.numerical
8+
import space.kscience.controls.constructor.units.Numeric
9+
import space.kscience.controls.constructor.units.numericalValue
1010
import space.kscience.dataforge.context.Context
1111
import space.kscience.dataforge.meta.Meta
1212
import space.kscience.dataforge.meta.MetaConverter
@@ -15,13 +15,13 @@ public class LinearDrive(
1515
drive: Drive,
1616
start: LimitSwitch,
1717
end: LimitSwitch,
18-
position: DeviceState<NumericalValue<Meters>>,
18+
position: DeviceState<Numeric<Meters>>,
1919
pidParameters: PidParameters,
2020
context: Context = drive.context,
2121
meta: Meta = Meta.EMPTY,
2222
) : DeviceConstructor(context, meta) {
2323

24-
public val position: DeviceState<NumericalValue<Meters>> by property(MetaConverter.numerical(), position)
24+
public val position: DeviceState<Numeric<Meters>> by property(MetaConverter.numericalValue(), position)
2525

2626
public val drive: Drive by device(drive)
2727
public val pid: PidRegulator<Meters, NewtonsMeters> = model(

controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/StepDrive.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package space.kscience.controls.constructor.devices
22

33
import space.kscience.controls.constructor.*
44
import space.kscience.controls.constructor.units.Degrees
5-
import space.kscience.controls.constructor.units.NumericalValue
5+
import space.kscience.controls.constructor.units.Numeric
66
import space.kscience.controls.constructor.units.plus
77
import space.kscience.controls.constructor.units.times
88
import space.kscience.dataforge.context.Context
@@ -57,9 +57,9 @@ public class StepDrive(
5757
* Compute a state using given tick-to-angle transformation
5858
*/
5959
public fun StepDrive.angle(
60-
step: NumericalValue<Degrees>,
61-
zero: NumericalValue<Degrees> = NumericalValue(0),
62-
): DeviceState<NumericalValue<Degrees>> = position.map {
60+
step: Numeric<Degrees>,
61+
zero: Numeric<Degrees> = Numeric(0),
62+
): DeviceState<Numeric<Degrees>> = position.map {
6363
zero + it * step
6464
}
6565

controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/Inertia.kt

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ public class Inertia<U : UnitsOfMeasurement, V : UnitsOfMeasurement>(
1313
context: Context,
1414
force: DeviceState<Double>, //TODO add system unit sets
1515
inertia: Double,
16-
public val position: MutableDeviceState<NumericalValue<U>>,
17-
public val velocity: MutableDeviceState<NumericalValue<V>>,
16+
public val position: MutableDeviceState<Numeric<U>>,
17+
public val velocity: MutableDeviceState<Numeric<V>>,
1818
) : ModelConstructor(context) {
1919

2020
init {
@@ -28,10 +28,10 @@ public class Inertia<U : UnitsOfMeasurement, V : UnitsOfMeasurement>(
2828
val dtSeconds = (next - prev).toDouble(DurationUnit.SECONDS)
2929

3030
// compute new value based on velocity and acceleration from the previous step
31-
position.value += NumericalValue(velocity.value.value * dtSeconds + currentForce / inertia * dtSeconds.pow(2) / 2)
31+
position.value += Numeric(velocity.value.value * dtSeconds + currentForce / inertia * dtSeconds.pow(2) / 2)
3232

3333
// compute new velocity based on acceleration on the previous step
34-
velocity.value += NumericalValue(currentForce / inertia * dtSeconds)
34+
velocity.value += Numeric(currentForce / inertia * dtSeconds)
3535
currentForce = force.value
3636
}
3737

@@ -41,10 +41,10 @@ public class Inertia<U : UnitsOfMeasurement, V : UnitsOfMeasurement>(
4141
*/
4242
public fun linear(
4343
context: Context,
44-
force: DeviceState<NumericalValue<Newtons>>,
45-
mass: NumericalValue<Kilograms>,
46-
position: MutableDeviceState<NumericalValue<Meters>>,
47-
velocity: MutableDeviceState<NumericalValue<MetersPerSecond>> = MutableDeviceState(NumericalValue(0.0)),
44+
force: DeviceState<Numeric<Newtons>>,
45+
mass: Numeric<Kilograms>,
46+
position: MutableDeviceState<Numeric<Meters>>,
47+
velocity: MutableDeviceState<Numeric<MetersPerSecond>> = MutableDeviceState(Numeric(0.0)),
4848
): Inertia<Meters, MetersPerSecond> = Inertia(
4949
context = context,
5050
force = force.values(),
@@ -55,10 +55,10 @@ public class Inertia<U : UnitsOfMeasurement, V : UnitsOfMeasurement>(
5555

5656
public fun circular(
5757
context: Context,
58-
force: DeviceState<NumericalValue<NewtonsMeters>>,
59-
momentOfInertia: NumericalValue<KgM2>,
60-
position: MutableDeviceState<NumericalValue<Degrees>>,
61-
velocity: MutableDeviceState<NumericalValue<DegreesPerSecond>> = MutableDeviceState(NumericalValue(0.0)),
58+
force: DeviceState<Numeric<NewtonsMeters>>,
59+
momentOfInertia: Numeric<KgM2>,
60+
position: MutableDeviceState<Numeric<Degrees>>,
61+
velocity: MutableDeviceState<Numeric<DegreesPerSecond>> = MutableDeviceState(Numeric(0.0)),
6262
): Inertia<Degrees, DegreesPerSecond> = Inertia(
6363
context = context,
6464
force = force.values(),

controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/Leadscrew.kt

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,20 @@ import kotlin.math.PI
1212
*/
1313
public class Leadscrew(
1414
context: Context,
15-
public val leverage: NumericalValue<Meters>,
15+
public val leverage: Numeric<Meters>,
1616
) : ModelConstructor(context) {
1717

1818
public fun torqueToForce(
19-
stateOfTorque: DeviceState<NumericalValue<NewtonsMeters>>,
20-
): DeviceState<NumericalValue<Newtons>> = DeviceState.map(stateOfTorque) { torque ->
21-
NumericalValue(torque.value / leverage.value )
19+
stateOfTorque: DeviceState<Numeric<NewtonsMeters>>,
20+
): DeviceState<Numeric<Newtons>> = DeviceState.map(stateOfTorque) { torque ->
21+
Numeric(torque.value / leverage.value )
2222
}
2323

2424
public fun degreesToMeters(
25-
stateOfAngle: DeviceState<NumericalValue<Degrees>>,
26-
offset: NumericalValue<Meters> = NumericalValue(0),
27-
): DeviceState<NumericalValue<Meters>> = DeviceState.map(stateOfAngle) { degrees ->
28-
offset + NumericalValue(degrees.value * 2 * PI / 360 * leverage.value )
25+
stateOfAngle: DeviceState<Numeric<Degrees>>,
26+
offset: Numeric<Meters> = Numeric(0),
27+
): DeviceState<Numeric<Meters>> = DeviceState.map(stateOfAngle) { degrees ->
28+
offset + Numeric(degrees.value * 2 * PI / 360 * leverage.value )
2929
}
3030

3131
}

0 commit comments

Comments
 (0)