diff --git a/pygridsim/__init__.py b/pygridsim/__init__.py index 8aea3b5..adb2fc4 100644 --- a/pygridsim/__init__.py +++ b/pygridsim/__init__.py @@ -6,4 +6,4 @@ __email__ = 'amzhao@mit.edu' __version__ = '0.1.0.dev1' -from pygridsim.core import PyGridSim \ No newline at end of file +from pygridsim.core import PyGridSim as PyGridSim \ No newline at end of file diff --git a/pygridsim/configs.py b/pygridsim/configs.py new file mode 100644 index 0000000..226698f --- /dev/null +++ b/pygridsim/configs.py @@ -0,0 +1,68 @@ +from pygridsim.enums import LoadType, LineType, GeneratorType, SourceType +import pygridsim.defaults as defaults + +LOAD_CONFIGURATIONS = { + LoadType.HOUSE: { + "kV": defaults.HOUSE_KV, + "kW": defaults.HOUSE_KW, + "kvar": defaults.HOUSE_KVAR + }, + LoadType.COMMERCIAL: { + "kV": defaults.COMMERCIAL_KV, + "kW": defaults.COMMERCIAL_KW, + "kvar": defaults.COMMERCIAL_KVAR + }, + LoadType.INDUSTRIAL: { + "kV": defaults.INDUSTRIAL_KV, + "kW": defaults.INDUSTRIAL_KW, + "kvar": defaults.INDUSTRIAL_KVAR + } +} + +SOURCE_CONFIGURATIONS = { + SourceType.TURBINE: { + "kV": defaults.TURBINE_BASE_KV + }, + SourceType.POWER_PLANT: { + "kV": defaults.POWER_PLANT_KV + }, + SourceType.LV_SUBSTATION: { + "kV": defaults.LV_SUBSTATION_BASE_KV + }, + SourceType.MV_SUBSTATION: { + "kV": defaults.MV_SUBSTATION_BASE_KV + }, + SourceType.HV_SUBSTATION: { + "kV": defaults.HV_SUBSTATION_BASE_KV + }, + SourceType.SHV_SUBSTATION: { + "kV": defaults.SHV_SUBSTATION_BASE_KV + }, +} + +LINE_CONFIGURATIONS = { + LineType.LV_LINE: { + "length": defaults.LV_LINE_LENGTH + }, + LineType.MV_LINE: { + "length": defaults.MV_LINE_LENGTH + }, + LineType.HV_LINE: { + "length": defaults.HV_LINE_LENGTH + } +} + +GENERATOR_CONFIGURATIONS = { + GeneratorType.SMALL: { + "kV": defaults.SMALL_GEN_KV, + "kW": defaults.SMALL_GEN_KW, + }, + GeneratorType.LARGE: { + "kV": defaults.LARGE_GEN_KV, + "kW": defaults.LARGE_GEN_KW, + }, + GeneratorType.INDUSTRIAL: { + "kV": defaults.INDUSTRIAL_GEN_KV, + "kW": defaults.INDUSTRIAL_GEN_KW, + } +} \ No newline at end of file diff --git a/pygridsim/core.py b/pygridsim/core.py index e79f25c..ca6a488 100644 --- a/pygridsim/core.py +++ b/pygridsim/core.py @@ -1,153 +1,190 @@ # -*- coding: utf-8 -*- from altdss import altdss -from altdss import AltDSS, Transformer, Vsource, Load, LoadModel, LoadShape -from dss.enums import LineUnits, SolveModes -from pygridsim.parameters import make_load_node, make_source_node -from pygridsim.results import query_solution -from pygridsim.lines import make_line -from pygridsim.transformers import make_transformer -from pygridsim.enums import LineType, SourceType, LoadType +from pygridsim.parameters import _make_load_node, _make_source_node, _make_generator, _make_pv +from pygridsim.results import _query_solution, _export_results +from pygridsim.lines import _make_line """Main module.""" class PyGridSim: def __init__(self): - """ - Initialize OpenDSS/AltDSS engine. Creates an Empty Circuit + """Initialize OpenDSS engine. + + Instantiate an OpenDSS circuit that user can build circuit components on. Stores numbers of circuit components + to ensure unique naming of repeat circuit components + + Attributes: + num_loads (int): Number of loads in circuit so far. + num_lines (int): Number of lines in circuit so far. + num_transformers (int): Number of transformers in circuit so far. + num_pv (int): Number of PV systems in circuit so far. + num_generators (int): Number generators in circuit so far. """ self.num_loads = 0 - self.num_sources = 0 self.num_lines = 0 self.num_transformers = 0 + self.num_pv = 0 + self.num_generators = 0 altdss.ClearAll() altdss('new circuit.MyCircuit') - def add_load_nodes(self, params = {}, load_type: LoadType = LoadType.HOUSE, num = 1): - """ - When the user wants to manually add nodes, or make nodes with varying parameters. + def add_load_nodes(self, load_type: str = "house", params: dict[str, int] = None, num: int = 1): + """Adds Load Node(s) to circuit. + + Allows the user to add num load nodes, either with customized parameters or using a default load_type. Args: - params: load parameters for these manual additions - lines: which nodes these new loads are connected to - num (optional): number of loads to create with these parameters - Return: - List of load_nodes + load_type (str, optional): + Load type as a string, one of "house", "commercial", "industrial". Defaults to "house". + params (dict[str, int], optional): + Load parameters for these manual additions. Defaults to empty dictionary. + num (int, optional): + The number of loads to create with these parameters. Defaults to 1. + + Returns: + list[OpenDSS object]: + A list of OpenDSS objects representing the load nodes created. """ + params = params or dict() load_nodes = [] - for i in range(num): - make_load_node(params, load_type, self.num_loads) + for _ in range(num): + _make_load_node(params, load_type, self.num_loads) self.num_loads += 1 + return load_nodes - def add_source_nodes(self, params = {}, source_type: SourceType = SourceType.TURBINE, num_in_batch = 1, num=1): - """ - When the user wants to manually add nodes, or make nodes with varying parameters. + def update_source(self, source_type: str = "turbine", params: dict[str, int] = None): + """Adds or updates source node in system. + + If a Vsource node does not exist, it is created. Otherwise, its parameters are updated based on the provided values. Args: - params: load parameters for these manual additions - lines: which nodes these new sources are connected to - num (optional): number of sources to create with these parameters (removed for now) - num_in_batch: how many to batch together directly (so they can't be connected to lines separately, etc. - most common use case is if a house has 20 solar panels it's more useful to group them together) - Return: - List of source_nodes - """ - source_nodes = [] - for i in range(num): - make_source_node(params, source_type, count=self.num_sources, num_in_batch=num_in_batch) - self.num_sources += 1 - return source_nodes + source_type (str, optional): + The type of the source (one of "turbine", "powerplant", "lvsub", "mvsub", "hvsub", "shvsub"). Defaults to "turbine". + params (dict[str, int], optional): + A dictionary of parameters to configure the source node. Defaults to None. - def add_lines(self, connections, line_type: LineType = LineType.LV_LINE, params = {}, transformer = True): + Returns: + OpenDSS object: + The OpenDSS object representing the source node. """ - Specify all lines that the user wants to add. If redundant lines, doesn't add anything + params = params or dict() + return _make_source_node(params, source_type) - Args: - connections: a list of new connections to add. Each item of the list follows the form (source1, load1) - TODO: allow the input to also contain optional parameters - """ - for src, dst in connections: - make_line(src, dst, line_type, self.num_lines, params, transformer) - self.num_lines += 1 + def add_PVSystem(self, load_nodes: list[str], params: dict[str, int] = None, num_panels: int = 1): + """Adds a photovoltaic (PV) system to the specified load nodes. - def add_transformers(self, connections, params = {}): - """ - Specify all transformers that the user wants to add, same input style as lines. + Adds PV system with num_panels to each of the listed load nodes. Can be customized with parameters. Args: - connections: a list of new transformers to add (where to add them), with these params - TODO: remove - """ - for src, dst in connections: - make_transformer(src, dst, self.num_transformers, params) - self.num_transformers += 1 - + load_nodes (list[str]): + A list of node names where the PV system will be connected. + params (dict[str, int], optional): + A dictionary of additional parameters for the PV system. Defaults to None. + num_panels (int, optional): + The number of PV panels in the system. Defaults to 1. - def view_load_nodes(self, indices = []): + Returns: + list[DSS objects]: + A list of OpenDSS objects representing the PV systems created. """ - View load nodes (what their parameters are) at the given indices. + params = params or dict() + if not load_nodes: + raise ValueError("Need to enter load nodes to add PVSystem to") + PV_nodes = [] + for load in load_nodes: + PV_nodes.append(_make_pv(load, params, num_panels, self.num_pv)) + self.num_pv += 1 + + return PV_nodes + + def add_generator(self, num: int = 1, gen_type: str = "small", params: dict[str, int] = None): + """Adds generator(s) to the system. + Args: - indices (optional): Which indices to view the nodes at. - If none given, display all + num (int, optional): + The number of generator units to add. Defaults to 1. + gen_type (str, optional): + The type of generator (one of "small", "large", "industrial"). Defaults to "small". + params (dict[str, int], optional): + A dictionary of parameters to configure the generator. Defaults to None. + + Returns: + list[DSS objects]: + A list of OpenDSS objects representing the generators created. """ - load_nodes = [] - if not indices: - indices = [i for i in range(self.num_loads)] - - for idx in indices: - load_obj = altdss.Load["load" + str(idx)] - load_info = {} - load_info["name"] = "load" + str(idx) - load_info["kV"] = load_obj.kV - load_info["kW"] = load_obj.kW - load_info["kVar"] = load_obj.kvar - load_nodes.append(load_info) - return load_nodes + params = params or dict() + generators = [] + for _ in range(num): + generators.append(_make_generator(params, gen_type, count=self.num_generators)) + self.num_generators += 1 + + return generators - def view_source_node(self): - """ - View source nodes (what their parameters are) at the given indices. + def add_lines(self, connections: list[tuple], line_type: str = "lv", params: dict[str, int] = None, transformer: bool = True): + """Adds lines to the system. + + Adds electrical lines according to the given connections. Users can specify the parameters of the lines or otherwise use given line type options. Args: - indices (optional): Which indices to view the nodes at. - If none given, display all - - TODO once capability for more source nodes is initialized + connections (list[tuple]): + A list of tuples defining the connections between nodes. + line_type (str, optional): + The type of line (one of "lv", "mv", "hv"). Defaults to "lv". + params (dict[str, int], optional): + A dictionary of parameters to configure the lines. Defaults to None. + transformer (bool, optional): + Whether to include a transformer in the connection. Defaults to True. + + Returns: + None """ - source_obj = altdss.Vsource["source"] - source_info = {} - source_info["name"] = "source" - source_info["kV"] = source_obj.BasekV - return source_info + params = params or dict() + for src, dst in connections: + _make_line(src, dst, line_type, self.num_lines, params, transformer) + self.num_lines += 1 def solve(self): - """ - Initialize "solve" mode in AltDSS, then allowing the user to query various results on the circuit + """Solves the OpenDSS circuit. + + Initializes "solve" mode in OpenDSS, which then allows the user to query results on the circuit. - TODO: error handling here + Returns: + None """ altdss.Solution.Solve() - def results(self, queries): - """ - Allow the user to query for many results at once instead of learning how to manually query + def results(self, queries: list[str], export_path = ""): + """Gets simulation results based on specified queries. + + Allows the user to query for many results at once by providing a list of desired queries. + + Args: + queries (list[str]): + A list of queries to the circuit ("Voltages", "Losses", "TotalPower") + export_path (str, optional): + The file path to export results. If empty, results are not exported. Defaults to "". Returns: - Results for each query, in a dictionary + dict: + A dictionary containing the fetched simulation results. """ results = {} for query in queries: - results[query] = query_solution(query) + results[query] = _query_solution(query) + + if (export_path): + _export_results(results, export_path) + return results def clear(self): - """ - Must call after we are done using the circuit, or will cause re-creation errors. + """Clears the OpenDSS circuit. - We only work with one circuit at a time, can only have one PyGridSim object at a time. - TODO: maybe this isn't necessary because it's done in the beginning + Returns: + None """ altdss.ClearAll() self.num_loads = 0 diff --git a/pygridsim/defaults.py b/pygridsim/defaults.py index c780bb5..7a67a33 100644 --- a/pygridsim/defaults.py +++ b/pygridsim/defaults.py @@ -1,25 +1,6 @@ """ Set any defaults (i.e. default source voltage, default node load etc.) -Will start with things like HOUSE_KV to define typical load of a house (perhaps with some variance) - -Source: -Define default values for a few types of objects. -In a neighborhood the main ones are -solar panels, wind turbines - -Load: -Define for a typical house, using statistics -https://forum.allaboutcircuits.com/threads/what-is-the-actual-household-voltage-110-115-120-220-240.3320/ -https://www.eia.gov/energyexplained/use-of-energy/electricity-use-in-homes.php?utm_source=chatgpt.com - -In the second iteration -- implement the typical LoadShape in the house -- some randomness to cover the standard distribution of houses, not all the averages - -For now, many of them are listed as tuples - lower end, higher end. -TODO: make generate function that does Math.rand for in the range (later: improve distribution to be non-uniform) """ -from altdss import altdss from altdss import Connection """ Overall Defaults, used for load, sources, lines, etc. @@ -46,12 +27,26 @@ INDUSTRIAL_KVAR = [20, 25] """ -Source Nodes -TODO also fuel cells, other less common forms of energy later +Source Nodes (including other form of sources, like PVSystem) """ +IMPEDANCE = 0.0001 +TURBINE_BASE_KV = [1,3] +POWER_PLANT_KV = [10, 20] +LV_SUBSTATION_BASE_KV = [0.2, 0.4] +MV_SUBSTATION_BASE_KV = [6, 35] +HV_SUBSTATION_BASE_KV = [66, 500] +SHV_SUBSTATION_BASE_KV = [500, 1000] -TURBINE_BASE_KV = [3000,4000] SOLAR_PANEL_BASE_KV = [0.2, 0.4] # per solar panel +""" +Generator default values (small, large, industrial) +""" +SMALL_GEN_KV = [0.2, 0.6] +LARGE_GEN_KV = [1, 35] +INDUSTRIAL_GEN_KV = [35, 100] +SMALL_GEN_KW = [2,5] +LARGE_GEN_KW = [5,10] +INDUSTRIAL_GEN_KW = [10,20] """ Units: KM @@ -72,6 +67,10 @@ """ Valid parameter lists """ -VALID_LOAD_PARAMS = ["kV", "kW", "kVar", "phases"] -VALID_SOURCE_PARAMS = ["kV", "phases", "frequency"] -VALID_LINE_TRANSFORMER_PARAMS = ["length", "XHL", "Conns"] \ No newline at end of file +IMPEDANCE_PARAMS = ["R0", "R1", "X0", "X1"] + +VALID_LOAD_PARAMS = ["kV", "kW", "kvar", "phases"] +VALID_SOURCE_PARAMS = ["kV", "phases", "frequency"] + IMPEDANCE_PARAMS +VALID_LINE_TRANSFORMER_PARAMS = ["length", "XHL", "Conns"] +VALID_PV_PARAMS = ["kV", "phases"] +VALID_GENERATOR_PARAMS = ["kV", "kW", "phases"] \ No newline at end of file diff --git a/pygridsim/enums.py b/pygridsim/enums.py index 48acd18..b6ed6d3 100644 --- a/pygridsim/enums.py +++ b/pygridsim/enums.py @@ -1,16 +1,24 @@ from enum import Enum -import pygridsim.defaults as defaults class SourceType(Enum): - TURBINE = defaults.TURBINE_BASE_KV - SOLAR_PANEL = defaults.SOLAR_PANEL_BASE_KV + TURBINE = "turbine" + POWER_PLANT = "powerplant" + LV_SUBSTATION = "lvsub" + MV_SUBSTATION = "mvsub" + HV_SUBSTATION = "hvsub" + SHV_SUBSTATION = "shvsub" class LineType(Enum): - LV_LINE = defaults.LV_LINE_LENGTH - MV_LINE = defaults.MV_LINE_LENGTH - HV_LINE = defaults.HV_LINE_LENGTH + LV_LINE = "lv" + MV_LINE = "mv" + HV_LINE = "hv" class LoadType(Enum): - HOUSE = {"kV": defaults.HOUSE_KV, "kW": defaults.HOUSE_KW, "kVar": defaults.HOUSE_KVAR} - COMMERCIAL = {"kV": defaults.COMMERCIAL_KV, "kW": defaults.COMMERCIAL_KW, "kVar": defaults.COMMERCIAL_KVAR} - INDUSTRIAL = {"kV": defaults.INDUSTRIAL_KV, "kW": defaults.INDUSTRIAL_KW, "kVar": defaults.INDUSTRIAL_KVAR} \ No newline at end of file + HOUSE = "house" + COMMERCIAL = "commercial" + INDUSTRIAL = "industrial" + +class GeneratorType(Enum): + SMALL = "small" + LARGE = "large" + INDUSTRIAL = "industrial" \ No newline at end of file diff --git a/pygridsim/lines.py b/pygridsim/lines.py index bbaab91..f7ac3a8 100644 --- a/pygridsim/lines.py +++ b/pygridsim/lines.py @@ -1,24 +1,27 @@ from altdss import altdss -from altdss import Transformer, Connection +from altdss import Transformer +from pygridsim.configs import LINE_CONFIGURATIONS import pygridsim.defaults as defaults -from pygridsim.parameters import get_param, random_param, check_valid_params +from pygridsim.enums import LineType +from pygridsim.parameters import _get_param, _random_param, _check_valid_params, _get_enum_obj from dss.enums import LineUnits -def make_line(src, dst, line_type, count, params = {}, transformer = True): - """ - Add a line between src and dst +def _get_kv(node_name): + if node_name == "source" and node_name in altdss.Vsource: + return altdss.Vsource[node_name].BasekV + elif "load" in node_name and node_name in altdss.Load: + return altdss.Load[node_name].kV + elif "generator" in node_name and node_name in altdss.Generator: + return altdss.Generator[node_name].kV + else: + raise KeyError("Invalid src or dst name") - Args: - src: where line starts (node) - dst: where line end (node) - params (optional): any non-default parameters to use. Params can also include transformer params like XHL, Conns - Returns: - Line object that was created - """ - check_valid_params(params, defaults.VALID_LINE_TRANSFORMER_PARAMS) +def _make_line(src, dst, line_type, count, params = {}, transformer = True): + _check_valid_params(params, defaults.VALID_LINE_TRANSFORMER_PARAMS) + line_type_obj = _get_enum_obj(LineType, line_type) line = altdss.Line.new('line' + str(count)) line.Phases = defaults.PHASES - line.Length = get_param(params, "length", random_param(line_type.value)) + line.Length = _get_param(params, "length", _random_param(LINE_CONFIGURATIONS[line_type_obj]["length"])) line.Bus1 = src line.Bus2 = dst line.Units = LineUnits.km @@ -33,8 +36,10 @@ def make_line(src, dst, line_type, count, params = {}, transformer = True): transformer: Transformer = altdss.Transformer.new('transformer' + str(count)) transformer.Phases = defaults.PHASES transformer.Windings = defaults.NUM_WINDINGS - transformer.XHL = get_param(params, "XHL", defaults.XHL) + transformer.XHL = _get_param(params, "XHL", defaults.XHL) transformer.Buses = [src, dst] - transformer.Conns = get_param(params, "Conns", [defaults.PRIMARY_CONN, defaults.SECONDARY_CONN]) - transformer.kVs = [altdss.Vsource[src].BasekV, altdss.Load[dst].kV] + transformer.Conns = _get_param(params, "Conns", [defaults.PRIMARY_CONN, defaults.SECONDARY_CONN]) + + transformer.kVs = [_get_kv(src), _get_kv(dst)] + transformer.end_edit() diff --git a/pygridsim/parameters.py b/pygridsim/parameters.py index db938ae..0904af3 100644 --- a/pygridsim/parameters.py +++ b/pygridsim/parameters.py @@ -2,94 +2,90 @@ Helper functions to parse the parameters used for loads and sources """ from altdss import altdss -from altdss import AltDSS, Transformer, Vsource, Load, LoadModel, LoadShape +from altdss import Load, PVSystem, Generator +from pygridsim.enums import LoadType, SourceType, GeneratorType +from pygridsim.configs import LOAD_CONFIGURATIONS, SOURCE_CONFIGURATIONS, GENERATOR_CONFIGURATIONS import pygridsim.defaults as defaults import random -def random_param(range): - """ - Given the range of a normal parameter (i.e. normal load for a house), uniformly select value. - In case the value is not a range and just a value, just return that value - - Args: - [lower_bound, upper_bound]; range of typical value - Return: - Randomly selected value in range - TODO: allow for non-uniform distributions - """ +def _get_enum_obj(enum_class, enum_val): + enum_obj = None + enum_val_lower = enum_val.lower().replace(" ", "") + for enum_type in enum_class: + if (enum_type.value == enum_val_lower): + enum_obj = enum_type + if not enum_obj: + raise KeyError("invalid enum input") + + return enum_obj + +def _random_param(range): if type(range) is not list: return range + [max, min] = range return random.random() * (max - min) + min -def get_param(params, name, default): - """ - Get param or use default - """ +def _get_param(params, name, default): if name in params: return params[name] else: return default -def check_valid_params(params, valid_params): +def _check_valid_params(params, valid_params): # Invalid parameter handling - for param in params: - if param not in valid_params: - raise KeyError(f"Parameter {param} is not supported") - -def make_load_node(load_params, load_type, count): - """ - Make a load node with the parmeters given, filling in with defaults for - any undefined but required parameter. Parse through the parameters, potentially throwing errors and warnings if - one of the parameter names is invalid. - - Args: - load_params: any specified parameters to override default ones - load_type: LoadType representing type of load, house, commercial, industrial - count: how many loads have already been made, to not use repeat names - Return: - load object - """ - check_valid_params(load_params, defaults.VALID_LOAD_PARAMS) + for key in params: + if key not in valid_params: + raise KeyError(f"Parameter {key} is not supported") + if not isinstance(params[key], (int, float)): + raise TypeError("Parameter input should be int or float") + if key in ["kV", "BasekV"] and params[key] < 0: + raise ValueError("KV cannot be less than 0") + +def _make_load_node(load_params, load_type, count): + _check_valid_params(load_params, defaults.VALID_LOAD_PARAMS) + load_type_obj = _get_enum_obj(LoadType, load_type) load : Load = altdss.Load.new('load' + str(count)) load.Bus1 = 'load' + str(count) - load.Phases = get_param(load_params, "phases", defaults.PHASES) - load.kV = get_param(load_params, "kV", random_param(load_type.value["kV"])) - load.kW = get_param(load_params, "kW", random_param(load_type.value["kW"])) - load.kvar = get_param(load_params, "kVar", random_param(load_type.value["kVar"])) - load.Daily = 'default' + load.Phases =_get_param(load_params, "phases", defaults.PHASES) + for attr in ["kV", "kW", "kvar"]: + load_type_param = LOAD_CONFIGURATIONS[load_type_obj][attr] + setattr(load, attr, _get_param(load_params, attr, _random_param(load_type_param))) - if (load.kV) < 0: - raise ValueError("Cannot have negative voltage in load") + load.Daily = 'default' return load -def make_source_node(source_params, source_type, count, num_in_batch = 1): - """ - Make a source node with the parmeters given, filling in with defaults for - any undefined but required parameter. Parse through the parameters, potentially throwing errors and warnings if - one of the parameter names is invalid. - - Args: - source_params: any specified parameters to override default ones - count: how many sources have already been made, to not use repeat names - num_in_batch: how many to batch into this same source. note this only causes a scaled kV - TODO: num, once we get the transformer thing working - Return: - source object - - TODO: There is a whole set of other vsource properties to set, like impedance and resistance - https://github.com/dss-extensions/AltDSS-Python/blob/2b6fa7e5961cedaf8482c07d377b20bdab4a1bee/altdss/Vsource.py#L694 - """ - check_valid_params(source_params, defaults.VALID_SOURCE_PARAMS) +def _make_source_node(source_params, source_type): + _check_valid_params(source_params, defaults.VALID_SOURCE_PARAMS) + source_type_obj = _get_enum_obj(SourceType, source_type) source = altdss.Vsource[0] source.Bus1 = 'source' - source.Phases = get_param(source_params, "phases", defaults.PHASES) - source.BasekV = get_param(source_params, "kV", num_in_batch*random_param(source_type.value)) - source.Frequency = get_param(source_params, "frequency", defaults.FREQUENCY) + source.Phases = _get_param(source_params, "phases", defaults.PHASES) + source_type_param = SOURCE_CONFIGURATIONS[source_type_obj]["kV"] + source.BasekV = _get_param(source_params, "kV", _random_param(source_type_param)) + source.Frequency = _get_param(source_params, "frequency", defaults.FREQUENCY) + + for imp in defaults.IMPEDANCE_PARAMS: + setattr(source, imp, _get_param(source_params, imp, defaults.IMPEDANCE)) + + return source + +def _make_pv(load_node, params, num_panels, count): + _check_valid_params(params, defaults.VALID_PV_PARAMS) + pv : PVSystem = altdss.PVSystem.new('pv' + str(count)) + pv.Bus1 = load_node + pv.Phases = _get_param(params, "phases", defaults.PHASES) + pv.kV = _get_param(params, "kV", _random_param(defaults.SOLAR_PANEL_BASE_KV) * num_panels) - if (source.BasekV) < 0: - raise ValueError("Cannot have negative voltage in source") +def _make_generator(params, gen_type, count): + _check_valid_params(params, defaults.VALID_GENERATOR_PARAMS) + gen_type_obj = _get_enum_obj(GeneratorType, gen_type) - return source \ No newline at end of file + generator : Generator = altdss.Generator.new('generator' + str(count)) + generator.Bus1 = 'generator' + str(count) + generator.Phases = _get_param(params, "phases", defaults.PHASES) + for attr in ["kV", "kW"]: + gen_type_param = GENERATOR_CONFIGURATIONS[gen_type_obj][attr] + setattr(generator, attr, _get_param(params, attr, _random_param(gen_type_param))) \ No newline at end of file diff --git a/pygridsim/results.py b/pygridsim/results.py index 7c63cd6..e9cb3da 100644 --- a/pygridsim/results.py +++ b/pygridsim/results.py @@ -3,17 +3,9 @@ provides helpers for the solve/results function. """ from altdss import altdss +import json -def query_solution(query): - """ - Given a query, return the query result or indicate it is invalid - - Args: - queries: a list of queriies for the solve function - TODO: only BusVMag, Losses, TotalPower is supported, need to make accessible which queries are supported - Return: - Query result or the string "Invalid" if the query is not supported - """ +def _query_solution(query): match query: case "Voltages": bus_vmags = {} @@ -21,7 +13,6 @@ def query_solution(query): bus_vmags[bus_name] = float(bus_vmag) return bus_vmags case "Losses": - # Parse it to output active power loss, reactive power loss, instead of just complex number. vector_losses = altdss.Losses() losses = {} losses["Active Power Loss"] = vector_losses.real @@ -32,3 +23,6 @@ def query_solution(query): case _: return "Invalid" +def _export_results(results, path): + with open(path, "w") as json_file: + json.dump(results, json_file, indent=4) \ No newline at end of file diff --git a/pygridsim/transformers.py b/pygridsim/transformers.py deleted file mode 100644 index 2adc45e..0000000 --- a/pygridsim/transformers.py +++ /dev/null @@ -1,28 +0,0 @@ -from altdss import altdss -from altdss import Transformer, Connection -import pygridsim.defaults as defaults -from pygridsim.parameters import get_param, random_param - -def make_transformer(src, dst, count, params): - """ - Add a Transformer between src and dst - - Args: - src: where line starts (source node) - dst: where line end (load node) - count: number of transformers so far - params (optional): any non-default parameters to use. - Returns: - Transformer object that was created - - TODO: - - used some of this logic in the line code, if we keep it there then delete this file - """ - transformer: Transformer = altdss.Transformer.new('transformer' + str(count)) - transformer.Phases = defaults.PHASES - transformer.Windings = defaults.NUM_WINDINGS - transformer.XHL = get_param(params, "XHL", defaults.XHL) - transformer.Buses = [src, dst] - transformer.Conns = get_param(params, "Conns", [defaults.PRIMARY_CONN, defaults.SECONDARY_CONN]) - transformer.kVs = [altdss.Vsource[src].BasekV, altdss.Load[dst].kV] - transformer.end_edit() \ No newline at end of file diff --git a/tests/temp.py b/tests/temp.py deleted file mode 100644 index 7db9df2..0000000 --- a/tests/temp.py +++ /dev/null @@ -1,41 +0,0 @@ -# -*- coding: utf-8 -*- -from altdss import altdss -from altdss import AltDSS, Transformer, Vsource, Load, LoadModel, LoadShape -from dss.enums import LineUnits, SolveModes -""" -this is from colab stuff, delete later -""" - -altdss('new circuit.IEEE13Nodeckt') - -# create voltage source -source1 = altdss.Vsource[0] -source1.Bus1 = 'SourceBus' -source1.BasekV = 0.6 -source1.Phases = 3 -source1.Frequency = 60 - -gen = altdss.Generator -gen.Bus1 = 'genSource' -gen.kV = 10 -gen.Phases = 3 - -# create load -load1 : Load = altdss.Load.new('load1') -load1.Bus1 = 'LoadBus' -load1.Phases = 3 -load1.kV = 200 -load1.kW = 1.2 -load1.kvar = 0.6 - -# line between voltage source and load -line1 = altdss.Line.new('line1') -line1.Phases = 3 -line1.Bus1 = 'SourceBus' -line1.Bus2 = 'LoadBus' -line1.Length = 0.1 -line1.Units = LineUnits.km - -# "solve" the circuit -altdss.Solution.Solve() -print(altdss.BusVMag()) \ No newline at end of file diff --git a/tests/test_circuit.py b/tests/test_circuit.py index b7d88eb..fa27276 100644 --- a/tests/test_circuit.py +++ b/tests/test_circuit.py @@ -1,16 +1,10 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- from pygridsim.core import PyGridSim -from pygridsim.enums import LineType, SourceType, LoadType -from altdss import altdss -from altdss import Connection - - -"""Tests for `pygridsim` package.""" - +from pygridsim.enums import LineType, LoadType, SourceType, GeneratorType import unittest -# from pygridsim import pygridsim +"""Tests for `pygridsim` package.""" class TestDefaultRangeCircuit(unittest.TestCase): @@ -30,7 +24,7 @@ def tearDown(self): def test_000_basic(self): circuit = PyGridSim() - circuit.add_source_nodes() + circuit.update_source() circuit.add_load_nodes() circuit.add_lines([("source", "load0")]) circuit.solve() @@ -39,12 +33,9 @@ def test_000_basic(self): def test_001_one_source_one_load(self): circuit = PyGridSim() - circuit.add_source_nodes(source_type=SourceType.TURBINE) - circuit.add_load_nodes(num=1, load_type=LoadType.HOUSE) - circuit.add_lines([("source", "load0")], LineType.MV_LINE) - #circuit.add_transformers([("source", "load0")], params={"Conns": [Connection.wye, Connection.delta]}) - print("Load Nodes:", circuit.view_load_nodes()) - print("Source Nodes:", circuit.view_source_node()) + circuit.update_source(source_type="turbine") + circuit.add_load_nodes(num=1, load_type="house") + circuit.add_lines([("source", "load0")], "MV") circuit.solve() print(circuit.results(["Voltages", "Losses"])) circuit.clear() @@ -52,9 +43,9 @@ def test_001_one_source_one_load(self): def test_002_one_source_one_load_no_transformer(self): # doesn't throw error, but should have stranger output VMag circuit = PyGridSim() - circuit.add_source_nodes(source_type=SourceType.TURBINE) - circuit.add_load_nodes(num=1, load_type=LoadType.HOUSE) - circuit.add_lines([("source", "load0")], LineType.MV_LINE, transformer=False) + circuit.update_source(source_type="turbine") + circuit.add_load_nodes(num=1, load_type="house") + circuit.add_lines([("source", "load0")],"MV", transformer=False) circuit.solve() print(circuit.results(["Voltages", "Losses"])) circuit.clear() @@ -64,39 +55,113 @@ def test_003_one_source_one_load_exhaustive(self): for source_type in SourceType: for load_type in LoadType: circuit = PyGridSim() - circuit.add_source_nodes(source_type=source_type) - circuit.add_load_nodes(num=1, load_type=load_type) - circuit.add_lines([("source", "load0")], line_type) + circuit.update_source(source_type=source_type.value) + circuit.add_load_nodes(num=1, load_type=load_type.value) + circuit.add_lines([("source", "load0")], line_type.value) circuit.solve() - print("LineType:", line_type, "SourceType", source_type, "LoadType", load_type) - print(circuit.results(["Voltages", "Losses"])) circuit.clear() def test_004_one_source_multi_load(self): circuit = PyGridSim() - circuit.add_source_nodes(num_in_batch=10, source_type=SourceType.SOLAR_PANEL) - circuit.add_load_nodes(num=4, load_type=LoadType.HOUSE) - circuit.add_lines([("source", "load0"), ("source", "load3")], LineType.HV_LINE) + circuit.update_source(source_type="turbine") + circuit.add_load_nodes(num=4, load_type="house") + circuit.add_lines([("source", "load0"), ("source", "load3")], "HV") circuit.solve() print(circuit.results(["Voltages"])) circuit.clear() def test_005_bad_query(self): circuit = PyGridSim() - circuit.add_source_nodes() + circuit.update_source() circuit.add_load_nodes() circuit.add_lines([("source", "load0")]) circuit.solve() print(circuit.results(["BadQuery"])) - def test_006_multi_source_bad(self): + def test_006_update_multiple_source(self): + circuit = PyGridSim() + circuit.update_source(source_type="turbine") + circuit.add_load_nodes(num=1, load_type="house") + circuit.update_source(source_type="turbine") + circuit.add_lines([("source", "load0")], "HV") + circuit.solve() + print(circuit.results(["Voltages"])) + + def test_007_export(self): + circuit = PyGridSim() + circuit.update_source() + circuit.add_load_nodes() + circuit.add_lines([("source", "load0")]) + circuit.solve() + print(circuit.results(["Voltages", "Losses"], export_path="sim.json")) + + def test_008_PVsystem(self): + circuit = PyGridSim() + circuit.update_source() + circuit.add_load_nodes(num=2) + circuit.add_PVSystem(load_nodes=["load0", "load1"], num_panels=5) + circuit.add_lines([("source", "load0")]) + circuit.solve() + print(circuit.results(["Voltages", "Losses"])) + + def test_009_generator(self): + circuit = PyGridSim() + circuit.update_source() + circuit.add_load_nodes() + circuit.add_generator(num=3, gen_type="small") + circuit.add_lines([("source", "load0"), ("generator0", "load0")]) + circuit.solve() + print(circuit.results(["Voltages", "Losses"])) + + def test_010_many_sources(self): + circuit = PyGridSim() + circuit.update_source(source_type="powerplant") + circuit.add_load_nodes(num=3) + circuit.add_PVSystem(load_nodes=["load1", "load2"], num_panels=10) + circuit.add_generator(num=3, gen_type="small") + circuit.update_source(source_type="turbine") # change to a turbine source midway + circuit.add_generator(num=4, gen_type="large") + circuit.add_lines([("source", "load0"), ("generator0", "load0"), ("generator5", "source")]) + circuit.solve() + print(circuit.results(["Voltages", "Losses"])) + + def test_011_configs(self): circuit = PyGridSim() - #circuit.add_source_nodes(num_in_batch=10, num=2, source_type=SourceType.SOLAR_PANEL) - #circuit.add_load_nodes(num=1, load_type=LoadType.HOUSE) - #circuit.add_lines([("source", "load0")], LineType.HV_LINE) - #circuit.solve() - # TODO: can add assert to make sure it's in reasonable range? + + # LOAD CONFIG + # should work, because not case sensitive + circuit.add_load_nodes(num=2, load_type="HOUSE") + circuit.add_load_nodes(num=2, load_type="hoUSE") + # should fail, invalid load_type value + with self.assertRaises(KeyError): + circuit.add_load_nodes(num=2, load_type="badloadtype") + # don't want loadtype input, just string + with self.assertRaises(Exception): + circuit.add_load_nodes(num=2, load_type=LoadType.HOUSE) + + # LINE CONFIG + # works, because not case sensitive + circuit.add_lines([("source", "load0")], line_type="HV") + # don't want linetype input, just string + with self.assertRaises(Exception): + circuit.add_lines([("source", "load0")], line_type=LineType.HV_LINE) + + # GENERATOR CONFIG + # works, because not case sensitive + circuit.add_generator(num=3, gen_type="SMALl") + # don't want linetype input, just string + with self.assertRaises(Exception): + circuit.add_generator(num=3, gen_type=GeneratorType.SMALL) + + # SOURCE CONFIG + # works, because not case sensitive + circuit.update_source(source_type="turBINE") + # source type as first param, ignores spaces, this should also work + circuit.update_source("power plant") + # don't want linetype input, just string + with self.assertRaises(Exception): + circuit.update_source(source_type=SourceType.TURBINE) class TestCustomizedCircuit(unittest.TestCase): @@ -115,8 +180,8 @@ def tearDown(self): def test_100_one_source_one_load(self): circuit = PyGridSim() - circuit.add_source_nodes(params={"kV": 100}) - circuit.add_load_nodes(num=1, params={"kV": 10, "kW": 20, "kVar":1}) + circuit.update_source(params={"kV": 100, "R0": 0.1, "R1": 0.2, "X0": 0.3, "X1": 0.4}) + circuit.add_load_nodes(num=1, params={"kV": 10, "kW": 20, "kvar":1}) circuit.add_lines([("source", "load0")], params={"length": 20}) circuit.solve() print(circuit.results(["Voltages", "Losses"])) @@ -127,8 +192,8 @@ def test_100_one_source_multi_load(self): Creates 10 loads, some of which are connected to source. all loads and lines here have the same params """ circuit = PyGridSim() - circuit.add_source_nodes(params={"kV": 100}) - circuit.add_load_nodes(num=10, params={"kV": 10, "kW": 20, "kVar":1}) + circuit.update_source(params={"kV": 100}) + circuit.add_load_nodes(num=10, params={"kV": 10, "kW": 20, "kvar":1}) circuit.add_lines([("source", "load0"), ("source", "load4"), ("source", "load6")], params={"length": 20}) circuit.solve() print(circuit.results(["Voltages", "Losses"])) @@ -140,7 +205,15 @@ def test_101_bad_parameter(self): """ circuit = PyGridSim() with self.assertRaises(KeyError): - circuit.add_source_nodes(params={"kV": 50, "badParam": 100}) + circuit.update_source(params={"kV": 50, "badParam": 100}) + with self.assertRaises(KeyError): + circuit.add_load_nodes(num=4, params={"badParam": 100}) + # add load nodes so we can test pv system erroring + circuit.add_load_nodes(num=2, params={"kV": 10, "kW": 20, "kvar":1}) + with self.assertRaises(KeyError): + circuit.add_generator(num=4, params={"badParam": 100}) + with self.assertRaises(KeyError): + circuit.add_PVSystem(load_nodes=["load0"], params={"badParam": 100}, num_panels=4) def test_102_negative_inputs(self): """ @@ -153,7 +226,7 @@ def test_102_negative_inputs(self): circuit.add_load_nodes(params={"kV": -1}) with self.assertRaises(ValueError): - circuit.add_source_nodes(params={"kV": -1}) + circuit.update_source(params={"kV": -1}) # properly add load and source, then create invalid line with self.assertRaises(ValueError): @@ -162,10 +235,15 @@ def test_102_negative_inputs(self): def test_103_invalid_nodes_in_line(self): circuit = PyGridSim() circuit.add_load_nodes() - circuit.add_source_nodes() - with self.assertRaises(ValueError): + circuit.update_source() + with self.assertRaises(KeyError): # only has source, load0 for now but tries to add another one circuit.add_lines([("source", "load5")]) + + def test_104_non_int_parameters(self): + circuit = PyGridSim() + with self.assertRaises(TypeError): + circuit.add_load_nodes(params={"kV": "stringInput"}) class TestLargeCircuit(unittest.TestCase):