WIP: Простая конфигурация устройств через спецификации #12

Closed
kolpakov.mm wants to merge 2 commits from dev-maxim into dev
73 changed files with 1817 additions and 2458 deletions
Showing only changes of commit b6f7963f68 - Show all commits

View File

@ -23,6 +23,7 @@ kscience{
commonTest{
implementation(spclibs.logback.classic)
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0")
}
}

View File

@ -74,6 +74,18 @@ public interface StateContainer : ContextAware, CoroutineScope {
}.launchIn(this@StateContainer).also {
registerElement(ConnectionConstrucorElement(reads + this, writes))
}
/**
* Create and register a derived state.
*/
public fun <T> derivedState(
Review

What is the reason for this method in the state container? Why computeValue does not take parameters?

What is the reason for this method in the state container? Why `computeValue` does not take parameters?
dependencies: List<DeviceState<*>>,
computeValue: () -> T,
Review

Should be suspended

Should be suspended
): DeviceStateWithDependencies<T> {
val state = DeviceState.derived(this, dependencies, computeValue)
registerElement(StateConstructorElement(state))
return state
}
}
/**
@ -210,7 +222,7 @@ public fun <T1, T2, R> StateContainer.combineTo(
): Job {
val descriptor = ConnectionConstrucorElement(setOf(sourceState1, sourceState2), setOf(targetState))
registerElement(descriptor)
return kotlinx.coroutines.flow.combine(sourceState1.valueFlow, sourceState2.valueFlow, transformation).onEach {
return combine(sourceState1.valueFlow, sourceState2.valueFlow, transformation).onEach {
targetState.value = it
}.launchIn(this).apply {
invokeOnCompletion {
@ -231,7 +243,7 @@ public inline fun <reified T, R> StateContainer.combineTo(
): Job {
val descriptor = ConnectionConstrucorElement(sourceStates, setOf(targetState))
registerElement(descriptor)
return kotlinx.coroutines.flow.combine(sourceStates.map { it.valueFlow }, transformation).onEach {
return combine(sourceStates.map { it.valueFlow }, transformation).onEach {
targetState.value = it
}.launchIn(this).apply {
invokeOnCompletion {

View File

@ -2,10 +2,7 @@ package space.kscience.controls.constructor
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.*
import space.kscience.controls.constructor.units.NumericalValue
import space.kscience.controls.constructor.units.UnitsOfMeasurement
import kotlin.reflect.KProperty
@ -44,18 +41,43 @@ public operator fun <T> MutableDeviceState<T>.setValue(thisRef: Any?, property:
}
/**
* Device state with a value that depends on other device states
* DeviceState with dependencies on other DeviceStates.
*/
public interface DeviceStateWithDependencies<T> : DeviceState<T> {
public val dependencies: Collection<DeviceState<*>>
}
/**
* Extension function to create a DerivedDeviceState.
*/
public fun <T> DeviceState.Companion.derived(
scope: CoroutineScope,
dependencies: List<DeviceState<*>>,
computeValue: () -> T
Review

Should use suspended function

Should use suspended function
): DeviceStateWithDependencies<T> = DerivedDeviceState(scope, dependencies, computeValue)
public fun <T> DeviceState<T>.withDependencies(
dependencies: Collection<DeviceState<*>>,
): DeviceStateWithDependencies<T> = object : DeviceStateWithDependencies<T>, DeviceState<T> by this {
override val dependencies: Collection<DeviceState<*>> = dependencies
}
public fun <T> DeviceState.Companion.fromFlow(
Review

Should be documented

Should be documented
scope: CoroutineScope,
flow: Flow<T>,
initialValue: T
): DeviceState<T> {
val stateFlow = flow.stateIn(scope, SharingStarted.Eagerly, initialValue)
return object : DeviceState<T> {
override val value: T get() = stateFlow.value
override val valueFlow: Flow<T> get() = stateFlow
override fun toString(): String {
return "DeviceState.fromFlow(scope=$scope, flow=$flow, initialValue=$initialValue)"
}
}
}
/**
* Create a new read-only [DeviceState] that mirrors receiver state by mapping the value with [mapper].
*/
@ -100,4 +122,28 @@ public fun <T1, T2, R> DeviceState.Companion.combine(
override val valueFlow: Flow<R> = kotlinx.coroutines.flow.combine(state1.valueFlow, state2.valueFlow, mapper)
override fun toString(): String = "DeviceState.combine(state1=$state1, state2=$state2)"
}
/**
* A DeviceState that derives its value from other DeviceStates.
*/
public class DerivedDeviceState<T>(
scope: CoroutineScope,
override val dependencies: List<DeviceState<*>>,
computeValue: () -> T
) : DeviceStateWithDependencies<T> {
private val _valueFlow: StateFlow<T>
init {
Review

Don't assign values in init. Use fake constructor if needed

Don't assign values in init. Use fake constructor if needed
val flows = dependencies.map { it.valueFlow }
_valueFlow = combine(flows) {
computeValue()
}.stateIn(scope, SharingStarted.Eagerly, computeValue())
}
override val value: T get() = _valueFlow.value
override val valueFlow: Flow<T> get() = _valueFlow
override fun toString(): String {
TODO("Not yet implemented")
}
}

View File

@ -3,7 +3,7 @@ package space.kscience.controls.constructor.devices
import space.kscience.controls.constructor.DeviceConstructor
import space.kscience.controls.constructor.MutableDeviceState
import space.kscience.controls.constructor.property
import space.kscience.controls.constructor.units.NewtonsMeters
import space.kscience.controls.constructor.units.NewtonMeters
import space.kscience.controls.constructor.units.NumericalValue
import space.kscience.controls.constructor.units.numerical
import space.kscience.dataforge.context.Context
@ -13,7 +13,7 @@ import space.kscience.dataforge.meta.MetaConverter
public class Drive(
context: Context,
force: MutableDeviceState<NumericalValue<NewtonsMeters>> = MutableDeviceState(NumericalValue(0)),
force: MutableDeviceState<NumericalValue<NewtonMeters>> = MutableDeviceState(NumericalValue(0)),
) : DeviceConstructor(context) {
public val force: MutableDeviceState<NumericalValue<NewtonsMeters>> by property(MetaConverter.numerical(), force)
public val force: MutableDeviceState<NumericalValue<NewtonMeters>> by property(MetaConverter.numerical(), force)
}

View File

