Fix ball on springs demo

This commit is contained in:
Alexander Nozik 2024-06-03 18:02:57 +03:00
parent 4a5f5fab8c
commit 9f21a14f96
6 changed files with 122 additions and 105 deletions

View File

@ -4,8 +4,6 @@ import space.kscience.controls.constructor.*
import space.kscience.controls.constructor.units.* import space.kscience.controls.constructor.units.*
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
import kotlin.math.pow import kotlin.math.pow
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.DurationUnit import kotlin.time.DurationUnit
/** /**
@ -17,7 +15,6 @@ public class Inertia<U : UnitsOfMeasurement, V : UnitsOfMeasurement>(
inertia: Double, inertia: Double,
public val position: MutableDeviceState<NumericalValue<U>>, public val position: MutableDeviceState<NumericalValue<U>>,
public val velocity: MutableDeviceState<NumericalValue<V>>, public val velocity: MutableDeviceState<NumericalValue<V>>,
timerPrecision: Duration = 10.milliseconds,
) : ModelConstructor(context) { ) : ModelConstructor(context) {
init { init {
@ -27,7 +24,7 @@ public class Inertia<U : UnitsOfMeasurement, V : UnitsOfMeasurement>(
private var currentForce = force.value 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) val dtSeconds = (next - prev).toDouble(DurationUnit.SECONDS)
// compute new value based on velocity and acceleration from the previous step // compute new value based on velocity and acceleration from the previous step

View File

@ -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<XYZ<Newtons>>,
mass: NumericalValue<Kilograms>,
public val position: MutableDeviceState<XYZ<Meters>>,
public val velocity: MutableDeviceState<XYZ<MetersPerSecond>>,
) : 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
}
}

View File

@ -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<U: UnitsOfMeasurement>(val x: NumericalValue<U>, val y: NumericalValue<U>)
public data class XYZ<U: UnitsOfMeasurement>(val x: NumericalValue<U>, val y: NumericalValue<U>, val z: NumericalValue<U>)

View File

@ -46,6 +46,8 @@ public operator fun <U : UnitsOfMeasurement> NumericalValue<U>.div(
public operator fun <U : UnitsOfMeasurement> NumericalValue<U>.div(other: NumericalValue<U>): Double = public operator fun <U : UnitsOfMeasurement> NumericalValue<U>.div(other: NumericalValue<U>): Double =
value / other.value value / other.value
public operator fun <U: UnitsOfMeasurement> NumericalValue<U>.unaryMinus(): NumericalValue<U> = NumericalValue(-value)
private object NumericalValueMetaConverter : MetaConverter<NumericalValue<*>> { private object NumericalValueMetaConverter : MetaConverter<NumericalValue<*>> {
override fun convert(obj: NumericalValue<*>): Meta = Meta(obj.value) override fun convert(obj: NumericalValue<*>): Meta = Meta(obj.value)

View File

@ -0,0 +1,36 @@
package space.kscience.controls.constructor.units
public data class XY<U : UnitsOfMeasurement>(val x: NumericalValue<U>, val y: NumericalValue<U>)
public fun <U : UnitsOfMeasurement> XY(x: Number, y: Number): XY<U> = XY(NumericalValue(x), NumericalValue((y)))
public operator fun <U : UnitsOfMeasurement> XY<U>.plus(other: XY<U>): XY<U> =
XY(x + other.x, y + other.y)
public operator fun <U : UnitsOfMeasurement> XY<U>.times(c: Number): XY<U> = XY(x * c, y * c)
public operator fun <U : UnitsOfMeasurement> XY<U>.div(c: Number): XY<U> = XY(x / c, y / c)
public operator fun <U : UnitsOfMeasurement> XY<U>.unaryMinus(): XY<U> = XY(-x, -y)
public data class XYZ<U : UnitsOfMeasurement>(
val x: NumericalValue<U>,
val y: NumericalValue<U>,
val z: NumericalValue<U>,
)
public fun <U : UnitsOfMeasurement> XYZ(x: Number, y: Number, z: Number): XYZ<U> =
XYZ(NumericalValue(x), NumericalValue((y)), NumericalValue(z))
@Suppress("UNCHECKED_CAST", "UNUSED_PARAMETER")
public fun <U : UnitsOfMeasurement, R : UnitsOfMeasurement> XYZ<U>.cast(units: R): XYZ<R> = this as XYZ<R>
public operator fun <U : UnitsOfMeasurement> XYZ<U>.plus(other: XYZ<U>): XYZ<U> =
XYZ(x + other.x, y + other.y, z + other.z)
public operator fun <U : UnitsOfMeasurement> XYZ<U>.minus(other: XYZ<U>): XYZ<U> =
XYZ(x - other.x, y - other.y, z - other.z)
public operator fun <U : UnitsOfMeasurement> XYZ<U>.times(c: Number): XYZ<U> = XYZ(x * c, y * c, z * c)
public operator fun <U : UnitsOfMeasurement> XYZ<U>.div(c: Number): XYZ<U> = XYZ(x / c, y / c, z / c)
public operator fun <U : UnitsOfMeasurement> XYZ<U>.unaryMinus(): XYZ<U> = XYZ(-x, -y, -z)

View File

@ -1,105 +1,50 @@
package space.kscience.controls.demo.constructor package space.kscience.controls.demo.constructor
import androidx.compose.foundation.Canvas import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application import androidx.compose.ui.window.application
import kotlinx.serialization.Serializable
import space.kscience.controls.compose.asComposeState import space.kscience.controls.compose.asComposeState
import space.kscience.controls.constructor.* 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 space.kscience.dataforge.context.Context
import java.awt.Dimension
import kotlin.math.pow import kotlin.math.pow
import kotlin.math.sqrt 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( private class Spring(
context: Context, context: Context,
val k: Double, val k: Double,
val l0: Double, val l0: NumericalValue<Meters>,
val begin: DeviceState<XY>, val begin: DeviceState<XYZ<Meters>>,
val end: DeviceState<XY>, val end: DeviceState<XYZ<Meters>>,
) : ModelConstructor(context) { ) : ModelConstructor(context) {
/** /**
* vector from start to end * Tension at the beginning point
*/ */
val direction = combineState(begin, end) { begin: XY, end: XY -> val tension: DeviceState<XYZ<Newtons>> = combineState(begin, end) { begin: XYZ<Meters>, end: XYZ<Meters> ->
val dx = end.x - begin.x val delta = end - begin
val dy = end.y - begin.y val l = sqrt(delta.x.value.pow(2) + delta.y.value.pow(2) + delta.z.value.pow(2))
val l = sqrt(dx.pow(2) + dy.pow(2)) ((delta / l) * k * (l - l0.value)).cast(Newtons)
XY(dx / l, dy / l)
}
val tension: DeviceState<Double> = 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<XY>,
val position: MutableDeviceState<XY>,
val velocity: MutableDeviceState<XY> = 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
} }
} }
private class BodyOnSprings( private class BodyOnSprings(
context: Context, context: Context,
mass: Double, mass: NumericalValue<Kilograms>,
k: Double, k: Double,
startPosition: XY, startPosition: XYZ<Meters>,
l0: Double = 1.0, l0: NumericalValue<Meters> = NumericalValue(1.0),
val xLeft: Double = -1.0, val xLeft: Double = -1.0,
val xRight: Double = 1.0, val xRight: Double = 1.0,
val yBottom: Double = -1.0, val yBottom: Double = -1.0,
@ -110,21 +55,23 @@ private class BodyOnSprings(
val height = yTop - yBottom val height = yTop - yBottom
val position = stateOf(startPosition) val position = stateOf(startPosition)
val velocity: MutableDeviceState<XYZ<MetersPerSecond>> = stateOf(XYZ(0, 0, 0))
private val leftAnchor = stateOf(XY(xLeft, (yTop + yBottom) / 2)) private val leftAnchor = stateOf(XYZ<Meters>(xLeft, (yTop + yBottom) / 2, 0.0))
val leftSpring = model( val leftSpring = model(
Spring(context, k, l0, leftAnchor, position) Spring(context, k, l0, leftAnchor, position)
) )
private val rightAnchor = stateOf(XY(xRight, (yTop + yBottom) / 2)) private val rightAnchor = stateOf(XYZ<Meters>(xRight, (yTop + yBottom) / 2, 0.0))
val rightSpring = model( val rightSpring = model(
Spring(context, k, l0, rightAnchor, position) Spring(context, k, l0, rightAnchor, position)
) )
val force: DeviceState<XY> = combineState(leftSpring.endForce, rightSpring.endForce) { left, right -> val force: DeviceState<XYZ<Newtons>> =
left + right combineState(leftSpring.tension, rightSpring.tension) { left: XYZ<Newtons>, right ->
-left - right
} }
@ -134,21 +81,23 @@ private class BodyOnSprings(
mass = mass, mass = mass,
force = force, force = force,
position = position, position = position,
velocity = velocity
) )
) )
} }
fun main() = application { fun main() = application {
val initialState = XY(0.1, 0.2) val initialState = XYZ<Meters>(0.01, 0.1, 0)
Window(title = "Ball on springs", onCloseRequest = ::exitApplication) { Window(title = "Ball on springs", onCloseRequest = ::exitApplication) {
window.minimumSize = Dimension(400, 400)
MaterialTheme { MaterialTheme {
val context = remember { val context = remember {
Context("simulation") Context("simulation")
} }
val model = remember { val model = remember {
BodyOnSprings(context, 100.0, 1000.0, initialState) BodyOnSprings(context, NumericalValue(10.0), 100.0, initialState)
} }
//TODO add ability to freeze model //TODO add ability to freeze model
@ -159,12 +108,12 @@ fun main() = application {
// }.collect() // }.collect()
// } // }
val position: XY by model.body.position.asComposeState() val position: XYZ<Meters> by model.body.position.asComposeState()
Box(Modifier.size(400.dp)) {
Canvas(modifier = Modifier.fillMaxSize()) { Canvas(modifier = Modifier.fillMaxSize()) {
fun XY.toOffset() = Offset( fun XYZ<Meters>.toOffset() = Offset(
center.x + (x / model.width * size.width).toFloat(), ((x.value - model.xLeft) / model.width * size.width).toFloat(),
center.y - (y / model.height * size.height).toFloat() ((y.value - model.yBottom) / model.height * size.height).toFloat()
) )
drawCircle( drawCircle(
@ -180,4 +129,3 @@ fun main() = application {
} }
} }
} }
}