def test_api_helm(): np.set_printoptions(precision=4) fname = os.path.join('..', '..', 'Grids_and_profiles', 'grids', 'IEEE 30 Bus with storage.xlsx') grid = FileOpen(fname).open() print('\n\n', grid.name) # print('Ybus:\n', grid.circuits[0].power_flow_input.Ybus.todense()) options = PowerFlowOptions(SolverType.HELM, verbose=False, tolerance=1e-9) power_flow = PowerFlowDriver(grid, options) power_flow.run() print_power_flow_results(power_flow)
def test_demo_5_node(root_path=ROOT_PATH): np.core.arrayprint.set_printoptions(precision=4) grid = MultiCircuit() # Add buses bus1 = Bus('Bus 1', vnom=20) grid.add_bus(bus1) gen1 = Generator('Slack Generator', voltage_module=1.0) grid.add_generator(bus1, gen1) bus2 = Bus('Bus 2', vnom=20) grid.add_bus(bus2) grid.add_load(bus2, Load('load 2', P=40, Q=20)) bus3 = Bus('Bus 3', vnom=20) grid.add_bus(bus3) grid.add_load(bus3, Load('load 3', P=25, Q=15)) bus4 = Bus('Bus 4', vnom=20) grid.add_bus(bus4) grid.add_load(bus4, Load('load 4', P=40, Q=20)) bus5 = Bus('Bus 5', vnom=20) grid.add_bus(bus5) grid.add_load(bus5, Load('load 5', P=50, Q=20)) # add branches (Lines in this case) grid.add_line(Line(bus1, bus2, 'line 1-2', r=0.05, x=0.11, b=0.02)) grid.add_line(Line(bus1, bus3, 'line 1-3', r=0.05, x=0.11, b=0.02)) grid.add_line(Line(bus1, bus5, 'line 1-5', r=0.03, x=0.08, b=0.02)) grid.add_line(Line(bus2, bus3, 'line 2-3', r=0.04, x=0.09, b=0.02)) grid.add_line(Line(bus2, bus5, 'line 2-5', r=0.04, x=0.09, b=0.02)) grid.add_line(Line(bus3, bus4, 'line 3-4', r=0.06, x=0.13, b=0.03)) grid.add_line(Line(bus4, bus5, 'line 4-5', r=0.04, x=0.09, b=0.02)) # grid.plot_graph() print('\n\n', grid.name) FileSave(grid, 'demo_5_node.json').save() options = PowerFlowOptions(SolverType.NR, verbose=False) power_flow = PowerFlowDriver(grid, options) power_flow.run() print_power_flow_results(power_flow=power_flow) v = np.array([1., 0.9553, 0.9548, 0.9334, 0.9534]) all_ok = np.isclose(np.abs(power_flow.results.voltage), v, atol=1e-3) return all_ok
def test_demo_5_node(root_path): np.core.arrayprint.set_printoptions(precision=4) grid = MultiCircuit() # Add buses bus1 = Bus('Bus 1', vnom=20) # bus1.is_slack = True grid.add_bus(bus1) gen1 = Generator('Slack Generator', voltage_module=1.0) grid.add_generator(bus1, gen1) bus2 = Bus('Bus 2', vnom=20) grid.add_bus(bus2) grid.add_load(bus2, Load('load 2', P=40, Q=20)) bus3 = Bus('Bus 3', vnom=20) grid.add_bus(bus3) grid.add_load(bus3, Load('load 3', P=25, Q=15)) bus4 = Bus('Bus 4', vnom=20) grid.add_bus(bus4) grid.add_load(bus4, Load('load 4', P=40, Q=20)) bus5 = Bus('Bus 5', vnom=20) grid.add_bus(bus5) grid.add_load(bus5, Load('load 5', P=50, Q=20)) # add branches (Lines in this case) grid.add_branch(Branch(bus1, bus2, 'line 1-2', r=0.05, x=0.11, b=0.02)) grid.add_branch(Branch(bus1, bus3, 'line 1-3', r=0.05, x=0.11, b=0.02)) grid.add_branch(Branch(bus1, bus5, 'line 1-5', r=0.03, x=0.08, b=0.02)) grid.add_branch(Branch(bus2, bus3, 'line 2-3', r=0.04, x=0.09, b=0.02)) grid.add_branch(Branch(bus2, bus5, 'line 2-5', r=0.04, x=0.09, b=0.02)) grid.add_branch(Branch(bus3, bus4, 'line 3-4', r=0.06, x=0.13, b=0.03)) grid.add_branch(Branch(bus4, bus5, 'line 4-5', r=0.04, x=0.09, b=0.02)) # grid.plot_graph() print('\n\n', grid.name) options = PowerFlowOptions(SolverType.NR, verbose=False) power_flow = PowerFlowDriver(grid, options) power_flow.run() print_power_flow_results(power_flow=power_flow)
def test_api_multi_core_starmap(): """ Test the pool.starmap function together with GridCal """ file_name = os.path.join('..', '..', 'Grids_and_profiles', 'grids', 'IEEE 30 Bus with storage.xlsx') batch_size = 100 grid = FileOpen(file_name).open() print('\n\n', grid.name) options = PowerFlowOptions(SolverType.NR, verbose=False) power_flow = PowerFlowDriver(grid, options) power_flow.run() # create instances of the of the power flow simulation given the grid print('running...') pool = Pool() results = pool.starmap(multi_island_pf, [(grid, options, 0)] * batch_size)
def test_line_losses_1(): """ Basic line losses test. """ test_name = "test_line_losses_1" grid = MultiCircuit(name=test_name) Sbase = 100 # MVA grid.Sbase = Sbase grid.time_profile = None grid.logger = Logger() # Create buses Bus0 = Bus(name="Bus0", vnom=25, is_slack=True) Bus1 = Bus(name="Bus1", vnom=25) grid.add_bus(Bus0) grid.add_bus(Bus1) # Create load grid.add_load(Bus1, Load(name="Load0", P=1.0, Q=0.4)) # Create slack bus grid.add_generator(Bus0, Generator(name="Utility")) # Create cable (r and x should be in pu) grid.add_branch( Line(bus_from=Bus0, bus_to=Bus1, name="Cable1", r=0.01, x=0.05)) # Run non-linear load flow options = PowerFlowOptions(verbose=True) power_flow = PowerFlowDriver(grid, options) power_flow.run() # Check solution approx_losses = round(1000 * power_flow.results.losses[0], 3) solution = complex(0.116, 0.58) # Expected solution from GridCal # Tested on ETAP 16.1.0 and pandapower print( "\n=================================================================") print(f"Test: {test_name}") print( "=================================================================\n") print(f"Results: {approx_losses}") print(f"Solution: {solution}") print() print("Buses:") for i, b in enumerate(grid.buses): print(f" - bus[{i}]: {b}") print() print("Branches:") branches = grid.get_branches() for b in branches: print(f" - {b}:") print(f" R = {round(b.R, 4)} pu") print(f" X = {round(b.X, 4)} pu") print(f" X/R = {round(b.X/b.R, 2)}") print() print("Voltages:") for i in range(len(grid.buses)): print( f" - {grid.buses[i]}: voltage={round(power_flow.results.voltage[i], 3)} pu" ) print() print("Losses:") for i in range(len(branches)): print( f" - {branches[i]}: losses={round(power_flow.results.losses[i], 3)} MVA" ) print() print("Loadings (power):") for i in range(len(branches)): print( f" - {branches[i]}: loading={round(power_flow.results.Sf[i], 3)} MVA" ) print() print("Loadings (current):") for i in range(len(branches)): print( f" - {branches[i]}: loading={round(power_flow.results.If[i], 3)} pu" ) print() assert approx_losses == solution
from GridCal.Engine.IO.file_handler import * from GridCal.Engine.Simulations.PowerFlow.power_flow_driver import PowerFlowOptions, ReactivePowerControlMode, PowerFlow, SolverType if __name__ == '__main__': fname = os.path.join('..', '..', 'Grids_and_profiles', 'grids', 'IEEE 30 Bus with storage.xlsx') print('Reading...') main_circuit = FileOpen(fname).open() options = PowerFlowOptions(SolverType.NR, verbose=False, initialize_with_existing_solution=False, multi_core=False, dispatch_storage=True, control_q=ReactivePowerControlMode.NoControl, control_p=True) # grid.export_profiles('ppppppprrrrroooofiles.xlsx') # exit() #################################################################################################################### # PowerFlow #################################################################################################################### print('\n\n') power_flow = PowerFlow(main_circuit, options) power_flow.run() print('\n\n', main_circuit.name) print('\t|V|:', abs(power_flow.results.voltage)) print('\t|Sbranch|:', abs(power_flow.results.Sbranch))
run the voltage collapse simulation @return: """ print('Running voltage collapse...') # compile the numerical circuit numerical_circuit = self.circuit.compile() evt = get_reliability_scenario(numerical_circuit) run_events(nc=numerical_circuit, events_list=evt) print('done!') self.progress_text.emit('Done!') self.done_signal.emit() def cancel(self): self.__cancel__ = True if __name__ == '__main__': from GridCal.Engine.All import MultiCircuit fname = '/home/santi/Documentos/GitHub/GridCal/Grids_and_profiles/grids/IEEE 30 Bus with storage.xlsx' circuit_ = MultiCircuit() circuit_.load_file(fname) study = ReliabilityStudy(circuit=circuit_, pf_options=PowerFlowOptions()) study.run()
LB = np.ones_like(x0) * -1 UB = np.ones_like(x0) * 1 x, fx = nelder_mead(f, x0, callback=print) print('Result') print(fx, '->', x) if __name__ == "__main__": # nelder_mead_test() main_circuit = MultiCircuit() # fname = 'D:\\GitHub\\GridCal\\Grids_and_profiles\\grids\\IEEE 30 Bus with storage.xlsx' fname = '/home/santi/Documentos/GitHub/GridCal/Grids_and_profiles/grids/IEEE 30 Bus with storage.xlsx' print('Reading...') main_circuit.load_file(fname) options = PowerFlowOptions(SolverType.NR, verbose=False, robust=False, initialize_with_existing_solution=False, multi_core=False, dispatch_storage=True, control_q=False, control_p=True) problem = AcOpfNelderMead(main_circuit, options=options) res = problem.solve() print('Done!') print('Overloads') print(problem.get_overloads()) pass
self.progress_signal.emit(0.0) self.progress_text.emit('Cancelled') self.done_signal.emit() if __name__ == '__main__': import time from GridCal.Engine import * # fname = '/home/santi/Documentos/GitHub/GridCal/Grids_and_profiles/grids/Lynn 5 Bus pv.gridcal' # fname = '/home/santi/Documentos/GitHub/GridCal/Grids_and_profiles/grids/IEEE39_1W.gridcal' # fname = '/home/santi/Documentos/GitHub/GridCal/Grids_and_profiles/grids/grid_2_islands.xlsx' fname = '/mnt/sdc1/tmp/src/ReePlexos/spain_plexos(sin restricciones).gridcal' main_circuit = FileOpen(fname).open() pf_options = PowerFlowOptions(solver_type=SolverType.LACPF) opt = OptimizeVoltageSetPoints(circuit=main_circuit, options=pf_options, max_iter=100) opt.progress_signal.connect(print) # opt.run_bfgs() # print(opt.solution) # opt.plot() a = time.time() opt.run_slsqp() print(opt.solution) opt.plot() print('Elapsed', time.time() - a)
def _test_api(): fname = os.path.join('..', '..', 'Grids_and_profiles', 'grids', 'IEEE 30 Bus with storage.xlsx') print('Reading...') main_circuit = FileOpen(fname).open() pf_options = PowerFlowOptions(SolverType.NR, verbose=False, initialize_with_existing_solution=False, multi_core=False, dispatch_storage=True, control_q=ReactivePowerControlMode.NoControl, control_p=True) #################################################################################################################### # PowerFlowDriver #################################################################################################################### print('\n\n') power_flow = PowerFlowDriver(main_circuit, pf_options) power_flow.run() print('\n\n', main_circuit.name) print('\t|V|:', abs(power_flow.results.voltage)) print('\t|Sbranch|:', abs(power_flow.results.Sbranch)) print('\t|loading|:', abs(power_flow.results.loading) * 100) print('\tReport') print(power_flow.results.get_report_dataframe()) #################################################################################################################### # Short circuit #################################################################################################################### print('\n\n') print('Short Circuit') sc_options = ShortCircuitOptions(bus_index=[16]) # grid, options, pf_options:, pf_results: sc = ShortCircuit(grid=main_circuit, options=sc_options, pf_options=pf_options, pf_results=power_flow.results) sc.run() print('\n\n', main_circuit.name) print('\t|V|:', abs(main_circuit.short_circuit_results.voltage)) print('\t|Sbranch|:', abs(main_circuit.short_circuit_results.Sbranch)) print('\t|loading|:', abs(main_circuit.short_circuit_results.loading) * 100) #################################################################################################################### # Time Series #################################################################################################################### print('Running TS...', '') ts = TimeSeries(grid=main_circuit, options=pf_options, start_=0, end_=96) ts.run() numeric_circuit = main_circuit.compile() ts_analysis = TimeSeriesResultsAnalysis(numeric_circuit, ts.results) #################################################################################################################### # OPF #################################################################################################################### print('Running OPF...', '') opf_options = OptimalPowerFlowOptions(verbose=False, solver=SolverType.DC_OPF, mip_solver=False) opf = OptimalPowerFlow(grid=main_circuit, options=opf_options) opf.run() #################################################################################################################### # OPF Time Series #################################################################################################################### print('Running OPF-TS...', '') opf_options = OptimalPowerFlowOptions(verbose=False, solver=SolverType.NELDER_MEAD_OPF, mip_solver=False) opf_ts = OptimalPowerFlowTimeSeries(grid=main_circuit, options=opf_options, start_=0, end_=96) opf_ts.run() #################################################################################################################### # Voltage collapse #################################################################################################################### vc_options = VoltageCollapseOptions() # just for this test numeric_circuit = main_circuit.compile() numeric_inputs = numeric_circuit.compute() Sbase = np.zeros(len(main_circuit.buses), dtype=complex) Vbase = np.zeros(len(main_circuit.buses), dtype=complex) for c in numeric_inputs: Sbase[c.original_bus_idx] = c.Sbus Vbase[c.original_bus_idx] = c.Vbus unitary_vector = -1 + 2 * np.random.random(len(main_circuit.buses)) vc_inputs = VoltageCollapseInput(Sbase=Sbase, Vbase=Vbase, Starget=Sbase * (1 + unitary_vector)) vc = VoltageCollapse(circuit=main_circuit, options=vc_options, inputs=vc_inputs) vc.run() # vc.results.plot() #################################################################################################################### # Monte Carlo #################################################################################################################### print('Running MC...') mc_sim = MonteCarlo(main_circuit, pf_options, mc_tol=1e-5, max_mc_iter=1000000) mc_sim.run() lst = np.array(list(range(mc_sim.results.n)), dtype=int) # mc_sim.results.plot(ResultTypes.BusVoltageAverage, indices=lst, names=lst) #################################################################################################################### # Latin Hypercube #################################################################################################################### print('Running LHC...') lhs_sim = LatinHypercubeSampling(main_circuit, pf_options, sampling_points=100) lhs_sim.run() #################################################################################################################### # Cascading #################################################################################################################### print('Running Cascading...') cascade = Cascading(main_circuit.copy(), pf_options, max_additional_islands=5, cascade_type_=CascadeType.LatinHypercube, n_lhs_samples_=10) cascade.run() cascade.perform_step_run() cascade.perform_step_run() cascade.perform_step_run() cascade.perform_step_run()
def opf(self, t_idx=None, collect=True): """ Run a power flow for every circuit @return: OptimalPowerFlowResults object """ # print('PowerFlow at ', self.grid.name) # self.progress_signal.emit(0.0) numerical_circuit = self.grid.compile() islands = numerical_circuit.compute() if self.options.solver == SolverType.DC_OPF: # DC optimal power flow problem = DcOpf(self.grid, verbose=False, allow_load_shedding=self.options.load_shedding, allow_generation_shedding=self.options.generation_shedding, generation_shedding_weight=self.options.generation_shedding_weight, load_shedding_weight=self.options.load_shedding_weight) elif self.options.solver == SolverType.AC_OPF: # AC optimal power flow problem = AcOpf(self.grid, verbose=False, allow_load_shedding=self.options.load_shedding, allow_generation_shedding=self.options.generation_shedding, generation_shedding_weight=self.options.generation_shedding_weight, load_shedding_weight=self.options.load_shedding_weight) elif self.options.solver == SolverType.NELDER_MEAD_OPF: if self.options.power_flow_options is None: options = PowerFlowOptions(SolverType.LACPF, verbose=False, initialize_with_existing_solution=False, multi_core=False, dispatch_storage=True, control_q=False, control_taps=False) else: options = self.options.power_flow_options problem = AcOpfNelderMead(self.grid, options, verbose=False, break_at_value=False) else: raise Exception('Solver not recognized ' + str(self.options.solver)) # Solve problem.build_solvers() problem.set_default_state() problem.solve(verbose=True) # get the branch flows (it is used more than one time) Sbr = problem.get_branch_flows() ld = problem.get_load_shedding() ld[ld == None] = 0 bt = problem.get_batteries_power() bt[bt == None] = 0 gn = problem.get_controlled_generation() gn[gn == None] = 0 gs = problem.get_generation_shedding() gs[gs == None] = 0 # pack the results self.results = OptimalPowerFlowResults(Sbus=None, voltage=problem.get_voltage(), load_shedding=ld * self.grid.Sbase, generation_shedding=gs * self.grid.Sbase, battery_power=bt * self.grid.Sbase, controlled_generation_power=gn * self.grid.Sbase, Sbranch=Sbr * self.grid.Sbase, overloads=problem.get_overloads(), loading=problem.get_loading(), converged=bool(problem.converged), bus_types = numerical_circuit.bus_types) return self.results
def run(self): """ Run the time series simulation @return: """ numerical_circuit = self.grid.compile() islands = numerical_circuit.compute() if self.grid.time_profile is not None: # declare the results self.results = OptimalPowerFlowTimeSeriesResults(n=len(self.grid.buses), m=len(self.grid.branches), nt=len(self.grid.time_profile), ngen=len(self.grid.get_generators()), nbat=len(self.grid.get_batteries()), nload=len(self.grid.get_loads()), time=self.grid.time_profile) self.results.bus_types = numerical_circuit.bus_types # declare LP problem if self.options.solver == SolverType.DC_OPF: # DC optimal power flow problem = DcOpf(self.grid, verbose=False, allow_load_shedding=self.options.load_shedding, allow_generation_shedding=self.options.generation_shedding) # elif self.options.solver == SolverType.DYCORS_OPF: # # problem = AcOpfDYCORS(self.grid, verbose=False) elif self.options.solver == SolverType.NELDER_MEAD_OPF: if self.options.power_flow_options is None: options = PowerFlowOptions(SolverType.LACPF, verbose=False, initialize_with_existing_solution=False, multi_core=False, dispatch_storage=True, control_q=False, control_taps=False) else: options = self.options.power_flow_options problem = AcOpfNelderMead(self.grid, options, verbose=False) elif self.options.solver == SolverType.AC_OPF: # AC optimal power flow problem = AcOpf(self.grid, verbose=False, allow_load_shedding=self.options.load_shedding, allow_generation_shedding=self.options.generation_shedding) else: self.logger.append('Not implemented method ' + str(self.options.solver)) # build problem.build_solvers() batteries = self.grid.get_batteries() nbat = len(batteries) E = np.zeros(nbat) E0 = np.zeros(nbat) minE = np.zeros(nbat) maxE = np.zeros(nbat) if self.options.control_batteries and (self.end_ - self.start_) > 1: control_batteries = True for i, bat in enumerate(batteries): E0[i] = bat.Enom * bat.soc_0 minE[i] = bat.min_soc * bat.Enom maxE[i] = bat.max_soc * bat.Enom E = E0.copy() else: control_batteries = False # all vectors declared already t = self.start_ bat_idx = [] force_batteries_to_charge = False dt = 0 execution_avg_time = 0 time_summation = 0 alpha = 0.2 while t < self.end_ and not self.__cancel__: start_time = datetime.datetime.now() problem.set_state_at(t, force_batteries_to_charge=force_batteries_to_charge, bat_idx=bat_idx, battery_loading_pu=0.01, Emin=minE/self.grid.Sbase, Emax=maxE/self.grid.Sbase, E=E/self.grid.Sbase, dt=dt) problem.solve(verbose=False) if problem.converged: # gather the results # get the branch flows (it is used more than one time) Sbr = problem.get_branch_flows() ld = problem.get_load_shedding().real ld[ld == None] = 0 bt = problem.get_batteries_power() bt[bt == None] = 0 gn = problem.get_controlled_generation() gn[gn==None] = 0 gs = problem.get_generation_shedding() gs[gs == None] = 0 self.results.voltage[t, :] = problem.get_voltage() self.results.load_shedding[t, :] = ld * self.grid.Sbase.real self.results.controlled_generator_shedding[t, :] = gs * self.grid.Sbase self.results.battery_power[t, :] = bt * self.grid.Sbase self.results.controlled_generator_power[t, :] = gn * self.grid.Sbase self.results.Sbranch[t, :] = Sbr self.results.overloads[t, :] = problem.get_overloads().real self.results.loading[t, :] = problem.get_loading().real self.results.converged[t] = bool(problem.converged) # control batteries energy if control_batteries and t > self.start_: dt = (self.grid.time_profile[t] - self.grid.time_profile[t - 1]).seconds / 3600 # time delta in hours dE = self.results.battery_power[t, :] * dt E -= dE # negative power(charge) -> more energy too_low = E <= minE bat_idx = np.where(too_low == True)[0] # check which energy values are less or equal to zero force_batteries_to_charge = bool(len(bat_idx)) self.results.battery_energy[t, :] = E else: print('\nDid not converge!\n') end_time = datetime.datetime.now() time_elapsed = end_time - start_time time_summation += time_elapsed.microseconds * 1e-6 execution_avg_time = (int(time_summation) / (t - self.start_ + 1)) * alpha + execution_avg_time * (1-alpha) remaining = (self.end_ - t) * execution_avg_time progress = ((t - self.start_ + 1) / (self.end_ - self.start_)) * 100 self.progress_signal.emit(progress) self.progress_text.emit('Solving OPF at ' + str(self.grid.time_profile[t]) + '\t remaining: ' + str(datetime.timedelta(seconds=remaining)) + '\tConverged:' + str(bool(problem.converged))) t += 1 else: print('There are no profiles') self.progress_text.emit('There are no profiles') # send the finnish signal self.progress_signal.emit(0.0) self.progress_text.emit('Done!') self.done_signal.emit()
def test_corr_line_losses(): test_name = "test_corr_line_losses" grid = MultiCircuit(name=test_name) grid.Sbase = Sbase grid.time_profile = None grid.logger = Logger() # Create buses Bus0 = Bus(name="Bus0", vnom=10, is_slack=True) Bus1 = Bus(name="Bus1", vnom=10) grid.add_bus(Bus0) grid.add_bus(Bus1) # Create load grid.add_load(Bus1, Load(name="Load0", P=1.0, Q=0.4)) # Create slack bus grid.add_generator(Bus0, Generator(name="Utility")) # Create cable cable = Branch( bus_from=Bus0, bus_to=Bus1, name="Cable0", r=0.784, x=0.174, temp_base=20, # °C temp_oper=90, # °C alpha=0.00323) # Copper grid.add_branch(cable) options = PowerFlowOptions(verbose=True, apply_temperature_correction=True) power_flow = PowerFlowDriver(grid, options) power_flow.run() # Check solution approx_losses = round(power_flow.results.losses[0], 3) solution = complex(0.011, 0.002) # Expected solution from GridCal # Tested on ETAP 16.1.0 print( "\n=================================================================") print(f"Test: {test_name}") print( "=================================================================\n") print(f"Results: {approx_losses}") print(f"Solution: {solution}") print() print("Buses:") for i, b in enumerate(grid.buses): print(f" - bus[{i}]: {b}") print() print("Branches:") for b in grid.branches: print(f" - {b}:") print(f" R = {round(b.R, 4)} pu") print(f" X = {round(b.X, 4)} pu") print(f" X/R = {round(b.X/b.R, 2)}") print() print("Voltages:") for i in range(len(grid.buses)): print( f" - {grid.buses[i]}: voltage={round(power_flow.results.voltage[i], 3)} pu" ) print() print("Losses:") for i in range(len(grid.branches)): print( f" - {grid.branches[i]}: losses={round(power_flow.results.losses[i], 3)} MVA" ) print() print("Loadings (power):") for i in range(len(grid.branches)): print( f" - {grid.branches[i]}: loading={round(power_flow.results.Sbranch[i], 3)} MVA" ) print() print("Loadings (current):") for i in range(len(grid.branches)): print( f" - {grid.branches[i]}: loading={round(power_flow.results.Ibranch[i], 3)} pu" ) print() assert approx_losses == solution