@ -4,7 +4,7 @@ import space.kscience.controls.constructor.*
import space.kscience.controls.constructor.models.PidParameters
import space.kscience.controls.constructor.models.PidRegulator
import space.kscience.controls.constructor.units.Meters
import space.kscience.controls.constructor.units.NewtonsMeters
import space.kscience.controls.constructor.units.NewtonMeters
import space.kscience.controls.constructor.units.NumericalValue
import space.kscience.controls.constructor.units.numerical
import space.kscience.dataforge.context.Context
@ -24,7 +24,7 @@ public class LinearDrive(
public val position: DeviceState<NumericalValue<Meters>> by property(MetaConverter.numerical(), position)
public val drive: Drive by device(drive)
public val pid: PidRegulator<Meters, NewtonsMeters> = model(
public val pid: PidRegulator<Meters, NewtonMeters> = model(
PidRegulator(
context = context,
position = position,

View File

@ -1,9 +0,0 @@
package space.kscience.controls.constructor.dsl.core
/**
* Класс для представления аннотации.
*/
public data class Annotation(
val name: String,
val properties: Map<String, Any>
)

View File

@ -1,196 +0,0 @@
package space.kscience.controls.constructor.dsl.core
import space.kscience.controls.constructor.dsl.core.equations.*
import space.kscience.controls.constructor.dsl.core.expressions.*
import space.kscience.controls.constructor.dsl.core.variables.ArrayVariable
import space.kscience.controls.constructor.dsl.core.variables.Variable
/**
* Класс для хранения и обработки системы уравнений.
*/
public class EquationSystem {
public val equations: MutableList<EquationBase> = mutableListOf()
public val variables: MutableSet<String> = mutableSetOf()
public val derivativeVariables: MutableSet<String> = mutableSetOf()
public val algebraicEquations: MutableList<Equation> = mutableListOf()
public val initialEquations: MutableList<EquationBase> = mutableListOf()
public val conditionalEquations: MutableList<ConditionalEquation> = mutableListOf()
/**
* Метод для добавления списка уравнений.
*/
public fun addEquations(eqs: List<EquationBase>) {
for (eq in eqs) {
addEquation(eq)
}
}
/**
* Метод для добавления одного уравнения.
*/
public fun addEquation(eq: EquationBase) {
equations.add(eq)
collectVariables(eq)
}
private fun collectVariables(eq: EquationBase) {
when (eq) {
is Equation -> {
collectVariablesFromExpression(eq.leftExpression)
collectVariablesFromExpression(eq.rightExpression)
// Classify the equation
if (isDifferentialEquation(eq)) {
// Handled in toODESystem()
} else {
algebraicEquations.add(eq)
}
}
is ConditionalEquation -> {
collectVariablesFromExpression(eq.condition)
collectVariables(eq.trueEquation)
eq.falseEquation?.let { collectVariables(it) }
conditionalEquations.add(eq)
}
is InitialEquation -> {
collectVariablesFromExpression(eq.leftExpression)
collectVariablesFromExpression(eq.rightExpression)
initialEquations.add(eq)
}
else -> {
throw UnsupportedOperationException("Unknown equation type: ${eq::class}")
}
}
}
private fun collectVariablesFromExpression(expr: Expression) {
when (expr) {
is VariableExpression -> variables.add(expr.variable.name)
is ConstantExpression -> {
// Constants do not contain variables
}
is DerivativeExpression -> {
collectVariablesFromExpression(expr.variable)
derivativeVariables.add(expr.variable.variable.name)
}
is PartialDerivativeExpression -> {
collectVariablesFromExpression(expr.variable)
collectVariablesFromExpression(expr.respectTo)
// For partial derivatives, variables may need special handling
}
is BinaryExpression -> {
collectVariablesFromExpression(expr.left)
collectVariablesFromExpression(expr.right)
}
is FunctionCallExpression -> {
expr.arguments.forEach { collectVariablesFromExpression(it) }
}
is ArrayVariableExpression -> {
collectVariablesFromArrayVariable(expr.left)
collectVariablesFromArrayVariable(expr.right)
}
is ArrayScalarExpression -> {
collectVariablesFromArrayVariable(expr.array)
collectVariablesFromExpression(expr.scalar)
}
else -> {
throw UnsupportedOperationException("Unknown expression type: ${expr::class}")
}
}
}
private fun collectVariablesFromArrayVariable(arrayVar: ArrayVariable) {
variables.add(arrayVar.name)
// Additional processing if necessary
}
private fun isDifferentialEquation(eq: Equation): Boolean {
val leftIsDerivative = containsDerivative(eq.leftExpression)
val rightIsDerivative = containsDerivative(eq.rightExpression)
return leftIsDerivative || rightIsDerivative
}
private fun containsDerivative(expr: Expression): Boolean {
return when (expr) {
is DerivativeExpression -> true
is BinaryExpression -> containsDerivative(expr.left) || containsDerivative(expr.right)
is FunctionCallExpression -> expr.arguments.any { containsDerivative(it) }
else -> false
}
}
/**
* Подготовка системы ОДУ для численного решения.
*/
public fun toODESystem(): ODESystem {
val odeEquations = mutableListOf<ODEEquation>()
// Process equations to separate ODEs and algebraic equations
for (eqBase in equations) {
when (eqBase) {
is Equation -> {
val eq = eqBase
if (isDifferentialEquation(eq)) {
// Handle differential equations
processDifferentialEquation(eq, odeEquations)
} else {
// Handle algebraic equations
algebraicEquations.add(eq)
}
}
is ConditionalEquation -> {
// Handle conditional equations
// For ODE system, we might need to process them differently
conditionalEquations.add(eqBase)
}
is InitialEquation -> {
// Initial equations are used for setting initial conditions
initialEquations.add(eqBase)
}
else -> {
throw UnsupportedOperationException("Unknown equation type: ${eqBase::class}")
}
}
}
return ODESystem(odeEquations, algebraicEquations, initialEquations, conditionalEquations)
}
private fun processDifferentialEquation(eq: Equation, odeEquations: MutableList<ODEEquation>) {
// Identify which side has the derivative
val derivativeExpr = findDerivativeExpression(eq.leftExpression) ?: findDerivativeExpression(eq.rightExpression)
if (derivativeExpr != null) {
val variableName = derivativeExpr.variable.variable.name
if (derivativeExpr.order != 1) {
throw UnsupportedOperationException("Only first-order derivatives are supported in this solver.")
}
val rhs = if (eq.leftExpression is DerivativeExpression) eq.rightExpression else eq.leftExpression
odeEquations.add(ODEEquation(variableName, rhs))
derivativeVariables.add(variableName)
} else {
throw UnsupportedOperationException("Equation is marked as differential but no derivative found.")
}
}
private fun findDerivativeExpression(expr: Expression): DerivativeExpression? {
return when (expr) {
is DerivativeExpression -> expr
is BinaryExpression -> findDerivativeExpression(expr.left) ?: findDerivativeExpression(expr.right)
is FunctionCallExpression -> expr.arguments.mapNotNull { findDerivativeExpression(it) }.firstOrNull()
else -> null
}
}
}
public data class ODEEquation(
val variableName: String,
val rhs: Expression
)
public class ODESystem(
public val odeEquations: List<ODEEquation>,
public val algebraicEquations: List<Equation>,
public val initialEquations: List<EquationBase>,
public val conditionalEquations: List<ConditionalEquation>
)

View File

@ -1,19 +0,0 @@
package space.kscience.controls.constructor.dsl.core
import space.kscience.controls.constructor.dsl.core.units.PhysicalUnit
import space.kscience.controls.constructor.dsl.core.variables.Variable
/**
* Класс для представления записи (record).
*/
public class Record(public val name: String) {
public val fields: MutableMap<String, Variable> = mutableMapOf()
public fun field(name: String, unit: PhysicalUnit) {
if (fields.containsKey(name)) {
throw IllegalArgumentException("Field with name $name already exists in record $name.")
}
fields[name] = Variable(name, unit)
}
}

View File

@ -1,37 +0,0 @@
package space.kscience.controls.constructor.dsl.core.algorithms
import space.kscience.controls.constructor.dsl.core.expressions.Expression
import space.kscience.controls.constructor.dsl.core.variables.Variable
/**
* Класс для построения алгоритмов.
*/
public class AlgorithmBuilder {
public val statements: MutableList<Statement> = mutableListOf()
public fun assign(variable: Variable, expression: Expression) {
statements.add(AssignmentStatement(variable, expression))
}
public fun ifStatement(condition: Expression, init: AlgorithmBuilder.() -> Unit) {
val trueBuilder = AlgorithmBuilder()
trueBuilder.init()
statements.add(IfStatement(condition, trueBuilder.build(), null))
}
public fun forStatement(variable: Variable, range: IntRange, init: AlgorithmBuilder.() -> Unit) {
val bodyBuilder = AlgorithmBuilder()
bodyBuilder.init()
statements.add(ForStatement(variable, range, bodyBuilder.build()))
}
public fun whileStatement(condition: Expression, init: AlgorithmBuilder.() -> Unit) {
val bodyBuilder = AlgorithmBuilder()
bodyBuilder.init()
statements.add(WhileStatement(condition, bodyBuilder.build()))
}
public fun build(): Algorithm {
return Algorithm(statements.toList())
}
}

View File

@ -1,80 +0,0 @@
package space.kscience.controls.constructor.dsl.core.algorithms
import space.kscience.controls.constructor.dsl.core.expressions.Expression
import space.kscience.controls.constructor.dsl.core.simulation.SimulationContext
import space.kscience.controls.constructor.dsl.core.variables.Variable
/**
* Абстрактный класс для операторов в алгоритмической секции.
*/
public sealed class Statement {
public abstract suspend fun execute(context: SimulationContext)
}
public data class AssignmentStatement(
val variable: Variable,
val expression: Expression
) : Statement() {
override suspend fun execute(context: SimulationContext) {
val value = expression.evaluate(context.getVariableValues())
context.updateVariable(variable.name, value)
}
}
public data class IfStatement(
val condition: Expression,
val trueBranch: Algorithm,
val falseBranch: Algorithm? = null
) : Statement() {
override suspend fun execute(context: SimulationContext) {
val conditionValue = condition.evaluate(context.getVariableValues())
if (conditionValue != 0.0) {
trueBranch.execute(context)
} else {
falseBranch?.execute(context)
}
}
}
public data class ForStatement(
val variable: Variable,
val range: IntRange,
val body: Algorithm
) : Statement() {
override suspend fun execute(context: SimulationContext) {
for (i in range) {
context.updateVariable(variable.name, i.toDouble())
body.execute(context)
}
}
}
public data class WhileStatement(
val condition: Expression,
val body: Algorithm
) : Statement() {
override suspend fun execute(context: SimulationContext) {
while (condition.evaluate(context.getVariableValues()) != 0.0) {
body.execute(context)
}
}
}
public class Algorithm(private val statements: List<Statement>) {
/**
* Добавляет оператор в алгоритм.
*/
public fun addStatement(statement: Statement) {
(statements as MutableList).add(statement)
}
/**
* Выполняет алгоритм в заданном контексте симуляции.
*/
public suspend fun execute(context: SimulationContext) {
for (statement in statements) {
statement.execute(context)
}
}
}

View File

@ -1,81 +0,0 @@
package space.kscience.controls.constructor.dsl.core.components
import space.kscience.controls.constructor.dsl.core.Annotation
import space.kscience.controls.constructor.dsl.core.equations.Equation
import space.kscience.controls.constructor.dsl.core.variables.ParameterVariable
import space.kscience.controls.constructor.dsl.core.variables.Variable
/**
* Базовый класс для всех компонентов модели.
*/
public abstract class Component<V : Variable, P : ParameterVariable, C : Connector>(public val name: String) {
public val variables: MutableMap<String, V> = mutableMapOf()
public val parameters: MutableMap<String, P> = mutableMapOf()
public val equations: MutableList<Equation> = mutableListOf()
public val connectors: MutableMap<String, C> = mutableMapOf()
public val annotations: MutableList<Annotation> = mutableListOf()
public var isReplaceable: Boolean = false
/**
* Метод для инициализации переменных.
*/
protected abstract fun initializeVariables()
/**
* Метод для определения уравнений компонента.
*/
protected abstract fun defineEquations()
/**
* Метод для добавления переменной.
*/
public fun addVariable(variable: V) {
if (variables.containsKey(variable.name)) {
throw IllegalArgumentException("Variable with name ${variable.name} already exists in component $name.")
}
variables[variable.name] = variable
}
/**
* Метод для добавления параметра.
*/
public fun addParameter(parameter: P) {
if (parameters.containsKey(parameter.name)) {
throw IllegalArgumentException("Parameter with name ${parameter.name} already exists in component $name.")
}
parameters[parameter.name] = parameter
}
/**
* Метод для добавления уравнения.
*/
public fun addEquation(equation: Equation) {
equations.add(equation)
}
/**
* Метод для добавления коннектора.
*/
public fun addConnector(name: String, connector: C) {
if (connectors.containsKey(name)) {
throw IllegalArgumentException("Connector with name $name already exists in component $name.")
}
connectors[name] = connector
}
public fun annotate(annotation: Annotation) {
annotations.add(annotation)
}
/**
* Метод для модификации параметров компонента.
*/
public fun modify(modifier: Component<V, P, C>.() -> Unit): Component<V, P, C> {
this.apply(modifier)
return this
}
public fun markAsReplaceable(isReplaceable: Boolean) {
this.isReplaceable = isReplaceable
}
}

View File

@ -1,38 +0,0 @@
package space.kscience.controls.constructor.dsl.core.components
import space.kscience.controls.constructor.dsl.core.equations.Equation
import space.kscience.controls.constructor.dsl.core.expressions.VariableExpression
import space.kscience.controls.constructor.dsl.core.variables.Variable
/**
* Класс для представления соединения между двумя коннекторами.
*/
public class Connection<C : Connector>(
public val connector1: C,
public val connector2: C
) {
init {
if (connector1.unit != connector2.unit) {
throw IllegalArgumentException("Cannot connect connectors with different units: ${connector1.unit} and ${connector2.unit}.")
}
}
/**
* Генерация уравнений для соединения.
*/
public fun generateEquations(): List<Equation> {
val equations = mutableListOf<Equation>()
for ((varName, var1) in connector1.variables) {
val var2 = connector2.variables[varName]
?: throw IllegalArgumentException("Variable $varName not found in connector ${connector2.name}.")
equations.add(
Equation(
VariableExpression(var1),
VariableExpression(var2)
)
)
}
return equations
}
}

View File

@ -1,22 +0,0 @@
package space.kscience.controls.constructor.dsl.core.components
import space.kscience.controls.constructor.dsl.core.units.PhysicalUnit
import space.kscience.controls.constructor.dsl.core.variables.Variable
/**
* Интерфейс для коннектора, позволяющего соединять компоненты.
*/
public interface Connector {
public val name: String
public val variables: Map<String, Variable>
public val unit: PhysicalUnit
}
/**
* Базовая реализация коннектора.
*/
public class BasicConnector(
override val name: String,
override val unit: PhysicalUnit,
override val variables: Map<String, Variable>
) : Connector

View File

@ -1,163 +0,0 @@
package space.kscience.controls.constructor.dsl.core.components
import space.kscience.controls.constructor.dsl.core.*
import space.kscience.controls.constructor.dsl.core.algorithms.*
import space.kscience.controls.constructor.dsl.core.equations.Equation
import space.kscience.controls.constructor.dsl.core.equations.ConditionalEquation
import space.kscience.controls.constructor.dsl.core.expressions.Expression
import space.kscience.controls.constructor.dsl.core.expressions.VariableExpression
import space.kscience.controls.constructor.dsl.core.units.PhysicalUnit
import space.kscience.controls.constructor.dsl.core.variables.ParameterVariable
import space.kscience.controls.constructor.dsl.core.variables.Variable
import space.kscience.controls.constructor.dsl.core.functions.Function
import space.kscience.controls.constructor.dsl.core.functions.FunctionBuilder
/**
* Класс для представления модели, содержащей компоненты и соединения.
*/
public class Model(public val name: String, init: Model.() -> Unit) {
public val components: MutableMap<String, Component<*, *, *>> = mutableMapOf<String, Component<*, *, *>>()
public val connections: MutableList<Connection<*>> = mutableListOf<Connection<*>>()
public val variables: MutableMap<String, Variable> = mutableMapOf<String, Variable>()
public val parameters: MutableMap<String, ParameterVariable> = mutableMapOf<String, ParameterVariable>()
public val equations: MutableList<Equation> = mutableListOf<Equation>()
public val initialValues: MutableMap<String, Double> = mutableMapOf<String, Double>()
public val initialEquations: MutableList<Equation> = mutableListOf<Equation>()
public val conditionalEquations: MutableList<ConditionalEquation> = mutableListOf<ConditionalEquation>()
public val algorithms: MutableList<Algorithm> = mutableListOf<Algorithm>()
public val functions: MutableMap<String, Function> = mutableMapOf<String, Function>()
init {
this.init()
}
/**
* Определение переменной.
*/
public fun variable(name: String, unit: PhysicalUnit): VariableExpression {
val variable = Variable(name, unit)
variables[name] = variable
return VariableExpression(variable)
}
/**
* Определение параметра.
*/
public fun parameter(name: String, unit: PhysicalUnit, value: Double): ParameterVariable {
val parameter = ParameterVariable(name, unit, value)
parameters[name] = parameter
return parameter
}
/**
* Добавление уравнения.
*/
public fun equation(equationBuilder: () -> Equation) {
val equation = equationBuilder()
equations.add(equation)
}
/**
* Установка начальных значений.
*/
public fun initialValues(init: MutableMap<String, Double>.() -> Unit) {
initialValues.apply(init)
}
/**
* Добавление инициализационного уравнения.
*/
public fun initialEquation(equationBuilder: () -> Equation) {
val equation = equationBuilder()
initialEquations.add(equation)
}
/**
* Метод для добавления компонента.
*/
public fun addComponent(component: Component<*, *, *>) {
if (components.containsKey(component.name)) {
throw IllegalArgumentException("Component with name ${component.name} already exists in model $name.")
}
components[component.name] = component
}
/**
* Метод для добавления соединения.
*/
public fun addConnection(connection: Connection<*>) {
connections.add(connection)
}
/**
* Метод для генерации системы уравнений модели.
*/
public fun generateEquationSystem(): EquationSystem {
val equationSystem = EquationSystem()
// Добавление уравнений модели
equationSystem.addEquations(equations)
// Добавление уравнений компонентов
for (component in components.values) {
equationSystem.addEquations(component.equations)
}
// Добавление уравнений соединений
for (connection in connections) {
equationSystem.addEquations(connection.generateEquations())
}
// Добавление условных уравнений
for (condEq in conditionalEquations) {
equationSystem.addEquation(condEq)
}
// Добавление инициализационных уравнений
equationSystem.addEquations(initialEquations)
return equationSystem
}
/**
* Метод для замены компонента.
*/
public fun replaceComponent(name: String, newComponent: Component<*, *, *>) {
val oldComponent = components[name]
if (oldComponent != null && oldComponent.isReplaceable) {
components[name] = newComponent
} else {
throw IllegalArgumentException("Component $name is not replaceable or does not exist.")
}
}
/**
* Добавление условного уравнения.
*/
public fun conditionalEquation(condition: Expression, trueEquation: Equation, falseEquation: Equation? = null) {
val condEq = ConditionalEquation(condition, trueEquation, falseEquation)
conditionalEquations.add(condEq)
}
/**
* Добавление алгоритма.
*/
public fun algorithm(init: AlgorithmBuilder.() -> Unit) {
val builder = AlgorithmBuilder()
builder.init()
algorithms.add(builder.build())
}
/**
* Добавление пользовательской функции.
*/
public fun function(name: String, init: FunctionBuilder.() -> Unit) {
if (functions.containsKey(name)) {
throw IllegalArgumentException("Function with name $name already exists in model $name.")
}
val builder = FunctionBuilder(name)
builder.init()
val function = builder.build()
functions[name] = function
}
}

View File

@ -1,21 +0,0 @@
package space.kscience.controls.constructor.dsl.core.components
/**
* Класс для представления пакета моделей.
*/
public class Package(public val name: String) {
public val models: MutableMap<String, Model> = mutableMapOf()
public val subPackages: MutableMap<String, Package> = mutableMapOf()
public fun addModel(model: Model) {
models[model.name] = model
}
public fun addPackage(pkg: Package) {
subPackages[pkg.name] = pkg
}
public fun getModel(name: String): Model? {
return models[name] ?: subPackages.values.mapNotNull { it.getModel(name) }.firstOrNull()
}
}

View File

@ -1,12 +0,0 @@
package space.kscience.controls.constructor.dsl.core.equations
import space.kscience.controls.constructor.dsl.core.expressions.Expression
/**
* Класс для условного уравнения.
*/
public class ConditionalEquation(
public val condition: Expression,
public val trueEquation: Equation,
public val falseEquation: Equation? = null
) : EquationBase()

View File

@ -1,24 +0,0 @@
package space.kscience.controls.constructor.dsl.core.equations
import space.kscience.controls.constructor.dsl.core.expressions.Expression
/**
* Класс для представления уравнения.
*/
public class Equation(
public val leftExpression: Expression,
public val rightExpression: Expression
) : EquationBase() {
init {
// Проверка совместимости единиц измерения
if (!leftExpression.unit.dimension.isCompatibleWith(rightExpression.unit.dimension)) {
throw IllegalArgumentException("Units of left and right expressions in the equation are not compatible.")
}
}
public fun evaluate(values: Map<String, Double>): Double {
val leftValue = leftExpression.evaluate(values)
val rightValue = rightExpression.evaluate(values)
return leftValue - rightValue
}
}

View File

@ -1,6 +0,0 @@
package space.kscience.controls.constructor.dsl.core.equations
/**
* Абстрактный базовый класс для уравнений.
*/
public sealed class EquationBase

View File

@ -1,11 +0,0 @@
package space.kscience.controls.constructor.dsl.core.equations
import space.kscience.controls.constructor.dsl.core.expressions.Expression
/**
* Class representing an initial equation for setting initial conditions.
*/
public class InitialEquation(
public val leftExpression: Expression,
public val rightExpression: Expression
) : EquationBase()

View File

@ -1,18 +0,0 @@
package space.kscience.controls.constructor.dsl.core.events
import space.kscience.controls.constructor.dsl.core.expressions.Expression
import space.kscience.controls.constructor.dsl.core.simulation.SimulationContext
import space.kscience.controls.constructor.dsl.core.variables.Variable
/**
* Действие присваивания нового значения переменной.
*/
public data class AssignAction(
val variable: Variable,
val newValue: Expression
) : EventAction() {
override suspend fun execute(context: SimulationContext) {
val value = newValue.evaluate(context.getVariableValues())
context.updateVariable(variable.name, value)
}
}

View File

@ -1,30 +0,0 @@
package space.kscience.controls.constructor.dsl.core.events
import space.kscience.controls.constructor.dsl.core.simulation.SimulationContext
import space.kscience.controls.constructor.dsl.core.expressions.Expression
/**
* Класс для представления события.
*/
public class Event(
public val condition: Expression,
public val actions: List<EventAction>
) {
/**
* Метод для проверки, произошло ли событие.
*/
public suspend fun checkEvent(context: SimulationContext): Boolean {
val values = context.getVariableValues()
val conditionValue = condition.evaluate(values)
return conditionValue > 0 // Событие происходит, когда условие положительно
}
/**
* Метод для выполнения действий при событии.
*/
public suspend fun executeActions(context: SimulationContext) {
for (action in actions) {
action.execute(context)
}
}
}

View File

@ -1,12 +0,0 @@
package space.kscience.controls.constructor.dsl.core.events
import space.kscience.controls.constructor.dsl.core.expressions.Expression
import space.kscience.controls.constructor.dsl.core.simulation.SimulationContext
import space.kscience.controls.constructor.dsl.core.variables.Variable
/**
* Абстрактный класс для действий при событии.
*/
public sealed class EventAction {
public abstract suspend fun execute(context: SimulationContext)
}

View File

@ -1,18 +0,0 @@
package space.kscience.controls.constructor.dsl.core.events
import space.kscience.controls.constructor.dsl.core.expressions.Expression
import space.kscience.controls.constructor.dsl.core.simulation.SimulationContext
import space.kscience.controls.constructor.dsl.core.variables.Variable
/**
* Действие реинициализации переменной.
*/
public data class ReinitAction(
val variable: Variable,
val newValue: Expression
) : EventAction() {
override suspend fun execute(context: SimulationContext) {
val value = newValue.evaluate(context.getVariableValues())
context.updateVariable(variable.name, value)
}
}

View File

@ -1,91 +0,0 @@
package space.kscience.controls.constructor.dsl.core.expression
import space.kscience.controls.constructor.dsl.core.variables.ArrayVariable
import space.kscience.controls.constructor.dsl.core.equations.Equation
import space.kscience.controls.constructor.dsl.core.expressions.ArrayBinaryExpression
import space.kscience.controls.constructor.dsl.core.expressions.ArrayBinaryOperator
import space.kscience.controls.constructor.dsl.core.expressions.ArrayScalarExpression
import space.kscience.controls.constructor.dsl.core.expressions.ArrayScalarOperator
import space.kscience.controls.constructor.dsl.core.expressions.BinaryExpression
import space.kscience.controls.constructor.dsl.core.expressions.BinaryOperator
import space.kscience.controls.constructor.dsl.core.expressions.ConstantExpression
import space.kscience.controls.constructor.dsl.core.expressions.Expression
import space.kscience.controls.constructor.dsl.core.expressions.FunctionCallExpression
import space.kscience.controls.constructor.dsl.core.units.Units
import space.kscience.controls.constructor.dsl.core.functions.Function
// Операторные функции для скалярных выражений
public operator fun Expression.plus(other: Expression): Expression {
return BinaryExpression(this, BinaryOperator.ADD, other)
}
public operator fun Expression.minus(other: Expression): Expression {
return BinaryExpression(this, BinaryOperator.SUBTRACT, other)
}
public operator fun Expression.times(other: Expression): Expression {
return BinaryExpression(this, BinaryOperator.MULTIPLY, other)
}
public operator fun Expression.div(other: Expression): Expression {
return BinaryExpression(this, BinaryOperator.DIVIDE, other)
}
public infix fun Expression.pow(exponent: Expression): Expression {
return FunctionCallExpression("pow", listOf(this, exponent))
}
public infix fun Expression.pow(exponent: Double): Expression {
return FunctionCallExpression("pow", listOf(this, ConstantExpression(exponent, Units.dimensionless)))
}
// Сравнительные операторы
public infix fun Expression.eq(other: Expression): Equation {
return Equation(this, other)
}
public infix fun Expression.gt(other: Expression): Expression {
return BinaryExpression(this, BinaryOperator.GREATER_THAN, other)
}
public infix fun Expression.lt(other: Expression): Expression {
return BinaryExpression(this, BinaryOperator.LESS_THAN, other)
}
public infix fun Expression.ge(other: Expression): Expression {
return BinaryExpression(this, BinaryOperator.GREATER_OR_EQUAL, other)
}
public infix fun Expression.le(other: Expression): Expression {
return BinaryExpression(this, BinaryOperator.LESS_OR_EQUAL, other)
}
// Операторные функции для массивов
public operator fun ArrayVariable.plus(other: ArrayVariable): Expression {
return ArrayBinaryExpression(this, ArrayBinaryOperator.ADD, other)
}
public operator fun ArrayVariable.minus(other: ArrayVariable): Expression {
return ArrayBinaryExpression(this, ArrayBinaryOperator.SUBTRACT, other)
}
public operator fun ArrayVariable.times(scalar: Expression): Expression {
return ArrayScalarExpression(this, ArrayScalarOperator.MULTIPLY, scalar)
}
public operator fun ArrayVariable.div(scalar: Expression): Expression {
return ArrayScalarExpression(this, ArrayScalarOperator.DIVIDE, scalar)
}
/**
* Функция для вызова пользовательской функции в выражении.
*/
public fun callFunction(function: Function, vararg args: Expression): FunctionCallExpression {
require(args.size == function.inputs.size) {
"Number of arguments does not match number of function inputs."
}
return FunctionCallExpression(function.name, args.toList())
}

View File

@ -1,37 +0,0 @@
package space.kscience.controls.constructor.dsl.core.expressions
import space.kscience.controls.constructor.dsl.core.units.PhysicalUnit
import space.kscience.controls.constructor.dsl.core.variables.ArrayVariable
/**
* Класс для представления бинарной операции между двумя массивами.
*/
public class ArrayBinaryExpression(
public val left: ArrayVariable,
public val operator: ArrayBinaryOperator,
public val right: ArrayVariable
) : Expression() {
override val unit: PhysicalUnit = when (operator) {
ArrayBinaryOperator.ADD, ArrayBinaryOperator.SUBTRACT -> {
if (left.unit != right.unit) {
throw IllegalArgumentException("Units must be the same for array addition or subtraction.")
}
left.unit
}
ArrayBinaryOperator.MULTIPLY -> left.unit * right.unit
ArrayBinaryOperator.DIVIDE -> left.unit / right.unit
}
override fun evaluate(values: Map<String, Double>): Double {
throw UnsupportedOperationException("Cannot evaluate array expression to a scalar value.")
}
override fun checkDimensions() {
TODO()
}
override fun derivative(variable: VariableExpression): Expression {
throw UnsupportedOperationException("Derivative of array expression is not implemented.")
}
}

View File

@ -1,11 +0,0 @@
package space.kscience.controls.constructor.dsl.core.expressions
/**
* Перечисление для бинарных операций над массивами.
*/
public enum class ArrayBinaryOperator {
ADD,
SUBTRACT,
MULTIPLY,
DIVIDE
}

View File

@ -1,3 +0,0 @@
package space.kscience.controls.constructor.dsl.core.expressions
public sealed class ArrayExpression : Expression()

View File

@ -1,34 +0,0 @@
package space.kscience.controls.constructor.dsl.core.expressions
import space.kscience.controls.constructor.dsl.core.units.PhysicalUnit
import space.kscience.controls.constructor.dsl.core.variables.ArrayVariable
/**
* Представляет операцию между массивом и скаляром.
*/
public class ArrayScalarExpression(
public val array: ArrayVariable,
public val operator: ArrayScalarOperator,
public val scalar: Expression
) : Expression() {
override val unit: PhysicalUnit = when (operator) {
ArrayScalarOperator.MULTIPLY -> array.unit * scalar.unit
ArrayScalarOperator.DIVIDE -> array.unit / scalar.unit
}
override fun evaluate(values: Map<String, Double>): Double {
// Невозможно вычислить массивное выражение до скалярного значения
throw UnsupportedOperationException("Cannot evaluate array expression to a scalar value.")
}
override fun checkDimensions() {
// array.checkDimensions()
scalar.checkDimensions()
// Единицы измерения уже проверены в свойстве `unit`
}
override fun derivative(variable: VariableExpression): Expression {
throw UnsupportedOperationException("Derivative of array expression is not implemented.")
}
}

View File

@ -1,9 +0,0 @@
package space.kscience.controls.constructor.dsl.core.expressions
/**
* Перечисление операторов между массивом и скаляром.
*/
public enum class ArrayScalarOperator {
MULTIPLY,
DIVIDE
}

View File

@ -1,42 +0,0 @@
package space.kscience.controls.constructor.dsl.core.expressions
import space.kscience.controls.constructor.dsl.core.expressions.Expression
import space.kscience.controls.constructor.dsl.core.units.PhysicalUnit
import space.kscience.controls.constructor.dsl.core.variables.ArrayVariable
/**
* Представляет операцию между двумя массивами.
*/
public class ArrayVariableExpression(
public val left: ArrayVariable,
public val operator: ArrayBinaryOperator,
public val right: ArrayVariable
) : Expression() {
override val unit: PhysicalUnit = when (operator) {
ArrayBinaryOperator.ADD, ArrayBinaryOperator.SUBTRACT -> {
if (left.unit != right.unit) {
throw IllegalArgumentException("Units must be the same for array addition or subtraction.")
}
left.unit
}
ArrayBinaryOperator.MULTIPLY -> left.unit * right.unit
ArrayBinaryOperator.DIVIDE -> left.unit / right.unit
}
override fun evaluate(values: Map<String, Double>): Double {
// Невозможно вычислить массивное выражение до скалярного значения
throw UnsupportedOperationException("Cannot evaluate array expression to a scalar value.")
}
override fun checkDimensions() {
// left.checkDimensions()
// right.checkDimensions()
// Единицы измерения уже проверены в свойстве `unit`
}
override fun derivative(variable: VariableExpression): Expression {
// Производная массива не реализована
throw UnsupportedOperationException("Derivative of array expression is not implemented.")
}
}

View File

@ -1,135 +0,0 @@
package space.kscience.controls.constructor.dsl.core.expressions
import space.kscience.controls.constructor.dsl.core.units.PhysicalUnit
import space.kscience.controls.constructor.dsl.core.units.Units
import kotlin.math.*
/**
* Перечисление типов бинарных операций.
*/
public enum class BinaryOperator {
ADD,
SUBTRACT,
MULTIPLY,
DIVIDE,
POWER,
GREATER_THAN,
LESS_THAN,
GREATER_OR_EQUAL,
LESS_OR_EQUAL
}
/**
* Класс для представления бинарной операции.
*/
public class BinaryExpression(
public val left: Expression,
public val operator: BinaryOperator,
public val right: Expression
) : Expression() {
override val unit: PhysicalUnit = when (operator) {
BinaryOperator.ADD, BinaryOperator.SUBTRACT -> {
if (left.unit != right.unit) {
throw IllegalArgumentException("Units of left and right expressions must be the same for addition or subtraction.")
}
left.unit
}
BinaryOperator.MULTIPLY -> left.unit * right.unit
BinaryOperator.DIVIDE -> left.unit / right.unit
BinaryOperator.POWER -> {
if (right is ConstantExpression) {
left.unit.pow(right.value)
} else {
throw IllegalArgumentException("Exponent must be a constant expression.")
}
}
BinaryOperator.GREATER_THAN, BinaryOperator.LESS_THAN,
BinaryOperator.GREATER_OR_EQUAL, BinaryOperator.LESS_OR_EQUAL -> {
Units.dimensionless
}
}
override fun evaluate(values: Map<String, Double>): Double {
val leftValue = left.evaluate(values)
val rightValue = right.evaluate(values)
return when (operator) {
BinaryOperator.ADD -> leftValue + rightValue
BinaryOperator.SUBTRACT -> leftValue - rightValue
BinaryOperator.MULTIPLY -> leftValue * rightValue
BinaryOperator.DIVIDE -> leftValue / rightValue
BinaryOperator.POWER -> leftValue.pow(rightValue)
BinaryOperator.GREATER_THAN -> if (leftValue > rightValue) 1.0 else 0.0
BinaryOperator.LESS_THAN -> if (leftValue < rightValue) 1.0 else 0.0
BinaryOperator.GREATER_OR_EQUAL -> if (leftValue >= rightValue) 1.0 else 0.0
BinaryOperator.LESS_OR_EQUAL -> if (leftValue <= rightValue) 1.0 else 0.0
}
}
override fun checkDimensions() {
left.checkDimensions()
right.checkDimensions()
when (operator) {
BinaryOperator.ADD, BinaryOperator.SUBTRACT -> {
if (left.unit != right.unit) {
throw IllegalArgumentException("Units must be the same for addition or subtraction.")
}
}
BinaryOperator.MULTIPLY, BinaryOperator.DIVIDE -> {
// Единицы измерения уже вычислены в `unit`
}
BinaryOperator.POWER -> {
if (!right.unit.dimension.isDimensionless()) {
throw IllegalArgumentException("Exponent must be dimensionless.")
}
}
BinaryOperator.GREATER_THAN, BinaryOperator.LESS_THAN,
BinaryOperator.GREATER_OR_EQUAL, BinaryOperator.LESS_OR_EQUAL -> {
if (left.unit != right.unit) {
throw IllegalArgumentException("Units must be the same for comparison operations.")
}
}
}
}
override fun derivative(variable: VariableExpression): Expression {
return when (operator) {
BinaryOperator.ADD, BinaryOperator.SUBTRACT -> {
BinaryExpression(left.derivative(variable), operator, right.derivative(variable))
}
BinaryOperator.MULTIPLY -> {
// Правило произведения: (u*v)' = u'*v + u*v'
val leftDerivative = BinaryExpression(left.derivative(variable), BinaryOperator.MULTIPLY, right)
val rightDerivative = BinaryExpression(left, BinaryOperator.MULTIPLY, right.derivative(variable))
BinaryExpression(leftDerivative, BinaryOperator.ADD, rightDerivative)
}
BinaryOperator.DIVIDE -> {
// Правило частного: (u/v)' = (u'*v - u*v') / v^2
val numerator = BinaryExpression(
BinaryExpression(left.derivative(variable), BinaryOperator.MULTIPLY, right),
BinaryOperator.SUBTRACT,
BinaryExpression(left, BinaryOperator.MULTIPLY, right.derivative(variable))
)
val denominator = BinaryExpression(right, BinaryOperator.POWER, ConstantExpression(2.0, Units.dimensionless))
BinaryExpression(numerator, BinaryOperator.DIVIDE, denominator)
}
BinaryOperator.POWER -> {
if (right is ConstantExpression) {
// (u^n)' = n * u^(n-1) * u'
val n = right.value
val newExponent = ConstantExpression(n - 1.0, Units.dimensionless)
val baseDerivative = left.derivative(variable)
val powerExpression = BinaryExpression(left, BinaryOperator.POWER, newExponent)
BinaryExpression(
BinaryExpression(ConstantExpression(n, Units.dimensionless), BinaryOperator.MULTIPLY, powerExpression),
BinaryOperator.MULTIPLY,
baseDerivative
)
} else {
throw UnsupportedOperationException("Derivative of expression with variable exponent is not supported.")
}
}
else -> throw UnsupportedOperationException("Derivative not implemented for operator $operator.")
}
}
}

View File

@ -1,20 +0,0 @@
package space.kscience.controls.constructor.dsl.core.expressions
import space.kscience.controls.constructor.dsl.core.units.PhysicalUnit
import space.kscience.controls.constructor.dsl.core.units.Units
/**
* Класс для представления константного значения.
*/
public class ConstantExpression(public val value: Double, override val unit: PhysicalUnit) : Expression() {
override fun evaluate(values: Map<String, Double>): Double {
return value
}
override fun checkDimensions() {
// Константа имеет единицу измерения, дополнительных проверок не требуется
}
override fun derivative(variable: VariableExpression): Expression {
return ConstantExpression(0.0, Units.dimensionless)
}
}

View File

@ -1,30 +0,0 @@
package space.kscience.controls.constructor.dsl.core.expressions
import space.kscience.controls.constructor.dsl.core.units.PhysicalUnit
import space.kscience.controls.constructor.dsl.core.units.Units
/**
* Класс для представления производной переменной по времени.
*/
public class DerivativeExpression(
public val variable: VariableExpression,
public val order: Int = 1 // Порядок производной (по умолчанию 1)
) : Expression() {
override val unit: PhysicalUnit = variable.unit / Units.second.pow(order.toDouble())
override fun evaluate(values: Map<String, Double>): Double {
// вычисление производной невозможно в этом контексте
throw UnsupportedOperationException("Cannot evaluate derivative directly.")
}
override fun checkDimensions() {
variable.checkDimensions()
// Единица измерения уже рассчитана
}
override fun derivative(variable: VariableExpression): Expression {
// Производная от производной - это производная более высокого порядка
return DerivativeExpression(this.variable, this.order + 1)
}
}

View File

@ -1,34 +0,0 @@
package space.kscience.controls.constructor.dsl.core.expressions
import space.kscience.controls.constructor.dsl.core.units.PhysicalUnit
/**
* Абстрактный класс для представления математических выражений.
* Каждое выражение связано с единицей измерения.
*/
public abstract class Expression {
public abstract val unit: PhysicalUnit
/**
* Метод для вычисления численного значения выражения.
* Параметр values содержит значения переменных.
*/
public abstract fun evaluate(values: Map<String, Double>): Double
/**
* Проверка размерности выражения.
*/
public abstract fun checkDimensions()
/**
* Упрощение выражения.
*/
public open fun simplify(): Expression = this
/**
* Производная выражения по заданной переменной.
*/
public open fun derivative(variable: VariableExpression): Expression {
throw UnsupportedOperationException("Derivative not implemented for this expression type.")
}
}

View File

@ -1,91 +0,0 @@
package space.kscience.controls.constructor.dsl.core.expressions
import space.kscience.controls.constructor.dsl.core.units.Units
import space.kscience.controls.constructor.dsl.core.units.PhysicalUnit
import kotlin.math.*
/**
* Класс для представления вызова математической функции.
*/
public class FunctionCallExpression(
public val functionName: String,
public val arguments: List<Expression>
) : Expression() {
override val unit: PhysicalUnit = determineUnit()
private fun determineUnit(): PhysicalUnit {
return when (functionName) {
"sin", "cos", "tan", "asin", "acos", "atan",
"sinh", "cosh", "tanh", "asinh", "acosh", "atanh",
"exp", "log", "log10", "signum" -> Units.dimensionless
"sqrt", "cbrt", "abs" -> arguments[0].unit
"pow", "max", "min" -> arguments[0].unit
else -> throw IllegalArgumentException("Unknown function: $functionName")
}
}
override fun evaluate(values: Map<String, Double>): Double {
val argValues = arguments.map { it.evaluate(values) }
return when (functionName) {
"sin" -> sin(argValues[0])
"cos" -> cos(argValues[0])
"tan" -> tan(argValues[0])
"asin" -> asin(argValues[0])
"acos" -> acos(argValues[0])
"atan" -> atan(argValues[0])
"sinh" -> sinh(argValues[0])
"cosh" -> cosh(argValues[0])
"tanh" -> tanh(argValues[0])
"asinh" -> asinh(argValues[0])
"acosh" -> acosh(argValues[0])
"atanh" -> atanh(argValues[0])
"exp" -> exp(argValues[0])
"log" -> ln(argValues[0])
"log10" -> log10(argValues[0])
"sqrt" -> sqrt(argValues[0])
"cbrt" -> cbrt(argValues[0])
"abs" -> abs(argValues[0])
"signum" -> sign(argValues[0])
"pow" -> argValues[0].pow(argValues[1])
"max" -> max(argValues[0], argValues[1])
"min" -> min(argValues[0], argValues[1])
else -> throw IllegalArgumentException("Unknown function: $functionName")
}
}
override fun checkDimensions() {
when (functionName) {
"sin", "cos", "tan", "asin", "acos", "atan",
"sinh", "cosh", "tanh", "asinh", "acosh", "atanh",
"exp", "log", "log10", "signum" -> {
val argUnit = arguments[0].unit
if (!argUnit.dimension.isDimensionless()) {
throw IllegalArgumentException("Argument of $functionName must be dimensionless.")
}
}
"sqrt", "cbrt", "abs" -> {
// Единица измерения сохраняется, дополнительных проверок не требуется
}
"pow" -> {
val exponentUnit = arguments[1].unit
if (!exponentUnit.dimension.isDimensionless()) {
throw IllegalArgumentException("Exponent in $functionName must be dimensionless.")
}
}
"max", "min" -> {
if (arguments[0].unit != arguments[1].unit) {
throw IllegalArgumentException("Arguments of $functionName must have the same units.")
}
}
else -> throw IllegalArgumentException("Unknown function: $functionName")
}
// Проверяем размерности аргументов
arguments.forEach { it.checkDimensions() }
}
override fun derivative(variable: VariableExpression): Expression {
// Реализация производной для функций
throw UnsupportedOperationException("Derivative for function $functionName is not implemented.")
}
}

View File

@ -1,31 +0,0 @@
package space.kscience.controls.constructor.dsl.core.expressions
import space.kscience.controls.constructor.dsl.core.units.PhysicalUnit
/**
* Представляет частную производную переменной по другой переменной.
*/
public class PartialDerivativeExpression(
public val variable: VariableExpression,
public val respectTo: VariableExpression,
public val order: Int = 1
) : Expression() {
override val unit: PhysicalUnit = variable.unit / respectTo.unit.pow(order.toDouble())
override fun evaluate(values: Map<String, Double>): Double {
// Невозможно вычислить частную производную без дополнительного контекста
throw UnsupportedOperationException("Cannot evaluate partial derivative directly.")
}
override fun checkDimensions() {
variable.checkDimensions()
respectTo.checkDimensions()
// Единицы измерения уже рассчитаны
}
override fun derivative(variable: VariableExpression): Expression {
// Производная частной производной не реализована
throw UnsupportedOperationException("Derivative of partial derivative is not implemented.")
}
}

View File

@ -1,32 +0,0 @@
package space.kscience.controls.constructor.dsl.core.expressions
import space.kscience.controls.constructor.dsl.core.units.PhysicalUnit
import space.kscience.controls.constructor.dsl.core.units.Units
import space.kscience.controls.constructor.dsl.core.variables.Variable
/**
* Класс для представления переменной в выражении.
*/
public class VariableExpression(public val variable: Variable) : Expression() {
override val unit: PhysicalUnit = variable.unit
override fun evaluate(values: Map<String, Double>): Double {
return values[variable.name] ?: throw IllegalArgumentException("Variable ${variable.name} is not defined.")
}
override fun checkDimensions() {
// Переменная уже имеет единицу измерения, дополнительных проверок не требуется
}
override fun derivative(variable: VariableExpression): Expression {
return if (this.variable == variable.variable) {
ConstantExpression(1.0, Units.dimensionless)
} else {
ConstantExpression(0.0, Units.dimensionless)
}
}
public fun partialDerivative(respectTo: VariableExpression, order: Int = 1): PartialDerivativeExpression {
return PartialDerivativeExpression(this, respectTo, order)
}
}

View File

@ -1,14 +0,0 @@
package space.kscience.controls.constructor.dsl.core.functions
import space.kscience.controls.constructor.dsl.core.algorithms.Algorithm
import space.kscience.controls.constructor.dsl.core.variables.Variable
/**
* Класс для представления пользовательской функции.
*/
public data class Function(
val name: String,
val inputs: List<Variable>,
val outputs: List<Variable>,
val algorithm: Algorithm
)

View File

@ -1,50 +0,0 @@
package space.kscience.controls.constructor.dsl.core.functions
import space.kscience.controls.constructor.dsl.core.algorithms.AlgorithmBuilder
import space.kscience.controls.constructor.dsl.core.algorithms.Algorithm
import space.kscience.controls.constructor.dsl.core.components.Model
import space.kscience.controls.constructor.dsl.core.units.PhysicalUnit
import space.kscience.controls.constructor.dsl.core.variables.Causality
import space.kscience.controls.constructor.dsl.core.variables.Variable
/**
* Класс для построения пользовательской функции.
*/
public class FunctionBuilder(private val name: String) {
public val inputs: MutableList<Variable> = mutableListOf<Variable>()
public val outputs: MutableList<Variable> = mutableListOf<Variable>()
private val algorithmBuilder = AlgorithmBuilder()
/**
* Определение входного параметра функции.
*/
public fun input(name: String, unit: PhysicalUnit): Variable {
val variable = Variable(name, unit, causality = Causality.INPUT)
inputs.add(variable)
return variable
}
/**
* Определение выходного параметра функции.
*/
public fun output(name: String, unit: PhysicalUnit): Variable {
val variable = Variable(name, unit, causality = Causality.OUTPUT)
outputs.add(variable)
return variable
}
/**
* Определение алгоритмического тела функции.
*/
public fun algorithm(init: AlgorithmBuilder.() -> Unit) {
algorithmBuilder.init()
}
/**
* Построение функции.
*/
public fun build(): Function {
val algorithm = algorithmBuilder.build()
return Function(name, inputs, outputs, algorithm)
}
}

View File

@ -1,37 +0,0 @@
package space.kscience.controls.constructor.dsl.core.simulation
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow
import space.kscience.controls.constructor.dsl.core.EquationSystem
import space.kscience.controls.constructor.dsl.core.events.Event
public interface NumericSolver {
/**
* Инициализирует решатель.
*/
public suspend fun initialize(
equationSystem: EquationSystem,
context: SimulationContext
)
/**
* Выполняет один шаг симуляции.
*/
public suspend fun step(context: SimulationContext, timeStep: Double): Boolean
/**
* Запускает симуляцию.
*/
public suspend fun solve(
equationSystem: EquationSystem,
context: SimulationContext,
timeStart: Double,
timeEnd: Double,
timeStep: Double
): SimulationResult
}
public data class SimulationResult(
val timePoints: List<Double>,
val variableValues: Map<String, List<Double>>
)

View File

@ -1,93 +0,0 @@
package space.kscience.controls.constructor.dsl.core.simulation
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import space.kscience.controls.constructor.dsl.core.events.Event
import space.kscience.controls.constructor.dsl.core.variables.Variable
/**
* Класс для хранения состояния симуляции.
*/
public class SimulationContext(
private val scope: CoroutineScope
) {
public var currentTime: Double = 0.0
private val _variableValues: MutableMap<String, MutableStateFlow<Double>> = mutableMapOf()
// Канал для событий
private val eventChannel = Channel<Event>()
// Список событий
public val events: MutableList<Event> = mutableListOf()
/**
* Регистрирует переменную в контексте симуляции.
*/
public fun registerVariable(variable: Variable) {
_variableValues[variable.name] = MutableStateFlow(variable.value)
}
/**
* Обновляет значение переменной.
*/
public suspend fun updateVariable(name: String, value: Double) {
_variableValues[name]?.emit(value)
?: throw IllegalArgumentException("Variable $name is not defined in the simulation context.")
}
/**
* Получает текущее значение переменной.
*/
public fun getCurrentVariableValue(name: String): Double {
return _variableValues[name]?.value
?: throw IllegalArgumentException("Variable $name is not defined in the simulation context.")
}
/**
* Возвращает текущие значения всех переменных.
*/
public fun getVariableValues(): Map<String, Double> {
return _variableValues.mapValues { it.value.value }
}
/**
* Отправляет событие на обработку.
*/
public suspend fun sendEvent(event: Event) {
eventChannel.send(event)
}
/**
* Запускает обработку событий.
*/
public fun startEventHandling() {
scope.launch {
for (event in eventChannel) {
if (event.checkEvent(this@SimulationContext)) {
event.executeActions(this@SimulationContext)
}
}
}
}
/**
* Обрабатывает события из списка events.
*/
public suspend fun handleEvents() {
for (event in events) {
if (event.checkEvent(this)) {
event.executeActions(this)
}
}
}
/**
* Увеличивает время симуляции.
*/
public suspend fun advanceTime(deltaTime: Double) {
currentTime += deltaTime
}
}

View File

@ -1,78 +0,0 @@
package space.kscience.controls.constructor.dsl.core.units
import kotlin.math.abs
/**
* Класс для представления размерности физической величины.
*/
public data class Dimension(
val length: Double = 0.0,
val mass: Double = 0.0,
val time: Double = 0.0,
val electricCurrent: Double = 0.0,
val temperature: Double = 0.0,
val amountOfSubstance: Double = 0.0,
val luminousIntensity: Double = 0.0
) {
public operator fun plus(other: Dimension): Dimension {
return Dimension(
length + other.length,
mass + other.mass,
time + other.time,
electricCurrent + other.electricCurrent,
temperature + other.temperature,
amountOfSubstance + other.amountOfSubstance,
luminousIntensity + other.luminousIntensity
)
}
public operator fun minus(other: Dimension): Dimension {
return Dimension(
length - other.length,
mass - other.mass,
time - other.time,
electricCurrent - other.electricCurrent,
temperature - other.temperature,
amountOfSubstance - other.amountOfSubstance,
luminousIntensity - other.luminousIntensity
)
}
public operator fun times(factor: Double): Dimension {
return Dimension(
length * factor,
mass * factor,
time * factor,
electricCurrent * factor,
temperature * factor,
amountOfSubstance * factor,
luminousIntensity * factor
)
}
/**
* Проверяет, является ли размерность безразмерной.
*/
public fun isDimensionless(epsilon: Double = 1e-10): Boolean {
return abs(length) < epsilon &&
abs(mass) < epsilon &&
abs(time) < epsilon &&
abs(electricCurrent) < epsilon &&
abs(temperature) < epsilon &&
abs(amountOfSubstance) < epsilon &&
abs(luminousIntensity) < epsilon
}
/**
* Проверяет совместимость размерностей.
*/
public fun isCompatibleWith(other: Dimension, epsilon: Double = 1e-10): Boolean {
return abs(length - other.length) < epsilon &&
abs(mass - other.mass) < epsilon &&
abs(time - other.time) < epsilon &&
abs(electricCurrent - other.electricCurrent) < epsilon &&
abs(temperature - other.temperature) < epsilon &&
abs(amountOfSubstance - other.amountOfSubstance) < epsilon &&
abs(luminousIntensity - other.luminousIntensity) < epsilon
}
}

View File

@ -1,51 +0,0 @@
package space.kscience.controls.constructor.dsl.core.units
import kotlin.math.pow
/**
* Класс для представления физической единицы измерения.
*/
public data class PhysicalUnit(
val name: String,
val symbol: String,
val conversionFactorToSI: Double,
val dimension: Dimension
) {
public operator fun times(other: PhysicalUnit): PhysicalUnit {
return PhysicalUnit(
name = "$name·${other.name}",
symbol = "$symbol·${other.symbol}",
conversionFactorToSI = this.conversionFactorToSI * other.conversionFactorToSI,
dimension = this.dimension + other.dimension
)
}
public operator fun div(other: PhysicalUnit): PhysicalUnit {
return PhysicalUnit(
name = "$name/${other.name}",
symbol = "$symbol/${other.symbol}",
conversionFactorToSI = this.conversionFactorToSI / other.conversionFactorToSI,
dimension = this.dimension - other.dimension
)
}
public fun pow(exponent: Double): PhysicalUnit {
return PhysicalUnit(
name = "$name^$exponent",
symbol = "${symbol}^$exponent",
conversionFactorToSI = this.conversionFactorToSI.pow(exponent),
dimension = this.dimension * exponent
)
}
/**
* Преобразует значение из текущей единицы в целевую единицу.
*/
public fun convertValueTo(valueInThisUnit: Double, targetUnit: PhysicalUnit): Double {
require(this.dimension.isCompatibleWith(targetUnit.dimension)) {
"Units are not compatible: $this and $targetUnit"
}
val valueInSI = valueInThisUnit * this.conversionFactorToSI
return valueInSI / targetUnit.conversionFactorToSI
}
}

View File

@ -1,89 +0,0 @@
package space.kscience.controls.constructor.dsl.core.units
import kotlin.math.*
public object Units {
private val unitsRegistry = mutableMapOf<String, PhysicalUnit>()
// Основные единицы СИ
public val meter: PhysicalUnit = PhysicalUnit("meter", "m", 1.0, Dimension(length = 1.0))
public val kilogram: PhysicalUnit = PhysicalUnit("kilogram", "kg", 1.0, Dimension(mass = 1.0))
public val second: PhysicalUnit = PhysicalUnit("second", "s", 1.0, Dimension(time = 1.0))
public val ampere: PhysicalUnit = PhysicalUnit("ampere", "A", 1.0, Dimension(electricCurrent = 1.0))
public val kelvin: PhysicalUnit = PhysicalUnit("kelvin", "K", 1.0, Dimension(temperature = 1.0))
public val mole: PhysicalUnit = PhysicalUnit("mole", "mol", 1.0, Dimension(amountOfSubstance = 1.0))
public val candela: PhysicalUnit = PhysicalUnit("candela", "cd", 1.0, Dimension(luminousIntensity = 1.0))
public val radian: PhysicalUnit = PhysicalUnit("radian", "rad", 1.0, Dimension())
public val hertz: PhysicalUnit = PhysicalUnit("hertz", "Hz", 1.0, Dimension(time = -1.0))
public val dimensionless: PhysicalUnit = PhysicalUnit("dimensionless", "", 1.0, Dimension())
init {
// Регистрируем основные единицы
registerUnit(meter)
registerUnit(kilogram)
registerUnit(second)
registerUnit(ampere)
registerUnit(kelvin)
registerUnit(mole)
registerUnit(candela)
registerUnit(radian)
registerUnit(hertz)
}
/**
* Регистрирует единицу измерения в реестре.
*/
public fun registerUnit(unit: PhysicalUnit) {
unitsRegistry[unit.name] = unit
}
/**
* Получает единицу измерения по имени.
*/
public fun getUnit(name: String): PhysicalUnit? {
return unitsRegistry[name]
}
/**
* Список префиксов СИ.
*/
public enum class Prefix(public val prefixName: String, public val symbol: String, public val factor: Double) {
YOTTA("yotta", "Y", 1e24),
ZETTA("zetta", "Z", 1e21),
EXA("exa", "E", 1e18),
PETA("peta", "P", 1e15),
TERA("tera", "T", 1e12),
GIGA("giga", "G", 1e9),
MEGA("mega", "M", 1e6),
KILO("kilo", "k", 1e3),
HECTO("hecto", "h", 1e2),
DECA("deca", "da", 1e1),
DECI("deci", "d", 1e-1),
CENTI("centi", "c", 1e-2),
MILLI("milli", "m", 1e-3),
MICRO("micro", "μ", 1e-6),
NANO("nano", "n", 1e-9),
PICO("pico", "p", 1e-12),
FEMTO("femto", "f", 1e-15),
ATTO("atto", "a", 1e-18),
ZEPTO("zepto", "z", 1e-21),
YOCTO("yocto", "y", 1e-24)
}
/**
* Создает новую единицу с заданным префиксом.
*/
public fun withPrefix(prefix: Prefix, unit: PhysicalUnit): PhysicalUnit {
val newName = "${prefix.prefixName}${unit.name}"
val newSymbol = "${prefix.symbol}${unit.symbol}"
val newConversionFactor = unit.conversionFactorToSI * prefix.factor
val newUnit = PhysicalUnit(newName, newSymbol, newConversionFactor, unit.dimension)
registerUnit(newUnit)
return newUnit
}
public val kilometer: PhysicalUnit = withPrefix(Prefix.KILO, meter)
public val millimeter: PhysicalUnit = withPrefix(Prefix.MILLI, meter)
public val microsecond: PhysicalUnit = withPrefix(Prefix.MICRO, second)
}

View File

@ -1,13 +0,0 @@
package space.kscience.controls.constructor.dsl.core.variables
import space.kscience.controls.constructor.dsl.core.units.PhysicalUnit
/**
* Абстрактный класс для переменных.
*/
public abstract class AbstractVariable(
public val name: String,
public val unit: PhysicalUnit,
public val variability: Variability,
public val causality: Causality
)

View File

@ -1,28 +0,0 @@
package space.kscience.controls.constructor.dsl.core.variables
import space.kscience.controls.constructor.dsl.core.units.PhysicalUnit
import org.jetbrains.kotlinx.multik.ndarray.data.D2
import org.jetbrains.kotlinx.multik.ndarray.data.NDArray
import org.jetbrains.kotlinx.multik.ndarray.data.set
import org.jetbrains.kotlinx.multik.ndarray.operations.plus
/**
* Класс для представления переменной-массива.
* Использует Multik для работы с многомерными массивами.
*/
public class ArrayVariable(
name: String,
unit: PhysicalUnit,
public val array: NDArray<Double, D2>,
causality: Causality = Causality.INTERNAL
) : AbstractVariable(name, unit, Variability.CONTINUOUS, causality) {
public fun add(other: ArrayVariable): ArrayVariable {
require(this.array.shape contentEquals other.array.shape) {
"Array shapes do not match."
}
val resultArray = this.array + other.array
return ArrayVariable("$name + ${other.name}", unit, resultArray, causality)
}
}

View File

@ -1,10 +0,0 @@
package space.kscience.controls.constructor.dsl.core.variables
/**
* Перечисление каузальности переменной.
*/
public enum class Causality {
INPUT,
OUTPUT,
INTERNAL
}

View File

@ -1,12 +0,0 @@
package space.kscience.controls.constructor.dsl.core.variables
import space.kscience.controls.constructor.dsl.core.units.PhysicalUnit
/**
* Класс для представления константы.
*/
public class ConstantVariable(
name: String,
unit: PhysicalUnit,
public val value: Double
) : AbstractVariable(name, unit, Variability.CONSTANT, Causality.INTERNAL)

View File

@ -1,22 +0,0 @@
package space.kscience.controls.constructor.dsl.core.variables
import space.kscience.controls.constructor.dsl.core.units.PhysicalUnit
public class ContinuousVariable(
name: String,
unit: PhysicalUnit,
public var value: Double = 0.0,
public val min: Double? = null,
public val max: Double? = null,
causality: Causality = Causality.INTERNAL
) : AbstractVariable(name, unit, Variability.CONTINUOUS, causality) {
init {
// Проверка допустимого диапазона
if (min != null && value < min) {
throw IllegalArgumentException("Value of $name is less than minimum allowed value.")
}
if (max != null && value > max) {
throw IllegalArgumentException("Value of $name is greater than maximum allowed value.")
}
}
}

View File

@ -1,13 +0,0 @@
package space.kscience.controls.constructor.dsl.core.variables
import space.kscience.controls.constructor.dsl.core.units.PhysicalUnit
/**
* Класс для представления дискретной переменной.
*/
public class DiscreteVariable(
name: String,
unit: PhysicalUnit,
public var value: Double = 0.0,
causality: Causality = Causality.INTERNAL
) : AbstractVariable(name, unit, Variability.DISCRETE, causality)

View File

@ -1,25 +0,0 @@
package space.kscience.controls.constructor.dsl.core.variables
import space.kscience.controls.constructor.dsl.core.units.PhysicalUnit
/**
* Класс для представления параметра (неизменяемой величины).
*/
public open class ParameterVariable(
name: String,
unit: PhysicalUnit,
public val value: Double,
public val min: Double? = null,
public val max: Double? = null,
causality: Causality = Causality.INTERNAL
) : AbstractVariable(name, unit, Variability.PARAMETER, causality) {
init {
// Проверка допустимого диапазона
if (min != null && value < min) {
throw IllegalArgumentException("Value of parameter $name is less than minimum allowed value.")
}
if (max != null && value > max) {
throw IllegalArgumentException("Value of parameter $name is greater than maximum allowed value.")
}
}
}

View File

@ -1,11 +0,0 @@
package space.kscience.controls.constructor.dsl.core.variables
/**
* Перечисление типов изменчивости переменной.
*/
public enum class Variability {
CONSTANT,
PARAMETER,
DISCRETE,
CONTINUOUS
}

View File

@ -1,28 +0,0 @@
package space.kscience.controls.constructor.dsl.core.variables
import space.kscience.controls.constructor.dsl.core.units.PhysicalUnit
/**
* Класс для представления переменной.
*/
public open class Variable(
public val name: String,
public val unit: PhysicalUnit,
public open var value: Double = 0.0,
public val min: Double? = null,
public val max: Double? = null,
public var variability: Variability = Variability.CONTINUOUS,
public var causality: Causality = Causality.INTERNAL
) {
init {
// Проверка, что значение находится в допустимом диапазоне
if (min != null && value < min) {
throw IllegalArgumentException("Value of $name is less than minimum allowed value.")
}
if (max != null && value > max) {
throw IllegalArgumentException("Value of $name is greater than maximum allowed value.")
}
}
public open fun checkDimensions() {}
}

View File

@ -55,7 +55,7 @@ public class Inertia<U : UnitsOfMeasurement, V : UnitsOfMeasurement>(
public fun circular(
context: Context,
force: DeviceState<NumericalValue<NewtonsMeters>>,
force: DeviceState<NumericalValue<NewtonMeters>>,
momentOfInertia: NumericalValue<KgM2>,
position: MutableDeviceState<NumericalValue<Degrees>>,
velocity: MutableDeviceState<NumericalValue<DegreesPerSecond>> = MutableDeviceState(NumericalValue(0.0)),

View File

@ -16,7 +16,7 @@ public class Leadscrew(
) : ModelConstructor(context) {
public fun torqueToForce(
stateOfTorque: DeviceState<NumericalValue<NewtonsMeters>>,
stateOfTorque: DeviceState<NumericalValue<NewtonMeters>>,
): DeviceState<NumericalValue<Newtons>> = DeviceState.map(stateOfTorque) { torque ->
NumericalValue(torque.value / leverage.value )
}

View File

@ -5,7 +5,6 @@ import space.kscience.dataforge.meta.MetaConverter
import space.kscience.dataforge.meta.double
import kotlin.jvm.JvmInline
/**
* A value without identity coupled to units of measurements.
*/

View File

@ -1,60 +1,244 @@
package space.kscience.controls.constructor.units
import kotlin.math.*
public interface UnitsOfMeasurement
/**
* Represents a unit of measurement.
* Provides methods to convert values to and from the base unit.
*/
public interface UnitsOfMeasurement {
/**
* Symbol representing the unit (e.g., "m" for meters).
*/
public val symbol: String
/**/
/**
* Converts a value from this unit to the base unit.
*/
public fun toBase(value: Double): Double
Review

Not clear how we select the base unit. Probably better documentation is needed. Is it better than the external dispatch that is used right now?

Not clear how we select the base unit. Probably better documentation is needed. Is it better than the external dispatch that is used right now?
/**
* Converts a value from the base unit to this unit.
*/
public fun fromBase(value: Double): Double
}
/**
* Base unit, where conversion to base unit is identity.
*/
public open class BaseUnit(
override val symbol: String,
) : UnitsOfMeasurement {
override fun toBase(value: Double): Double = value
override fun fromBase(value: Double): Double = value
}
/**
* Derived unit with a conversion factor to the base unit.
*/
public class DerivedUnit(
override val symbol: String,
private val conversionFactor: Double, // Factor to convert to base unit.
) : UnitsOfMeasurement {
override fun toBase(value: Double): Double = value * conversionFactor
override fun fromBase(value: Double): Double = value / conversionFactor
}
/**
* Enumeration of SI prefixes with their symbols and factors.
*/
public enum class SIPrefix(
public val symbol: String,
public val factor: Double
) {
YOTTA("Y", 1e24),
ZETTA("Z", 1e21),
EXA("E", 1e18),
PETA("P", 1e15),
TERA("T", 1e12),
GIGA("G", 1e9),
MEGA("M", 1e6),
KILO("k", 1e3),
HECTO("h", 1e2),
DECA("da", 1e1),
NONE("", 1.0),
DECI("d", 1e-1),
CENTI("c", 1e-2),
MILLI("m", 1e-3),
MICRO("μ", 1e-6),
NANO("n", 1e-9),
PICO("p", 1e-12),
FEMTO("f", 1e-15),
ATTO("a", 1e-18),
ZEPTO("z", 1e-21),
YOCTO("y", 1e-24),
}
/**
* Creates a new unit by applying an SI prefix to the current unit.
*/
public fun <U : UnitsOfMeasurement> U.withPrefix(prefix: SIPrefix): UnitsOfMeasurement {
val prefixedSymbol = prefix.symbol + this.symbol
val prefixedFactor = prefix.factor
return object : UnitsOfMeasurement {
override val symbol: String = prefixedSymbol
override fun toBase(value: Double): Double = this@withPrefix.toBase(value * prefixedFactor)
override fun fromBase(value: Double): Double = this@withPrefix.fromBase(value) / prefixedFactor
}
}
/**
* Interface for units of length.
*/
public interface UnitsOfLength : UnitsOfMeasurement
public data object Meters : UnitsOfLength
/**
* Base unit for length: Meter.
*/
public data object Meters : UnitsOfLength {
override val symbol: String = "m"
override fun toBase(value: Double): Double = value // Base unit
override fun fromBase(value: Double): Double = value
}
/**/
/**
* Interface for units of time.
*/
public interface UnitsOfTime : UnitsOfMeasurement
public data object Seconds : UnitsOfTime
/**/
/**
* Base unit for time: Second.
*/
public data object Seconds : UnitsOfTime {
override val symbol: String = "s"
override fun toBase(value: Double): Double = value // Base unit
override fun fromBase(value: Double): Double = value
}
/**
* Interface for units of velocity.
*/
public interface UnitsOfVelocity : UnitsOfMeasurement
public data object MetersPerSecond : UnitsOfVelocity
/**
* Derived unit for velocity: Meters per Second.
*/
public data object MetersPerSecond : UnitsOfVelocity {
override val symbol: String = "m/s"
override fun toBase(value: Double): Double = value // Base unit for velocity
override fun fromBase(value: Double): Double = value
}
/**/
/**
* Sealed interface for units of angles.
*/
public sealed interface UnitsOfAngles : UnitsOfMeasurement
public data object Radians : UnitsOfAngles
public data object Degrees : UnitsOfAngles
/**
* Base unit for angles: Radian.
*/
public data object Radians : UnitsOfAngles {
override val symbol: String = "rad"
override fun toBase(value: Double): Double = value // Base unit
override fun fromBase(value: Double): Double = value
}
/**
* Unit for angles: Degree.
*/
public data object Degrees : UnitsOfAngles {
override val symbol: String = "deg"
override fun toBase(value: Double): Double = value * (PI / 180.0)
override fun fromBase(value: Double): Double = value * (180.0 / PI)
}
/**/
/**
* Sealed interface for units of angular velocity.
*/
public sealed interface UnitsAngularOfVelocity : UnitsOfMeasurement
public data object RadiansPerSecond : UnitsAngularOfVelocity
/**
* Base unit for angular velocity: Radians per Second.
*/
public data object RadiansPerSecond : UnitsAngularOfVelocity {
override val symbol: String = "rad/s"
override fun toBase(value: Double): Double = value // Base unit
override fun fromBase(value: Double): Double = value
}
public data object DegreesPerSecond : UnitsAngularOfVelocity
/**
* Unit for angular velocity: Degrees per Second.
*/
public data object DegreesPerSecond : UnitsAngularOfVelocity {
override val symbol: String = "deg/s"
override fun toBase(value: Double): Double = value * (PI / 180.0)
override fun fromBase(value: Double): Double = value * (180.0 / PI)
}
/**/
public interface UnitsOfForce: UnitsOfMeasurement
/**
* Interface for units of force.
*/
public interface UnitsOfForce : UnitsOfMeasurement
public data object Newtons: UnitsOfForce
/**
* Base unit for force: Newton.
*/
public data object Newtons : UnitsOfForce {
override val symbol: String = "N"
override fun toBase(value: Double): Double = value // Base unit
override fun fromBase(value: Double): Double = value
}
/**/
/**
* Interface for units of torque.
*/
public interface UnitsOfTorque : UnitsOfMeasurement
public interface UnitsOfTorque: UnitsOfMeasurement
/**
* Base unit for torque: Newton Meter.
*/
public data object NewtonMeters : UnitsOfTorque {
override val symbol: String = "N·m"
override fun toBase(value: Double): Double = value // Base unit
override fun fromBase(value: Double): Double = value
}
public data object NewtonsMeters: UnitsOfTorque
/**
* Interface for units of mass.
*/
public interface UnitsOfMass : UnitsOfMeasurement
/**/
/**
* Base unit for mass: Kilogram.
*/
public data object Kilograms : UnitsOfMass {
override val symbol: String = "kg"
override fun toBase(value: Double): Double = value // Base unit
override fun fromBase(value: Double): Double = value
}
public interface UnitsOfMass: UnitsOfMeasurement
/**
* Interface for units of moment of inertia.
*/
public interface UnitsOfMomentOfInertia : UnitsOfMeasurement
public data object Kilograms : UnitsOfMass
/**/
public interface UnitsOfMomentOfInertia: UnitsOfMeasurement
public data object KgM2: UnitsOfMomentOfInertia
/**
* Base unit for moment of inertia: Kilogram Meter Squared.
*/
public data object KgM2 : UnitsOfMomentOfInertia {
override val symbol: String = "kg·m²"
override fun toBase(value: Double): Double = value // Base unit
override fun fromBase(value: Double): Double = value
}

View File

@ -0,0 +1,806 @@
package space.kscience.controls.constructor
import kotlinx.coroutines.*
import kotlinx.coroutines.test.runTest
import space.kscience.controls.api.LifecycleState
import space.kscience.controls.spec.*
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.meta.*
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
// Спецификация шагового мотора
object StepperMotorSpec : DeviceSpec<StepperMotorDevice>() {
val position by intProperty(
read = { propertyName -> getPosition() },
write = { propertyName, value -> setPosition(value) }
)
val maxPosition by intProperty(
read = { propertyName -> maxPosition }
)
override fun createDevice(context: Context, meta: Meta): StepperMotorDevice {
return StepperMotorDevice(context, meta)
}
}
// Реализация устройства шагового мотора
class StepperMotorDevice(
context: Context,
meta: Meta = Meta.EMPTY
) : DeviceBySpec<StepperMotorDevice>(StepperMotorSpec, context, meta) {
private var _position: Int = 0
val maxPosition: Int = meta["maxPosition"].int ?: 100
// Получить текущую позицию мотора
suspend fun getPosition(): Int = _position
Review

probably better to use readPosition instead of getPosition. It communicates what the functions do better.

probably better to use `readPosition` instead of `getPosition`. It communicates what the functions do better.
// Установить позицию мотора
suspend fun setPosition(value: Int) {
Review

Consider encapsulating those functions.

Consider encapsulating those functions.
if (value in 0..maxPosition) {
_position = value
println("StepperMotorDevice: Перемещен в позицию $_position")
delay(100) // Имитация времени на перемещение
} else {
println("StepperMotorDevice: Неверная позиция $value (макс: $maxPosition)")
}
}
override fun toString(): String = "StepperMotorDevice"
}
// Спецификация клапана
object ValveSpec : DeviceSpec<ValveDevice>() {
val state by booleanProperty(
read = { propertyName -> getState() },
write = { propertyName, value -> setState(value) }
)
override fun createDevice(context: Context, meta: Meta): ValveDevice {
return ValveDevice(context, meta)
}
}
// Реализация устройства клапана
class ValveDevice(
Review

Maybe call it VirtualValv or similar to discriminate between virtual devices and real ones.

Maybe call it `VirtualValv` or similar to discriminate between virtual devices and real ones.
context: Context,
meta: Meta = Meta.EMPTY
) : DeviceBySpec<ValveDevice>(ValveSpec, context, meta) {
private var _state: Boolean = false
// Получить состояние клапана
suspend fun getState(): Boolean = _state
// Установить состояние клапана
suspend fun setState(value: Boolean) {
_state = value
val stateStr = if (_state) "открыт" else "закрыт"
println("ValveDevice: Клапан теперь $stateStr")
Review

Remove debug prints. Use property subscription for testing. Don't use Russian.

Remove debug prints. Use property subscription for testing. Don't use Russian.
delay(50) // Имитация времени на изменение состояния
}
// Метод для щелчка клапана
suspend fun click() {
println("ValveDevice: Клик клапана...")
setState(true)
delay(50)
setState(false)
println("ValveDevice: Клик клапана завершен")
}
override fun toString(): String = "ValveDevice"
}
// Спецификация камеры давления
object PressureChamberSpec : DeviceSpec<PressureChamberDevice>() {
val pressure by doubleProperty(
read = { propertyName -> getPressure() },
write = { propertyName, value -> setPressure(value) }
)
override fun createDevice(context: Context, meta: Meta): PressureChamberDevice {
return PressureChamberDevice(context, meta)
}
}
// Реализация устройства камеры давления
class PressureChamberDevice(
context: Context,
meta: Meta = Meta.EMPTY
) : DeviceBySpec<PressureChamberDevice>(PressureChamberSpec, context, meta) {
private var _pressure: Double = 0.0
// Получить текущее давление
suspend fun getPressure(): Double = _pressure
Review

readPressure?

readPressure?
// Установить давление
suspend fun setPressure(value: Double) {
_pressure = value
println("PressureChamberDevice: Давление установлено на $_pressure")
delay(50)
}
override fun toString(): String = "PressureChamberDevice"
}
// Спецификация шприцевого насоса
object SyringePumpSpec : DeviceSpec<SyringePumpDevice>() {
val volume by doubleProperty(
read = { propertyName -> getVolume() },
write = { propertyName, value -> setVolume(value) }
)
override fun createDevice(context: Context, meta: Meta): SyringePumpDevice {
return SyringePumpDevice(context, meta)
}
}
// Реализация устройства шприцевого насоса
class SyringePumpDevice(
context: Context,
meta: Meta = Meta.EMPTY
) : DeviceBySpec<SyringePumpDevice>(SyringePumpSpec, context, meta) {
private var _volume: Double = 0.0
val maxVolume: Double = meta["maxVolume"].double ?: 5.0
// Получить текущий объем
suspend fun getVolume(): Double = _volume
// Установить объем
suspend fun setVolume(value: Double) {
if (value in 0.0..maxVolume) {
_volume = value
println("SyringePumpDevice: Объем установлен на $_volume мл")
delay(100)
} else {
println("SyringePumpDevice: Неверный объем $value (макс: $maxVolume)")
}
}
override fun toString(): String = "SyringePumpDevice"
}
// Спецификация датчика реагента
object ReagentSensorSpec : DeviceSpec<ReagentSensorDevice>() {
val isPresent by booleanProperty(
read = { propertyName -> checkReagent() }
)
override fun createDevice(context: Context, meta: Meta): ReagentSensorDevice {
return ReagentSensorDevice(context, meta)
}
}
// Реализация устройства датчика реагента
class ReagentSensorDevice(
context: Context,
meta: Meta = Meta.EMPTY
) : DeviceBySpec<ReagentSensorDevice>(ReagentSensorSpec, context, meta) {
// Проверить наличие реагента
suspend fun checkReagent(): Boolean {
println("ReagentSensorDevice: Проверка наличия реагента...")
delay(100) // Имитация времени на проверку
val isPresent = true // Предполагаем, что реагент присутствует
println("ReagentSensorDevice: Реагент ${if (isPresent) "обнаружен" else "не обнаружен"}")
return isPresent
}
override fun toString(): String = "ReagentSensorDevice"
}
// Спецификация иглы
object NeedleSpec : DeviceSpec<NeedleDevice>() {
val mode by enumProperty(
enumValues = NeedleDevice.Mode.entries.toTypedArray(),
read = { propertyName -> getMode() },
write = { propertyName, value -> setMode(value) }
)
val position by doubleProperty(
read = { propertyName -> getPosition() },
write = { propertyName, value -> setPosition(value) }
)
override fun createDevice(context: Context, meta: Meta): NeedleDevice {
return NeedleDevice(context, meta)
}
}
// Реализация устройства иглы
class NeedleDevice(
context: Context,
meta: Meta = Meta.EMPTY
) : DeviceBySpec<NeedleDevice>(NeedleSpec, context, meta) {
// Режимы работы иглы
enum class Mode { SAMPLING, WASHING }
private var mode: Mode = Mode.WASHING
private var position: Double = 0.0 // в мм
// Получить текущий режим
suspend fun getMode(): Mode = mode
// Установить режим
suspend fun setMode(value: Mode) {
mode = value
println("NeedleDevice: Режим установлен на $mode")
delay(50)
}
// Получить текущую позицию
suspend fun getPosition(): Double = position
// Установить позицию
suspend fun setPosition(value: Double) {
if (value in 0.0..100.0) {
position = value
println("NeedleDevice: Перемещена в позицию $position мм")
delay(100)
} else {
println("NeedleDevice: Неверная позиция $value мм")
}
}
// Выполнить промывку
suspend fun performWashing(duration: Int) {
println("NeedleDevice: Промывка в течение $duration секунд")
delay(duration * 1000L) // Имитация промывки (1 секунда = 1000 мс)
}
// Выполнить забор пробы
suspend fun performSampling() {
println("NeedleDevice: Забор пробы в позиции $position мм")
Review

Remove debug prints. If needed, use device log

Remove debug prints. If needed, use device log
delay(500) // Имитация забора пробы
}
override fun toString(): String = "NeedleDevice"
}
// Спецификация шейкера
object ShakerSpec : DeviceSpec<ShakerDevice>() {
val verticalMotor by device(StepperMotorSpec, name = "verticalMotor")
val horizontalMotor by device(StepperMotorSpec, name = "horizontalMotor")
override fun createDevice(context: Context, meta: Meta): ShakerDevice {
return ShakerDevice(context, meta)
}
}
// Реализация устройства шейкера
class ShakerDevice(
context: Context,
meta: Meta = Meta.EMPTY
) : DeviceBySpec<ShakerDevice>(ShakerSpec, context, meta) {
val verticalMotor: StepperMotorDevice
get() = nestedDevices["verticalMotor"] as StepperMotorDevice
val horizontalMotor: StepperMotorDevice
get() = nestedDevices["horizontalMotor"] as StepperMotorDevice
// Метод встряхивания
suspend fun shake(cycles: Int) {
println("ShakerDevice: Начало встряхивания, циклов: $cycles")
repeat(cycles) {
verticalMotor.setPosition(3)
verticalMotor.setPosition(1)
}
println("ShakerDevice: Встряхивание завершено")
}
override fun toString(): String = "ShakerDevice"
}
// Спецификация системы транспортировки
object TransportationSystemSpec : DeviceSpec<TransportationSystem>() {
val slideMotor by device(StepperMotorSpec, name = "slideMotor")
val pushMotor by device(StepperMotorSpec, name = "pushMotor")
val receiveMotor by device(StepperMotorSpec, name = "receiveMotor")
override fun createDevice(context: Context, meta: Meta): TransportationSystem {
return TransportationSystem(context, meta)
}
}
// Реализация системы транспортировки
class TransportationSystem(
context: Context,
meta: Meta = Meta.EMPTY
) : DeviceBySpec<TransportationSystem>(TransportationSystemSpec, context, meta) {
val slideMotor: StepperMotorDevice
get() = nestedDevices["slideMotor"] as StepperMotorDevice
val pushMotor: StepperMotorDevice
get() = nestedDevices["pushMotor"] as StepperMotorDevice
val receiveMotor: StepperMotorDevice
get() = nestedDevices["receiveMotor"] as StepperMotorDevice
override fun toString(): String = "TransportationSystem"
}
// Спецификация анализатора
object AnalyzerSpec : DeviceSpec<AnalyzerDevice>() {
val transportationSystem by device(TransportationSystemSpec, name = "transportationSystem")
val shakerDevice by device(ShakerSpec, name = "shakerDevice")
val needleDevice by device(NeedleSpec, name = "needleDevice")
val valveV20 by device(ValveSpec, name = "valveV20")
val valveV17 by device(ValveSpec, name = "valveV17")
val valveV18 by device(ValveSpec, name = "valveV18")
val valveV35 by device(ValveSpec, name = "valveV35")
val pressureChamberHigh by device(PressureChamberSpec, name = "pressureChamberHigh")
val pressureChamberLow by device(PressureChamberSpec, name = "pressureChamberLow")
val syringePumpMA100 by device(SyringePumpSpec, name = "syringePumpMA100")
val syringePumpMA25 by device(SyringePumpSpec, name = "syringePumpMA25")
val reagentSensor1 by device(ReagentSensorSpec, name = "reagentSensor1")
val reagentSensor2 by device(ReagentSensorSpec, name = "reagentSensor2")
val reagentSensor3 by device(ReagentSensorSpec, name = "reagentSensor3")
override fun createDevice(context: Context, meta: Meta): AnalyzerDevice {
return AnalyzerDevice(context, meta)
}
}
// Реализация анализатора
class AnalyzerDevice(
context: Context,
meta: Meta = Meta.EMPTY
) : DeviceBySpec<AnalyzerDevice>(AnalyzerSpec, context, meta) {
val transportationSystem: TransportationSystem
get() = nestedDevices["transportationSystem"] as TransportationSystem
Review

Probably better to use DeviceGroup API?

Probably better to use DeviceGroup API?
val shakerDevice: ShakerDevice
get() = nestedDevices["shakerDevice"] as ShakerDevice
val needleDevice: NeedleDevice
get() = nestedDevices["needleDevice"] as NeedleDevice
val valveV20: ValveDevice
get() = nestedDevices["valveV20"] as ValveDevice
val valveV17: ValveDevice
get() = nestedDevices["valveV17"] as ValveDevice
val valveV18: ValveDevice
get() = nestedDevices["valveV18"] as ValveDevice
val valveV35: ValveDevice
get() = nestedDevices["valveV35"] as ValveDevice
val pressureChamberHigh: PressureChamberDevice
get() = nestedDevices["pressureChamberHigh"] as PressureChamberDevice
val pressureChamberLow: PressureChamberDevice
get() = nestedDevices["pressureChamberLow"] as PressureChamberDevice
val syringePumpMA100: SyringePumpDevice
get() = nestedDevices["syringePumpMA100"] as SyringePumpDevice
val syringePumpMA25: SyringePumpDevice
get() = nestedDevices["syringePumpMA25"] as SyringePumpDevice
val reagentSensor1: ReagentSensorDevice
get() = nestedDevices["reagentSensor1"] as ReagentSensorDevice
val reagentSensor2: ReagentSensorDevice
get() = nestedDevices["reagentSensor2"] as ReagentSensorDevice
val reagentSensor3: ReagentSensorDevice
get() = nestedDevices["reagentSensor3"] as ReagentSensorDevice
// Процесс обработки проб
suspend fun processSample() {
println("Начало процесса забора пробы")
// Шаг 1: Открыть клапан V20 и начать забор пробы с помощью шприца MA 100 мкл
valveV20.setState(true)
syringePumpMA100.setVolume(0.1)
delay(500) // Имитация времени для забора жидкости
valveV20.setState(false)
// Шаг 2: Открыть клапан V17 и начать подачу лизиса в WBC с помощью MA 2.5 мл
valveV17.setState(true)
syringePumpMA25.setVolume(2.5)
delay(500) // Имитация времени для подачи лизиса
valveV17.setState(false)
// Шаг 3: Очистка системы
syringePumpMA100.setVolume(0.0)
syringePumpMA25.setVolume(0.0)
println("Процесс забора пробы завершен")
}
// Реализация рецепта калибровки
suspend fun calibrate() {
println("Начало калибровки...")
// Шаг 1: Откалибровать положения всех двигателей
val motors = listOf(
transportationSystem.slideMotor,
transportationSystem.pushMotor,
transportationSystem.receiveMotor,
shakerDevice.verticalMotor,
shakerDevice.horizontalMotor
)
for (motor in motors) {
for (position in 0..motor.maxPosition) {
motor.setPosition(position)
}
motor.setPosition(0)
}
// Шаг 2: Щелкнуть всеми клапанами и перевести их в нулевое положение
val valves = listOf(valveV20, valveV17, valveV18, valveV35)
for (valve in valves) {
valve.click()
valve.setState(false)
}
// Шаг 3: Провести накачку давления в камеру повышенного давления
pressureChamberHigh.setPressure(2.0)
// Шаг 4: Провести откачку камеры пониженного давления
pressureChamberLow.setPressure(-1.0)
// Шаг 5: Заполнить гидравлическую систему
// 5.1 Проверить наличие всех реагентов
val sensors = listOf(reagentSensor1, reagentSensor2, reagentSensor3)
for (sensor in sensors) {
sensor.checkReagent()
}
// 5.2 Прокачать все шприцевые дозаторы (5 раз движение между крайними положениями)
val pumps = listOf(syringePumpMA100, syringePumpMA25)
for (pump in pumps) {
repeat(5) {
pump.setVolume(pump.maxVolume)
pump.setVolume(0.0)
}
}
// 5.3 Провести промывку иглы в положении промывки для забора пробы
needleDevice.setPosition(0.0)
needleDevice.setMode(NeedleDevice.Mode.WASHING)
needleDevice.performWashing(5)
println("Калибровка завершена")
}
// Рецепт 1 - Подача пробирки на измерение
suspend fun executeRecipe1() {
println("Выполнение Рецепта 1")
// Шаг 1: Сдвинуть планшет на одну позицию
val currentSlidePosition = transportationSystem.slideMotor.getPosition()
transportationSystem.slideMotor.setPosition(currentSlidePosition + 1)
println("Сдвинуто планшет на позицию ${currentSlidePosition + 1}")
// Шаг 2: Захват пробирки для смешивания
println("Захват пробирки для смешивания")
// 2.1 - 2.10: Управление шейкером и двигателями
shakerDevice.verticalMotor.setPosition(1)
shakerDevice.horizontalMotor.setPosition(1)
println("Шейкер: вертикальный - 1, горизонтальный - 1")
shakerDevice.horizontalMotor.setPosition(2)
println("Шейкер: горизонтальный - 2")
shakerDevice.verticalMotor.setPosition(2)
println("Шейкер: вертикальный - 2")
// Встряхивание 5 циклов
repeat(5) {
shakerDevice.verticalMotor.setPosition(3)
shakerDevice.verticalMotor.setPosition(1)
println("Шейкер: цикл ${it + 1}")
}
shakerDevice.verticalMotor.setPosition(2)
shakerDevice.horizontalMotor.setPosition(1)
println("Шейкер: окончание движения")
// Шаг 3: Забор и измерение пробы
executeSampling()
needleDevice.setPosition(0.0)
println("Игла вернулась в исходное положение")
}
// Функция для выполнения забора пробы
suspend fun executeSampling() {
println("Забор и измерение пробы")
needleDevice.setPosition(0.0)
needleDevice.setMode(NeedleDevice.Mode.WASHING)
needleDevice.performWashing(5)
needleDevice.setPosition(10.0)
needleDevice.setMode(NeedleDevice.Mode.SAMPLING)
needleDevice.performSampling()
needleDevice.setPosition(0.0)
needleDevice.setMode(NeedleDevice.Mode.WASHING)
needleDevice.performWashing(5)
needleDevice.setPosition(20.0)
println("Игла в положении WOC")
}
// Рецепт 2 - Автоматическое измерение
suspend fun executeRecipe2() {
println("Выполнение Рецепта 2 - Автоматическое измерение")
transportationSystem.receiveMotor.setPosition(transportationSystem.receiveMotor.getPosition() + 1)
println("Сдвинуто податчик на 1 позицию")
// Проверка лотка
if (!checkTrayInPushSystem()) {
println("Лоток отсутствует. Повторный сдвиг")
transportationSystem.receiveMotor.setPosition(transportationSystem.receiveMotor.getPosition() + 1)
} else {
executeSampling()
}
// Если достигнута последняя позиция, меняем планшет
if (transportationSystem.receiveMotor.getPosition() >= transportationSystem.receiveMotor.maxPosition) {
println("Смена планшета. Возврат податчика в исходное положение")
transportationSystem.receiveMotor.setPosition(0)
}
println("Рецепт 2 завершен")
needleDevice.setPosition(0.0)
println("Игла вернулась в исходное положение")
}
// Рецепт 3 - Одиночное измерение
suspend fun executeRecipe3() {
println("Выполнение Рецепта 3 - Одиночное измерение")
executeSampling()
println("Рецепт 3 завершен")
needleDevice.setPosition(0.0)
println("Игла вернулась в исходное положение")
}
// функция проверки наличия лотка
private suspend fun checkTrayInPushSystem(): Boolean {
println("Проверка наличия лотка в системе проталкивания")
delay(200)
return true // Имитация наличия лотка
}
}
// -----------------------
// Тестирование анализатора
// -----------------------
class AnalyzerTest {
@Test
fun testAnalyzerInitialization() = runTest {
val context = Context("TestContext")
val analyzer = AnalyzerDevice(context)
analyzer.start()
// Проверка состояния устройств
assertEquals(LifecycleState.STARTED, analyzer.lifecycleState)
assertEquals(LifecycleState.STARTED, analyzer.transportationSystem.lifecycleState)
assertEquals(LifecycleState.STARTED, analyzer.shakerDevice.lifecycleState)
assertEquals(LifecycleState.STARTED, analyzer.needleDevice.lifecycleState)
// Проверка начальных положений двигателей
assertEquals(0, analyzer.transportationSystem.slideMotor.getPosition())
assertEquals(0, analyzer.transportationSystem.pushMotor.getPosition())
assertEquals(0, analyzer.transportationSystem.receiveMotor.getPosition())
assertEquals(0, analyzer.shakerDevice.verticalMotor.getPosition())
assertEquals(0, analyzer.shakerDevice.horizontalMotor.getPosition())
assertEquals(0.0, analyzer.needleDevice.getPosition())
// Проверка состояния клапанов
assertFalse(analyzer.valveV20.getState())
assertFalse(analyzer.valveV17.getState())
assertFalse(analyzer.valveV18.getState())
assertFalse(analyzer.valveV35.getState())
analyzer.stop()
assertEquals(LifecycleState.STOPPED, analyzer.lifecycleState)
}
@Test
fun testCalibration() = runTest {
val context = Context("TestContext")
val analyzer = AnalyzerDevice(context)
analyzer.start()
// Запуск калибровки
analyzer.calibrate()
// Проверка состояния двигателей после калибровки
assertEquals(0, analyzer.transportationSystem.slideMotor.getPosition())
assertEquals(0, analyzer.transportationSystem.pushMotor.getPosition())
assertEquals(0, analyzer.transportationSystem.receiveMotor.getPosition())
assertEquals(0, analyzer.shakerDevice.verticalMotor.getPosition())
assertEquals(0, analyzer.shakerDevice.horizontalMotor.getPosition())
// Проверка давления после калибровки
assertEquals(2.0, analyzer.pressureChamberHigh.getPressure())
assertEquals(-1.0, analyzer.pressureChamberLow.getPressure())
// Проверка состояния реагентных сенсоров
assertTrue(analyzer.reagentSensor1.checkReagent())
assertTrue(analyzer.reagentSensor2.checkReagent())
assertTrue(analyzer.reagentSensor3.checkReagent())
analyzer.stop()
}
@Test
fun testRecipe1() = runTest {
val context = Context("TestContext")
val analyzer = AnalyzerDevice(context)
analyzer.start()
// Выполнение рецепта 1 (подачи пробирки)
analyzer.executeRecipe1()
// Проверка конечных состояний после рецепта
assertEquals(1, analyzer.transportationSystem.slideMotor.getPosition())
assertEquals(1, analyzer.shakerDevice.verticalMotor.getPosition())
assertEquals(1, analyzer.shakerDevice.horizontalMotor.getPosition())
// Проверка положения иглы после забора пробы
assertEquals(0.0, analyzer.needleDevice.getPosition())
analyzer.stop()
}
@Test
fun testRecipe2() = runTest {
val context = Context("TestContext")
val analyzer = AnalyzerDevice(context)
analyzer.start()
// Выполнение рецепта 2 (автоматическое измерение)
analyzer.executeRecipe2()
// Проверка конечного положения двигателей
assertTrue(analyzer.transportationSystem.receiveMotor.getPosition() > 0)
// Проверка иглы после выполнения рецепта
assertEquals(0.0, analyzer.needleDevice.getPosition())
analyzer.stop()
}
@Test
fun testRecipe3() = runTest {
val context = Context("TestContext")
val analyzer = AnalyzerDevice(context)
analyzer.start()
// Выполнение рецепта 3 (одиночное измерение)
analyzer.executeRecipe3()
// Проверка иглы после одиночного измерения
assertEquals(0.0, analyzer.needleDevice.getPosition())
// Проверка завершения одиночного измерения
println("Одиночное измерение завершено")
analyzer.stop()
}
@Test
fun testDetailedMotorMovements() = runTest {
val context = Context("TestContext")
val analyzer = AnalyzerDevice(context)
analyzer.start()
// Проверка движения двигателей при выполнении задач
analyzer.transportationSystem.slideMotor.setPosition(2)
assertEquals(2, analyzer.transportationSystem.slideMotor.getPosition())
analyzer.shakerDevice.verticalMotor.setPosition(1)
analyzer.shakerDevice.horizontalMotor.setPosition(3)
assertEquals(1, analyzer.shakerDevice.verticalMotor.getPosition())
assertEquals(3, analyzer.shakerDevice.horizontalMotor.getPosition())
analyzer.stop()
}
@Test
fun testValveStates() = runTest {
val context = Context("TestContext")
val analyzer = AnalyzerDevice(context)
analyzer.start()
// Проверка работы клапанов
analyzer.valveV20.setState(true)
assertTrue(analyzer.valveV20.getState())
analyzer.valveV17.setState(false)
assertFalse(analyzer.valveV17.getState())
analyzer.valveV18.click() // Щелчок клапаном
assertFalse(analyzer.valveV18.getState())
analyzer.stop()
}
@Test
fun testSyringePumpOperations() = runTest {
val context = Context("TestContext")
val analyzer = AnalyzerDevice(context)
analyzer.start()
// Проверка работы шприцевого насоса
analyzer.syringePumpMA100.setVolume(0.05)
assertEquals(0.05, analyzer.syringePumpMA100.getVolume())
analyzer.syringePumpMA100.setVolume(0.0)
assertEquals(0.0, analyzer.syringePumpMA100.getVolume())
analyzer.syringePumpMA25.setVolume(2.5)
assertEquals(2.5, analyzer.syringePumpMA25.getVolume())
analyzer.stop()
}
@Test
fun testNeedleOperations() = runTest {
val context = Context("TestContext")
val analyzer = AnalyzerDevice(context)
analyzer.start()
// Проверка работы иглы в режиме промывки и забора проб
analyzer.needleDevice.setMode(NeedleDevice.Mode.WASHING)
analyzer.needleDevice.setPosition(0.0)
assertEquals(0.0, analyzer.needleDevice.getPosition())
assertEquals(NeedleDevice.Mode.WASHING, analyzer.needleDevice.getMode())
analyzer.needleDevice.performWashing(5) // Промывка иглы
analyzer.needleDevice.setMode(NeedleDevice.Mode.SAMPLING)
analyzer.needleDevice.setPosition(10.0)
assertEquals(10.0, analyzer.needleDevice.getPosition())
assertEquals(NeedleDevice.Mode.SAMPLING, analyzer.needleDevice.getMode())
analyzer.needleDevice.performSampling() // Забор пробы
analyzer.stop()
}
}

View File

@ -0,0 +1,182 @@
package space.kscience.controls.constructor
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
import space.kscience.controls.api.*
import space.kscience.controls.spec.*
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.meta.*
class CompositeDeviceTest {
// simple MotorDevice and MotorSpec
class MotorSpec : DeviceSpec<MotorDevice>() {
val speed by doubleProperty(
name = "speed",
read = { propertyName ->
getSpeed()
},
write = { propertyName, value ->
setSpeed(value)
}
)
val position by doubleProperty(
name = "position",
read = { propertyName ->
getPosition()
}
// Assuming position is read-only
)
val reset by unitAction(
name = "reset",
execute = {
resetMotor()
}
)
override fun createDevice(context: Context, meta: Meta): MotorDevice {
return MotorDevice(context, meta)
}
}
class MotorDevice(
context: Context,
meta: Meta = Meta.EMPTY
) : DeviceBySpec<MotorDevice>(MotorSpec(), context, meta) {
private var _speed: Double = 0.0
private var _position: Double = 0.0
suspend fun getSpeed(): Double = _speed
suspend fun setSpeed(value: Double) {
_speed = value
}
suspend fun getPosition(): Double = _position
suspend fun resetMotor() {
_speed = 0.0
_position = 0.0
}
override fun toString(): String = "MotorDevice"
}
data class Position(val base: Double, val elbow: Double, val wrist: Double)
object PositionConverter : MetaConverter<Position> {
override fun readOrNull(source: Meta): Position? {
val base = source["base"]?.value?.number?.toDouble() ?: return null
val elbow = source["elbow"]?.value?.number?.toDouble() ?: return null
val wrist = source["wrist"]?.value?.number?.toDouble() ?: return null
return Position(base, elbow, wrist)
}
override fun convert(obj: Position): Meta = Meta {
"base" put obj.base
"elbow" put obj.elbow
"wrist" put obj.wrist
}
}
object RobotArmSpec : DeviceSpec<RobotArmDevice>() {
val baseMotorSpec = MotorSpec()
val elbowMotorSpec = MotorSpec()
val wristMotorSpec = MotorSpec()
val baseMotor by device(baseMotorSpec)
val elbowMotor by device(elbowMotorSpec)
val wristMotor by device(wristMotorSpec)
val moveToPosition by action(
inputConverter = PositionConverter,
outputConverter = MetaConverter.unit,
name = "moveToPosition",
execute = { input ->
baseMotor.writeProperty("speed", Meta(input.base))
elbowMotor.writeProperty("speed", Meta(input.elbow))
wristMotor.writeProperty("speed", Meta(input.wrist))
}
)
override fun createDevice(context: Context, meta: Meta): RobotArmDevice {
return RobotArmDevice(context, meta)
}
}
class RobotArmDevice(
context: Context,
meta: Meta = Meta.EMPTY
) : DeviceBySpec<RobotArmDevice>(RobotArmSpec, context, meta) {
val baseMotor: MotorDevice get() = nestedDevices["baseMotor"] as MotorDevice
val elbowMotor: MotorDevice get() = nestedDevices["elbowMotor"] as MotorDevice
val wristMotor: MotorDevice get() = nestedDevices["wristMotor"] as MotorDevice
override suspend fun onStart() {
super.onStart()
}
override suspend fun onStop() {
super.onStop()
}
override fun toString(): String = "RobotArmDevice"
}
@Test
fun testNestedDeviceInitialization() = runTest {
val context = Context("TestContext")
val robotArm = RobotArmDevice(context)
// Start the robot arm device
robotArm.start()
// Check that all motors are started
assertEquals(LifecycleState.STARTED, robotArm.baseMotor.lifecycleState)
assertEquals(LifecycleState.STARTED, robotArm.elbowMotor.lifecycleState)
assertEquals(LifecycleState.STARTED, robotArm.wristMotor.lifecycleState)
// Stop the robot arm device
robotArm.stop()
}
@Test
fun testCompositeDevicePropertyAndActionAccess() = runTest {
val context = Context("TestContext")
val robotArm = RobotArmDevice(context)
robotArm.start()
robotArm.baseMotor.writeProperty("speed", Meta(500.0))
val speedMeta = robotArm.baseMotor.getProperty("speed")
val speed = speedMeta?.value?.number?.toDouble()
assertEquals(500.0, speed)
// Stop the robot arm device
robotArm.stop()
}
@Test
fun testCompositeDeviceLifecycleManagement() = runTest {
val context = Context("TestContext")
val robotArm = RobotArmDevice(context)
// Start the robot arm device
robotArm.start()
assertEquals(LifecycleState.STARTED, robotArm.lifecycleState)
assertEquals(LifecycleState.STARTED, robotArm.baseMotor.lifecycleState)
assertEquals(LifecycleState.STARTED, robotArm.elbowMotor.lifecycleState)
assertEquals(LifecycleState.STARTED, robotArm.wristMotor.lifecycleState)
// Stop the robot arm device
robotArm.stop()
assertEquals(LifecycleState.STOPPED, robotArm.lifecycleState)
assertEquals(LifecycleState.STOPPED, robotArm.baseMotor.lifecycleState)
assertEquals(LifecycleState.STOPPED, robotArm.elbowMotor.lifecycleState)
assertEquals(LifecycleState.STOPPED, robotArm.wristMotor.lifecycleState)
}
}

View File

@ -0,0 +1,168 @@
package space.kscience.controls.constructor
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlinx.coroutines.test.runTest
import space.kscience.controls.api.LifecycleState
import space.kscience.controls.spec.DeviceActionSpec
import space.kscience.controls.spec.DeviceBase
import space.kscience.controls.spec.DeviceBySpec
import space.kscience.controls.spec.DevicePropertySpec
import space.kscience.controls.spec.DeviceSpec
import space.kscience.controls.spec.doubleProperty
import space.kscience.controls.spec.unitAction
import space.kscience.controls.spec.validate
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.number
class DeviceSpecTest {
class MotorSpec : DeviceSpec<MotorDevice>() {
val speed by doubleProperty(
name = "speed",
read = { propertyName ->
getSpeed()
},
write = { propertyName, value ->
setSpeed(value)
}
)
val position by doubleProperty(
name = "position",
read = { propertyName ->
getPosition()
}
// Assuming position is read-only
)
val reset by unitAction(
name = "reset",
execute = {
resetMotor()
}
)
}
class MotorDevice(
context: Context,
meta: Meta = Meta.EMPTY
) : DeviceBySpec<MotorDevice>(MotorSpec(), context, meta) {
private var _speed: Double = 0.0
private var _position: Double = 0.0
suspend fun getSpeed(): Double = _speed
suspend fun setSpeed(value: Double) {
_speed = value
}
suspend fun getPosition(): Double = _position
suspend fun resetMotor() {
_speed = 0.0
_position = 0.0
}
override fun toString(): String = "MotorDevice"
}
@Test
fun testDevicePropertyDefinitionAndAccess() = runTest {
val context = Context("TestContext")
val motor = MotorDevice(context)
motor.start()
val speedMeta = Meta(1000.0)
motor.writeProperty("speed", speedMeta)
val speedMetaRead = motor.readProperty("speed")
val speed = speedMetaRead.value?.number?.toDouble()
assertEquals(1000.0, speed)
val positionMeta = motor.readProperty("position")
val position = positionMeta.value?.number?.toDouble()
assertEquals(0.0, position)
motor.stop()
}
@Test
fun testDeviceActionDefinitionAndExecution() = runTest {
val context = Context("TestContext")
val motor = MotorDevice(context)
motor.start()
val speedMeta = Meta(1000.0)
motor.writeProperty("speed", speedMeta)
motor.execute("reset", Meta.EMPTY)
val speedMetaRead = motor.readProperty("speed")
val speed = speedMetaRead.value?.number?.toDouble()
assertEquals(0.0, speed)
val positionMeta = motor.readProperty("position")
val position = positionMeta.value?.number?.toDouble()
assertEquals(0.0, position)
motor.stop()
}
@Test
fun testDeviceLifecycleManagement() = runTest {
val context = Context("TestContext")
val motor = MotorDevice(context)
assertEquals(LifecycleState.STOPPED, motor.lifecycleState)
motor.start()
assertEquals(LifecycleState.STARTED, motor.lifecycleState)
motor.stop()
assertEquals(LifecycleState.STOPPED, motor.lifecycleState)
}
@Test
fun testDeviceErrorHandling() = runTest {
val context = Context("TestContext")
val motor = MotorDevice(context)
motor.start()
assertFailsWith<IllegalStateException> {
motor.readProperty("nonExistentProperty")
}
assertFailsWith<IllegalStateException> {
motor.execute("nonExistentAction", Meta.EMPTY)
}
motor.stop()
}
@Test
fun testDeviceSpecValidation() = runTest {
val context = Context("TestContext")
val motor = MotorDevice(context)
motor.spec.validate(motor)
val invalidMotor = object : DeviceBase<MotorDevice>(context, Meta.EMPTY) {
override val properties: Map<String, DevicePropertySpec<MotorDevice, *>>
get() = emptyMap()
override val actions: Map<String, DeviceActionSpec<MotorDevice, *, *>>
get() = emptyMap()
override fun toString(): String = "InvalidMotorDevice"
}
// Expect validation to fail
assertFailsWith<IllegalStateException> {
motor.spec.validate(invalidMotor)
}
}
}

View File

@ -1,89 +0,0 @@
package space.kscience.controls.constructor
import space.kscience.controls.constructor.dsl.core.EquationSystem
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
import space.kscience.controls.constructor.dsl.core.equations.*
import space.kscience.controls.constructor.dsl.core.expressions.*
import space.kscience.controls.constructor.dsl.core.units.Units
import space.kscience.controls.constructor.dsl.core.variables.Variable
class EquationsTest {
@Test
fun testCreatingEquations() {
val xVar = Variable("x", Units.meter)
val xExpr = VariableExpression(xVar)
val yVar = Variable("y", Units.meter)
val yExpr = VariableExpression(yVar)
val constExpr = ConstantExpression(5.0, Units.meter)
// Regular equation: x = y + 5
val equation = Equation(xExpr, BinaryExpression(yExpr, BinaryOperator.ADD, constExpr))
assertEquals(xExpr, equation.leftExpression)
assertEquals(BinaryExpression(yExpr, BinaryOperator.ADD, constExpr), equation.rightExpression)
// Conditional equation: if x > 0 then y = x else y = -x
val condition = BinaryExpression(xExpr, BinaryOperator.GREATER_THAN, ConstantExpression(0.0, Units.meter))
val trueEquation = Equation(yExpr, xExpr)
val falseEquation = Equation(yExpr, BinaryExpression(ConstantExpression(0.0, Units.meter), BinaryOperator.SUBTRACT, xExpr))
val conditionalEquation = ConditionalEquation(condition, trueEquation, falseEquation)
assertEquals(condition, conditionalEquation.condition)
assertEquals(trueEquation, conditionalEquation.trueEquation)
assertEquals(falseEquation, conditionalEquation.falseEquation)
// Initial equation: x(0) = 0
val initialEquation = InitialEquation(xExpr, ConstantExpression(0.0, Units.meter))
assertEquals(xExpr, initialEquation.leftExpression)
assertEquals(ConstantExpression(0.0, Units.meter), initialEquation.rightExpression)
}
@Test
fun testUnitCompatibilityInEquations() {
val lengthVar = Variable("length", Units.meter)
val lengthExpr = VariableExpression(lengthVar)
val timeVar = Variable("time", Units.second)
val timeExpr = VariableExpression(timeVar)
// Correct equation: length = length + length
val validEquation = Equation(lengthExpr, BinaryExpression(lengthExpr, BinaryOperator.ADD, ConstantExpression(5.0, Units.meter)))
assertFailsWith<Exception> {
validEquation
}
// Incorrect equation: length = time
val exception = assertFailsWith<IllegalArgumentException> {
Equation(lengthExpr, timeExpr)
}
assertEquals("Units of left and right expressions in the equation are not compatible.", exception.message)
}
@Test
fun testEquationSystem() {
val xVar = Variable("x", Units.meter)
val xExpr = VariableExpression(xVar)
val vVar = Variable("v", Units.meter / Units.second)
val vExpr = VariableExpression(vVar)
val aVar = Variable("a", Units.meter / Units.second.pow(2.0))
val aExpr = VariableExpression(aVar)
// Differential equation: der(x) = v
val derX = DerivativeExpression(xExpr)
val eq1 = Equation(derX, vExpr)
// Algebraic equation: v = a * t
val tVar = Variable("t", Units.second)
val tExpr = VariableExpression(tVar)
val eq2 = Equation(vExpr, BinaryExpression(aExpr, BinaryOperator.MULTIPLY, tExpr))
val equationSystem = EquationSystem()
equationSystem.addEquation(eq1)
equationSystem.addEquation(eq2)
assertEquals(2, equationSystem.equations.size)
assertTrue(equationSystem.variables.containsAll(listOf("x", "v", "a", "t")))
assertTrue(equationSystem.derivativeVariables.contains("x"))
}
}

View File

@ -1,73 +0,0 @@
package space.kscience.controls.constructor
import kotlin.test.Test
import kotlin.test.assertEquals
import space.kscience.controls.constructor.dsl.core.events.Event
import space.kscience.controls.constructor.dsl.core.events.ReinitAction
import space.kscience.controls.constructor.dsl.core.events.AssignAction
import space.kscience.controls.constructor.dsl.core.expressions.BinaryExpression
import space.kscience.controls.constructor.dsl.core.expressions.BinaryOperator
import space.kscience.controls.constructor.dsl.core.expressions.ConstantExpression
import space.kscience.controls.constructor.dsl.core.expressions.VariableExpression
import space.kscience.controls.constructor.dsl.core.simulation.SimulationContext
import space.kscience.controls.constructor.dsl.core.units.Units
import space.kscience.controls.constructor.dsl.core.variables.Variable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
class EventsAndActionsTest {
@Test
fun testCreatingEvents() {
val xVar = Variable("x", Units.meter)
val xExpr = VariableExpression(xVar)
val condition = BinaryExpression(xExpr, BinaryOperator.GREATER_THAN, ConstantExpression(10.0, Units.meter))
val action = AssignAction(xVar, ConstantExpression(0.0, Units.meter))
val event = Event(condition, listOf(action))
assertEquals(condition, event.condition)
assertEquals(1, event.actions.size)
assertEquals(action, event.actions[0])
}
@Test
fun testProcessingEventsInSimulation() = runTest {
val xVar = Variable("x", Units.meter)
val xExpr = VariableExpression(xVar)
val context = SimulationContext(CoroutineScope(Dispatchers.Default)).apply {
registerVariable(xVar) // Регистрация переменной
launch { updateVariable("x", 5.0) } // Установка начального значения переменной
}
val condition = BinaryExpression(xExpr, BinaryOperator.GREATER_THAN, ConstantExpression(10.0, Units.meter))
val action = AssignAction(xVar, ConstantExpression(0.0, Units.meter))
val event = Event(condition, listOf(action))
context.events.add(event)
// Simulate x crossing the threshold
context.updateVariable("x", 15.0)
context.handleEvents()
assertEquals(0.0, context.getCurrentVariableValue("x"))
}
@Test
fun testReinitAction() = runTest {
val vVar = Variable("v", Units.meter / Units.second)
val vExpr = VariableExpression(vVar)
val context = SimulationContext(CoroutineScope(Dispatchers.Default)).apply {
registerVariable(vVar) // Регистрация переменной
launch { updateVariable("v", 20.0) } // Установка начального значения переменной
}
val action = ReinitAction(vVar, ConstantExpression(0.0, Units.meter / Units.second))
action.execute(context)
assertEquals(0.0, context.getCurrentVariableValue("v"))
}
}

View File

@ -1,101 +0,0 @@
package space.kscience.controls.constructor
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
import space.kscience.controls.constructor.dsl.core.expressions.*
import space.kscience.controls.constructor.dsl.core.units.Units
import space.kscience.controls.constructor.dsl.core.variables.Variable
class ExpressionsTest {
@Test
fun testCreationOfDifferentTypesOfExpressions() {
// Constant expression
val constExpr = ConstantExpression(5.0, Units.meter)
assertEquals(5.0, constExpr.value)
assertEquals(Units.meter, constExpr.unit)
// Variable expression
val speedVar = Variable("speed", Units.meter / Units.second)
val speedExpr = VariableExpression(speedVar)
assertEquals(speedVar, speedExpr.variable)
assertEquals(Units.meter / Units.second, speedExpr.unit)
// Binary expression (addition)
val distanceVar = Variable("distance", Units.meter)
val distanceExpr = VariableExpression(distanceVar)
val totalDistanceExpr = BinaryExpression(distanceExpr, BinaryOperator.ADD, constExpr)
assertEquals(Units.meter, totalDistanceExpr.unit)
// Function call expression
val sinExpr = FunctionCallExpression("sin", listOf(constExpr))
assertEquals(Units.dimensionless, sinExpr.unit)
}
@Test
fun testEvaluationOfExpressions() {
val xVar = Variable("x", Units.meter)
val xExpr = VariableExpression(xVar)
val constExpr = ConstantExpression(2.0, Units.meter)
val values = mapOf("x" to 3.0)
// Binary expression: x + 2
val sumExpr = BinaryExpression(xExpr, BinaryOperator.ADD, constExpr)
assertEquals(5.0, sumExpr.evaluate(values))
// Binary expression: x * 2
val mulExpr = BinaryExpression(xExpr, BinaryOperator.MULTIPLY, ConstantExpression(2.0, Units.dimensionless))
assertEquals(6.0, mulExpr.evaluate(values))
// Function call: sin(x)
val sinExpr = FunctionCallExpression("sin", listOf(xExpr))
assertEquals(kotlin.math.sin(3.0), sinExpr.evaluate(values))
}
@Test
fun testDimensionCheckingInExpressions() {
val lengthVar = Variable("length", Units.meter)
val timeVar = Variable("time", Units.second)
val lengthExpr = VariableExpression(lengthVar)
val timeExpr = VariableExpression(timeVar)
// Correct addition
val sumExpr = BinaryExpression(lengthExpr, BinaryOperator.ADD, ConstantExpression(5.0, Units.meter))
assertFailsWith<Exception> {
sumExpr.checkDimensions()
}
// Incorrect addition (length + time)
val invalidSumExpr = BinaryExpression(lengthExpr, BinaryOperator.ADD, timeExpr)
val exception = assertFailsWith<IllegalArgumentException> {
invalidSumExpr.checkDimensions()
}
assertEquals("Units must be the same for addition or subtraction.", exception.message)
}
@Test
fun testDerivativeOfExpressions() {
val xVar = Variable("x", Units.meter)
val xExpr = VariableExpression(xVar)
val constExpr = ConstantExpression(5.0, Units.meter)
// Derivative of constant: should be zero
val derivativeConst = constExpr.derivative(xExpr)
assertTrue(derivativeConst is ConstantExpression)
assertEquals(0.0, derivativeConst.value)
// Derivative of variable with respect to itself: should be one
val derivativeVar = xExpr.derivative(xExpr)
assertTrue(derivativeVar is ConstantExpression)
assertEquals(1.0, derivativeVar.value)
// Derivative of x + 5 with respect to x: should be one
val sumExpr = BinaryExpression(xExpr, BinaryOperator.ADD, constExpr)
val derivativeSum = sumExpr.derivative(xExpr)
assertTrue(derivativeSum is ConstantExpression)
assertEquals(1.0, derivativeSum.value)
}
}

View File

@ -1,29 +0,0 @@
package space.kscience.controls.constructor
import kotlin.test.Test
import kotlin.test.assertEquals
import space.kscience.controls.constructor.dsl.core.functions.FunctionBuilder
import space.kscience.controls.constructor.dsl.core.expressions.*
import space.kscience.controls.constructor.dsl.core.variables.Variable
import space.kscience.controls.constructor.dsl.core.units.Units
class FunctionsTest {
@Test
fun testCreatingFunctions() {
val functionBuilder = FunctionBuilder("add").apply {
val aVar = input("a", Units.dimensionless)
val bVar = input("b", Units.dimensionless)
val resultVar = output("result", Units.dimensionless)
algorithm {
assign(resultVar, BinaryExpression(VariableExpression(aVar), BinaryOperator.ADD, VariableExpression(bVar)))
}
}
val addFunction = functionBuilder.build()
assertEquals("add", addFunction.name)
assertEquals(2, addFunction.inputs.size)
assertEquals(1, addFunction.outputs.size)
}
}

View File

@ -0,0 +1,76 @@
package space.kscience.controls.constructor
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlinx.coroutines.test.runTest
import space.kscience.controls.constructor.CompositeDeviceTest.Position
import space.kscience.controls.constructor.CompositeDeviceTest.PositionConverter
import space.kscience.controls.constructor.CompositeDeviceTest.RobotArmDevice
import space.kscience.controls.spec.*
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.number
class IntegrationTest {
@Test
fun testFullDeviceSimulation() = runTest {
val context = Context("TestContext")
val robotArm = RobotArmDevice(context)
robotArm.start()
val position = Position(base = 90.0, elbow = 45.0, wrist = 30.0)
robotArm.execute("moveToPosition", PositionConverter.convert(position))
val baseSpeedMeta = robotArm.baseMotor.readProperty("speed")
val baseSpeed = baseSpeedMeta.value?.number?.toDouble()
assertEquals(90.0, baseSpeed)
val elbowSpeedMeta = robotArm.elbowMotor.readProperty("speed")
val elbowSpeed = elbowSpeedMeta.value?.number?.toDouble()
assertEquals(45.0, elbowSpeed)
val wristSpeedMeta = robotArm.wristMotor.readProperty("speed")
val wristSpeed = wristSpeedMeta.value?.number?.toDouble()
assertEquals(30.0, wristSpeed)
robotArm.stop()
}
class SensorDevice(
context: Context,
meta: Meta = Meta.EMPTY
) : DeviceBySpec<SensorDevice>(SensorSpec, context, meta) {
private var _pressure: Double = 101325.0 // Atmospheric pressure in Pascals
suspend fun getPressure(): Double = _pressure
override fun toString(): String = "SensorDevice"
}
object SensorSpec : DeviceSpec<SensorDevice>() {
val pressure by doubleProperty(
name = "pressure",
read = { propertyName ->
getPressure()
}
)
}
@Test
fun testUnitsInDeviceProperties() = runTest {
val context = Context("TestContext")
val sensor = SensorDevice(context)
sensor.start()
val pressureMeta = sensor.readProperty("pressure")
val pressureValue = pressureMeta.value?.number?.toDouble()
assertEquals(101325.0, pressureValue)
sensor.stop()
}
}

View File

@ -0,0 +1,18 @@
package space.kscience.controls.constructor
import space.kscience.controls.constructor.units.Degrees
import space.kscience.controls.constructor.units.Meters
import space.kscience.controls.constructor.units.NumericalValue
import space.kscience.controls.constructor.units.Radians
import space.kscience.controls.constructor.units.SIPrefix
import space.kscience.controls.constructor.units.Seconds
import space.kscience.controls.constructor.units.UnitsOfMeasurement
import space.kscience.controls.constructor.units.withPrefix
import kotlin.math.PI
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
class UnitsOfMeasurementTest {
}

View File

@ -1,85 +0,0 @@
package space.kscience.controls.constructor
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import space.kscience.controls.constructor.dsl.core.units.Dimension
import space.kscience.controls.constructor.dsl.core.units.PhysicalUnit
import space.kscience.controls.constructor.dsl.core.units.Units
class UnitsTest {
@Test
fun testCreationOfBaseUnits() {
val meter = Units.meter
assertEquals("meter", meter.name)
assertEquals("m", meter.symbol)
assertEquals(1.0, meter.conversionFactorToSI)
assertEquals(Dimension(length = 1.0), meter.dimension)
val kilogram = Units.kilogram
assertEquals("kilogram", kilogram.name)
assertEquals("kg", kilogram.symbol)
assertEquals(1.0, kilogram.conversionFactorToSI)
assertEquals(Dimension(mass = 1.0), kilogram.dimension)
val second = Units.second
assertEquals("second", second.name)
assertEquals("s", second.symbol)
assertEquals(1.0, second.conversionFactorToSI)
assertEquals(Dimension(time = 1.0), second.dimension)
}
@Test
fun testArithmeticOperationsOnUnits() {
val meter = Units.meter
val second = Units.second
val kilogram = Units.kilogram
val velocityUnit = meter / second
assertEquals(Dimension(length = 1.0, time = -1.0), velocityUnit.dimension)
assertEquals("m/s", velocityUnit.symbol)
val accelerationUnit = meter / (second * second)
assertEquals(Dimension(length = 1.0, time = -2.0), accelerationUnit.dimension)
assertEquals("m/s^2", accelerationUnit.symbol)
val forceUnit = kilogram * accelerationUnit
assertEquals(Dimension(mass = 1.0, length = 1.0, time = -2.0), forceUnit.dimension)
assertEquals("kg*m/s^2", forceUnit.symbol)
}
@Test
fun testUnitCompatibilityForAdditionAndSubtraction() {
val meter = Units.meter
val kilogram = Units.kilogram
// Correct addition
val length1 = meter
val length2 = meter
assertEquals(length1.dimension, length2.dimension)
// Incorrect addition should throw exception
val mass = kilogram
val exception = assertFailsWith<IllegalArgumentException> {
if (length1.dimension != mass.dimension) {
throw IllegalArgumentException("Units are not compatible for addition.")
}
}
assertEquals("Units are not compatible for addition.", exception.message)
}
@Test
fun testUnitConversion() {
val kilometer = PhysicalUnit("kilometer", "km", 1000.0, Units.meter.dimension)
val centimeter = PhysicalUnit("centimeter", "cm", 0.01, Units.meter.dimension)
// Convert 1 km to meters
val kmInMeters = 1.0 * kilometer.conversionFactorToSI
assertEquals(1000.0, kmInMeters)
// Convert 100 cm to meters
val cmInMeters = 100.0 * centimeter.conversionFactorToSI
assertEquals(1.0, cmInMeters)
}
}

View File

@ -0,0 +1,88 @@
package space.kscience.controls.constructor
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlinx.coroutines.test.runTest
import space.kscience.controls.spec.*
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.boolean
import space.kscience.dataforge.meta.number
class UtilitiesTest {
class SensorSpec : DeviceSpec<SensorDevice>() {
val temperature by doubleProperty(
name = "temperature",
read = { propertyName ->
getTemperature()
},
write = { propertyName, value ->
setTemperature(value)
}
)
val isActive by booleanProperty(
name = "isActive",
read = { propertyName ->
isActive()
},
write = { propertyName, value ->
setActive(value)
}
)
}
class SensorDevice(
context: Context,
meta: Meta = Meta.EMPTY
) : DeviceBySpec<SensorDevice>(SensorSpec(), context, meta) {
private var _temperature: Double = 25.0
private var _isActive: Boolean = true
suspend fun getTemperature(): Double = _temperature
suspend fun setTemperature(value: Double) {
_temperature = value
}
suspend fun isActive(): Boolean = _isActive
suspend fun setActive(value: Boolean) {
_isActive = value
}
override fun toString(): String = "SensorDevice"
}
@Test
fun testDoublePropertyUtility() = runTest {
val context = Context("TestContext")
val sensor = SensorDevice(context)
sensor.start()
sensor.writeProperty("temperature", Meta(30.0))
val tempMeta = sensor.readProperty("temperature")
val temperature = tempMeta.value?.number?.toDouble()
assertEquals(30.0, temperature)
sensor.stop()
}
@Test
fun testBooleanPropertyUtility() = runTest {
val context = Context("TestContext")
val sensor = SensorDevice(context)
sensor.start()
sensor.writeProperty("isActive", Meta(false))
val activeMeta = sensor.readProperty("isActive")
val isActive = activeMeta.value?.boolean
assertEquals(false, isActive)
sensor.stop()
}
}

View File

@ -1,41 +0,0 @@
package space.kscience.controls.constructor
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertNull
import space.kscience.controls.constructor.dsl.core.units.Units
import space.kscience.controls.constructor.dsl.core.variables.ParameterVariable
import space.kscience.controls.constructor.dsl.core.variables.Variable
class VariablesTest {
@Test
fun testCreationOfVariablesAndParameters() {
val length = Variable("length", Units.meter)
assertEquals("length", length.name)
assertEquals(Units.meter, length.unit)
assertNull(length.value)
val mass = ParameterVariable("mass", Units.kilogram, value = 10.0)
assertEquals("mass", mass.name)
assertEquals(Units.kilogram, mass.unit)
assertEquals(10.0, mass.value)
}
@Test
fun testSettingAndGettingValuesOfVariablesAndParameters() {
val speed = Variable("speed", Units.meter / Units.second)
speed.value = 15.0
assertEquals(15.0, speed.value)
val density = ParameterVariable("density", Units.kilogram / Units.meter.pow(3.0), value = 1000.0)
assertEquals(1000.0, density.value)
// Attempt to create a parameter with value outside the allowed range
val exception = assertFailsWith<IllegalArgumentException> {
ParameterVariable("temperature", Units.kelvin, value = -10.0, min = 0.0)
}
assertEquals("Value of parameter temperature is less than minimum allowed value.", exception.message)
}
}

View File

@ -50,6 +50,13 @@ public abstract class DeviceBase<D : Device>(
final override val meta: Meta = Meta.EMPTY,
) : CachingDevice {
private val stateLock = Mutex()
/**
* Logical state store
*/
private val logicalState: MutableMap<String, Meta?> = HashMap()
/**
* Collection of property specifications
*/
@ -89,20 +96,12 @@ public abstract class DeviceBase<D : Device>(
}
)
/**
* Logical state store
*/
private val logicalState: HashMap<String, Meta?> = HashMap()
public override val messageFlow: SharedFlow<DeviceMessage> get() = sharedMessageFlow
@Suppress("UNCHECKED_CAST")
internal val self: D
get() = this as D
private val stateLock = Mutex()
/**
* Update logical property state and notify listeners
*/

View File

@ -2,7 +2,7 @@ package space.kscience.controls.spec
import space.kscience.controls.api.Device
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.*
/**
* A device generated from specification
@ -16,14 +16,36 @@ public open class DeviceBySpec<D : Device>(
override val properties: Map<String, DevicePropertySpec<D, *>> get() = spec.properties
override val actions: Map<String, DeviceActionSpec<D, *, *>> get() = spec.actions
// Map to store instances of nested devices
private val _nestedDevices = hashMapOf<String, Device>()
// Provide access to nested devices
public val nestedDevices: Map<String, Device> get() = _nestedDevices
override suspend fun onStart(): Unit = with(spec) {
for ((name, deviceSpec) in spec.nestedDeviceSpecs) {
val nestedDevice = createNestedDevice(deviceSpec, name)
_nestedDevices[name] = nestedDevice
nestedDevice.start()
}
self.onOpen()
}
override suspend fun onStop(): Unit = with(spec){
for (device in _nestedDevices.values) {
device.stop()
}
self.onClose()
}
override fun toString(): String = "Device(spec=$spec)"
private fun <ND : Device> createNestedDevice(deviceSpec: DeviceSpec<ND>, name: String): ND {
// Create an instance of the nested device.
val nestedMeta = meta[name] ?: Meta.EMPTY
val nestedDevice = deviceSpec.createDevice(context, nestedMeta)
return nestedDevice
}
}

View File

@ -2,6 +2,7 @@ package space.kscience.controls.spec
import kotlinx.coroutines.withContext
import space.kscience.controls.api.*
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MetaConverter
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
@ -20,15 +21,34 @@ public val MetaConverter.Companion.unit: MetaConverter<Unit> get() = UnitMetaCon
@OptIn(InternalDeviceAPI::class)
public abstract class DeviceSpec<D : Device> {
//initializing the metadata property for everyone
private val _properties = hashMapOf<String, DevicePropertySpec<D, *>>(
DeviceMetaPropertySpec.name to DeviceMetaPropertySpec
)
// Map to store properties
private val _properties = hashMapOf<String, DevicePropertySpec<D, *>>()
public val properties: Map<String, DevicePropertySpec<D, *>> get() = _properties
private val _actions = HashMap<String, DeviceActionSpec<D, *, *>>()
// Map to store actions
private val _actions = hashMapOf<String, DeviceActionSpec<D, *, *>>()
public val actions: Map<String, DeviceActionSpec<D, *, *>> get() = _actions
/**
* Registers a property in the spec.
*/
public fun <T, P : DevicePropertySpec<D, T>> registerProperty(deviceProperty: P): P {
_properties[deviceProperty.name] = deviceProperty
return deviceProperty
}
// Map to store nested device specifications
private val _nestedDeviceSpecs = hashMapOf<String, DeviceSpec<*>>()
public val nestedDeviceSpecs: Map<String, DeviceSpec<*>> get() = _nestedDeviceSpecs
/**
* Registers an action in the spec.
*/
public fun <I, O> registerAction(deviceAction: DeviceActionSpec<D, I, O>): DeviceActionSpec<D, I, O> {
_actions[deviceAction.name] = deviceAction
return deviceAction
}
public open suspend fun D.onOpen() {
}
@ -36,12 +56,6 @@ public abstract class DeviceSpec<D : Device> {
public open suspend fun D.onClose() {
}
public fun <T, P : DevicePropertySpec<D, T>> registerProperty(deviceProperty: P): P {
_properties[deviceProperty.name] = deviceProperty
return deviceProperty
}
public fun <T> property(
converter: MetaConverter<T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
@ -110,12 +124,6 @@ public abstract class DeviceSpec<D : Device> {
}
}
public fun <I, O> registerAction(deviceAction: DeviceActionSpec<D, I, O>): DeviceActionSpec<D, I, O> {
_actions[deviceAction.name] = deviceAction
return deviceAction
}
public fun <I, O> action(
inputConverter: MetaConverter<I>,
outputConverter: MetaConverter<O>,
@ -156,6 +164,24 @@ public abstract class DeviceSpec<D : Device> {
deviceAction
}
}
public open fun createDevice(context: Context, meta: Meta): D {
// Since DeviceSpec<D> doesn't know how to create an instance of D,
// you need to override this method in subclasses.
throw NotImplementedError("createDevice must be implemented in subclasses")
}
public fun <ND : Device> device(
deviceSpec: DeviceSpec<ND>,
name: String? = null
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceSpec<ND>>> =
PropertyDelegateProvider { _, property ->
val deviceName = name ?: property.name
// Register the nested device spec
_nestedDeviceSpecs[deviceName] = deviceSpec
ReadOnlyProperty { _, _ -> deviceSpec }
}
}
/**

View File

@ -6,6 +6,8 @@ import space.kscience.controls.api.metaDescriptor
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MetaConverter
import space.kscience.dataforge.meta.ValueType
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
import space.kscience.dataforge.meta.string
import kotlin.properties.PropertyDelegateProvider
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KMutableProperty1
@ -191,4 +193,123 @@ public fun <D : Device> DeviceSpec<D>.metaProperty(
read: suspend D.(propertyName: String) -> Meta,
write: suspend D.(propertyName: String, value: Meta) -> Unit
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, Meta>>> =
mutableProperty(MetaConverter.meta, descriptorBuilder, name, read, write)
mutableProperty(MetaConverter.meta, descriptorBuilder, name, read, write)
public fun <D : Device> DeviceSpec<D>.doubleProperty(
description: String? = null,
name: String? = null,
read: suspend D.(String) -> Double?,
write: (suspend D.(String, Double) -> Unit)? = null
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Double>>> {
val converter = MetaConverter.double
val descriptorBuilder: PropertyDescriptor.() -> Unit = {
this.description = description
metaDescriptor {
valueType(ValueType.NUMBER)
}
}
return if (write != null) {
mutableProperty(converter, descriptorBuilder, name, read, write)
} else {
property(converter, descriptorBuilder, name, read)
}
}
public fun <D : Device> DeviceSpec<D>.booleanProperty(
description: String? = null,
name: String? = null,
read: suspend D.(String) -> Boolean?,
write: (suspend D.(String, Boolean) -> Unit)? = null
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Boolean>>> {
val converter = MetaConverter.boolean
val descriptorBuilder: PropertyDescriptor.() -> Unit = {
this.description = description
metaDescriptor {
valueType(ValueType.BOOLEAN)
}
}
return if (write != null) {
mutableProperty(converter, descriptorBuilder, name, read, write)
} else {
property(converter, descriptorBuilder, name, read)
}
}
public fun <D : Device> DeviceSpec<D>.stringProperty(
description: String? = null,
name: String? = null,
read: suspend D.(String) -> String?,
write: (suspend D.(String, String) -> Unit)? = null
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, String>>> {
val converter = MetaConverter.string
val descriptorBuilder: PropertyDescriptor.() -> Unit = {
this.description = description
metaDescriptor {
valueType(ValueType.STRING)
}
}
return if (write != null) {
mutableProperty(converter, descriptorBuilder, name, read, write)
} else {
property(converter, descriptorBuilder, name, read)
}
}
public fun <D : Device> DeviceSpec<D>.intProperty(
description: String? = null,
name: String? = null,
read: suspend D.(propertyName: String) -> Int?,
write: (suspend D.(propertyName: String, value: Int) -> Unit)? = null
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Int>>> {
val converter = MetaConverter.int
val descriptorBuilder: PropertyDescriptor.() -> Unit = {
this.description = description
metaDescriptor {
valueType(ValueType.NUMBER)
}
}
return if (write != null) {
mutableProperty(converter, descriptorBuilder, name, read, write)
} else {
property(converter, descriptorBuilder, name, read)
}
}
public fun <E : Enum<E>, D : Device> DeviceSpec<D>.enumProperty(
enumValues: Array<E>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.(propertyName: String) -> E?,
write: (suspend D.(propertyName: String, value: E) -> Unit)? = null
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, E>>> {
val converter = object : MetaConverter<E> {
override val descriptor: MetaDescriptor = MetaDescriptor {
valueType(ValueType.STRING)
allowedValues(enumValues.map { it.name })
}
override fun readOrNull(source: Meta): E? {
val value = source.string ?: return null
return enumValues.firstOrNull { it.name == value }
}
override fun convert(obj: E): Meta = Meta(obj.name)
}
return if (write != null) {
mutableProperty(
converter = converter,
descriptorBuilder = descriptorBuilder,
name = name,
read = read,
write = write
)
} else {
property(
converter = converter,
descriptorBuilder = descriptorBuilder,
name = name,
read = read
)
}
}