diff --git a/Sources/PoieticFlows/Compiler/Compiler.swift b/Sources/PoieticFlows/Compiler/Compiler.swift index 7c90dbd..17beea0 100644 --- a/Sources/PoieticFlows/Compiler/Compiler.swift +++ b/Sources/PoieticFlows/Compiler/Compiler.swift @@ -8,51 +8,9 @@ import PoieticCore -/// Error thrown by the compiler during compilation. +/// Systems required to be run for creating a simulation plan. /// -/// The only relevant case is ``hasIssues``, any other case means a programming error. -/// -/// After catching the ``hasIssues`` error, the caller might get the issues from -/// the compiler and propagate them to the user. -/// -public enum CompilerError: Error { - case issues([ObjectID:[Issue]]) - - /// Error caused by some internal functioning. This error typically means something was not - /// correctly validated either within the library or by an application. The internal error - /// is not caused by the user. - case internalError(InternalCompilerError) - -} - -/// Error caused by some compiler internals, not by the user. -/// -/// This error should not be displayed to the user fully, only as a debug information or as an -/// information provided to the developers by the user. -/// -public enum InternalCompilerError: Error, Equatable { - /// Error thrown during compilation that should be captured by the compiler. - /// - /// Used to indicate that the compilation might continue to collect more errors, but must - /// result in an error at the end. - /// - /// This error should never escape the compiler. - /// - case objectIssue - - /// Attribute is missing or attribute type is mismatched. This error means - /// that the frame is not valid according to the ``FlowsMetamodel``. - case attributeExpectationFailure(ObjectID, String) - - /// Formula compilation failed in an unexpected way. - case formulaCompilationFailure(ObjectID) - - // Invalid Frame Error - validation on the caller side failed - case structureTypeMismatch(ObjectID) - case objectNotFound(ObjectID) -} - -nonisolated(unsafe) public let ModelInspectionSystemGroup: [System.Type] = [ +nonisolated(unsafe) public let SimulationPlanningSystems: [System.Type] = [ ExpressionParserSystem.self, ParameterResolutionSystem.self, ComputationOrderSystem.self, @@ -62,56 +20,15 @@ nonisolated(unsafe) public let ModelInspectionSystemGroup: [System.Type] = [ SimulationPlanningSystem.self, ] -nonisolated(unsafe) public let SimulationPlanningSystemGroup: [System.Type] = -ModelInspectionSystemGroup + [ - SimulationPlanningSystem.self, +nonisolated(unsafe) public let SimulationRunningSystems: [System.Type] = [ + StockFlowSimulationSystem.self, ] -nonisolated(unsafe) public let SimulationPresentationSystemGroup: [System.Type] = -SimulationPlanningSystemGroup + [ - ChartResolutionSystem.self, -] - -/// Legacy wrapper to provide same API. DO NOT USE! -/// -/// An object that compiles the model into an internal representation called Compiled Model. -/// -/// The design represents an idea or a creation of a user in a form that -/// is closest to the user. To perform a simulation we need a different form -/// that can be interpreted by a machine. -/// -/// The purpose of the compiler is to validate the design and -/// translate it into an internal representation. +/// Systems used to present the simulation results. /// -/// - SeeAlso: ``compile()``, ``SimulationPlan`` +/// The systems in this collection are expected to be run after ``SimulationPlanningSystems``. /// -@available(*, deprecated, message: "Moving towards Systems") -public class Compiler { - public let frame: AugmentedFrame - - @available(*, deprecated, message: "Moving towards Systems") - public init(frame: DesignFrame) { - self.frame = AugmentedFrame(frame) - } - - @available(*, deprecated, message: "Moving towards Systems") - public func compile() throws (CompilerError) -> SimulationPlan { - let systems = SystemGroup(SimulationPlanningSystemGroup) - - do { - try systems.update(frame) - } - catch { - fatalError("Execution failed: \(error)") - } - - if frame.hasIssues { - throw .issues(frame.issues) - } - guard let plan: SimulationPlan = frame.component(for: .Frame) else { - fatalError("Plan was not created") - } - return plan - } -} +nonisolated(unsafe) public let SimulationPresentationSystems: [System.Type] = [ + ChartResolutionSystem.self, +] diff --git a/Sources/PoieticFlows/Components/Chart.swift b/Sources/PoieticFlows/Components/Chart.swift index 535b8a0..6a3e91c 100644 --- a/Sources/PoieticFlows/Components/Chart.swift +++ b/Sources/PoieticFlows/Components/Chart.swift @@ -63,3 +63,20 @@ public struct ChartComponent: Component { public let series: [ObjectSnapshot] } +extension ChartComponent: InspectableComponent { + public static let attributeKeys: [String] = ["name"] + public func attribute(forKey key: String) -> Variant? { + switch key { + case "name": name.map { Variant($0) } + default: nil + } + } + + public static let toManyDesignReferenceKeys: [String] = ["series"] + public func designReferences(forKey key: String) -> [DesignEntityID] { + switch key { + case "series": series.map {$0.objectID} + default: [] + } + } +} diff --git a/Sources/PoieticFlows/Components/SimulationComponents.swift b/Sources/PoieticFlows/Components/SimulationComponents.swift index d5b9dcd..7c538dc 100644 --- a/Sources/PoieticFlows/Components/SimulationComponents.swift +++ b/Sources/PoieticFlows/Components/SimulationComponents.swift @@ -11,7 +11,7 @@ import PoieticCore /// /// The computational dependency is determined by flow and parameters edges in the design graph. /// -/// - **Produced by:** ``SimulationOrderDependencySystem`` +/// - **Produced by:** ``ComputationOrderSystem`` /// public struct SimulationOrderComponent: Component { internal init(objects: [ObjectSnapshot] = [], stocks: [ObjectID] = [], flows: [ObjectID] = []) { @@ -44,7 +44,7 @@ public struct SimulationOrderComponent: Component { /// can incorporate variety of object types into the simulation. However, computational perspective /// we recognise only three roles of nodes: stocks, flows and auxiliaries. /// -/// - **Produced by:** ``SimulationOrderDependencySystem`` +/// - **Produced by:** ``ComputationOrderSystem`` /// public struct SimulationRoleComponent: Component { public var role: SimulationObject.Role @@ -69,10 +69,10 @@ public struct SimulationObjectNameComponent: Component { /// - SeeAlso: ``StockDependencySystem``, ``FlowCollectorSystem``. /// public struct StockComponent: Component { - /// List of ``ObjectType/FlowRate`` nodes that fill the stock. + /// List of ``/PoieticCore/ObjectType/FlowRate`` nodes that fill the stock. public let inflowRates: [ObjectID] - /// List of ``ObjectType/FlowRate`` nodes that drain the stock. + /// List of ``/PoieticCore/ObjectType/FlowRate`` nodes that drain the stock. public let outflowRates: [ObjectID] /// List of stocks that are drained. diff --git a/Sources/PoieticFlows/Components/SimulationPlan.swift b/Sources/PoieticFlows/Components/SimulationPlan.swift index f4c3fc5..0f7fb2b 100644 --- a/Sources/PoieticFlows/Components/SimulationPlan.swift +++ b/Sources/PoieticFlows/Components/SimulationPlan.swift @@ -7,34 +7,25 @@ import PoieticCore -/// Core structure describing the simulation. +/// Structure according to which a simulation is performed. /// -/// Simulation plan describes how the simulation is computed, how does the simulation state look -/// like, what is the order in which the objects are being computed. +/// The design describes the model from user's perspective. The content and data structures needed +/// for the modelling process – for the editing – are different than the data used by the machine to +/// perform the simulation. Simulation plan is contains validated and derived information from the +/// design. /// -/// The main content of the simulation plan is a list of computed objects in order of computational -/// dependency ``simulationObjects`` and a list of simulation state variables ``stateVariables``. +/// The simulation plan is created by the ``SimulationPlanningSystems`` and typically used by the +/// ``StockFlowSimulationSystem``. It can be also used to explain the simulation process (loosely +/// analogous to a SQL explain plan). /// -/// ## Uses by Applications +/// The primary content of the simulation plan is: /// -/// Applications running simulations can use the simulation plan to fetch various -/// information that is to be presented to the user or that can be expected -/// from the user as an input or as a configuration. For example: +/// - List of simulation objects ``SimulationObject`` in order of their computational dependency: ``simulationObjects``. +/// - Structure of the simulation state, list of state variables ``StateVariable``: ``stateVariables``. +/// - List of stocks with inflows and outflows resolved (``BoundStock``). +/// - List of flows with resolved stocks that the flow drains and fills (``BoundFlow``). /// -/// - ``charts`` to get a list of charts that are specified in the design -/// that the designer considers relevant to be displayed to the user. -/// - ``valueBindings`` to get a list of controls and their targets to generate -/// user interface for changing initial values of model-specific objects. -/// - ``stateVariables`` and their stored property ``StateVariable/name`` to -/// get a list of variables that can be observed. -/// - ``variable(named:)`` to fetch detailed information about a specific -/// variable. -/// - ``builtins`` to get indices of builtin variables like time. -/// - ``simulationDefaults`` for simulation run configuration. -/// -/// - Note: The simulation plan is loosely analogous to a SQL execution plan. -/// -/// - SeeAlso: ``Compiler/compile()``, ``StockFlowSimulation``, +/// - SeeAlso: ``StockFlowSimulationSystem``, ``SimulationPlanningSystems``. /// public struct SimulationPlan { internal init(simulationObjects: [SimulationObject] = [], @@ -44,7 +35,7 @@ public struct SimulationPlan { flows: [BoundFlow] = [], // charts: [Chart] = [], valueBindings: [CompiledControlBinding] = [], - simulationParameters: SimulationParameters? = nil) { + simulationParameters: SimulationSettings? = nil) { self.simulationObjects = simulationObjects self.stateVariables = stateVariables self.builtins = builtins @@ -52,7 +43,7 @@ public struct SimulationPlan { self.flows = flows // self.charts = charts self.valueBindings = valueBindings - self.simulationParameters = simulationParameters + self.simulationSettings = simulationParameters } /// List of objects that are considered in the computation computed, ordered by computational @@ -66,28 +57,24 @@ public struct SimulationPlan { /// Computing objects in this order assures that we have all the parameters computed when /// they are needed. /// - /// - SeeAlso: ``variableIndex(of:)`` + /// The order is computed by the ``ComputationOrderSystem`` and then filled with details in the + /// ``SimulationPlanningSystem``. + /// + /// - SeeAlso: ``variableIndex(_:)`` /// public let simulationObjects: [SimulationObject] /// List of simulation state variables. /// - /// The list of state variables contain values of builtins, values of - /// nodes and values of internal states. + /// The list of state variables contain values of simulation objects (usually nodes) their + /// internal states (for example previous values for delay) and built-ins. /// - /// Each node is typically assigned one state variable which represents - /// the node's value at given state. Some nodes might contain internal - /// state that might be present in multiple state variables. + /// Simulation object's state might be contained in multiple state variables. For example, delay + /// uses two state variables: list of double values for the queue and an initial value. /// - /// The internal state is typically not user-presentable and is a state - /// associated with stateful functions or other computation objects. + /// The internal state is typically not to be presented to the user. /// - /// A state of a variable is computed in the simulator with - /// ``StockFlowSimulation/update(_:)``. - /// - /// - SeeAlso: ``StockFlowSimulation/initialize(_:)``, - /// ``StockFlowSimulation/update(objectAt:in:)``, - /// ``Compiler/stateVariables`` + /// - SeeAlso: ``SimulationPlanningSystem``. /// public let stateVariables: [StateVariable] @@ -96,45 +83,30 @@ public struct SimulationPlan { /// The compiled builtin variable references a state variable that holds /// the value for the builtin variable and a kind of the builtin variable. /// - /// - SeeAlso: ``stateVariables``, ``CompiledBuiltin``, ``/PoieticCore/Variable``, - /// ``FlowsMetamodel`` - /// public let builtins: BoundBuiltins - /// Stocks ordered by the computation (parameter) dependency. - /// - /// This list contains all stocks used in the simulation and adds - /// derived information to each stock such as its inflows and outflows. + /// Stocks with resolved inflows and outflows, ordered by the computation dependency. /// - /// This property is used in computation. - /// - /// See ``SimulatedStock`` for more information. - /// - /// - SeeAlso: ``StockFlowSimulation/computeStockDelta(_:in:)``, - /// ``StockFlowSimulation/stockDifference(state:time:)`` + /// - SeeAlso: ``BoundStock``, ``StockFlowSimulationSystem``. /// public let stocks: [BoundStock] - public let flows: [BoundFlow] - - /// List of charts. + /// Flows with resolved stocks the flow drains and fills. /// - /// This property is not used during computation, it is provided for - /// consumers of the simulation state or simulation result. + /// - SeeAlso: ``BoundFlow``, ``StockFlowSimulationSystem``. /// -// public let charts: [Chart] - + public let flows: [BoundFlow] /// Compiled bindings of controls to their value objects. /// public let valueBindings: [CompiledControlBinding] - /// Collection of default values for running a simulation. + /// Time range, time delta and other settings to control the simulation. /// - /// See ``SimulationParameters`` for more information. + /// See ``SimulationSettings`` for more information. /// - public var simulationParameters: SimulationParameters? + public let simulationSettings: SimulationSettings? /// Get index into a list of computed variables for an object with given ID. /// @@ -144,7 +116,7 @@ public struct SimulationPlan { /// - Complexity: O(n) /// - SeeAlso: ``stateVariables``, ``simulationObject(_:)`` /// - public func variableIndex(of id: ObjectID) -> SimulationState.Index? { + public func variableIndex(_ id: ObjectID) -> SimulationState.Index? { // Since this is just for debug purposes, O(n) should be fine, no need // for added complexity of the code. guard let first = simulationObjects.first(where: {$0.objectID == id}) else { @@ -158,10 +130,8 @@ public struct SimulationPlan { /// This function is not used during computation, it is provided for /// consumers of the simulation state or simulation result. /// - /// The objects are computed with ``StockFlowSimulation/update(objectAt:in:)``. - /// /// - Complexity: O(n) - /// - SeeAlso: ``simulationObjects``, ``variableIndex(of:)`` + /// - SeeAlso: ``simulationObjects``, ``variableIndex(_:)`` /// public func simulationObject(_ id: ObjectID) -> SimulationObject? { return simulationObjects.first { $0.objectID == id } @@ -191,26 +161,5 @@ public struct SimulationPlan { return object } - - /// Index of a stock in a list of stocks or in a stock difference vector. - /// - /// This function is not used during computation. It is provided for - /// potential inspection, testing and debugging. - /// - /// - Precondition: The plan must contain a stock with given ID. - /// - func stockIndex(_ id: ObjectID) -> NumericVector.Index { - guard let index = stocks.firstIndex(where: { $0.objectID == id }) else { - preconditionFailure("The plan does not contain stock with ID \(id)") - } - return index - } - func flowIndex(_ id: ObjectID) -> NumericVector.Index { - guard let index = flows.firstIndex(where: { $0.objectID == id }) else { - preconditionFailure("The plan does not contain flow with ID \(id)") - } - return index - } - } diff --git a/Sources/PoieticFlows/Documentation.docc/CompiledModelAndCompiler.md b/Sources/PoieticFlows/Documentation.docc/CompiledModelAndCompiler.md index 6b8c8a8..38f4684 100644 --- a/Sources/PoieticFlows/Documentation.docc/CompiledModelAndCompiler.md +++ b/Sources/PoieticFlows/Documentation.docc/CompiledModelAndCompiler.md @@ -1,58 +1,79 @@ -# Compiled Model and Compiler +# Simulation Planning and Simulation Plan + +To perform a simulation, the computer needs to understand how it is to be performed and needs to +make sure that it is possible to simulate the model. The simulation plan is computer-oriented +representation of the model, derived from the user-oriented representation, which is ``Design``. -Compiler creates a compiled model, which is an internal representation of -a design that can be simulated. ## Overview -A design represents user's idea, user's creation. To be able to perform the -computation, the design has to be validated and converted into a -representation understandable by a simulator. That conversion is done by -the compiler. +A design represents user's idea, user's creation. To be able to perform the computation, +the design has to be validated and converted into a representation understandable by a simulator. +That conversion from ``Design`` to ``SimulationPlan`` is done by the ``SimulationPlanningSystem`` +through multiple steps: -![Compiler Overview](compiler-overview) +1. Arithmetic expressions are parsed by ``ExpressionParserSystem``. +2. Computation order is determined in ``ComputationOrderSystem``. +3. Object names are resolved and bi-directional object-name mappings are created in ``NameResolutionSystem``. +4. Flows and stocks are collected and resolved in the ``FlowCollectorSystem`` and ``StockDependencySystem`` respectively. +5. The simulation plan is finalised from all the components created by the above systems. +![Compiler Overview](compiler-overview) ## Topics -### Compiler and Compiled Model +### Simulation Plan -- ``Compiler`` -- ``CompiledModel`` -- ``StockFlowView`` +- ``SimulationPlan`` +- ``SimulationObject`` +- ``StateVariable`` +- ``BoundBuiltins`` +- ``BoundStock`` +- ``BoundFlow`` +- ``SimulationSettings`` +- ``ModelError`` ### Compiled Model Components +- ``SimulationOrderComponent`` +- ``SimulationRoleComponent`` +- ``SimulationNameLookupComponent`` +- ``SimulationObjectNameComponent`` +- ``FlowRateComponent`` +- ``StockComponent`` + - ``BuiltinVariable`` - ``Chart`` -- ``CompiledBuiltin`` + +### Other + - ``CompiledControlBinding`` -- ``CompiledDelay`` -- ``CompiledSmooth`` -- ``CompiledGraphicalFunction`` -- ``CompiledStock`` +- ``BoundDelay`` +- ``BoundSmooth`` +- ``BoundGraphicalFunction`` +- ``BoundStock`` - ``ComputationalRepresentation`` -- ``SimulationDefaults`` -- ``SimulationObject`` -- ``StateVariable`` -- ``StockAdjacency`` +- ``ResolvedParametersComponent`` ### Systems -Note: This is part of experimental architecture. -- ``FormulaCompilerSystem`` -- ``ParsedFormulaComponent`` +- ``SimulationPlanningSystem`` +- ``ComputationOrderSystem`` +- ``NameResolutionSystem`` +- ``FlowCollectorSystem`` +- ``StockDependencySystem`` +- ``ParameterResolutionSystem`` -### Errors +#### Schedule Collections: -- ``CompilerError`` -- ``ObjectIssue`` -- ``ParameterStatus`` +- ``SimulationPlanningSystems`` +- ``SimulationRunningSystems`` +- ``SimulationPresentationSystems`` ### Bound Expression - ``BoundExpression`` - ``BoundVariable`` - ``ExpressionError`` -- ``bindExpression(_:variables:names:functions:)`` + diff --git a/Sources/PoieticFlows/Metamodel/BuiltinVariables.swift b/Sources/PoieticFlows/Metamodel/BuiltinVariables.swift index 260a3b0..d9c52c8 100644 --- a/Sources/PoieticFlows/Metamodel/BuiltinVariables.swift +++ b/Sources/PoieticFlows/Metamodel/BuiltinVariables.swift @@ -7,14 +7,10 @@ import PoieticCore - /// Builtin variables for the Stock and Flow simulation. /// /// The enum is used during computation to set value of a builtin variable. /// -/// - SeeAlso: ``CompiledBuiltinState``, -/// ``StockFlowSimulation/initialize(_:)``, -/// public enum BuiltinVariable: Equatable, CaseIterable, CustomStringConvertible { case time case timeDelta @@ -51,7 +47,6 @@ public enum BuiltinVariable: Equatable, CaseIterable, CustomStringConvertible { case .step: Variable.SimulationStepVariable } } - } extension Variable { diff --git a/Sources/PoieticFlows/Metamodel/Traits.swift b/Sources/PoieticFlows/Metamodel/Traits.swift index daa62ea..b8b2129 100644 --- a/Sources/PoieticFlows/Metamodel/Traits.swift +++ b/Sources/PoieticFlows/Metamodel/Traits.swift @@ -212,6 +212,10 @@ extension Trait { optional: true, abstract: "Number of steps the simulation is run by default [deprecated]" ), + Attribute("solver_type", type: .string, + optional: true, + abstract: "Solver type name" + ), // TODO: Add stop_time or final_time // TODO: Support date/time // TODO: Add Solver type diff --git a/Sources/PoieticFlows/Simulation/BoundExpression.swift b/Sources/PoieticFlows/Simulation/BoundExpression.swift index 2c1663e..0fb281d 100644 --- a/Sources/PoieticFlows/Simulation/BoundExpression.swift +++ b/Sources/PoieticFlows/Simulation/BoundExpression.swift @@ -87,126 +87,6 @@ extension ExpressionError: IssueProtocol { /// - Throws: ``ExpressionError`` when a variable or a function is not known /// or when the function arguments do not match the function's requirements. /// -public func bindExpression(_ expression: UnboundExpression, - variables: [StateVariable], - names: [String:SimulationState.Index], - functions: [String:Function]) throws (ExpressionError) -> BoundExpression { - - switch expression { - case let .value(value): - return .value(value) - - case let .variable(name): - guard let index = names[name] else { - throw ExpressionError.unknownVariable(name) - } - let variable = variables[index] - return .variable(BoundVariable(index: index, - valueType: variable.valueType)) - case let .unary(op, operand): - let funcName: String = switch op { - case "-": "__neg__" - default: fatalError("Unknown unary operator: '\(op)'. Hint: check the expression parser.") - } - - guard let function = functions[funcName] else { - fatalError("No function '\(funcName)' for unary operator: '\(op)'. Hint: Make sure it is defined in the builtin function list.") - } - - let boundOperand = try bindExpression(operand, - variables: variables, - names: names, - functions: functions) - - let result = function.signature.validate([boundOperand.valueType]) - switch result { - case .invalidNumberOfArguments: - throw ExpressionError.invalidNumberOfArguments(1, - function.signature.minimalArgumentCount) - case .typeMismatch(_): - throw ExpressionError.argumentTypeMismatch(1, "int or double") - default: - return .unary(function, boundOperand) - } - - - case let .binary(op, lhs, rhs): - let funcName: String = switch op { - case "+": "__add__" - case "-": "__sub__" - case "*": "__mul__" - case "/": "__div__" - case "%": "__mod__" - case "^": "__pow__" - // Comparison - case "==": "__eq__" - case "!=": "__ne__" - case "<" : "__lt__" - case "<=": "__le__" - case ">" : "__gt__" - case ">=": "__ge__" - default: fatalError("Unknown binary operator: '\(op)'. Internal hint: check the expression parser.") - } - - guard let function = functions[funcName] else { - fatalError("No function '\(funcName)' for binary operator: '\(op)'. Internal hint: Make sure it is defined in the builtin function list.") - } - - let lBound = try bindExpression(lhs, - variables: variables, - names: names, - functions: functions) - let rBound = try bindExpression(rhs, - variables: variables, - names: names, - functions: functions) - - let args = [lBound.valueType, rBound.valueType] - let result = function.signature.validate(args) - switch result { - case .invalidNumberOfArguments: - throw ExpressionError.invalidNumberOfArguments(2, - function.signature.minimalArgumentCount) - case .typeMismatch(let index): - // TODO: We need all indices - throw ExpressionError.argumentTypeMismatch(index.first! + 1, String(describing: function.signature.returnType)) - default: // - return .binary(function, lBound, rBound) - } - - case let .function(name, arguments): - guard let function = functions[name] else { - throw ExpressionError.unknownFunction(name) - } - - var boundArgs: [BoundExpression] = [] - - // NOTE: arguments.map(...) has no typed throw (Swift 6.0) - for arg in arguments { - let boundArg = try bindExpression(arg, - variables: variables, - names: names, - functions: functions) - boundArgs.append(boundArg) - } - - let types = boundArgs.map { $0.valueType } - let result = function.signature.validate(types) - - switch result { - case .invalidNumberOfArguments: - throw ExpressionError.invalidNumberOfArguments(arguments.count, - function.signature.minimalArgumentCount) - case .typeMismatch(let index): - // TODO: We need all indices - throw ExpressionError.argumentTypeMismatch(index.first! + 1, "int or double") - default: // - return .function(function, boundArgs) - } - } -} - -// FIXME: [REFACTORING] [SYSTEMS] Use this one instead of the one above. func bindExpression(_ expression: UnboundExpression, variables: StateVariableTable, functions: [String:Function]) diff --git a/Sources/PoieticFlows/Simulation/Scenario.swift b/Sources/PoieticFlows/Simulation/Scenario.swift new file mode 100644 index 0000000..5091869 --- /dev/null +++ b/Sources/PoieticFlows/Simulation/Scenario.swift @@ -0,0 +1,129 @@ +// +// Scenario.swift +// poietic-flows +// +// Created by Stefan Urbanek on 04/01/2026. +// + +import PoieticCore + +/// Settings of the simulation – time and solver. +/// +/// - SeeAlso: ``ScenarioParameters``. +/// +public struct SimulationSettings: Component { + /// Time of the initialisation state of the simulation. + public var initialTime: Double + + /// Advancement of time for each simulation step. + public var timeDelta: Double + + /// Number of steps to run. + /// + public var steps: UInt + + /// Final simulation time. + /// + /// Simulation is run while the simulation is less than ``endTime``. + public var endTime: Double { initialTime + timeDelta * Double(steps) } + + /// Solver type name. + /// + public var solverType: String + + /// Create new simulation settings. + /// + /// - Parameters: + /// - initialTime: Time of the initialisation state of the simulation. + /// - timeDelta: Advancement of time for each simulation step. + /// - steps: Number of steps to run. + /// - solverType: Name of a solver to be used. + /// + public init(initialTime: Double = 0.0, + timeDelta: Double = 1.0, + steps: UInt = 10, + solverType: String = "euler") + { + self.initialTime = initialTime + self.timeDelta = timeDelta + self.steps = steps + self.solverType = solverType + } + + public init(initialTime: Double = 0.0, + timeDelta: Double = 1.0, + endTime: Double, + solverType: String = "euler") + { + self.initialTime = initialTime + self.timeDelta = timeDelta + if endTime <= initialTime { + self.steps = 0 + } + else if let floor = UInt(exactly: ((endTime - initialTime) / timeDelta).rounded(.down)) { + self.steps = floor + } + else { + self.steps = 0 + } + self.solverType = solverType + } + + /// Create new simulation settings from an object. + /// + /// The object is expected to be of a ``Trait/Simulation`` type, although any object with + /// expected attributes can be used. + /// + public init(fromObject object: ObjectSnapshot) { + let initialTime = object["initial_time", default: 0.0] + let timeDelta = object["time_delta", default: 0.0] + let solverType = object["solver_type", default: "euler"] + + if let endTime: Double = object["end_time"] { + self.init(initialTime: initialTime, + timeDelta: timeDelta, + endTime: endTime, + solverType: solverType) + } + else if let steps: Int = object["steps"], steps >= 0 { + self.init(initialTime: initialTime, + timeDelta: timeDelta, + steps: UInt(steps), + solverType: solverType) + } + else { + self.init(initialTime: initialTime, + timeDelta: timeDelta, + steps: 0, + solverType: solverType) + } + + + } +} + +extension SimulationSettings: InspectableComponent { + public static let attributeKeys: [String] = [ + "initial_time", "time_delta", "end_time", "steps", "solver_type", + ] + public func attribute(forKey key: String) -> Variant? { + switch key { + case "initial_time": Variant(self.initialTime) + case "time_delta": Variant(self.timeDelta) + case "ent_time": Variant(self.endTime) + case "steps": Variant(Int(exactly: self.steps) ?? 0) + case "solver_type": Variant(self.solverType) + default: nil + } + } +} +/// Initial values of simulation variables. +/// +/// - SeeAlso: ``SimulationSettings``. +/// +public struct ScenarioParameters: Component { + public let initialValues: [ObjectID:Variant] + public init(initialValues: [ObjectID:Variant] = [:]) { + self.initialValues = initialValues + } +} diff --git a/Sources/PoieticFlows/Simulation/Simulation.swift b/Sources/PoieticFlows/Simulation/Simulation.swift index b7acc74..18746b2 100644 --- a/Sources/PoieticFlows/Simulation/Simulation.swift +++ b/Sources/PoieticFlows/Simulation/Simulation.swift @@ -7,7 +7,7 @@ import PoieticCore -// TODO: Move this to Core +// TODO: Deprecate the protocol(?) public struct SimulationError: Error { let objectID: ObjectID @@ -35,16 +35,16 @@ public protocol Simulation { /// Create and initialise a simulation state. /// /// - Parameters: - /// - step: The initial step number of the simulation. /// - time: Initial time. /// - timeDelta: Time delta between simulation steps. + /// - parameters: Initial parameters that override computed values. /// /// This function creates and computes the initial state of the computation by /// evaluating all the nodes in the order of their dependency by parameter. /// /// - Returns: Newly initialised simulation state. /// - func initialize(time: Double, timeDelta: Double, override: [ObjectID:Variant]) throws (SimulationError) -> SimulationState + func initialize(time: Double, timeDelta: Double, parameters: [ObjectID:Variant]) throws (SimulationError) -> SimulationState /// Function that updates a simulation state. /// diff --git a/Sources/PoieticFlows/Simulation/SimulationState.swift b/Sources/PoieticFlows/Simulation/SimulationState.swift index 98f67f6..d125cb8 100644 --- a/Sources/PoieticFlows/Simulation/SimulationState.swift +++ b/Sources/PoieticFlows/Simulation/SimulationState.swift @@ -11,7 +11,7 @@ import PoieticCore /// A collection of simulation state variables. /// -public struct SimulationState: CustomStringConvertible { +public struct SimulationState: Component, CustomStringConvertible { public typealias Index = Int public let step: Int diff --git a/Sources/PoieticFlows/Simulation/Simulator.swift b/Sources/PoieticFlows/Simulation/Simulator.swift deleted file mode 100644 index a1d19ad..0000000 --- a/Sources/PoieticFlows/Simulation/Simulator.swift +++ /dev/null @@ -1,193 +0,0 @@ -// -// Simulator.swift -// -// -// Created by Stefan Urbanek on 25/08/2023. -// - -import PoieticCore - -/// Parameters of the simulation. -/// -public struct SimulationParameters { - /// Time of the initialisation state of the simulation. - public var initialTime: Double - - /// Advancement of time for each simulation step. - public var timeDelta: Double - - /// Final simulation time. - /// - /// Simulation is run while the simulation is less than ``endTime``. - public var endTime: Double - - /// Create new simulation options. - /// - /// - Parameters: - /// - initialTime: Time of the initialisation state of the simulation. - /// - timeDelta: Advancement of time for each simulation step. - /// - endTime: Final simulation time. - /// - /// The default ``endTime`` is set to 10.0 (like the `head` UNIX command number of lines). - /// It is enough for a reasonable default preview. - /// - public init(initialTime: Double = 0.0, timeDelta: Double = 1.0, endTime: Double = 10.0) { - self.initialTime = initialTime - self.timeDelta = timeDelta - self.endTime = endTime - } - - /// Create new simulation parameters from an object. - /// - /// The object is expected to be of a ``Trait/Simulation`` type, although any object with - /// expected attributes can be used. - /// - public init(fromObject object: ObjectSnapshot) { - self.initialTime = object["initial_time", default: 0.0] - self.timeDelta = object["time_delta", default: 0.0] - - if let endTime: Double = object["end_time"] { - self.endTime = endTime - } - else { - if let steps: Int = object["steps"] { - self.endTime = initialTime + Double(steps + 1) * timeDelta - } - else { - let steps = 10 - self.endTime = initialTime + Double(steps + 1) * timeDelta - } - } - } -} - -// TODO: Move this class to Core, we are half-way through decoupling. - -/// Object for controlling a simulation session. -/// -public class Simulator { - // Simulation state - public var parameters: SimulationParameters - public var currentTime: Double = 0.0 - public var currentStep: Int = 0 - public var currentState: SimulationState? - - /// Collected data - public var result: SimulationResult - - // TODO: Allow multiple - public let simulation: Simulation - - // MARK: - Creation - - /// Creates and initialises a simulator. - /// - /// - Properties: - /// - model: Compiled simulation model that describes the computation. - /// - solverType: Type of the solver to be used. - /// - /// The simulator is initialised by creating a new solver and initialising - /// simulation values from the ``SimulationPlan/simulationDefaults`` such as - /// initial time or time delta (_dt_). If the defaults are not provided then - /// the following values are used: - /// - /// - `initialTime = 0.0` - /// - `timeDelta = 1.0` - /// - public init(simulation: Simulation, parameters: SimulationParameters? = nil) { - self.simulation = simulation - self.parameters = parameters ?? SimulationParameters() - self.currentState = nil - self.result = SimulationResult(initialTime: self.parameters.initialTime, - timeDelta: self.parameters.timeDelta) - } - - // MARK: - State Initialisation - - /// Initialise the computation state. - /// - /// - Parameters: - /// - `time`: Initial time. This parameter is usually not used, but - /// some computations in the model might use it. Default value is 0.0 - /// - `override`: Dictionary of values to override during initialisation. - /// The values of nodes that are present in the dictionary will not be - /// evaluated, but the value from the dictionary will be used. - /// - /// - Returns: `StateVector` with initialised values. - /// - /// - Note: Use only constants in the `override` dictionary. Even-though - /// any node value can be provided, in the future only constants will - /// be allowed. - /// - @discardableResult - public func initializeState(time: Double? = nil, override: [ObjectID:Double] = [:]) throws -> SimulationState { - // TODO: Rename to createInitialState() - currentStep = 0 - currentTime = time ?? parameters.initialTime - - var overrideVariants: [ObjectID:Variant] = [:] - for (id, value) in override { - overrideVariants[id] = Variant(value) - } - - let state = try simulation.initialize(time: currentTime, - timeDelta: parameters.timeDelta, - override: overrideVariants) - - self.result = SimulationResult(initialTime: currentTime, timeDelta: parameters.timeDelta) - self.result.append(state) - self.currentState = state - - return state - } - - - // MARK: - Step - - /// Perform one step of the simulation. - /// - /// First, step number is increased by one, current time is increased by time - /// delta, all built-in variables are set to their current values. - /// - /// Then simulation updates the state and result is appended to the collection - /// of simulation outputs. - /// - /// Current state is set to the most recently computed state. - /// - /// - SeeAlso: ``Simulation/update(_:context:)`` - /// - public func step() throws { - guard let currentState else { - fatalError("Trying to run an uninitialised simulator") - } - - // 1. Advance time and prepare - // ------------------------------------------------------- - // 2. Computation - // ------------------------------------------------------- - let result = try simulation.step(currentState) - currentStep = result.step - currentTime = result.time - - // 3. Finalisation - // ------------------------------------------------------- - - self.result.append(result) - self.currentState = result - } - - /// Run the simulation for given number of steps. - /// - /// Convenience method. - /// - public func run(_ steps: Int? = nil) throws { - var stepCount = 0 - while currentTime < parameters.endTime { - if let steps, stepCount >= steps { - break - } - try step() - stepCount += 1 - } - } -} diff --git a/Sources/PoieticFlows/Simulation/StateVariable.swift b/Sources/PoieticFlows/Simulation/StateVariable.swift index 5e406bc..07a83fc 100644 --- a/Sources/PoieticFlows/Simulation/StateVariable.swift +++ b/Sources/PoieticFlows/Simulation/StateVariable.swift @@ -8,7 +8,7 @@ import PoieticCore -/// Structure representing a reference to a simulation variable. +/// Structure representing a variable in a simulation state. /// /// The structure is analogous to an entry in a symbol table generated by the /// compiler. It provides information about where a particular simulation diff --git a/Sources/PoieticFlows/Simulation/StockFlowSimulation.swift b/Sources/PoieticFlows/Simulation/StockFlowSimulation.swift index 0b0acda..f6423a7 100644 --- a/Sources/PoieticFlows/Simulation/StockFlowSimulation.swift +++ b/Sources/PoieticFlows/Simulation/StockFlowSimulation.swift @@ -39,25 +39,29 @@ public class StockFlowSimulation: Simulation { self.solver = solver self.flowScaling = flowScaling } - + // MARK: - Initialization /// Create and initialise a simulation state. /// /// - Parameters: - /// - step: The initial step number of the simulation. /// - time: Initial time. /// - timeDelta: Time delta between simulation steps. - /// - override: Dictionary of values to override during initialisation. + /// - parameters: Dictionary of values to override during initialisation. /// /// This function creates and computes the initial state of the computation by - /// evaluating all the nodes in the order of their dependency by parameter. + /// evaluating all the nodes in the order of their dependency by parameter. The order is provided + /// by the ``SimulationPlan/simulationObjects``. /// - /// When the ``override`` parameter is specified, then values for objects with given - /// ID will be used from the dictionary instead of being computed. + /// When the parameters dictionary is provided, then initial values for specified object IDs will + /// be taken from the dictionary instead of being computed. /// /// - Returns: Newly initialised simulation state. /// - public func initialize(time: Double=0, timeDelta: Double=1.0, override: [ObjectID:Variant]=[:]) throws (SimulationError) -> SimulationState { + public func initialize(time: Double=0, + timeDelta: Double=1.0, + parameters: [ObjectID:Variant]=[:]) + throws (SimulationError) -> SimulationState + { var state = SimulationState(count: plan.stateVariables.count, step: 0, time: time, @@ -65,9 +69,9 @@ public class StockFlowSimulation: Simulation { setBuiltins(in: &state) - for (index, obj) in plan.simulationObjects.enumerated() { - if let value = override[obj.objectID] { - state[obj.variableIndex] = value + for (index, simObject) in plan.simulationObjects.enumerated() { + if let value = parameters[simObject.objectID] { + state[simObject.variableIndex] = value } else { try initialize(objectAt: index, in: &state) @@ -219,7 +223,7 @@ public class StockFlowSimulation: Simulation { /// well. /// /// - Parameters: - /// - index: Index of the object to be evaluated. + /// - object: Object to be evaluated. /// - state: simulation state within which the expression is evaluated /// /// - Throws: ``SimulationError`` @@ -325,8 +329,8 @@ public class StockFlowSimulation: Simulation { /// Evaluate graphical function /// - /// - Throws: ``SimulationError/functionError(_:_:)`` if the parameter is no convertible to - /// a numeric type. + /// - Throws: ``EvaluationError/valueError(_:)`` if the parameter is no convertible to + /// a numeric type. /// public func evaluate(graphicalFunction function: BoundGraphicalFunction, with state: SimulationState) throws (EvaluationError) -> Variant { let parameter: Double diff --git a/Sources/PoieticFlows/Systems/ComputationOrderSystem.swift b/Sources/PoieticFlows/Systems/ComputationOrderSystem.swift index d591303..ef8c885 100644 --- a/Sources/PoieticFlows/Systems/ComputationOrderSystem.swift +++ b/Sources/PoieticFlows/Systems/ComputationOrderSystem.swift @@ -9,7 +9,7 @@ import PoieticCore /// System that collects objects for computation and orders them by computational dependency. /// -/// The computational dependency is determined by edges of type ``ObjectType/Parameter``. +/// The computational dependency is determined by edges of type ``/PoieticCore/ObjectType/Parameter``. /// /// - **Input:** Simulation objects (is `Stock` || is `FlowRate` || has trait `Auxiliary`) /// - **Output:** @@ -18,8 +18,12 @@ import PoieticCore /// - **Forgiveness:** ... /// public struct ComputationOrderSystem: System { - public init() {} - public func update(_ frame: AugmentedFrame) throws (InternalSystemError) { + public init(_ world: World) { } + + public func update(_ world: World) throws (InternalSystemError) { + guard let frame = world.frame + else { return } + // TODO: Replace with SimulationObject trait once we have it (there are practical reasons we don't yet) // TODO: Should we use Trait.Stock? (also below) // Note: See roles below @@ -30,7 +34,7 @@ public struct ComputationOrderSystem: System { } // 2. Sort nodes based on computation dependency. - let parameterEdges:[EdgeObject] = frame.edges.filter { + let parameterEdges:[DesignObjectEdge] = frame.edges.filter { $0.object.type === ObjectType.Parameter } @@ -50,7 +54,7 @@ public struct ComputationOrderSystem: System { system: self, error: ModelError.computationCycle, ) - frame.appendIssue(issue, for: edge.key) + world.appendIssue(issue, for: edge.id) } for node in nodes { let issue = Issue( @@ -59,7 +63,7 @@ public struct ComputationOrderSystem: System { system: self, error: ModelError.computationCycle, ) - frame.appendIssue(issue, for: node) + world.appendIssue(issue, for: node) } return } @@ -91,16 +95,17 @@ public struct ComputationOrderSystem: System { context: .object(object.objectID)) } let comp = SimulationRoleComponent(role: role) - frame.setComponent(comp, for: .object(object.objectID)) + world.setComponent(comp, for: object.objectID) } - let component = SimulationOrderComponent( + let orderComponent = SimulationOrderComponent( objects: snapshots, stocks: stocks, flows: flows ) - frame.setComponent(component, for: .Frame) + world.setSingleton(orderComponent) } + } diff --git a/Sources/PoieticFlows/Systems/NameResolutionSystem.swift b/Sources/PoieticFlows/Systems/NameResolutionSystem.swift index 7b62be1..ec968dc 100644 --- a/Sources/PoieticFlows/Systems/NameResolutionSystem.swift +++ b/Sources/PoieticFlows/Systems/NameResolutionSystem.swift @@ -10,7 +10,7 @@ import PoieticCore /// System that collects object names and creates a name lookup. /// /// - **Input:** Ordered simulation objects in frame component ``SimulationOrderComponent``. -/// - **Output:** ``NamedObjectComponent`` for objects where the name is present and not visually +/// - **Output:** ``SimulationObjectNameComponent`` for objects where the name is present and not visually /// empty; ``SimulationNameLookupComponent`` for the frame. /// - **Forgiveness:** Objects without name attribute set - assumed they can't be referred to by /// name, but can by other means, such as an edge. @@ -19,13 +19,13 @@ import PoieticCore public struct NameResolutionSystem: System { // Note: In the future this system might be doing fully qualified name resolution, once we get // nested simulation blocks. - public init() {} + public init(_ world: World) { } nonisolated(unsafe) public static let dependencies: [SystemDependency] = [ .after(ComputationOrderSystem.self), ] - public func update(_ frame: AugmentedFrame) throws (InternalSystemError) { - guard let order: SimulationOrderComponent = frame.component(for: .Frame) else { + public func update(_ world: World) throws (InternalSystemError) { + guard let order: SimulationOrderComponent = world.singleton() else { return } @@ -42,7 +42,7 @@ public struct NameResolutionSystem: System { system: self, error: ModelError.emptyName, ) - frame.appendIssue(issue, for: object.objectID) + world.appendIssue(issue, for: object.objectID) continue } namedObjects[trimmedName, default: []].append(object.objectID) @@ -59,18 +59,19 @@ public struct NameResolutionSystem: System { ) // TODO: Add related nodes for id in ids { - frame.appendIssue(issue, for: id) + world.appendIssue(issue, for: id) } continue } nameLookup[name] = ids[0] let comp = SimulationObjectNameComponent(name: name) - frame.setComponent(comp, for: .object(ids[0])) + world.setComponent(comp, for: ids[0]) } let component = SimulationNameLookupComponent( namedObjects: nameLookup ) - frame.setComponent(component, for: .Frame) + world.setSingleton(component) } + } diff --git a/Sources/PoieticFlows/Systems/ParameterConnectProposalSystem.swift b/Sources/PoieticFlows/Systems/ParameterConnectProposalSystem.swift new file mode 100644 index 0000000..3efc0d7 --- /dev/null +++ b/Sources/PoieticFlows/Systems/ParameterConnectProposalSystem.swift @@ -0,0 +1,96 @@ +// +// AutoParameterProposalSystem.swift +// poietic-flows +// +// Created by Stefan Urbanek on 09/11/2025. +// + +import PoieticCore + +/// Component that proposes parameter edges to be created or removed. +/// +/// The component is created by the ``ParameterConnectionProposalSystem`` and is typically +/// set as a singleton component. +/// +public struct ParameterProposal: Component { + public struct EdgeProposal { + public let origin: ObjectID + public let target: ObjectID + } + /// IDs of unused parameter edges. + public let toRemove: [ObjectID] + /// Endpoints of edges to be created. Create edges of type `Parameter`. + public let toAdd: [EdgeProposal] + + /// Flag whether there are any parameters to be removed or added. + /// + public var isEmpty: Bool { toRemove.isEmpty && toAdd.isEmpty } +} + +/// System that proposes parameter edges to be added and to be removed. The proposal is based on +/// the parameter names used in parsed expressions (typically from `formula` attribute) and +/// +/// - **Input:** Singleton ``SimulationNameLookupComponent`` +/// and objects with ``ResolvedParametersComponent``. +/// +/// If a singleton ``Selection`` is present, then only relevant objects in the selection are +/// considered. +/// +/// - **Output:** ``ParameterProposal`` singleton. +/// - **Forgiveness:** Nothing is proposed if the singleton is missing. +/// - **Issues:** No issues created. +/// +/// After updating the world using the system, one can remove and create parameter edges +/// as follows: +/// +/// ```swift +/// // Assume the world and trans are given as: +/// let world: World +/// let trans: TransientFrame +/// +/// let proposal: ParameterProposal = world.singleton()! +/// +/// for id in proposal.toRemove { +/// trans.removeCascading(id) +/// } +/// for edgeProposal in proposal.toAdd { +/// trans.createEdge(.Parameter, +/// origin: edgeProposal.origin, +/// target: edgeProposal.target) +/// } +/// ``` + +public struct ParameterConnectionProposalSystem: System { + nonisolated(unsafe) public static let dependencies: [SystemDependency] = [ + .after(NameResolutionSystem.self), + .after(ParameterResolutionSystem.self), // We need variable names + ] + public init(_ world: World) { } + public func update(_ world: World) throws (InternalSystemError) { + guard let lookup: SimulationNameLookupComponent = world.singleton() + else { return } + + let selection: Selection? = world.singleton() + + var toRemove: [ObjectID] = [] + var toAdd: [ParameterProposal.EdgeProposal] = [] + + for (entityID, resolution) in world.query(ResolvedParametersComponent.self) { + guard let objectID = world.entityToObject(entityID) + else { continue } + if let selection, !selection.contains(objectID) { continue } + + toRemove += resolution.unused + + for name in resolution.missing { + guard let parameterID = lookup.namedObjects[name] + else { continue } + + let edge = ParameterProposal.EdgeProposal(origin: parameterID, target: objectID) + toAdd.append(edge) + } + } + let proposal = ParameterProposal(toRemove: toRemove, toAdd: toAdd) + world.setSingleton(proposal) + } +} diff --git a/Sources/PoieticFlows/Systems/ParameterResolutionSystem.swift b/Sources/PoieticFlows/Systems/ParameterResolutionSystem.swift index 5bdd4f3..0e386b0 100644 --- a/Sources/PoieticFlows/Systems/ParameterResolutionSystem.swift +++ b/Sources/PoieticFlows/Systems/ParameterResolutionSystem.swift @@ -57,27 +57,29 @@ public struct ParameterResolutionSystem: System { .after(ExpressionParserSystem.self), // We need variable names ] - public init() {} + public init(_ world: World) { } - public func update(_ frame: AugmentedFrame) throws (InternalSystemError) { - try resolveFormulas(frame) - try resolveAuxiliaries(frame, type: .GraphicalFunction) - try resolveAuxiliaries(frame, type: .Delay) - try resolveAuxiliaries(frame, type: .Smooth) + public func update(_ world: World) throws (InternalSystemError) { + guard let frame = world.frame else { return } + try resolveFormulas(world, frame: frame) + try resolveAuxiliaries(world, frame: frame, type: .GraphicalFunction) + try resolveAuxiliaries(world, frame: frame, type: .Delay) + try resolveAuxiliaries(world, frame: frame, type: .Smooth) } - public func resolveFormulas(_ frame: AugmentedFrame) throws (InternalSystemError) { + public func resolveFormulas(_ world: World, frame: DesignFrame) throws (InternalSystemError) { let builtinNames = BuiltinVariable.allNames - for (id, exprComponent) in frame.filter(ParsedExpressionComponent.self) { + for (entityID, exprComponent) in world.query(ParsedExpressionComponent.self) { + guard let objectID = world.entityToObject(entityID) else { continue } let requiredParams = exprComponent.variables.subtracting(builtinNames) - let incomingParams = frame.incoming(id).filter { + let incomingParams = frame.incoming(objectID).filter { $0.object.type === ObjectType.Parameter } var connected: [String:ObjectID] = [:] var missing: Set = Set(requiredParams) - var unused: [EdgeObject] = [] + var unused: [DesignObjectEdge] = [] for edge in incomingParams { let parameter = edge.originObject @@ -104,7 +106,7 @@ public struct ParameterResolutionSystem: System { error: ModelError.unknownParameter(name), details: ["name": Variant(name)] ) - frame.appendIssue(issue, for: id) + world.appendIssue(issue, for: objectID) } for edge in unused { @@ -116,15 +118,15 @@ public struct ParameterResolutionSystem: System { error: ModelError.unusedInput(name), details: ["name": Variant(name)] ) - frame.appendIssue(issue, for: id) + world.appendIssue(issue, for: objectID) } let paramComponent = ResolvedParametersComponent( incoming: connected, missing: Array(missing), - unused: unused.map { $0.key } + unused: unused.map { $0.id } ) - frame.setComponent(paramComponent, for: .object(id)) + world.setComponent(paramComponent, for: entityID) } } /// Resolve connections of single-parameter auxiliaries such as graphical function, @@ -132,7 +134,7 @@ public struct ParameterResolutionSystem: System { /// /// - Requirement: The auxiliary should have one incoming parameter. /// - public func resolveAuxiliaries(_ frame: AugmentedFrame, type: ObjectType) + public func resolveAuxiliaries(_ world: World, frame: DesignFrame, type: ObjectType) throws (InternalSystemError) { for object in frame.filter(type: type) { let incomingParams = frame.incoming(object.objectID).filter { @@ -147,7 +149,7 @@ public struct ParameterResolutionSystem: System { system: self, error: ModelError.missingRequiredParameter, ) - frame.appendIssue(issue, for: object.objectID) + world.appendIssue(issue, for: object.objectID) component = ResolvedParametersComponent( missingUnnamed: 1 @@ -160,7 +162,7 @@ public struct ParameterResolutionSystem: System { system: self, error: ModelError.tooManyParameters, ) - frame.appendIssue(issue, for: object.objectID) + world.appendIssue(issue, for: object.objectID) component = ResolvedParametersComponent( unused: incomingParams.map { $0.origin } @@ -172,7 +174,7 @@ public struct ParameterResolutionSystem: System { ) } - frame.setComponent(component, for: .object(object.objectID)) + world.setComponent(component, for: object.objectID) } diff --git a/Sources/PoieticFlows/Systems/PresentationSystems.swift b/Sources/PoieticFlows/Systems/PresentationSystems.swift index 62918e9..415924d 100644 --- a/Sources/PoieticFlows/Systems/PresentationSystems.swift +++ b/Sources/PoieticFlows/Systems/PresentationSystems.swift @@ -10,15 +10,16 @@ import PoieticCore /// System that collects all flow rates and determines their inflows and outflows. /// -/// - **Input:** Nodes of type ``ObjectType/Chart``, -/// - **Output:** Set ``ChartsComponent`` for each chart node. -/// - **Forgiveness:** If multiple ``ObjectType/Flow`` edges exist, only one is picked arbitrarily. +/// - **Input:** Nodes of type ``/PoieticCore/ObjectType/Chart``, +/// - **Output:** Set ``ChartComponent`` for each chart node. +/// - **Forgiveness:** If multiple ``/PoieticCore/ObjectType/Flow`` edges exist, only one is picked arbitrarily. /// public struct ChartResolutionSystem: System { - public init() {} + public init(_ world: World) { } - public func update(_ frame: AugmentedFrame) throws (InternalSystemError) { + public func update(_ world: World) throws (InternalSystemError) { + guard let frame = world.frame else { return } let chartObjects = frame.filter { $0.type === ObjectType.Chart } for chartObject in chartObjects { @@ -28,16 +29,7 @@ public struct ChartResolutionSystem: System { let series = seriesEdges.map { $0.targetObject } let chart = ChartComponent(chartObject: chartObject, series: series) - frame.setComponent(chart, for: .object(chartObject.objectID)) + world.setComponent(chart, for: chartObject.objectID) } } - public func makeChart(_ chart: ObjectSnapshot, frame: AugmentedFrame) -> ChartComponent { - let edges = frame.outgoing(chart.objectID).filter { - $0.object.type === ObjectType.ChartSeries - } - let series = edges.map { $0.targetObject } - // Check that: - // - target is numeric value component (can be presented) - return ChartComponent(chartObject: chart, series: series) - } } diff --git a/Sources/PoieticFlows/Systems/SimulationPlanningSystem.swift b/Sources/PoieticFlows/Systems/SimulationPlanningSystem.swift index 8d8ad77..61271e4 100644 --- a/Sources/PoieticFlows/Systems/SimulationPlanningSystem.swift +++ b/Sources/PoieticFlows/Systems/SimulationPlanningSystem.swift @@ -72,7 +72,27 @@ class StateVariableTable { } -struct SimulationPlanningSystem: System { +/// System that creates the final simulation plan. +/// +/// - **Dependency:** +/// - Must run after `ExpressionParserSystem` to parse expressions and get variable names. +/// - Must run after ``ComputationOrderSystem`` to get the simulation order, which is one of the +/// key simulation information. +/// - Must run after ``NameResolutionSystem``. +/// - Must run after ``FlowCollectorSystem`` and ``StockDependencySystem`` to collect stocks +/// and flows. +/// - **Input:** +/// - ``SimulationOrderComponent``: singleton, required. +/// - ``SimulationObjectNameComponent``: required for simulation objects, otherwise no plan is created. +/// - ``SimulationRoleComponent``: required for simulation objects, otherwise no plan is created. +/// - `ParsedExpressionComponent`: semantically required by formula objects. +/// - ``ResolvedParametersComponent``: semantically required – registers an object issue if missing. +/// - ``FlowRateComponent``: required for flow nodes. +/// - ``StockComponent``: required for stock nodes. +/// - **Output:** ``SimulationPlan`` singleton if there were no issues, otherwise sets object issues. +/// - **Forgiveness:** The system is forgiving in a way that it does not fail on semantic errors. +/// +public struct SimulationPlanningSystem: System { /// Error thrown during the planning process internal enum CompilationError: Error, Equatable { /// Issue with object has been detected, appended to the list of issues. The caller might @@ -94,7 +114,7 @@ struct SimulationPlanningSystem: System { let builtinFunctions: [String:Function] - init() { + public init(_ world: World) { var builtinFunctions: [String:Function] = [:] for function in Function.AllBuiltinFunctions { builtinFunctions[function.name] = function @@ -102,10 +122,10 @@ struct SimulationPlanningSystem: System { self.builtinFunctions = builtinFunctions } - func update(_ frame: AugmentedFrame) throws (InternalSystemError) { - guard let simOrder: SimulationOrderComponent = frame.component(for: .Frame) else { - return - } + public func update(_ world: World) throws (InternalSystemError) { + guard let frame = world.frame, + let simOrder: SimulationOrderComponent = world.singleton() + else { return } var hasError: Bool = false let variables = StateVariableTable() @@ -116,8 +136,8 @@ struct SimulationPlanningSystem: System { let builtins = prepareBuiltins(variables: variables) for object in simOrder.objects { - guard let nameComp: SimulationObjectNameComponent = frame.component(for: .object(object.objectID)), - let roleComp: SimulationRoleComponent = frame.component(for: .object(object.objectID)) + guard let nameComp: SimulationObjectNameComponent = world.component(for: object.objectID), + let roleComp: SimulationRoleComponent = world.component(for: object.objectID) else { hasError = true continue @@ -126,7 +146,7 @@ struct SimulationPlanningSystem: System { let rep: ComputationalRepresentation do { - rep = try compileObject(object, frame: frame, variables: variables) + rep = try compileObject(object, world: world, variables: variables) } catch .objectIssue { hasError = true @@ -168,23 +188,23 @@ struct SimulationPlanningSystem: System { message: "Unprocessed simulation objects. Expected \(simOrder.objects.count), got \(simulationObjects.count)") } - let boundFlows = try bindFlows(flows, frame: frame, variables: variables) + let boundFlows = try bindFlows(flows, world: world, variables: variables) var flowIndices: [ObjectID:Int] = [:] for (index, flow) in boundFlows.enumerated() { flowIndices[flow.objectID] = index } - let boundStocks = try bindStocks(stocks, flowIndices: flowIndices, frame: frame) + let boundStocks = try bindStocks(stocks, flowIndices: flowIndices, world: world) // Simulation parameters - let params: SimulationParameters + let params: SimulationSettings if let simInfo = frame.first(trait: Trait.Simulation) { - params = SimulationParameters(fromObject: simInfo) + params = SimulationSettings(fromObject: simInfo) } else { - params = SimulationParameters() + params = SimulationSettings() } @@ -199,7 +219,7 @@ struct SimulationPlanningSystem: System { simulationParameters: params ) - frame.setComponent(plan, for: .Frame) + world.setSingleton(plan) } func prepareBuiltins(variables: StateVariableTable) -> BoundBuiltins { @@ -223,22 +243,22 @@ struct SimulationPlanningSystem: System { /// - missing required attribute /// func compileObject(_ object: ObjectSnapshot, - frame: AugmentedFrame, + world: World, variables: StateVariableTable) throws (CompilationError) -> ComputationalRepresentation { // FIXME: Precompute representation type (and add rep.type type) in sim ordering let rep: ComputationalRepresentation if object.type.hasTrait(Trait.Formula) { - rep = try compileFormulaObject(object, frame: frame, variables: variables) + rep = try compileFormulaObject(object, world: world, variables: variables) } else if object.type.hasTrait(Trait.GraphicalFunction) { - rep = try compileGraphicalFunctionNode(object, frame: frame, variables: variables) + rep = try compileGraphicalFunctionNode(object, world: world, variables: variables) } else if object.type.hasTrait(Trait.Delay) { - rep = try compileDelayNode(object, frame: frame, variables: variables) + rep = try compileDelayNode(object, world: world, variables: variables) } else if object.type.hasTrait(Trait.Smooth) { - rep = try compileSmoothNode(object, frame: frame, variables: variables) + rep = try compileSmoothNode(object, world: world, variables: variables) } else { // Hint: If this error happens, then check one of the the following: @@ -273,19 +293,16 @@ struct SimulationPlanningSystem: System { /// - Throws: ``NodeIssueError`` if there is an issue with parameters, /// function names or other variable names in the expression. /// - func compileFormulaObject(_ object: ObjectSnapshot, frame: AugmentedFrame, + func compileFormulaObject(_ object: ObjectSnapshot, + world: World, variables: StateVariableTable) throws (CompilationError) -> ComputationalRepresentation { - guard let component: ParsedExpressionComponent = frame.component(for: .object(object.objectID)) else { + guard let component: ParsedExpressionComponent = world.component(for: object.objectID) else { throw .missingComponent("ParsedExpressionComponent") } - guard let expression = component.expression else { - // Since the expression parsing failed, we already have an error stored in the - // issue list. - throw .objectIssue - } + let expression = component.expression // Finally bind the expression. // @@ -307,7 +324,7 @@ struct SimulationPlanningSystem: System { ] ) - frame.appendIssue(issue, for: object.objectID) + world.appendIssue(issue, for: object.objectID) throw .objectIssue } @@ -327,7 +344,7 @@ struct SimulationPlanningSystem: System { /// - SeeAlso: ``CompiledGraphicalFunction``, ``Solver/evaluate(objectAt:with:)`` /// func compileGraphicalFunctionNode(_ object: ObjectSnapshot, - frame: AugmentedFrame, + world: World, variables: StateVariableTable) throws (CompilationError) -> ComputationalRepresentation { let points:[Point] = object["graphical_function_points", default: []] @@ -339,7 +356,7 @@ struct SimulationPlanningSystem: System { let function = GraphicalFunction(points: points, method: method) - guard let paramComp: ResolvedParametersComponent = frame.component(for: .object(object.objectID)), + guard let paramComp: ResolvedParametersComponent = world.component(for: object.objectID), paramComp.connectedUnnamed.count == 1, let parameterID = paramComp.connectedUnnamed.first else { @@ -354,8 +371,8 @@ struct SimulationPlanningSystem: System { return .graphicalFunction(boundFunc) } - public func compileDelayNode(_ object: ObjectSnapshot, - frame: AugmentedFrame, + func compileDelayNode(_ object: ObjectSnapshot, + world: World, variables: StateVariableTable) throws (CompilationError) -> ComputationalRepresentation { @@ -372,7 +389,7 @@ struct SimulationPlanningSystem: System { name: "delay_init_\(object.objectID)" ) - guard let paramComp: ResolvedParametersComponent = frame.component(for: .object(object.objectID)), + guard let paramComp: ResolvedParametersComponent = world.component(for: object.objectID), paramComp.connectedUnnamed.count == 1, let parameterID = paramComp.connectedUnnamed.first else { @@ -396,7 +413,7 @@ struct SimulationPlanningSystem: System { error: ModelError.invalidParameterType, relatedObjects: [parameterID] ) - frame.appendIssue(issue, for: object.objectID) + world.appendIssue(issue, for: object.objectID) throw .objectIssue } @@ -412,8 +429,8 @@ struct SimulationPlanningSystem: System { return .delay(compiled) } - public func compileSmoothNode(_ object: ObjectSnapshot, - frame: AugmentedFrame, + func compileSmoothNode(_ object: ObjectSnapshot, + world: World, variables: StateVariableTable) throws (CompilationError) -> ComputationalRepresentation { let smoothValueIndex = variables.allocate( @@ -422,7 +439,7 @@ struct SimulationPlanningSystem: System { name: "smooth_value_\(object.objectID)" ) - guard let paramComp: ResolvedParametersComponent = frame.component(for: .object(object.objectID)), + guard let paramComp: ResolvedParametersComponent = world.component(for: object.objectID), paramComp.connectedUnnamed.count == 1, let parameterID = paramComp.connectedUnnamed.first else { @@ -443,11 +460,11 @@ struct SimulationPlanningSystem: System { error: ModelError.invalidParameterType, relatedObjects: [parameterID] ) - frame.appendIssue(issue, for: object.objectID) + world.appendIssue(issue, for: object.objectID) throw .objectIssue } - // FIXME: [REFACTORING] Require the attribute, do not assume the default here + // TODO: Require the attribute, do not assume the default here? // This requires attribute error let windowTime: Double = object["window_time", default: 1] @@ -463,7 +480,7 @@ struct SimulationPlanningSystem: System { // MARK: - Flow - func bindFlows(_ flows: [SimulationObject], frame: AugmentedFrame, variables: StateVariableTable) + func bindFlows(_ flows: [SimulationObject], world: World, variables: StateVariableTable) throws (InternalSystemError) -> [BoundFlow] { var boundFlows: [BoundFlow] = [] @@ -473,7 +490,7 @@ struct SimulationPlanningSystem: System { name: flow.name, valueType: flow.valueType, variables: variables, - frame: frame) + world: world) boundFlows.append(boundFlow) } return boundFlows @@ -484,12 +501,12 @@ struct SimulationPlanningSystem: System { name: String, valueType: ValueType, // rep.valueType variables: StateVariableTable, - frame: AugmentedFrame) + world: World) throws (InternalSystemError) -> BoundFlow { - guard let component: FlowRateComponent = frame.component(for: .object(objectID)) else { + guard let component: FlowRateComponent = world.component(for: objectID) else { throw InternalSystemError(self, message: "Missing required component", - context: .frameComponent("FlowRateComponent")) + context: .singleton("FlowRateComponent")) } guard let objectIndex = variables.objectIndex[objectID] else { // TODO: Throw corrupted component @@ -512,7 +529,7 @@ struct SimulationPlanningSystem: System { /// func bindStocks(_ stocks: [SimulationObject], flowIndices: [ObjectID:Int], // Index into list of flows - frame: AugmentedFrame) + world: World) throws (InternalSystemError) -> [BoundStock] { var result: [BoundStock] = [] @@ -521,7 +538,7 @@ struct SimulationPlanningSystem: System { boundStock = try bindStock(stock.objectID, variableIndex: stock.variableIndex, flowIndices: flowIndices, - frame: frame) + world: world) result.append(boundStock) } return result @@ -530,12 +547,12 @@ struct SimulationPlanningSystem: System { func bindStock(_ objectID: ObjectID, variableIndex: Int, flowIndices: [ObjectID:Int], // Index into list of flows - frame: AugmentedFrame) + world: World) throws (InternalSystemError) -> BoundStock { - guard let comp: StockComponent = frame.component(for: .object(objectID)) else { + guard let comp: StockComponent = world.component(for: objectID) else { throw InternalSystemError(self, message: "Missing component", - context: .frameComponent("StockDependencyComponent")) + context: .singleton("StockDependencyComponent")) } let inflowIndices = comp.inflowRates.compactMap { flowIndices[$0] } @@ -546,7 +563,7 @@ struct SimulationPlanningSystem: System { else { throw InternalSystemError(self, message: "Corrupted component", - context: .frameComponent("StockDependencyComponent")) + context: .singleton("StockDependencyComponent")) } let boundStock = BoundStock( diff --git a/Sources/PoieticFlows/Systems/StockFlowSimulationSystem.swift b/Sources/PoieticFlows/Systems/StockFlowSimulationSystem.swift new file mode 100644 index 0000000..8b2d453 --- /dev/null +++ b/Sources/PoieticFlows/Systems/StockFlowSimulationSystem.swift @@ -0,0 +1,109 @@ +// +// StockFlowSimulationSystem.swift +// poietic-flows +// +// Created by Stefan Urbanek on 31/12/2025. +// + +import PoieticCore + +/// System that runs a Stock-Flow simulation and stores results in a ``SimulationResult`` +/// singleton. +/// +/// The simulation time and solver type is retrieved from the ``SimulationSettings`` component. +/// If the component is not present, then defaults are used +/// (see ``SimulationSettings/init(initialTime:timeDelta:endTime:solverType:)``). The simulation +/// is run whole, from the initial time to the end time. +/// +/// +/// The system has the following limitations, which might be removed in the future: +/// +/// - The simulation is run as a whole, there are no external events triggered on each step. +/// - Only the singleton plan is used - only one simulation can be run per world. +/// +/// - **Input:** ``SimulationPlan`` singleton – required. Optional ``SimulationSettings`` +/// singleton and ``ScenarioParameters`` singleton. +/// - **Output:** ``SimulationResult`` singleton is set when simulation was finished successfully, +/// otherwise the simulation result will be removed or not set. +/// - **Forgiveness:** No forgiveness. +/// - **Issues:** No issues created. +/// +public struct StockFlowSimulationSystem: System { + nonisolated(unsafe) public static let dependencies: [SystemDependency] = [ + .after(SimulationPlanningSystem.self), + ] + public init(_ world: World) { } + public func update(_ world: World) throws (InternalSystemError) { + world.removeSingleton(SimulationResult.self) + + guard let plan: SimulationPlan = world.singleton() else { return } + let settings: SimulationSettings = world.singleton() ?? SimulationSettings() + let params: ScenarioParameters = world.singleton() ?? ScenarioParameters() + guard let solverType = StockFlowSimulation.SolverType(rawValue: settings.solverType) else { + throw InternalSystemError(self, message: "Unknown solver type: \(settings.solverType)") + } + + let simulation = StockFlowSimulation(plan, solver: solverType) + + var result = SimulationResult(initialTime: settings.initialTime, + timeDelta: settings.timeDelta) + + var currentState = try initialize(world: world, + plan: plan, + settings: settings, + parameters: params) + result.append(currentState) + var currentTime = settings.initialTime + var step: UInt = 1 + + while step <= settings.steps { + let newState = try self.step(simulation: simulation, state: currentState) + result.append(newState) + currentState = newState + currentTime += settings.timeDelta + step += 1 + } + + world.setSingleton(result) + } + + public func initialize(world: World, + plan: SimulationPlan, + settings: SimulationSettings, + parameters: ScenarioParameters) + throws (InternalSystemError) -> SimulationState + { + guard let solverType = StockFlowSimulation.SolverType(rawValue: settings.solverType) else { + throw InternalSystemError(self, message: "Unknown solver type: \(settings.solverType)") + } + // TODO: Add flow scaling parameter + let simulation = StockFlowSimulation(plan, solver: solverType) + + let state: SimulationState + do { + state = try simulation.initialize(time: settings.initialTime, + timeDelta: settings.timeDelta, + parameters: parameters.initialValues) + } + catch { + // FIXME: Handle this error with a simulation result/error component. + throw InternalSystemError(self, message: "Unhandled simulation error: \(error)") + } + return state + } + + public func step(simulation: StockFlowSimulation, + state currentState: SimulationState) + throws (InternalSystemError) -> SimulationState { + let newState: SimulationState + do { + newState = try simulation.step(currentState) + } + catch { + throw InternalSystemError(self, message: "Unhandled simulation error: \(error)") + } + + return newState + } + +} diff --git a/Sources/PoieticFlows/Systems/StockFlowSystem.swift b/Sources/PoieticFlows/Systems/StockFlowSystem.swift index e7590a5..390eb9c 100644 --- a/Sources/PoieticFlows/Systems/StockFlowSystem.swift +++ b/Sources/PoieticFlows/Systems/StockFlowSystem.swift @@ -10,15 +10,17 @@ import PoieticCore /// System that collects all flow rates and determines their inflows and outflows. /// -/// - **Input:** Nodes of type ``ObjectType/FlowRate``, +/// - **Input:** Nodes of type ``/PoieticCore/ObjectType/FlowRate``, /// - **Output:** Set ``FlowRateComponent`` for each flow rate node. -/// - **Forgiveness:** If multiple ``ObjectType/Flow`` edges exist, only one is picked arbitrarily. +/// - **Forgiveness:** If multiple ``/PoieticCore/ObjectType/Flow`` edges exist, only one is picked arbitrarily. /// public struct FlowCollectorSystem: System { - public init() {} + public init(_ world: World) { } - public func update(_ frame: AugmentedFrame) throws (InternalSystemError) { + public func update(_ world: World) throws (InternalSystemError) { + guard let frame = world.frame else { return } + for flow in frame.filter(type: .FlowRate) { // We assume the frame edge reqThank uirements were satisfied, therefore there is most one edge of each let fills: ObjectID? = frame.outgoing(flow.objectID).first { @@ -34,7 +36,7 @@ public struct FlowCollectorSystem: System { let component = FlowRateComponent(drainsStock: drains, fillsStock: fills, priority: priority) - frame.setComponent(component, for: .object(flow.objectID)) + world.setComponent(component, for: flow.objectID) } } } @@ -42,16 +44,20 @@ public struct FlowCollectorSystem: System { /// System that collects all stocks and determines their dependent relationships. /// /// - **Dependency:** Must run after ``FlowCollectorSystem`` to get the ``FlowRateComponent``. -/// - **Input:** Nodes of type ``ObjectType/Stock``, Flow rates with ``FlowRateComponent``. -/// - **Output:** ``StockDependencyComponent`` set to each stock. +/// - **Input:** Nodes of type ``/PoieticCore/ObjectType/Stock``, Flow rates with ``FlowRateComponent``. +/// - **Output:** ``StockComponent`` set to each stock. /// - **Forgiveness:** Flow rates without computed component are ignored. /// -struct StockDependencySystem: System { +public struct StockDependencySystem: System { + public init(_ world: World) { } + nonisolated(unsafe) public static let dependencies: [SystemDependency] = [ .after(FlowCollectorSystem.self) ] - func update(_ frame: AugmentedFrame) throws (InternalSystemError) { + public func update(_ world: World) throws (InternalSystemError) { + guard let frame = world.frame else { return } + var filledByRate: [ObjectID:[ObjectID]] = [:] // Flows filling a stock var drainedByRate: [ObjectID:[ObjectID]] = [:] // Flows draining a stock @@ -63,7 +69,7 @@ struct StockDependencySystem: System { var outflowStocks: [ObjectID:[ObjectID]] = [:] // [drained stock:[to filling stock]] for flow in frame.filter(type: .FlowRate) { - guard let component: FlowRateComponent = frame.component(for: .object(flow.objectID)) else { + guard let component: FlowRateComponent = world.component(for: flow.objectID) else { continue } if let stockID = component.fillsStock { @@ -88,7 +94,7 @@ struct StockDependencySystem: System { outflowStocks: outflowStocks[stock.objectID] ?? [], allowsNegative: stock["allows_negative", default: false] ) - frame.setComponent(component, for: .object(stock.objectID)) + world.setComponent(component, for: stock.objectID) } } } diff --git a/Tests/PoieticFlowsTests/CompilerTests.swift b/Tests/PoieticFlowsTests/CompilerTests.swift index fca1160..f2518d5 100644 --- a/Tests/PoieticFlowsTests/CompilerTests.swift +++ b/Tests/PoieticFlowsTests/CompilerTests.swift @@ -9,31 +9,6 @@ import Testing @testable import PoieticFlows @testable import PoieticCore -extension CompilerError { - func objectHasIssue(_ objectID: ObjectID, identifier: String) -> Bool { - guard let issues = objectIssues(objectID) else { return false } - return issues.contains { $0.identifier == identifier } - } - - func objectHasError(_ objectID: ObjectID, error: T) -> Bool { - guard let issues = objectIssues(objectID) else { return false } - for issue in issues { - if let objectError = issue.error as? T { - return objectError == error - } - } - return false - } - - func objectIssues(_ objectID: ObjectID) -> [PoieticCore.Issue]? { - switch self { - case .internalError(_): return nil - case .issues(let issues): return issues[objectID] - } - - } -} - extension TransientFrame { @discardableResult public func createEdge(_ type: ObjectType, @@ -54,15 +29,27 @@ extension TransientFrame { @Suite struct CompilerTest { let design: Design let frame: TransientFrame + let world: World init() throws { self.design = Design(metamodel: StockFlowMetamodel) self.frame = design.createFrame() + self.world = World(design: design) + self.world.addSchedule(Schedule( + label: FrameChangeSchedule.self, + systems: SimulationPlanningSystems + )) + } + + func acceptAndUpdate() throws { + let accepted = try design.accept(frame) + world.setFrame(accepted) + try world.run(schedule: FrameChangeSchedule.self) } @Test func noComputedVariables() throws { - let compiler = Compiler(frame: try design.accept(frame)) - let plan = try compiler.compile() + try acceptAndUpdate() + let plan: SimulationPlan = try #require(world.singleton()) #expect(plan.simulationObjects.count == 0) #expect(plan.stateVariables.count == BuiltinVariable.allCases.count) @@ -74,8 +61,8 @@ extension TransientFrame { frame.createNode(ObjectType.Stock, name: "c", attributes: ["formula": "0"]) frame.createNode(ObjectType.Note, name: "note") - let compiler = Compiler(frame: try design.accept(frame)) - let plan = try compiler.compile() + try acceptAndUpdate() + let plan: SimulationPlan = try #require(world.singleton()) let names = plan.simulationObjects.map { $0.name } .sorted() #expect(names == ["a", "b", "c"]) @@ -85,25 +72,19 @@ extension TransientFrame { @Test func badFunctionName() throws { let aux = frame.createNode(ObjectType.Auxiliary, name: "a", attributes: ["formula": "nonexistent(10)"]) - let compiler = Compiler(frame: try design.accept(frame)) - #expect { - try compiler.compile() - } throws: { - guard let error = $0 as? CompilerError else { - Issue.record("Unexpected error: \($0)") - return false - } - return error.objectHasError(aux.objectID, - error: ExpressionError.unknownFunction("nonexistent")) - } + try acceptAndUpdate() + let plan: SimulationPlan? = world.singleton() + #expect(plan == nil) + #expect(world.objectHasError(aux.objectID, + error: ExpressionError.unknownFunction("nonexistent"))) } @Test func singleComputedVariable() throws { let _ = frame.createNode(ObjectType.Auxiliary, name: "a", attributes: ["formula": "if(time < 2, 0, 1)"]) - let compiler = Compiler(frame: try design.accept(frame)) - let compiled = try compiler.compile() - let names = compiled.simulationObjects.map { $0.name }.sorted() + try acceptAndUpdate() + let plan: SimulationPlan = try #require(world.singleton()) + let names = plan.simulationObjects.map { $0.name }.sorted() #expect(names == ["a"]) } @@ -116,37 +97,30 @@ extension TransientFrame { frame.createEdge(ObjectType.Flow, origin: a, target: flow) frame.createEdge(ObjectType.Flow, origin: flow, target: b) - let compiler = Compiler(frame: try design.accept(frame)) - let compiled = try compiler.compile() - - let aIndex = try #require(compiled.stocks.firstIndex { $0.objectID == a.objectID }) - let bIndex = try #require(compiled.stocks.firstIndex { $0.objectID == b.objectID }) + try acceptAndUpdate() + let plan: SimulationPlan = try #require(world.singleton()) - #expect(compiled.stocks.count == 2) - #expect(compiled.stocks[aIndex].objectID == a.objectID) - #expect(compiled.stocks[aIndex].inflows == []) - #expect(compiled.stocks[aIndex].outflows == [compiled.flowIndex(flow.objectID)]) + let aIndex = try #require(plan.stocks.firstIndex { $0.objectID == a.objectID }) + let bIndex = try #require(plan.stocks.firstIndex { $0.objectID == b.objectID }) - #expect(compiled.stocks[bIndex].objectID == b.objectID) - #expect(compiled.stocks[bIndex].inflows == [compiled.flowIndex(flow.objectID)]) - #expect(compiled.stocks[bIndex].outflows == []) + #expect(plan.stocks.count == 2) + #expect(plan.stocks[aIndex].objectID == a.objectID) + #expect(plan.stocks[aIndex].inflows == []) + #expect(plan.stocks[aIndex].outflows == [plan.flowIndex(flow.objectID)]) + + #expect(plan.stocks[bIndex].objectID == b.objectID) + #expect(plan.stocks[bIndex].inflows == [plan.flowIndex(flow.objectID)]) + #expect(plan.stocks[bIndex].outflows == []) } @Test func disconnectedGraphicalFunction() throws { let gf = frame.createNode(ObjectType.GraphicalFunction, name: "g") - let compiler = Compiler(frame: try design.accept(frame)) - #expect { - try compiler.compile() - } throws: { - guard let error = $0 as? CompilerError else { - Issue.record("Unexpected error: \($0)") - return false - } - - return error.objectHasIssue(gf.objectID, identifier: "missing_required_parameter") - } + try acceptAndUpdate() + let plan: SimulationPlan? = world.singleton() + #expect(plan == nil) + #expect(world.objectHasIssue(gf.objectID, identifier: "missing_required_parameter")) } @Test func graphicalFunctionComputation() throws { @@ -160,9 +134,9 @@ extension TransientFrame { frame.createEdge(ObjectType.Parameter, origin: p, target: gf) frame.createEdge(ObjectType.Parameter, origin: gf, target: aux) - let compiler = Compiler(frame: try design.accept(frame)) - let compiled = try compiler.compile() - let object = try #require(compiled.simulationObject(gf.objectID), + try acceptAndUpdate() + let plan: SimulationPlan = try #require(world.singleton()) + let object = try #require(plan.simulationObject(gf.objectID), "No compiled variable for the graphical function") switch object.computation { @@ -172,20 +146,4 @@ extension TransientFrame { Issue.record("Graphical function compiled as: \(object.computation)") } } - - @Test func syntaxErrorIsNotInternalError() throws { - let a = frame.createNode(ObjectType.Auxiliary, - name:"a", - attributes: ["formula": "10 + "]) - let compiler = Compiler(frame: try design.accept(frame)) - #expect { - try compiler.compile() - } throws: { - guard let error = $0 as? CompilerError else { - Issue.record("Unexpected error: \($0)") - return false - } - return error.objectHasIssue(a.objectID, identifier: "syntax_error") - } - } } diff --git a/Tests/PoieticFlowsTests/SimulationPlan+extensions.swift b/Tests/PoieticFlowsTests/SimulationPlan+extensions.swift new file mode 100644 index 0000000..674c314 --- /dev/null +++ b/Tests/PoieticFlowsTests/SimulationPlan+extensions.swift @@ -0,0 +1,41 @@ +// +// SimulationPlan+extensions.swift +// poietic-flows +// +// Created by Stefan Urbanek on 22/12/2025. +// + +import PoieticCore +import PoieticFlows + +extension SimulationPlan { + /// Index of a stock in a list of stocks or in a stock difference vector. + /// + /// This function is not used during computation. It is provided for + /// potential inspection, testing and debugging. + /// + /// - Precondition: The plan must contain a stock with given ID. + /// + func stockIndex(_ id: ObjectID) -> NumericVector.Index { + guard let index = stocks.firstIndex(where: { $0.objectID == id }) else { + preconditionFailure("The plan does not contain stock with ID \(id)") + } + return index + } + func flowIndex(_ id: ObjectID) -> NumericVector.Index { + guard let index = flows.firstIndex(where: { $0.objectID == id }) else { + preconditionFailure("The plan does not contain flow with ID \(id)") + } + return index + } + + func variableIndex(_ object: some ObjectProtocol) -> SimulationState.Index? { + // Since this is just for debug purposes, O(n) should be fine, no need + // for added complexity of the code. + guard let first = simulationObjects.first(where: {$0.objectID == object.objectID}) else { + return nil + } + return first.variableIndex + } + +} diff --git a/Tests/PoieticFlowsTests/SolverTests.swift b/Tests/PoieticFlowsTests/SolverTests.swift deleted file mode 100644 index f6294ae..0000000 --- a/Tests/PoieticFlowsTests/SolverTests.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// File.swift -// -// -// Created by Stefan Urbanek on 08/06/2023. -// - -import XCTest -@testable import PoieticFlows -@testable import PoieticCore - -final class TestSimulator: XCTestCase { - var design: Design! - var frame: TransientFrame! - - override func setUp() { - design = Design(metamodel: StockFlowMetamodel) - frame = design.createFrame() - } - - func compile() throws -> SimulationPlan { - let compiler = Compiler(frame: try design.accept(frame)) - return try compiler.compile() - } - - func testTime() throws { - let plan = try compile() - let simulation = StockFlowSimulation(plan) - - let simulator = Simulator(simulation: simulation) - - let state = try simulator.initializeState(time: 10.0) - XCTAssertEqual(state[plan.builtins.time], 10.0) - } -} diff --git a/Tests/PoieticFlowsTests/StockFlowSimulationTests.swift b/Tests/PoieticFlowsTests/StockFlowSimulationTests.swift index a064675..f9f41c0 100644 --- a/Tests/PoieticFlowsTests/StockFlowSimulationTests.swift +++ b/Tests/PoieticFlowsTests/StockFlowSimulationTests.swift @@ -15,36 +15,71 @@ import Testing @testable import PoieticFlows @testable import PoieticCore + @Suite struct TestStockFlowSimulation { + let solverType: StockFlowSimulation.SolverType let design: Design let frame: TransientFrame - var plan: SimulationPlan! - + let world: World + init() throws { + self.solverType = .euler self.design = Design(metamodel: StockFlowMetamodel) self.frame = design.createFrame() + self.world = World(design: design) + self.world.addSchedule(Schedule( + label: FrameChangeSchedule.self, + systems: SimulationPlanningSystems + )) } - mutating func compile() throws { - let compiler = Compiler(frame: try design.accept(frame)) - self.plan = try compiler.compile() + func accept() throws -> SimulationPlan { + let accepted = try design.accept(frame) + world.setFrame(accepted) + try world.run(schedule: FrameChangeSchedule.self) + let plan: SimulationPlan = try #require(world.singleton()) + return plan } - - func index(_ object: TransientObject) -> SimulationState.Index { - plan.variableIndex(of: object.objectID)! + + func run(_ plan: SimulationPlan, + settings: SimulationSettings? = nil, + parameters: ScenarioParameters? = nil) + throws (SimulationError) -> SimulationState + { + let settings = settings ?? SimulationSettings() + let parameters = parameters ?? ScenarioParameters() + + let simulation = StockFlowSimulation(plan, solver: solverType) + var state = try simulation.initialize(time: settings.initialTime, + timeDelta: settings.timeDelta, + parameters: parameters.initialValues) + var result = SimulationResult(initialTime: settings.initialTime, + timeDelta: settings.timeDelta) + result.append(state) + var step: UInt = 1 + + while step <= settings.steps { + print("STEP \(step)") + let newState = try simulation.step(state) + result.append(newState) + state = newState + step += 1 + } + + return state } - + @Test mutating func initializeStocks() throws { let c = frame.createNode(ObjectType.Stock, name: "const", attributes: ["formula": "100"]) let a = frame.createNode(ObjectType.Auxiliary, name: "a", attributes: ["formula": "1"]) - try compile() - - let sim = StockFlowSimulation(plan) - let state = try sim.initialize() - - #expect(state[index(c)] == 100) - #expect(state[index(a)] == 1) + let plan = try self.accept() + + let simulation = StockFlowSimulation(plan) + let state = try simulation.initialize() + + #expect(state[plan.variableIndex(c.objectID)!] == 100) + #expect(state[plan.variableIndex(a.objectID)!] == 1) } @Test mutating func testEverythingInitialized() throws { @@ -52,55 +87,45 @@ import Testing let stock = frame.createNode(ObjectType.Stock, name: "b", attributes: ["formula": "20"]) let flow = frame.createNode(ObjectType.FlowRate, name: "c", attributes: ["formula": "30"]) - try compile() - - let sim = StockFlowSimulation(plan) - let state = try sim.initialize() - - #expect(state[index(aux)] == 10) - #expect(state[index(stock)] == 20) - #expect(state[index(flow)] == 30) + let plan = try self.accept() + let simulation = StockFlowSimulation(plan) + let state = try simulation.initialize() + + #expect(state[plan.variableIndex(aux)!] == 10) + #expect(state[plan.variableIndex(stock)!] == 20) + #expect(state[plan.variableIndex(flow)!] == 30) } - @Test mutating func initializeOverride() throws { + @Test mutating func initializeWithParams() throws { let a = frame.createNode(ObjectType.Auxiliary, name: "a", attributes: ["formula": "10"]) let b = frame.createNode(ObjectType.Auxiliary, name: "b", attributes: ["formula": "20"]) let c = frame.createNode(ObjectType.Auxiliary, name: "c", attributes: ["formula": "a - 1"]) frame.createEdge(ObjectType.Parameter, origin: a.objectID, target: c.objectID) - try compile() - - let sim = StockFlowSimulation(plan) - - let overrides: [ObjectID:Variant] = [ - a.objectID: Variant(999), - ] - let state = try sim.initialize(override: overrides) - - #expect(state[index(a)] == 999) - #expect(state[index(b)] == 20) - #expect(state[index(c)] == 998) + let plan = try self.accept() + + let params = ScenarioParameters( + initialValues: [a.objectID: 999.0] + ) + let simulation = StockFlowSimulation(plan) + let state = try simulation.initialize(parameters: params.initialValues) + + #expect(state[plan.variableIndex(a)!] == 999) + #expect(state[plan.variableIndex(b)!] == 20) + #expect(state[plan.variableIndex(c)!] == 998) } @Test mutating func timeDependentExpression() throws { - let a_time = frame.createNode(ObjectType.Auxiliary, name: "a_time", attributes: ["formula": "time"]) - let f_time = frame.createNode(ObjectType.FlowRate, name: "f_time", attributes: ["formula": "time * 10"]) - - try compile() + let t = frame.createNode(ObjectType.Auxiliary, name: "t", attributes: ["formula": "time"]) - let sim = StockFlowSimulation(plan) - let state = try sim.initialize(time: 1.0) - - #expect(state[index(a_time)] == 1.0) - #expect(state[index(f_time)] == 10.0) - - let state2 = try sim.step(state) - #expect(state2[index(a_time)] == 2.0) - #expect(state2[index(f_time)] == 20.0) + let plan = try self.accept() + let state = try self.run(plan, settings: SimulationSettings(initialTime: 1.0, steps: 0)) + + #expect(state[plan.variableIndex(t)!] == 1.0) - let state3 = try sim.step(state2) - #expect(state3[index(a_time)] == 3.0) - #expect(state3[index(f_time)] == 30.0) + let state2 = try self.run(plan, settings: SimulationSettings(initialTime: 1.0, timeDelta: 10.0, steps: 2)) + + #expect(state2[plan.variableIndex(t)!] == 21.0) } @Test mutating func estimatedFlows() throws { @@ -110,7 +135,7 @@ import Testing let outflow = frame.createNode(.FlowRate, name: "outflow", attributes: ["formula": "20"]) frame.createEdge(.Flow, origin: stock, target: outflow) frame.createEdge(.Flow, origin: inflow, target: stock) - try compile() + let plan = try accept() let sim = StockFlowSimulation(plan) let state = try sim.initialize() let flows = sim.flows(state) @@ -131,7 +156,7 @@ import Testing frame.createEdge(.Flow, origin: inflow, target: stock) frame.createEdge(.Flow, origin: stock, target: out1) frame.createEdge(.Flow, origin: stock, target: out2) - try compile() + let plan = try accept() let sim = StockFlowSimulation(plan, flowScaling: .outflowFirst) let state = try sim.initialize() let flows = sim.flows(state) @@ -156,7 +181,7 @@ import Testing frame.createEdge(.Flow, origin: inflow, target: stock) frame.createEdge(.Flow, origin: stock, target: out1) frame.createEdge(.Flow, origin: stock, target: out2) - try compile() + let plan = try accept() let sim = StockFlowSimulation(plan, flowScaling: .inflowFirst) let state = try sim.initialize() @@ -183,7 +208,7 @@ import Testing frame.createEdge(.Flow, origin: inflow, target: stock) frame.createEdge(.Flow, origin: stock, target: out1) frame.createEdge(.Flow, origin: stock, target: out2) - try compile() + let plan = try accept() let sim = StockFlowSimulation(plan, flowScaling: .inflowFirst) let state = try sim.initialize() @@ -208,7 +233,7 @@ import Testing frame.createEdge(.Flow, origin: inflow, target: stock) frame.createEdge(.Flow, origin: stock, target: out1) frame.createEdge(.Flow, origin: stock, target: out2) - try compile() + let plan = try accept() let sim = StockFlowSimulation(plan, flowScaling: .outflowFirst) let state = try sim.initialize(timeDelta: 0.5) let flows = sim.flows(state) @@ -231,13 +256,13 @@ import Testing frame.createEdge(ObjectType.Flow, origin: stock, target: flow) - try compile() - + let plan = try accept() + let sim = StockFlowSimulation(plan) let state = try sim.initialize() let result = try sim.step(state) - #expect(result[index(stock)] == -5) + #expect(result[plan.variableIndex(stock)!] == -5) } @Test mutating func nonNegativeStock() throws { @@ -248,13 +273,13 @@ import Testing frame.createEdge(ObjectType.Flow, origin: stock, target: flow) - try compile() - + let plan = try accept() + let sim = StockFlowSimulation(plan) let state = try sim.initialize() let result = try sim.step(state) - #expect(result[index(stock)] == 0) + #expect(result[plan.variableIndex(stock)!] == 0) } @Test mutating func cloudOutflow() throws { let stock = frame.createNode(.Stock, name: "stock", @@ -266,13 +291,13 @@ import Testing frame.createEdge(ObjectType.Flow, origin: stock, target: flow) frame.createEdge(ObjectType.Flow, origin: flow, target: cloud) - try compile() - + let plan = try accept() + let sim = StockFlowSimulation(plan) let state = try sim.initialize() let result = try sim.step(state) - #expect(result[index(stock)] == 0) + #expect(result[plan.variableIndex(stock)!] == 0) } @Test mutating func cloudInflow() throws { let stock = frame.createNode(.Stock, name: "stock", @@ -284,13 +309,13 @@ import Testing frame.createEdge(ObjectType.Flow, origin: flow, target: stock) frame.createEdge(ObjectType.Flow, origin: cloud, target: flow) - try compile() - + let plan = try accept() + let sim = StockFlowSimulation(plan) let state = try sim.initialize() let result = try sim.step(state) - #expect(result[index(stock)] == 100) + #expect(result[plan.variableIndex(stock)!] == 100) } @@ -343,13 +368,13 @@ import Testing frame.createEdge(.Flow, origin: source, target: bRate) frame.createEdge(.Flow, origin: bRate, target: b) - try compile() - + let plan = try accept() + let sim = StockFlowSimulation(plan) let state = try sim.initialize() let state2 = try sim.step(state) - #expect(state2[index(a)] == 4.0) - #expect(state2[index(b)] == 8.0) + #expect(state2[plan.variableIndex(a)!] == 4.0) + #expect(state2[plan.variableIndex(b)!] == 8.0) } @Test mutating func compute() throws { @@ -360,18 +385,18 @@ import Testing frame.createEdge(ObjectType.Flow, origin: kettle, target: flow) frame.createEdge(ObjectType.Flow, origin: flow, target: cup) - try compile() - + let plan = try accept() + let sim = StockFlowSimulation(plan) let state = try sim.initialize() let state2 = try sim.step(state) - #expect(state2[index(kettle)] == 900.0 ) - #expect(state2[index(cup)] == 100.0) + #expect(state2[plan.variableIndex(kettle)!] == 900.0 ) + #expect(state2[plan.variableIndex(cup)!] == 100.0) let state3 = try sim.step(state2) - #expect(state3[index(kettle)] == 800.0 ) - #expect(state3[index(cup)] == 200.0) + #expect(state3[plan.variableIndex(kettle)!] == 800.0 ) + #expect(state3[plan.variableIndex(cup)!] == 200.0) } @Test mutating func graphicalFunction() throws { @@ -387,14 +412,14 @@ import Testing frame.createEdge(ObjectType.Parameter, origin: p1, target: g1) frame.createEdge(ObjectType.Parameter, origin: p2, target: g2) - try compile() - + let plan = try accept() + let sim = StockFlowSimulation(plan) let state = try sim.initialize() - #expect(state[index(g1)] == 0.0) - #expect(state[index(g2)] == 10.0) - #expect(state[index(aux)] == 10.0) + #expect(state[plan.variableIndex(g1)!] == 0.0) + #expect(state[plan.variableIndex(g2)!] == 10.0) + #expect(state[plan.variableIndex(aux)!] == 10.0) } // Other tests - that should rather be at lower level @@ -405,21 +430,21 @@ import Testing name: "a", attributes: ["formula": "if(time < 2, 0, 1)"]) - try compile() - + let plan = try accept() + let sim = StockFlowSimulation(plan) let state = try sim.initialize() - #expect(state[index(aux)] == 0.0) + #expect(state[plan.variableIndex(aux)!] == 0.0) let state1 = try sim.step(state) - #expect(state1[index(aux)] == 0.0) + #expect(state1[plan.variableIndex(aux)!] == 0.0) let state2 = try sim.step(state1) - #expect(state2[index(aux)] == 1.0) + #expect(state2[plan.variableIndex(aux)!] == 1.0) let state3 = try sim.step(state2) - #expect(state3[index(aux)] == 1.0) + #expect(state3[plan.variableIndex(aux)!] == 1.0) } @Test mutating func delay() throws { @@ -435,39 +460,39 @@ import Testing frame.createEdge(ObjectType.Parameter, origin: input, target: delay1) frame.createEdge(ObjectType.Parameter, origin: input, target: delay3) - try compile() - + let plan = try accept() + let sim = StockFlowSimulation(plan) let state = try sim.initialize() // Init 0 - #expect(state.double(at: index(delay0)) == 0.0) - #expect(state.double(at: index(delay1)) == 0.0) - #expect(state.double(at: index(delay3)) == 0.0) + #expect(state.double(at: plan.variableIndex(delay0)!) == 0.0) + #expect(state.double(at: plan.variableIndex(delay1)!) == 0.0) + #expect(state.double(at: plan.variableIndex(delay3)!) == 0.0) // Step 1 let state1 = try sim.step(state) - #expect(state1[index(delay0)] == 10.0) - #expect(state1[index(delay1)] == 0.0) - #expect(state1[index(delay3)] == 0.0) + #expect(state1[plan.variableIndex(delay0)!] == 10.0) + #expect(state1[plan.variableIndex(delay1)!] == 0.0) + #expect(state1[plan.variableIndex(delay3)!] == 0.0) // Step 2 let state2 = try sim.step(state1) - #expect(state2[index(delay0)] == 10.0) - #expect(state2[index(delay1)] == 10.0) - #expect(state2[index(delay3)] == 0.0) + #expect(state2[plan.variableIndex(delay0)!] == 10.0) + #expect(state2[plan.variableIndex(delay1)!] == 10.0) + #expect(state2[plan.variableIndex(delay3)!] == 0.0) // Step 3 let state3 = try sim.step(state2) - #expect(state3[index(delay0)] == 10.0) - #expect(state3[index(delay1)] == 10.0) - #expect(state3[index(delay3)] == 0.0) + #expect(state3[plan.variableIndex(delay0)!] == 10.0) + #expect(state3[plan.variableIndex(delay1)!] == 10.0) + #expect(state3[plan.variableIndex(delay3)!] == 0.0) // Step 4 let state4 = try sim.step(state3) - #expect(state4[index(delay0)] == 10.0) - #expect(state4[index(delay1)] == 10.0) - #expect(state4[index(delay3)] == 10.0) + #expect(state4[plan.variableIndex(delay0)!] == 10.0) + #expect(state4[plan.variableIndex(delay1)!] == 10.0) + #expect(state4[plan.variableIndex(delay3)!] == 10.0) } @Test mutating func divByZeroFlow() throws { @@ -475,12 +500,12 @@ import Testing let flow = frame.createNode(ObjectType.FlowRate, name: "flow", attributes: ["formula": "1 / 0"]) frame.createEdge(ObjectType.Flow, origin: flow, target: stock) - try compile() - + let plan = try accept() + let sim = StockFlowSimulation(plan) let state = try sim.initialize() let state1 = try sim.step(state) - #expect(state1[index(stock)].isInfinite) + #expect(state1[plan.variableIndex(stock)!].isInfinite) } @Test mutating func stockCycle() throws { @@ -492,16 +517,16 @@ import Testing frame.createEdge(.Flow, origin: atob, target: b) frame.createEdge(.Flow, origin: b, target: btoa) frame.createEdge(.Flow, origin: btoa, target: a) - try compile() + let plan = try accept() let sim = StockFlowSimulation(plan) let state = try sim.initialize() let state1 = try sim.step(state) - #expect(state1[index(a)] == 8.0) - #expect(state1[index(b)] == 2.0) + #expect(state1[plan.variableIndex(a)!] == 8.0) + #expect(state1[plan.variableIndex(b)!] == 2.0) let state2 = try sim.step(state1) - #expect(state2[index(a)] == 7.0) - #expect(state2[index(b)] == 3.0) + #expect(state2[plan.variableIndex(a)!] == 7.0) + #expect(state2[plan.variableIndex(b)!] == 3.0) } } diff --git a/Tests/PoieticFlowsTests/Systems/ComputationOrderTests.swift b/Tests/PoieticFlowsTests/Systems/ComputationOrderTests.swift index 94c6681..7099581 100644 --- a/Tests/PoieticFlowsTests/Systems/ComputationOrderTests.swift +++ b/Tests/PoieticFlowsTests/Systems/ComputationOrderTests.swift @@ -18,18 +18,18 @@ import Testing self.frame = design.createFrame() } - func accept(_ frame: TransientFrame) throws -> AugmentedFrame { + func accept(_ frame: TransientFrame) throws -> World { let accepted = try design.accept(frame) - let runtime = AugmentedFrame(accepted) - return runtime + let world = World(frame: accepted) + return world } @Test func empty() throws { - let runtime = try accept(frame) - let system = ComputationOrderSystem() - try system.update(runtime) - let component: SimulationOrderComponent = try #require(runtime.component(for: .Frame)) + let world = try accept(frame) + let system = ComputationOrderSystem(world) + try system.update(world) + let component: SimulationOrderComponent = try #require(world.singleton()) #expect(component.objects.isEmpty) } @@ -42,10 +42,10 @@ import Testing frame.createEdge(ObjectType.Parameter, origin: a, target: b) frame.createEdge(ObjectType.Parameter, origin: b, target: c) - let runtime = try accept(frame) - let system = ComputationOrderSystem() - try system.update(runtime) - let component: SimulationOrderComponent = try #require(runtime.component(for: .Frame)) + let world = try accept(frame) + let system = ComputationOrderSystem(world) + try system.update(world) + let component: SimulationOrderComponent = try #require(world.singleton()) #expect(component.objects.count == 3) #expect(component.objects[0].objectID == a.objectID) @@ -59,14 +59,14 @@ import Testing frame.createEdge(ObjectType.Parameter, origin: a, target: b) frame.createEdge(ObjectType.Parameter, origin: b, target: a) - let runtime = try accept(frame) - let system = ComputationOrderSystem() - try system.update(runtime) - let component: SimulationOrderComponent? = runtime.component(for: .Frame) + let world = try accept(frame) + let system = ComputationOrderSystem(world) + try system.update(world) + let component: SimulationOrderComponent? = world.singleton() #expect(component == nil) - #expect(runtime.objectHasError(a.objectID, error: ModelError.computationCycle)) - #expect(runtime.objectHasError(b.objectID, error: ModelError.computationCycle)) + #expect(world.objectHasError(a.objectID, error: ModelError.computationCycle)) + #expect(world.objectHasError(b.objectID, error: ModelError.computationCycle)) } @Test func orderWithSpecialAuxiliary() throws { // p:Aux -> g:GF -> a:Aux @@ -77,11 +77,11 @@ import Testing frame.createEdge(ObjectType.Parameter, origin: param, target: gf) frame.createEdge(ObjectType.Parameter, origin: gf, target: aux) - let runtime = try accept(frame) - let system = ComputationOrderSystem() - try system.update(runtime) + let world = try accept(frame) + let system = ComputationOrderSystem(world) + try system.update(world) - let component: SimulationOrderComponent = try #require(runtime.component(for: .Frame)) + let component: SimulationOrderComponent = try #require(world.singleton()) #expect(component.objects.count == 3) #expect(component.objects[0].objectID == param.objectID) #expect(component.objects[1].objectID == gf.objectID) diff --git a/Tests/PoieticFlowsTests/Systems/NameCollectorSystemTests.swift b/Tests/PoieticFlowsTests/Systems/NameCollectorSystemTests.swift index 433e31d..e9b9df3 100644 --- a/Tests/PoieticFlowsTests/Systems/NameCollectorSystemTests.swift +++ b/Tests/PoieticFlowsTests/Systems/NameCollectorSystemTests.swift @@ -18,26 +18,26 @@ import Testing self.frame = design.createFrame() } - func accept(_ frame: TransientFrame) throws -> AugmentedFrame { + func accept(_ frame: TransientFrame) throws -> World { let accepted = try design.accept(frame) - let runtime = AugmentedFrame(accepted) + let world = World(frame: accepted) - let system = ComputationOrderSystem() - try system.update(runtime) - return runtime + let system = ComputationOrderSystem(world) + try system.update(world) + return world } @Test func empty() throws { let object = frame.createNode(.Note, name: "note") - let runtime = try accept(frame) - let system = NameResolutionSystem() - try system.update(runtime) + let world = try accept(frame) + let system = NameResolutionSystem(world) + try system.update(world) - let lookup: SimulationNameLookupComponent = try #require(runtime.component(for: .Frame)) + let lookup: SimulationNameLookupComponent = try #require(world.singleton()) #expect(lookup.namedObjects.isEmpty) - let component: SimulationObjectNameComponent? = runtime.component(for: .object(object.objectID)) + let component: SimulationObjectNameComponent? = world.component(for: object.objectID) #expect(component == nil) } @@ -45,69 +45,69 @@ import Testing let empty = frame.createNode(.Auxiliary, name: "") let whitespace = frame.createNode(.Auxiliary, name: " \t\n\r") - let runtime = try accept(frame) - let system = NameResolutionSystem() - try system.update(runtime) + let world = try accept(frame) + let system = NameResolutionSystem(world) + try system.update(world) - let lookup: SimulationNameLookupComponent = try #require(runtime.component(for: .Frame)) + let lookup: SimulationNameLookupComponent = try #require(world.singleton()) #expect(lookup.namedObjects.isEmpty) - #expect(runtime.objectHasError(empty.objectID, error: ModelError.emptyName)) - #expect(runtime.objectHasError(whitespace.objectID, error: ModelError.emptyName)) + #expect(world.objectHasError(empty.objectID, error: ModelError.emptyName)) + #expect(world.objectHasError(whitespace.objectID, error: ModelError.emptyName)) - let component1: SimulationObjectNameComponent? = runtime.component(for: .object(empty.objectID)) + let component1: SimulationObjectNameComponent? = world.component(for: empty.objectID) #expect(component1 == nil) - let component2: SimulationObjectNameComponent? = runtime.component(for: .object(whitespace.objectID)) + let component2: SimulationObjectNameComponent? = world.component(for: whitespace.objectID) #expect(component2 == nil) } @Test func trimmedName() throws { let object = frame.createNode(.Auxiliary, name: " object \n") - let runtime = try accept(frame) - let system = NameResolutionSystem() - try system.update(runtime) + let world = try accept(frame) + let system = NameResolutionSystem(world) + try system.update(world) - let lookup: SimulationNameLookupComponent = try #require(runtime.component(for: .Frame)) + let lookup: SimulationNameLookupComponent = try #require(world.singleton()) #expect(lookup.namedObjects["object"] == object.objectID) - let component: SimulationObjectNameComponent = try #require(runtime.component(for: .object(object.objectID))) + let component: SimulationObjectNameComponent = try #require(world.component(for: object.objectID)) #expect(component.name == "object") } @Test func duplicateName() throws { let object = frame.createNode(.Auxiliary, name: "object") let dupe = frame.createNode(.Auxiliary, name: "object") - let runtime = try accept(frame) - let system = NameResolutionSystem() - try system.update(runtime) + let world = try accept(frame) + let system = NameResolutionSystem(world) + try system.update(world) - let component: SimulationObjectNameComponent? = runtime.component(for: .object(object.objectID)) + let component: SimulationObjectNameComponent? = world.component(for: object.objectID) #expect(component == nil) - let dupeComponent: SimulationObjectNameComponent? = runtime.component(for: .object(dupe.objectID)) + let dupeComponent: SimulationObjectNameComponent? = world.component(for: dupe.objectID) #expect(dupeComponent == nil) - #expect(runtime.objectHasError(object.objectID, error: ModelError.duplicateName("object"))) - #expect(runtime.objectHasError(dupe.objectID, error: ModelError.duplicateName("object"))) + #expect(world.objectHasError(object.objectID, error: ModelError.duplicateName("object"))) + #expect(world.objectHasError(dupe.objectID, error: ModelError.duplicateName("object"))) } @Test func validAndDuplicateMix() throws { let object = frame.createNode(.Auxiliary, name: "object") let dupe = frame.createNode(.Auxiliary, name: "object") let single = frame.createNode(.Auxiliary, name: "single") - let runtime = try accept(frame) - let system = NameResolutionSystem() - try system.update(runtime) + let world = try accept(frame) + let system = NameResolutionSystem(world) + try system.update(world) - let component: SimulationObjectNameComponent? = runtime.component(for: .object(object.objectID)) + let component: SimulationObjectNameComponent? = world.component(for: object.objectID) #expect(component == nil) - let dupeComponent: SimulationObjectNameComponent? = runtime.component(for: .object(dupe.objectID)) + let dupeComponent: SimulationObjectNameComponent? = world.component(for: dupe.objectID) #expect(dupeComponent == nil) - let singleComponent: SimulationObjectNameComponent? = runtime.component(for: .object(single.objectID)) + let singleComponent: SimulationObjectNameComponent? = world.component(for: single.objectID) #expect(singleComponent?.name == "single") - #expect(runtime.objectHasError(object.objectID, error: ModelError.duplicateName("object"))) - #expect(runtime.objectHasError(dupe.objectID, error: ModelError.duplicateName("object"))) - #expect(!runtime.objectHasIssues(single.objectID)) + #expect(world.objectHasError(object.objectID, error: ModelError.duplicateName("object"))) + #expect(world.objectHasError(dupe.objectID, error: ModelError.duplicateName("object"))) + #expect(!world.objectHasIssues(single.objectID)) } } diff --git a/Tests/PoieticFlowsTests/Systems/ParameterResolutionSystemTests.swift b/Tests/PoieticFlowsTests/Systems/ParameterResolutionSystemTests.swift index da05058..741461b 100644 --- a/Tests/PoieticFlowsTests/Systems/ParameterResolutionSystemTests.swift +++ b/Tests/PoieticFlowsTests/Systems/ParameterResolutionSystemTests.swift @@ -18,9 +18,9 @@ import Testing self.frame = design.createFrame() } - func accept(_ frame: TransientFrame) throws -> AugmentedFrame { + func accept(_ frame: TransientFrame) throws -> World { let accepted = try design.accept(frame) - return AugmentedFrame(accepted) + return World(frame: accepted) } // MARK: - Basic Sanity Tests @@ -29,15 +29,15 @@ import Testing // DesignInfo has no formula, so no ResolvedParametersComponent should be created let info = frame.create(.DesignInfo, structure: .unstructured) - let runtime = try accept(frame) + let world = try accept(frame) - let parser = ExpressionParserSystem() - parser.update(runtime) + let parser = ExpressionParserSystem(world) + parser.update(world) - let system = ParameterResolutionSystem() - try system.update(runtime) + let system = ParameterResolutionSystem(world) + try system.update(world) - let component: ResolvedParametersComponent? = runtime.component(for: .object(info.objectID)) + let component: ResolvedParametersComponent? = world.component(for: info.objectID) #expect(component == nil) } @@ -48,17 +48,17 @@ import Testing let aux = frame.createNode(ObjectType.Auxiliary, name: "aux", attributes: ["formula": "1 + 1"]) - let runtime = try accept(frame) + let world = try accept(frame) - let parser = ExpressionParserSystem() - parser.update(runtime) + let parser = ExpressionParserSystem(world) + parser.update(world) - let system = ParameterResolutionSystem() - try system.update(runtime) + let system = ParameterResolutionSystem(world) + try system.update(world) - let component: ResolvedParametersComponent? = runtime.component(for: .object(aux.objectID)) + let component: ResolvedParametersComponent? = world.component(for: aux.objectID) #expect(component == nil, "No component should be created when no parameters needed") - #expect(!runtime.objectHasIssues(aux.objectID)) + #expect(!world.objectHasIssues(aux.objectID)) } @Test func formulaWithCorrectParameter() throws { @@ -68,39 +68,39 @@ import Testing frame.createEdge(.Parameter, origin: x, target: aux) - let runtime = try accept(frame) + let world = try accept(frame) - let parser = ExpressionParserSystem() - parser.update(runtime) + let parser = ExpressionParserSystem(world) + parser.update(world) - let system = ParameterResolutionSystem() - try system.update(runtime) + let system = ParameterResolutionSystem(world) + try system.update(world) - let component: ResolvedParametersComponent = try #require(runtime.component(for: .object(aux.objectID))) + let component: ResolvedParametersComponent = try #require(world.component(for: aux.objectID)) #expect(component.incoming.count == 1) #expect(component.incoming["x"] == aux.objectID) #expect(component.missing.isEmpty == true) #expect(component.unused.isEmpty == true) - #expect(!runtime.objectHasIssues(aux.objectID)) + #expect(!world.objectHasIssues(aux.objectID)) } @Test func formulaWithMissingParameter() throws { // Formula "x" without parameter connection let aux = frame.createNode(ObjectType.Auxiliary, name: "consumer", attributes: ["formula": "x"]) - let runtime = try accept(frame) + let world = try accept(frame) - let parser = ExpressionParserSystem() - parser.update(runtime) + let parser = ExpressionParserSystem(world) + parser.update(world) - let system = ParameterResolutionSystem() - try system.update(runtime) + let system = ParameterResolutionSystem(world) + try system.update(world) - let component: ResolvedParametersComponent = try #require(runtime.component(for: .object(aux.objectID))) + let component: ResolvedParametersComponent = try #require(world.component(for: aux.objectID)) #expect(component.incoming.isEmpty == true) #expect(component.missing == ["x"]) #expect(component.unused.isEmpty == true) - #expect(runtime.objectHasError(aux.objectID, error: ModelError.unknownParameter("x"))) + #expect(world.objectHasError(aux.objectID, error: ModelError.unknownParameter("x"))) } @Test func formulaWithUnusedParameter() throws { @@ -112,20 +112,20 @@ import Testing frame.createEdge(.Parameter, origin: x, target: aux) frame.createEdge(.Parameter, origin: y, target: aux) - let runtime = try accept(frame) + let world = try accept(frame) - let parser = ExpressionParserSystem() - parser.update(runtime) + let parser = ExpressionParserSystem(world) + parser.update(world) - let system = ParameterResolutionSystem() - try system.update(runtime) + let system = ParameterResolutionSystem(world) + try system.update(world) - let component: ResolvedParametersComponent = try #require(runtime.component(for: .object(aux.objectID))) + let component: ResolvedParametersComponent = try #require(world.component(for: aux.objectID)) #expect(component.incoming.count == 1) #expect(component.incoming["x"] == aux.objectID) #expect(component.missing.isEmpty == true) #expect(component.unused.count == 1) - #expect(runtime.objectHasError(aux.objectID, error: ModelError.unusedInput("y"))) + #expect(world.objectHasError(aux.objectID, error: ModelError.unusedInput("y"))) } @Test func formulaWithMixedParameters() throws { @@ -139,38 +139,38 @@ import Testing frame.createEdge(.Parameter, origin: c, target: aux) // Note: b is created but not connected - let runtime = try accept(frame) + let world = try accept(frame) - let parser = ExpressionParserSystem() - parser.update(runtime) + let parser = ExpressionParserSystem(world) + parser.update(world) - let system = ParameterResolutionSystem() - try system.update(runtime) + let system = ParameterResolutionSystem(world) + try system.update(world) - let component: ResolvedParametersComponent = try #require(runtime.component(for: .object(aux.objectID))) + let component: ResolvedParametersComponent = try #require(world.component(for: aux.objectID)) #expect(component.incoming.count == 1) #expect(component.incoming["a"] == aux.objectID) #expect(component.missing == ["b"]) #expect(component.unused.count == 1) - #expect(runtime.objectHasError(aux.objectID, error: ModelError.unknownParameter("b"))) - #expect(runtime.objectHasError(aux.objectID, error: ModelError.unusedInput("c"))) + #expect(world.objectHasError(aux.objectID, error: ModelError.unknownParameter("b"))) + #expect(world.objectHasError(aux.objectID, error: ModelError.unusedInput("c"))) } @Test func formulaWithBuiltinVariable() throws { // Formula using "time" builtin - should not require connection let aux = frame.createNode(.Auxiliary, name: "timer", attributes: ["formula": "time * 2"]) - let runtime = try accept(frame) + let world = try accept(frame) - let parser = ExpressionParserSystem() - parser.update(runtime) + let parser = ExpressionParserSystem(world) + parser.update(world) - let system = ParameterResolutionSystem() - try system.update(runtime) + let system = ParameterResolutionSystem(world) + try system.update(world) - let component: ResolvedParametersComponent? = runtime.component(for: .object(aux.objectID)) + let component: ResolvedParametersComponent? = world.component(for: aux.objectID) #expect(component == nil, "No component needed when only builtins used") - #expect(!runtime.objectHasIssues(aux.objectID)) + #expect(!world.objectHasIssues(aux.objectID)) } @Test func formulaWithMultipleCorrectParameters() throws { @@ -184,22 +184,22 @@ import Testing frame.createEdge(.Parameter, origin: y, target: aux) frame.createEdge(.Parameter, origin: z, target: aux) - let runtime = try accept(frame) + let world = try accept(frame) - let parser = ExpressionParserSystem() - parser.update(runtime) + let parser = ExpressionParserSystem(world) + parser.update(world) - let system = ParameterResolutionSystem() - try system.update(runtime) + let system = ParameterResolutionSystem(world) + try system.update(world) - let component: ResolvedParametersComponent = try #require(runtime.component(for: .object(aux.objectID))) + let component: ResolvedParametersComponent = try #require(world.component(for: aux.objectID)) #expect(component.incoming.count == 3) #expect(component.incoming["x"] == aux.objectID) #expect(component.incoming["y"] == aux.objectID) #expect(component.incoming["z"] == aux.objectID) #expect(component.missing.isEmpty == true) #expect(component.unused.isEmpty == true) - #expect(!runtime.objectHasIssues(aux.objectID)) + #expect(!world.objectHasIssues(aux.objectID)) } // MARK: - Delay Tests @@ -208,18 +208,18 @@ import Testing // Delay node with no parameter - should error let delay = frame.createNode(.Delay, name: "delayed", attributes: ["delay_duration": 5]) - let runtime = try accept(frame) + let world = try accept(frame) - let parser = ExpressionParserSystem() - parser.update(runtime) + let parser = ExpressionParserSystem(world) + parser.update(world) - let system = ParameterResolutionSystem() - try system.update(runtime) + let system = ParameterResolutionSystem(world) + try system.update(world) - let component: ResolvedParametersComponent = try #require(runtime.component(for: .object(delay.objectID))) + let component: ResolvedParametersComponent = try #require(world.component(for: delay.objectID)) #expect(component.connectedUnnamed.isEmpty == true) #expect(component.missingUnnamed == 1) - #expect(runtime.objectHasError(delay.objectID, error: ModelError.missingRequiredParameter)) + #expect(world.objectHasError(delay.objectID, error: ModelError.missingRequiredParameter)) } @Test func delayWithOneParameter() throws { @@ -228,19 +228,19 @@ import Testing let delay = frame.createNode(.Delay, name: "delayed", attributes: ["delay_duration": 5]) frame.createEdge(.Parameter, origin: source, target: delay) - let runtime = try accept(frame) + let world = try accept(frame) - let parser = ExpressionParserSystem() - parser.update(runtime) + let parser = ExpressionParserSystem(world) + parser.update(world) - let system = ParameterResolutionSystem() - try system.update(runtime) + let system = ParameterResolutionSystem(world) + try system.update(world) - let component: ResolvedParametersComponent = try #require(runtime.component(for: delay.objectID)) + let component: ResolvedParametersComponent = try #require(world.component(for: delay.objectID)) #expect(component.connectedUnnamed == [source.objectID]) #expect(component.missingUnnamed == 0) #expect(component.unused.isEmpty == true) - #expect(!runtime.objectHasIssues(delay.objectID)) + #expect(!world.objectHasIssues(delay.objectID)) } @Test func delayWithTwoParameters() throws { @@ -250,17 +250,18 @@ import Testing frame.createEdge(.Parameter, origin: source1, target: delay) frame.createEdge(.Parameter, origin: source2, target: delay) - let runtime = try accept(frame) + let world = try accept(frame) - let parser = ExpressionParserSystem() - parser.update(runtime) + let parser = ExpressionParserSystem(world) + parser.update(world) - let system = ParameterResolutionSystem() - try system.update(runtime) + let system = ParameterResolutionSystem(world) + try system.update(world) + try system.update(world) - let component: ResolvedParametersComponent = try #require(runtime.component(for: delay.objectID)) + let component: ResolvedParametersComponent = try #require(world.component(for: delay.objectID)) #expect(component.unused.count == 2) - #expect(runtime.objectHasError(delay.objectID, error: ModelError.tooManyParameters)) + #expect(world.objectHasError(delay.objectID, error: ModelError.tooManyParameters)) } // MARK: - Other auxiliaries @@ -275,22 +276,22 @@ import Testing frame.createEdge(.Parameter, origin: source, target: delay) frame.createEdge(.Parameter, origin: source, target: smooth) - let runtime = try accept(frame) + let world = try accept(frame) - let parser = ExpressionParserSystem() - parser.update(runtime) + let parser = ExpressionParserSystem(world) + parser.update(world) - let system = ParameterResolutionSystem() - try system.update(runtime) + let system = ParameterResolutionSystem(world) + try system.update(world) - let gfComp: ResolvedParametersComponent = try #require(runtime.component(for: gf.objectID)) - let delayComp: ResolvedParametersComponent = try #require(runtime.component(for: delay.objectID)) - let smoothComp: ResolvedParametersComponent = try #require(runtime.component(for: smooth.objectID)) + let gfComp: ResolvedParametersComponent = try #require(world.component(for: gf.objectID)) + let delayComp: ResolvedParametersComponent = try #require(world.component(for: delay.objectID)) + let smoothComp: ResolvedParametersComponent = try #require(world.component(for: smooth.objectID)) #expect(gfComp.connectedUnnamed == [source.objectID]) #expect(delayComp.connectedUnnamed == [source.objectID]) #expect(smoothComp.connectedUnnamed == [source.objectID]) - #expect(!runtime.objectHasIssues(gf.objectID)) - #expect(!runtime.objectHasIssues(delay.objectID)) - #expect(!runtime.objectHasIssues(smooth.objectID)) + #expect(!world.objectHasIssues(gf.objectID)) + #expect(!world.objectHasIssues(delay.objectID)) + #expect(!world.objectHasIssues(smooth.objectID)) } } diff --git a/Tests/PoieticFlowsTests/Systems/SimulationPlanningSystem.swift b/Tests/PoieticFlowsTests/Systems/SimulationPlanningSystem.swift index ea57cec..c72575b 100644 --- a/Tests/PoieticFlowsTests/Systems/SimulationPlanningSystem.swift +++ b/Tests/PoieticFlowsTests/Systems/SimulationPlanningSystem.swift @@ -17,24 +17,4 @@ import Testing @testable import PoieticCore @Suite struct SimulationPlannerSystemTests { - let design: Design - let frame: TransientFrame - - init() throws { - self.design = Design(metamodel: StockFlowMetamodel) - self.frame = design.createFrame() - } - - func accept(_ frame: TransientFrame) throws -> AugmentedFrame { - let stable = try design.accept(frame) - return AugmentedFrame(stable) - } - - // TEST: Failed expression -> tries to compile - - func createPlan() throws -> SimulationPlan? { - fatalError() - // TODO - - } } diff --git a/Tests/PoieticFlowsTests/Systems/StockFlowSystemTests.swift b/Tests/PoieticFlowsTests/Systems/StockFlowSystemTests.swift index 20cd689..0621ebe 100644 --- a/Tests/PoieticFlowsTests/Systems/StockFlowSystemTests.swift +++ b/Tests/PoieticFlowsTests/Systems/StockFlowSystemTests.swift @@ -18,9 +18,9 @@ import Testing self.frame = design.createFrame() } - func accept(_ frame: TransientFrame) throws -> AugmentedFrame { + func accept(_ frame: TransientFrame) throws -> World { let stable = try design.accept(frame) - return AugmentedFrame(stable) + return World(frame: stable) } // MARK: - Basic Sanity Tests @@ -28,11 +28,11 @@ import Testing @Test func isolatedFlowRate() throws { let flowRate = frame.createNode(.FlowRate, name: "isolated") - let runtime = try accept(frame) - let system = FlowCollectorSystem() - try system.update(runtime) + let world = try accept(frame) + let system = FlowCollectorSystem(world) + try system.update(world) - let component: FlowRateComponent = try #require(runtime.component(for: flowRate.objectID)) + let component: FlowRateComponent = try #require(world.component(for: flowRate.objectID)) #expect(component.drainsStock == nil) #expect(component.fillsStock == nil) #expect(component.priority == 0) @@ -45,11 +45,11 @@ import Testing let flowRate = frame.createNode(.FlowRate, name: "drain") frame.createEdge(.Flow, origin: stock, target: flowRate) - let runtime = try accept(frame) - let system = FlowCollectorSystem() - try system.update(runtime) + let world = try accept(frame) + let system = FlowCollectorSystem(world) + try system.update(world) - let component: FlowRateComponent = try #require(runtime.component(for: flowRate.objectID)) + let component: FlowRateComponent = try #require(world.component(for: flowRate.objectID)) #expect(component.drainsStock == stock.objectID) #expect(component.fillsStock == nil) #expect(component.priority == 0) @@ -60,11 +60,11 @@ import Testing let stock = frame.createNode(.Stock, name: "stock") frame.createEdge(.Flow, origin: flowRate, target: stock) - let runtime = try accept(frame) - let system = FlowCollectorSystem() - try system.update(runtime) + let world = try accept(frame) + let system = FlowCollectorSystem(world) + try system.update(world) - let component: FlowRateComponent = try #require(runtime.component(for: flowRate.objectID)) + let component: FlowRateComponent = try #require(world.component(for: flowRate.objectID)) #expect(component.drainsStock == nil) #expect(component.fillsStock == stock.objectID) #expect(component.priority == 0) @@ -78,11 +78,11 @@ import Testing frame.createEdge(.Flow, origin: source, target: flowRate) frame.createEdge(.Flow, origin: flowRate, target: target) - let runtime = try accept(frame) - let system = FlowCollectorSystem() - try system.update(runtime) + let world = try accept(frame) + let system = FlowCollectorSystem(world) + try system.update(world) - let component: FlowRateComponent = try #require(runtime.component(for: flowRate.objectID)) + let component: FlowRateComponent = try #require(world.component(for: flowRate.objectID)) #expect(component.drainsStock == source.objectID) #expect(component.fillsStock == target.objectID) #expect(component.priority == 0) @@ -96,11 +96,11 @@ import Testing frame.createEdge(.Flow, origin: source, target: flowRate) frame.createEdge(.Flow, origin: flowRate, target: target) - let runtime = try accept(frame) - let system = FlowCollectorSystem() - try system.update(runtime) + let world = try accept(frame) + let system = FlowCollectorSystem(world) + try system.update(world) - let component: FlowRateComponent = try #require(runtime.component(for: flowRate.objectID)) + let component: FlowRateComponent = try #require(world.component(for: flowRate.objectID)) #expect(component.drainsStock == source.objectID) #expect(component.fillsStock == target.objectID) #expect(component.priority == 0) @@ -111,11 +111,11 @@ import Testing @Test func flowRateWithExplicitPriority() throws { let flowRate = frame.createNode(.FlowRate, name: "priority_flow", attributes: ["priority": 5]) - let runtime = try accept(frame) - let system = FlowCollectorSystem() - try system.update(runtime) + let world = try accept(frame) + let system = FlowCollectorSystem(world) + try system.update(world) - let component: FlowRateComponent = try #require(runtime.component(for: flowRate.objectID)) + let component: FlowRateComponent = try #require(world.component(for: flowRate.objectID)) #expect(component.priority == 5) } } @@ -129,12 +129,12 @@ import Testing self.frame = design.createFrame() } - func accept(_ frame: TransientFrame) throws -> AugmentedFrame { + func accept(_ frame: TransientFrame) throws -> World { let stable = try design.accept(frame) - let runtime = AugmentedFrame(stable) - let flowSystem = FlowCollectorSystem() - try flowSystem.update(runtime) - return runtime + let world = World(frame: stable) + let flowSystem = FlowCollectorSystem(world) + try flowSystem.update(world) + return world } // MARK: - Basic Sanity Tests @@ -142,12 +142,12 @@ import Testing @Test func isolatedStock() throws { let stock = frame.createNode(.Stock, name: "isolated") - let runtime = try accept(frame) + let world = try accept(frame) - let system = StockDependencySystem() - try system.update(runtime) + let system = StockDependencySystem(world) + try system.update(world) - let component: StockComponent = try #require(runtime.component(for: stock.objectID)) + let component: StockComponent = try #require(world.component(for: stock.objectID)) #expect(component.inflowRates.isEmpty) #expect(component.outflowRates.isEmpty) #expect(component.inflowStocks.isEmpty) @@ -162,12 +162,12 @@ import Testing let stock = frame.createNode(.Stock, name: "stock") frame.createEdge(.Flow, origin: flowRate, target: stock) - let runtime = try accept(frame) + let world = try accept(frame) - let system = StockDependencySystem() - try system.update(runtime) + let system = StockDependencySystem(world) + try system.update(world) - let component: StockComponent = try #require(runtime.component(for: stock.objectID)) + let component: StockComponent = try #require(world.component(for: stock.objectID)) #expect(component.inflowRates == [flowRate.objectID]) #expect(component.outflowRates.isEmpty) #expect(component.inflowStocks.isEmpty) @@ -179,12 +179,12 @@ import Testing let flowRate = frame.createNode(.FlowRate, name: "outflow") frame.createEdge(.Flow, origin: stock, target: flowRate) - let runtime = try accept(frame) + let world = try accept(frame) - let system = StockDependencySystem() - try system.update(runtime) + let system = StockDependencySystem(world) + try system.update(world) - let component: StockComponent = try #require(runtime.component(for: stock.objectID)) + let component: StockComponent = try #require(world.component(for: stock.objectID)) #expect(component.inflowRates.isEmpty) #expect(component.outflowRates == [flowRate.objectID]) #expect(component.inflowStocks.isEmpty) @@ -199,12 +199,12 @@ import Testing frame.createEdge(.Flow, origin: inflow, target: stock) frame.createEdge(.Flow, origin: stock, target: outflow) - let runtime = try accept(frame) + let world = try accept(frame) - let system = StockDependencySystem() - try system.update(runtime) + let system = StockDependencySystem(world) + try system.update(world) - let component: StockComponent = try #require(runtime.component(for: stock.objectID)) + let component: StockComponent = try #require(world.component(for: stock.objectID)) #expect(component.inflowRates == [inflow.objectID]) #expect(component.outflowRates == [outflow.objectID]) #expect(component.inflowStocks.isEmpty) @@ -225,12 +225,12 @@ import Testing frame.createEdge(.Flow, origin: stock, target: outflow1) frame.createEdge(.Flow, origin: stock, target: outflow2) - let runtime = try accept(frame) + let world = try accept(frame) - let system = StockDependencySystem() - try system.update(runtime) + let system = StockDependencySystem(world) + try system.update(world) - let component: StockComponent = try #require(runtime.component(for: stock.objectID)) + let component: StockComponent = try #require(world.component(for: stock.objectID)) #expect(component.inflowRates.count == 2) #expect(component.inflowRates.contains(inflow1.objectID)) #expect(component.inflowRates.contains(inflow2.objectID)) @@ -249,17 +249,17 @@ import Testing frame.createEdge(.Flow, origin: stockA, target: flowRate) frame.createEdge(.Flow, origin: flowRate, target: stockB) - let runtime = try accept(frame) + let world = try accept(frame) - let system = StockDependencySystem() - try system.update(runtime) + let system = StockDependencySystem(world) + try system.update(world) - let compA: StockComponent = try #require(runtime.component(for: stockA.objectID)) + let compA: StockComponent = try #require(world.component(for: stockA.objectID)) #expect(compA.outflowRates == [flowRate.objectID]) #expect(compA.outflowStocks == [stockB.objectID]) #expect(compA.inflowStocks.isEmpty) - let compB: StockComponent = try #require(runtime.component(for: stockB.objectID)) + let compB: StockComponent = try #require(world.component(for: stockB.objectID)) #expect(compB.inflowRates == [flowRate.objectID]) #expect(compB.inflowStocks == [stockA.objectID]) #expect(compB.outflowStocks.isEmpty) @@ -278,23 +278,23 @@ import Testing frame.createEdge(.Flow, origin: stockB, target: flowBC) frame.createEdge(.Flow, origin: flowBC, target: stockC) - let runtime = try accept(frame) + let world = try accept(frame) - let flowSystem = FlowCollectorSystem() - try flowSystem.update(runtime) + let flowSystem = FlowCollectorSystem(world) + try flowSystem.update(world) - let system = StockDependencySystem() - try system.update(runtime) + let system = StockDependencySystem(world) + try system.update(world) - let compA: StockComponent = try #require(runtime.component(for: stockA.objectID)) + let compA: StockComponent = try #require(world.component(for: stockA.objectID)) #expect(compA.inflowStocks.isEmpty) #expect(compA.outflowStocks == [stockB.objectID]) - let compB: StockComponent = try #require(runtime.component(for: stockB.objectID)) + let compB: StockComponent = try #require(world.component(for: stockB.objectID)) #expect(compB.inflowStocks == [stockA.objectID]) #expect(compB.outflowStocks == [stockC.objectID]) - let compC: StockComponent = try #require(runtime.component(for: stockC.objectID)) + let compC: StockComponent = try #require(world.component(for: stockC.objectID)) #expect(compC.inflowStocks == [stockB.objectID]) #expect(compC.outflowStocks.isEmpty) } @@ -310,18 +310,18 @@ import Testing frame.createEdge(.Flow, origin: stockB, target: rateBA) frame.createEdge(.Flow, origin: rateBA, target: stockA) - let runtime = try accept(frame) + let world = try accept(frame) - let system = StockDependencySystem() - try system.update(runtime) + let system = StockDependencySystem(world) + try system.update(world) - let compA: StockComponent = try #require(runtime.component(for: stockA.objectID)) + let compA: StockComponent = try #require(world.component(for: stockA.objectID)) #expect(compA.outflowRates == [rateAB.objectID]) #expect(compA.outflowStocks == [stockB.objectID]) #expect(compA.inflowRates == [rateBA.objectID]) #expect(compA.inflowStocks == [stockB.objectID]) - let compB: StockComponent = try #require(runtime.component(for: stockB.objectID)) + let compB: StockComponent = try #require(world.component(for: stockB.objectID)) #expect(compB.inflowRates == [rateAB.objectID]) #expect(compB.inflowStocks == [stockA.objectID]) #expect(compB.outflowRates == [rateBA.objectID]) @@ -334,14 +334,14 @@ import Testing let stockNeg = frame.createNode(.Stock, name: "stock1", attributes: ["allows_negative": true]) let stockNotNeg = frame.createNode(.Stock, name: "stock2", attributes: ["allows_negative": false]) - let runtime = try accept(frame) + let world = try accept(frame) - let system = StockDependencySystem() - try system.update(runtime) + let system = StockDependencySystem(world) + try system.update(world) - let component1: StockComponent = try #require(runtime.component(for: stockNeg.objectID)) + let component1: StockComponent = try #require(world.component(for: stockNeg.objectID)) #expect(component1.allowsNegative == true) - let component2: StockComponent = try #require(runtime.component(for: stockNotNeg.objectID)) + let component2: StockComponent = try #require(world.component(for: stockNotNeg.objectID)) #expect(component2.allowsNegative == false) } } diff --git a/Tests/PoieticFlowsTests/World+extensions.swift b/Tests/PoieticFlowsTests/World+extensions.swift new file mode 100644 index 0000000..4d411ab --- /dev/null +++ b/Tests/PoieticFlowsTests/World+extensions.swift @@ -0,0 +1,28 @@ +// +// World+extensions.swift +// poietic-core +// +// Created by Stefan Urbanek on 22/12/2025. +// + +@testable import PoieticCore + +// Testing convenience methods +extension World { + func objectHasIssue(_ objectID: ObjectID, identifier: String) -> Bool { + guard let issues = objectIssues(objectID) else { return false } + return issues.contains { $0.identifier == identifier } + } + + + func objectHasError(_ objectID: ObjectID, error: T) -> Bool { + guard let issues = objectIssues(objectID) else { return false } + + for issue in issues { + if let objectError = issue.error as? T, objectError == error { + return true + } + } + return false + } +}