@@ -750,6 +750,213 @@ def plot_precautionary_gaps(
750750
751751 plt .tight_layout ()
752752
753+ def plot_solution_gaps (
754+ truth_solution ,
755+ approx_solutions ,
756+ title : str ,
757+ subtitle : str ,
758+ * ,
759+ m_min : float = 0.0 ,
760+ m_max : float = 30.0 ,
761+ n_points : int = 100 ,
762+ legend : str | list [str ] | None = None ,
763+ ) -> None :
764+ """Plot savings function gaps comparing truth vs approximation(s).
765+
766+ Parameters
767+ ----------
768+ truth_solution : ConsumerSolution
769+ High-precision "truth" solution for comparison
770+ approx_solutions : ConsumerSolution or list[ConsumerSolution]
771+ Approximation solution(s) to compare (single or list for multiple methods)
772+ title : str
773+ Figure title
774+ subtitle : str
775+ Figure subtitle
776+ m_max : float, optional
777+ Maximum market resources for plot range, by default 30.0
778+ n_points : int, optional
779+ Number of points in evaluation grid, by default 100
780+ legend : str or list[str], optional
781+ Legend labels for approximation(s). If None, auto-generates from solution type.
782+
783+ """
784+ # Ensure approx_solutions is a list
785+ if not isinstance (approx_solutions , list ):
786+ approx_solutions = [approx_solutions ]
787+
788+ # Auto-generate legend labels if not provided
789+ if legend is None :
790+ legend = []
791+ for sol in approx_solutions :
792+ if _is_mom_solution (sol ):
793+ legend .append ("MoM Approximation" )
794+ else :
795+ legend .append ("EGM Approximation" )
796+ elif not isinstance (legend , list ):
797+ legend = [legend ]
798+
799+ # Create evaluation grid
800+ #m_min = approx_solutions[0].cFunc.x_list[-2]#truth_solution.mNrmMin
801+ m_grid = np .linspace (m_min + 0.001 , m_max , n_points )
802+
803+ # Compute solution differences
804+ approx_gaps = []
805+ for sol in approx_solutions :
806+ # Use the same optimist as truth solution
807+ gap = truth_solution .cFunc (m_grid ) - sol .cFunc (m_grid )
808+ approx_gaps .append (gap )
809+
810+ _fig , ax = setup_figure (title = title )
811+
812+ # Plot each approximation method
813+ for gap_vals , method_label , sol in zip (
814+ approx_gaps ,
815+ legend ,
816+ approx_solutions ,
817+ strict = False ,
818+ ):
819+ color = get_concept_color (method_label )
820+ linestyle = get_concept_linestyle (method_label )
821+
822+ ax .plot (
823+ m_grid ,
824+ gap_vals ,
825+ label = method_label ,
826+ color = color ,
827+ linewidth = LINE_WIDTH_THICK ,
828+ linestyle = linestyle ,
829+ )
830+
831+ # Extract and plot grid points for this solution
832+ try :
833+ # Use unified grid extraction
834+ grid_points_m , grid_points_c = extract_grid_points (
835+ sol ,
836+ GridType .CONSUMPTION ,
837+ )
838+ # Determine grid boundary based on solution type
839+ if grid_points_m is not None :
840+ if _is_mom_solution (sol ) and len (grid_points_m ) > 1 :
841+ grid_boundary = grid_points_m [- 2 ] # MoM: second-to-last point
842+ else :
843+ grid_boundary = grid_points_m [- 1 ] # EGM: last point
844+ else :
845+ grid_boundary = None
846+
847+ # Plot grid points if successfully extracted
848+ if grid_points_m is not None and grid_points_c is not None :
849+ # Get the gap values at grid point locations by interpolation
850+ gap_at_grid_points = np .interp (grid_points_m , m_grid , gap_vals )
851+
852+ # Plot actual grid points as scatter
853+ _plot_grid_points_scatter (ax , grid_points_m , gap_at_grid_points , color )
854+
855+ # Also plot grid boundary line
856+ if grid_boundary is not None :
857+ ax .axvline (
858+ x = grid_boundary ,
859+ color = "gray" ,
860+ linestyle = LINE_STYLE_DASHED ,
861+ alpha = ALPHA_MEDIUM ,
862+ label = "Grid boundary" ,
863+ )
864+
865+ except (AttributeError , KeyError , IndexError , TypeError ):
866+ # Grid extraction can fail for various solution types or incomplete solutions.
867+ # Continue plotting without grid point markers.
868+ pass
869+
870+ _configure_standard_axes (
871+ ax ,
872+ xlabel = "Normalized Market Resources (m)" ,
873+ ylabel = "Precautionary Saving Gap" ,
874+ subtitle = subtitle ,
875+ )
876+ _add_reference_lines (ax )
877+ #ax.set_ylim(*YLIM_PRECAUTIONARY_GAPS)
878+ _set_xlim_with_padding (ax , m_grid )
879+
880+ plt .tight_layout ()
881+
882+ def average_solution_gaps (
883+ truth_solution ,
884+ approx_solutions ,
885+ title : str ,
886+ subtitle : str ,
887+ * ,
888+ m_min : float = 0.0 ,
889+ m_max : float = 30.0 ,
890+ n_points : int = 100 ,
891+ legend : str | list [str ] | None = None ,
892+ ) -> None :
893+ """Plot savings function gaps comparing truth vs approximation(s).
894+
895+ Parameters
896+ ----------
897+ truth_solution : ConsumerSolution
898+ High-precision "truth" solution for comparison
899+ approx_solutions : ConsumerSolution or list[ConsumerSolution]
900+ Approximation solution(s) to compare (single or list for multiple methods)
901+ title : str
902+ Figure title
903+ subtitle : str
904+ Figure subtitle
905+ m_max : float, optional
906+ Maximum market resources for plot range, by default 30.0
907+ n_points : int, optional
908+ Number of points in evaluation grid, by default 100
909+ legend : str or list[str], optional
910+ Legend labels for approximation(s). If None, auto-generates from solution type.
911+
912+ """
913+ # Ensure approx_solutions is a list
914+ if not isinstance (approx_solutions , list ):
915+ approx_solutions = [approx_solutions ]
916+
917+ approx_gaps_all_solutions = []
918+ # Compute the average error between each pair of grid points for each approximation method
919+ for sol in approx_solutions :
920+ approx_gaps = []
921+ # Extract and plot grid points for this solution
922+ try :
923+ # Use unified grid extraction
924+ grid_points_m , grid_points_c = extract_grid_points (
925+ sol ,
926+ GridType .CONSUMPTION ,
927+ )
928+ # Determine grid boundary based on solution type
929+ if grid_points_m is not None :
930+ if _is_mom_solution (sol ) and len (grid_points_m ) > 1 :
931+ grid_boundary = grid_points_m [- 2 ] # MoM: second-to-last point
932+ else :
933+ grid_boundary = grid_points_m [- 1 ] # EGM: last point
934+ else :
935+ grid_boundary = None
936+
937+ # Plot grid points if successfully extracted
938+ if grid_points_m is not None and grid_points_c is not None :
939+ for left_point , right_point in zip (grid_points_m [0 :- 1 ],grid_points_m [1 :]):
940+ # Contruct a fine grid between this pair of grid points
941+ m_grid = np .linspace (left_point + 0.001 , right_point , n_points )
942+ # Get the mean absolute gap value for this interval
943+ ave_gap = np .max (np .abs (truth_solution .cFunc (m_grid ) - sol .cFunc (m_grid )))
944+ approx_gaps .append (ave_gap )
945+ m_grid = np .linspace (grid_boundary + 0.001 , m_max , n_points )
946+ ave_gap = np .max (np .abs (truth_solution .cFunc (m_grid ) - sol .cFunc (m_grid )))
947+ approx_gaps .append (ave_gap )
948+ if _is_mom_solution (sol ) and len (grid_points_m ) > 1 :
949+ approx_gaps = np .append (approx_gaps [1 :- 2 ],approx_gaps [- 1 ]) # MoM: second-to-last point
950+ else :
951+ approx_gaps = approx_gaps [1 :] # EGM: last point
952+ approx_gaps_all_solutions .append (approx_gaps )
953+
954+ except (AttributeError , KeyError , IndexError , TypeError ):
955+ # Grid extraction can fail for various solution types or incomplete solutions.
956+ # Continue plotting without grid point markers.
957+ pass
958+
959+ return approx_gaps_all_solutions
753960
754961def plot_consumption_bounds (
755962 solution ,
0 commit comments