From 20561a9339d400f8ad32904ec5951daf8ea8ca6c Mon Sep 17 00:00:00 2001 From: Stefan Urbanek Date: Fri, 27 Feb 2026 11:31:25 +0100 Subject: [PATCH 1/3] Use new queries and entities from core - Updated the systems to use new entity-based query instead of world-entity access (now deprecated) --- Sources/PoieticFlows/Metamodel/Traits.swift | 1 - .../Simulation/RegularTimeSeries.swift | 20 ++ .../PoieticFlows/Simulation/Scenario.swift | 8 +- .../Systems/ComputationOrderSystem.swift | 95 ++++---- .../Systems/NameResolutionSystem.swift | 33 +-- .../ParameterConnectProposalSystem.swift | 4 +- .../Systems/ParameterResolutionSystem.swift | 18 +- .../Systems/PresentationSystems.swift | 3 +- .../Systems/SimulationPlanningSystem.swift | 206 +++++++----------- .../Systems/StockFlowSystem.swift | 10 +- Tests/PoieticFlowsTests/CompilerTests.swift | 8 +- .../Systems/ComputationOrderTests.swift | 6 +- .../Systems/NameCollectorSystemTests.swift | 44 ++-- .../ParameterResolutionSystemTests.swift | 73 ++++--- .../Systems/StockFlowSystemTests.swift | 40 ++-- .../PoieticFlowsTests/World+extensions.swift | 10 +- 16 files changed, 302 insertions(+), 277 deletions(-) diff --git a/Sources/PoieticFlows/Metamodel/Traits.swift b/Sources/PoieticFlows/Metamodel/Traits.swift index b8b2129..d550e59 100644 --- a/Sources/PoieticFlows/Metamodel/Traits.swift +++ b/Sources/PoieticFlows/Metamodel/Traits.swift @@ -216,7 +216,6 @@ extension Trait { 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/RegularTimeSeries.swift b/Sources/PoieticFlows/Simulation/RegularTimeSeries.swift index 20710e3..0d52703 100644 --- a/Sources/PoieticFlows/Simulation/RegularTimeSeries.swift +++ b/Sources/PoieticFlows/Simulation/RegularTimeSeries.swift @@ -71,3 +71,23 @@ public class RegularTimeSeries: Component /*: Sequence, RandomAccessCollection? return result } } + +extension RegularTimeSeries: InspectableComponent { + public static let attributeKeys: [String] = [ + "time_start", "time_end", "time_delta", "data_min", "data_max", + "values", "points", "count" + ] + public func attribute(forKey key: String) -> Variant? { + switch key { + case "time_start": Variant(self.startTime) + case "time_end": Variant(self.endTime) + case "time_delta": Variant(self.timeDelta) + case "data_min": Variant(self.dataMin) + case "data_max": Variant(self.dataMax) + case "values": Variant(self.data) + case "points": Variant(self.points()) + case "count": Variant(self.data.count) + default: nil + } + } +} diff --git a/Sources/PoieticFlows/Simulation/Scenario.swift b/Sources/PoieticFlows/Simulation/Scenario.swift index 5091869..93fe0b3 100644 --- a/Sources/PoieticFlows/Simulation/Scenario.swift +++ b/Sources/PoieticFlows/Simulation/Scenario.swift @@ -79,16 +79,16 @@ public struct SimulationSettings: Component { let timeDelta = object["time_delta", default: 0.0] let solverType = object["solver_type", default: "euler"] - if let endTime: Double = object["end_time"] { + if let steps: Int = object["steps"], steps >= 0 { self.init(initialTime: initialTime, timeDelta: timeDelta, - endTime: endTime, + steps: UInt(steps), solverType: solverType) } - else if let steps: Int = object["steps"], steps >= 0 { + else if let endTime: Double = object["end_time"] { self.init(initialTime: initialTime, timeDelta: timeDelta, - steps: UInt(steps), + endTime: endTime, solverType: solverType) } else { diff --git a/Sources/PoieticFlows/Systems/ComputationOrderSystem.swift b/Sources/PoieticFlows/Systems/ComputationOrderSystem.swift index ef8c885..838115a 100644 --- a/Sources/PoieticFlows/Systems/ComputationOrderSystem.swift +++ b/Sources/PoieticFlows/Systems/ComputationOrderSystem.swift @@ -24,9 +24,53 @@ public struct ComputationOrderSystem: System { guard let frame = world.frame else { return } + guard let snapshots = orderedSnapshots(world: world, frame: frame) else { + return + } + + var stocks: [ObjectID] = [] + var flows: [ObjectID] = [] + + // Determine simulation role: stock, flow, aux + // Note: See also filter in orderedSnapshots + for object in snapshots { + guard let entity = world.entity(object.objectID) else { continue /* Error? */ } + let role: SimulationObject.Role + + // TODO: Should we use Trait.Stock? + if object.type === ObjectType.Stock { + role = .stock + stocks.append(object.objectID) + } + else if object.type === ObjectType.FlowRate { + role = .flow + flows.append(object.objectID) + } + else if object.type.hasTrait(Trait.Auxiliary) { + role = .auxiliary + } + else { + throw InternalSystemError(self, + message: "Unknown simulation object role for object type: \(object.type.name)", + context: .object(object.objectID)) + } + let comp = SimulationRoleComponent(role: role) + entity.setComponent(comp) + } + + let orderComponent = SimulationOrderComponent( + objects: snapshots, + stocks: stocks, + flows: flows + ) + + world.setSingleton(orderComponent) + } + + func orderedSnapshots(world: World, frame: DesignFrame) -> [ObjectSnapshot]? { // 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 + // TODO: Should we use Trait.Stock? + // Note: See also roles in update() method let unordered: [ObjectSnapshot] = frame.filter { ($0.type === ObjectType.Stock || $0.type === ObjectType.FlowRate @@ -46,6 +90,7 @@ public struct ComputationOrderSystem: System { var nodes: Set = Set() for edge in cycleEdges { + guard let entity = world.entity(edge.id) else { continue } nodes.insert(edge.origin) nodes.insert(edge.target) let issue = Issue( @@ -54,57 +99,23 @@ public struct ComputationOrderSystem: System { system: self, error: ModelError.computationCycle, ) - world.appendIssue(issue, for: edge.id) + entity.appendIssue(issue) } for node in nodes { + guard let entity = world.entity(node) else { continue } let issue = Issue( identifier: "computation_cycle", severity: .error, system: self, error: ModelError.computationCycle, ) - world.appendIssue(issue, for: node) + entity.appendIssue(issue) } - return + return nil } - let snapshots = ordered.compactMap { frame[$0] } - - var stocks: [ObjectID] = [] - var flows: [ObjectID] = [] - - // Determine simulation role: stock, flow, aux - // Note: See filter at the beginning of the method - for object in snapshots { - let role: SimulationObject.Role - // TODO: Should we use Trait.Stock? - if object.type === ObjectType.Stock { - role = .stock - stocks.append(object.objectID) - } - else if object.type === ObjectType.FlowRate { - role = .flow - flows.append(object.objectID) - } - else if object.type.hasTrait(Trait.Auxiliary) { - role = .auxiliary - } - else { - throw InternalSystemError(self, - message: "Unknown simulation object role for object type: \(object.type.name)", - context: .object(object.objectID)) - } - let comp = SimulationRoleComponent(role: role) - world.setComponent(comp, for: object.objectID) - } - - let orderComponent = SimulationOrderComponent( - objects: snapshots, - stocks: stocks, - flows: flows - ) - - world.setSingleton(orderComponent) + let snapshots = ordered.compactMap { frame[$0] } + return snapshots } } diff --git a/Sources/PoieticFlows/Systems/NameResolutionSystem.swift b/Sources/PoieticFlows/Systems/NameResolutionSystem.swift index ec968dc..f407cb2 100644 --- a/Sources/PoieticFlows/Systems/NameResolutionSystem.swift +++ b/Sources/PoieticFlows/Systems/NameResolutionSystem.swift @@ -33,7 +33,9 @@ public struct NameResolutionSystem: System { var nameLookup: [String:ObjectID] = [:] for object in order.objects { - guard let name = object.name else { continue } + guard let name = object.name, + let entity = world.entity(object.objectID) + else { continue } let trimmedName = name.trimmingCharacters(in: .whitespacesAndNewlines) if trimmedName.isEmpty { let issue = Issue( @@ -42,15 +44,22 @@ public struct NameResolutionSystem: System { system: self, error: ModelError.emptyName, ) - world.appendIssue(issue, for: object.objectID) + entity.appendIssue(issue) continue } namedObjects[trimmedName, default: []].append(object.objectID) } - + // 2. Find duplicates - for (name, ids) in namedObjects where ids.count >= 1 { - guard ids.count == 1 else { + for (name, ids) in namedObjects { + if ids.count == 1 { + let onlyID = ids[0] + nameLookup[name] = onlyID + guard let entity = world.entity(onlyID) else { continue } + let comp = SimulationObjectNameComponent(name: name) + entity.setComponent(comp) + } + else if ids.count > 1 { let issue = Issue( identifier: "duplicate_name", severity: .error, @@ -58,20 +67,12 @@ public struct NameResolutionSystem: System { error: ModelError.duplicateName(name), ) // TODO: Add related nodes - for id in ids { - world.appendIssue(issue, for: id) + for entity in world.query(ids) { + entity.appendIssue(issue) } - continue } - nameLookup[name] = ids[0] - let comp = SimulationObjectNameComponent(name: name) - world.setComponent(comp, for: ids[0]) } - - let component = SimulationNameLookupComponent( - namedObjects: nameLookup - ) + let component = SimulationNameLookupComponent(namedObjects: nameLookup) world.setSingleton(component) } - } diff --git a/Sources/PoieticFlows/Systems/ParameterConnectProposalSystem.swift b/Sources/PoieticFlows/Systems/ParameterConnectProposalSystem.swift index 3efc0d7..576e244 100644 --- a/Sources/PoieticFlows/Systems/ParameterConnectProposalSystem.swift +++ b/Sources/PoieticFlows/Systems/ParameterConnectProposalSystem.swift @@ -75,8 +75,8 @@ public struct ParameterConnectionProposalSystem: System { var toRemove: [ObjectID] = [] var toAdd: [ParameterProposal.EdgeProposal] = [] - for (entityID, resolution) in world.query(ResolvedParametersComponent.self) { - guard let objectID = world.entityToObject(entityID) + for (entity, resolution) in world.query(ResolvedParametersComponent.self) { + guard let objectID = entity.objectID else { continue } if let selection, !selection.contains(objectID) { continue } diff --git a/Sources/PoieticFlows/Systems/ParameterResolutionSystem.swift b/Sources/PoieticFlows/Systems/ParameterResolutionSystem.swift index 0e386b0..bf01d6a 100644 --- a/Sources/PoieticFlows/Systems/ParameterResolutionSystem.swift +++ b/Sources/PoieticFlows/Systems/ParameterResolutionSystem.swift @@ -70,8 +70,8 @@ public struct ParameterResolutionSystem: System { public func resolveFormulas(_ world: World, frame: DesignFrame) throws (InternalSystemError) { let builtinNames = BuiltinVariable.allNames - for (entityID, exprComponent) in world.query(ParsedExpressionComponent.self) { - guard let objectID = world.entityToObject(entityID) else { continue } + for (entity, exprComponent) in world.query(ParsedExpressionComponent.self) { + guard let objectID = entity.objectID else { continue } let requiredParams = exprComponent.variables.subtracting(builtinNames) let incomingParams = frame.incoming(objectID).filter { $0.object.type === ObjectType.Parameter @@ -106,7 +106,7 @@ public struct ParameterResolutionSystem: System { error: ModelError.unknownParameter(name), details: ["name": Variant(name)] ) - world.appendIssue(issue, for: objectID) + entity.appendIssue(issue) } for edge in unused { @@ -118,7 +118,7 @@ public struct ParameterResolutionSystem: System { error: ModelError.unusedInput(name), details: ["name": Variant(name)] ) - world.appendIssue(issue, for: objectID) + entity.appendIssue(issue) } let paramComponent = ResolvedParametersComponent( @@ -126,7 +126,7 @@ public struct ParameterResolutionSystem: System { missing: Array(missing), unused: unused.map { $0.id } ) - world.setComponent(paramComponent, for: entityID) + entity.setComponent(paramComponent) } } /// Resolve connections of single-parameter auxiliaries such as graphical function, @@ -137,6 +137,8 @@ public struct ParameterResolutionSystem: System { public func resolveAuxiliaries(_ world: World, frame: DesignFrame, type: ObjectType) throws (InternalSystemError) { for object in frame.filter(type: type) { + guard let entity = world.entity(object.objectID) else { continue } + let incomingParams = frame.incoming(object.objectID).filter { $0.object.type === ObjectType.Parameter } @@ -149,7 +151,7 @@ public struct ParameterResolutionSystem: System { system: self, error: ModelError.missingRequiredParameter, ) - world.appendIssue(issue, for: object.objectID) + entity.appendIssue(issue) component = ResolvedParametersComponent( missingUnnamed: 1 @@ -162,7 +164,7 @@ public struct ParameterResolutionSystem: System { system: self, error: ModelError.tooManyParameters, ) - world.appendIssue(issue, for: object.objectID) + entity.appendIssue(issue) component = ResolvedParametersComponent( unused: incomingParams.map { $0.origin } @@ -174,7 +176,7 @@ public struct ParameterResolutionSystem: System { ) } - world.setComponent(component, for: object.objectID) + entity.setComponent(component) } diff --git a/Sources/PoieticFlows/Systems/PresentationSystems.swift b/Sources/PoieticFlows/Systems/PresentationSystems.swift index 415924d..b8b7bec 100644 --- a/Sources/PoieticFlows/Systems/PresentationSystems.swift +++ b/Sources/PoieticFlows/Systems/PresentationSystems.swift @@ -23,13 +23,14 @@ public struct ChartResolutionSystem: System { let chartObjects = frame.filter { $0.type === ObjectType.Chart } for chartObject in chartObjects { + guard let entity = world.entity(chartObject.objectID) else { continue } let seriesEdges = frame.outgoing(chartObject.objectID).filter { $0.object.type === ObjectType.ChartSeries } let series = seriesEdges.map { $0.targetObject } let chart = ChartComponent(chartObject: chartObject, series: series) - world.setComponent(chart, for: chartObject.objectID) + entity.setComponent(chart) } } } diff --git a/Sources/PoieticFlows/Systems/SimulationPlanningSystem.swift b/Sources/PoieticFlows/Systems/SimulationPlanningSystem.swift index 61271e4..91ce94a 100644 --- a/Sources/PoieticFlows/Systems/SimulationPlanningSystem.swift +++ b/Sources/PoieticFlows/Systems/SimulationPlanningSystem.swift @@ -20,14 +20,11 @@ class StateVariableTable { let index = self.allocate(content: .builtin(builtin), valueType: builtin.valueType, name: builtin.name) - return index } @discardableResult - func allocate(content: StateVariable.Content, - valueType: ValueType, - name: String) -> Int + func allocate(content: StateVariable.Content, valueType: ValueType, name: String) -> Int { let index = variables.count let variable = StateVariable(index: index, @@ -136,8 +133,9 @@ public struct SimulationPlanningSystem: System { let builtins = prepareBuiltins(variables: variables) for object in simOrder.objects { - guard let nameComp: SimulationObjectNameComponent = world.component(for: object.objectID), - let roleComp: SimulationRoleComponent = world.component(for: object.objectID) + guard let entity = world.entity(object.objectID), + let nameComp: SimulationObjectNameComponent = entity.component(), + let roleComp: SimulationRoleComponent = entity.component() else { hasError = true continue @@ -146,7 +144,7 @@ public struct SimulationPlanningSystem: System { let rep: ComputationalRepresentation do { - rep = try compileObject(object, world: world, variables: variables) + rep = try compileObject(object, entity: entity, variables: variables) } catch .objectIssue { hasError = true @@ -169,7 +167,6 @@ public struct SimulationPlanningSystem: System { valueType: rep.valueType, name: nameComp.name) - simulationObjects.append(sim) switch roleComp.role { @@ -198,7 +195,6 @@ public struct SimulationPlanningSystem: System { let boundStocks = try bindStocks(stocks, flowIndices: flowIndices, world: world) // Simulation parameters - let params: SimulationSettings if let simInfo = frame.first(trait: Trait.Simulation) { params = SimulationSettings(fromObject: simInfo) @@ -214,7 +210,6 @@ public struct SimulationPlanningSystem: System { builtins: builtins, stocks: boundStocks, flows: boundFlows, -// charts: [], // FIXME: Relic from the past, remove valueBindings: [], // FIXME: Relic from the past, remove simulationParameters: params ) @@ -223,7 +218,6 @@ public struct SimulationPlanningSystem: System { } func prepareBuiltins(variables: StateVariableTable) -> BoundBuiltins { - // 1. Builtins let builtins = BoundBuiltins( step: variables.allocate(builtin: .step), time: variables.allocate(builtin: .time), @@ -235,38 +229,33 @@ public struct SimulationPlanningSystem: System { /// Compile an object into its computational representation. /// - /// - Returns: Computational representation of the object or `nil`. - /// - /// nil result if: - /// - /// - missing required component - /// - missing required attribute + /// - Returns: Computational representation of the object. + /// - Throws: ``CompilationError`` when missing required component or an attribute. /// func compileObject(_ object: ObjectSnapshot, - world: World, + entity: RuntimeEntity, 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, world: world, variables: variables) + rep = try compileFormulaObject(object, entity: entity, variables: variables) } else if object.type.hasTrait(Trait.GraphicalFunction) { - rep = try compileGraphicalFunctionNode(object, world: world, variables: variables) + rep = try compileGraphicalFunctionNode(object, entity: entity, variables: variables) } else if object.type.hasTrait(Trait.Delay) { - rep = try compileDelayNode(object, world: world, variables: variables) + rep = try compileDelayNode(object, entity: entity, variables: variables) } else if object.type.hasTrait(Trait.Smooth) { - rep = try compileSmoothNode(object, world: world, variables: variables) + rep = try compileSmoothNode(object, entity: entity, variables: variables) } else { - // Hint: If this error happens, then check one of the the following: - // - the condition in the stock-flows view method returning - // simulation nodes - // - whether the object design constraints work properly - // - whether the object design metamodel is stock-flows metamodel - // and that it has necessary components + // HINT: If this error happens, then check one of the the following: + // - ComputationOrderSystem and SimulationOrderComponent + // - whether the design object constraints work properly + // - whether the design metamodel is stock-flows metamodel + // and that it has necessary traits // fatalError("Unknown simulation object type \(object.type.name), object: \(object.objectID)") } @@ -294,12 +283,11 @@ public struct SimulationPlanningSystem: System { /// function names or other variable names in the expression. /// func compileFormulaObject(_ object: ObjectSnapshot, - world: World, + entity: RuntimeEntity, variables: StateVariableTable) throws (CompilationError) -> ComputationalRepresentation - { - guard let component: ParsedExpressionComponent = world.component(for: object.objectID) else { + guard let component: ParsedExpressionComponent = entity.component() else { throw .missingComponent("ParsedExpressionComponent") } let expression = component.expression @@ -324,7 +312,7 @@ public struct SimulationPlanningSystem: System { ] ) - world.appendIssue(issue, for: object.objectID) + entity.appendIssue(issue) throw .objectIssue } @@ -344,7 +332,7 @@ public struct SimulationPlanningSystem: System { /// - SeeAlso: ``CompiledGraphicalFunction``, ``Solver/evaluate(objectAt:with:)`` /// func compileGraphicalFunctionNode(_ object: ObjectSnapshot, - world: World, + entity: RuntimeEntity, variables: StateVariableTable) throws (CompilationError) -> ComputationalRepresentation { let points:[Point] = object["graphical_function_points", default: []] @@ -356,12 +344,10 @@ public struct SimulationPlanningSystem: System { let function = GraphicalFunction(points: points, method: method) - guard let paramComp: ResolvedParametersComponent = world.component(for: object.objectID), + guard let paramComp: ResolvedParametersComponent = entity.component(), paramComp.connectedUnnamed.count == 1, let parameterID = paramComp.connectedUnnamed.first - else { - throw .objectIssue - } + else { throw .objectIssue } guard let paramIndex = variables.index(parameterID) else { throw .corruptedState("Invalid variable index)") @@ -372,8 +358,8 @@ public struct SimulationPlanningSystem: System { } func compileDelayNode(_ object: ObjectSnapshot, - world: World, - variables: StateVariableTable) + entity: RuntimeEntity, + variables: StateVariableTable) throws (CompilationError) -> ComputationalRepresentation { // TODO: What to do if the input is not numeric or not an atom? @@ -389,12 +375,10 @@ public struct SimulationPlanningSystem: System { name: "delay_init_\(object.objectID)" ) - guard let paramComp: ResolvedParametersComponent = world.component(for: object.objectID), + guard let paramComp: ResolvedParametersComponent = entity.component(), paramComp.connectedUnnamed.count == 1, let parameterID = paramComp.connectedUnnamed.first - else { - throw .objectIssue - } + else { throw .objectIssue } guard let parameterIndex = variables.index(parameterID) else { throw .corruptedState("Invalid variable index)") @@ -413,7 +397,7 @@ public struct SimulationPlanningSystem: System { error: ModelError.invalidParameterType, relatedObjects: [parameterID] ) - world.appendIssue(issue, for: object.objectID) + entity.appendIssue(issue) throw .objectIssue } @@ -430,8 +414,8 @@ public struct SimulationPlanningSystem: System { return .delay(compiled) } func compileSmoothNode(_ object: ObjectSnapshot, - world: World, - variables: StateVariableTable) + entity: RuntimeEntity, + variables: StateVariableTable) throws (CompilationError) -> ComputationalRepresentation { let smoothValueIndex = variables.allocate( content: .internalState(object.objectID), @@ -439,12 +423,10 @@ public struct SimulationPlanningSystem: System { name: "smooth_value_\(object.objectID)" ) - guard let paramComp: ResolvedParametersComponent = world.component(for: object.objectID), + guard let paramComp: ResolvedParametersComponent = entity.component(), paramComp.connectedUnnamed.count == 1, let parameterID = paramComp.connectedUnnamed.first - else { - throw .objectIssue - } + else { throw .objectIssue } guard let parameterIndex = variables.index(parameterID) else { throw .corruptedState("Invalid variable index)") @@ -460,7 +442,7 @@ public struct SimulationPlanningSystem: System { error: ModelError.invalidParameterType, relatedObjects: [parameterID] ) - world.appendIssue(issue, for: object.objectID) + entity.appendIssue(issue) throw .objectIssue } @@ -485,46 +467,34 @@ public struct SimulationPlanningSystem: System { var boundFlows: [BoundFlow] = [] for flow in flows { - let boundFlow: BoundFlow - boundFlow = try bindFlow(flow.objectID, - name: flow.name, - valueType: flow.valueType, - variables: variables, - world: world) + guard let entity = world.entity(flow.objectID), + let component: FlowRateComponent = entity.component() + else { + throw InternalSystemError(self, + message: "Malformed flow rate simulation object", + context: .singleton("FlowRateComponent")) + } + guard let objectIndex = variables.objectIndex[flow.objectID] else { + // TODO: Throw corrupted component + preconditionFailure() + } + let actualIndex = variables.allocate(content: .adjustedResult(flow.objectID), + valueType: flow.valueType, + name: flow.name) + + let boundFlow = BoundFlow(objectID: flow.objectID, + estimatedValueIndex: objectIndex, + adjustedValueIndex: actualIndex, + priority: component.priority, + drains: component.drainsStock, + fills: component.fillsStock) + boundFlows.append(boundFlow) } return boundFlows } - func bindFlow(_ objectID: ObjectID, - name: String, - valueType: ValueType, // rep.valueType - variables: StateVariableTable, - world: World) - throws (InternalSystemError) -> BoundFlow { - guard let component: FlowRateComponent = world.component(for: objectID) else { - throw InternalSystemError(self, - message: "Missing required component", - context: .singleton("FlowRateComponent")) - } - guard let objectIndex = variables.objectIndex[objectID] else { - // TODO: Throw corrupted component - preconditionFailure() - } - let actualIndex = variables.allocate(content: .adjustedResult(objectID), - valueType: valueType, - name: name) - let boundFlow = BoundFlow(objectID: objectID, - estimatedValueIndex: objectIndex, - adjustedValueIndex: actualIndex, - priority: component.priority, - drains: component.drainsStock, - fills: component.fillsStock) - - return boundFlow - } - /// Bind stocks with their variables. /// func bindStocks(_ stocks: [SimulationObject], @@ -534,48 +504,36 @@ public struct SimulationPlanningSystem: System { var result: [BoundStock] = [] for stock in stocks { - let boundStock: BoundStock - boundStock = try bindStock(stock.objectID, - variableIndex: stock.variableIndex, - flowIndices: flowIndices, - world: world) - result.append(boundStock) - } - return result - } - - func bindStock(_ objectID: ObjectID, - variableIndex: Int, - flowIndices: [ObjectID:Int], // Index into list of flows - world: World) - throws (InternalSystemError) -> BoundStock { - guard let comp: StockComponent = world.component(for: objectID) else { - throw InternalSystemError(self, - message: "Missing component", - context: .singleton("StockDependencyComponent")) - } + guard let entity = world.entity(stock.objectID), + let component: StockComponent = entity.component() + else { + throw InternalSystemError(self, + message: "Malformed stock simulation object", + context: .singleton("FlowRateComponent")) + } - let inflowIndices = comp.inflowRates.compactMap { flowIndices[$0] } - let outflowIndices = comp.outflowRates.compactMap { flowIndices[$0] } + let inflowIndices = component.inflowRates.compactMap { flowIndices[$0] } + let outflowIndices = component.outflowRates.compactMap { flowIndices[$0] } - guard inflowIndices.count == comp.inflowRates.count && - outflowIndices.count == comp.outflowRates.count - else { - throw InternalSystemError(self, - message: "Corrupted component", - context: .singleton("StockDependencyComponent")) - } - - let boundStock = BoundStock( - objectID: objectID, - variableIndex: variableIndex, - allowsNegative: comp.allowsNegative, - inflows: inflowIndices, - outflows: outflowIndices - ) - - return boundStock + guard inflowIndices.count == component.inflowRates.count && + outflowIndices.count == component.outflowRates.count + else { + throw InternalSystemError(self, + message: "Corrupted component", + context: .singleton("StockDependencyComponent")) + } + + let boundStock = BoundStock( + objectID: stock.objectID, + variableIndex: stock.variableIndex, + allowsNegative: component.allowsNegative, + inflows: inflowIndices, + outflows: outflowIndices + ) + result.append(boundStock) + } + return result } } diff --git a/Sources/PoieticFlows/Systems/StockFlowSystem.swift b/Sources/PoieticFlows/Systems/StockFlowSystem.swift index 390eb9c..21b8285 100644 --- a/Sources/PoieticFlows/Systems/StockFlowSystem.swift +++ b/Sources/PoieticFlows/Systems/StockFlowSystem.swift @@ -22,6 +22,8 @@ public struct FlowCollectorSystem: System { guard let frame = world.frame else { return } for flow in frame.filter(type: .FlowRate) { + let entity = world.entity(flow.objectID)! + // 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 { $0.object.type === ObjectType.Flow @@ -36,7 +38,7 @@ public struct FlowCollectorSystem: System { let component = FlowRateComponent(drainsStock: drains, fillsStock: fills, priority: priority) - world.setComponent(component, for: flow.objectID) + entity.setComponent(component) } } } @@ -69,7 +71,8 @@ public struct StockDependencySystem: System { var outflowStocks: [ObjectID:[ObjectID]] = [:] // [drained stock:[to filling stock]] for flow in frame.filter(type: .FlowRate) { - guard let component: FlowRateComponent = world.component(for: flow.objectID) else { + let entity = world.entity(flow.objectID)! + guard let component: FlowRateComponent = entity.component() else { continue } if let stockID = component.fillsStock { @@ -87,6 +90,7 @@ public struct StockDependencySystem: System { } for stock in frame.filter(type: .Stock) { + let entity = world.entity(stock.objectID)! let component = StockComponent( inflowRates: filledByRate[stock.objectID] ?? [], outflowRates: drainedByRate[stock.objectID] ?? [], @@ -94,7 +98,7 @@ public struct StockDependencySystem: System { outflowStocks: outflowStocks[stock.objectID] ?? [], allowsNegative: stock["allows_negative", default: false] ) - world.setComponent(component, for: stock.objectID) + entity.setComponent(component) } } } diff --git a/Tests/PoieticFlowsTests/CompilerTests.swift b/Tests/PoieticFlowsTests/CompilerTests.swift index f2518d5..310b735 100644 --- a/Tests/PoieticFlowsTests/CompilerTests.swift +++ b/Tests/PoieticFlowsTests/CompilerTests.swift @@ -75,8 +75,9 @@ extension TransientFrame { try acceptAndUpdate() let plan: SimulationPlan? = world.singleton() #expect(plan == nil) - #expect(world.objectHasError(aux.objectID, - error: ExpressionError.unknownFunction("nonexistent"))) + + let entity = try #require(world.entity(aux.objectID)) + #expect(entity.hasError(ExpressionError.unknownFunction("nonexistent"))) } @Test func singleComputedVariable() throws { @@ -120,7 +121,8 @@ extension TransientFrame { try acceptAndUpdate() let plan: SimulationPlan? = world.singleton() #expect(plan == nil) - #expect(world.objectHasIssue(gf.objectID, identifier: "missing_required_parameter")) + let entity = try #require(world.entity(gf.objectID)) + #expect(entity.hasIssue(identifier: "missing_required_parameter")) } @Test func graphicalFunctionComputation() throws { diff --git a/Tests/PoieticFlowsTests/Systems/ComputationOrderTests.swift b/Tests/PoieticFlowsTests/Systems/ComputationOrderTests.swift index 7099581..6a4ee7b 100644 --- a/Tests/PoieticFlowsTests/Systems/ComputationOrderTests.swift +++ b/Tests/PoieticFlowsTests/Systems/ComputationOrderTests.swift @@ -65,8 +65,10 @@ import Testing let component: SimulationOrderComponent? = world.singleton() #expect(component == nil) - #expect(world.objectHasError(a.objectID, error: ModelError.computationCycle)) - #expect(world.objectHasError(b.objectID, error: ModelError.computationCycle)) + let aEnt = try #require(world.entity(a.objectID)) + #expect(aEnt.hasError(ModelError.computationCycle)) + let bEnt = try #require(world.entity(a.objectID)) + #expect(bEnt.hasError(ModelError.computationCycle)) } @Test func orderWithSpecialAuxiliary() throws { // p:Aux -> g:GF -> a:Aux diff --git a/Tests/PoieticFlowsTests/Systems/NameCollectorSystemTests.swift b/Tests/PoieticFlowsTests/Systems/NameCollectorSystemTests.swift index e9b9df3..4b141be 100644 --- a/Tests/PoieticFlowsTests/Systems/NameCollectorSystemTests.swift +++ b/Tests/PoieticFlowsTests/Systems/NameCollectorSystemTests.swift @@ -37,7 +37,7 @@ import Testing let lookup: SimulationNameLookupComponent = try #require(world.singleton()) #expect(lookup.namedObjects.isEmpty) - let component: SimulationObjectNameComponent? = world.component(for: object.objectID) + let component: SimulationObjectNameComponent? = world.entity(object.objectID)?.component() #expect(component == nil) } @@ -52,12 +52,14 @@ import Testing let lookup: SimulationNameLookupComponent = try #require(world.singleton()) #expect(lookup.namedObjects.isEmpty) - #expect(world.objectHasError(empty.objectID, error: ModelError.emptyName)) - #expect(world.objectHasError(whitespace.objectID, error: ModelError.emptyName)) + let emptyEnt = try #require(world.entity(empty.objectID)) + #expect(emptyEnt.hasError(ModelError.emptyName)) + let wsEnt = try #require(world.entity(whitespace.objectID)) + #expect(wsEnt.hasError(ModelError.emptyName)) - let component1: SimulationObjectNameComponent? = world.component(for: empty.objectID) + let component1: SimulationObjectNameComponent? = world.entity(empty.objectID)?.component() #expect(component1 == nil) - let component2: SimulationObjectNameComponent? = world.component(for: whitespace.objectID) + let component2: SimulationObjectNameComponent? = world.entity(whitespace.objectID)?.component() #expect(component2 == nil) } @@ -71,7 +73,7 @@ import Testing let lookup: SimulationNameLookupComponent = try #require(world.singleton()) #expect(lookup.namedObjects["object"] == object.objectID) - let component: SimulationObjectNameComponent = try #require(world.component(for: object.objectID)) + let component: SimulationObjectNameComponent = try #require(world.entity(object.objectID)?.component()) #expect(component.name == "object") } @Test func duplicateName() throws { @@ -82,13 +84,15 @@ import Testing let system = NameResolutionSystem(world) try system.update(world) - let component: SimulationObjectNameComponent? = world.component(for: object.objectID) + let objEnt = try #require(world.entity(object.objectID)) + let component: SimulationObjectNameComponent? = objEnt.component() #expect(component == nil) - let dupeComponent: SimulationObjectNameComponent? = world.component(for: dupe.objectID) - #expect(dupeComponent == nil) + #expect(objEnt.hasError(ModelError.duplicateName("object"))) - #expect(world.objectHasError(object.objectID, error: ModelError.duplicateName("object"))) - #expect(world.objectHasError(dupe.objectID, error: ModelError.duplicateName("object"))) + let dupeEnt = try #require(world.entity(dupe.objectID)) + let dupeComponent: SimulationObjectNameComponent? = dupeEnt.component() + #expect(dupeComponent == nil) + #expect(dupeEnt.hasError(ModelError.duplicateName("object"))) } @Test func validAndDuplicateMix() throws { let object = frame.createNode(.Auxiliary, name: "object") @@ -99,15 +103,19 @@ import Testing let system = NameResolutionSystem(world) try system.update(world) - let component: SimulationObjectNameComponent? = world.component(for: object.objectID) + let objEnt = try #require(world.entity(object.objectID)) + let component: SimulationObjectNameComponent? = objEnt.component() #expect(component == nil) - let dupeComponent: SimulationObjectNameComponent? = world.component(for: dupe.objectID) + #expect(objEnt.hasError(ModelError.duplicateName("object"))) + + let dupeEnt = try #require(world.entity(dupe.objectID)) + let dupeComponent: SimulationObjectNameComponent? = dupeEnt.component() #expect(dupeComponent == nil) - let singleComponent: SimulationObjectNameComponent? = world.component(for: single.objectID) - #expect(singleComponent?.name == "single") + #expect(dupeEnt.hasError(ModelError.duplicateName("object"))) - #expect(world.objectHasError(object.objectID, error: ModelError.duplicateName("object"))) - #expect(world.objectHasError(dupe.objectID, error: ModelError.duplicateName("object"))) - #expect(!world.objectHasIssues(single.objectID)) + let singleEnt = try #require(world.entity(single.objectID)) + let singleComponent: SimulationObjectNameComponent? = singleEnt.component() + #expect(singleComponent?.name == "single") + #expect(!singleEnt.hasIssues) } } diff --git a/Tests/PoieticFlowsTests/Systems/ParameterResolutionSystemTests.swift b/Tests/PoieticFlowsTests/Systems/ParameterResolutionSystemTests.swift index 741461b..52b5eeb 100644 --- a/Tests/PoieticFlowsTests/Systems/ParameterResolutionSystemTests.swift +++ b/Tests/PoieticFlowsTests/Systems/ParameterResolutionSystemTests.swift @@ -37,7 +37,8 @@ import Testing let system = ParameterResolutionSystem(world) try system.update(world) - let component: ResolvedParametersComponent? = world.component(for: info.objectID) + let entity = try #require(world.entity(info.objectID)) + let component: ResolvedParametersComponent? = entity.component() #expect(component == nil) } @@ -56,9 +57,10 @@ import Testing let system = ParameterResolutionSystem(world) try system.update(world) - let component: ResolvedParametersComponent? = world.component(for: aux.objectID) + let entity = try #require(world.entity(aux.objectID)) + let component: ResolvedParametersComponent? = entity.component() #expect(component == nil, "No component should be created when no parameters needed") - #expect(!world.objectHasIssues(aux.objectID)) + #expect(!entity.hasIssues) } @Test func formulaWithCorrectParameter() throws { @@ -76,12 +78,13 @@ import Testing let system = ParameterResolutionSystem(world) try system.update(world) - let component: ResolvedParametersComponent = try #require(world.component(for: aux.objectID)) + let entity = try #require(world.entity(aux.objectID)) + let component: ResolvedParametersComponent = try #require(entity.component()) #expect(component.incoming.count == 1) #expect(component.incoming["x"] == aux.objectID) #expect(component.missing.isEmpty == true) #expect(component.unused.isEmpty == true) - #expect(!world.objectHasIssues(aux.objectID)) + #expect(!entity.hasIssues) } @Test func formulaWithMissingParameter() throws { @@ -96,11 +99,13 @@ import Testing let system = ParameterResolutionSystem(world) try system.update(world) - let component: ResolvedParametersComponent = try #require(world.component(for: aux.objectID)) + + let entity = try #require(world.entity(aux.objectID)) + let component: ResolvedParametersComponent = try #require(entity.component()) #expect(component.incoming.isEmpty == true) #expect(component.missing == ["x"]) #expect(component.unused.isEmpty == true) - #expect(world.objectHasError(aux.objectID, error: ModelError.unknownParameter("x"))) + #expect(entity.hasError(ModelError.unknownParameter("x"))) } @Test func formulaWithUnusedParameter() throws { @@ -120,12 +125,13 @@ import Testing let system = ParameterResolutionSystem(world) try system.update(world) - let component: ResolvedParametersComponent = try #require(world.component(for: aux.objectID)) + let entity = try #require(world.entity(aux.objectID)) + let component: ResolvedParametersComponent = try #require(entity.component()) #expect(component.incoming.count == 1) #expect(component.incoming["x"] == aux.objectID) #expect(component.missing.isEmpty == true) #expect(component.unused.count == 1) - #expect(world.objectHasError(aux.objectID, error: ModelError.unusedInput("y"))) + #expect(entity.hasError(ModelError.unusedInput("y"))) } @Test func formulaWithMixedParameters() throws { @@ -147,13 +153,14 @@ import Testing let system = ParameterResolutionSystem(world) try system.update(world) - let component: ResolvedParametersComponent = try #require(world.component(for: aux.objectID)) + let entity = try #require(world.entity(aux.objectID)) + let component: ResolvedParametersComponent = try #require(entity.component()) #expect(component.incoming.count == 1) #expect(component.incoming["a"] == aux.objectID) #expect(component.missing == ["b"]) #expect(component.unused.count == 1) - #expect(world.objectHasError(aux.objectID, error: ModelError.unknownParameter("b"))) - #expect(world.objectHasError(aux.objectID, error: ModelError.unusedInput("c"))) + #expect(entity.hasError(ModelError.unknownParameter("b"))) + #expect(entity.hasError(ModelError.unusedInput("c"))) } @Test func formulaWithBuiltinVariable() throws { @@ -168,9 +175,10 @@ import Testing let system = ParameterResolutionSystem(world) try system.update(world) - let component: ResolvedParametersComponent? = world.component(for: aux.objectID) + let entity = try #require(world.entity(aux.objectID)) + let component: ResolvedParametersComponent? = entity.component() #expect(component == nil, "No component needed when only builtins used") - #expect(!world.objectHasIssues(aux.objectID)) + #expect(!entity.hasIssues) } @Test func formulaWithMultipleCorrectParameters() throws { @@ -192,14 +200,15 @@ import Testing let system = ParameterResolutionSystem(world) try system.update(world) - let component: ResolvedParametersComponent = try #require(world.component(for: aux.objectID)) + let entity = try #require(world.entity(aux.objectID)) + let component: ResolvedParametersComponent = try #require(entity.component()) #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(!world.objectHasIssues(aux.objectID)) + #expect(!entity.hasIssues) } // MARK: - Delay Tests @@ -216,10 +225,11 @@ import Testing let system = ParameterResolutionSystem(world) try system.update(world) - let component: ResolvedParametersComponent = try #require(world.component(for: delay.objectID)) + let entity = try #require(world.entity(delay.objectID)) + let component: ResolvedParametersComponent = try #require(entity.component()) #expect(component.connectedUnnamed.isEmpty == true) #expect(component.missingUnnamed == 1) - #expect(world.objectHasError(delay.objectID, error: ModelError.missingRequiredParameter)) + #expect(entity.hasError(ModelError.missingRequiredParameter)) } @Test func delayWithOneParameter() throws { @@ -236,11 +246,12 @@ import Testing let system = ParameterResolutionSystem(world) try system.update(world) - let component: ResolvedParametersComponent = try #require(world.component(for: delay.objectID)) + let entity = try #require(world.entity(delay.objectID)) + let component: ResolvedParametersComponent = try #require(entity.component()) #expect(component.connectedUnnamed == [source.objectID]) #expect(component.missingUnnamed == 0) #expect(component.unused.isEmpty == true) - #expect(!world.objectHasIssues(delay.objectID)) + #expect(!entity.hasIssues) } @Test func delayWithTwoParameters() throws { @@ -259,9 +270,10 @@ import Testing try system.update(world) try system.update(world) - let component: ResolvedParametersComponent = try #require(world.component(for: delay.objectID)) + let entity = try #require(world.entity(delay.objectID)) + let component: ResolvedParametersComponent = try #require(entity.component()) #expect(component.unused.count == 2) - #expect(world.objectHasError(delay.objectID, error: ModelError.tooManyParameters)) + #expect(entity.hasError(ModelError.tooManyParameters)) } // MARK: - Other auxiliaries @@ -284,14 +296,19 @@ import Testing let system = ParameterResolutionSystem(world) try system.update(world) - 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)) + let gfEnt = try #require(world.entity(gf.objectID)) + let gfComp: ResolvedParametersComponent = try #require(gfEnt.component()) #expect(gfComp.connectedUnnamed == [source.objectID]) + #expect(!gfEnt.hasIssues) + + let delayEnt = try #require(world.entity(delay.objectID)) + let delayComp: ResolvedParametersComponent = try #require(delayEnt.component()) #expect(delayComp.connectedUnnamed == [source.objectID]) + #expect(!delayEnt.hasIssues) + + let smoothEnt = try #require(world.entity(smooth.objectID)) + let smoothComp: ResolvedParametersComponent = try #require(smoothEnt.component()) #expect(smoothComp.connectedUnnamed == [source.objectID]) - #expect(!world.objectHasIssues(gf.objectID)) - #expect(!world.objectHasIssues(delay.objectID)) - #expect(!world.objectHasIssues(smooth.objectID)) + #expect(!smoothEnt.hasIssues) } } diff --git a/Tests/PoieticFlowsTests/Systems/StockFlowSystemTests.swift b/Tests/PoieticFlowsTests/Systems/StockFlowSystemTests.swift index 0621ebe..7f4d922 100644 --- a/Tests/PoieticFlowsTests/Systems/StockFlowSystemTests.swift +++ b/Tests/PoieticFlowsTests/Systems/StockFlowSystemTests.swift @@ -32,7 +32,7 @@ import Testing let system = FlowCollectorSystem(world) try system.update(world) - let component: FlowRateComponent = try #require(world.component(for: flowRate.objectID)) + let component: FlowRateComponent = try #require(world.entity(flowRate.objectID)?.component()) #expect(component.drainsStock == nil) #expect(component.fillsStock == nil) #expect(component.priority == 0) @@ -49,7 +49,7 @@ import Testing let system = FlowCollectorSystem(world) try system.update(world) - let component: FlowRateComponent = try #require(world.component(for: flowRate.objectID)) + let component: FlowRateComponent = try #require(world.entity(flowRate.objectID)?.component()) #expect(component.drainsStock == stock.objectID) #expect(component.fillsStock == nil) #expect(component.priority == 0) @@ -64,7 +64,7 @@ import Testing let system = FlowCollectorSystem(world) try system.update(world) - let component: FlowRateComponent = try #require(world.component(for: flowRate.objectID)) + let component: FlowRateComponent = try #require(world.entity(flowRate.objectID)?.component()) #expect(component.drainsStock == nil) #expect(component.fillsStock == stock.objectID) #expect(component.priority == 0) @@ -82,7 +82,7 @@ import Testing let system = FlowCollectorSystem(world) try system.update(world) - let component: FlowRateComponent = try #require(world.component(for: flowRate.objectID)) + let component: FlowRateComponent = try #require(world.entity(flowRate.objectID)?.component()) #expect(component.drainsStock == source.objectID) #expect(component.fillsStock == target.objectID) #expect(component.priority == 0) @@ -100,7 +100,7 @@ import Testing let system = FlowCollectorSystem(world) try system.update(world) - let component: FlowRateComponent = try #require(world.component(for: flowRate.objectID)) + let component: FlowRateComponent = try #require(world.entity(flowRate.objectID)?.component()) #expect(component.drainsStock == source.objectID) #expect(component.fillsStock == target.objectID) #expect(component.priority == 0) @@ -115,7 +115,7 @@ import Testing let system = FlowCollectorSystem(world) try system.update(world) - let component: FlowRateComponent = try #require(world.component(for: flowRate.objectID)) + let component: FlowRateComponent = try #require(world.entity(flowRate.objectID)?.component()) #expect(component.priority == 5) } } @@ -147,7 +147,7 @@ import Testing let system = StockDependencySystem(world) try system.update(world) - let component: StockComponent = try #require(world.component(for: stock.objectID)) + let component: StockComponent = try #require(world.entity(stock.objectID)?.component()) #expect(component.inflowRates.isEmpty) #expect(component.outflowRates.isEmpty) #expect(component.inflowStocks.isEmpty) @@ -167,7 +167,7 @@ import Testing let system = StockDependencySystem(world) try system.update(world) - let component: StockComponent = try #require(world.component(for: stock.objectID)) + let component: StockComponent = try #require(world.entity(stock.objectID)?.component()) #expect(component.inflowRates == [flowRate.objectID]) #expect(component.outflowRates.isEmpty) #expect(component.inflowStocks.isEmpty) @@ -184,7 +184,7 @@ import Testing let system = StockDependencySystem(world) try system.update(world) - let component: StockComponent = try #require(world.component(for: stock.objectID)) + let component: StockComponent = try #require(world.entity(stock.objectID)?.component()) #expect(component.inflowRates.isEmpty) #expect(component.outflowRates == [flowRate.objectID]) #expect(component.inflowStocks.isEmpty) @@ -204,7 +204,7 @@ import Testing let system = StockDependencySystem(world) try system.update(world) - let component: StockComponent = try #require(world.component(for: stock.objectID)) + let component: StockComponent = try #require(world.entity(stock.objectID)?.component()) #expect(component.inflowRates == [inflow.objectID]) #expect(component.outflowRates == [outflow.objectID]) #expect(component.inflowStocks.isEmpty) @@ -230,7 +230,7 @@ import Testing let system = StockDependencySystem(world) try system.update(world) - let component: StockComponent = try #require(world.component(for: stock.objectID)) + let component: StockComponent = try #require(world.entity(stock.objectID)?.component()) #expect(component.inflowRates.count == 2) #expect(component.inflowRates.contains(inflow1.objectID)) #expect(component.inflowRates.contains(inflow2.objectID)) @@ -254,12 +254,12 @@ import Testing let system = StockDependencySystem(world) try system.update(world) - let compA: StockComponent = try #require(world.component(for: stockA.objectID)) + let compA: StockComponent = try #require(world.entity(stockA.objectID)?.component()) #expect(compA.outflowRates == [flowRate.objectID]) #expect(compA.outflowStocks == [stockB.objectID]) #expect(compA.inflowStocks.isEmpty) - let compB: StockComponent = try #require(world.component(for: stockB.objectID)) + let compB: StockComponent = try #require(world.entity(stockB.objectID)?.component()) #expect(compB.inflowRates == [flowRate.objectID]) #expect(compB.inflowStocks == [stockA.objectID]) #expect(compB.outflowStocks.isEmpty) @@ -286,15 +286,15 @@ import Testing let system = StockDependencySystem(world) try system.update(world) - let compA: StockComponent = try #require(world.component(for: stockA.objectID)) + let compA: StockComponent = try #require(world.entity(stockA.objectID)?.component()) #expect(compA.inflowStocks.isEmpty) #expect(compA.outflowStocks == [stockB.objectID]) - let compB: StockComponent = try #require(world.component(for: stockB.objectID)) + let compB: StockComponent = try #require(world.entity(stockB.objectID)?.component()) #expect(compB.inflowStocks == [stockA.objectID]) #expect(compB.outflowStocks == [stockC.objectID]) - let compC: StockComponent = try #require(world.component(for: stockC.objectID)) + let compC: StockComponent = try #require(world.entity(stockC.objectID)?.component()) #expect(compC.inflowStocks == [stockB.objectID]) #expect(compC.outflowStocks.isEmpty) } @@ -315,13 +315,13 @@ import Testing let system = StockDependencySystem(world) try system.update(world) - let compA: StockComponent = try #require(world.component(for: stockA.objectID)) + let compA: StockComponent = try #require(world.entity(stockA.objectID)?.component()) #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(world.component(for: stockB.objectID)) + let compB: StockComponent = try #require(world.entity(stockB.objectID)?.component()) #expect(compB.inflowRates == [rateAB.objectID]) #expect(compB.inflowStocks == [stockA.objectID]) #expect(compB.outflowRates == [rateBA.objectID]) @@ -339,9 +339,9 @@ import Testing let system = StockDependencySystem(world) try system.update(world) - let component1: StockComponent = try #require(world.component(for: stockNeg.objectID)) + let component1: StockComponent = try #require(world.entity(stockNeg.objectID)?.component()) #expect(component1.allowsNegative == true) - let component2: StockComponent = try #require(world.component(for: stockNotNeg.objectID)) + let component2: StockComponent = try #require(world.entity(stockNotNeg.objectID)?.component()) #expect(component2.allowsNegative == false) } } diff --git a/Tests/PoieticFlowsTests/World+extensions.swift b/Tests/PoieticFlowsTests/World+extensions.swift index 4d411ab..cdcc10e 100644 --- a/Tests/PoieticFlowsTests/World+extensions.swift +++ b/Tests/PoieticFlowsTests/World+extensions.swift @@ -8,15 +8,15 @@ @testable import PoieticCore // Testing convenience methods -extension World { - func objectHasIssue(_ objectID: ObjectID, identifier: String) -> Bool { - guard let issues = objectIssues(objectID) else { return false } +extension RuntimeEntity { + func hasIssue(identifier: String) -> Bool { + guard let issues = self.issues else { return false } return issues.contains { $0.identifier == identifier } } - func objectHasError(_ objectID: ObjectID, error: T) -> Bool { - guard let issues = objectIssues(objectID) else { return false } + func hasError(_ error: T) -> Bool { + guard let issues = self.issues else { return false } for issue in issues { if let objectError = issue.error as? T, objectError == error { From ff9651fa64343f57a89c555395a04fabaddeebaa Mon Sep 17 00:00:00 2001 From: Stefan Urbanek Date: Sun, 1 Mar 2026 21:01:42 +0100 Subject: [PATCH 2/3] Reflect World and Component changes in core --- Sources/PoieticFlows/Components/Chart.swift | 18 ----------------- .../Simulation/RegularTimeSeries.swift | 20 ------------------- .../PoieticFlows/Simulation/Scenario.swift | 15 -------------- 3 files changed, 53 deletions(-) diff --git a/Sources/PoieticFlows/Components/Chart.swift b/Sources/PoieticFlows/Components/Chart.swift index 6a3e91c..a45d458 100644 --- a/Sources/PoieticFlows/Components/Chart.swift +++ b/Sources/PoieticFlows/Components/Chart.swift @@ -62,21 +62,3 @@ 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/Simulation/RegularTimeSeries.swift b/Sources/PoieticFlows/Simulation/RegularTimeSeries.swift index 0d52703..20710e3 100644 --- a/Sources/PoieticFlows/Simulation/RegularTimeSeries.swift +++ b/Sources/PoieticFlows/Simulation/RegularTimeSeries.swift @@ -71,23 +71,3 @@ public class RegularTimeSeries: Component /*: Sequence, RandomAccessCollection? return result } } - -extension RegularTimeSeries: InspectableComponent { - public static let attributeKeys: [String] = [ - "time_start", "time_end", "time_delta", "data_min", "data_max", - "values", "points", "count" - ] - public func attribute(forKey key: String) -> Variant? { - switch key { - case "time_start": Variant(self.startTime) - case "time_end": Variant(self.endTime) - case "time_delta": Variant(self.timeDelta) - case "data_min": Variant(self.dataMin) - case "data_max": Variant(self.dataMax) - case "values": Variant(self.data) - case "points": Variant(self.points()) - case "count": Variant(self.data.count) - default: nil - } - } -} diff --git a/Sources/PoieticFlows/Simulation/Scenario.swift b/Sources/PoieticFlows/Simulation/Scenario.swift index 93fe0b3..4dd9b3e 100644 --- a/Sources/PoieticFlows/Simulation/Scenario.swift +++ b/Sources/PoieticFlows/Simulation/Scenario.swift @@ -102,21 +102,6 @@ public struct SimulationSettings: Component { } } -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``. From 7a8fa64430b1f199451c55dbe74695137361f396 Mon Sep 17 00:00:00 2001 From: Stefan Urbanek Date: Tue, 3 Mar 2026 09:07:41 +0100 Subject: [PATCH 3/3] Make SimulationPlan.settings non-optional --- Sources/PoieticFlows/Components/SimulationPlan.swift | 6 +++--- Sources/PoieticFlows/Simulation/SimulationResult.swift | 3 ++- .../PoieticFlows/Systems/SimulationPlanningSystem.swift | 8 ++++---- .../PoieticFlows/Systems/StockFlowSimulationSystem.swift | 2 +- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/Sources/PoieticFlows/Components/SimulationPlan.swift b/Sources/PoieticFlows/Components/SimulationPlan.swift index 0f7fb2b..7b48553 100644 --- a/Sources/PoieticFlows/Components/SimulationPlan.swift +++ b/Sources/PoieticFlows/Components/SimulationPlan.swift @@ -35,7 +35,7 @@ public struct SimulationPlan { flows: [BoundFlow] = [], // charts: [Chart] = [], valueBindings: [CompiledControlBinding] = [], - simulationParameters: SimulationSettings? = nil) { + settings: SimulationSettings) { self.simulationObjects = simulationObjects self.stateVariables = stateVariables self.builtins = builtins @@ -43,7 +43,7 @@ public struct SimulationPlan { self.flows = flows // self.charts = charts self.valueBindings = valueBindings - self.simulationSettings = simulationParameters + self.simulationSettings = settings } /// List of objects that are considered in the computation computed, ordered by computational @@ -106,7 +106,7 @@ public struct SimulationPlan { /// /// See ``SimulationSettings`` for more information. /// - public let simulationSettings: SimulationSettings? + public let simulationSettings: SimulationSettings /// Get index into a list of computed variables for an object with given ID. /// diff --git a/Sources/PoieticFlows/Simulation/SimulationResult.swift b/Sources/PoieticFlows/Simulation/SimulationResult.swift index 91d1290..1f6208b 100644 --- a/Sources/PoieticFlows/Simulation/SimulationResult.swift +++ b/Sources/PoieticFlows/Simulation/SimulationResult.swift @@ -39,6 +39,7 @@ public struct SimulationResult: Component { self.states.append(state) } +#if false // This was used for Godot backend /// Get numeric time series for a state variable at given index. /// /// - Precondition: Variable at given index in all states is convertible to a float. @@ -60,5 +61,5 @@ public struct SimulationResult: Component { // func numericTimeSeries(index:Int, convert: (Int, Variant) -> Double) -> RegularTimeSeries // func numericTimeSeries(index:Int, notConvertibleDefault: Double) -> RegularTimeSeries - +#endif // false } diff --git a/Sources/PoieticFlows/Systems/SimulationPlanningSystem.swift b/Sources/PoieticFlows/Systems/SimulationPlanningSystem.swift index 91ce94a..abf7672 100644 --- a/Sources/PoieticFlows/Systems/SimulationPlanningSystem.swift +++ b/Sources/PoieticFlows/Systems/SimulationPlanningSystem.swift @@ -195,12 +195,12 @@ public struct SimulationPlanningSystem: System { let boundStocks = try bindStocks(stocks, flowIndices: flowIndices, world: world) // Simulation parameters - let params: SimulationSettings + let settings: SimulationSettings if let simInfo = frame.first(trait: Trait.Simulation) { - params = SimulationSettings(fromObject: simInfo) + settings = SimulationSettings(fromObject: simInfo) } else { - params = SimulationSettings() + settings = SimulationSettings() } @@ -211,7 +211,7 @@ public struct SimulationPlanningSystem: System { stocks: boundStocks, flows: boundFlows, valueBindings: [], // FIXME: Relic from the past, remove - simulationParameters: params + settings: settings ) world.setSingleton(plan) diff --git a/Sources/PoieticFlows/Systems/StockFlowSimulationSystem.swift b/Sources/PoieticFlows/Systems/StockFlowSimulationSystem.swift index 8b2d453..95e6966 100644 --- a/Sources/PoieticFlows/Systems/StockFlowSimulationSystem.swift +++ b/Sources/PoieticFlows/Systems/StockFlowSimulationSystem.swift @@ -37,7 +37,7 @@ public struct StockFlowSimulationSystem: System { world.removeSingleton(SimulationResult.self) guard let plan: SimulationPlan = world.singleton() else { return } - let settings: SimulationSettings = world.singleton() ?? SimulationSettings() + let settings: SimulationSettings = world.singleton() ?? plan.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)")