def __init__(self, circuit: MultiCircuit, options: PowerFlowOptions, max_iter=1000): """ Constructor Args: circuit: Grid to cascade options: Power flow Options max_iter: max iterations """ QThread.__init__(self) self.circuit = circuit self.options = options self.__cancel__ = False # initialize the power flow self.power_flow = PowerFlow(self.circuit, self.options) self.max_eval = max_iter n = len(self.circuit.buses) m = len(self.circuit.branches) # the dimension is the number of nodes self.dim = n # results self.results = MonteCarloResults(n, m, self.max_eval) # variables for the optimization self.xlow = zeros(n) # lower bounds self.xup = ones(n) self.info = "" # info self.integer = array([]) # integer variables self.continuous = arange(0, n, 1) # continuous variables self.solution = None self.optimization_values = None self.it = 0 # compile # compile circuits self.numerical_circuit = self.circuit.compile() self.numerical_input_islands = self.numerical_circuit.compute()
def perform_step_run(self): """ Perform only one step cascading Returns: Nothing """ # recompile the grid self.grid.compile() # initialize the simulator if self.cascade_type is CascadeType.PowerFlow: model_simulator = PowerFlow(self.grid, self.options) elif self.cascade_type is CascadeType.LatinHypercube: model_simulator = LatinHypercubeSampling( self.grid, self.options, sampling_points=self.n_lhs_samples) else: model_simulator = PowerFlow(self.grid, self.options) # For every circuit, run a power flow # for c in self.grid.circuits: model_simulator.run() if self.current_step == 0: # the first iteration try to trigger the selected indices, if any idx = self.remove_elements(self.grid, idx=self.triggering_idx) else: # cascade normally idx = self.remove_elements(self.grid) # store the removed indices and the results entry = CascadingReportElement(idx, model_simulator.results) self.results.events.append(entry) # increase the step number self.current_step += 1 # print(model_simulator.results.get_convergence_report()) # send the finnish signal self.progress_signal.emit(0.0) self.progress_text.emit('Done!') self.done_signal.emit()
def test_xfo_static_tap_3(): """ Basic test with the main transformer's HV tap (X_C3) set at -2.5% (0.975 pu), which raises the LV by the same amount (+2.5%). """ grid = MultiCircuit(name=test_name) grid.Sbase = Sbase grid.time_profile = None grid.logger = list() # Create buses POI = Bus( name="POI", vnom=100, # kV is_slack=True) grid.add_bus(POI) B_C3 = Bus(name="B_C3", vnom=10) # kV grid.add_bus(B_C3) B_MV_M32 = Bus(name="B_MV_M32", vnom=10) # kV grid.add_bus(B_MV_M32) B_LV_M32 = Bus(name="B_LV_M32", vnom=0.6) # kV grid.add_bus(B_LV_M32) # Create voltage controlled generators (or slack, a.k.a. swing) UT = ControlledGenerator(name="Utility") UT.bus = POI grid.add_controlled_generator(POI, UT) # Create static generators (with fixed power factor) M32 = StaticGenerator(name="M32", power=4.2 + 0.0j) # MVA (complex) M32.bus = B_LV_M32 grid.add_static_generator(B_LV_M32, M32) # Create transformer types s = 5 # MVA z = 8 # % xr = 40 SS = TransformerType( name="SS", hv_nominal_voltage=100, # kV lv_nominal_voltage=10, # kV nominal_power=s, copper_losses=complexe(z, xr).real * s * 1000 / Sbase, iron_losses=6.25, # kW no_load_current=0.5, # % short_circuit_voltage=z) grid.add_transformer_type(SS) s = 5 # MVA z = 6 # % xr = 20 PM = TransformerType( name="PM", hv_nominal_voltage=10, # kV lv_nominal_voltage=0.6, # kV nominal_power=s, copper_losses=complexe(z, xr).real * s * 1000 / Sbase, iron_losses=6.25, # kW no_load_current=0.5, # % short_circuit_voltage=z) grid.add_transformer_type(PM) # Create branches X_C3 = Branch(bus_from=POI, bus_to=B_C3, name="X_C3", branch_type=BranchType.Transformer, template=SS, tap=0.975) # update to a more precise tap changer X_C3.apply_tap_changer( TapChanger(taps_up=20, taps_down=20, max_reg=1.1, min_reg=0.9)) grid.add_branch(X_C3) C_M32 = Branch(bus_from=B_C3, bus_to=B_MV_M32, name="C_M32", r=0.784, x=0.174) grid.add_branch(C_M32) X_M32 = Branch(bus_from=B_MV_M32, bus_to=B_LV_M32, name="X_M32", branch_type=BranchType.Transformer, template=PM) grid.add_branch(X_M32) # Apply templates (device types) grid.apply_all_branch_types() print("Buses:") for i, b in enumerate(grid.buses): print(f" - bus[{i}]: {b}") print() grid.compile() options = PowerFlowOptions(SolverType.NR, verbose=True, robust=True, initialize_with_existing_solution=True, multi_core=True, control_q=True, control_taps=True, tolerance=1e-6, max_iter=15) power_flow = PowerFlow(grid, options) power_flow.run() print() print(f"Test: {test_name}") print() print("Controlled generators:") for g in grid.get_controlled_generators(): print(f" - Generator {g}: q_min={g.Qmin} MVAR, q_max={g.Qmax} MVAR") 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, 1)}") print(f" G = {round(b.G, 4)} pu") print(f" B = {round(b.B, 4)} pu") print() print("Transformer types:") for t in grid.transformer_types: print( f" - {t}: Copper losses={int(t.Copper_losses)}kW, " f"Iron losses={int(t.Iron_losses)}kW, SC voltage={t.Short_circuit_voltage}%" ) print() print("Losses:") for i in range(len(grid.branches)): print( f" - {grid.branches[i]}: losses={1000*round(power_flow.results.losses[i], 3)} kVA" ) print() equal = False for i, branch in enumerate(grid.branches): if branch.name == "X_C3": equal = power_flow.results.tap_module[i] == branch.tap_module if not equal: grid.export_pf(f"{test_name}_results.xlsx", power_flow.results) grid.save_excel(f"{test_name}_grid.xlsx") assert equal
def test_pv_1(): """ Voltage controlled generator test, also useful for a basic tutorial. In this case the generator M32 regulates the voltage at a setpoint of 1.025 pu, and the slack bus (POI) regulates it at 1.0 pu. The transformers' magnetizing branch losses are considered, but their voltage regulators aren't. """ grid = MultiCircuit(name=test_name) grid.Sbase = Sbase grid.time_profile = None grid.logger = list() # Create buses POI = Bus(name="POI", vnom=100, #kV is_slack=True) grid.add_bus(POI) B_MV_M32 = Bus(name="B_MV_M32", vnom=10) #kV grid.add_bus(B_MV_M32) B_LV_M32 = Bus(name="B_LV_M32", vnom=0.6) #kV grid.add_bus(B_LV_M32) # Create voltage controlled generators (or slack, a.k.a. swing) UT = ControlledGenerator(name="Utility") UT.bus = POI grid.add_controlled_generator(POI, UT) M32 = ControlledGenerator(name="M32", active_power=4.2, voltage_module=1.025, Qmin=-2.5, Qmax=2.5) M32.bus = B_LV_M32 grid.add_controlled_generator(B_LV_M32, M32) # Create transformer types s = 100 # MVA z = 8 # % xr = 40 SS = TransformerType(name="SS", hv_nominal_voltage=100, # kV lv_nominal_voltage=10, # kV nominal_power=s, copper_losses=complexe(z, xr).real*s*1000/Sbase, iron_losses=125, # kW no_load_current=0.5, # % short_circuit_voltage=z) grid.add_transformer_type(SS) s = 5 # MVA z = 6 # % xr = 20 PM = TransformerType(name="PM", hv_nominal_voltage=10, # kV lv_nominal_voltage=0.6, # kV nominal_power=s, copper_losses=complexe(z, xr).real*s*1000/Sbase, iron_losses=6.25, # kW no_load_current=0.5, # % short_circuit_voltage=z) grid.add_transformer_type(PM) # Create branches X_C3 = Branch(bus_from=POI, bus_to=B_MV_M32, name="X_C3", branch_type=BranchType.Transformer, template=SS) grid.add_branch(X_C3) X_M32 = Branch(bus_from=B_MV_M32, bus_to=B_LV_M32, name="X_M32", branch_type=BranchType.Transformer, template=PM) grid.add_branch(X_M32) # Apply templates (device types) grid.apply_all_branch_types() print("Buses:") for i, b in enumerate(grid.buses): print(f" - bus[{i}]: {b}") print() grid.compile() options = PowerFlowOptions(SolverType.LM, verbose=True, robust=True, initialize_with_existing_solution=True, multi_core=True, control_q=True, tolerance=1e-6, max_iter=99) power_flow = PowerFlow(grid, options) power_flow.run() approx_volt = [round(100*abs(v), 1) for v in power_flow.results.voltage] solution = [100.0, 100.1, 102.5] # Expected solution from GridCal and ETAP 16.1.0, for reference print() print(f"Test: {test_name}") print(f"Results: {approx_volt}") print(f"Solution: {solution}") print() print("Controlled generators:") for g in grid.get_controlled_generators(): print(f" - Generator {g}: q_min={g.Qmin}pu, q_max={g.Qmax}pu") 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, 1)}") print(f" G = {round(b.G, 4)} pu") print(f" B = {round(b.B, 4)} pu") print() print("Transformer types:") for t in grid.transformer_types: print(f" - {t}: Copper losses={int(t.Copper_losses)}kW, Iron losses={int(t.Iron_losses)}kW, SC voltage={t.Short_circuit_voltage}%") print() print("Losses:") for i in range(len(grid.branches)): print(f" - {grid.branches[i]}: losses={1000*round(power_flow.results.losses[i], 3)} kVA") print() equal = True for i in range(len(approx_volt)): if approx_volt[i] != solution[i]: equal = False assert equal
def test_xfo_static_tap_1(): """ Basic test with the main transformer's HV tap (X_C3) set at +5% (1.05 pu), which lowers the LV by the same amount (-5%). """ grid = MultiCircuit(name=test_name) grid.Sbase = Sbase grid.time_profile = None grid.logger = list() # Create buses POI = Bus( name="POI", vnom=100, #kV is_slack=True) grid.add_bus(POI) B_C3 = Bus(name="B_C3", vnom=10) #kV grid.add_bus(B_C3) B_MV_M32 = Bus(name="B_MV_M32", vnom=10) #kV grid.add_bus(B_MV_M32) B_LV_M32 = Bus(name="B_LV_M32", vnom=0.6) #kV grid.add_bus(B_LV_M32) # Create voltage controlled generators (or slack, a.k.a. swing) UT = ControlledGenerator(name="Utility") UT.bus = POI grid.add_controlled_generator(POI, UT) # Create static generators (with fixed power factor) M32 = StaticGenerator(name="M32", power=4.2 + 0.0j) # MVA (complex) M32.bus = B_LV_M32 grid.add_static_generator(B_LV_M32, M32) # Create transformer types s = 5 # MVA z = 8 # % xr = 40 SS = TransformerType( name="SS", hv_nominal_voltage=100, # kV lv_nominal_voltage=10, # kV nominal_power=s, copper_losses=complexe(z, xr).real * s * 1000 / Sbase, iron_losses=6.25, # kW no_load_current=0.5, # % short_circuit_voltage=z) grid.add_transformer_type(SS) s = 5 # MVA z = 6 # % xr = 20 PM = TransformerType( name="PM", hv_nominal_voltage=10, # kV lv_nominal_voltage=0.6, # kV nominal_power=s, copper_losses=complexe(z, xr).real * s * 1000 / Sbase, iron_losses=6.25, # kW no_load_current=0.5, # % short_circuit_voltage=z) grid.add_transformer_type(PM) # Create branches X_C3 = Branch(bus_from=POI, bus_to=B_C3, name="X_C3", branch_type=BranchType.Transformer, template=SS, tap=1.05) grid.add_branch(X_C3) C_M32 = Branch(bus_from=B_C3, bus_to=B_MV_M32, name="C_M32", r=0.784, x=0.174) grid.add_branch(C_M32) X_M32 = Branch(bus_from=B_MV_M32, bus_to=B_LV_M32, name="X_M32", branch_type=BranchType.Transformer, template=PM) grid.add_branch(X_M32) # Apply templates (device types) grid.apply_all_branch_types() print("Buses:") for i, b in enumerate(grid.buses): print(f" - bus[{i}]: {b}") print() grid.compile() options = PowerFlowOptions(SolverType.LM, verbose=True, robust=True, initialize_with_existing_solution=True, multi_core=True, control_q=True, tolerance=1e-6, max_iter=99) power_flow = PowerFlow(grid, options) power_flow.run() approx_volt = [round(100 * abs(v), 1) for v in power_flow.results.voltage] solution = [100.0, 94.7, 98.0, 98.1] # Expected solution from GridCal print() print(f"Test: {test_name}") print(f"Results: {approx_volt}") print(f"Solution: {solution}") print() print("Controlled generators:") for g in grid.get_controlled_generators(): print(f" - Generator {g}: q_min={g.Qmin} MVAR, q_max={g.Qmax} MVAR") 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, 1)}") print(f" G = {round(b.G, 4)} pu") print(f" B = {round(b.B, 4)} pu") print() print("Transformer types:") for t in grid.transformer_types: print( f" - {t}: Copper losses={int(t.Copper_losses)}kW, Iron losses={int(t.Iron_losses)}kW, SC voltage={t.Short_circuit_voltage}%" ) print() print("Losses:") for i in range(len(grid.branches)): print( f" - {grid.branches[i]}: losses={1000*round(power_flow.results.losses[i], 3)} kVA" ) print() equal = True for i in range(len(approx_volt)): if approx_volt[i] != solution[i]: equal = False assert equal
def test_basic(): """ Basic GridCal test, also useful for a basic tutorial. In this case the magnetizing branch of the transformers is neglected by inputting 1e-20 excitation current and iron core losses. The results are identical to ETAP's, which always uses this assumption in balanced load flow calculations. """ grid = MultiCircuit(name=test_name) grid.Sbase = Sbase grid.time_profile = None grid.logger = list() # Create buses POI = Bus( name="POI", vnom=100, #kV is_slack=True) grid.add_bus(POI) B_C3 = Bus(name="B_C3", vnom=10) #kV grid.add_bus(B_C3) B_MV_M32 = Bus(name="B_MV_M32", vnom=10) #kV grid.add_bus(B_MV_M32) B_LV_M32 = Bus(name="B_LV_M32", vnom=0.6) #kV grid.add_bus(B_LV_M32) # Create voltage controlled generators (or slack, a.k.a. swing) UT = ControlledGenerator(name="Utility") UT.bus = POI grid.add_controlled_generator(POI, UT) # Create static generators (with fixed power factor) M32 = StaticGenerator(name="M32", power=4.2 + 0.0j) # MVA (complex) M32.bus = B_LV_M32 grid.add_static_generator(B_LV_M32, M32) # Create transformer types s = 5 # MVA z = 8 # % xr = 40 SS = TransformerType( name="SS", hv_nominal_voltage=100, # kV lv_nominal_voltage=10, # kV nominal_power=s, copper_losses=complexe(z, xr).real * s * 1000 / Sbase, iron_losses=1e-20, no_load_current=1e-20, short_circuit_voltage=z) grid.add_transformer_type(SS) s = 5 # MVA z = 6 # % xr = 20 PM = TransformerType( name="PM", hv_nominal_voltage=10, # kV lv_nominal_voltage=0.6, # kV nominal_power=s, copper_losses=complexe(z, xr).real * s * 1000 / Sbase, iron_losses=1e-20, no_load_current=1e-20, short_circuit_voltage=z) grid.add_transformer_type(PM) # Create branches X_C3 = Branch(bus_from=POI, bus_to=B_C3, name="X_C3", branch_type=BranchType.Transformer, template=SS) grid.add_branch(X_C3) C_M32 = Branch(bus_from=B_C3, bus_to=B_MV_M32, name="C_M32", r=0.784, x=0.174) grid.add_branch(C_M32) X_M32 = Branch(bus_from=B_MV_M32, bus_to=B_LV_M32, name="X_M32", branch_type=BranchType.Transformer, template=PM) grid.add_branch(X_M32) # Apply templates (device types) grid.apply_all_branch_types() print("Buses:") for i, b in enumerate(grid.buses): print(f" - bus[{i}]: {b}") print() grid.compile() options = PowerFlowOptions(SolverType.LM, verbose=True, robust=True, initialize_with_existing_solution=True, multi_core=True, control_q=True, tolerance=1e-6, max_iter=99) power_flow = PowerFlow(grid, options) power_flow.run() approx_volt = [round(100 * abs(v), 1) for v in power_flow.results.voltage] solution = [ 100.0, 99.6, 102.7, 102.9 ] # Expected solution from GridCal and ETAP 16.1.0, for reference print() print(f"Test: {test_name}") print(f"Results: {approx_volt}") print(f"Solution: {solution}") print() print("Controlled generators:") for g in grid.get_controlled_generators(): print(f" - Generator {g}: q_min={g.Qmin}pu, q_max={g.Qmax}pu") 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, 1)}") print(f" G = {round(b.G, 4)} pu") print(f" B = {round(b.B, 4)} pu") print() print("Transformer types:") for t in grid.transformer_types: print( f" - {t}: Copper losses={int(t.Copper_losses)}kW, Iron losses={int(t.Iron_losses)}kW, SC voltage={t.Short_circuit_voltage}%" ) print() print("Losses:") for i in range(len(grid.branches)): print( f" - {grid.branches[i]}: losses={1000*round(power_flow.results.losses[i], 3)} kVA" ) print() equal = True for i in range(len(approx_volt)): if approx_volt[i] != solution[i]: equal = False assert equal
class Optimize(QThread): progress_signal = pyqtSignal(float) progress_text = pyqtSignal(str) done_signal = pyqtSignal() def __init__(self, circuit: MultiCircuit, options: PowerFlowOptions, max_iter=1000): """ Constructor Args: circuit: Grid to cascade options: Power flow Options max_iter: max iterations """ QThread.__init__(self) self.circuit = circuit self.options = options self.__cancel__ = False # initialize the power flow self.power_flow = PowerFlow(self.circuit, self.options) self.max_eval = max_iter n = len(self.circuit.buses) m = len(self.circuit.branches) # the dimension is the number of nodes self.dim = n # results self.results = MonteCarloResults(n, m, self.max_eval) # variables for the optimization self.xlow = zeros(n) # lower bounds self.xup = ones(n) self.info = "" # info self.integer = array([]) # integer variables self.continuous = arange(0, n, 1) # continuous variables self.solution = None self.optimization_values = None self.it = 0 # compile # compile circuits self.numerical_circuit = self.circuit.compile() self.numerical_input_islands = self.numerical_circuit.compute() def objfunction(self, x): """ Objective function to run :param x: combinations of values between 0~1 :return: objective function value, the average voltage in this case """ # For every circuit, run the time series for numerical_island in self.numerical_input_islands: # sample from the CDF give the vector x of values in [0, 1] # c.sample_at(x) monte_carlo_input = make_monte_carlo_input(numerical_island) mc_time_series = monte_carlo_input.get_at(x) Y, I, S = mc_time_series.get_at(t=0) # run the sampled values # res = self.power_flow.run_at(0, mc=True) res = self.power_flow.run_pf(circuit=numerical_island, Vbus=numerical_island.Vbus, Sbus=S, Ibus=I) # Y, I, S = circuit.mc_time_series.get_at(0) self.results.S_points[self.it, numerical_island.original_bus_idx] = S self.results.V_points[ self.it, numerical_island.original_bus_idx] = res.voltage[ numerical_island.original_bus_idx] self.results.I_points[ self.it, numerical_island.original_branch_idx] = res.Ibranch[ numerical_island.original_branch_idx] self.results.loading_points[ self.it, numerical_island.original_branch_idx] = res.loading[ numerical_island.original_branch_idx] self.it += 1 prog = self.it / self.max_eval * 100 # self.progress_signal.emit(prog) f = abs(self.results.V_points[self.it - 1, :].sum()) / self.dim # print(prog, ' % \t', f) return f def run(self): """ Run the optimization @return: Nothing """ self.it = 0 n = len(self.circuit.buses) m = len(self.circuit.branches) self.xlow = zeros(n) # lower bounds self.xup = ones(n) # upper bounds self.progress_signal.emit(0.0) self.progress_text.emit('Running stochastic voltage collapse...') self.results = MonteCarloResults(n, m, self.max_eval) # (1) Optimization problem # print(data.info) # (2) Experimental design # Use a symmetric Latin hypercube with 2d + 1 samples exp_des = SymmetricLatinHypercube(dim=self.dim, npts=2 * self.dim + 1) # (3) Surrogate model # Use a cubic RBF interpolant with a linear tail surrogate = RBFInterpolant(kernel=CubicKernel, tail=LinearTail, maxp=self.max_eval) # (4) Adaptive sampling # Use DYCORS with 100d candidate points adapt_samp = CandidateDYCORS(data=self, numcand=100 * self.dim) # Use the serial controller (uses only one thread) controller = SerialController(self.objfunction) # (5) Use the sychronous strategy without non-bound constraints strategy = SyncStrategyNoConstraints(worker_id=0, data=self, maxeval=self.max_eval, nsamples=1, exp_design=exp_des, response_surface=surrogate, sampling_method=adapt_samp) controller.strategy = strategy # Run the optimization strategy result = controller.run() # Print the final result print('Best value found: {0}'.format(result.value)) print('Best solution found: {0}'.format( np.array_str(result.params[0], max_line_width=np.inf, precision=5, suppress_small=True))) self.solution = result.params[0] # Extract function values from the controller self.optimization_values = np.array( [o.value for o in controller.fevals]) # send the finnish signal self.progress_signal.emit(0.0) self.progress_text.emit('Done!') self.done_signal.emit() def plot(self, ax=None): """ Plot the optimization convergence """ clr = np.array([ '#2200CC', '#D9007E', '#FF6600', '#FFCC00', '#ACE600', '#0099CC', '#8900CC', '#FF0000', '#FF9900', '#FFFF00', '#00CC01', '#0055CC' ]) if self.optimization_values is not None: max_eval = len(self.optimization_values) if ax is None: f, ax = plt.subplots() # Points ax.scatter(np.arange(0, max_eval), self.optimization_values, color=clr[6]) # Best value found ax.plot(np.arange(0, max_eval), np.minimum.accumulate(self.optimization_values), color=clr[1], linewidth=3.0) ax.set_xlabel('Evaluations') ax.set_ylabel('Function Value') ax.set_title('Optimization convergence') def cancel(self): """ Cancel the simulation """ self.__cancel__ = True self.progress_signal.emit(0.0) self.progress_text.emit('Cancelled') self.done_signal.emit()