From 9f21a14f96fae6588f4802903ca8dfb829db4fc7 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Mon, 3 Jun 2024 18:02:57 +0300 Subject: [PATCH] Fix ball on springs demo --- .../controls/constructor/models/Inertia.kt | 5 +- .../constructor/models/MaterialPoint.kt | 42 ++++++ .../constructor/models/coordinates.kt | 8 -- .../constructor/units/NumericalValue.kt | 2 + .../controls/constructor/units/coordinates.kt | 36 +++++ .../src/jvmMain/kotlin/BodyOnSprings.kt | 134 ++++++------------ 6 files changed, 122 insertions(+), 105 deletions(-) create mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/MaterialPoint.kt delete mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/coordinates.kt create mode 100644 controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/coordinates.kt diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/Inertia.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/Inertia.kt index 88a377e..1c6a507 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/Inertia.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/Inertia.kt @@ -4,8 +4,6 @@ import space.kscience.controls.constructor.* import space.kscience.controls.constructor.units.* import space.kscience.dataforge.context.Context import kotlin.math.pow -import kotlin.time.Duration -import kotlin.time.Duration.Companion.milliseconds import kotlin.time.DurationUnit /** @@ -17,7 +15,6 @@ public class Inertia( inertia: Double, public val position: MutableDeviceState>, public val velocity: MutableDeviceState>, - timerPrecision: Duration = 10.milliseconds, ) : ModelConstructor(context) { init { @@ -27,7 +24,7 @@ public class Inertia( private var currentForce = force.value - private val movement = onTimer (timerPrecision) { prev, next -> + private val movement = onTimer { prev, next -> val dtSeconds = (next - prev).toDouble(DurationUnit.SECONDS) // compute new value based on velocity and acceleration from the previous step diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/MaterialPoint.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/MaterialPoint.kt new file mode 100644 index 0000000..c03841c --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/MaterialPoint.kt @@ -0,0 +1,42 @@ +package space.kscience.controls.constructor.models + +import space.kscience.controls.constructor.* +import space.kscience.controls.constructor.units.* +import space.kscience.dataforge.context.Context +import kotlin.math.pow +import kotlin.time.DurationUnit + + +/** + * 3D material point + */ +public class MaterialPoint( + context: Context, + force: DeviceState>, + mass: NumericalValue, + public val position: MutableDeviceState>, + public val velocity: MutableDeviceState>, +) : ModelConstructor(context) { + + init { + registerState(position) + registerState(velocity) + } + + private var currentForce = force.value + + private val movement = onTimer( + reads = setOf(velocity, position), + writes = setOf(velocity, position) + ) { prev, next -> + val dtSeconds = (next - prev).toDouble(DurationUnit.SECONDS) + + // compute new value based on velocity and acceleration from the previous step + position.value += (velocity.value * dtSeconds).cast(Meters) + + (currentForce / mass.value * dtSeconds.pow(2) / 2).cast(Meters) + + // compute new velocity based on acceleration on the previous step + velocity.value += (currentForce / mass.value * dtSeconds).cast(MetersPerSecond) + currentForce = force.value + } +} \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/coordinates.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/coordinates.kt deleted file mode 100644 index 50db92b..0000000 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/models/coordinates.kt +++ /dev/null @@ -1,8 +0,0 @@ -package space.kscience.controls.constructor.models - -import space.kscience.controls.constructor.units.NumericalValue -import space.kscience.controls.constructor.units.UnitsOfMeasurement - -public data class XY(val x: NumericalValue, val y: NumericalValue) - -public data class XYZ(val x: NumericalValue, val y: NumericalValue, val z: NumericalValue) \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/NumericalValue.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/NumericalValue.kt index e503623..8e77001 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/NumericalValue.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/NumericalValue.kt @@ -46,6 +46,8 @@ public operator fun NumericalValue.div( public operator fun NumericalValue.div(other: NumericalValue): Double = value / other.value +public operator fun NumericalValue.unaryMinus(): NumericalValue = NumericalValue(-value) + private object NumericalValueMetaConverter : MetaConverter> { override fun convert(obj: NumericalValue<*>): Meta = Meta(obj.value) diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/coordinates.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/coordinates.kt new file mode 100644 index 0000000..88d7bd6 --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/units/coordinates.kt @@ -0,0 +1,36 @@ +package space.kscience.controls.constructor.units + +public data class XY(val x: NumericalValue, val y: NumericalValue) + +public fun XY(x: Number, y: Number): XY = XY(NumericalValue(x), NumericalValue((y))) + +public operator fun XY.plus(other: XY): XY = + XY(x + other.x, y + other.y) + +public operator fun XY.times(c: Number): XY = XY(x * c, y * c) +public operator fun XY.div(c: Number): XY = XY(x / c, y / c) + +public operator fun XY.unaryMinus(): XY = XY(-x, -y) + +public data class XYZ( + val x: NumericalValue, + val y: NumericalValue, + val z: NumericalValue, +) + +public fun XYZ(x: Number, y: Number, z: Number): XYZ = + XYZ(NumericalValue(x), NumericalValue((y)), NumericalValue(z)) + +@Suppress("UNCHECKED_CAST", "UNUSED_PARAMETER") +public fun XYZ.cast(units: R): XYZ = this as XYZ + +public operator fun XYZ.plus(other: XYZ): XYZ = + XYZ(x + other.x, y + other.y, z + other.z) + +public operator fun XYZ.minus(other: XYZ): XYZ = + XYZ(x - other.x, y - other.y, z - other.z) + +public operator fun XYZ.times(c: Number): XYZ = XYZ(x * c, y * c, z * c) +public operator fun XYZ.div(c: Number): XYZ = XYZ(x / c, y / c, z / c) + +public operator fun XYZ.unaryMinus(): XYZ = XYZ(-x, -y, -z) \ No newline at end of file diff --git a/demo/constructor/src/jvmMain/kotlin/BodyOnSprings.kt b/demo/constructor/src/jvmMain/kotlin/BodyOnSprings.kt index e4de468..e937d1b 100644 --- a/demo/constructor/src/jvmMain/kotlin/BodyOnSprings.kt +++ b/demo/constructor/src/jvmMain/kotlin/BodyOnSprings.kt @@ -1,105 +1,50 @@ package space.kscience.controls.demo.constructor import androidx.compose.foundation.Canvas -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.size import androidx.compose.material.MaterialTheme import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window import androidx.compose.ui.window.application -import kotlinx.serialization.Serializable import space.kscience.controls.compose.asComposeState import space.kscience.controls.constructor.* +import space.kscience.controls.constructor.models.MaterialPoint +import space.kscience.controls.constructor.units.* import space.kscience.dataforge.context.Context +import java.awt.Dimension import kotlin.math.pow import kotlin.math.sqrt -import kotlin.time.Duration.Companion.milliseconds -import kotlin.time.DurationUnit -@Serializable -private data class XY(val x: Double, val y: Double) { - companion object { - val ZERO = XY(0.0, 0.0) - } -} - -private val XY.length: Double get() = sqrt(x.pow(2) + y.pow(2)) - -private operator fun XY.plus(other: XY): XY = XY(x + other.x, y + other.y) -private operator fun XY.times(c: Double): XY = XY(x * c, y * c) -private operator fun XY.div(c: Double): XY = XY(x / c, y / c) private class Spring( context: Context, val k: Double, - val l0: Double, - val begin: DeviceState, - val end: DeviceState, + val l0: NumericalValue, + val begin: DeviceState>, + val end: DeviceState>, ) : ModelConstructor(context) { /** - * vector from start to end + * Tension at the beginning point */ - val direction = combineState(begin, end) { begin: XY, end: XY -> - val dx = end.x - begin.x - val dy = end.y - begin.y - val l = sqrt(dx.pow(2) + dy.pow(2)) - XY(dx / l, dy / l) - } - - val tension: DeviceState = combineState(begin, end) { begin: XY, end: XY -> - val dx = end.x - begin.x - val dy = end.y - begin.y - k * sqrt(dx.pow(2) + dy.pow(2)) - } - - - val beginForce = combineState(direction, tension) { direction: XY, tension: Double -> - direction * (tension) - } - - - val endForce = combineState(direction, tension) { direction: XY, tension: Double -> - direction * (-tension) - } -} - -private class MaterialPoint( - context: Context, - val mass: Double, - val force: DeviceState, - val position: MutableDeviceState, - val velocity: MutableDeviceState = MutableDeviceState(XY.ZERO), -) : ModelConstructor(context, force, position, velocity) { - - private val timer: TimerState = timer(2.milliseconds) - - //TODO synchronize force change - - private val movement = timer.onChange( - writes = setOf(position, velocity), - reads = setOf(force, velocity, position) - ) { prev, next -> - val dt = (next - prev).toDouble(DurationUnit.SECONDS) - val a = force.value / mass - position.value += a * (dt * dt / 2) + velocity.value * dt - velocity.value += a * dt + val tension: DeviceState> = combineState(begin, end) { begin: XYZ, end: XYZ -> + val delta = end - begin + val l = sqrt(delta.x.value.pow(2) + delta.y.value.pow(2) + delta.z.value.pow(2)) + ((delta / l) * k * (l - l0.value)).cast(Newtons) } } private class BodyOnSprings( context: Context, - mass: Double, + mass: NumericalValue, k: Double, - startPosition: XY, - l0: Double = 1.0, + startPosition: XYZ, + l0: NumericalValue = NumericalValue(1.0), val xLeft: Double = -1.0, val xRight: Double = 1.0, val yBottom: Double = -1.0, @@ -110,22 +55,24 @@ private class BodyOnSprings( val height = yTop - yBottom val position = stateOf(startPosition) + val velocity: MutableDeviceState> = stateOf(XYZ(0, 0, 0)) - private val leftAnchor = stateOf(XY(xLeft, (yTop + yBottom) / 2)) + private val leftAnchor = stateOf(XYZ(xLeft, (yTop + yBottom) / 2, 0.0)) val leftSpring = model( Spring(context, k, l0, leftAnchor, position) ) - private val rightAnchor = stateOf(XY(xRight, (yTop + yBottom) / 2)) + private val rightAnchor = stateOf(XYZ(xRight, (yTop + yBottom) / 2, 0.0)) val rightSpring = model( Spring(context, k, l0, rightAnchor, position) ) - val force: DeviceState = combineState(leftSpring.endForce, rightSpring.endForce) { left, right -> - left + right - } + val force: DeviceState> = + combineState(leftSpring.tension, rightSpring.tension) { left: XYZ, right -> + -left - right + } val body = model( @@ -134,21 +81,23 @@ private class BodyOnSprings( mass = mass, force = force, position = position, + velocity = velocity ) ) } fun main() = application { - val initialState = XY(0.1, 0.2) + val initialState = XYZ(0.01, 0.1, 0) Window(title = "Ball on springs", onCloseRequest = ::exitApplication) { + window.minimumSize = Dimension(400, 400) MaterialTheme { val context = remember { Context("simulation") } val model = remember { - BodyOnSprings(context, 100.0, 1000.0, initialState) + BodyOnSprings(context, NumericalValue(10.0), 100.0, initialState) } //TODO add ability to freeze model @@ -159,24 +108,23 @@ fun main() = application { // }.collect() // } - val position: XY by model.body.position.asComposeState() - Box(Modifier.size(400.dp)) { - Canvas(modifier = Modifier.fillMaxSize()) { - fun XY.toOffset() = Offset( - center.x + (x / model.width * size.width).toFloat(), - center.y - (y / model.height * size.height).toFloat() - ) + val position: XYZ by model.body.position.asComposeState() + Canvas(modifier = Modifier.fillMaxSize()) { + fun XYZ.toOffset() = Offset( + ((x.value - model.xLeft) / model.width * size.width).toFloat(), + ((y.value - model.yBottom) / model.height * size.height).toFloat() - drawCircle( - Color.Red, 10f, center = position.toOffset() - ) - drawLine(Color.Blue, model.leftSpring.begin.value.toOffset(), model.leftSpring.end.value.toOffset()) - drawLine( - Color.Blue, - model.rightSpring.begin.value.toOffset(), - model.rightSpring.end.value.toOffset() - ) - } + ) + + drawCircle( + Color.Red, 10f, center = position.toOffset() + ) + drawLine(Color.Blue, model.leftSpring.begin.value.toOffset(), model.leftSpring.end.value.toOffset()) + drawLine( + Color.Blue, + model.rightSpring.begin.value.toOffset(), + model.rightSpring.end.value.toOffset() + ) } } }