def simulate(matlab_filename, single_item=True): """func simulate :: Str -> [Bool] ---- call matlab with the specified script. """ try: # print "simulate", matlab_filename parameters = '-nodisplay -nojvm -nosplash -minimize -r ' # parameters = '-automation -r ' proc = subprocess.Popen('matlab ' + parameters + matlab_filename, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE) so, se = proc.communicate() if (False): print "SE" print "=================================" print se print "=================================" print "SO" print "=================================" print so print "=================================" EnsureEqual(se, "") result = parse_matlab_output(so) Ensure( len(result) >= 1, "there has to be at least one result (%s)" % str(result)) if single_item: EnsureEqual(len(result), 1) if result[0]: print "[S] simulate passed." else: print "[S] simulate failed." return result except Exception as exce: print "[E] Error Caught at script.simulate (%s)" % matlab_filename print exce raise
def add(self, scenario): # if already added increment count rather than append. scenario.invariant() dicthash = scenario.dicthash() if dicthash in self.scenarios: # print "Updated [%d] = %s" % (self.scenarios[dicthash].count+1, # dicthash) # make sure we don't have hash collision Ensure(self.scenarios[dicthash].equal(scenario), "we have a hash collision") # make sure we keep result info if scenario.result: if self.scenarios[dicthash].result: EnsureEqual(scenario.result, self.scenarios[dicthash].result) else: self.scenarios[dicthash].result = scenario.result self.scenarios[dicthash].increment(scenario.count) else: # print "New [1]", dicthash self.scenarios[dicthash] = scenario
def batch_matlab_script(filename, batch): """func batch_matlab_script :: Str, SimulationBatch -> ---- create a matlab script file which simulates all the Scenarios in the batch assuming their filename is "psat_" + scenario.title + ".m" """ EnsureNotEqual(len(batch), 0) with open(filename, "w") as matlab_stream: matlab_stream.write("initpsat;\n") matlab_stream.write("Settings.lfmit = 50;\n") matlab_stream.write("Settings.violations = 'on'\n") matlab_stream.write("OPF.basepg = 0;\n") matlab_stream.write("OPF.basepl = 0;\n") simtype = batch[0].simtype for scenario in batch: filename = "psat_" + scenario.title + ".m" matlab_stream.write("runpsat('" + filename + "','data');\n") EnsureEqual(simtype, scenario.simtype) if simtype == "pf": matlab_stream.write("runpsat pf;\n") elif simtype == "opf": matlab_stream.write("runpsat opf;\n") else: raise Error("expected pf or opf got: " + scenario.simtype) matlab_stream.write("runpsat pfrep;\n") matlab_stream.write("closepsat;\n") matlab_stream.write("exit\n")
def report_to_psat(report, psat): """func report_to_psat :: PsatReport, PsatData -> PsatData ---- Make a new PsatData based upon `psat` but contains the voltage, angle, and power values from `report`. """ # TODO: if we can work out why PSAT has discrepencies in the # number of items in input and output then add these line back # assert len(psat.lines) == report.num_line # assert len(psat.slack) == 1 # assert len(psat.generators) == report.num_generator # assert len(psat.busses) == report.num_bus # assert len(psat.loads) == report.num_load # assert len(psat.demand) == 0 # assert len(psat.supply) == len(psat.loads) new_psat = deepcopy(psat) pf = report.power_flow EnsureEqual(len(new_psat.slack), 1) slack = new_psat.slack.values()[0] slack.v_magnitude = float(pf[slack.bus_no].v) slack.ref_angle = float(pf[slack.bus_no].phase) slack.p_guess = float(pf[slack.bus_no].pg) for gen in new_psat.generators.values(): if gen.bus_no in pf: gen.p = float(pf[gen.bus_no].pg) gen.v = float(pf[gen.bus_no].v) else: print "ERROR:", gen.bus_no for load in new_psat.loads.values(): if load.bus_no in pf: load.p = float(pf[load.bus_no].pl) load.q = float(pf[load.bus_no].ql) else: print "ERROR:", load.bus_no # fix for reactive power on bus 39-43 # for x in range(39,44): # assert str(new_psat.generators[x].v) == "1.014" # new_psat.generators[x].v = "1.01401" # assert len(new_psat.shunts) == 0 # assert new_psat.loads[6].q == "1.299" if not new_psat.in_limits(): print "not in limits" # TODO: I think it probably should raise... maybe # raise Error("new file is invalid. It hits static limits.") return new_psat
def make_failures(prob, count): """func make_failures :: NetworkProbability, Int -> SimulationBatch ---- Monte Carlo sample the network for unexpected changes. e.g. new outages (failures), actual weather, actual load level, etc. """ batch = SimulationBatch() for x in range(count): batch.add(prob.failures(str(x))) EnsureEqual(count, batch.size()) return batch
def make_outages(prob, count): """func make_outages :: NetworkProbability, Int -> SimulationBatch ---- Monte Carlo sample the network for it's expected condition. e.g. existing outages, weather & load forcast, etc. """ batch = SimulationBatch() for x in range(count): batch.add(prob.outages(str(x))) EnsureEqual(count, batch.size()) return batch
def text_to_scenario(text): """func text_to_scenario :: Str -> Scenario ---- make `text` into a Scenario by reading as if a single element batch file """ with closing(StringIO(text)) as batch_stream: batch = SimulationBatch() batch.read(batch_stream) EnsureEqual(len(batch), 1) return list(batch)[0]
def remove_bus(self, bus_no): # TODO: really should be an 'assert' not an 'if' but make testing easier if len(self.slack) == 1: slack = self.slack.values()[0] EnsureNotEqual(slack.bus_no, bus_no) else: print "Expected one slack got %d" % len(self.slack) EnsureEqual(self.busses[bus_no].bus_no, bus_no) del self.busses[bus_no] # as we now have virtal busses that connect to generators # we need to find and delete any of those hat connect to # this bus. # We can find them by using the cid of the lines # find all lines with who's cid starts with "C" that # connect to this bus for line in self.lines.values(): if line.fbus == bus_no: self.remove_line(line.cid) if line.cid.startswith("c"): self.remove_bus(line.tbus) elif line.tbus == bus_no: self.remove_line(line.cid) if line.cid.startswith("c"): self.remove_bus(line.fbus) # kill all connecting items for idx, shunt in self.shunts.items(): if shunt.bus_no == bus_no: del self.shunts[idx] for idx, item in self.demand.items(): if item.bus_no == bus_no: del self.demand[idx] for idx, item in self.supply.items(): if item.bus_no == bus_no: del self.supply[idx] for idx, gen in self.generators.items(): if gen.bus_no == bus_no: self.mismatch -= gen.p del self.generators[idx] for idx, load in self.loads.items(): if load.bus_no == bus_no: self.mismatch += load.p del self.loads[idx]
def make_outage_cases(prob, count): """func make_outage_cases :: NetworkProbability, Int -> SimulationBatch ---- like `make_outages` but makes `count` *after* reducing. Monte Carlo sample the network for it's expected condition. e.g. existing outages, weather & load forcast, etc. """ batch = SimulationBatch() current = 0 while len(batch) < count: batch.add(prob.outages(str(current))) current += 1 EnsureEqual(count, len(batch)) return batch
def make_failure_cases(prob, count): """func make_failure_cases :: NetworkProbability, Int -> SimulationBatch ---- like `make_failures` but makes `count` *after* reducing. Monte Carlo sample the network for unexpected changes. e.g. new outages (failures), actual weather, actual load level, etc. """ batch = SimulationBatch() current = 0 while len(batch) < count: batch.add(prob.failures(str(current))) current += 1 EnsureEqual(count, len(batch)) return batch
def read(self, stream): current_scen = None for line in stream: line = [x.lower() for x in line.split()] # comments if len(line) == 0 or line[0].startswith("#"): continue # title elif line[0].startswith("["): if current_scen is not None: # logger.debug("Added Scenario: %s" % title) self.add(current_scen) title = line[0][1:-1] simtype = line[1] if len(line) == 3: count = int(line[2]) else: EnsureEqual(len(line), 2) count = 1 EnsureIn(simtype, set("pf opf".split())) current_scen = Scenario(title, simtype, count) # remove elif line[0] == "remove": if line[1] == "bus": current_scen.kill_bus.append(int(line[2])) elif line[1] == "line": current_scen.kill_line.append(line[2]) elif line[1] == "generator": current_scen.kill_gen.append(line[2]) else: raise Error("got %s expected (line, generator, bus)" % line[1]) # set elif line[0] == "set": if line[1:3] == ["all", "demand"]: current_scen.all_demand = float(line[3]) else: raise Error("got %s expected 'demand'" % line[1]) # results elif line[0] == "result": EnsureIn(line[1], set("pass fail error".split())) current_scen.result = line[1] # nothing else allowed else: raise Error("got %s expected (remove, set, result, [])" % line[0]) if current_scen is not None: # logger.debug("Added Scenario: %s" % title) self.add(current_scen)
def batch_simulate(batch, psat, size=10, clean=True, mismatch_file=None): """func batch_simulate :: SimulationBatch, PsatData, Int -> ---- Simulate all Scenarios in `batch` (with a base of `psat`) in groups of size `size`. Modify `batch` in place. delete all temp files if it succedes """ print "[b] batch simulate %d cases" % len(batch) for n, group in enumerate(split_every(size, batch)): try: timer_start = time.clock() print "[b] simulating batch", n + 1, "of", int( math.ceil(len(batch) / size)) + 1 sys.stdout.flush() # make the matlab_script matlab_filename = "matlab_" + str(n) batch_matlab_script(matlab_filename + ".m", group) # write all the scenarios to file as psat_files for scenario in group: scenario.result = None try: new_psat = scenario_to_psat(scenario, psat) except Exception as exce: print "[E] Error Caught at script.batch_simulate (%s) - failed to convert scenario to psat" % scenario.title print exce scenario.result = "error" # we probably shouldn't have this but it might cause error in matlab as it is expected. new_psat = deepcopy(psat) if mismatch_file: mismatch_file.write( as_csv([scenario.title] + list(new_psat.get_stats())) + "\n") new_psat_filename = "psat_" + scenario.title + ".m" with open(new_psat_filename, "w") as new_psat_file: new_psat.write(new_psat_file) # run matlab resutls = simulate(matlab_filename, False) EnsureEqual(len(resutls), len(group)) for res, scenario in zip(resutls, group): if not (res): print "[b] did not converge (%s)" % scenario.title scenario.result = "fail" # gather results for scenario in group: if not scenario.result: report_filename = "psat_" + scenario.title + "_01.txt" try: report = read_report(report_filename) scenario.result = report_in_limits(report) except Exception as exce: print "[E] Error Caught at script.batch_simulate (%s) - report check failure" % scenario.title print exce scenario.result = "error" timer_end = time.clock() timer_time = (timer_end - timer_start) print "[b] batch time of", int(math.ceil(timer_time)), "seconds" except Exception as exce: print "[E] Error Caught at script.batch_simulate (%s) - failed to simulate batch" % scenario.title print exce if clean: clean_files()
def fix_mismatch(mismatch, power, min_limit, max_limit): """ func fix_mismatch :: Real, [Real], [Real], [Real] -> [Real] change the total generated power by `mismatch`. Do this based upon current power of each generator taking into account its limits. Returns a list of new generator powers """ EnsureEqual(len(power), len(min_limit)) EnsureEqual(len(power), len(max_limit)) if mismatch == 0: return power done = [False for _ in range(len(power))] result = [0.0 for _ in range(len(power))] def find_limit_max(m): """find the index of the first generator that will be limited. or None """ for n in range(len(done)): if (not done[n]) and (power[n] * m > max_limit[n]): return n return None def find_limit_min(m): """find the index of the first generator that will be limited. or None """ for n in range(len(done)): if (not done[n]) and (power[n] * m < min_limit[n]): return n return None Ensure( sum(min_limit) < sum(power) + mismatch < sum(max_limit), "mismatch of %f is outside limits (%f < %f < %f)" % (mismatch, sum(min_limit), sum(power) + mismatch, sum(max_limit))) # print "mismatch\t%f" % mismatch # print "total gen\t%f" % sum(power) # print "total min gen\t%f" % sum(min_limit) # print "total max gen\t%f" % sum(max_limit) # print "-"*10 # print "power\t%s" % as_csv(power,"\t") # print "min_limit\t%s" % as_csv(min_limit,"\t") # print "max_limit\t%s" % as_csv(max_limit,"\t") # if mismatch > 0: # print as_csv([b-a for a,b in zip(power, max_limit)], "\t") # print sum(max_limit) - sum(power) # else: # print as_csv([b-a for a,b in zip(power, min_limit)], "\t") # print sum(power) - sum(min_limit) # deal with each generator that will be limited while True: Ensure(not all(done), "programmer error") # print "fix_mismatch", len([1 for x in done if x]) total_gen = sum(power[i] for i in range(len(done)) if not done[i]) EnsureNotEqual(total_gen, 0) multiplier = 1.0 + (mismatch / total_gen) # we shouldn't really care about the miltiplier as long as # the limits are being met should we? Ensure(0 <= multiplier <= 5, "vague sanity check") if mismatch < 0: idx_gen = find_limit_min(multiplier) if idx_gen is None: break # print "generator hit min limit:", idx_gen result[idx_gen] = min_limit[idx_gen] mismatch -= result[idx_gen] - power[idx_gen] done[idx_gen] = True else: idx_gen = find_limit_max(multiplier) if idx_gen is None: break # print "generator hit max limit:", idx_gen result[idx_gen] = max_limit[idx_gen] mismatch -= result[idx_gen] - power[idx_gen] done[idx_gen] = True # deal with all the other generators # knowing that none of them will limit for idx in range(len(power)): if not done[idx]: # print "set generator", idx result[idx] = power[idx] * multiplier mismatch -= result[idx] - power[idx] done[idx] = True # check nothing is out of limits for idx in range(len(power)): Ensure( power[idx] == 0 or (min_limit[idx] <= power[idx] <= max_limit[idx]), "Power (%d) out of limit (%f<=%f<=%f)" % (idx, min_limit[idx], power[idx], max_limit[idx])) Ensure(mismatch < 0.001, "should be much mismatch left after fixing it") Ensure(all(done), "should have fixed everything") return result
def remove_generator(self, supply_id): """kill the specified supply and corresponding generator with the requiement that there is only one generator per busbar. Could kill the line and busbar as well in most cases. But where there was only one gen on a bus anyway it doesn't have a virtual bus hence might have a load. """ bus_no = self.supply[supply_id].bus_no # really just for testing (my god that is poor style) if len(self.slack) == 0: Ensure(bus_no in self.generators, "missing generator info (%s)" % bus_no) self.mismatch -= self.generators[bus_no].p del self.generators[bus_no] del self.supply[supply_id] return EnsureEqual(len(self.slack), 1) slack = self.slack.values()[0] if slack.bus_no == bus_no: # todo: lets just hope that's accurate, it's probably not. self.mismatch -= slack.p_guess # del self.generators[bus_no] # we are not doing this because we instead change the slack to the # new value from the chosen bus. # we do want to remove this. del self.supply[supply_id] allowed_slacks = [37, 38] gen = None for x in allowed_slacks: if x in self.generators: # print "deleting slack bus: new slack =", x EnsureEqual(self.generators[x].bus_no, x, "%s, %s" % (self.generators[x].bus_no, x)) gen = self.generators[x] break Ensure(gen, "no slack bus") slack.bus_no = gen.bus_no slack.s_rating = gen.s_rating slack.v_rating = gen.v_rating slack.v_magnitude = gen.v slack.ref_angle = 0.0 slack.q_max = gen.q_max slack.q_min = gen.q_min slack.v_max = gen.v_max slack.v_min = gen.v_min slack.p_guess = gen.p slack.lp_coeff = gen.lp_coeff slack.ref_bus = 1.0 slack.status = gen.status del self.generators[gen.bus_no] else: EnsureIn(bus_no, self.generators, "missing generator info") self.mismatch -= self.generators[bus_no].p del self.generators[bus_no] del self.supply[supply_id]
def read_as_bus_no(storage, classtype): EnsureEqual(len(storage), 0) for item in read_section(stream, classtype): storage[item.bus_no] = item Ensure(len(storage) >= 0, "failed to read any items")