def _build_primal_rom(self): if self.reductor_type == 'simple_coercive': print('building simple coercive primal reductor...') primal_reductor = SimpleCoerciveRBReductor( self.fom.primal_model, RB=self.RBPrimal, product=self.opt_product, coercivity_estimator=self.coercivity_estimator) elif self.reductor_type == 'non_assembled': print('building non assembled for primal reductor...') primal_reductor = NonAssembledCoerciveRBReductor( self.fom.primal_model, RB=self.RBPrimal, product=self.opt_product, coercivity_estimator=self.coercivity_estimator) else: print('building coercive primal reductor...') primal_reductor = CoerciveRBReductor( self.fom.primal_model, RB=self.RBPrimal, product=self.opt_product, coercivity_estimator=self.coercivity_estimator) primal_rom = primal_reductor.reduce() return primal_rom, primal_reductor
def _build_dual_models(self): assert self.primal_rom is not None assert self.RBPrimal is not None RBbasis = self.RBPrimal rhs_operators = list( self.fom.output_functional_dict['d_u_linear_part'].operators) rhs_coefficients = list( self.fom.output_functional_dict['d_u_linear_part'].coefficients) bilinear_part = self.fom.output_functional_dict['d_u_bilinear_part'] for i in range(len(RBbasis)): u = RBbasis[i] if isinstance(bilinear_part, LincombOperator): for j, op in enumerate(bilinear_part.operators): rhs_operators.append(VectorOperator(op.apply(u))) rhs_coefficients.append( ExpressionParameterFunctional( 'basis_coefficients[{}]'.format(i), {'basis_coefficients': len(RBbasis)}) * bilinear_part.coefficients[j]) else: rhs_operators.append( VectorOperator(bilinear_part.apply(u, None))) rhs_coefficients.append(1. * ExpressionParameterFunctional( 'basis_coefficients[{}]'.format(i), {'basis_coefficients': len(RBbasis)})) dual_rhs_operator = LincombOperator(rhs_operators, rhs_coefficients) dual_intermediate_fom = self.fom.primal_model.with_( rhs=dual_rhs_operator) if self.reductor_type == 'simple_coercive': print('building simple coercive dual reductor...') dual_reductor = SimpleCoerciveRBReductor( dual_intermediate_fom, RB=self.RBDual, product=self.opt_product, coercivity_estimator=self.coercivity_estimator) elif self.reductor_type == 'non_assembled': print('building non assembled dual reductor...') dual_reductor = NonAssembledCoerciveRBReductor( dual_intermediate_fom, RB=self.RBDual, product=self.opt_product, coercivity_estimator=self.coercivity_estimator) else: print('building coercive dual reductor...') dual_reductor = CoerciveRBReductor( dual_intermediate_fom, RB=self.RBDual, product=self.opt_product, coercivity_estimator=self.coercivity_estimator) dual_rom = dual_reductor.reduce() return dual_intermediate_fom, dual_rom, dual_reductor
def thermalblock_demo(args): args['--grid'] = int(args['--grid']) args['RBSIZE'] = int(args['RBSIZE']) args['--test'] = int(args['--test']) args['--ipython-engines'] = int(args['--ipython-engines']) args['--extension-alg'] = args['--extension-alg'].lower() assert args['--extension-alg'] in {'trivial', 'gram_schmidt'} args['--product'] = args['--product'].lower() assert args['--product'] in {'trivial', 'h1'} args['--reductor'] = args['--reductor'].lower() assert args['--reductor'] in {'traditional', 'residual_basis'} args['--cache-region'] = args['--cache-region'].lower() args['--validation-mus'] = int(args['--validation-mus']) args['--rho'] = float(args['--rho']) args['--gamma'] = float(args['--gamma']) args['--theta'] = float(args['--theta']) problem = thermal_block_problem(num_blocks=(2, 2)) functionals = [ ExpressionParameterFunctional('diffusion[0]', {'diffusion': 2}), ExpressionParameterFunctional('diffusion[1]**2', {'diffusion': 2}), ExpressionParameterFunctional('diffusion[0]', {'diffusion': 2}), ExpressionParameterFunctional('diffusion[1]', {'diffusion': 2}) ] problem = problem.with_( diffusion=problem.diffusion.with_(coefficients=functionals), ) print('Discretize ...') fom, _ = discretize_stationary_cg(problem, diameter=1. / args['--grid']) if args['--list-vector-array']: from pymor.discretizers.builtin.list import convert_to_numpy_list_vector_array fom = convert_to_numpy_list_vector_array(fom) if args['--cache-region'] != 'none': # building a cache_id is only needed for persistent CacheRegions cache_id = f"pymordemos.thermalblock_adaptive {args['--grid']}" fom.enable_caching(args['--cache-region'], cache_id) if args['--plot-solutions']: print('Showing some solutions') Us = () legend = () for mu in problem.parameter_space.sample_randomly(2): print(f"Solving for diffusion = \n{mu['diffusion']} ... ") sys.stdout.flush() Us = Us + (fom.solve(mu), ) legend = legend + (str(mu['diffusion']), ) fom.visualize(Us, legend=legend, title='Detailed Solutions for different parameters', block=True) print('RB generation ...') product = fom.h1_0_semi_product if args['--product'] == 'h1' else None coercivity_estimator = ExpressionParameterFunctional( 'min([diffusion[0], diffusion[1]**2])', fom.parameters) reductors = { 'residual_basis': CoerciveRBReductor(fom, product=product, coercivity_estimator=coercivity_estimator), 'traditional': SimpleCoerciveRBReductor(fom, product=product, coercivity_estimator=coercivity_estimator) } reductor = reductors[args['--reductor']] pool = new_parallel_pool(ipython_num_engines=args['--ipython-engines'], ipython_profile=args['--ipython-profile']) greedy_data = rb_adaptive_greedy( fom, reductor, problem.parameter_space, validation_mus=args['--validation-mus'], rho=args['--rho'], gamma=args['--gamma'], theta=args['--theta'], use_estimator=not args['--without-estimator'], error_norm=fom.h1_0_semi_norm, max_extensions=args['RBSIZE'], visualize=not args['--no-visualize-refinement']) rom = greedy_data['rom'] if args['--pickle']: print( f"\nWriting reduced model to file {args['--pickle']}_reduced ...") with open(args['--pickle'] + '_reduced', 'wb') as f: dump(rom, f) print( f"Writing detailed model and reductor to file {args['--pickle']}_detailed ..." ) with open(args['--pickle'] + '_detailed', 'wb') as f: dump((fom, reductor), f) print('\nSearching for maximum error on random snapshots ...') results = reduction_error_analysis( rom, fom=fom, reductor=reductor, estimator=True, error_norms=(fom.h1_0_semi_norm, ), condition=True, test_mus=problem.parameter_space.sample_randomly(args['--test']), basis_sizes=25 if args['--plot-error-sequence'] else 1, plot=True, pool=pool) real_rb_size = rom.solution_space.dim print(''' *** RESULTS *** Problem: number of blocks: 2x2 h: sqrt(2)/{args[--grid]} Greedy basis generation: estimator disabled: {args[--without-estimator]} extension method: {args[--extension-alg]} product: {args[--product]} prescribed basis size: {args[RBSIZE]} actual basis size: {real_rb_size} elapsed time: {greedy_data[time]} '''.format(**locals())) print(results['summary']) sys.stdout.flush() if args['--plot-error-sequence']: from matplotlib import pyplot as plt plt.show(results['figure']) if args['--plot-err']: mumax = results['max_error_mus'][0, -1] U = fom.solve(mumax) URB = reductor.reconstruct(rom.solve(mumax)) fom.visualize( (U, URB, U - URB), legend=('Detailed Solution', 'Reduced Solution', 'Error'), title='Maximum Error Solution', separate_colorbars=True, block=True)
def main(args): args = parse_arguments(args) pool = new_parallel_pool(ipython_num_engines=args['--ipython-engines'], ipython_profile=args['--ipython-profile']) if args['--fenics']: fom, fom_summary = discretize_fenics(args['XBLOCKS'], args['YBLOCKS'], args['--grid'], args['--order']) else: fom, fom_summary = discretize_pymor(args['XBLOCKS'], args['YBLOCKS'], args['--grid'], args['--list-vector-array']) if args['--cache-region'] != 'none': # building a cache_id is only needed for persistent CacheRegions cache_id = (f"pymordemos.thermalblock {args['--fenics']} {args['XBLOCKS']} {args['YBLOCKS']}" f"{args['--grid']} {args['--order']}") fom.enable_caching(args['--cache-region'], cache_id) if args['--plot-solutions']: print('Showing some solutions') Us = () legend = () for mu in fom.parameter_space.sample_randomly(2): print(f"Solving for diffusion = \n{mu['diffusion']} ... ") sys.stdout.flush() Us = Us + (fom.solve(mu),) legend = legend + (str(mu['diffusion']),) fom.visualize(Us, legend=legend, title='Detailed Solutions for different parameters', separate_colorbars=False, block=True) print('RB generation ...') # define estimator for coercivity constant from pymor.parameters.functionals import ExpressionParameterFunctional coercivity_estimator = ExpressionParameterFunctional('min(diffusion)', fom.parameter_type) # inner product for computation of Riesz representatives product = fom.h1_0_semi_product if args['--product'] == 'h1' else None if args['--reductor'] == 'residual_basis': from pymor.reductors.coercive import CoerciveRBReductor reductor = CoerciveRBReductor(fom, product=product, coercivity_estimator=coercivity_estimator, check_orthonormality=False) elif args['--reductor'] == 'traditional': from pymor.reductors.coercive import SimpleCoerciveRBReductor reductor = SimpleCoerciveRBReductor(fom, product=product, coercivity_estimator=coercivity_estimator, check_orthonormality=False) else: assert False # this should never happen if args['--alg'] == 'naive': rom, red_summary = reduce_naive(fom=fom, reductor=reductor, basis_size=args['RBSIZE']) elif args['--alg'] == 'greedy': parallel = not (args['--fenics'] and args['--greedy-without-estimator']) # cannot pickle FEniCS model rom, red_summary = reduce_greedy(fom=fom, reductor=reductor, snapshots_per_block=args['SNAPSHOTS'], extension_alg_name=args['--extension-alg'], max_extensions=args['RBSIZE'], use_estimator=not args['--greedy-without-estimator'], pool=pool if parallel else None) elif args['--alg'] == 'adaptive_greedy': parallel = not (args['--fenics'] and args['--greedy-without-estimator']) # cannot pickle FEniCS model rom, red_summary = reduce_adaptive_greedy(fom=fom, reductor=reductor, validation_mus=args['SNAPSHOTS'], extension_alg_name=args['--extension-alg'], max_extensions=args['RBSIZE'], use_estimator=not args['--greedy-without-estimator'], rho=args['--adaptive-greedy-rho'], gamma=args['--adaptive-greedy-gamma'], theta=args['--adaptive-greedy-theta'], pool=pool if parallel else None) elif args['--alg'] == 'pod': rom, red_summary = reduce_pod(fom=fom, reductor=reductor, snapshots_per_block=args['SNAPSHOTS'], basis_size=args['RBSIZE']) else: assert False # this should never happen if args['--pickle']: print(f"\nWriting reduced model to file {args['--pickle']}_reduced ...") with open(args['--pickle'] + '_reduced', 'wb') as f: dump(rom, f) if not args['--fenics']: # FEniCS data structures do not support serialization print(f"Writing detailed model and reductor to file {args['--pickle']}_detailed ...") with open(args['--pickle'] + '_detailed', 'wb') as f: dump((fom, reductor), f) print('\nSearching for maximum error on random snapshots ...') results = reduction_error_analysis(rom, fom=fom, reductor=reductor, estimator=True, error_norms=(fom.h1_0_semi_norm, fom.l2_norm), condition=True, test_mus=args['--test'], basis_sizes=0 if args['--plot-error-sequence'] else 1, plot=args['--plot-error-sequence'], pool=None if args['--fenics'] else pool, # cannot pickle FEniCS model random_seed=999) print('\n*** RESULTS ***\n') print(fom_summary) print(red_summary) print(results['summary']) sys.stdout.flush() if args['--plot-error-sequence']: import matplotlib.pyplot matplotlib.pyplot.show(results['figure']) if args['--plot-err']: mumax = results['max_error_mus'][0, -1] U = fom.solve(mumax) URB = reductor.reconstruct(rom.solve(mumax)) fom.visualize((U, URB, U - URB), legend=('Detailed Solution', 'Reduced Solution', 'Error'), title='Maximum Error Solution', separate_colorbars=True, block=True) return results
def main( xblocks: int = Argument(..., help='Number of blocks in x direction.'), yblocks: int = Argument(..., help='Number of blocks in y direction.'), snapshots: int = Argument( ..., help='naive: ignored\n\n' 'greedy/pod: Number of training_set parameters per block ' '(in total SNAPSHOTS^(XBLOCKS * YBLOCKS) parameters).\n\n' 'adaptive_greedy: size of validation set.\n\n'), rbsize: int = Argument(..., help='Size of the reduced basis.'), adaptive_greedy_gamma: float = Option( 0.2, help='See pymor.algorithms.adaptivegreedy.'), adaptive_greedy_rho: float = Option( 1.1, help='See pymor.algorithms.adaptivegreedy.'), adaptive_greedy_theta: float = Option( 0., help='See pymor.algorithms.adaptivegreedy.'), alg: Choices('naive greedy adaptive_greedy pod') = Option( 'greedy', help='The model reduction algorithm to use.'), cache_region: Choices('none memory disk persistent') = Option( 'none', help='Name of cache region to use for caching solution snapshots.'), extension_alg: Choices('trivial gram_schmidt') = Option( 'gram_schmidt', help='Basis extension algorithm to be used.'), fenics: bool = Option(False, help='Use FEniCS model.'), greedy_with_error_estimator: bool = Option( True, help='Use error estimator for basis generation.'), grid: int = Option(100, help='Use grid with 4*NI*NI elements'), ipython_engines: int = Option( None, help='If positive, the number of IPython cluster engines to use for ' 'parallel greedy search. If zero, no parallelization is performed.'), ipython_profile: str = Option( None, help='IPython profile to use for parallelization.'), list_vector_array: bool = Option( False, help= 'Solve using ListVectorArray[NumpyVector] instead of NumpyVectorArray.' ), order: int = Option( 1, help= 'Polynomial order of the Lagrange finite elements to use in FEniCS.'), pickle: str = Option( None, help= 'Pickle reduced model, as well as reductor and high-dimensional model ' 'to files with this prefix.'), product: Choices('euclidean h1') = Option( 'h1', help= 'Product w.r.t. which to orthonormalize and calculate Riesz representatives.' ), plot_err: bool = Option(False, help='Plot error'), plot_error_sequence: bool = Option( False, help='Plot reduction error vs. basis size.'), plot_solutions: bool = Option(False, help='Plot some example solutions.'), reductor: Choices('traditional residual_basis') = Option( 'residual_basis', help='Reductor (error estimator) to choose.'), test: int = Option( 10, help='Use COUNT snapshots for stochastic error estimation.'), ): """Thermalblock demo.""" if fenics and cache_region != 'none': raise ValueError( 'Caching of high-dimensional solutions is not supported for FEniCS model.' ) if not fenics and order != 1: raise ValueError( 'Higher-order finite elements only supported for FEniCS model.') pool = new_parallel_pool(ipython_num_engines=ipython_engines, ipython_profile=ipython_profile) if fenics: fom, fom_summary = discretize_fenics(xblocks, yblocks, grid, order) else: fom, fom_summary = discretize_pymor(xblocks, yblocks, grid, list_vector_array) parameter_space = fom.parameters.space(0.1, 1.) if cache_region != 'none': # building a cache_id is only needed for persistent CacheRegions cache_id = (f"pymordemos.thermalblock {fenics} {xblocks} {yblocks}" f"{grid} {order}") fom.enable_caching(cache_region.value, cache_id) if plot_solutions: print('Showing some solutions') Us = () legend = () for mu in parameter_space.sample_randomly(2): print(f"Solving for diffusion = \n{mu['diffusion']} ... ") sys.stdout.flush() Us = Us + (fom.solve(mu), ) legend = legend + (str(mu['diffusion']), ) fom.visualize(Us, legend=legend, title='Detailed Solutions for different parameters', separate_colorbars=False, block=True) print('RB generation ...') # define estimator for coercivity constant from pymor.parameters.functionals import ExpressionParameterFunctional coercivity_estimator = ExpressionParameterFunctional( 'min(diffusion)', fom.parameters) # inner product for computation of Riesz representatives product = fom.h1_0_semi_product if product == 'h1' else None if reductor == 'residual_basis': from pymor.reductors.coercive import CoerciveRBReductor reductor = CoerciveRBReductor( fom, product=product, coercivity_estimator=coercivity_estimator, check_orthonormality=False) elif reductor == 'traditional': from pymor.reductors.coercive import SimpleCoerciveRBReductor reductor = SimpleCoerciveRBReductor( fom, product=product, coercivity_estimator=coercivity_estimator, check_orthonormality=False) else: assert False # this should never happen if alg == 'naive': rom, red_summary = reduce_naive(fom=fom, reductor=reductor, parameter_space=parameter_space, basis_size=rbsize) elif alg == 'greedy': parallel = greedy_with_error_estimator or not fenics # cannot pickle FEniCS model rom, red_summary = reduce_greedy( fom=fom, reductor=reductor, parameter_space=parameter_space, snapshots_per_block=snapshots, extension_alg_name=extension_alg.value, max_extensions=rbsize, use_error_estimator=greedy_with_error_estimator, pool=pool if parallel else None) elif alg == 'adaptive_greedy': parallel = greedy_with_error_estimator or not fenics # cannot pickle FEniCS model rom, red_summary = reduce_adaptive_greedy( fom=fom, reductor=reductor, parameter_space=parameter_space, validation_mus=snapshots, extension_alg_name=extension_alg.value, max_extensions=rbsize, use_error_estimator=greedy_with_error_estimator, rho=adaptive_greedy_rho, gamma=adaptive_greedy_gamma, theta=adaptive_greedy_theta, pool=pool if parallel else None) elif alg == 'pod': rom, red_summary = reduce_pod(fom=fom, reductor=reductor, parameter_space=parameter_space, snapshots_per_block=snapshots, basis_size=rbsize) else: assert False # this should never happen if pickle: print(f"\nWriting reduced model to file {pickle}_reduced ...") with open(pickle + '_reduced', 'wb') as f: dump((rom, parameter_space), f) if not fenics: # FEniCS data structures do not support serialization print( f"Writing detailed model and reductor to file {pickle}_detailed ..." ) with open(pickle + '_detailed', 'wb') as f: dump((fom, reductor), f) print('\nSearching for maximum error on random snapshots ...') results = reduction_error_analysis( rom, fom=fom, reductor=reductor, error_estimator=True, error_norms=(fom.h1_0_semi_norm, fom.l2_norm), condition=True, test_mus=parameter_space.sample_randomly(test, seed=999), basis_sizes=0 if plot_error_sequence else 1, plot=plot_error_sequence, pool=None if fenics else pool # cannot pickle FEniCS model ) print('\n*** RESULTS ***\n') print(fom_summary) print(red_summary) print(results['summary']) sys.stdout.flush() if plot_error_sequence: import matplotlib.pyplot matplotlib.pyplot.show() if plot_err: mumax = results['max_error_mus'][0, -1] U = fom.solve(mumax) URB = reductor.reconstruct(rom.solve(mumax)) fom.visualize( (U, URB, U - URB), legend=('Detailed Solution', 'Reduced Solution', 'Error'), title='Maximum Error Solution', separate_colorbars=True, block=True) global test_results test_results = results
def main( rbsize: int = Argument(..., help='Size of the reduced basis.'), cache_region: Choices('none memory disk persistent') = Option( 'none', help='Name of cache region to use for caching solution snapshots.' ), error_estimator: bool = Option(True, help='Use error estimator for basis generation.'), gamma: float = Option(0.2, help='Weight factor for age penalty term in refinement indicators.'), grid: int = Option(100, help='Use grid with 2*NI*NI elements.'), ipython_engines: int = Option( 0, help='If positive, the number of IPython cluster engines to use for parallel greedy search. ' 'If zero, no parallelization is performed.' ), ipython_profile: str = Option(None, help='IPython profile to use for parallelization.'), list_vector_array: bool = Option( False, help='Solve using ListVectorArray[NumpyVector] instead of NumpyVectorArray.' ), pickle: str = Option( None, help='Pickle reduced discretization, as well as reductor and high-dimensional model to files with this prefix.' ), plot_err: bool = Option(False, help='Plot error.'), plot_solutions: bool = Option(False, help='Plot some example solutions.'), plot_error_sequence: bool = Option(False, help='Plot reduction error vs. basis size.'), product: Choices('euclidean h1') = Option( 'h1', help='Product w.r.t. which to orthonormalize and calculate Riesz representatives.' ), reductor: Choices('traditional residual_basis') = Option( 'residual_basis', help='Reductor (error estimator) to choose (traditional, residual_basis).' ), rho: float = Option(1.1, help='Maximum allowed ratio between error on validation set and on training set.'), test: int = Option(10, help='Use COUNT snapshots for stochastic error estimation.'), theta: float = Option(0., help='Ratio of elements to refine.'), validation_mus: int = Option(0, help='Size of validation set.'), visualize_refinement: bool = Option(True, help='Visualize the training set refinement indicators.'), ): """Modified thermalblock demo using adaptive greedy basis generation algorithm.""" problem = thermal_block_problem(num_blocks=(2, 2)) functionals = [ExpressionParameterFunctional('diffusion[0]', {'diffusion': 2}), ExpressionParameterFunctional('diffusion[1]**2', {'diffusion': 2}), ExpressionParameterFunctional('diffusion[0]', {'diffusion': 2}), ExpressionParameterFunctional('diffusion[1]', {'diffusion': 2})] problem = problem.with_( diffusion=problem.diffusion.with_(coefficients=functionals), ) print('Discretize ...') fom, _ = discretize_stationary_cg(problem, diameter=1. / grid) if list_vector_array: from pymor.discretizers.builtin.list import convert_to_numpy_list_vector_array fom = convert_to_numpy_list_vector_array(fom) if cache_region != 'none': # building a cache_id is only needed for persistent CacheRegions cache_id = f"pymordemos.thermalblock_adaptive {grid}" fom.enable_caching(cache_region.value, cache_id) if plot_solutions: print('Showing some solutions') Us = () legend = () for mu in problem.parameter_space.sample_randomly(2): print(f"Solving for diffusion = \n{mu['diffusion']} ... ") sys.stdout.flush() Us = Us + (fom.solve(mu),) legend = legend + (str(mu['diffusion']),) fom.visualize(Us, legend=legend, title='Detailed Solutions for different parameters', block=True) print('RB generation ...') product_op = fom.h1_0_semi_product if product == 'h1' else None coercivity_estimator = ExpressionParameterFunctional('min([diffusion[0], diffusion[1]**2])', fom.parameters) reductors = {'residual_basis': CoerciveRBReductor(fom, product=product_op, coercivity_estimator=coercivity_estimator), 'traditional': SimpleCoerciveRBReductor(fom, product=product_op, coercivity_estimator=coercivity_estimator)} reductor = reductors[reductor] pool = new_parallel_pool(ipython_num_engines=ipython_engines, ipython_profile=ipython_profile) greedy_data = rb_adaptive_greedy( fom, reductor, problem.parameter_space, validation_mus=validation_mus, rho=rho, gamma=gamma, theta=theta, use_error_estimator=error_estimator, error_norm=fom.h1_0_semi_norm, max_extensions=rbsize, visualize=visualize_refinement ) rom = greedy_data['rom'] if pickle: print(f"\nWriting reduced model to file {pickle}_reduced ...") with open(pickle + '_reduced', 'wb') as f: dump(rom, f) print(f"Writing detailed model and reductor to file {pickle}_detailed ...") with open(pickle + '_detailed', 'wb') as f: dump((fom, reductor), f) print('\nSearching for maximum error on random snapshots ...') results = reduction_error_analysis(rom, fom=fom, reductor=reductor, error_estimator=True, error_norms=(fom.h1_0_semi_norm,), condition=True, test_mus=problem.parameter_space.sample_randomly(test), basis_sizes=25 if plot_error_sequence else 1, plot=True, pool=pool) real_rb_size = rom.solution_space.dim print(''' *** RESULTS *** Problem: number of blocks: 2x2 h: sqrt(2)/{grid} Greedy basis generation: error estimator enalbed: {error_estimator} product: {product} prescribed basis size: {rbsize} actual basis size: {real_rb_size} elapsed time: {greedy_data[time]} '''.format(**locals())) print(results['summary']) sys.stdout.flush() if plot_error_sequence: from matplotlib import pyplot as plt plt.show() if plot_err: mumax = results['max_error_mus'][0, -1] U = fom.solve(mumax) URB = reductor.reconstruct(rom.solve(mumax)) fom.visualize((U, URB, U - URB), legend=('Detailed Solution', 'Reduced Solution', 'Error'), title='Maximum Error Solution', separate_colorbars=True, block=True)