From 348320c6e40d38c8125455a44d7f1c19bdf47ed4 Mon Sep 17 00:00:00 2001 From: Dehann Fourie <6412556+dehann@users.noreply.github.com> Date: Mon, 3 Jun 2024 22:40:06 -0700 Subject: [PATCH 1/9] Create CI.yml --- .github/workflows/CI.yml | 49 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 .github/workflows/CI.yml diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 0000000..005ad86 --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,49 @@ +name: CI +on: + pull_request: + push: + branches: + - master + +jobs: + test: + name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} + runs-on: ${{ matrix.os }} + # env: + # JULIA_PKG_SERVER: "" + strategy: + fail-fast: false + matrix: + version: + - '1.10' + - 'nightly' + os: + - ubuntu-latest + arch: + - x64 + steps: + - uses: actions/checkout@v2 + - uses: julia-actions/setup-julia@v1 + with: + version: ${{ matrix.version }} + arch: ${{ matrix.arch }} + - uses: actions/cache@v1 + env: + cache-name: cache-artifacts + with: + path: ~/.julia/artifacts + key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} + restore-keys: | + ${{ runner.os }}-test-${{ env.cache-name }}- + ${{ runner.os }}-test- + ${{ runner.os }}- + - uses: julia-actions/julia-buildpkg@latest + - run: | + git config --global user.name Tester + git config --global user.email te@st.er + - uses: julia-actions/julia-runtest@latest + continue-on-error: ${{ matrix.version == 'nightly' }} + - uses: julia-actions/julia-processcoverage@v1 + - uses: codecov/codecov-action@v1 + with: + file: lcov.info From 49a11f0cc31735c87cb8644d6436c979b1274115 Mon Sep 17 00:00:00 2001 From: Dehann Fourie <6412556+dehann@users.noreply.github.com> Date: Mon, 3 Jun 2024 22:41:08 -0700 Subject: [PATCH 2/9] Delete .travis.yml --- .travis.yml | 40 ---------------------------------------- 1 file changed, 40 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index bb1b548..0000000 --- a/.travis.yml +++ /dev/null @@ -1,40 +0,0 @@ -language: julia - -addons: - apt: - packages: - - hdf5-tools - - graphviz - -arch: - - amd64 - -os: - - linux - -julia: - - 1.5 - -jobs: - include: - - name: "Legacy, Julia 1.0" - julia: 1.0 - - name: "MASTER AND TAGS" - - name: "Julia nightly" - julia: nightly - - name: "OSX" - os: osx - - name: "ARM64" - arch: arm64 - # if: NOT branch =~ ^release.*$ - fast_finish: true - allow_failures: - - julia: nightly - - os: osx - - arch: arm64 - -notifications: - email: false - -after_success: - - julia -e 'using Pkg; cd(Pkg.dir("FunctionalStateMachine")); Pkg.add("Coverage"); using Coverage; Codecov.submit(process_folder())' From a172c0a528aa754de736dafde3df8e712e8c7157 Mon Sep 17 00:00:00 2001 From: Dehann Fourie <6412556+dehann@users.noreply.github.com> Date: Mon, 3 Jun 2024 22:42:30 -0700 Subject: [PATCH 3/9] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4df7eb9..782e0f0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # FunctionalStateMachine.jl -[![Build Status](https://travis-ci.org/JuliaRobotics/FunctionalStateMachine.jl.svg?branch=master)](https://travis-ci.org/JuliaRobotics/FunctionalStateMachine.jl) +[![CI](https://github.com/JuliaRobotics/FunctionalStateMachine.jl/actions/workflows/ci.yml/badge.svg)](https://github.com/JuliaRobotics/FunctionalStateMachine.jl/actions/workflows/ci.yml) [![codecov.io](https://codecov.io/github/JuliaRobotics/FunctionalStateMachine.jl/coverage.svg?branch=master)](https://codecov.io/github/JuliaRobotics/FunctionalStateMachine.jl?branch=master) [![Average time to resolve an issue](https://isitmaintained.com/badge/resolution/JuliaRobotics/FunctionalStateMachine.jl.svg)](https://github.com/JuliaRobotics/FunctionalStateMachine.jl/issues) From 298cecae0d40cc697f4002dc12e77ae8d2e9360a Mon Sep 17 00:00:00 2001 From: Dehann Fourie <6412556+dehann@users.noreply.github.com> Date: Mon, 3 Jun 2024 22:45:39 -0700 Subject: [PATCH 4/9] Update testStateMachine.jl --- test/testStateMachine.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/testStateMachine.jl b/test/testStateMachine.jl index 5ca150e..6b61392 100644 --- a/test/testStateMachine.jl +++ b/test/testStateMachine.jl @@ -82,7 +82,9 @@ while statemachine(nothing, recordhistory=true); end hists = Dict{Symbol,Vector{Tuple{DateTime,Int,Function,Nothing}}}(:first => statemachine.history) -animateStateMachineHistoryIntervalCompound(hists, interval=1) +@error "Restore weakdeps animateStateMa... test" +@test_broken false +# animateStateMachineHistoryIntervalCompound(hists, interval=1) end From 43d638b27071239919aeb45eda7326cdeb9553ef Mon Sep 17 00:00:00 2001 From: Dehann Fourie <6412556+dehann@users.noreply.github.com> Date: Mon, 3 Jun 2024 22:47:57 -0700 Subject: [PATCH 5/9] Update Project.toml --- Project.toml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 83a9cd6..c74f26c 100644 --- a/Project.toml +++ b/Project.toml @@ -2,7 +2,7 @@ name = "FunctionalStateMachine" uuid = "3e9e306e-7e3c-11e9-12d2-8f8f67a2f951" keywords = ["state machine"] desc = "Functional state machine with stepping and visualization tools." -version = "0.2.9" +version = "0.3.0" [deps] Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" @@ -10,6 +10,12 @@ DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" ProgressMeter = "92933f4c-e287-5a05-a399-4b506db050ca" Requires = "ae029012-a4dd-5104-9daa-d747884805df" +[weakdeps] +Graphs = "b5cc3c7e-6572-11e9-2517-99fb8daf2f04" + +[extensions] +FunctionalStateMachineGraphsExt = "Graphs" + [compat] DocStringExtensions = "0.7, 0.8, 0.9" ProgressMeter = "0.9, 1" From c6ead0652fd8f56b476e228a9cf7b67d8f2eeffe Mon Sep 17 00:00:00 2001 From: Dehann Fourie <6412556+dehann@users.noreply.github.com> Date: Mon, 3 Jun 2024 22:51:00 -0700 Subject: [PATCH 6/9] Create FunctionalStateMachineGraphsExt.jl --- ext/FunctionalStateMachineGraphsExt.jl | 480 +++++++++++++++++++++++++ 1 file changed, 480 insertions(+) create mode 100644 ext/FunctionalStateMachineGraphsExt.jl diff --git a/ext/FunctionalStateMachineGraphsExt.jl b/ext/FunctionalStateMachineGraphsExt.jl new file mode 100644 index 0000000..d4ac596 --- /dev/null +++ b/ext/FunctionalStateMachineGraphsExt.jl @@ -0,0 +1,480 @@ +module FunctionalStateMachineGraphsExt + +using Graphs +import FunctionalStateMachine: histGraphStateMachineTransitions, drawStateTransitionStep, drawStateMachineHistory +import FunctionalStateMachine: animateStateMachineHistoryByTime, animateStateMachineHistoryByTimeCompound, animateStateMachineHistoryIntervalCompound + +""" + $SIGNATURES + +Create a `Graphs.incdict` object and populate with nodes (states) and edges (transitions) +according to the contents of parameters passed in. + +Notes: +- Current implementation repeats duplicate transitions as new edges. +""" +function histGraphStateMachineTransitions(stateVisits, allStates::Vector{Symbol}; + maxpenwidth::Real=5, minpenwidth::Real=0.75 ) + # + g = Graphs.incdict(Dict{Int,Graphs.ExVertex}(), Graphs.ExEdge{Graphs.ExVertex}; is_directed=true) + # g = Graphs.incdict(Graphs.ExVertex,Graphs.ExEdge,is_directed=true) + lookup = Dict{Symbol, Int}() + + # add all required states as nodes to the visualization graph + fid = 0 + for state in allStates + fid += 1 + lbl = string(state) + exvert = Graphs.ExVertex(fid, lbl) + exvert.attributes["label"] = lbl + Graphs.add_vertex!(g, exvert) + lookup[state] = fid + end + + alledges = [] + + # add all edges to graph + maxtransedge = 0 + count = 0 + for (from, tos) in stateVisits + for to in tos + count += 1 + exvf = g.vertices[lookup[from]] + exvt = g.vertices[lookup[to]] + # add the edge from one to the next state + # TODO, don't add if already there. + addedge = true + for oun in Graphs.out_neighbors(exvf, g) + if oun.index == exvt.index + addedge = false + # increase penwidth+=1 on that edge + for ed in Graphs.out_edges(exvf, g) + ed.attributes["penwidth"] += 1 + # ed.attributes["penwidth"] = minimum([maxpenwidth;ed.attributes["penwidth"]]) + maxtransedge = maxtransedge < ed.attributes["penwidth"] ? ed.attributes["penwidth"] : maxtransedge + end + break + end + end + if addedge + edge = Graphs.make_edge(g, exvf, exvt) + edge.attributes["penwidth"] = 1.0 + Graphs.add_edge!(g, edge) + push!(alledges, edge) + end + end + end + + # normalize edge penwidth to maxpenwidth + normwidth = maxtransedge/maxpenwidth + for ed in alledges + ed.attributes["penwidth"] = maximum([minpenwidth; ed.attributes["penwidth"]/normwidth]) + + end + + return g, lookup +end + + +function renderStateMachineFrame( vg, + frame::Int; + title::String="", + viewerapp::String="eog", + fext::String="png", + dpi::Int=100, + engine::String="dot", + show::Bool=true, + folder::String="fsm_animation", + folderpath = "/tmp/$folder/", + timest::String="", + rmfirst::Bool=false ) + # + if rmfirst + @warn "removing contents of $(folderpath)" + Base.rm(folderpath, recursive=true, force=true) + end + mkpath(folderpath) + dotfile = "$folderpath/csm_$frame.dot" + filepath= "$folderpath/csm_$frame.$(fext)" + + # dot file handle + fid = open(dotfile,"w") + write(fid,Graphs.to_dot(vg)) + close(fid) + + # build the dot file somewhat manually + fid = open("$folderpath/dotscript.sh","w") + str = "head -n `wc -l $dotfile | awk '{print \$1-1}'` $dotfile > $folderpath/tmpdot.dot" + println(fid, str) + println(fid, "echo \"graph [label=\\\"$title, $(timest)\\\", labelloc=t];\" >> $folderpath/tmpdot.dot") + println(fid, "echo \"}\" >> $folderpath/tmpdot.dot") + close(fid) + run(`chmod u+x $folderpath/dotscript.sh`) + run(`sh $folderpath/dotscript.sh`) + Base.rm(dotfile) + Base.rm("$folderpath/dotscript.sh") + run(`mv $folderpath/tmpdot.dot $dotfile`) + + # compile output and maybe show to user + run(`$(engine) $(dotfile) -T$(fext) -Gdpi=$dpi -o $(filepath)`) + show ? (@async run(`$viewerapp $filepath`)) : nothing + return filepath +end + +function setVisGraphOnState!( vg, vertid; + xlabel::String="", + appendxlabel::String="", + vertColor::AbstractString="red" ) + # + vg.vertices[vertid].attributes["fillcolor"] = vertColor + vg.vertices[vertid].attributes["style"] = "filled" + if length(xlabel) > 0 + vg.vertices[vertid].attributes["xlabel"] = xlabel + end + if haskey(vg.vertices[vertid].attributes, "xlabel") + vg.vertices[vertid].attributes["xlabel"] = vg.vertices[vertid].attributes["xlabel"]*appendxlabel + elseif length(appendxlabel) > 0 + vg.vertices[vertid].attributes["xlabel"] = appendxlabel + end +end + +function clearVisGraphAttributes!(vg) + for (vid,vert) in vg.vertices + haskey(vert.attributes, "fillcolor") ? delete!(vert.attributes, "fillcolor") : nothing + haskey(vert.attributes, "style") ? delete!(vert.attributes, "style") : nothing + haskey(vert.attributes, "xlabel") ? delete!(vert.attributes, "xlabel") : nothing + end + nothing +end + +function drawStateTransitionStep( hist, + step::Int, + vg, + lookup::Dict{Symbol,Int}; + title::String="", + viewerapp::String="eog", + fext::String="png", + engine::String="dot", + show::Bool=true, + folder::String="", + frame::Int=step, + vertColor::AbstractString="red" ) + # + + lbl = getStateLabel(hist[step][3]) + vertid = lookup[lbl] + vert = vg.vertices[vertid] + + fillcolorbefore = haskey(vert.attributes, "fillcolor") ? deepcopy(vg.vertices[vertid].attributes["fillcolor"]) : nothing + stylebefore = haskey(vert.attributes, "style") ? deepcopy(vg.vertices[vertid].attributes["style"]) : nothing + xlabelbefore = haskey(vg.vertices[vertid].attributes,"xlabel") ? deepcopy(vg.vertices[vertid].attributes["xlabel"]) : nothing + # delete!(vert.attributes, "fillcolor") + # delete!(vert.attributes, "style") + + # identify and set the node + xlabel = length(title) > 0 ? (xlabelbefore !== nothing ? xlabelbefore*"," : "")*title : "" + setVisGraphOnState!(vg, vertid, xlabel=xlabel, vertColor=vertColor ) + + # render state machine frame + filepath = renderStateMachineFrame( vg, + frame, + title=title, + viewerapp=viewerapp, + fext=fext, + engine=engine, + show=show, + folder=folder, + timest=string(split(string(hist[step][1]),'T')[end]), + rmfirst=false) + # + + # clean up the vg structure + fillcolorbefore === nothing ? delete!(vert.attributes, "fillcolor") : (vert.attributes["fillcolor"]=fillcolorbefore) + stylebefore === nothing ? delete!(vert.attributes, "style") : (vert.attributes["style"]=stylebefore) + xlabelbefore === nothing ? delete!(vert.attributes, "xlabel") : (vert.attributes["xlabel"]=xlabelbefore) + + return filepath +end + + + + + +function drawStateMachineHistory(hist; show::Bool=false, folder::String="" ) + + stateVisits, allStates = histStateMachineTransitions(hist) + + vg, lookup = histGraphStateMachineTransitions(stateVisits, allStates) + + for i in 1:length(hist) + drawStateTransitionStep(hist, i, vg, lookup, folder=folder, show=show ) + end + + return nothing +end + + + +""" + $SIGNATURES + +Draw simultaneously separate time synchronized frames from each of the desired +state machines. These images can be produced into synchronous side-by-side videos +which allows for easier debugging and comparison of concurrently running state +machines. +""" +function animateStateMachineHistoryByTime(hist::Vector{Tuple{DateTime, Int, <: Function, T}}; + frames::Int=100, + folder="animatestate", + title::String="", + show::Bool=false, + startT=hist[1][1], + stopT=hist[end][1], + rmfirst::Bool=true, + vertColor::AbstractString="red" ) where T + # + stateVisits, allStates = histStateMachineTransitions(hist) + + vg, lookup = histGraphStateMachineTransitions(stateVisits, allStates) + + totT = stopT - startT + totT = Millisecond(round(Int, 1.05*totT.value)) + # totT *= 1.05 + + step = 1 + len = length(hist) + @showprogress "exporting state machine images, $title " for i in 1:frames + aniT = Millisecond(round(Int, i/frames*totT.value)) + startT + # aniT = i/frames*totT + startT + if hist[step][1] < aniT && step < len + step += 1 + end + drawStateTransitionStep(hist, step, vg, lookup, title=title, folder=folder, show=show, frame=i, vertColor=vertColor ) + end + + nothing +end + +function animateStateMachineHistoryByTimeCompound(hists::Dict{Symbol, Vector{Tuple{DateTime, Int, <: Function, T}}}, + startT, + stopT; + frames::Int=100, + folder="animatestate", + title::String="", + show::Bool=false, + clearstale::Bool=true, + rmfirst::Bool=true, + fsmColors::Dict{Symbol,String}=Dict{Symbol,String}(), + defaultColor::AbstractString="gray", + autocolor_cb::Function=(histstep,csym,aniT)->(haskey(fsmColors, csym) ? fsmColors[csym] : defaultColor) ) where T + # + # Dict{Symbol, Vector{Symbol}} + stateVisits = Dict{Symbol, Vector{Symbol}}() + allStates = Vector{Symbol}() + for (csym,hist) in hists + stateVisits, allStates = histStateMachineTransitions(hist,allStates=allStates, stateVisits=stateVisits ) + end + + # + vg, lookup = histGraphStateMachineTransitions(stateVisits, allStates) + + # total draw time and step initialization + totT = stopT - startT + totT = Millisecond(round(Int, 1.05*totT.value)) + histsteps = ones(Int, length(hists)) + + # clear any stale state + clearstale ? clearVisGraphAttributes!(vg) : nothing + + # loop across time + @showprogress "exporting state machine images, $title " for i in 1:frames + # calc frame time + aniT = Millisecond(round(Int, i/frames*totT.value)) + startT + + # loop over all state machines + histidx = 0 + for (csym, hist) in hists + histidx += 1 + step = histsteps[histidx] + len = length(hist) + if hist[step][1] < aniT && step < len + histsteps[histidx] += 1 + end + # redefine after +1 + step = histsteps[histidx] + + # modify vg for each history + lbl = getStateLabel(hist[step][3]) + vertid = lookup[lbl] + vertColor=autocolor_cb(hist[step], csym, aniT) + # vertColor=haskey(fsmColors, csym) ? fsmColors[csym] : defaultColor + setVisGraphOnState!(vg, vertid, appendxlabel=string(csym)*",", vertColor=vertColor ) + end + + # finally render one frame + renderStateMachineFrame(vg, + i, + title=title, + show=false, + folder=folder, + timest=string(split(string(aniT),' ')[1]), + rmfirst=false ) + # + clearVisGraphAttributes!(vg) + end + +end + +# count the total number of transitions contained in hists +function getTotalNumberSteps( hists::Dict{Symbol, Vector{Tuple{DateTime, Int, <: Function, T}}} ) where T + totSteps = 0 + for (whId, hist) in hists, hi in hist + totSteps += 1 + end + return totSteps +end + +# point to the start step among all history steps +function getFirstStepHist( hists::Dict{Symbol, Vector{Tuple{DateTime, Int, <: Function, T}}} ) where T + startTime = now() + maxTime = DateTime(0) + # NOTE, this whichId=:null is super important to ensure rendering loop can exit properly + whichId, whichStep = :null, 0 + for (whId, hist) in hists, (st,hi) in enumerate(hist) + if hi[1] < startTime + # new starting point indicator + whichId = whId + whichStep = st + startTime = hi[1] + end + if maxTime < hi[1] + maxTime = hi[1] + end + end + return whichId, whichStep, startTime, maxTime +end + +# give the next step, closest in time and that has not previously been added to `prevList`. +# Also update prevList +function getNextStepHist!(hists, + intuple::Tuple{Symbol, Int, DateTime}, + maxTime::DateTime, + prevList::Dict{Symbol, Vector{Int}} ) + # + oldId, oldStep, oldT = intuple + + whichId, whichStep, newT = :null, 0, maxTime + for (whId, hist) in hists, (st,hi) in enumerate(hist) + # make sure all options are populated in previous list tracker + if !haskey(prevList, whId) prevList[whId] = Int[]; end + if oldT < hi[1] && Millisecond(0) <= (hi[1] - oldT) < (newT-oldT) && + !(st in prevList[whId]) # must be a different step than before + # new closest next step + whichId = whId + whichStep = st + newT = hi[1] + end + end + + # register this step has previously been taken + if !haskey(prevList, whichId) + prevList[whichId] = Int[] + end + push!(prevList[whichId], whichStep) + + return whichId, whichStep, newT +end + + +# for slower movies, use a slower fps +# run(`ffmpeg -r 10 -i /tmp/caesar/csmCompound/csm_%d.png -c:v libtheora -vf fps=5 -pix_fmt yuv420p -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" -q 10 /tmp/caesar/csmCompound/out.ogv`) +# @async run(`totem /tmp/caesar/csmCompound/out.ogv`) +# draw_more_cb(::Tuple, ::Int, ::String) +function animateStateMachineHistoryIntervalCompound(hists::Dict{Symbol, Vector{Tuple{DateTime, Int, <: Function, T}}}; + easyNames::Dict{Symbol,N}=Dict{Symbol,Nothing}(), + interval::Int=2, + folderpath="/tmp/animatestate", + dpi::Int=100, + title::String="", + show::Bool=false, + clearstale::Bool=true, + rmfirst::Bool=true, + draw_more_cb::Function=(x...)->(), + fsmColors::Dict{Symbol,String}=Dict{Symbol,String}(), + defaultColor::AbstractString="red", + autocolor_cb::Function=(histstep,csym,aniT)->(haskey(fsmColors, csym) ? fsmColors[csym] : defaultColor) ) where {T, N} + # + # Dict{Symbol, Vector{Symbol}} + stateVisits = Dict{Symbol, Vector{Symbol}}() + allStates = Vector{Symbol}() + for (csym,hist) in hists + stateVisits, allStates = histStateMachineTransitions(hist,allStates=allStates, stateVisits=stateVisits ) + end + + # + vg, lookup = histGraphStateMachineTransitions(stateVisits, allStates) + + # total draw time and step initialization + # totT = stopT - startT + # totT = Millisecond(round(Int, 1.05*totT.value)) + # histsteps = ones(Int, length(hists)) + + # clear any stale state + clearstale ? clearVisGraphAttributes!(vg) : nothing + + totSteps = getTotalNumberSteps(hists) + whId, fsmStep, aniT, maxTime = getFirstStepHist(hists) + prevList = Dict{Symbol, Vector{Int}}() + latestList = Dict{Symbol, Int}(whId => fsmStep) + + prevT = aniT + frameCount = 0 + # loop across time + @showprogress "exporting state machine images, $title " for stepCount in 1:totSteps + # which step among the hist fsms is next + if 1 < stepCount + # skip first would-be repeat + whId, fsmStep, aniT = getNextStepHist!(hists, (whId, fsmStep, aniT), maxTime, prevList) + latestList[whId] = fsmStep + end + + # loop over all state "known" machines + for (csym, lstep) in latestList + # terminate at end of drawing sequence + csym == :null ? break : nothing + # modify vg for each history + lbl = getStateLabel(hists[csym][lstep][3]) + vertid = lookup[lbl] + vertColor=autocolor_cb(hists[csym][lstep], csym, aniT) + easyn = haskey(easyNames, csym) ? easyNames[csym] : csym + setVisGraphOnState!(vg, vertid, appendxlabel="($easyn.$lstep),", vertColor=vertColor ) + end + + # and draw as many frames for that setup + for itr in 1:interval + # increment frame counter + frameCount += 1 + deltaT = (aniT - prevT).value + prevT = aniT + # finally render one frame + renderStateMachineFrame(vg, + frameCount, + title=title*" || dt=$deltaT ms ||", + show=false, + dpi=dpi, + folderpath=folderpath, + timest=string(split(string(aniT),' ')[1]), + rmfirst=false ) + # + # terminate at end of drawing sequence + whId == :null ? break : nothing + draw_more_cb(hists[whId][fsmStep], frameCount, folderpath) + end + # clear current frame in prep for the next interval + clearVisGraphAttributes!(vg) + end + +end + + +end # module From 75137b1790154bfe2ac32db9f950895e3f2ce777 Mon Sep 17 00:00:00 2001 From: Dehann Fourie <6412556+dehann@users.noreply.github.com> Date: Mon, 3 Jun 2024 22:54:00 -0700 Subject: [PATCH 7/9] Update and rename StateMachineAnimation.jl to WeakdepsPrototypes.jl --- src/StateMachineAnimation.jl | 485 ----------------------------------- src/WeakdepsPrototypes.jl | 41 +++ 2 files changed, 41 insertions(+), 485 deletions(-) delete mode 100644 src/StateMachineAnimation.jl create mode 100644 src/WeakdepsPrototypes.jl diff --git a/src/StateMachineAnimation.jl b/src/StateMachineAnimation.jl deleted file mode 100644 index cf0fde9..0000000 --- a/src/StateMachineAnimation.jl +++ /dev/null @@ -1,485 +0,0 @@ -@info "FunctionalStateMachine.jl is adding Graphs.jl related tools (Visualization)." - -export - histGraphStateMachineTransitions, - drawStateTransitionStep, - drawStateMachineHistory, - animateStateMachineHistoryByTime, - animateStateMachineHistoryByTimeCompound, - animateStateMachineHistoryIntervalCompound - -# bad import warning -# import Graphs: incdict -# incdict(::Type{V}, ::Type{E}; is_directed::Bool = true) where {V,E} = incdict(Dict{Int,V}(), E{V}; is_directed=is_directed) - -""" - $SIGNATURES - -Create a `Graphs.incdict` object and populate with nodes (states) and edges (transitions) -according to the contents of parameters passed in. - -Notes: -- Current implementation repeats duplicate transitions as new edges. -""" -function histGraphStateMachineTransitions(stateVisits, allStates::Vector{Symbol}; - maxpenwidth::Real=5, minpenwidth::Real=0.75 ) - # - g = Graphs.incdict(Dict{Int,Graphs.ExVertex}(), Graphs.ExEdge{Graphs.ExVertex}; is_directed=true) - # g = Graphs.incdict(Graphs.ExVertex,Graphs.ExEdge,is_directed=true) - lookup = Dict{Symbol, Int}() - - # add all required states as nodes to the visualization graph - fid = 0 - for state in allStates - fid += 1 - lbl = string(state) - exvert = Graphs.ExVertex(fid, lbl) - exvert.attributes["label"] = lbl - Graphs.add_vertex!(g, exvert) - lookup[state] = fid - end - - alledges = [] - - # add all edges to graph - maxtransedge = 0 - count = 0 - for (from, tos) in stateVisits - for to in tos - count += 1 - exvf = g.vertices[lookup[from]] - exvt = g.vertices[lookup[to]] - # add the edge from one to the next state - # TODO, don't add if already there. - addedge = true - for oun in Graphs.out_neighbors(exvf, g) - if oun.index == exvt.index - addedge = false - # increase penwidth+=1 on that edge - for ed in Graphs.out_edges(exvf, g) - ed.attributes["penwidth"] += 1 - # ed.attributes["penwidth"] = minimum([maxpenwidth;ed.attributes["penwidth"]]) - maxtransedge = maxtransedge < ed.attributes["penwidth"] ? ed.attributes["penwidth"] : maxtransedge - end - break - end - end - if addedge - edge = Graphs.make_edge(g, exvf, exvt) - edge.attributes["penwidth"] = 1.0 - Graphs.add_edge!(g, edge) - push!(alledges, edge) - end - end - end - - # normalize edge penwidth to maxpenwidth - normwidth = maxtransedge/maxpenwidth - for ed in alledges - ed.attributes["penwidth"] = maximum([minpenwidth; ed.attributes["penwidth"]/normwidth]) - - end - - return g, lookup -end - - -function renderStateMachineFrame( vg, - frame::Int; - title::String="", - viewerapp::String="eog", - fext::String="png", - dpi::Int=100, - engine::String="dot", - show::Bool=true, - folder::String="fsm_animation", - folderpath = "/tmp/$folder/", - timest::String="", - rmfirst::Bool=false ) - # - if rmfirst - @warn "removing contents of $(folderpath)" - Base.rm(folderpath, recursive=true, force=true) - end - mkpath(folderpath) - dotfile = "$folderpath/csm_$frame.dot" - filepath= "$folderpath/csm_$frame.$(fext)" - - # dot file handle - fid = open(dotfile,"w") - write(fid,Graphs.to_dot(vg)) - close(fid) - - # build the dot file somewhat manually - fid = open("$folderpath/dotscript.sh","w") - str = "head -n `wc -l $dotfile | awk '{print \$1-1}'` $dotfile > $folderpath/tmpdot.dot" - println(fid, str) - println(fid, "echo \"graph [label=\\\"$title, $(timest)\\\", labelloc=t];\" >> $folderpath/tmpdot.dot") - println(fid, "echo \"}\" >> $folderpath/tmpdot.dot") - close(fid) - run(`chmod u+x $folderpath/dotscript.sh`) - run(`sh $folderpath/dotscript.sh`) - Base.rm(dotfile) - Base.rm("$folderpath/dotscript.sh") - run(`mv $folderpath/tmpdot.dot $dotfile`) - - # compile output and maybe show to user - run(`$(engine) $(dotfile) -T$(fext) -Gdpi=$dpi -o $(filepath)`) - show ? (@async run(`$viewerapp $filepath`)) : nothing - return filepath -end - -function setVisGraphOnState!( vg, vertid; - xlabel::String="", - appendxlabel::String="", - vertColor::AbstractString="red" ) - # - vg.vertices[vertid].attributes["fillcolor"] = vertColor - vg.vertices[vertid].attributes["style"] = "filled" - if length(xlabel) > 0 - vg.vertices[vertid].attributes["xlabel"] = xlabel - end - if haskey(vg.vertices[vertid].attributes, "xlabel") - vg.vertices[vertid].attributes["xlabel"] = vg.vertices[vertid].attributes["xlabel"]*appendxlabel - elseif length(appendxlabel) > 0 - vg.vertices[vertid].attributes["xlabel"] = appendxlabel - end -end - -function clearVisGraphAttributes!(vg) - for (vid,vert) in vg.vertices - haskey(vert.attributes, "fillcolor") ? delete!(vert.attributes, "fillcolor") : nothing - haskey(vert.attributes, "style") ? delete!(vert.attributes, "style") : nothing - haskey(vert.attributes, "xlabel") ? delete!(vert.attributes, "xlabel") : nothing - end - nothing -end - -function drawStateTransitionStep( hist, - step::Int, - vg, - lookup::Dict{Symbol,Int}; - title::String="", - viewerapp::String="eog", - fext::String="png", - engine::String="dot", - show::Bool=true, - folder::String="", - frame::Int=step, - vertColor::AbstractString="red" ) - # - - lbl = getStateLabel(hist[step][3]) - vertid = lookup[lbl] - vert = vg.vertices[vertid] - - fillcolorbefore = haskey(vert.attributes, "fillcolor") ? deepcopy(vg.vertices[vertid].attributes["fillcolor"]) : nothing - stylebefore = haskey(vert.attributes, "style") ? deepcopy(vg.vertices[vertid].attributes["style"]) : nothing - xlabelbefore = haskey(vg.vertices[vertid].attributes,"xlabel") ? deepcopy(vg.vertices[vertid].attributes["xlabel"]) : nothing - # delete!(vert.attributes, "fillcolor") - # delete!(vert.attributes, "style") - - # identify and set the node - xlabel = length(title) > 0 ? (xlabelbefore !== nothing ? xlabelbefore*"," : "")*title : "" - setVisGraphOnState!(vg, vertid, xlabel=xlabel, vertColor=vertColor ) - - # render state machine frame - filepath = renderStateMachineFrame( vg, - frame, - title=title, - viewerapp=viewerapp, - fext=fext, - engine=engine, - show=show, - folder=folder, - timest=string(split(string(hist[step][1]),'T')[end]), - rmfirst=false) - # - - # clean up the vg structure - fillcolorbefore === nothing ? delete!(vert.attributes, "fillcolor") : (vert.attributes["fillcolor"]=fillcolorbefore) - stylebefore === nothing ? delete!(vert.attributes, "style") : (vert.attributes["style"]=stylebefore) - xlabelbefore === nothing ? delete!(vert.attributes, "xlabel") : (vert.attributes["xlabel"]=xlabelbefore) - - return filepath -end - - - - - -function drawStateMachineHistory(hist; show::Bool=false, folder::String="" ) - - stateVisits, allStates = histStateMachineTransitions(hist) - - vg, lookup = histGraphStateMachineTransitions(stateVisits, allStates) - - for i in 1:length(hist) - drawStateTransitionStep(hist, i, vg, lookup, folder=folder, show=show ) - end - - return nothing -end - - - -""" - $SIGNATURES - -Draw simultaneously separate time synchronized frames from each of the desired -state machines. These images can be produced into synchronous side-by-side videos -which allows for easier debugging and comparison of concurrently running state -machines. -""" -function animateStateMachineHistoryByTime(hist::Vector{Tuple{DateTime, Int, <: Function, T}}; - frames::Int=100, - folder="animatestate", - title::String="", - show::Bool=false, - startT=hist[1][1], - stopT=hist[end][1], - rmfirst::Bool=true, - vertColor::AbstractString="red" ) where T - # - stateVisits, allStates = histStateMachineTransitions(hist) - - vg, lookup = histGraphStateMachineTransitions(stateVisits, allStates) - - totT = stopT - startT - totT = Millisecond(round(Int, 1.05*totT.value)) - # totT *= 1.05 - - step = 1 - len = length(hist) - @showprogress "exporting state machine images, $title " for i in 1:frames - aniT = Millisecond(round(Int, i/frames*totT.value)) + startT - # aniT = i/frames*totT + startT - if hist[step][1] < aniT && step < len - step += 1 - end - drawStateTransitionStep(hist, step, vg, lookup, title=title, folder=folder, show=show, frame=i, vertColor=vertColor ) - end - - nothing -end - -function animateStateMachineHistoryByTimeCompound(hists::Dict{Symbol, Vector{Tuple{DateTime, Int, <: Function, T}}}, - startT, - stopT; - frames::Int=100, - folder="animatestate", - title::String="", - show::Bool=false, - clearstale::Bool=true, - rmfirst::Bool=true, - fsmColors::Dict{Symbol,String}=Dict{Symbol,String}(), - defaultColor::AbstractString="gray", - autocolor_cb::Function=(histstep,csym,aniT)->(haskey(fsmColors, csym) ? fsmColors[csym] : defaultColor) ) where T - # - # Dict{Symbol, Vector{Symbol}} - stateVisits = Dict{Symbol, Vector{Symbol}}() - allStates = Vector{Symbol}() - for (csym,hist) in hists - stateVisits, allStates = histStateMachineTransitions(hist,allStates=allStates, stateVisits=stateVisits ) - end - - # - vg, lookup = histGraphStateMachineTransitions(stateVisits, allStates) - - # total draw time and step initialization - totT = stopT - startT - totT = Millisecond(round(Int, 1.05*totT.value)) - histsteps = ones(Int, length(hists)) - - # clear any stale state - clearstale ? clearVisGraphAttributes!(vg) : nothing - - # loop across time - @showprogress "exporting state machine images, $title " for i in 1:frames - # calc frame time - aniT = Millisecond(round(Int, i/frames*totT.value)) + startT - - # loop over all state machines - histidx = 0 - for (csym, hist) in hists - histidx += 1 - step = histsteps[histidx] - len = length(hist) - if hist[step][1] < aniT && step < len - histsteps[histidx] += 1 - end - # redefine after +1 - step = histsteps[histidx] - - # modify vg for each history - lbl = getStateLabel(hist[step][3]) - vertid = lookup[lbl] - vertColor=autocolor_cb(hist[step], csym, aniT) - # vertColor=haskey(fsmColors, csym) ? fsmColors[csym] : defaultColor - setVisGraphOnState!(vg, vertid, appendxlabel=string(csym)*",", vertColor=vertColor ) - end - - # finally render one frame - renderStateMachineFrame(vg, - i, - title=title, - show=false, - folder=folder, - timest=string(split(string(aniT),' ')[1]), - rmfirst=false ) - # - clearVisGraphAttributes!(vg) - end - -end - -# count the total number of transitions contained in hists -function getTotalNumberSteps( hists::Dict{Symbol, Vector{Tuple{DateTime, Int, <: Function, T}}} ) where T - totSteps = 0 - for (whId, hist) in hists, hi in hist - totSteps += 1 - end - return totSteps -end - -# point to the start step among all history steps -function getFirstStepHist( hists::Dict{Symbol, Vector{Tuple{DateTime, Int, <: Function, T}}} ) where T - startTime = now() - maxTime = DateTime(0) - # NOTE, this whichId=:null is super important to ensure rendering loop can exit properly - whichId, whichStep = :null, 0 - for (whId, hist) in hists, (st,hi) in enumerate(hist) - if hi[1] < startTime - # new starting point indicator - whichId = whId - whichStep = st - startTime = hi[1] - end - if maxTime < hi[1] - maxTime = hi[1] - end - end - return whichId, whichStep, startTime, maxTime -end - -# give the next step, closest in time and that has not previously been added to `prevList`. -# Also update prevList -function getNextStepHist!(hists, - intuple::Tuple{Symbol, Int, DateTime}, - maxTime::DateTime, - prevList::Dict{Symbol, Vector{Int}} ) - # - oldId, oldStep, oldT = intuple - - whichId, whichStep, newT = :null, 0, maxTime - for (whId, hist) in hists, (st,hi) in enumerate(hist) - # make sure all options are populated in previous list tracker - if !haskey(prevList, whId) prevList[whId] = Int[]; end - if oldT < hi[1] && Millisecond(0) <= (hi[1] - oldT) < (newT-oldT) && - !(st in prevList[whId]) # must be a different step than before - # new closest next step - whichId = whId - whichStep = st - newT = hi[1] - end - end - - # register this step has previously been taken - if !haskey(prevList, whichId) - prevList[whichId] = Int[] - end - push!(prevList[whichId], whichStep) - - return whichId, whichStep, newT -end - - -# for slower movies, use a slower fps -# run(`ffmpeg -r 10 -i /tmp/caesar/csmCompound/csm_%d.png -c:v libtheora -vf fps=5 -pix_fmt yuv420p -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" -q 10 /tmp/caesar/csmCompound/out.ogv`) -# @async run(`totem /tmp/caesar/csmCompound/out.ogv`) -# draw_more_cb(::Tuple, ::Int, ::String) -function animateStateMachineHistoryIntervalCompound(hists::Dict{Symbol, Vector{Tuple{DateTime, Int, <: Function, T}}}; - easyNames::Dict{Symbol,N}=Dict{Symbol,Nothing}(), - interval::Int=2, - folderpath="/tmp/animatestate", - dpi::Int=100, - title::String="", - show::Bool=false, - clearstale::Bool=true, - rmfirst::Bool=true, - draw_more_cb::Function=(x...)->(), - fsmColors::Dict{Symbol,String}=Dict{Symbol,String}(), - defaultColor::AbstractString="red", - autocolor_cb::Function=(histstep,csym,aniT)->(haskey(fsmColors, csym) ? fsmColors[csym] : defaultColor) ) where {T, N} - # - # Dict{Symbol, Vector{Symbol}} - stateVisits = Dict{Symbol, Vector{Symbol}}() - allStates = Vector{Symbol}() - for (csym,hist) in hists - stateVisits, allStates = histStateMachineTransitions(hist,allStates=allStates, stateVisits=stateVisits ) - end - - # - vg, lookup = histGraphStateMachineTransitions(stateVisits, allStates) - - # total draw time and step initialization - # totT = stopT - startT - # totT = Millisecond(round(Int, 1.05*totT.value)) - # histsteps = ones(Int, length(hists)) - - # clear any stale state - clearstale ? clearVisGraphAttributes!(vg) : nothing - - totSteps = getTotalNumberSteps(hists) - whId, fsmStep, aniT, maxTime = getFirstStepHist(hists) - prevList = Dict{Symbol, Vector{Int}}() - latestList = Dict{Symbol, Int}(whId => fsmStep) - - prevT = aniT - frameCount = 0 - # loop across time - @showprogress "exporting state machine images, $title " for stepCount in 1:totSteps - # which step among the hist fsms is next - if 1 < stepCount - # skip first would-be repeat - whId, fsmStep, aniT = getNextStepHist!(hists, (whId, fsmStep, aniT), maxTime, prevList) - latestList[whId] = fsmStep - end - - # loop over all state "known" machines - for (csym, lstep) in latestList - # terminate at end of drawing sequence - csym == :null ? break : nothing - # modify vg for each history - lbl = getStateLabel(hists[csym][lstep][3]) - vertid = lookup[lbl] - vertColor=autocolor_cb(hists[csym][lstep], csym, aniT) - easyn = haskey(easyNames, csym) ? easyNames[csym] : csym - setVisGraphOnState!(vg, vertid, appendxlabel="($easyn.$lstep),", vertColor=vertColor ) - end - - # and draw as many frames for that setup - for itr in 1:interval - # increment frame counter - frameCount += 1 - deltaT = (aniT - prevT).value - prevT = aniT - # finally render one frame - renderStateMachineFrame(vg, - frameCount, - title=title*" || dt=$deltaT ms ||", - show=false, - dpi=dpi, - folderpath=folderpath, - timest=string(split(string(aniT),' ')[1]), - rmfirst=false ) - # - # terminate at end of drawing sequence - whId == :null ? break : nothing - draw_more_cb(hists[whId][fsmStep], frameCount, folderpath) - end - # clear current frame in prep for the next interval - clearVisGraphAttributes!(vg) - end - -end \ No newline at end of file diff --git a/src/WeakdepsPrototypes.jl b/src/WeakdepsPrototypes.jl new file mode 100644 index 0000000..f0db3d0 --- /dev/null +++ b/src/WeakdepsPrototypes.jl @@ -0,0 +1,41 @@ + +export + histGraphStateMachineTransitions, + drawStateTransitionStep, + drawStateMachineHistory, + animateStateMachineHistoryByTime, + animateStateMachineHistoryByTimeCompound, + animateStateMachineHistoryIntervalCompound + +""" + $SIGNATURES + +Create a `Graphs.incdict` object and populate with nodes (states) and edges (transitions) +according to the contents of parameters passed in. + +Notes: +- Current implementation repeats duplicate transitions as new edges. +""" +function histGraphStateMachineTransitions end + +function renderStateMachineFrame end +function setVisGraphOnState! end +function drawStateTransitionStep end +function drawStateMachineHistory end + +""" + $SIGNATURES + +Draw simultaneously separate time synchronized frames from each of the desired +state machines. These images can be produced into synchronous side-by-side videos +which allows for easier debugging and comparison of concurrently running state +machines. +""" +function animateStateMachineHistoryByTime end + +function animateStateMachineHistoryByTimeCompound end +function getTotalNumberSteps end +function getFirstStepHist end +function getNextStepHist! end +function animateStateMachineHistoryIntervalCompound end + From 1842c46e02c773c6f3462c5b368c1822347ba178 Mon Sep 17 00:00:00 2001 From: Dehann Fourie <6412556+dehann@users.noreply.github.com> Date: Mon, 3 Jun 2024 22:54:37 -0700 Subject: [PATCH 8/9] Update FunctionalStateMachine.jl --- src/FunctionalStateMachine.jl | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/FunctionalStateMachine.jl b/src/FunctionalStateMachine.jl index 1a61d1b..eff6c2e 100644 --- a/src/FunctionalStateMachine.jl +++ b/src/FunctionalStateMachine.jl @@ -16,10 +16,7 @@ export include("StateMachine.jl") - -#FIXME Graphs here was the old Graphs.jl and needs to be updated to the new Graphs.jl (previously LightGraphs.jl) -# function __init__() -# @require Graphs="86223c79-3864-5bf0-83f7-82e725a168b6" include("StateMachineAnimation.jl") -# end +# FIXME Graphs here was the old Graphs.jl and needs to be updated to the new Graphs.jl (previously LightGraphs.jl) +include("WeakdepsPrototypes.jl") end From 068a0d19c2c408a332f27dcdae98d19132c9c52d Mon Sep 17 00:00:00 2001 From: Dehann Fourie <6412556+dehann@users.noreply.github.com> Date: Mon, 3 Jun 2024 22:55:55 -0700 Subject: [PATCH 9/9] Update CompatHelper.yml --- .github/workflows/CompatHelper.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/CompatHelper.yml b/.github/workflows/CompatHelper.yml index 2f3769c..7e0c722 100644 --- a/.github/workflows/CompatHelper.yml +++ b/.github/workflows/CompatHelper.yml @@ -5,6 +5,7 @@ on: - cron: '30 00 * * *' issues: types: [opened, reopened] + workflow_dispatch: jobs: CompatHelper: