@@ -109,9 +109,9 @@ indices of locations visited by that route (excluding the depot).
109109function plot_routes (
110110 state:: DVSPState ,
111111 routes:: Vector{Vector{Int}} ;
112- route_colors= nothing ,
113- route_linewidth= 3 , # Increased from 2 to 3
114- route_alpha= 0.7 ,
112+ # route_colors=nothing,
113+ route_linewidth= 2 , # Increased from 2 to 3
114+ # route_alpha=0.7,
115115 show_route_labels= true ,
116116 kwargs... ,
117117)
@@ -128,9 +128,9 @@ function plot_routes(
128128 y_depot = y[1 ]
129129
130130 # Default route colors if not provided
131- if isnothing (route_colors)
132- route_colors = [:blue , :purple , :orange , :brown , :pink , :gray , :olive , :cyan ]
133- end
131+ # if isnothing(route_colors)
132+ # route_colors = [:blue, :purple, :orange, :brown, :pink, :gray, :olive, :cyan]
133+ # end
134134
135135 # Plot each route
136136 for (route_idx, route) in enumerate (routes)
@@ -140,15 +140,15 @@ function plot_routes(
140140 route_y = vcat (y_depot, y[route], y_depot)
141141
142142 # Select color for this route
143- color = route_colors[(route_idx - 1 ) % length (route_colors) + 1 ]
143+ # color = route_colors[(route_idx - 1) % length(route_colors) + 1]
144144
145145 # Plot the route with more visible styling
146146 label = show_route_labels ? " Route $route_idx " : nothing
147147 plot! (
148148 fig,
149149 route_x,
150150 route_y;
151- # color=color ,
151+ # color=:black ,
152152 linewidth= route_linewidth,
153153 alpha= 1.0 , # Make routes fully opaque
154154 label= label,
@@ -674,3 +674,267 @@ function animate_epochs(
674674
675675 return anim
676676end
677+
678+ """
679+ Animate multiple solutions where each solution is provided as its own vector of
680+ `DataSample` objects (one per epoch). This treats each solution's `DataSample`
681+ as the canonical source for that column in the side-by-side animation.
682+ """
683+ function animate_solutions_side_by_side (
684+ solutions_data_samples:: AbstractVector ;
685+ solution_names= nothing ,
686+ filename= " dvsp_solutions_side_by_side.gif" ,
687+ fps= 1 ,
688+ figsize= (1200 , 600 ),
689+ margin= 0.1 ,
690+ legend_margin_factor= 0.15 ,
691+ titlefontsize= 14 ,
692+ guidefontsize= 12 ,
693+ legendfontsize= 11 ,
694+ tickfontsize= 10 ,
695+ show_axis_labels= true ,
696+ show_cost_bar= true ,
697+ cost_bar_width= 0.05 ,
698+ cost_bar_margin= 0.02 ,
699+ cost_bar_color_palette= :turbo ,
700+ kwargs... ,
701+ )
702+ n_solutions = length (solutions_data_samples)
703+ if n_solutions == 0
704+ error (" No solutions provided" )
705+ end
706+
707+ # Ensure all solution sequences have the same number of epochs
708+ n_epochs = length (solutions_data_samples[1 ])
709+ for (i, s) in enumerate (solutions_data_samples)
710+ if length (s) != n_epochs
711+ error (
712+ " All solution DataSample vectors must have the same length. Solution $i has length $(length (s)) but expected $n_epochs " ,
713+ )
714+ end
715+ end
716+
717+ if isnothing (solution_names)
718+ solution_names = [" Solution $(i) " for i in 1 : n_solutions]
719+ end
720+
721+ # Collect global coordinates and start times across all solutions/epochs
722+ all_coordinates = []
723+ all_start_times = []
724+ epoch_costs_per_solution = [Float64[] for _ in 1 : n_solutions]
725+
726+ for j in 1 : n_solutions
727+ samples = solutions_data_samples[j]
728+ for (t, sample) in enumerate (samples)
729+ if ! isnothing (sample. instance)
730+ append! (all_coordinates, coordinate (sample. instance))
731+ append! (all_start_times, start_time (sample. instance))
732+
733+ if sample. y_true isa BitMatrix
734+ routes = decode_bitmatrix_to_routes (sample. y_true)
735+ else
736+ routes = sample. y_true isa Vector{Int} ? [sample. y_true] : sample. y_true
737+ end
738+ c = isnothing (routes) ? 0.0 : cost (sample. instance, routes)
739+ push! (epoch_costs_per_solution[j], c)
740+ else
741+ push! (epoch_costs_per_solution[j], NaN )
742+ end
743+ end
744+ end
745+
746+ if isempty (all_coordinates)
747+ error (" No valid coordinates found in solution data samples" )
748+ end
749+
750+ # Global limits
751+ xlims = (
752+ minimum (p. x for p in all_coordinates) - margin,
753+ maximum (p. x for p in all_coordinates) + margin,
754+ )
755+
756+ y_min = minimum (p. y for p in all_coordinates) - margin
757+ y_max = maximum (p. y for p in all_coordinates) + margin
758+ y_range = y_max - y_min
759+ legend_margin = y_range * legend_margin_factor
760+ ylims = (y_min, y_max + legend_margin)
761+
762+ clims = if ! isempty (all_start_times)
763+ (minimum (all_start_times), maximum (all_start_times))
764+ else
765+ (0.0 , 1.0 )
766+ end
767+
768+ # Robust cumulative costs per solution
769+ robust_cumulative = Vector {Vector{Float64}} (undef, n_solutions)
770+ for j in 1 : n_solutions
771+ robust = Float64[]
772+ s = 0.0
773+ for c in epoch_costs_per_solution[j]
774+ if ! isnan (c)
775+ s += c
776+ end
777+ push! (robust, s)
778+ end
779+ robust_cumulative[j] = robust
780+ end
781+
782+ function has_routes_local (routes)
783+ if isnothing (routes)
784+ return false
785+ elseif routes isa Vector{Vector{Int}}
786+ return any (! isempty (route) for route in routes)
787+ elseif routes isa Vector{Int}
788+ return ! isempty (routes)
789+ elseif routes isa BitMatrix
790+ return any (routes)
791+ else
792+ return false
793+ end
794+ end
795+
796+ anim = @animate for t in 1 : n_epochs
797+ col_plots = []
798+ for j in 1 : n_solutions
799+ sample = solutions_data_samples[j][t]
800+ state = sample. instance
801+ routes = sample. y_true
802+
803+ if isnothing (state)
804+ fig = plot (;
805+ xlims= xlims,
806+ ylims= ylims,
807+ title= " $(solution_names[j]) - Epoch $t (No Data)" ,
808+ titlefontsize= titlefontsize,
809+ guidefontsize= guidefontsize,
810+ tickfontsize= tickfontsize,
811+ legend= false ,
812+ kwargs... ,
813+ )
814+ else
815+ if has_routes_local (routes)
816+ fig = plot_routes (
817+ state,
818+ routes;
819+ xlims= xlims,
820+ ylims= ylims,
821+ clims= clims,
822+ title= " $(solution_names[j]) - Epoch $(state. current_epoch) " ,
823+ titlefontsize= titlefontsize,
824+ guidefontsize= guidefontsize,
825+ legendfontsize= legendfontsize,
826+ tickfontsize= tickfontsize,
827+ show_axis_labels= show_axis_labels,
828+ markerstrokewidth= 0.5 ,
829+ show_route_labels= false ,
830+ show_colorbar= false ,
831+ size= (floor (Int, figsize[1 ] / n_solutions), figsize[2 ]),
832+ kwargs... ,
833+ )
834+ else
835+ fig = plot_state (
836+ state;
837+ xlims= xlims,
838+ ylims= ylims,
839+ clims= clims,
840+ title= " $(solution_names[j]) - Epoch $(state. current_epoch) " ,
841+ titlefontsize= titlefontsize,
842+ guidefontsize= guidefontsize,
843+ legendfontsize= legendfontsize,
844+ tickfontsize= tickfontsize,
845+ show_axis_labels= show_axis_labels,
846+ markerstrokewidth= 0.5 ,
847+ size= (floor (Int, figsize[1 ] / n_solutions), figsize[2 ]),
848+ kwargs... ,
849+ )
850+ end
851+ end
852+
853+ # cost bar
854+ if show_cost_bar
855+ current_cost = robust_cumulative[j][t]
856+ max_cost = maximum ([robust_cumulative[k][end ] for k in 1 : n_solutions])
857+
858+ x_min, x_max = xlims
859+ x_range = x_max - x_min
860+ bar_x_start = x_max - cost_bar_width * x_range
861+ bar_x_end = x_max - cost_bar_margin * x_range
862+
863+ y_min, y_max = ylims
864+ y_range = y_max - y_min
865+ bar_y_start = y_min + 0.1 * y_range
866+ bar_y_end = y_max - 0.1 * y_range
867+ bar_height = bar_y_end - bar_y_start
868+
869+ if max_cost > 0
870+ filled_height = (current_cost / max_cost) * bar_height
871+ else
872+ filled_height = 0.0
873+ end
874+
875+ plot! (
876+ fig,
877+ [bar_x_start, bar_x_end, bar_x_end, bar_x_start, bar_x_start],
878+ [bar_y_start, bar_y_start, bar_y_end, bar_y_end, bar_y_start];
879+ seriestype= :shape ,
880+ color= :white ,
881+ alpha= 0.8 ,
882+ linecolor= :black ,
883+ linewidth= 2 ,
884+ label= " " ,
885+ )
886+
887+ if filled_height > 0
888+ cmap = Plots. cgrad (cost_bar_color_palette)
889+ ratio = max_cost > 0 ? current_cost / max_cost : 0.0
890+ color_at_val = Plots. get (cmap, ratio)
891+ plot! (
892+ fig,
893+ [bar_x_start, bar_x_end, bar_x_end, bar_x_start, bar_x_start],
894+ [
895+ bar_y_start,
896+ bar_y_start,
897+ bar_y_start + filled_height,
898+ bar_y_start + filled_height,
899+ bar_y_start,
900+ ];
901+ seriestype= :shape ,
902+ color= color_at_val,
903+ alpha= 0.7 ,
904+ linecolor= :darkred ,
905+ linewidth= 1 ,
906+ label= " " ,
907+ )
908+ end
909+
910+ cost_text_y = bar_y_start + filled_height + 0.02 * y_range
911+ if cost_text_y > bar_y_end
912+ cost_text_y = bar_y_end
913+ end
914+ plot! (
915+ fig,
916+ [bar_x_start + (bar_x_end - bar_x_start) / 2 ],
917+ [cost_text_y];
918+ seriestype= :scatter ,
919+ markersize= 0 ,
920+ label= " " ,
921+ annotations= (
922+ bar_x_start - 0.04 * x_range,
923+ cost_text_y,
924+ (@sprintf (" %.1f" , current_cost), :center , guidefontsize),
925+ ),
926+ )
927+ end
928+
929+ push! (col_plots, fig)
930+ end
931+
932+ combined = plot (
933+ col_plots... ; layout= (1 , n_solutions), size= figsize, link= :both , clims= clims
934+ )
935+ combined
936+ end
937+
938+ gif (anim, filename; fps= fps)
939+ return anim
940+ end
0 commit comments