From 1be29831d658ed492a8be268339c02f16f66ced7 Mon Sep 17 00:00:00 2001 From: Johannes Terblanche Date: Sat, 21 Mar 2026 18:54:46 +0200 Subject: [PATCH 1/6] delete return 0 for missing entries and merge functions --- src/DataBlobs/services/BlobEntry.jl | 3 +- src/DataBlobs/services/BlobStores.jl | 17 +-- src/Deprecated.jl | 201 +++++++++++++++++++++++++++ src/DistributedFactorGraphs.jl | 2 +- src/GraphsDFG/services/GraphsDFG.jl | 50 +++---- src/entities/Agent_and_Graph.jl | 35 +++++ src/entities/Bloblet.jl | 5 +- src/entities/DFGFactor.jl | 74 ++++++++++ src/entities/DFGVariable.jl | 34 ++++- src/errors.jl | 14 ++ src/services/AbstractDFG.jl | 191 +------------------------ src/services/DFGVariable.jl | 4 +- test/iifInterfaceTests.jl | 7 +- test/testBlocks.jl | 22 +-- 14 files changed, 409 insertions(+), 250 deletions(-) diff --git a/src/DataBlobs/services/BlobEntry.jl b/src/DataBlobs/services/BlobEntry.jl index fc4d7592..e116bdef 100644 --- a/src/DataBlobs/services/BlobEntry.jl +++ b/src/DataBlobs/services/BlobEntry.jl @@ -46,6 +46,7 @@ function mergeBlobentry!(node, entry::Blobentry) end function mergeBlobentries!(node, entries::Vector{Blobentry}) + #TODO optimize with something like: merge!(refBlobentries(node), entries) mergeBlobentry!.(node, entries) return length(entries) end @@ -54,7 +55,7 @@ end $(SIGNATURES) """ function deleteBlobentry!(node, label::Symbol) - !haskey(refBlobentries(node), label) && throw(LabelNotFoundError("Blobentry", label)) + !haskey(refBlobentries(node), label) && return 0 pop!(refBlobentries(node), label) return 1 end diff --git a/src/DataBlobs/services/BlobStores.jl b/src/DataBlobs/services/BlobStores.jl index 6f506d6e..c7b2ce36 100644 --- a/src/DataBlobs/services/BlobStores.jl +++ b/src/DataBlobs/services/BlobStores.jl @@ -172,15 +172,12 @@ function deleteBlob!(store::FolderStore{T}, blobid::UUID) where {T} rm(blobfilename) # Create a tombstone marker open(tombstonefile, "w") do f - return write(f, "deleted") + return write(f, string("DELETED: ", now(UTC))) end return 1 - elseif isfile(tombstonefile) - # Already deleted - return 0 else - # Not found - throw(IdNotFoundError("Blob", blobid)) + # Already deleted or doesn't exist + return 0 end end @@ -235,9 +232,7 @@ function addBlob!(store::InMemoryBlobstore{T}, blobid::UUID, data::T) where {T} end function deleteBlob!(store::InMemoryBlobstore, blobid::UUID) - if !haskey(store.blobs, blobid) - throw(IdNotFoundError("Blob", blobid)) - end + !haskey(store.blobs, blobid) && return 0 pop!(store.blobs, blobid) return 1 end @@ -372,9 +367,7 @@ function addBlob!(store::RowBlobstore{T}, blobid::UUID, blob::T) where {T} end function deleteBlob!(store::RowBlobstore, blobid::UUID) - if !haskey(store.blobs, blobid) - throw(IdNotFoundError("Blob", blobid)) - end + !haskey(store.blobs, blobid) && return 0 pop!(store.blobs, blobid) return 1 end diff --git a/src/Deprecated.jl b/src/Deprecated.jl index f8344c21..0711dd14 100644 --- a/src/Deprecated.jl +++ b/src/Deprecated.jl @@ -4,6 +4,7 @@ export FactorCompute const FactorCompute = FactorDFG +#TODO maybe keep for Bloblet type assert later. const MetadataTypes = Union{ Int, Float64, @@ -424,3 +425,203 @@ function getCoordinates( X = ManifoldsBase.log(M, p0, p) return ManifoldsBase.get_coordinates(M, p0, X, basis) end + +# old merge and copy functions, will be replaced by cleaner merge!, sync!, and copyto! functions + +# """ +# $(SIGNATURES) +# Merger sourceDFG to destDFG given an optional list of variables and factors and distance. +# Notes: +# - Nodes already in the destination graph are updated from sourceDFG. +# - Orphaned factors (where the subgraph does not contain all the related variables) are not included. +# Related: +# - [`copyGraph!`](@ref) +# - [`buildSubgraph`](@ref) +# - [`listNeighborhood`](@ref) +# - [`deepcopyGraph`](@ref) +# """ + +##============================================================================== +## Copy Functions #TODO replace with sync +##============================================================================== + +""" + $(SIGNATURES) +Common function for copying nodes from one graph into another graph. +This is overridden in specialized implementations for performance. +Orphaned factors are not added, with a warning if verbose. +Set `overwriteDest` to overwrite existing variables and factors in the destination DFG. +NOTE: `copyGraphMetadata` is deprecated – use agent/graph Bloblets instead. +Related: +- [`deepcopyGraph`](@ref) +- [`deepcopyGraph!`](@ref) +- [`buildSubgraph`](@ref) +- [`listNeighborhood`](@ref) +- [`mergeGraph!`](@ref) +""" +# +function copyGraph!( + destDFG::AbstractDFG, + sourceDFG::AbstractDFG, + variableLabels::AbstractVector{Symbol} = listVariables(sourceDFG), + factorLabels::AbstractVector{Symbol} = listFactors(sourceDFG); + copyGraphMetadata::Bool = false, + overwriteDest::Bool = false, + deepcopyNodes::Bool = false, + verbose::Bool = false, + showprogress::Bool = verbose, +) + # Split into variables and factors + sourceVariables = getVariables(sourceDFG, variableLabels) + sourceFactors = getFactors(sourceDFG, factorLabels) + # Now we have to add all variables first, + @showprogress desc = "copy variables" enabled = showprogress for variable in + sourceVariables + + variableCopy = deepcopyNodes ? deepcopy(variable) : variable + if !hasVariable(destDFG, variable.label) + addVariable!(destDFG, variableCopy) + elseif overwriteDest + mergeVariable!(destDFG, variableCopy) + else + throw(LabelExistsError("Variable", variable.label)) + end + end + # And then all factors to the destDFG. + @showprogress desc = "copy factors" enabled = showprogress for factor in sourceFactors + # Get the original factor variables (we need them to create it) + sourceFactorVariableIds = collect(factor.variableorder) + # Find the labels and associated variables in our new subgraph + factVariableIds = Symbol[] + for variable in sourceFactorVariableIds + if hasVariable(destDFG, variable) + push!(factVariableIds, variable) + end + end + # Only if we have all of them should we add it (otherwise strange things may happen on evaluation) + if length(factVariableIds) == length(sourceFactorVariableIds) + factorCopy = deepcopyNodes ? deepcopy(factor) : factor + if !hasFactor(destDFG, factor.label) + addFactor!(destDFG, factorCopy) + elseif overwriteDest + mergeFactor!(destDFG, factorCopy) + else + throw(LabelExistsError("Factor", factor.label)) + end + elseif verbose + @warn "Factor $(factor.label) will be an orphan in the destination graph, and therefore not added." + end + end + + if copyGraphMetadata + error( + "copyGraphMetadata keyword has been removed – metadata APIs were replaced by Bloblets. " * + "Copy agent/graph Bloblets manually before calling copyGraph!", + ) + end + return nothing +end + +""" + $(SIGNATURES) +Copy nodes from one graph into another graph by making deepcopies. +see [`copyGraph!`](@ref) for more detail. +Related: +- [`deepcopyGraph`](@ref) +- [`buildSubgraph`](@ref) +- [`listNeighborhood`](@ref) +- [`mergeGraph!`](@ref) +""" +# +function deepcopyGraph!( + destDFG::AbstractDFG, + sourceDFG::AbstractDFG, + variableLabels::Vector{Symbol} = ls(sourceDFG), + factorLabels::Vector{Symbol} = lsf(sourceDFG); + kwargs..., +) + return copyGraph!( + destDFG, + sourceDFG, + variableLabels, + factorLabels; + deepcopyNodes = true, + kwargs..., + ) +end + +""" + $(SIGNATURES) +Copy nodes from one graph into a new graph by making deepcopies. +see [`copyGraph!`](@ref) for more detail. +Related: +- [`deepcopyGraph!`](@ref) +- [`buildSubgraph`](@ref) +- [`listNeighborhood`](@ref) +- [`mergeGraph!`](@ref) +""" +# +function deepcopyGraph( + ::Type{T}, + sourceDFG::AbstractDFG, + variableLabels::Vector{Symbol} = ls(sourceDFG), + factorLabels::Vector{Symbol} = lsf(sourceDFG); + graphLabel::Symbol = Symbol(getGraphLabel(sourceDFG), "_cp_$(string(uuid4())[1:6])"), + kwargs..., +) where {T <: AbstractDFG} + destDFG = T(; + solverParams = getSolverParams(sourceDFG), + graph = sourceDFG.graph, + agent = sourceDFG.agent, + graphLabel, + ) + copyGraph!( + destDFG, + sourceDFG, + variableLabels, + factorLabels; + deepcopyNodes = true, + kwargs..., + ) + return destDFG +end + +function mergeGraph!( + destDFG::AbstractDFG, + sourceDFG::AbstractDFG, + variableLabels::Vector{Symbol}, + factorLabels::Vector{Symbol} = lsf(sourceDFG), + distance::Int = 0; + solvableFilter = nothing, + tagsFilter = nothing, + kwargs..., +) + Base.depwarn( + """ + mergeGraph! with variableLabels, factorLabels, and distance is deprecated. + For now use function composition ending with mergeGraph!, + syncGraph!(Coming soon) will replace this functionality. + """, + :mergeGraph!, + ) + # find neighbors at distance to add + sourceVariables, sourceFactors = listNeighborhood( + sourceDFG, + union(variableLabels, factorLabels), + distance; + solvableFilter, + tagsFilter, + ) + + copyGraph!( + destDFG, + sourceDFG, + sourceVariables, + sourceFactors; + deepcopyNodes = true, + overwriteDest = true, + kwargs..., + ) + + return destDFG +end diff --git a/src/DistributedFactorGraphs.jl b/src/DistributedFactorGraphs.jl index 162bcbca..76afc6b4 100644 --- a/src/DistributedFactorGraphs.jl +++ b/src/DistributedFactorGraphs.jl @@ -509,7 +509,7 @@ const unstable_functions::Vector{Symbol} = [ :unpackDistribution, :hasTagsNeighbors, # :updateBlobstore!,## TODO deprecated or obsolete - :emptyMetadata!, #TODO maybe deprecate for just deleteMetadata! + # :emptyMetadata!, #TODO maybe deprecate for just deleteMetadata! # :emptyBlobstore!, #TODO maybe deprecate for just deleteBlobstore! :MetadataTypes, #maybe make public after metadata stable :getVariableTypeName, diff --git a/src/GraphsDFG/services/GraphsDFG.jl b/src/GraphsDFG/services/GraphsDFG.jl index a0e22fa9..5e38aada 100644 --- a/src/GraphsDFG/services/GraphsDFG.jl +++ b/src/GraphsDFG/services/GraphsDFG.jl @@ -86,8 +86,17 @@ function mergeVariable!(dfg::GraphsDFG, variable::AbstractGraphVariable) if !haskey(dfg.g.variables, variable.label) addVariable!(dfg, variable) else - dfg.g.variables[variable.label] = variable - end + patch!(dfg.g.variables[variable.label], variable) + end + # metrics = (; + # tags = length(variable.tags), + # states = length(variable.states), + # bloblets = length(variable.bloblets), + # blobentries = length(variable.blobentries), + # ) + #TODO return metrics or 1 to keep it simple? + # if 1, the merge result does not include the children. + # if metrics, the merge result includes the children counts return 1 end @@ -95,40 +104,25 @@ function mergeFactor!(dfg::GraphsDFG, factor::AbstractGraphFactor) label = getLabel(factor) if !haskey(dfg.g.factors, label) addFactor!(dfg, factor) - elseif DFG.getVariableOrder(dfg, label) != DFG.getVariableOrder(factor) - throw( - DomainError( - factor.variableorder, - "Cannot merge factor with label $(label): factor neighbors differ. " * - "Existing neighbors: $(DFG.getVariableOrder(dfg, label)), " * - "new neighbors: $(DFG.getVariableOrder(factor)), " * - "To mutate factor neighbors, delete and re-add the factor.", - ), - ) else - dfg.g.factors[label] = factor + patch!(dfg.g.factors[label], factor) end - + #TODO also same metrics consideration as mergeVariable! return 1 end function deleteVariable!(dfg::GraphsDFG, label::Symbol)#::Tuple{AbstractGraphVariable, Vector{<:AbstractGraphFactor}} - if !haskey(dfg.g.variables, label) - throw(LabelNotFoundError("Variable", label)) - end + !haskey(dfg.g.variables, label) && return 0 + + # orphaned factors are not supported. + del_facs = map(l -> deleteFactor!(dfg, l), listNeighbors(dfg, label)) - deleteNeighbors = true # reserved, orphaned factors are not supported at this time - if deleteNeighbors - del_facs = map(l -> deleteFactor!(dfg, l), listNeighbors(dfg, label)) - end rem_vertex!(dfg.g, dfg.g.labels[label]) return sum(del_facs) + 1 end function deleteFactor!(dfg::GraphsDFG, label::Symbol) - if !haskey(dfg.g.factors, label) - throw(LabelNotFoundError("Factor", label)) - end + !haskey(dfg.g.factors, label) && return 0 rem_vertex!(dfg.g, dfg.g.labels[label]) return 1 end @@ -591,17 +585,13 @@ function DFG.deleteFactorBlobentry!(dfg::GraphsDFG, label::Symbol, entryLabel::S end function DFG.deleteGraphBlobentry!(dfg::GraphsDFG, label::Symbol) - if !haskey(dfg.graph.blobentries, label) - throw(LabelNotFoundError("Blobentry", label)) - end + !haskey(dfg.graph.blobentries, label) && return 0 delete!(dfg.graph.blobentries, label) return 1 end function DFG.deleteAgentBlobentry!(dfg::GraphsDFG, label::Symbol) - if !haskey(dfg.agent.blobentries, label) - throw(LabelNotFoundError("Blobentry", label)) - end + !haskey(dfg.agent.blobentries, label) && return 0 delete!(dfg.agent.blobentries, label) return 1 end diff --git a/src/entities/Agent_and_Graph.jl b/src/entities/Agent_and_Graph.jl index d2f19ad6..e09ff59c 100644 --- a/src/entities/Agent_and_Graph.jl +++ b/src/entities/Agent_and_Graph.jl @@ -14,3 +14,38 @@ end bloblets::Bloblets = Bloblets() blobentries::Blobentries = Blobentries() end + +# Patching (for merge!) +# standard patch description merging +function patch(dest::String, src::String) + # 1. Quick exits + (src == "" || src == dest) && return dest + dest == "" && return src + dest === src && return dest + + separator = " | " + # 2. Combine: Split into parts and combine uniquely for idempotent merging. + # This prevents "Graph" from being swallowed by "FactorGraph" + dest_parts = split(dest, separator) + src_parts = split(src, separator) + return join(unique(vcat(dest_parts, src_parts)), separator) +end + +function patch!(dest::T, src::T) where {T <: Union{Agent, Graphroot}} + dest === src && return dest # avoid unnecessary work if same object + + dest.label !== src.label && + throw(ArgumentError("Nodes has different labels: $(dest.label) vs $(src.label)")) + + dest.description = patch(dest.description, src.description) + + union!(dest.tags, src.tags) + merge!(dest.blobentries, src.blobentries) + merge!(dest.bloblets, src.bloblets) + + return dest +end + +#TODO should we make agent immutable and only allow adding? complex ACID? +# reason for is once an agent's config is used in a graph it should not be modified. +mergeAgent!(dest::Agent, src::Agent) = patch!(dest, src) diff --git a/src/entities/Bloblet.jl b/src/entities/Bloblet.jl index 854060a0..e9e06838 100644 --- a/src/entities/Bloblet.jl +++ b/src/entities/Bloblet.jl @@ -89,14 +89,13 @@ end $(SIGNATURES) """ function deleteBloblet!(node, label::Symbol) - !haskey(refBloblets(node), label) && throw(LabelNotFoundError("Bloblet", label)) + !haskey(refBloblets(node), label) && return 0 pop!(refBloblets(node), label) return 1 end function deleteBloblets!(node, labels::Vector{Symbol}) - deleteBloblet!.(node, labels) - return length(labels) + return sum(deleteBloblet!.(node, labels)) end """ diff --git a/src/entities/DFGFactor.jl b/src/entities/DFGFactor.jl index ce523429..6d7dfa41 100644 --- a/src/entities/DFGFactor.jl +++ b/src/entities/DFGFactor.jl @@ -205,6 +205,80 @@ function FactorDFG( ) end +#TODO do we change Recipehyper to be immutable and replace the entire object? +#TODO Should we call this copyto! or patch!, going with patch! to not confuse possible expected behaviour of copyto!. +function patch!(dest::Recipehyper, src::Recipehyper) + resize!(dest.multihypo, length(src.multihypo)) + copyto!(dest.multihypo, src.multihypo) + dest.nullhypo = src.nullhypo + dest.inflation = src.inflation + return dest +end + +function patch!(dest::Recipestate, src::Recipestate) + dest.eliminated = src.eliminated + dest.potentialused = src.potentialused + return dest +end + +# we can only use this fallback once all patch! methods are defined +# function patch!(dest::D, src::S) where {D <: AbstractGraphNode, S <: AbstractGraphNode} +function patch!(dest::FactorDFG, src::FactorDFG) + throw( + ArgumentError( + "Type mismatch in patch!: Cannot patch a $(typeof(src)) into a $(typeof(dest)). ", + ), + ) +end + +""" + $SIGNATURES + +Merge the contents of `src` into `dest` by only patching child collections/containers. +Notes: +- Cascades into collections (`tags`, `blobentries`, `bloblets`). +- Assumes `label`, `timestamp`, and `statekind` are immutable and does not update them. +""" +function patch!(dest::FactorDFG{T, N}, src::FactorDFG{T, N}) where {T, N} + dest === src && return dest # avoid unnecessary work if same object + + dest.label !== src.label && throw( + MergeConflictError("Conflicting Factor labels: $(dest.label) vs $(src.label)"), + ) + dest.timestamp !== src.timestamp && throw( + MergeConflictError( + "Factor $(dest.label) has conflicting timestamps: $(dest.timestamp) vs $(src.timestamp)", + ), + ) + + if DFG.getVariableOrder(dfg, label) != DFG.getVariableOrder(factor) + throw( + MergeConflictError( + """ + Cannot merge factor with label $(label): factor neighbors differ. + Existing neighbors: $(DFG.getVariableOrder(dfg, label)), + new neighbors: $(DFG.getVariableOrder(factor)). + """, + ), + ) + end + + dest.observation != src.observation && + throw(MergeConflictError("Conflict in observations for factor $(label).")) + + union!(dest.tags, src.tags) + merge!(dest.blobentries, src.blobentries) + merge!(dest.bloblets, src.bloblets) + dest.solvable[] = src.solvable[] + + patch!(dest.hyper, src.hyper) + patch!(dest.state, src.state) + + #TODO Confirm solvercache merge policy, overwriting seems logical. + dest.solvercache[] = src.solvercache[] + + return dest +end ##------------------------------------------------------------------------------ ## FactorSummary lv1 ##------------------------------------------------------------------------------ diff --git a/src/entities/DFGVariable.jl b/src/entities/DFGVariable.jl index 8185130f..b4d4a034 100644 --- a/src/entities/DFGVariable.jl +++ b/src/entities/DFGVariable.jl @@ -252,6 +252,7 @@ variable_timestamp_note = """ your state type rather than relying on this metadata field. """ +#TODO move solvable to State, and update filters """ $(TYPEDEF) Complete variable structure for a DistributedFactorGraph variable. @@ -335,7 +336,7 @@ The default VariableDFG constructor. #IIF like contruction helper for VariableDFG function VariableDFG( label::Symbol, - statekind::Union{T, Type{T}}; + ::Union{T, Type{T}}; # statekind tags::Union{Set{Symbol}, Vector{Symbol}} = Set{Symbol}(), timestamp::Union{TimeDateZone, ZonedDateTime} = now_tdz(), solvable::Union{Int, Base.RefValue{Int}} = Ref{Int}(1), @@ -391,6 +392,37 @@ end # end # end +""" + $SIGNATURES + +Merge the contents of `src` into `dest` by only patching child collections/containers. +Notes: +- Cascades into collections (`tags`, `states`, `blobentries`, `bloblets`). +- Assumes `label`, `timestamp`, and `statekind` are immutable and does not update them. +""" +function patch!(dest::VariableDFG{T}, src::VariableDFG{T}) where {T} + dest === src && return dest # avoid unnecessary work if same object + + dest.label !== src.label && throw( + ArgumentError("Variables has different labels: $(dest.label) vs $(src.label)"), + ) + dest.timestamp !== src.timestamp && throw( + ArgumentError( + "Variables has different timestamps: $(dest.timestamp) vs $(src.timestamp).", + ), + ) + # you will get a method error if statekind is different, so maybe we don't need to check that here. + + union!(dest.tags, src.tags) + merge!(dest.states, src.states) + merge!(dest.blobentries, src.blobentries) + merge!(dest.bloblets, src.bloblets) + + dest.solvable[] = src.solvable[] + + return dest +end + ##------------------------------------------------------------------------------ ## VariableSummary lv1 ##------------------------------------------------------------------------------ diff --git a/src/errors.jl b/src/errors.jl index a8993693..7bae3e6b 100644 --- a/src/errors.jl +++ b/src/errors.jl @@ -93,3 +93,17 @@ end function Base.showerror(io::IO, ex::SerializationError) return print(io, "SerializationError: ", ex.msg) end + +#http error 409 +""" + MergeConflictError(msg) + +Error thrown when a merge conflict occurs. +""" +struct MergeConflictError <: Exception + msg::String +end + +function Base.showerror(io::IO, ex::MergeConflictError) + return print(io, "MergeConflictError: ", ex.msg) +end diff --git a/src/services/AbstractDFG.jl b/src/services/AbstractDFG.jl index 42584404..8c212b4e 100644 --- a/src/services/AbstractDFG.jl +++ b/src/services/AbstractDFG.jl @@ -113,6 +113,7 @@ function mergeBlobstore!(dfg::AbstractDFG, store::AbstractBlobstore) return 1 end function deleteBlobstore!(dfg::AbstractDFG, key::Symbol) + !haskey(refBlobstores(dfg), key) && return 0 pop!(refBlobstores(dfg), key) return 1 end @@ -415,148 +416,6 @@ function exists(dfg::AbstractDFG, node::AbstractGraphNode) return exists(dfg, node.label) end -##============================================================================== -## Copy Functions #TODO replace with sync -##============================================================================== - -""" - $(SIGNATURES) -Common function for copying nodes from one graph into another graph. -This is overridden in specialized implementations for performance. -Orphaned factors are not added, with a warning if verbose. -Set `overwriteDest` to overwrite existing variables and factors in the destination DFG. -NOTE: `copyGraphMetadata` is deprecated – use agent/graph Bloblets instead. -Related: -- [`deepcopyGraph`](@ref) -- [`deepcopyGraph!`](@ref) -- [`buildSubgraph`](@ref) -- [`listNeighborhood`](@ref) -- [`mergeGraph!`](@ref) -""" -function copyGraph!( - destDFG::AbstractDFG, - sourceDFG::AbstractDFG, - variableLabels::AbstractVector{Symbol} = listVariables(sourceDFG), - factorLabels::AbstractVector{Symbol} = listFactors(sourceDFG); - copyGraphMetadata::Bool = false, - overwriteDest::Bool = false, - deepcopyNodes::Bool = false, - verbose::Bool = false, - showprogress::Bool = verbose, -) - # Split into variables and factors - sourceVariables = getVariables(sourceDFG, variableLabels) - sourceFactors = getFactors(sourceDFG, factorLabels) - # Now we have to add all variables first, - @showprogress desc = "copy variables" enabled = showprogress for variable in - sourceVariables - - variableCopy = deepcopyNodes ? deepcopy(variable) : variable - if !hasVariable(destDFG, variable.label) - addVariable!(destDFG, variableCopy) - elseif overwriteDest - mergeVariable!(destDFG, variableCopy) - else - throw(LabelExistsError("Variable", variable.label)) - end - end - # And then all factors to the destDFG. - @showprogress desc = "copy factors" enabled = showprogress for factor in sourceFactors - # Get the original factor variables (we need them to create it) - sourceFactorVariableIds = collect(factor.variableorder) - # Find the labels and associated variables in our new subgraph - factVariableIds = Symbol[] - for variable in sourceFactorVariableIds - if hasVariable(destDFG, variable) - push!(factVariableIds, variable) - end - end - # Only if we have all of them should we add it (otherwise strange things may happen on evaluation) - if length(factVariableIds) == length(sourceFactorVariableIds) - factorCopy = deepcopyNodes ? deepcopy(factor) : factor - if !hasFactor(destDFG, factor.label) - addFactor!(destDFG, factorCopy) - elseif overwriteDest - mergeFactor!(destDFG, factorCopy) - else - throw(LabelExistsError("Factor", factor.label)) - end - elseif verbose - @warn "Factor $(factor.label) will be an orphan in the destination graph, and therefore not added." - end - end - - if copyGraphMetadata - error( - "copyGraphMetadata keyword has been removed – metadata APIs were replaced by Bloblets. " * - "Copy agent/graph Bloblets manually before calling copyGraph!", - ) - end - return nothing -end - -""" - $(SIGNATURES) -Copy nodes from one graph into another graph by making deepcopies. -see [`copyGraph!`](@ref) for more detail. -Related: -- [`deepcopyGraph`](@ref) -- [`buildSubgraph`](@ref) -- [`listNeighborhood`](@ref) -- [`mergeGraph!`](@ref) -""" -function deepcopyGraph!( - destDFG::AbstractDFG, - sourceDFG::AbstractDFG, - variableLabels::Vector{Symbol} = ls(sourceDFG), - factorLabels::Vector{Symbol} = lsf(sourceDFG); - kwargs..., -) - return copyGraph!( - destDFG, - sourceDFG, - variableLabels, - factorLabels; - deepcopyNodes = true, - kwargs..., - ) -end - -""" - $(SIGNATURES) -Copy nodes from one graph into a new graph by making deepcopies. -see [`copyGraph!`](@ref) for more detail. -Related: -- [`deepcopyGraph!`](@ref) -- [`buildSubgraph`](@ref) -- [`listNeighborhood`](@ref) -- [`mergeGraph!`](@ref) -""" -function deepcopyGraph( - ::Type{T}, - sourceDFG::AbstractDFG, - variableLabels::Vector{Symbol} = ls(sourceDFG), - factorLabels::Vector{Symbol} = lsf(sourceDFG); - graphLabel::Symbol = Symbol(getGraphLabel(sourceDFG), "_cp_$(string(uuid4())[1:6])"), - kwargs..., -) where {T <: AbstractDFG} - destDFG = T(; - solverParams = getSolverParams(sourceDFG), - graph = sourceDFG.graph, - agent = sourceDFG.agent, - graphLabel, - ) - copyGraph!( - destDFG, - sourceDFG, - variableLabels, - factorLabels; - deepcopyNodes = true, - kwargs..., - ) - return destDFG -end - ##============================================================================== ## Subgraphs and Neighborhoods ##============================================================================== @@ -687,48 +546,12 @@ function buildSubgraph( return buildSubgraph(LocalDFG, dfg, variableFactorLabels, distance; kwargs...) end -""" - $(SIGNATURES) -Merger sourceDFG to destDFG given an optional list of variables and factors and distance. -Notes: -- Nodes already in the destination graph are updated from sourceDFG. -- Orphaned factors (where the subgraph does not contain all the related variables) are not included. -Related: -- [`copyGraph!`](@ref) -- [`buildSubgraph`](@ref) -- [`listNeighborhood`](@ref) -- [`deepcopyGraph`](@ref) -""" -function mergeGraph!( - destDFG::AbstractDFG, - sourceDFG::AbstractDFG, - variableLabels::Vector{Symbol} = ls(sourceDFG), - factorLabels::Vector{Symbol} = lsf(sourceDFG), - distance::Int = 0; - solvableFilter = nothing, - tagsFilter = nothing, - kwargs..., -) - - # find neighbors at distance to add - sourceVariables, sourceFactors = listNeighborhood( - sourceDFG, - union(variableLabels, factorLabels), - distance; - solvableFilter, - tagsFilter, - ) - - copyGraph!( - destDFG, - sourceDFG, - sourceVariables, - sourceFactors; - deepcopyNodes = true, - overwriteDest = true, - kwargs..., - ) - +function mergeGraph!(srcDFG::AbstractDFG, destDFG::AbstractDFG) + patch!(destDFG.graph, srcDFG.graph) + mergeAgent!(destDFG.agent, srcDFG.agent) + mergeBlobstores!(destDFG, srcDFG) + mergeVariables!(destDFG, getVariables(srcDFG)) + mergeFactors!(destDFG, getFactors(srcDFG)) return destDFG end diff --git a/src/services/DFGVariable.jl b/src/services/DFGVariable.jl index 564d501b..bd5536d0 100644 --- a/src/services/DFGVariable.jl +++ b/src/services/DFGVariable.jl @@ -457,9 +457,7 @@ function deleteState!(dfg::GraphsDFG, variableLabel::Symbol, label::Symbol) end function deleteState!(v::VariableDFG, label::Symbol) - if !haskey(v.states, label) - throw(LabelNotFoundError("State", label)) - end + !haskey(v.states, label) && return 0 delete!(v.states, label) return 1 end diff --git a/test/iifInterfaceTests.jl b/test/iifInterfaceTests.jl index e482b4e6..ae2aee74 100644 --- a/test/iifInterfaceTests.jl +++ b/test/iifInterfaceTests.jl @@ -75,12 +75,11 @@ end @test mergeFactor!(dfg2, f2) == 1 @test_throws LabelExistsError addFactor!(dfg2, f2) - dv3 = deleteVariable!(dfg2, v3) - @test dv3 == 2 - @test_throws LabelNotFoundError deleteVariable!(dfg2, v3) + @test deleteVariable!(dfg2, v3) == 2 + @test deleteVariable!(dfg2, v3) == 0 @test issetequal(ls(dfg2), [:a, :b]) - @test_throws LabelNotFoundError deleteFactor!(dfg2, f2) + @test deleteFactor!(dfg2, f2) == 0 @test lsf(dfg2) == [:abf1] end diff --git a/test/testBlocks.jl b/test/testBlocks.jl index 42121a51..2236a183 100644 --- a/test/testBlocks.jl +++ b/test/testBlocks.jl @@ -151,7 +151,7 @@ function GraphAgentBloblets!(fg::AbstractDFG) @test agent_blob.label in listAgentBloblets(fg) @test deleteAgentBloblet!(fg, agent_blob.label) == 1 @test_throws DFG.LabelNotFoundError getAgentBloblet(fg, agent_blob.label) - @test_throws DFG.LabelNotFoundError deleteAgentBloblet!(fg, agent_blob.label) + @test deleteAgentBloblet!(fg, agent_blob.label) == 0 # Graph-level bloblets graph_blob = Bloblet(:graph_blob, "running") @@ -163,7 +163,7 @@ function GraphAgentBloblets!(fg::AbstractDFG) @test graph_blob.label in listGraphBloblets(fg) @test deleteGraphBloblet!(fg, graph_blob.label) == 1 @test_throws DFG.LabelNotFoundError getGraphBloblet(fg, graph_blob.label) - @test_throws DFG.LabelNotFoundError deleteGraphBloblet!(fg, graph_blob.label) + @test deleteGraphBloblet!(fg, graph_blob.label) == 0 end # User, Robot, Session Data Blob Entries @@ -183,7 +183,7 @@ function GraphAgentBlobentries!(fg::AbstractDFG) @test de == 1 @test hasAgentBlobentry(fg, :key1) == false @test_throws DFG.LabelNotFoundError getAgentBlobentry(fg, :key1) - @test_throws DFG.LabelNotFoundError deleteAgentBlobentry!(fg, :key1) + @test deleteAgentBlobentry!(fg, :key1) == 0 @test addAgentBlobentries!(fg, [be]) == [be] @test deleteAgentBlobentries!(fg, [:key1]) == 1 @test mergeAgentBlobentries!(fg, [be]) == 1 @@ -202,7 +202,7 @@ function GraphAgentBlobentries!(fg::AbstractDFG) @test de == 1 @test hasGraphBlobentry(fg, :key1) == false @test_throws DFG.LabelNotFoundError getGraphBlobentry(fg, :key1) - @test_throws DFG.LabelNotFoundError deleteGraphBlobentry!(fg, :key1) + @test deleteGraphBlobentry!(fg, :key1) == 0 @test addGraphBlobentries!(fg, [be]) == [be] @test deleteGraphBlobentries!(fg, [:key1]) == 1 @test mergeGraphBlobentries!(fg, [be]) == 1 @@ -456,14 +456,14 @@ function VariablesandFactorsCRUD_SET!(fg, v1, v2, v3, f0, f1, f2) delfacCompare = getFactor(fg, :bcf1) ndel = deleteVariable!(fg, v3) @test ndel == 2 - @test_throws LabelNotFoundError deleteVariable!(fg, v3) + @test deleteVariable!(fg, v3) == 0 @test setdiff(ls(fg), [:a, :b]) == [] @test addVariable!(fg, v3) === v3 @test addFactor!(fg, f2) === f2 @test deleteFactor!(fg, f2) == 1 - @test_throws LabelNotFoundError deleteFactor!(fg, f2) + @test deleteFactor!(fg, f2) == 0 @test lsf(fg) == [:abf1] delvarCompare = getVariable(fg, :c) @@ -783,7 +783,7 @@ function DataEntriesTestBlock!(fg, v2) @test mergeVariableBlobentries!(fg, :a, [de1, de2]) == 2 @test deleteVariableBlobentries!(fg, :a, [:key1]) == 1 - @test_throws LabelNotFoundError deleteVariableBlobentries!(fg, :a, [:key1]) + @test deleteVariableBlobentries!(fg, :a, [:key1]) == 0 @test_throws LabelExistsError addVariableBlobentries!(fg, :a, [de2]) @test deleteVariableBlobentries!(fg, :a, [:key2]) == 1 @@ -797,7 +797,7 @@ function DataEntriesTestBlock!(fg, v2) @test mergeFactorBlobentry!(fg, :abf1, de2_update) == 1 @test listFactorBlobentries(fg, :abf1) == [getLabel(de1), getLabel(de2_update)] @test deleteFactorBlobentry!(fg, :abf1, getLabel(de2_update)) == 1 - @test_throws LabelNotFoundError deleteFactorBlobentry!(fg, :abf1, getLabel(de2_update)) + @test deleteFactorBlobentry!(fg, :abf1, getLabel(de2_update)) == 0 @test getFactorBlobentries(fg, :abf1) == [de1] @test getLabel.(addFactorBlobentries!(fg, :abf1, [de2])) == [getLabel(de2)] @test mergeFactorBlobentries!(fg, :abf1, [de1, de2_update]) == 2 @@ -812,7 +812,7 @@ function DataEntriesTestBlock!(fg, v2) @test mergeGraphBlobentry!(fg, de2_update) == 1 @test listGraphBlobentries(fg) == [getLabel(de1), getLabel(de2_update)] @test deleteGraphBlobentry!(fg, getLabel(de2_update)) == 1 - @test_throws LabelNotFoundError deleteGraphBlobentry!(fg, getLabel(de2_update)) + @test deleteGraphBlobentry!(fg, getLabel(de2_update)) == 0 @test getGraphBlobentries(fg) == [de1] @test addGraphBlobentries!(fg, [de2]) == [de2] @test mergeGraphBlobentries!(fg, [de1, de2_update]) == 2 @@ -827,7 +827,7 @@ function DataEntriesTestBlock!(fg, v2) @test mergeAgentBlobentry!(fg, de2_update) == 1 @test listAgentBlobentries(fg) == [getLabel(de1), getLabel(de2_update)] @test deleteAgentBlobentry!(fg, getLabel(de2_update)) == 1 - @test_throws LabelNotFoundError deleteAgentBlobentry!(fg, getLabel(de2_update)) + @test deleteAgentBlobentry!(fg, getLabel(de2_update)) @test getAgentBlobentries(fg) == [de1] @test addAgentBlobentries!(fg, [de2]) == [de2] @test mergeAgentBlobentries!(fg, [de1, de2_update]) == 2 @@ -937,7 +937,7 @@ function blobsStoresTestBlock!(fg) @test_throws DFG.IdExistsError addBlob!(fs, blobid, testData) @test getBlob(fs, blobid) == testData @test_throws DFG.IdNotFoundError getBlob(fs, uuid4()) - @test_throws DFG.IdNotFoundError deleteBlob!(fs, uuid4()) + @test deleteBlob!(fs, uuid4()) == 0 @test deleteBlob!(fs, blobid) == 1 @test_throws DFG.IdNotFoundError getBlob(fs, blobid) @test deleteBlob!(fs, blobid) == 0 From ea0b9080bd0bc72514b835d4a83ddc436d5505b5 Mon Sep 17 00:00:00 2001 From: Johannes Terblanche Date: Mon, 23 Mar 2026 11:49:04 +0200 Subject: [PATCH 2/6] Pin StructUtils, deprecate buildSubgraph, and better merge/delete --- Project.toml | 2 +- src/Deprecated.jl | 2 + src/DistributedFactorGraphs.jl | 12 +-- src/GraphsDFG/GraphsDFG.jl | 6 +- src/GraphsDFG/services/GraphsDFG.jl | 2 +- src/entities/Bloblet.jl | 10 +- src/entities/DFGFactor.jl | 78 +++++++++++---- src/entities/DFGVariable.jl | 34 ++++++- src/serialization/DFGStructStyles.jl | 1 + src/services/AbstractDFG.jl | 51 ++++++++-- src/services/Bloblet.jl | 22 ++++ src/services/DFGVariable.jl | 8 +- src/services/Tags.jl | 7 +- test/interfaceTests.jl | 15 +++ test/testBlocks.jl | 144 ++++++++++++++++++++++++++- 15 files changed, 345 insertions(+), 49 deletions(-) diff --git a/Project.toml b/Project.toml index 6397e746..6f3c24b5 100644 --- a/Project.toml +++ b/Project.toml @@ -65,7 +65,7 @@ SHA = "0.7, 1" SparseArrays = "1.11" StaticArrays = "1" Statistics = "1.11" -StructUtils = "2.6.0" +StructUtils = "=2.7.0" Tables = "1.11.1" Tar = "1.9" TensorCast = "0.3.3, 0.4" diff --git a/src/Deprecated.jl b/src/Deprecated.jl index 0711dd14..63d2e946 100644 --- a/src/Deprecated.jl +++ b/src/Deprecated.jl @@ -625,3 +625,5 @@ function mergeGraph!( return destDFG end + +@deprecate buildSubgraph(args...; kwargs...) getSubgraph(args...; kwargs...) diff --git a/src/DistributedFactorGraphs.jl b/src/DistributedFactorGraphs.jl index 76afc6b4..3863a1a9 100644 --- a/src/DistributedFactorGraphs.jl +++ b/src/DistributedFactorGraphs.jl @@ -181,7 +181,7 @@ export listFactors export listStates ## -export getObservation +public getObservation ##------------------------------------------------------------------------------ # Tags @@ -307,10 +307,10 @@ export listFactorBloblets export listGraphBloblets export listAgentBloblets -# export hasVariableBloblet -# export hasFactorBloblet -# export hasGraphBloblet -# export hasAgentBloblet +export hasVariableBloblet +export hasFactorBloblet +export hasGraphBloblet +export hasAgentBloblet ## v1 name, signiture, and return @@ -485,7 +485,7 @@ const unstable_functions::Vector{Symbol} = [ :natural_lt, #TODO do we export stable functions such as natural_lt or just mark as public :sortDFG, #TODO do we export stable functions such as natural_lt or just mark as public :mergeGraph!, - :buildSubgraph, + :getSubgraph, :incrDataLabelSuffix,# TODO somewhat used, do we deprecate? # set # TODO what to do here, maybe `ref` verb + setproperty. diff --git a/src/GraphsDFG/GraphsDFG.jl b/src/GraphsDFG/GraphsDFG.jl index 5d72a05f..5e2dd28f 100644 --- a/src/GraphsDFG/GraphsDFG.jl +++ b/src/GraphsDFG/GraphsDFG.jl @@ -11,6 +11,7 @@ using ...DistributedFactorGraphs: Agent, LabelNotFoundError, LabelExistsError, + MergeConflictError, Graphroot, AbstractGraphVariable, AbstractGraphFactor, @@ -25,7 +26,10 @@ using ...DistributedFactorGraphs: Blobentries, FolderStore, refTags, - listTags + listTags, + patch!, + getVariableOrder, + getObservation # import DFG functions to extend import ...DistributedFactorGraphs: diff --git a/src/GraphsDFG/services/GraphsDFG.jl b/src/GraphsDFG/services/GraphsDFG.jl index 5e38aada..9c20be2e 100644 --- a/src/GraphsDFG/services/GraphsDFG.jl +++ b/src/GraphsDFG/services/GraphsDFG.jl @@ -118,7 +118,7 @@ function deleteVariable!(dfg::GraphsDFG, label::Symbol)#::Tuple{AbstractGraphVar del_facs = map(l -> deleteFactor!(dfg, l), listNeighbors(dfg, label)) rem_vertex!(dfg.g, dfg.g.labels[label]) - return sum(del_facs) + 1 + return sum(del_facs; init = 0) + 1 end function deleteFactor!(dfg::GraphsDFG, label::Symbol) diff --git a/src/entities/Bloblet.jl b/src/entities/Bloblet.jl index e9e06838..66edcc7a 100644 --- a/src/entities/Bloblet.jl +++ b/src/entities/Bloblet.jl @@ -81,7 +81,7 @@ function mergeBloblet!(node, bloblet::Bloblet) end function mergeBloblets!(node, bloblets::Vector{Bloblet}) - mergeBloblet!.(node, bloblets) + foreach(bl -> mergeBloblet!(node, bl), bloblets) return length(bloblets) end @@ -95,7 +95,7 @@ function deleteBloblet!(node, label::Symbol) end function deleteBloblets!(node, labels::Vector{Symbol}) - return sum(deleteBloblet!.(node, labels)) + return sum(l -> deleteBloblet!(node, l), labels) end """ @@ -105,3 +105,9 @@ List all Bloblet keys for a variable `label` in `dfg` function listBloblets(node) return collect(keys(refBloblets(node))) end + +""" + $(SIGNATURES) +Check if a Bloblet with the given label exists on the node. +""" +hasBloblet(node, label::Symbol) = haskey(refBloblets(node), label) diff --git a/src/entities/DFGFactor.jl b/src/entities/DFGFactor.jl index 6d7dfa41..8d31ecdc 100644 --- a/src/entities/DFGFactor.jl +++ b/src/entities/DFGFactor.jl @@ -223,13 +223,13 @@ end # we can only use this fallback once all patch! methods are defined # function patch!(dest::D, src::S) where {D <: AbstractGraphNode, S <: AbstractGraphNode} -function patch!(dest::FactorDFG, src::FactorDFG) - throw( - ArgumentError( - "Type mismatch in patch!: Cannot patch a $(typeof(src)) into a $(typeof(dest)). ", - ), - ) -end +# function patch!(dest::FactorDFG, src::FactorDFG) +# throw( +# MergeConflictError( +# "Type mismatch in patch!: Cannot patch a $(typeof(src)) into a $(typeof(dest)). ", +# ), +# ) +# end """ $SIGNATURES @@ -238,33 +238,38 @@ Merge the contents of `src` into `dest` by only patching child collections/conta Notes: - Cascades into collections (`tags`, `blobentries`, `bloblets`). - Assumes `label`, `timestamp`, and `statekind` are immutable and does not update them. +- Throws `MergeConflictError` when factor types differ (different observation or arity). """ -function patch!(dest::FactorDFG{T, N}, src::FactorDFG{T, N}) where {T, N} +function patch!(dest::FactorDFG, src::FactorDFG) dest === src && return dest # avoid unnecessary work if same object + # Catch mismatched factor types (different observation T or arity N). + # @TODO maybe revert to dispatch as it was before to shorten this method. + typeof(dest) !== typeof(src) && throw( + MergeConflictError( + "Cannot merge factors of different types: $(typeof(dest)) vs $(typeof(src)).", + ), + ) + dest.label !== src.label && throw( MergeConflictError("Conflicting Factor labels: $(dest.label) vs $(src.label)"), ) - dest.timestamp !== src.timestamp && throw( + dest.timestamp != src.timestamp && throw( MergeConflictError( "Factor $(dest.label) has conflicting timestamps: $(dest.timestamp) vs $(src.timestamp)", ), ) - if DFG.getVariableOrder(dfg, label) != DFG.getVariableOrder(factor) + if DFG.getVariableOrder(dest) != DFG.getVariableOrder(src) throw( MergeConflictError( - """ - Cannot merge factor with label $(label): factor neighbors differ. - Existing neighbors: $(DFG.getVariableOrder(dfg, label)), - new neighbors: $(DFG.getVariableOrder(factor)). - """, + "Cannot merge factor $(dest.label): variable order differs. Existing: $(dest.variableorder), new: $(src.variableorder).", ), ) end dest.observation != src.observation && - throw(MergeConflictError("Conflict in observations for factor $(label).")) + throw(MergeConflictError("Conflict in observations for factor $(dest.label).")) union!(dest.tags, src.tags) merge!(dest.blobentries, src.blobentries) @@ -275,7 +280,9 @@ function patch!(dest::FactorDFG{T, N}, src::FactorDFG{T, N}) where {T, N} patch!(dest.state, src.state) #TODO Confirm solvercache merge policy, overwriting seems logical. - dest.solvercache[] = src.solvercache[] + if isassigned(src.solvercache) + dest.solvercache[] = src.solvercache[] + end return dest end @@ -361,3 +368,40 @@ end function FactorSkeleton(f::AbstractGraphFactor) return FactorSkeleton(f.label, copy(f.tags), f.variableorder) end + +##============================================================================== +## patch! for Summary/Skeleton types +##============================================================================== + +function patch!(dest::FactorSummary, src::FactorSummary) + dest === src && return dest + dest.label !== src.label && throw( + MergeConflictError("Conflicting Factor labels: $(dest.label) vs $(src.label)"), + ) + dest.timestamp != src.timestamp && throw( + MergeConflictError( + "Factor $(dest.label) has conflicting timestamps: $(dest.timestamp) vs $(src.timestamp)", + ), + ) + dest.variableorder != src.variableorder && throw( + MergeConflictError( + "Cannot merge factor $(dest.label): variable order differs. Existing: $(dest.variableorder), new: $(src.variableorder).", + ), + ) + union!(dest.tags, src.tags) + return dest +end + +function patch!(dest::FactorSkeleton, src::FactorSkeleton) + dest === src && return dest + dest.label !== src.label && throw( + MergeConflictError("Conflicting Factor labels: $(dest.label) vs $(src.label)"), + ) + dest.variableorder != src.variableorder && throw( + MergeConflictError( + "Cannot merge factor $(dest.label): variable order differs. Existing: $(dest.variableorder), new: $(src.variableorder).", + ), + ) + union!(dest.tags, src.tags) + return dest +end diff --git a/src/entities/DFGVariable.jl b/src/entities/DFGVariable.jl index b4d4a034..6b20359d 100644 --- a/src/entities/DFGVariable.jl +++ b/src/entities/DFGVariable.jl @@ -404,10 +404,10 @@ function patch!(dest::VariableDFG{T}, src::VariableDFG{T}) where {T} dest === src && return dest # avoid unnecessary work if same object dest.label !== src.label && throw( - ArgumentError("Variables has different labels: $(dest.label) vs $(src.label)"), + MergeConflictError("Variables has different labels: $(dest.label) vs $(src.label)"), ) - dest.timestamp !== src.timestamp && throw( - ArgumentError( + dest.timestamp != src.timestamp && throw( + MergeConflictError( "Variables has different timestamps: $(dest.timestamp) vs $(src.timestamp).", ), ) @@ -489,3 +489,31 @@ end function VariableSkeleton(v::AbstractGraphVariable) return VariableSkeleton(v.label, copy(v.tags)) end + +##============================================================================== +## patch! for Summary/Skeleton types +##============================================================================== + +function patch!(dest::VariableSummary, src::VariableSummary) + dest === src && return dest + dest.label !== src.label && throw( + MergeConflictError("Variables has different labels: $(dest.label) vs $(src.label)"), + ) + dest.timestamp != src.timestamp && throw( + MergeConflictError( + "Variables has different timestamps: $(dest.timestamp) vs $(src.timestamp).", + ), + ) + union!(dest.tags, src.tags) + merge!(dest.blobentries, src.blobentries) + return dest +end + +function patch!(dest::VariableSkeleton, src::VariableSkeleton) + dest === src && return dest + dest.label !== src.label && throw( + MergeConflictError("Variables has different labels: $(dest.label) vs $(src.label)"), + ) + union!(dest.tags, src.tags) + return dest +end diff --git a/src/serialization/DFGStructStyles.jl b/src/serialization/DFGStructStyles.jl index 3921cef7..0f6bb0a1 100644 --- a/src/serialization/DFGStructStyles.jl +++ b/src/serialization/DFGStructStyles.jl @@ -14,6 +14,7 @@ function StructUtils.lift(::DFGJSONStyle, ::Type{TimeDateZone}, x::AbstractStrin return TimeDateZone(x), nothing end +#TODO StructUtils v2.7 adds support for StaticArrays, update, test, and remove these overloads if they work as expected # SArray serialization overloads StructUtils.lower(::DFGJSONStyle, x::SArray) = x diff --git a/src/services/AbstractDFG.jl b/src/services/AbstractDFG.jl index 8c212b4e..3f845cba 100644 --- a/src/services/AbstractDFG.jl +++ b/src/services/AbstractDFG.jl @@ -103,15 +103,38 @@ function getBlobstore(dfg::AbstractDFG, storeLabel::Symbol) return store end +function getBlobstores(dfg::AbstractDFG) + stores = map(listBlobstores(dfg)) do label + return getBlobstore(dfg, label) + end + return isempty(stores) ? AbstractBlobstore[] : stores +end + function addBlobstore!(dfg::AbstractDFG, store::AbstractBlobstore) label = getLabel(store) haskey(refBlobstores(dfg), label) && throw(LabelExistsError("Blobstore", label)) return push!(refBlobstores(dfg), label => store) end -function mergeBlobstore!(dfg::AbstractDFG, store::AbstractBlobstore) - push!(refBlobstores(dfg), getLabel(store) => store) + +# TODO edge api is a work in progress and only internal +function mergeStorelink!(dfg::AbstractDFG, link_to_store::AbstractBlobstore) + # we currenlty onlly have a label for a storelink, so we do not know if it is the same link + # so we have to look at the Blobstore node to find out. we only merge if the label=>store matches + # + label = getLabel(link_to_store) # the edge in this case is from the virtual dfg object to the Blobstore, so simply label. + if hasBlobstore(dfg, label) + existing_store = getBlobstore(dfg, label) + if existing_store != link_to_store + throw(MergeConflictError("Blobstore", label)) + else + return 0 # no merge needed, they are the same store + end + else + push!(refBlobstores(dfg), label => link_to_store) + end return 1 end + function deleteBlobstore!(dfg::AbstractDFG, key::Symbol) !haskey(refBlobstores(dfg), key) && return 0 pop!(refBlobstores(dfg), key) @@ -119,6 +142,18 @@ function deleteBlobstore!(dfg::AbstractDFG, key::Symbol) end listBlobstores(dfg::AbstractDFG) = collect(keys(refBlobstores(dfg))) +function mergeStorelinks!(destDFG::AbstractDFG, blobstores::Vector{<:AbstractBlobstore}) + count = 0 + for store in blobstores + count += mergeStorelink!(destDFG, store) + end + return count +end + +function hasBlobstore(dfg::AbstractDFG, label::Symbol) + return haskey(refBlobstores(dfg), label) +end + #TODO empty as verb or only `deleteNouns!` # emptyBlobstore!(dfg::AbstractDFG) = empty!(refBlobstores(dfg)) @@ -242,7 +277,7 @@ function mergeVariable! end function mergeVariables!(dfg::AbstractDFG, variables::Vector{<:AbstractGraphVariable}) counts = asyncmap(v->mergeVariable!(dfg, v), variables) - return sum(counts) + return sum(counts; init = 0) end """ @@ -255,7 +290,7 @@ function mergeFactor! end function mergeFactors!(dfg::AbstractDFG, factors::Vector{<:AbstractGraphFactor}) counts = asyncmap(f->mergeFactor!(dfg, f), factors) - return sum(counts) + return sum(counts; init = 0) end """ @@ -508,7 +543,7 @@ Related: Dev Notes - Bulk vs node for node: a list of labels are compiled and the sugraph is copied in bulk. """ -function buildSubgraph( +function getSubgraph( ::Type{G}, dfg::AbstractDFG, variableFactorLabels::Vector{Symbol}, @@ -546,12 +581,12 @@ function buildSubgraph( return buildSubgraph(LocalDFG, dfg, variableFactorLabels, distance; kwargs...) end -function mergeGraph!(srcDFG::AbstractDFG, destDFG::AbstractDFG) +function mergeGraph!(destDFG::AbstractDFG, srcDFG::AbstractDFG) patch!(destDFG.graph, srcDFG.graph) - mergeAgent!(destDFG.agent, srcDFG.agent) - mergeBlobstores!(destDFG, srcDFG) mergeVariables!(destDFG, getVariables(srcDFG)) mergeFactors!(destDFG, getFactors(srcDFG)) + mergeAgent!(destDFG.agent, srcDFG.agent) + mergeStorelinks!(destDFG, getBlobstores(srcDFG)) return destDFG end diff --git a/src/services/Bloblet.jl b/src/services/Bloblet.jl index 7c9152b6..002ef744 100644 --- a/src/services/Bloblet.jl +++ b/src/services/Bloblet.jl @@ -69,6 +69,13 @@ function listVariableBloblets(dfg::GraphsDFG, var_label::Symbol) return listBloblets(getVariable(dfg, var_label)) end +""" + $(SIGNATURES) +""" +function hasVariableBloblet(dfg::GraphsDFG, var_label::Symbol, label::Symbol) + return hasBloblet(getVariable(dfg, var_label), label) +end + ## ============================================================================== ## Factor Bloblets ## ============================================================================== @@ -135,6 +142,13 @@ function listFactorBloblets(dfg::GraphsDFG, fac_label::Symbol) return listBloblets(getFactor(dfg, fac_label)) end +""" + $(SIGNATURES) +""" +function hasFactorBloblet(dfg::GraphsDFG, fac_label::Symbol, label::Symbol) + return hasBloblet(getFactor(dfg, fac_label), label) +end + ##============================================================================== ## Agent Bloblets ##============================================================================== @@ -180,6 +194,10 @@ end $(SIGNATURES) """ listAgentBloblets(dfg::GraphsDFG) = listBloblets(dfg.agent) +""" + $(SIGNATURES) +""" +hasAgentBloblet(dfg::GraphsDFG, label::Symbol) = hasBloblet(dfg.agent, label) ##============================================================================== ## Graph Bloblets @@ -226,3 +244,7 @@ end $(SIGNATURES) """ listGraphBloblets(dfg::GraphsDFG) = listBloblets(dfg.graph) +""" + $(SIGNATURES) +""" +hasGraphBloblet(dfg::GraphsDFG, label::Symbol) = hasBloblet(dfg.graph, label) diff --git a/src/services/DFGVariable.jl b/src/services/DFGVariable.jl index bd5536d0..161c48f5 100644 --- a/src/services/DFGVariable.jl +++ b/src/services/DFGVariable.jl @@ -388,14 +388,16 @@ NOTE: If an error occurs while adding one of the states, previously added states """ function addStates!(dfg::AbstractDFG, variableLabel::Symbol, states::Vector{<:State}) cnt = asyncmap(states) do state - return addState!(dfg, variableLabel, state) + addState!(dfg, variableLabel, state) + return 1 end return sum(cnt) end function addStates!(dfg::AbstractDFG, varLabel_state_pairs::Vector{<:Pair{Symbol, <:State}}) cnt = asyncmap(varLabel_state_pairs) do (varLabel, state) - return addState!(dfg, varLabel, state) + addState!(dfg, varLabel, state) + return 1 end return sum(cnt) end @@ -527,5 +529,5 @@ function listStates( for vl in vls union!(labels, listStates(dfg, vl; labelFilter)) end - return labels + return collect(labels) end diff --git a/src/services/Tags.jl b/src/services/Tags.jl index 65d27657..7623f908 100644 --- a/src/services/Tags.jl +++ b/src/services/Tags.jl @@ -134,15 +134,16 @@ function listTags(dfg::AbstractDFG, sym::Symbol) return listTags(getFnc(dfg, sym)) end -#TODO FIXME for DFGv1, merge and delete should return the number of tags added/removed. function mergeTags!(dfg::InMemoryDFGTypes, sym::Symbol, tags) getFnc = isVariable(dfg, sym) ? getVariable : getFactor - return union!(refTags(getFnc(dfg, sym)), tags) + union!(refTags(getFnc(dfg, sym)), tags) + return length(tags) end function deleteTags!(dfg::InMemoryDFGTypes, sym::Symbol, tags) getFnc = isVariable(dfg, sym) ? getVariable : getFactor - return setdiff!(refTags(getFnc(dfg, sym)), tags) + setdiff!(refTags(getFnc(dfg, sym)), tags) + return length(tags) end function emptyTags!(dfg::InMemoryDFGTypes, sym::Symbol) diff --git a/test/interfaceTests.jl b/test/interfaceTests.jl index 0d77b6c2..72678517 100644 --- a/test/interfaceTests.jl +++ b/test/interfaceTests.jl @@ -127,6 +127,18 @@ end blobletTestBlock!(fg1) end +@testset "Factor Bloblet CRUD" begin + factorBlobletTestBlock!(fg1) +end + +@testset "Has Bloblet" begin + hasBlobletTestBlock!(fg1) +end + +@testset "States Extended" begin + statesExtendedTestBlock!(fg1) +end + @testset "Data Entries and Blobs" begin if typeof(fg1) <: InMemoryDFGTypes DataEntriesTestBlock!(fg1, var2) @@ -134,6 +146,9 @@ end @testset "Data blob tests" begin blobsStoresTestBlock!(fg1) end + @testset "Blobstore Extended" begin + blobstoreExtendedTestBlock!(fg1) + end end @testset "TODO Sorteer groep" begin diff --git a/test/testBlocks.jl b/test/testBlocks.jl index 2236a183..99c58aed 100644 --- a/test/testBlocks.jl +++ b/test/testBlocks.jl @@ -446,7 +446,7 @@ function VariablesandFactorsCRUD_SET!(fg, v1, v2, v3, f0, f1, f2) f2_mod = typeof(f2)(f2.label, (:a,)) end - @test_throws DomainError mergeFactor!(fg, f2_mod) + @test_throws DFG.MergeConflictError mergeFactor!(fg, f2_mod) @test issetequal(lsf(fg), [:bcf1, :abf1]) # Extra timestamp functions https://github.com/JuliaRobotics/DistributedFactorGraphs.jl/issues/315 @@ -543,8 +543,10 @@ function tagsTestBlock!(fg, v1, v1_tags) v1Tags = deepcopy(DFG.refTags(v1)) @test issetequal(v1Tags, v1_tags) @test issetequal(listTags(fg, :a), v1Tags) - @test issetequal(mergeTags!(fg, :a, [:TAG]), v1Tags ∪ [:TAG]) - @test issetequal(deleteTags!(fg, :a, [:TAG]), v1Tags) + @test mergeTags!(fg, :a, [:TAG]) == 1 + @test issetequal(listTags(fg, :a), v1Tags ∪ [:TAG]) + @test deleteTags!(fg, :a, [:TAG]) == 1 + @test issetequal(listTags(fg, :a), v1Tags) @test emptyTags!(fg, :a) == Set{Symbol}() v2Tags = listTags(fg, :b) @@ -705,6 +707,140 @@ function blobletTestBlock!(fg) @test length(listVariableBloblets(fg, :a)) == 9 # emptyVariableBloblets!(fg, :a) # @test length(listVariableBloblets(fg, :a)) == 0 + + # has bloblet + @test hasVariableBloblet(fg, :a, :small) + @test !hasVariableBloblet(fg, :a, :nonexistent) + + # delete non-existent returns 0 + @test deleteVariableBloblet!(fg, :a, :nonexistent) == 0 + + # Bulk operations + bulk_bloblets = [Bloblet(:bulk1, 1), Bloblet(:bulk2, 2)] + @test addVariableBloblets!(fg, :a, bulk_bloblets) == bulk_bloblets + @test hasVariableBloblet(fg, :a, :bulk1) + @test hasVariableBloblet(fg, :a, :bulk2) + @test mergeVariableBloblets!(fg, :a, [Bloblet(:bulk1, 10), Bloblet(:bulk2, 20)]) == 2 + @test deleteVariableBloblets!(fg, :a, [:bulk1, :bulk2]) == 2 + @test !hasVariableBloblet(fg, :a, :bulk1) + + # get non-existent throws + @test_throws LabelNotFoundError getVariableBloblet(fg, :a, :nonexistent) +end + +function factorBlobletTestBlock!(fg) + # Factor bloblet CRUD + @test listFactorBloblets(fg, :abf1) == Symbol[] + @test addFactorBloblet!(fg, :abf1, Bloblet(:fbl1, "factor_data")) == + Bloblet(:fbl1, "factor_data") + @test_throws LabelExistsError addFactorBloblet!(fg, :abf1, Bloblet(:fbl1, "dup")) + @test hasFactorBloblet(fg, :abf1, :fbl1) + @test !hasFactorBloblet(fg, :abf1, :nonexistent) + @test getFactorBloblet(fg, :abf1, :fbl1) == Bloblet(:fbl1, "factor_data") + @test_throws LabelNotFoundError getFactorBloblet(fg, :abf1, :nonexistent) + @test mergeFactorBloblet!(fg, :abf1, Bloblet(:fbl1, "updated")) == 1 + @test getFactorBloblet(fg, :abf1, :fbl1) == Bloblet(:fbl1, "updated") + @test :fbl1 in listFactorBloblets(fg, :abf1) + + # Bulk operations + @test addFactorBloblets!(fg, :abf1, [Bloblet(:fbl2, 1), Bloblet(:fbl3, 2)]) == + [Bloblet(:fbl2, 1), Bloblet(:fbl3, 2)] + @test length(getFactorBloblets(fg, :abf1)) == 3 + @test mergeFactorBloblets!(fg, :abf1, [Bloblet(:fbl2, 10), Bloblet(:fbl3, 20)]) == 2 + @test deleteFactorBloblets!(fg, :abf1, [:fbl2, :fbl3]) == 2 + @test deleteFactorBloblet!(fg, :abf1, :fbl1) == 1 + @test listFactorBloblets(fg, :abf1) == Symbol[] +end + +function hasBlobletTestBlock!(fg) + # Agent bloblets has + addAgentBloblet!(fg, Bloblet(:agent_has_test, "data")) + @test hasAgentBloblet(fg, :agent_has_test) + @test !hasAgentBloblet(fg, :nonexistent) + deleteAgentBloblet!(fg, :agent_has_test) + @test !hasAgentBloblet(fg, :agent_has_test) + + # Graph bloblets has + addGraphBloblet!(fg, Bloblet(:graph_has_test, "data")) + @test hasGraphBloblet(fg, :graph_has_test) + @test !hasGraphBloblet(fg, :nonexistent) + deleteGraphBloblet!(fg, :graph_has_test) + @test !hasGraphBloblet(fg, :graph_has_test) +end + +function statesExtendedTestBlock!(fg) + # hasState + @test hasState(fg, :a, :default) + @test !hasState(fg, :a, :nonexistent) + + # getStates returns Vector + states = getStates(fg, :a) + @test states isa Vector + @test length(states) >= 1 + + # addStates! bulk + s1 = State{TestVariableType1}(; label = :bulk_s1) + s2 = State{TestVariableType1}(; label = :bulk_s2) + @test addStates!(fg, :a, [s1, s2]) == 2 + @test hasState(fg, :a, :bulk_s1) + @test hasState(fg, :a, :bulk_s2) + + # addStates! with pair syntax + s3 = State{TestVariableType2}(; label = :bulk_s3) + @test addStates!(fg, [:b => s3]) == 1 + @test hasState(fg, :b, :bulk_s3) + + # deleteStates! bulk + @test deleteStates!(fg, :a, [:bulk_s1, :bulk_s2]) == 2 + @test !hasState(fg, :a, :bulk_s1) + @test !hasState(fg, :a, :bulk_s2) + + # deleteStates! with pair syntax + @test deleteStates!(fg, [:b => :bulk_s3]) == 1 + @test !hasState(fg, :b, :bulk_s3) + + # deleteState! non-existent returns 0 + @test deleteState!(fg, :a, :nonexistent) == 0 + + # listStates with filters + @test :default in listStates(fg, :a) + @test listStates(fg, :a; labelFilter = ==(Symbol("default")) ∘ identity) == [:default] + + # listStates across dfg returns Vector + all_states = listStates(fg) + @test all_states isa AbstractVector + @test :default in all_states + + # LabelNotFoundError for getState on missing state + @test_throws LabelNotFoundError getState(fg, :a, :nonexistent) + + # mergeStates! bulk + s4 = State{TestVariableType1}(; label = :merge_bulk) + @test mergeStates!(fg, :a, [s4]) == 1 + @test hasState(fg, :a, :merge_bulk) + @test mergeStates!(fg, :a, [s4]) == 1 # idempotent + deleteState!(fg, :a, :merge_bulk) + return nothing +end + +function blobstoreExtendedTestBlock!(fg) + store = DFG.InMemoryBlobstore(:mergestore) + @test addBlobstore!(fg, store) isa Any + @test_throws LabelExistsError addBlobstore!(fg, store) + + # mergeStorelinks! + store2 = DFG.InMemoryBlobstore(:mergestore2) + @test DFG.mergeStorelinks!(fg, [store2]) == 1 + @test :mergestore2 in listBlobstores(fg) + # merge again is idempotent + @test DFG.mergeStorelinks!(fg, [store2]) == 0 + + @test_throws LabelNotFoundError getBlobstore(fg, :nonexistent) + + # cleanup + @test deleteBlobstore!(fg, :mergestore) == 1 + @test deleteBlobstore!(fg, :mergestore2) == 1 + @test deleteBlobstore!(fg, :nonexistent) == 0 end function DataEntriesTestBlock!(fg, v2) @@ -827,7 +963,7 @@ function DataEntriesTestBlock!(fg, v2) @test mergeAgentBlobentry!(fg, de2_update) == 1 @test listAgentBlobentries(fg) == [getLabel(de1), getLabel(de2_update)] @test deleteAgentBlobentry!(fg, getLabel(de2_update)) == 1 - @test deleteAgentBlobentry!(fg, getLabel(de2_update)) + @test deleteAgentBlobentry!(fg, getLabel(de2_update)) == 0 @test getAgentBlobentries(fg) == [de1] @test addAgentBlobentries!(fg, [de2]) == [de2] @test mergeAgentBlobentries!(fg, [de1, de2_update]) == 2 From 7e59d45eb78703243dda35140cb399deb88fe1a3 Mon Sep 17 00:00:00 2001 From: Johannes Terblanche <6612981+Affie@users.noreply.github.com> Date: Mon, 23 Mar 2026 12:36:17 +0200 Subject: [PATCH 3/6] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/services/AbstractDFG.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/AbstractDFG.jl b/src/services/AbstractDFG.jl index 3f845cba..9832441c 100644 --- a/src/services/AbstractDFG.jl +++ b/src/services/AbstractDFG.jl @@ -125,7 +125,7 @@ function mergeStorelink!(dfg::AbstractDFG, link_to_store::AbstractBlobstore) if hasBlobstore(dfg, label) existing_store = getBlobstore(dfg, label) if existing_store != link_to_store - throw(MergeConflictError("Blobstore", label)) + throw(MergeConflictError("Merge conflict for Blobstore with label $(label)")) else return 0 # no merge needed, they are the same store end From 7035c2776957ecd9e1d4eb31902fe6d61cf300d9 Mon Sep 17 00:00:00 2001 From: Johannes Terblanche Date: Mon, 23 Mar 2026 12:53:30 +0200 Subject: [PATCH 4/6] Fix typos and GraphsDFGs import/using --- src/DistributedFactorGraphs.jl | 2 +- src/GraphsDFG/GraphsDFG.jl | 5 +---- src/GraphsDFG/services/GraphsDFG.jl | 2 +- src/services/AbstractDFG.jl | 2 +- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/DistributedFactorGraphs.jl b/src/DistributedFactorGraphs.jl index 3863a1a9..d5f0ba0a 100644 --- a/src/DistributedFactorGraphs.jl +++ b/src/DistributedFactorGraphs.jl @@ -77,7 +77,7 @@ export AbstractStateType, StateType ##------------------------------------------------------------------------------ ## Types ##------------------------------------------------------------------------------ -#TODO types are not yet stable - also, we might not export types such as VariableCompute +#TODO types are not yet stable - also, we might not export all types # Variables export VariableDFG export VariableSummary diff --git a/src/GraphsDFG/GraphsDFG.jl b/src/GraphsDFG/GraphsDFG.jl index 5e2dd28f..8fcd6602 100644 --- a/src/GraphsDFG/GraphsDFG.jl +++ b/src/GraphsDFG/GraphsDFG.jl @@ -27,9 +27,7 @@ using ...DistributedFactorGraphs: FolderStore, refTags, listTags, - patch!, - getVariableOrder, - getObservation + patch! # import DFG functions to extend import ...DistributedFactorGraphs: @@ -57,7 +55,6 @@ import ...DistributedFactorGraphs: isConnected, listNeighbors, buildSubgraph, - copyGraph!, getBiadjacencyMatrix, toDot, toDotFile, diff --git a/src/GraphsDFG/services/GraphsDFG.jl b/src/GraphsDFG/services/GraphsDFG.jl index 9c20be2e..7d8462a2 100644 --- a/src/GraphsDFG/services/GraphsDFG.jl +++ b/src/GraphsDFG/services/GraphsDFG.jl @@ -184,7 +184,7 @@ function getFactors( filterDFG!(factors, labelFilter, getLabel) filterDFG!(factors, solvableFilter, getSolvable) filterDFG!(factors, tagsFilter, refTags) - filterDFG!(factors, typeFilter, typeof ∘ getObservation) + filterDFG!(factors, typeFilter, typeof ∘ DFG.getObservation) return factors end diff --git a/src/services/AbstractDFG.jl b/src/services/AbstractDFG.jl index 9832441c..2267f746 100644 --- a/src/services/AbstractDFG.jl +++ b/src/services/AbstractDFG.jl @@ -118,7 +118,7 @@ end # TODO edge api is a work in progress and only internal function mergeStorelink!(dfg::AbstractDFG, link_to_store::AbstractBlobstore) - # we currenlty onlly have a label for a storelink, so we do not know if it is the same link + # we currently only have a label for a storelink, so we do not know if it is the same link # so we have to look at the Blobstore node to find out. we only merge if the label=>store matches # label = getLabel(link_to_store) # the edge in this case is from the virtual dfg object to the Blobstore, so simply label. From af20784592b3fa6fb377ca126dd09ce545b5b7c7 Mon Sep 17 00:00:00 2001 From: Johannes Terblanche Date: Mon, 23 Mar 2026 13:32:16 +0200 Subject: [PATCH 5/6] Fix/update docs --- docs/src/BuildingGraphs.md | 2 +- src/services/AbstractDFG.jl | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/docs/src/BuildingGraphs.md b/docs/src/BuildingGraphs.md index 561aa731..cb8af5e5 100644 --- a/docs/src/BuildingGraphs.md +++ b/docs/src/BuildingGraphs.md @@ -116,7 +116,7 @@ To list all variables or factors (instead of just their labels), use the Traversing and Querying functions for finding the relationships and building subtraphs include: - [`listNeighbors`](@ref) -- [`buildSubgraph`](@ref) +- [`getSubgraph`](@ref) - [`getBiadjacencyMatrix`](@ref) ## Getting (Reading) Variables and Factors diff --git a/src/services/AbstractDFG.jl b/src/services/AbstractDFG.jl index 2267f746..1ce7b1a5 100644 --- a/src/services/AbstractDFG.jl +++ b/src/services/AbstractDFG.jl @@ -484,9 +484,7 @@ Notes - Returns a tuple `(variableLabels, factorLabels)`, where each element is a `Vector{Symbol}`. Related: -- [`copyGraph!`](@ref) -- [`buildSubgraph`](@ref) -- [`deepcopyGraph`](@ref) +- [`getSubgraph`](@ref) - [`mergeGraph!`](@ref) """ function listNeighborhood(dfg::AbstractDFG, label::Symbol, distance::Int; filters...) @@ -536,9 +534,7 @@ end Build a deep subgraph copy from the DFG given a list of variables and factors and an optional distance. Note: Orphaned factors (where the subgraph does not contain all the related variables) are not returned. Related: -- [`copyGraph!`](@ref) - [`listNeighborhood`](@ref) -- [`deepcopyGraph`](@ref) - [`mergeGraph!`](@ref) Dev Notes - Bulk vs node for node: a list of labels are compiled and the sugraph is copied in bulk. @@ -581,6 +577,16 @@ function buildSubgraph( return buildSubgraph(LocalDFG, dfg, variableFactorLabels, distance; kwargs...) end +""" + $(SIGNATURES) +Merge the source DFG into the destination DFG cascading down the hierarchy of DFG nodes. +Merge rules: +- Variables, Factors, Agent, and Graphroot, with the same label are merged if they are equal. + - On conflicts, a `MergeConflictError` is thrown. + - Child nodes (eg. tags, Bloblets, Blobentries, States, etc.) are using `merge!`. +- The Blobstore links are merged provided they point to the same blobstore. + - On conflicts, a `MergeConflictError` is thrown. +""" function mergeGraph!(destDFG::AbstractDFG, srcDFG::AbstractDFG) patch!(destDFG.graph, srcDFG.graph) mergeVariables!(destDFG, getVariables(srcDFG)) @@ -634,7 +640,7 @@ Notes function toDot(dfg::AbstractDFG) #convert to GraphsDFG ldfg = GraphsDFG{NoSolverParams}() - copyGraph!(ldfg, dfg, listVariables(dfg), listFactors(dfg)) + copyGraph!(ldfg, dfg, listVariables(dfg), listFactors(dfg)) #fixme, this is probably copyto!/sync! return toDot(ldfg) end From 464c386b46c5f010cbc1f4eb7df995c5a163a82f Mon Sep 17 00:00:00 2001 From: Johannes Terblanche Date: Mon, 23 Mar 2026 14:23:35 +0200 Subject: [PATCH 6/6] fix deleteBlobentries! return --- src/DataBlobs/services/BlobEntry.jl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/DataBlobs/services/BlobEntry.jl b/src/DataBlobs/services/BlobEntry.jl index e116bdef..a869dfce 100644 --- a/src/DataBlobs/services/BlobEntry.jl +++ b/src/DataBlobs/services/BlobEntry.jl @@ -63,8 +63,7 @@ end deleteBlobentry!(node, entry) = deleteBlobentry!(node, getLabel(entry)) function deleteBlobentries!(node, labels::Vector{Symbol}) - deleteBlobentry!.(node, labels) - return length(labels) + return sum(deleteBlobentry!.(node, labels)) end """