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.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<U : UnitsOfMeasurement, V : UnitsOfMeasurement>(
inertia: Double,
public val position: MutableDeviceState<NumericalValue<U>>,
public val velocity: MutableDeviceState<NumericalValue<V>>,
timerPrecision: Duration = 10.milliseconds,
) : ModelConstructor(context) {
init {
@ -27,7 +24,7 @@ public class Inertia<U : UnitsOfMeasurement, V : UnitsOfMeasurement>(
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

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 =
value / other.value
public operator fun <U: UnitsOfMeasurement> NumericalValue<U>.unaryMinus(): NumericalValue<U> = NumericalValue(-value)
private object NumericalValueMetaConverter : MetaConverter<NumericalValue<*>> {
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
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<XY>,
val end: DeviceState<XY>,
val l0: NumericalValue<Meters>,
val begin: DeviceState<XYZ<Meters>>,
val end: DeviceState<XYZ<Meters>>,
) : 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<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
val tension: DeviceState<XYZ<Newtons>> = combineState(begin, end) { begin: XYZ<Meters>, end: XYZ<Meters> ->
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<Kilograms>,
k: Double,
startPosition: XY,
l0: Double = 1.0,
startPosition: XYZ<Meters>,
l0: NumericalValue<Meters> = 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<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(
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(
Spring(context, k, l0, rightAnchor, position)
)
val force: DeviceState<XY> = combineState(leftSpring.endForce, rightSpring.endForce) { left, right ->
left + right
}
val force: DeviceState<XYZ<Newtons>> =
combineState(leftSpring.tension, rightSpring.tension) { left: XYZ<Newtons>, 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<Meters>(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<Meters> by model.body.position.asComposeState()
Canvas(modifier = Modifier.fillMaxSize()) {
fun XYZ<Meters>.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()
)
}
}
}