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 single_simulate(psat, simtype, title, clean=True): """func single_simulate :: PsatData, Str, Bool -> PsatReport ---- run matlab with the PsatData `psat` as either power flow (pf) or optimal power flow (opf) return the results of the simulation. remove temp files if specified """ matlab_filename = "matlab_" + title psat_filename = "psat_" + title + ".m" report_filename = "psat_" + title + "_01.txt" # make the matlab_script single_matlab_script(matlab_filename + ".m", psat_filename, simtype) # write the PsatData to file Ensure(psat.in_limits(), "no point simulating if it's already out of limits") with open(psat_filename, "w") as psat_file: psat.write(psat_file) # run matlab simulate(matlab_filename) # return the parsed report report = read_report(report_filename) if clean: clean_files() return report
def set_all_demand(self, value): # Note:: should I change P, Q or both. # this doesn't really do much other than check the data type # we need it that high for testing but the system wont work # with value above 1.08 currently Ensure(0 < value <= 4, "just a vague sanity check") for load in self.loads.values(): newval = load.p * value self.mismatch += load.p - newval load.p = newval self.demand[load.bus_no].p_bid_max = newval self.demand[load.bus_no].p_bid_min = newval
def process_pflow_bus(self, tokens): # print "Bus Power Flow : %d : %s" % (tokens["bus"][0],tokens) # print tokens["bus"][0] pu_check = lambda x: dec_check(x, Decimal("-10.0"), Decimal("10.0")) for x in "v pg qg pl ql".split(): self.ensure(pu_check(tokens[x]), "error : \n%s" % tokens) self.ensure(dec_check(tokens["phase"]), "error : \n%s" % tokens) # actually add to self.power_flow # are we to assume that there is a 1-to-1 mapping of names to the # natural numbers if not then we need a very gay lookup, if so then # ... woo # im going to assume they do map. even then, do we then keep the 1 based # counting or convert to zero based. GAAAAYYYY # * if I convert the external file to zero based. it wont then match the # journal paper # * if I convert the interal representation it wont match the file or # paper # * if I leave it then there may be strange bugs as there will be a # dummy element # I have decided to use a dict rather than a list, it still has integers # as the key it sovles the other problems. only requirement is that they # are unique integers. bus_num = tokens["bus"][0] Ensure(bus_num >= 0, "cant have negative bus_num (%s)" % bus_num) Ensure(bus_num not in self.power_flow, "already added bus " + str(bus_num)) self.power_flow[bus_num] = self.PowerFlow(bus_num, tokens["v"], tokens["phase"], tokens["pg"], tokens["qg"], tokens["pl"], tokens["ql"])
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 parse_matlab_output(text): def found(msg, in_text): return in_text.find(msg) != -1 result = [] split_by_sim = text.split("Newton-Raphson Method for Power Flow") for n, sim_text in enumerate(split_by_sim[1:]): passed = True if found("IPM-OPF", sim_text): if not found("IPM-OPF completed in", sim_text): print "[P] opf not completed", n passed = False else: # we don't cae if the PF before the OPF fails. if found("Warning: Matrix is singular", sim_text): print "[P] singular matrix warning", n passed = False if found("Warning: Matrix is close to singular", sim_text): print "[P] near singular matrix warning", n passed = False if found("The error is increasing too much", sim_text): print "[P] large error", n passed = False if found("Convergence is likely not reachable", sim_text): print "[P] non-convergence", n passed = False if not found("Power Flow completed in", sim_text): print "[P] power flow not completed", n passed = False result.append(passed) Ensure(len(result) >= 1, str(result)) return result
def __init__(self, seq): for x in seq: Ensure(x is True or x is False, "expected Boolena got %s" % str(x)) self.seq = seq self.n = 0
def invariant(self): Ensure(len(self.title) > 0, "Scenarios must have a title") Ensure(self.count > 0, "Scenarios must have a count") EnsureIn(self.simtype, set(["pf", "opf"])) if self.result: EnsureIn(self.result, set(["pass", "fail", "error"]))
def generate_cases(n_outages=10, n_failures=1000, sim=True, full_sim=True): timer_begin = time.clock() timer_start = timer_begin print "[G] start simulation with %d states and %d contingencies." % ( n_outages, n_failures) if full_sim: Ensure(n_outages and n_failures and sim, "can only do full sim if we have everything") clean_files() batch_size = 100 psat = read_psat("rts.m") prob = read_probabilities("rts.net") # create the base cases by sampling for outages # simulate these and print to a file. # it should contain `n_outages` outages. summary_file = open("summary.txt", "w") mismatch_file = open("mismatch.txt", "w") print "Base Stats: mis= %f gen= %f load= %f lim ( %f < X < %f )" % psat.get_stats( ) mismatch_file.write( as_csv("title mismatch gen load min max".split()) + "\n") mismatch_file.write(as_csv(["base"] + list(psat.get_stats())) + "\n") try: if n_outages: outage_batch = make_outage_cases(prob, n_outages) if sim: batch_simulate(outage_batch, psat, batch_size, True, mismatch_file) with open("outage.txt", "w") as result_file: outage_batch.csv_write(result_file) print "-" * 80 print "---- Outage Stats ----" outage_batch.write_stats(sys.stdout) summary_file.write("%s\n" % ("-" * 80)) summary_file.write("Outage Stats\n") summary_file.write("%s\n" % ("-" * 80)) outage_batch.write_stats(summary_file) timer_end = time.clock() timer_time = (timer_end - timer_start) print "[G] outages created in %d seconds." % int( math.ceil(timer_time)) timer_start = time.clock() # do the same for one hour changes to the system. if n_failures: failure_batch = make_failure_cases(prob, n_failures) if sim: batch_simulate(failure_batch, psat, batch_size, True, mismatch_file) with open("failure.txt", "w") as result_file: failure_batch.csv_write(result_file) print "-" * 80 print "---- Failure Stats ----" failure_batch.write_stats(sys.stdout) summary_file.write("Failure Stats\n") summary_file.write("%s\n" % ("-" * 80)) failure_batch.write_stats(summary_file) timer_end = time.clock() timer_time = (timer_end - timer_start) print "[G] failures created in %d seconds." % int( math.ceil(timer_time)) timer_start = time.clock() # simulate each of the changes to each base case if full_sim: simulate_cases(outage_batch, failure_batch, psat, summary_file, mismatch_file) timer_end = time.clock() timer_time = (timer_end - timer_start) print "[G] full sim in %d seconds." % int(math.ceil(timer_time)) timer_start = time.clock() finally: timer_end = time.clock() timer_time = (timer_end - timer_begin) print "[G] total time %d seconds." % int(math.ceil(timer_time))
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 fix_mismatch(self): """ Changes the generators power to compensate for the imbalance caused by remove_* or set_*. It sets each generator proportionally based upon it's current generating power (though it respects generator limits). We then need to check limits, though if the algorithm is working we shouldn't need to. It does this by using `self.mismatch` TODO: Not sure what to do with reactive power TODO: make this only count scheduleable generators i.e. not wind farms """ if self.mismatch != 0: scheduleable_generators = self.generators.values() gpowers = [gen.p for gen in scheduleable_generators ] + [slack.p_guess for slack in self.slack.values()] min_limit = [] max_limit = [] for gen in scheduleable_generators: min_limit.append( sum(supply.p_bid_min for supply in self.supply.values() if supply.bus_no == gen.bus_no)) max_limit.append( sum(supply.p_bid_max for supply in self.supply.values() if supply.bus_no == gen.bus_no)) for slack in self.slack.values(): min_limit.append( sum(supply.p_bid_min for supply in self.supply.values() if supply.bus_no == slack.bus_no)) max_limit.append( sum(supply.p_bid_max for supply in self.supply.values() if supply.bus_no == slack.bus_no)) #print "-----" #print "t %f => %f < %f < %f" % (self.mismatch, sum(min_limit), sum(powers), sum(max_limit)) #for pmin,power,pmax in zip(min_limit, powers, max_limit): # print "p %f < %f < %f" % (pmin,power,pmax) #print "-----" # check nothing starts out of limits gnames = [gen.bus_no for gen in scheduleable_generators ] + [slack.bus_no for slack in self.slack.values()] for idx in range(len(gpowers)): if not (min_limit[idx] <= gpowers[idx] <= max_limit[idx]): print "Power (#%d - bus(%d)) started out of limit (%f<=%f<=%f)" % ( idx, gnames[idx], min_limit[idx], gpowers[idx], max_limit[idx]) res = fix_mismatch(-self.mismatch, gpowers, min_limit, max_limit) for newp, generator in zip(res, scheduleable_generators): generator.p = newp Ensure(self.in_limits(), "fixing mismatch should leave it in limit")
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")
def lfgenerator(self, output, scenario): """from the loadflow kill everything in the killlist and save resulting loadlow to writer.""" # 1. find all in the set killlist & loadflow.branches.keys # 2. find all in the set killlist & loadflow.busbars.keys # 3. kill all the generator linking lines # i.e. kill lines 'X + 2.' killlist = set([x.strip() for x in scenario.kill_list]) allbranches = set(self.branches.keys()) allbusses = set(self.busbars.keys()) branchkill = allbranches & killlist buskill = allbusses & killlist assert (branchkill | buskill == killlist) # kill lines on dead busbars deadlines = [] for (name, value) in self.branches.items(): if name in branchkill or value[4].strip( ) in buskill or value[5].strip() in buskill: deadlines += [name] deadlines = set(deadlines) # if we have killed a generator connecting line, kill its generator special_kill = set() for name in deadlines: if name.startswith("XG"): special_kill |= set([name[1:]]) buskill |= special_kill # print "killlist", killlist # print "deadlines", deadlines # print "branchkill", branchkill # print "buskill", buskill branchkill |= deadlines assert (branchkill <= allbranches) assert (buskill <= allbusses) csvwriter = csv.writer(output) # the title csvwriter.writerow([self.title]) # remembering to change the number of bus and line numbus = int(self.header[0]) - len(buskill) numline = int(self.header[1]) - len(branchkill) csvwriter.writerow([numbus, numline] + self.header[2:]) # make sure there is a slack bus newslackbus = None def is_slack_bus(busline): return int(busline[0]) == 2 slackbuslines = filter(is_slack_bus, self.busbars.values()) assert (len(slackbuslines) == 1) slackbus = slackbuslines[0][2].strip() if slackbus in buskill: for item in ["G13", "G14"]: if item not in buskill: newslackbus = item break Ensure(newslackbus is not None, "failed to find a replacement slackbus") # fix power mismatch names = {} powers = [] load_powers = [] min_limit = [] max_limit = [] unscheduleable = windlevel.unscheduleable total_gen_power = 0 total_unscheduleable_gen = 0 for name, value in self.busbars.items(): if name not in killlist: if value[3].strip() != "": if name not in unscheduleable: total_gen_power += float(value[3]) names[name] = len(powers) powers.append(float(value[3])) min_limit.append(0) # no minimum level for a generator max_limit.append( float(self.limits_checker.gen_limit[name])) else: gen_id = windlevel.unscheduleable.index(name) gen_power = float(value[3]) * float( scenario.wind_levels[gen_id]) total_gen_power += gen_power total_unscheduleable_gen += gen_power if value[7].strip() != "": load_powers.append(float(value[7])) # mismatch is sum(load_power) + line_losses - sum(gen_power) after: fix mismatch, bus_level and killed. mismatch = (sum(load_powers) * scenario.bus_level) + self.line_losses - total_gen_power fixed_powers = fix_mismatch(mismatch, powers, min_limit, max_limit) # line for `main.py test` to check with notes. # print "mismatch", 8550 - (sum(load_powers) * scenario.bus_level), 8997.9 - sum(fixed_powers) - total_unscheduleable_gen # ignore everything in killlist, print the rest for (name, value) in self.busbars.items(): if name not in buskill: # don't modify self new_value = value[:] # if we have a load power - times it by the bus_level if new_value[7].strip() != "": new_value[7] = str( float(new_value[7]) * scenario.bus_level) # if we have a new generator power get it from fix mismatch if name in names: new_value[3] = str(fixed_powers[names[name]]) # if we have an unscheduleable gen get it's power from the level if name in windlevel.unscheduleable: gen_id = windlevel.unscheduleable.index(name) new_value[3] = str( float(new_value[3]) * float(scenario.wind_levels[gen_id])) # print it out # print len(new_value), " ".join(new_value) if name != newslackbus: csvwriter.writerow(new_value) else: csvwriter.writerow(["2"] + new_value[1:]) # same with the branches # remember to kill lines on dead busbars for (name, value) in self.branches.items(): if name not in branchkill: if value[4].strip() not in buskill and value[5].strip( ) not in buskill: csvwriter.writerow(value)